/** * THE MEET DOMAIN MODEL. * * D3: an audio call, a video call, and a meeting are ONE thing — a room. An * audio call is a room with video off; a meeting is a room plus scheduling, * lobby and host metadata. That ruling is expressed here as ONE `MeetRoom` * identity carried by both `/calls` and `/meetings`, not as three parallel * hierarchies that drift. * * Identity-bearing values are BRANDED (C22). A `MeetingId` and a `RoomName` are * both strings and are NOT interchangeable — passing the meeting's row id where * LiveKit wants the room name produces a room nobody else joins, which looks * exactly like "the other person's camera is broken". */ declare const brand: unique symbol; type Brand = T & { readonly [brand]: B; }; type MeetingId = Brand; type CallId = Brand; /** The LiveKit room name. THE MEDIA ROOM KEY — durable for a meeting link. */ type RoomName = Brand; type UserId = Brand; type OrganizationId = Brand; /** LiveKit participant identity. `user:` or `guest:`. */ type ParticipantIdentity = Brand; declare const asMeetingId: (value: string) => MeetingId; declare const asCallId: (value: string) => CallId; declare const asRoomName: (value: string) => RoomName; declare const asUserId: (value: string) => UserId; declare const asOrganizationId: (value: string) => OrganizationId; declare const asParticipantIdentity: (value: string) => ParticipantIdentity; type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue; }; type JsonObject = { [key: string]: JsonValue; }; /** Audio-only or with video. A call escalates; it never becomes a new call. */ type CallMode = "audio" | "video"; /** * WHO IS IN THE ROOM, as far as authorization is concerned. * * A guest has no `userId` — by construction, not by convention. Everything that * requires authentication (host powers, recording, starting a call) takes a * `userId` and therefore cannot compile against a guest identity (D6). */ interface MeetIdentity { readonly userId: UserId | null; readonly organizationId: OrganizationId; readonly displayName: string; readonly avatarUrl: string | null; /** Stable per-device value so a reconnect keeps ONE LiveKit identity. */ readonly deviceId: string; } declare function isGuest(identity: MeetIdentity): boolean; /** * WHO SOMEBODY IS IN A ROOM — the SERVER's word, never this client's (MRI-A11). * * THE ROLE VOCABULARY (CORE-DESIGN §2.1, S3): `host · cohost · presenter · participant · * viewer`. `host` is `meet_meetings.host_user_id`; `cohost` is a real `iam` grant on the * meeting. Being a guest is a STANDING (`ParticipantStanding`), never a role: a guest is a * `participant` whose standing is `guest`. Pinned against aidream's `config.py` by * `test_meet_service.py`. */ type ParticipantRole = "host" | "cohost" | "presenter" | "participant" | "viewer"; /** * Where a person stands with the meeting, beside the role: `member` of its organization, * `invited` (shared with them), `outsider` (signed in, followed the link), `guest` (no * session). The server mints it (attribute `matrx.participant.standing`). */ type ParticipantStanding = "member" | "invited" | "outsider" | "guest"; /** Rank, so "can this actor do that to that participant" is one comparison. */ declare const ROLE_RANK: Readonly>; /** Does this standing follow the link-holder (guest) rules? `outsider` and `guest`. */ declare function isLinkFollower(standing: ParticipantStanding | null | undefined): boolean; /** * 🚨 THE ONE PREDICATE FOR "DOES THIS PERSON HOLD HOST POWERS". * * Every host control in this package answers this question and no other. It is * a pure function of a SERVER-minted role, so there is no path by which a * client's own state can grant it anything. */ declare function isHostRole(role: ParticipantRole): boolean; /** * THE SILENT OBSERVER (HR-360 wave 2, MD-16) — the watch-only role `viewer` the core design * already froze: publishes nothing, has no tile on anyone's stage, and is listed in People * as "Observing". The server decides it at the join gate; nothing here grants it. */ declare function isObserverRole(role: ParticipantRole | null | undefined): boolean; /** Who sees the "Observing" line in People — knob `meet.observers_visible_to`. */ type ObserversVisibleTo = "everyone" | "hosts"; /** * THE LOBBY LADDER (D6). A guest link-follower starts `knocking`; the host * admits or denies. An authenticated member of the meeting's org is `admitted` * on arrival unless the meeting is locked. */ type AdmissionState = "waiting_host" | "knocking" | "admitted" | "denied" | "expired" | "withdrawn" | "removed" | "ended"; interface LobbyEntry { readonly identity: ParticipantIdentity; readonly displayName: string; readonly avatarUrl: string | null; readonly userId: UserId | null; readonly state: AdmissionState; readonly knockedAt: string; /** When an unanswered knock lapses (`knock_timeout_minutes`); null = never. */ readonly knockExpiresAt?: string | null | undefined; readonly decidedAt: string | null; readonly decidedBy: UserId | null; /** * The server's flag on this knock (`mod-removed-bypass`): `previously_removed` when its coarse * signals match someone a host removed from this meeting. Advice for the host, never a block. */ readonly flag?: string | null | undefined; } type ConnectionQuality = "excellent" | "good" | "poor" | "lost" | "unknown"; /** One person in the room, as the UI needs them. Media state is a FACT here. */ interface MeetParticipant { readonly identity: ParticipantIdentity; readonly userId: UserId | null; readonly displayName: string; readonly avatarUrl: string | null; readonly role: ParticipantRole; /** Where they stand with the meeting (server-minted); null when the server sent none. */ readonly standing?: ParticipantStanding | null; /** * Whether the SERVER granted this participant the power to start a recording * (MRI-A11). It is not `role === "host"`: a meeting whose recording policy is * `always-on` is the server's to start and a host may not touch it, and a * `disabled` policy grants it to nobody. The client renders this; it never * recomputes the policy. */ readonly canRecord: boolean; readonly isLocal: boolean; readonly isAgent: boolean; readonly micEnabled: boolean; readonly cameraEnabled: boolean; readonly screenSharing: boolean; readonly speaking: boolean; readonly handRaised: boolean; /** * When the hand went up (epoch ms), or null. The HAND QUEUE is ordered by it — * first raised, first called on (Meet wave 2). Optional so a roster built by a * host before 0.8 still type-checks. */ readonly handRaisedAt?: number | null | undefined; readonly quality: ConnectionQuality; /** * S6b (`net-others-see-drop`): this person dropped without leaving and is held as a * `reconnecting` tile for `reconnect_grace_seconds`. Absent/false = a live participant. */ readonly reconnecting?: boolean | undefined; /** * P1 `net-video-auto-off`: this person's camera is paused by their network (to keep their * audio), not by their choice — from their own heartbeat. Absent/false = not paused. */ readonly videoPaused?: boolean | undefined; readonly joinedAt: string; /** * LiveKit participant attributes, verbatim. The note-taker announces itself * here (`matrx.agent.consent_notice`), which is how a consent notice can be * rendered for a participant whose token metadata this client cannot read. */ readonly attributes: Readonly>; } type MeetingKind = "instant" | "scheduled" | "recurring"; /** * WHAT THE LINK'S CURRENT RUN IS, read through the no-pass link door (`meet_link_state`) so the * entry screen can say it before any Join (CORE-DESIGN §2.3). One vocabulary: `reason` is a * join-gate reason code (`ended` · `cancelled`) or null; no private data rides it. */ interface LinkRunState { readonly state: "open" | "not_started" | "ended" | "cancelled"; readonly reason: "ended" | "cancelled" | null; /** `ended`: the next run's start (null = the meeting can never run again). `not_started`: when it starts. */ readonly nextStart: string | null; } interface MeetingRecord { readonly id: MeetingId; readonly organizationId: OrganizationId; /** Durable: the same room name before, during, and after (R3). */ readonly roomName: RoomName; /** The public slug in a meeting link. Durable for recurring meetings. */ readonly slug: string; readonly title: string; readonly kind: MeetingKind; readonly hostUserId: UserId; readonly scheduledFor: string | null; readonly scheduledDurationMinutes: number | null; readonly startedAt: string | null; readonly endedAt: string | null; /** Present only on a row resolved through the link door; absent = unknown (never guessed). */ readonly linkRun?: LinkRunState | undefined; readonly locked: boolean; readonly lobbyEnabled: boolean; readonly recordingPolicy: RecordingPolicy; readonly aiEnabled: boolean; readonly metadata: JsonObject; /** IANA zone the meeting's wall-clock time is authored in (`America/Los_Angeles`). */ readonly timeZone?: string; /** Description / agenda text. */ readonly agenda?: string | null; /** RFC 5545 RRULE subset — one row and one durable link per series. */ readonly recurrenceRule?: string | null; /** Signed-in members and invitees may enter before a host arrives. */ readonly joinBeforeHost?: boolean; /** Cancel is a STATE: the link still resolves and says so; nobody can join. */ readonly cancelledAt?: string | null; readonly cancellationReason?: string | null; /** Archived (soft deleted). Restorable by the host or a co-host. */ readonly deletedAt?: string | null; /** iCalendar SEQUENCE — bumped by the database on every reschedule and on cancel. */ readonly calendarSequence?: number; /** Row version, for the compare-and-swap in `updateMeeting`. */ readonly version?: number; } /** An invitee's answer. `needs_action` = not answered yet (iCalendar NEEDS-ACTION). */ type RsvpState = "needs_action" | "accepted" | "declined" | "tentative"; /** The answers a person can give. */ type RsvpAnswer = Exclude; /** A co-host holds host moderation through a real `iam` admin grant on the meeting. */ type InviteeRole = "invitee" | "cohost"; /** One person a meeting was sent to (`communication.meet_invitees`). */ interface MeetingInvitee { readonly id: string; readonly meetingId: MeetingId; /** Set when the person has an account — they skip the waiting room and answer in-app. */ readonly userId: UserId | null; readonly email: string | null; readonly displayName: string | null; readonly role: InviteeRole; readonly rsvpState: RsvpState; readonly respondedAt: string | null; readonly rsvpNote: string | null; /** The calendar_sequence of the last invitation they were sent; null = never told. */ readonly lastSentSequence: number | null; readonly lastSentAt: string | null; } /** Who to invite: an account (`userId`) or an address (`email`), or both. */ interface InviteeInput { readonly userId?: UserId | undefined; readonly email?: string | undefined; readonly displayName?: string | undefined; /** Omit to keep an existing invitee's role; `cohost` needs the host or a co-host. */ readonly role?: InviteeRole | undefined; } /** Where one occurrence of a meeting stands. */ type OccurrenceState = "scheduled" | "moved" | "cancelled"; /** One occurrence — for a one-off meeting, the meeting itself. */ interface MeetingOccurrence { readonly meetingId: MeetingId; /** The start the rule generates (iCalendar RECURRENCE-ID). The key for exceptions. */ readonly originalStart: string; /** When it actually happens (differs from `originalStart` only when `moved`). */ readonly occurrenceStart: string; readonly durationMinutes: number | null; readonly state: OccurrenceState; readonly sequence: number; } /** One row of the Upcoming list — an occurrence plus what the list shows about its meeting. */ interface UpcomingOccurrence extends MeetingOccurrence { readonly organizationId: OrganizationId; readonly title: string; readonly slug: string; readonly kind: MeetingKind; readonly hostUserId: UserId; readonly timeZone: string; readonly meetingCancelled: boolean; /** The signed-in reader's own answer, when they are an invitee. */ readonly myRsvp: RsvpState | null; /** `host`, `cohost` or `invitee` — null when the reader simply can see it (org member). */ readonly myRole: "host" | InviteeRole | null; } /** An exception row (`communication.meet_occurrence_overrides`). */ interface OccurrenceOverride { readonly id: string; readonly meetingId: MeetingId; readonly originalStart: string; readonly state: "cancelled" | "moved"; readonly newStart: string | null; readonly newDurationMinutes: number | null; readonly reason: string | null; readonly calendarSequence: number; /** Set when the exception was withdrawn (`restore`) and the occurrence is back on schedule. */ readonly deletedAt: string | null; } /** Settings a meeting carries; unset ones come from the person's / organization's knobs. */ interface MeetingSettings { readonly lobbyEnabled?: boolean | undefined; readonly recordingPolicy?: RecordingPolicy | undefined; readonly aiEnabled?: boolean | undefined; readonly joinBeforeHost?: boolean | undefined; } /** A partial edit. `null` clears a clearable field (agenda, recurrence rule, time). */ interface MeetingChanges extends MeetingSettings { readonly title?: string | undefined; readonly agenda?: string | null | undefined; readonly timeZone?: string | undefined; readonly scheduledFor?: string | null | undefined; readonly scheduledDurationMinutes?: number | null | undefined; readonly recurrenceRule?: string | null | undefined; /** * When the SERIES moves (start, rule or zone), carry its per-date changes * (canceled or moved occurrences) to the new schedule instead of archiving * them — Google Calendar's and Outlook's "keep changes to individual events". * Absent or false archives them (restorable). Ask the person; never decide. */ readonly keepExceptions?: boolean | undefined; } /** What the emailed RSVP link shows and answers (no account needed). */ type RsvpInvitation = { readonly ok: true; readonly meeting: { readonly title: string; readonly slug: string; readonly scheduledFor: string | null; readonly durationMinutes: number | null; readonly timeZone: string; readonly agenda: string | null; readonly recurrenceRule: string | null; readonly cancelledAt: string | null; readonly cancellationReason: string | null; readonly endedAt: string | null; }; readonly invitee: { readonly displayName: string | null; readonly email: string | null; readonly rsvpState: RsvpState; readonly respondedAt: string | null; }; } | { readonly ok: false; readonly message: string; }; /** Org-configurable, never hardcoded taste (Law 6 / "opinions become knobs"). */ type RecordingPolicy = "disabled" | "host-controlled" | "always-on"; type RecordingState = "idle" | "starting" | "recording" | "stopping" | "processing" | "available" /** Bytes were purged under retention; notes and transcript remain durable. */ | "expired" | "failed"; /** * WHICH transport last said what the recording is doing (MRI-A16). * * `local` — this browser's own optimistic `starting`/`stopping`, which is all * the indicator ever had through 0.4.7, and the reason it never went solid. * `room` — the room's replicated state (metadata), the ONLY feed a guest has. * `record` — the durable `meet_recordings` row. It outranks both (D16). */ type RecordingSource = "local" | "room" | "record"; /** The server's durable retention receipt; never inferred from a browser clock. */ interface RecordingRetention { readonly operationId: string; readonly policyId: string; readonly policyUpdatedAt: string; readonly phase: "warning" | "expired"; readonly dueAt: string; readonly expiredAt: string | null; } interface RecordingRecord { readonly id: string; readonly meetingId: MeetingId; readonly state: RecordingState; readonly startedAt: string | null; readonly endedAt: string | null; readonly startedBy: UserId | null; /** The PLATFORM file id. Never a provider URL (R3). */ readonly fileId: string | null; readonly failureReason: string | null; /** Present when central retention warned or purged this recording. */ readonly retention: RecordingRetention | null; } /** * WHERE A TRANSCRIPT LINE CAME FROM — and therefore how much it is worth. * * D16: `lk.transcription` is published by a room participant and its * `sender_identity` is client-supplied and unvalidated by LiveKit, so a caption * is a HINT — good enough to read live, never good enough to reason from. A * `record` line is a durable `meet_transcript_segments` row written server-side * by the note-taker: that is the transcript. A record always beats a caption; * a caption never overwrites a record. */ type TranscriptSource = "caption" | "record"; interface TranscriptSegment { readonly id: string; readonly meetingId: MeetingId; readonly identity: ParticipantIdentity; readonly speaker: string; readonly text: string; readonly startedAt: string; /** Interim text is replaced in place by its final; it is never appended. */ readonly final: boolean; readonly source: TranscriptSource; /** * The `lk.segment_id` this line was captioned under, when known. A durable * row carries it too (the worker writes it into the row's metadata), which is * what lets a record REPLACE the caption it supersedes rather than doubling it. */ readonly captionId: string | null; } /** * SOMEBODY WHO WAS IN THE MEETING — the durable `meet_participants` row, which * outlives the LiveKit room (MRI-D2). `MeetParticipant` above is a live tile; * THIS is the attendance record a person reads a week later. */ interface MeetingAttendee { readonly id: string; readonly meetingId: MeetingId; readonly identity: ParticipantIdentity; readonly displayName: string; readonly avatarUrl: string | null; readonly userId: UserId | null; readonly role: ParticipantRole; readonly admissionState: AdmissionState; /** When this person knocked, and when an unanswered knock lapses (null = never). */ readonly knockedAt?: string | null | undefined; readonly knockExpiresAt?: string | null | undefined; /** The server's flag on a knock (`metadata.knock_flag`), e.g. `previously_removed`. */ readonly knockFlag?: string | null | undefined; readonly isAgent: boolean; /** FIRST arrival in the room — a rejoin never moves it. */ readonly joinedAt: string | null; /** Null while in the room, else the last time they left. */ readonly leftAt: string | null; /** * Every stay in the room, oldest first (`metadata.sessions`, written by the * server's one attendance rule). Empty for a row recorded before stays were * kept; the report then falls back to `joinedAt`–`leftAt`. */ readonly sessions?: readonly AttendanceStay[] | undefined; } /** One stay in the room: arrived, and left (null = still there or never recorded). */ interface AttendanceStay { readonly joinedAt: string; readonly leftAt: string | null; } type MeetingNoteKind = "note" | "decision" | "action-item"; /** * ONE ROW OF WHAT THE MEETING DECIDED. `meet_notes.kind` is a live CHECK * constraint of exactly three values (D5) — there is no `summary` kind and no * `transcript` kind — so the wrap-up summary is a `note` carrying * `metadata.role = "summary"`, and a live-window note carries `metadata.window`. * Both are projected here so the UI never re-reads raw metadata. */ interface MeetingNote { readonly id: string; readonly meetingId: MeetingId; readonly text: string; readonly createdAt: string; readonly kind: MeetingNoteKind; /** Set ONLY when the assignee resolved to a real user (D18). */ readonly assigneeUserId: UserId | null; /** Who the item is for, as a person reads it. A guest keeps their name here. */ readonly assigneeDisplayName: string | null; /** `"summary"` for the wrap-up; `null` for everything else. */ readonly role: "summary" | null; /** The live-notes window index this came out of, when it came from one. */ readonly window: number | null; /** * 🚨 THE WRAP-UP IS THE AUTHORITATIVE POST-MEETING SET (D20). * * A live-notes window and the wrap-up both write decisions and action items, * and on production 2026-09-09 meeting `9cb-39f8-782` carried the SAME * decision twice — once from the note-taker at 04:43:57 and once from the * wrap-up at 04:47:15 — and the record view listed both. A reader cannot tell * a repeated decision from two decisions. * * So when the wrap-up lands, the server MARKS the window-written decisions * and action items it supersedes (`metadata.superseded_by` = the wrap-up's * run id) and never deletes them: the row is still the timeline of what was * known at 04:43. This field carries that marker, and the record view and the * assistant panel render only the rows where it is `null`. */ readonly supersededBy: string | null; /** * WHICH TRANSPORT DELIVERED THIS COPY (MRI-A12). The same durable row reaches * a signed-in member twice — once as a `meet_notes` row over Postgres Changes * (`"record"`) and once as a `matrx.meet.note` packet on the media plane * (`"room"`), which is the ONLY copy a session-less guest can receive, because * `anon` holds no grant on the table (MRI-A10). * * They carry the same id and the same words, so the store renders one note. * Where they could ever disagree, `"record"` wins and `"room"` never overwrites * it — the database is the record and the packet is the courtesy copy. That is * the same precedence captions and segments already follow (D16). */ readonly source: "record" | "room"; } /** * THE WRAP-UP, ANNOUNCED (MRI-A12). The summary, decisions and action items * arrive as ordinary notes; this says the pass that wrote them FINISHED, which * is the difference between "no summary yet" and "no summary is coming". * * 🚨 It reaches a screen only while the room still exists. `POST /v1/meet/end` * deletes the LiveKit room and the wrap-up runs afterwards, so on the ordinary * ending path nobody is in the room to be told — the post-meeting record view is * where the summary is read. A surface must therefore treat `null` as "not * announced", never as "not written". */ interface WrapUpAnnouncement { readonly meetingId: MeetingId; /** The `meet_notes` ids the pass wrote, in the order it wrote them. */ readonly noteIds: readonly string[]; /** How many durable transcript segments it summarized. */ readonly segmentCount: number; readonly at: string; } /** * THE NOTE-TAKER'S STATE, as the meeting row records it * (`meet_meetings.metadata.note_taker.state`, MRI-A2). Five values and no * spinner: a surface renders the state it is in, or the server's refusal. * * - `absent` — nobody summoned it. The control offers to. * - `joining` — dispatched; it has not entered the room yet. * - `transcribing` — the server recorded active transcription and the exact * deterministic actor is present in the room. * - `handoff_pending` — a row that said transcribing no longer has that exact * actor in the room. The next state and any missed audio are not inferred. * - `ended` — the recorded note-taker stopped. The cause is not inferred. */ type NoteTakerState = "absent" | "joining" | "transcribing" | "handoff_pending" | "ended"; /** The lifecycle of a 1:1 or ad-hoc group call (R2). */ type CallState = "idle" | "dialing" | "ringing" | "connecting" | "active" | "declined" | "missed" | "busy" | "cancelled" | "ended" | "failed"; interface CallInvite { readonly id: CallId; readonly organizationId: OrganizationId; readonly roomName: RoomName; readonly mode: CallMode; readonly callerUserId: UserId; readonly callerName: string; readonly callerAvatarUrl: string | null; readonly calleeUserIds: readonly UserId[]; /** The durable conversation the invite was delivered into, if any. */ readonly conversationId: string | null; readonly createdAt: string; readonly expiresAt: string; readonly state: CallState; readonly declineMessage: string | null; } /** A person the UI can call. Deliberately minimal: hosts already have this. */ interface CallablePerson { readonly userId: UserId; readonly displayName: string; readonly avatarUrl: string | null; } type StageLayout = "grid" | "speaker" | "sidebar"; /** * THE ONE WAY THIS PACKAGE TALKS TO AIDREAM (MRI-A16). * * 🚨 **Every request to the aidream server carries `x-organization-id`, and the * only way to guarantee "every" is for there to be ONE place that builds the * request.** aidream's org-admission middleware refuses an authenticated * request that does not name the organization it is acting in — 400 * `organization_required`, before the router is ever reached. * * 0.1.3 learned that lesson and put the header on the three meet callers by * hand. That fixed the instances and left the CLASS wide open, and on * 2026-09-09 the class collected its next victim: `` fetched * `GET /files/{id}/download?inline=1` through a bare `fetch` with only a bearer * token, so a perfectly landed 1.38 MB recording could not be played by the * host who made it (MRI-C8; the identical URL returned `200 video/mp4` the * moment the header was added by hand). A fourth caller was written months * after the lesson and never heard it, because a lesson written into three call * sites is not a lesson, it is three copies of a workaround. * * So: this module owns the base URL, the organization header and the bearer * token, and `api.test.ts` holds a guard that FAILS THE BUILD if any other file * in the package calls `fetch(` against a base URL or points an element `src=` * at one. A new caller cannot be written without the header any more. * * 🚨 **HOW A `