@lssm-tech/integration.provider-meeting-recorder
Website: https://contractspec.io
Meeting recorder provider implementations for ContractSpec.
Authorized bot capture
AttendeeMeetingCaptureProvider supports Google Meet and Microsoft Teams through either a managed or customer-operated Attendee origin. Configure baseUrl before the /api/v1 suffix, a server-only secret callback, the webhook URL, licensing and EU-processing references, and a mandatory current-authority callback. These references are configuration, not proof of commercial or residency approval.
The adapter supports start/read/pause/resume/stop, transcript retrieval, media deletion and deduplication-key reconciliation. It never retries a POST. An ambiguous create requires reconciliation; a null lookup is not proof of absence. Callbacks use verifyAttendeeCaptureWebhook; hosts must persist event IDs and digests and verify the provider/capture mapping before changing domain state.
No native client, provider account or production capture host is enabled by importing this adapter. Keep returned provider identifiers and transcript text out of public logs and analytics.
Usage
Import the root entrypoint for the domain surface, or import a concrete implementation from the ./impls/* subpaths.
Governed meeting intake
Granola and Fireflies are concrete reviewed sources. Fireflies requires a non-empty server credential, HTTPS GraphQL endpoint, bounded page size, and an optional signed webhook secret. Its fetch injection exists for server adapters and deterministic tests; browser code must never construct provider clients.
./meeting-intake-binding composes any CalendarProvider with any
MeetingRecorderProvider. It reads a bounded time window, hydrates recorder summaries, matches only digested calendar/participant evidence, and invokes a host-owned governed recorder-binding operation only for one unambiguous match at or above the automatic confidence threshold. Weak, tied, and missing matches must be durably routed through the required review port; they never bind by default.
The binding caps windows, pages, and candidates, rejects repeated provider cursors, prevents one recorder meeting from binding twice in a cycle, and emits content-free counters. Hosts still own selected-calendar authority, consent, credential resolution, durable occurrence lookup, review persistence, and the
opa.recording.source_bind authority/transaction boundary.
Granola notes and transcripts
GranolaMeetingRecorderProvider reads the public API or MCP with exact note identity: get_meetings is called with meeting_ids, and a payload that does not contain the requested ID yields granola_not_found (no substring or first-result fallback). Requests have a deadline (timeoutMs, default 20s) and response bodies are validated. Failures are GranolaProviderError codes without provider bodies (granola_entitlement_denied,
granola_transcript_unavailable, granola_rate_limited, ...).
Set transcriptRoute: 'paged' for durable ingestion: the adapter traverses the note's transcript pages and holds a repeated cursor, missing page, foreign note or oversized transcript as an explicit error instead of returning a partial transcript. speaker.source is the audio channel (metadata.audioChannel), never a person; invitees come only from the calendar event; Granola attendee metadata is metadata.knownParticipants (attendance unverified); absent times stay absent. The paged route shape and webhook signing scheme are fixture contracts until live Granola qualification passes.
Shared recorder behavior
All four recorder providers (Fathom, Fireflies, Granola, tl;dv) work the same way in three areas:
- Transcripts.
getTranscript() returns normalized segments: startMs/endMs in milliseconds from the start of the transcript, or null when unknown, plus speaker { providerSpeakerId, displayName, email, channel }. The legacy speaker* and start/endTimeMs fields hold the same values.
- Webhooks.
verifyMeetingRecorderWebhook(provider, { request, secret }) returns { ok: true, event } | { ok: false, reason }. Fathom uses Standard Webhooks and Fireflies uses a hex HMAC. Granola and tl;dv are polling-only (webhook_unsupported; see meetingRecorderCapabilities). A verified event only names a record to re-fetch.
- Retries.
withRecorderRetry retries idempotent reads on 429/503, with at most 3 attempts and Retry-After or jittered backoff. A final 429 becomes MeetingRecorderRateLimitedError (recorder_rate_limited).
Meeting evidence ingestion
./meeting-evidence-ingestion stores calendar events, provider notes, provider summaries and transcripts as separate account-scoped evidence records (authority: 'evidence_only') through MeetingEvidenceStorePort. Each record carries tenant, connector account, context, workspace, entitlement and exact external ID. Runs are incremental (cursor plus 24h overlap, 31-day window cap), idempotent (content-digest revisions) and record explicit freshness; use
evaluateMeetingSourceFreshness to report fresh, stale, failing,
disconnected or never_synced. A transcript-disabled plan is recorded as an
unavailable transcript; an earlier complete transcript is held, not replaced.
./meeting-evidence-webhook verifies signed recorder hooks and accepts them through a durable receipt port before reconciling the one exact note. Hooks are hints, never evidence; periodic ingestion covers missed deliveries.
Compatibility
This package owns the meeting-recorder implementation slice split out of @lssm-tech/integration.providers-impls. The legacy package continues to re-export these subpaths for existing consumers.
Bot capture authority receives the exact attempted request: start includes its immutable meeting URL/media/schedule intent; existing-bot actions include providerCaptureId; reconciliation includes the durable idempotencyKey. The host must compare these supplied values with its owner-scoped persisted capture ledger before invoking the effect. Supplied values are evidence to verify, never authority. Attendee snapshots start/reconcile inputs before asynchronous authorization. This contract does not install a permissive default or constitute deployment qualification.
Attendee transcript normalization accepts both the upstream README's plain-string transcription field and structured transcript envelopes, retaining validated timing/speaker fields and rejecting malformed non-text values. Ended calls remain processing until recording and transcription are complete; deleted calls never advertise either readiness flag. These are adapter semantics, not proof of deployed processing completion.
Attendee webhook canonicalization serializes sorted keys directly using Unicode code-point order. It does not rebuild a JavaScript object, which would reorder integer-like keys before signing. A Python-generated fixture covers accented text, numeric keys and supplementary Unicode ordering. Numeric lexemes are preserved from the raw delivered JSON, including Python float/exponent/negative-zero forms; duplicate keys and unsafe decoded integers are rejected. Deployed-version callback compatibility still requires qualification; hosts must persist replay IDs and validate capture mappings.
Attendee HTTP failures map to closed codes: 401 is meeting_capture_unauthorized (revoked or wrong API key; hosts must alert and retry, never treat it as "already gone"), other 4xx are meeting_capture_rejected, 429/5xx are meeting_capture_unavailable, and a failed POST is meeting_capture_ambiguous.
Bot reconciliation provider reads default to a 30-second deadline (maximum 60 seconds), shorter than the job lease. The worker passes an AbortSignal and rejects late results even if a provider ignores it. Shutdown before claim consumes no attempt; cancellation during finalization prevents apply. Attendee get accepts the optional signal, combines it with its transport timeout, and checks cancellation before and after secret resolution. Host read adapters must forward the worker signal.
Attendee transport deadlines now bound secret lookup, network response and body consumption even when injected operations ignore cancellation. Body cancellation is best-effort and never awaited. Late responses are discarded; successful POSTs with unusable bodies remain ambiguous and must be reconciled rather than blindly repeated. The transport cannot guarantee an ignored network abort prevented a bot from starting.