recording-fence.ts2.4 KBView on GitHub
/**
 * Parsing for the ```recording fence — a meeting recording embedded in a document.
 *
 * Body shape: `key=[redacted] header lines and nothing else.
 *
 *   ```recording
 *   provider: fathom
 *   externalId: 177047966
 *   title: Adapt Insurance // Cedar onboarding
 *   at: 12:40
 *   recording: https://fathom.video/share/5mga_KS6XbvzmFJiRxRpzD-YFyGCMAQy
 *   ```
 *
 * `externalId` is the load-bearing key: the server resolves the provider, the media and the
 * page from it, so the embed keeps working after the signed URL that was current when the
 * document was written has expired. `recording:` is a fallback link only, never the source
 * of playback — for Circleback it is a link that died 24 hours after ingest.
 *
 * See apps/server/docs/inline-meeting-video-in-documents.md.
 */

export interface ParsedRecording {
  /** `crm_meeting_events.external_id`. Everything else is decoration. */
  externalId: string;
  /** Canonical provider id, lowercased. Display only — the server is the authority. */
  provider?: string;
  /** Human label for the call. */
  title?: string;
  /** Where to open the recording, as written (`MM:SS` / `HH:MM:SS` / seconds). */
  at?: string;
  /** A provider page to fall back to when nothing can be minted. */
  recording?: string;
}

/**
 * Parse a fence body into a recording reference, or null when it has no `externalId`.
 *
 * A fence with no id cannot resolve to anything, so the node renders the body raw instead —
 * the same degradation the coaching fences use, and for the same reason: a visible broken
 * block is debuggable, a silently empty one is not.
 */
export function parseRecording(body: string): ParsedRecording | null {
  const header: Record<string, string> = {};
  for (const line of body.split('\n')) {
    if (!line.trim()) continue;
    const idx = line.indexOf(':');
    if (idx <= 0) continue;
    header[line.slice(0, idx).trim().toLowerCase()] = line.slice(idx + 1).trim();
  }

  // `externalid` because header keys are lowercased on the way in — the agent writes
  // `externalId`, which is the spelling used everywhere else in the codebase.
  const externalId = header.externalid;
  if (!externalId) return null;

  return {
    externalId,
    ...(header.provider ? { provider: header.provider.toLowerCase() } : {}),
    ...(header.title ? { title: header.title } : {}),
    ...(header.at ? { at: header.at } : {}),
    ...(header.recording ? { recording: header.recording } : {}),
  };
}