@ai-matrx/meet 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog — `@ai-matrx/meet`
2
2
 
3
+ ## 0.9.0
4
+ ### S3: the meeting pass, the role/standing vocabulary, refusal phases
5
+
6
+ - **The meeting pass.** `RoomToken` gains `meetingPass`, `meetingPassExpiresAt`, `sessionId`,
7
+ `standing`. Every aidream meet route (record, claim, chat, collab, moderate, Q&A, captions)
8
+ now reads `x-meet-pass` (`MEETING_PASS_HEADER`, `meetingPassHeaders`); the LiveKit `token` is
9
+ for media only. `ROOM_TOKEN_HEADER` is removed (no shim). `rememberedMeetingPass(room)` reads
10
+ a guest's kept pass.
11
+ - **Role vocabulary** `host · cohost · presenter · participant · viewer`; being a guest is the
12
+ STANDING `guest` (`ParticipantStanding`, `isLinkFollower`, attribute
13
+ `matrx.participant.standing`). `data-meet-role` still renders `guest` for a link follower.
14
+ - **Refusals are phases.** `MeetError.reason` carries the join gate's reason code; a refused
15
+ join renders `refused:<reason>` (`ended` renders the `ended` phase), never pre-join.
16
+ - **Session-aware record:** `loadGuestMeetingRecord({ sessionId })`.
17
+ - **Removed:** `RPCS.endMeeting` / `repository.endMeeting` (the `meet_end_meeting` RPC is
18
+ dropped; `/v1/meet/end` is the one end door).
19
+
20
+ Consumer action: send `meetingPass` (not `token`) as `x-meet-pass`; read the guest badge from
21
+ `standing`, not `role === "guest"`; replace `ROOM_TOKEN_HEADER` with `MEETING_PASS_HEADER`.
22
+
23
+ ## 0.8.1
24
+
25
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
26
+ `git diff npm/meet/v0.8.0..npm/meet/v0.8.1 -- apps/shared/meet`).
27
+ No source changes intended and no consumer action required.
28
+
3
29
  ## 0.8.0 — session identity and global slots have one home
4
30
 
5
31
  - The session reader is `@ai-matrx/data/session-identity` (meet supplies its words and branded
@@ -57,19 +57,23 @@ declare function isGuest(identity: MeetIdentity): boolean;
57
57
  /**
58
58
  * WHO SOMEBODY IS IN A ROOM — the SERVER's word, never this client's (MRI-A11).
59
59
  *
60
- * `host` is `meet_meetings.host_user_id` or a real `iam` grant on the meeting;
61
- * `participant` is an authenticated member of the meeting's organization;
62
- * `guest` is a link-follower with no session. Organization membership is
63
- * TENANCY and never permission, which is why "member of the org" lands on
64
- * `participant` and not on `host`.
65
- *
66
- * `cohost` has no server-side minter yet and is kept because the rank ladder
67
- * below is the thing that stops a co-host from removing the host; the moment a
68
- * co-host grant exists it slots in without a second vocabulary.
60
+ * THE ROLE VOCABULARY (CORE-DESIGN §2.1, S3): `host · cohost · presenter · participant ·
61
+ * viewer`. `host` is `meet_meetings.host_user_id`; `cohost` is a real `iam` grant on the
62
+ * meeting. Being a guest is a STANDING (`ParticipantStanding`), never a role: a guest is a
63
+ * `participant` whose standing is `guest`. Pinned against aidream's `config.py` by
64
+ * `test_meet_service.py`.
65
+ */
66
+ type ParticipantRole = "host" | "cohost" | "presenter" | "participant" | "viewer";
67
+ /**
68
+ * Where a person stands with the meeting, beside the role: `member` of its organization,
69
+ * `invited` (shared with them), `outsider` (signed in, followed the link), `guest` (no
70
+ * session). The server mints it (attribute `matrx.participant.standing`).
69
71
  */
70
- type ParticipantRole = "host" | "cohost" | "participant" | "guest";
72
+ type ParticipantStanding = "member" | "invited" | "outsider" | "guest";
71
73
  /** Rank, so "can this actor do that to that participant" is one comparison. */
72
74
  declare const ROLE_RANK: Readonly<Record<ParticipantRole, number>>;
75
+ /** Does this standing follow the link-holder (guest) rules? `outsider` and `guest`. */
76
+ declare function isLinkFollower(standing: ParticipantStanding | null | undefined): boolean;
73
77
  /**
74
78
  * 🚨 THE ONE PREDICATE FOR "DOES THIS PERSON HOLD HOST POWERS".
75
79
  *
@@ -102,6 +106,8 @@ interface MeetParticipant {
102
106
  readonly displayName: string;
103
107
  readonly avatarUrl: string | null;
104
108
  readonly role: ParticipantRole;
109
+ /** Where they stand with the meeting (server-minted); null when the server sent none. */
110
+ readonly standing?: ParticipantStanding | null;
105
111
  /**
106
112
  * Whether the SERVER granted this participant the power to start a recording
107
113
  * (MRI-A11). It is not `role === "host"`: a meeting whose recording policy is
@@ -800,6 +806,6 @@ interface MeetingActivities {
800
806
  * The same `GET /v1/meet/collab` the room reads on join; a refusal is returned as data,
801
807
  * so the record still renders everything else.
802
808
  */
803
- declare function loadMeetingActivities(api: MeetApi, meetingId: string, roomToken?: string | null): Promise<MeetingActivities>;
809
+ declare function loadMeetingActivities(api: MeetApi, meetingId: string, meetingPass?: string | null): Promise<MeetingActivities>;
804
810
 
805
- export { type NoteTakerState as $, type AccessTokenSource as A, type MeetPollState as B, type CollabPanel as C, type MeetQuestion as D, EMPTY_COLLAB as E, FILE_SESSION_PATH as F, type MeetQuestionState as G, type MeetStroke as H, type InviteeInput as I, type JsonObject as J, type MeetingActivities as K, type LobbyEntry as L, type MeetingRecord as M, type MeetingAttendee as N, type OrganizationId as O, type ParticipantIdentity as P, type MeetingChanges as Q, type RoomName as R, type StageLayout as S, type TranscriptSource as T, type UserId as U, type MeetingInvitee as V, type WrapUpAnnouncement as W, type MeetingKind as X, type MeetingNoteKind as Y, type MeetingOccurrence as Z, type MeetingSettings as _, type MeetParticipant as a, ORGANIZATION_HEADER as a0, type OccurrenceOverride as a1, type OccurrenceState as a2, ROLE_RANK as a3, type RecordingPolicy as a4, type RecordingRecord as a5, type RecordingState as a6, type RoomPhase as a7, type RsvpAnswer as a8, type RsvpInvitation as a9, type RsvpState as aa, type TranscriptSegment as ab, type UpcomingOccurrence as ac, applyCollabPacket as ad, asCallId as ae, asMeetingId as af, asOrganizationId as ag, asParticipantIdentity as ah, asRoomName as ai, asUserId as aj, assignedRoom as ak, autoAssign as al, breakoutRemainingMs as am, collabFromState as an, createMeetApi as ao, defaultCaptionLanguage as ap, fileDownloadPath as aq, isGuest as ar, isHostRole as as, loadMeetingActivities as at, withItem as au, type RecordingSource as av, type MeetApi as b, type MeetIdentity as c, type CallInvite as d, type ParticipantRole as e, type CallMode as f, type MeetingNote as g, type MeetingId as h, type CollabSnapshot as i, type AdmissionState as j, CAPTION_LANGUAGES as k, type CallId as l, type CallState as m, type CallablePerson as n, type ConnectionQuality as o, type InviteeRole as p, type JsonValue as q, type MeetApiOptions as r, type MeetApiRequest as s, type MeetBreakoutBroadcast as t, type MeetBreakoutPlan as u, type MeetBreakoutRoom as v, type MeetCollabSettings as w, type MeetFileMedia as x, type MeetPoll as y, type MeetPollOption as z };
811
+ export { type NoteTakerState as $, type AccessTokenSource as A, type MeetPollState as B, type CollabPanel as C, type MeetQuestion as D, EMPTY_COLLAB as E, FILE_SESSION_PATH as F, type MeetQuestionState as G, type MeetStroke as H, type InviteeInput as I, type JsonObject as J, type MeetingActivities as K, type LobbyEntry as L, type MeetingRecord as M, type MeetingAttendee as N, type OrganizationId as O, type ParticipantIdentity as P, type MeetingChanges as Q, type RoomName as R, type StageLayout as S, type TranscriptSource as T, type UserId as U, type MeetingInvitee as V, type WrapUpAnnouncement as W, type MeetingKind as X, type MeetingNoteKind as Y, type MeetingOccurrence as Z, type MeetingSettings as _, type MeetParticipant as a, ORGANIZATION_HEADER as a0, type OccurrenceOverride as a1, type OccurrenceState as a2, type ParticipantStanding as a3, ROLE_RANK as a4, type RecordingPolicy as a5, type RecordingRecord as a6, type RecordingState as a7, type RoomPhase as a8, type RsvpAnswer as a9, type RsvpInvitation as aa, type RsvpState as ab, type TranscriptSegment as ac, type UpcomingOccurrence as ad, applyCollabPacket as ae, asCallId as af, asMeetingId as ag, asOrganizationId as ah, asParticipantIdentity as ai, asRoomName as aj, asUserId as ak, assignedRoom as al, autoAssign as am, breakoutRemainingMs as an, collabFromState as ao, createMeetApi as ap, defaultCaptionLanguage as aq, fileDownloadPath as ar, isGuest as as, isHostRole as at, isLinkFollower as au, loadMeetingActivities as av, withItem as aw, type RecordingSource as ax, type MeetApi as b, type MeetIdentity as c, type CallInvite as d, type ParticipantRole as e, type CallMode as f, type MeetingNote as g, type MeetingId as h, type CollabSnapshot as i, type AdmissionState as j, CAPTION_LANGUAGES as k, type CallId as l, type CallState as m, type CallablePerson as n, type ConnectionQuality as o, type InviteeRole as p, type JsonValue as q, type MeetApiOptions as r, type MeetApiRequest as s, type MeetBreakoutBroadcast as t, type MeetBreakoutPlan as u, type MeetBreakoutRoom as v, type MeetCollabSettings as w, type MeetFileMedia as x, type MeetPoll as y, type MeetPollOption as z };
@@ -57,19 +57,23 @@ declare function isGuest(identity: MeetIdentity): boolean;
57
57
  /**
58
58
  * WHO SOMEBODY IS IN A ROOM — the SERVER's word, never this client's (MRI-A11).
59
59
  *
60
- * `host` is `meet_meetings.host_user_id` or a real `iam` grant on the meeting;
61
- * `participant` is an authenticated member of the meeting's organization;
62
- * `guest` is a link-follower with no session. Organization membership is
63
- * TENANCY and never permission, which is why "member of the org" lands on
64
- * `participant` and not on `host`.
65
- *
66
- * `cohost` has no server-side minter yet and is kept because the rank ladder
67
- * below is the thing that stops a co-host from removing the host; the moment a
68
- * co-host grant exists it slots in without a second vocabulary.
60
+ * THE ROLE VOCABULARY (CORE-DESIGN §2.1, S3): `host · cohost · presenter · participant ·
61
+ * viewer`. `host` is `meet_meetings.host_user_id`; `cohost` is a real `iam` grant on the
62
+ * meeting. Being a guest is a STANDING (`ParticipantStanding`), never a role: a guest is a
63
+ * `participant` whose standing is `guest`. Pinned against aidream's `config.py` by
64
+ * `test_meet_service.py`.
65
+ */
66
+ type ParticipantRole = "host" | "cohost" | "presenter" | "participant" | "viewer";
67
+ /**
68
+ * Where a person stands with the meeting, beside the role: `member` of its organization,
69
+ * `invited` (shared with them), `outsider` (signed in, followed the link), `guest` (no
70
+ * session). The server mints it (attribute `matrx.participant.standing`).
69
71
  */
70
- type ParticipantRole = "host" | "cohost" | "participant" | "guest";
72
+ type ParticipantStanding = "member" | "invited" | "outsider" | "guest";
71
73
  /** Rank, so "can this actor do that to that participant" is one comparison. */
72
74
  declare const ROLE_RANK: Readonly<Record<ParticipantRole, number>>;
75
+ /** Does this standing follow the link-holder (guest) rules? `outsider` and `guest`. */
76
+ declare function isLinkFollower(standing: ParticipantStanding | null | undefined): boolean;
73
77
  /**
74
78
  * 🚨 THE ONE PREDICATE FOR "DOES THIS PERSON HOLD HOST POWERS".
75
79
  *
@@ -102,6 +106,8 @@ interface MeetParticipant {
102
106
  readonly displayName: string;
103
107
  readonly avatarUrl: string | null;
104
108
  readonly role: ParticipantRole;
109
+ /** Where they stand with the meeting (server-minted); null when the server sent none. */
110
+ readonly standing?: ParticipantStanding | null;
105
111
  /**
106
112
  * Whether the SERVER granted this participant the power to start a recording
107
113
  * (MRI-A11). It is not `role === "host"`: a meeting whose recording policy is
@@ -800,6 +806,6 @@ interface MeetingActivities {
800
806
  * The same `GET /v1/meet/collab` the room reads on join; a refusal is returned as data,
801
807
  * so the record still renders everything else.
802
808
  */
803
- declare function loadMeetingActivities(api: MeetApi, meetingId: string, roomToken?: string | null): Promise<MeetingActivities>;
809
+ declare function loadMeetingActivities(api: MeetApi, meetingId: string, meetingPass?: string | null): Promise<MeetingActivities>;
804
810
 
805
- export { type NoteTakerState as $, type AccessTokenSource as A, type MeetPollState as B, type CollabPanel as C, type MeetQuestion as D, EMPTY_COLLAB as E, FILE_SESSION_PATH as F, type MeetQuestionState as G, type MeetStroke as H, type InviteeInput as I, type JsonObject as J, type MeetingActivities as K, type LobbyEntry as L, type MeetingRecord as M, type MeetingAttendee as N, type OrganizationId as O, type ParticipantIdentity as P, type MeetingChanges as Q, type RoomName as R, type StageLayout as S, type TranscriptSource as T, type UserId as U, type MeetingInvitee as V, type WrapUpAnnouncement as W, type MeetingKind as X, type MeetingNoteKind as Y, type MeetingOccurrence as Z, type MeetingSettings as _, type MeetParticipant as a, ORGANIZATION_HEADER as a0, type OccurrenceOverride as a1, type OccurrenceState as a2, ROLE_RANK as a3, type RecordingPolicy as a4, type RecordingRecord as a5, type RecordingState as a6, type RoomPhase as a7, type RsvpAnswer as a8, type RsvpInvitation as a9, type RsvpState as aa, type TranscriptSegment as ab, type UpcomingOccurrence as ac, applyCollabPacket as ad, asCallId as ae, asMeetingId as af, asOrganizationId as ag, asParticipantIdentity as ah, asRoomName as ai, asUserId as aj, assignedRoom as ak, autoAssign as al, breakoutRemainingMs as am, collabFromState as an, createMeetApi as ao, defaultCaptionLanguage as ap, fileDownloadPath as aq, isGuest as ar, isHostRole as as, loadMeetingActivities as at, withItem as au, type RecordingSource as av, type MeetApi as b, type MeetIdentity as c, type CallInvite as d, type ParticipantRole as e, type CallMode as f, type MeetingNote as g, type MeetingId as h, type CollabSnapshot as i, type AdmissionState as j, CAPTION_LANGUAGES as k, type CallId as l, type CallState as m, type CallablePerson as n, type ConnectionQuality as o, type InviteeRole as p, type JsonValue as q, type MeetApiOptions as r, type MeetApiRequest as s, type MeetBreakoutBroadcast as t, type MeetBreakoutPlan as u, type MeetBreakoutRoom as v, type MeetCollabSettings as w, type MeetFileMedia as x, type MeetPoll as y, type MeetPollOption as z };
811
+ export { type NoteTakerState as $, type AccessTokenSource as A, type MeetPollState as B, type CollabPanel as C, type MeetQuestion as D, EMPTY_COLLAB as E, FILE_SESSION_PATH as F, type MeetQuestionState as G, type MeetStroke as H, type InviteeInput as I, type JsonObject as J, type MeetingActivities as K, type LobbyEntry as L, type MeetingRecord as M, type MeetingAttendee as N, type OrganizationId as O, type ParticipantIdentity as P, type MeetingChanges as Q, type RoomName as R, type StageLayout as S, type TranscriptSource as T, type UserId as U, type MeetingInvitee as V, type WrapUpAnnouncement as W, type MeetingKind as X, type MeetingNoteKind as Y, type MeetingOccurrence as Z, type MeetingSettings as _, type MeetParticipant as a, ORGANIZATION_HEADER as a0, type OccurrenceOverride as a1, type OccurrenceState as a2, type ParticipantStanding as a3, ROLE_RANK as a4, type RecordingPolicy as a5, type RecordingRecord as a6, type RecordingState as a7, type RoomPhase as a8, type RsvpAnswer as a9, type RsvpInvitation as aa, type RsvpState as ab, type TranscriptSegment as ac, type UpcomingOccurrence as ad, applyCollabPacket as ae, asCallId as af, asMeetingId as ag, asOrganizationId as ah, asParticipantIdentity as ai, asRoomName as aj, asUserId as ak, assignedRoom as al, autoAssign as am, breakoutRemainingMs as an, collabFromState as ao, createMeetApi as ap, defaultCaptionLanguage as aq, fileDownloadPath as ar, isGuest as as, isHostRole as at, isLinkFollower as au, loadMeetingActivities as av, withItem as aw, type RecordingSource as ax, type MeetApi as b, type MeetIdentity as c, type CallInvite as d, type ParticipantRole as e, type CallMode as f, type MeetingNote as g, type MeetingId as h, type CollabSnapshot as i, type AdmissionState as j, CAPTION_LANGUAGES as k, type CallId as l, type CallState as m, type CallablePerson as n, type ConnectionQuality as o, type InviteeRole as p, type JsonValue as q, type MeetApiOptions as r, type MeetApiRequest as s, type MeetBreakoutBroadcast as t, type MeetBreakoutPlan as u, type MeetBreakoutRoom as v, type MeetCollabSettings as w, type MeetFileMedia as x, type MeetPoll as y, type MeetPollOption as z };
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-Bss-_Jfp.cjs';
2
+ import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-B6scmSST.cjs';
3
3
 
4
4
  /**
5
5
  * THE PRODUCT UI (R6/R7). Not a headless kernel with a note telling the host to
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-Bss-_Jfp.js';
2
+ import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-B6scmSST.js';
3
3
 
4
4
  /**
5
5
  * THE PRODUCT UI (R6/R7). Not a headless kernel with a note telling the host to
package/dist/core.cjs CHANGED
@@ -50,6 +50,7 @@ __export(core_exports, {
50
50
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
51
51
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
52
52
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
53
+ MEETING_PASS_HEADER: () => MEETING_PASS_HEADER,
53
54
  MEETING_RECORD_GRACE_MS: () => MEETING_RECORD_GRACE_MS,
54
55
  MEET_ANSWER_CONTEXT_EVENT: () => MEET_ANSWER_CONTEXT_EVENT,
55
56
  MEET_ANSWER_EVENTS: () => MEET_ANSWER_EVENTS,
@@ -65,12 +66,12 @@ __export(core_exports, {
65
66
  ORGANIZATION_HEADER: () => ORGANIZATION_HEADER,
66
67
  PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
67
68
  PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
69
+ PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
68
70
  REACTION_TTL_MS: () => REACTION_TTL_MS,
69
71
  REFUSED_ABSENCE: () => REFUSED_ABSENCE,
70
72
  ROLE_RANK: () => ROLE_RANK,
71
73
  ROOM_EVENTS: () => ROOM_EVENTS,
72
74
  ROOM_EVENT_PREFIX: () => ROOM_EVENT_PREFIX,
73
- ROOM_TOKEN_HEADER: () => ROOM_TOKEN_HEADER,
74
75
  RPCS: () => RPCS,
75
76
  TABLES: () => TABLES,
76
77
  TRANSCRIPTION_ATTRIBUTES: () => TRANSCRIPTION_ATTRIBUTES,
@@ -141,6 +142,7 @@ __export(core_exports, {
141
142
  isGuest: () => isGuest,
142
143
  isGuestIdentity: () => isGuestIdentity,
143
144
  isHostRole: () => isHostRole,
145
+ isLinkFollower: () => isLinkFollower,
144
146
  isListenOnly: () => isListenOnly,
145
147
  isNoteTaker: () => isNoteTaker,
146
148
  isRealName: () => isRealName,
@@ -159,6 +161,7 @@ __export(core_exports, {
159
161
  meetingInviteAction: () => meetingInviteAction,
160
162
  meetingLength: () => meetingLength,
161
163
  meetingLink: () => meetingLink,
164
+ meetingPassHeaders: () => meetingPassHeaders,
162
165
  meetingPath: () => meetingPath,
163
166
  meetingSlug: () => meetingSlug,
164
167
  meetingTopic: () => meetingTopic,
@@ -202,6 +205,7 @@ __export(core_exports, {
202
205
  recordingLabel: () => recordingLabel,
203
206
  recordingNotice: () => recordingNotice,
204
207
  recordingOffsetMs: () => recordingOffsetMs,
208
+ rememberedMeetingPass: () => rememberedMeetingPass,
205
209
  roleOf: () => roleOf,
206
210
  roomTopic: () => roomTopic,
207
211
  rootAttributes: () => rootAttributes,
@@ -236,11 +240,18 @@ var MeetError = class extends Error {
236
240
  /** What to DO. Rendered to the user for the device codes. */
237
241
  remedy;
238
242
  cause;
239
- constructor(code, message, remedy, cause) {
243
+ /**
244
+ * The join gate's reason code (CORE-DESIGN §2.5) when the server named one: `not_found`,
245
+ * `cancelled`, `ended`, `locked`, `removed`, `lobby_rejects`, `not_invited`… The phase
246
+ * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
247
+ */
248
+ reason;
249
+ constructor(code, message, remedy, cause, reason) {
240
250
  super(message);
241
251
  this.name = "MeetError";
242
252
  this.code = code;
243
253
  this.remedy = remedy;
254
+ this.reason = reason ?? null;
244
255
  if (cause !== void 0) this.cause = cause;
245
256
  }
246
257
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -458,6 +469,17 @@ function createWebRoomTokenStorage(onDiagnostic) {
458
469
  }
459
470
  };
460
471
  }
472
+ function rememberedMeetingPass(roomName, storage = createWebRoomTokenStorage()) {
473
+ const raw = storage.read(roomName);
474
+ if (raw === null) return null;
475
+ try {
476
+ const parsed = JSON.parse(raw);
477
+ if (parsed.standing !== "guest") return null;
478
+ return typeof parsed.meetingPass === "string" && parsed.meetingPass.length > 0 ? parsed.meetingPass : null;
479
+ } catch {
480
+ return null;
481
+ }
482
+ }
461
483
  var MEET_ROUTES = {
462
484
  token: "/api/v1/meet/token",
463
485
  recordingStart: "/api/v1/meet/recording/start",
@@ -471,7 +493,7 @@ var MEET_ROUTES = {
471
493
  /**
472
494
  * THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`,
473
495
  * authorized by the room token this device already holds, carried in the
474
- * `x-meet-room-token` header — never a query parameter, because a bearer
496
+ * `x-meet-pass` header — never a query parameter, because a bearer
475
497
  * credential in a URL lands in access logs, referrers and history.
476
498
  */
477
499
  record: "/api/v1/meet/record",
@@ -479,7 +501,7 @@ var MEET_ROUTES = {
479
501
  recordAccess: "/api/v1/meet/record/access",
480
502
  /**
481
503
  * KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents
482
- * their kept pass in `x-meet-room-token` with their new session, and the
504
+ * their kept pass in `x-meet-pass` with their new session, and the
483
505
  * meeting they attended becomes theirs to read. Idempotent; never a grant.
484
506
  */
485
507
  claim: "/api/v1/meet/claim",
@@ -501,7 +523,7 @@ var MEET_ROUTES = {
501
523
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
502
524
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
503
525
  * Decided by the server from the meeting itself; a guest's pass rides
504
- * `x-meet-room-token`.
526
+ * `x-meet-pass`.
505
527
  */
506
528
  moderate: "/api/v1/meet/moderate",
507
529
  /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
@@ -517,7 +539,11 @@ var MEET_ROUTES = {
517
539
  function meetingPath(template, meetingId) {
518
540
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
519
541
  }
520
- var ROOM_TOKEN_HEADER = "x-meet-room-token";
542
+ var MEETING_PASS_HEADER = "x-meet-pass";
543
+ function meetingPassHeaders(token) {
544
+ const pass = token?.meetingPass ?? null;
545
+ return pass !== null && pass.length > 0 ? { [MEETING_PASS_HEADER]: pass } : {};
546
+ }
521
547
  var REFRESH_RATIO = 0.8;
522
548
  function readString(source, key) {
523
549
  const value = source[key];
@@ -527,7 +553,12 @@ function readBoolean(source, key, fallback) {
527
553
  const value = source[key];
528
554
  return typeof value === "boolean" ? value : fallback;
529
555
  }
530
- var TOKEN_ROLES = ["host", "cohost", "participant", "guest"];
556
+ var TOKEN_STANDINGS = ["member", "invited", "outsider", "guest"];
557
+ function readStanding(source, key) {
558
+ const value = source[key];
559
+ return TOKEN_STANDINGS.includes(value) ? value : null;
560
+ }
561
+ var TOKEN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
531
562
  function readRole(source, key) {
532
563
  const value = source[key];
533
564
  return TOKEN_ROLES.includes(value) ? value : null;
@@ -570,7 +601,12 @@ function parseRoomToken(body, roomName) {
570
601
  // Deriving the role from what DID arrive keeps a host a host against a
571
602
  // server that has not landed MRI-A11 — and never invents a host from
572
603
  // silence, because both fallbacks below grant nothing.
573
- role: readRole(payload, "role") ?? (isHost ? "host" : identity.startsWith("guest:") ? "guest" : "participant"),
604
+ // A word this package does not know grants nothing: `participant`, never a host.
605
+ role: readRole(payload, "role") ?? (isHost ? "host" : "participant"),
606
+ standing: readStanding(payload, "standing"),
607
+ meetingPass: readString(payload, "meeting_pass"),
608
+ meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
609
+ sessionId: readString(payload, "session_id"),
574
610
  lobby: readBoolean(payload, "lobby", false),
575
611
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null
576
612
  };
@@ -581,7 +617,7 @@ function createTokenClient(options) {
581
617
  const storage = options.storage;
582
618
  function remember(token) {
583
619
  if (storage === void 0) return;
584
- if (!token.identity.startsWith("guest:")) return;
620
+ if (token.standing !== "guest") return;
585
621
  storage.write(token.roomName, JSON.stringify(token));
586
622
  }
587
623
  function recall(roomName) {
@@ -639,22 +675,28 @@ function createTokenClient(options) {
639
675
  throw new MeetError(
640
676
  "forbidden",
641
677
  said?.message ?? "You can't join this meeting.",
642
- said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
678
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
679
+ void 0,
680
+ said?.reason ?? null
643
681
  );
644
682
  }
645
683
  if (response.status === 404) {
646
684
  throw new MeetError(
647
685
  "not-found",
648
686
  "meet.token: no such meeting",
649
- "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
687
+ "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry.",
688
+ void 0,
689
+ "not_found"
650
690
  );
651
691
  }
652
692
  if (response.status === 410) {
653
- const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
693
+ const said = await serverSentence(response);
654
694
  throw new MeetError(
655
695
  "cancelled",
656
- reason,
657
- "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
696
+ said?.message ?? "This meeting was cancelled.",
697
+ "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one.",
698
+ void 0,
699
+ said?.reason ?? "cancelled"
658
700
  );
659
701
  }
660
702
  if (response.status === 423) {
@@ -662,7 +704,9 @@ function createTokenClient(options) {
662
704
  throw new MeetError(
663
705
  "room-locked",
664
706
  said?.message ?? "The meeting is locked.",
665
- said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
707
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again.",
708
+ void 0,
709
+ said?.reason ?? "locked"
666
710
  );
667
711
  }
668
712
  if (response.status === 503) {
@@ -731,6 +775,10 @@ function placeholder(roomName) {
731
775
  canRecord: false,
732
776
  isHost: false,
733
777
  role: "participant",
778
+ standing: null,
779
+ meetingPass: null,
780
+ meetingPassExpiresAt: null,
781
+ sessionId: null,
734
782
  lobby: false,
735
783
  breakoutRoomId: null
736
784
  };
@@ -743,7 +791,8 @@ async function serverSentence(response) {
743
791
  const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
744
792
  if (message === null) return null;
745
793
  const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
746
- return { message, remedy };
794
+ const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
795
+ return { message, remedy, reason };
747
796
  } catch {
748
797
  return null;
749
798
  }
@@ -932,7 +981,7 @@ function createMeetAi(options) {
932
981
  operation: "meet.ai.ask",
933
982
  // A GUEST MAY ASK. The server decides whether it can answer.
934
983
  requireAuth: false,
935
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {},
984
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {},
936
985
  ...signal !== void 0 ? { signal } : {}
937
986
  });
938
987
  if (!response.ok) {
@@ -1192,11 +1241,15 @@ function isGuest(identity) {
1192
1241
  return identity.userId === null;
1193
1242
  }
1194
1243
  var ROLE_RANK = {
1195
- host: 3,
1196
- cohost: 2,
1244
+ host: 4,
1245
+ cohost: 3,
1246
+ presenter: 2,
1197
1247
  participant: 1,
1198
- guest: 0
1248
+ viewer: 0
1199
1249
  };
1250
+ function isLinkFollower(standing) {
1251
+ return standing === "guest" || standing === "outsider";
1252
+ }
1200
1253
  function isHostRole(role) {
1201
1254
  return role === "host" || role === "cohost";
1202
1255
  }
@@ -1749,15 +1802,15 @@ var CAPTION_LANGUAGES = [
1749
1802
  { tag: "id", name: "Indonesian" },
1750
1803
  { tag: "sv", name: "Swedish" }
1751
1804
  ];
1752
- async function loadMeetingActivities(api, meetingId, roomToken = null) {
1805
+ async function loadMeetingActivities(api, meetingId, meetingPass = null) {
1753
1806
  let response;
1754
1807
  try {
1755
1808
  response = await api.request({
1756
1809
  path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
1757
1810
  method: "GET",
1758
1811
  operation: "meet.loadMeetingActivities",
1759
- requireAuth: roomToken === null,
1760
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
1812
+ requireAuth: meetingPass === null,
1813
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
1761
1814
  });
1762
1815
  } catch (error) {
1763
1816
  return {
@@ -1814,7 +1867,6 @@ var RPCS = {
1814
1867
  meetingBySlug: "meet_meeting_by_slug",
1815
1868
  admit: "meet_admit_participant",
1816
1869
  setLock: "meet_set_lock",
1817
- endMeeting: "meet_end_meeting",
1818
1870
  recordConsent: "meet_record_consent",
1819
1871
  /*
1820
1872
  * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
@@ -2228,7 +2280,7 @@ function createMeetRepository(options) {
2228
2280
  const operation = "meet.projectAttendee";
2229
2281
  const role = text(row, "role");
2230
2282
  const admission = text(row, "admission_state");
2231
- const known = ["host", "cohost", "participant", "guest"];
2283
+ const known = ["host", "cohost", "presenter", "participant", "viewer"];
2232
2284
  const states = ["knocking", "admitted", "denied", "removed"];
2233
2285
  const userId = text(row, "participant_user_id");
2234
2286
  const identity = requireText(row, "identity", operation);
@@ -2543,16 +2595,6 @@ function createMeetRepository(options) {
2543
2595
  );
2544
2596
  return projectMeeting(row);
2545
2597
  },
2546
- async endMeeting(meetingId, byUserId) {
2547
- const row = await guard(
2548
- "meet.endMeeting",
2549
- () => db.rpc(RPCS.endMeeting, {
2550
- p_meeting_id: meetingId,
2551
- p_by_user_id: byUserId
2552
- })
2553
- );
2554
- return projectMeeting(row);
2555
- },
2556
2598
  async transcript(meetingId, limit = 1e3) {
2557
2599
  const response = await db.from(TABLES.transcripts).select(SEGMENT_COLUMNS).eq("meeting_id", meetingId).order("started_at", { ascending: true }).order("id", { ascending: true }).limit(limit);
2558
2600
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
@@ -2881,6 +2923,7 @@ var DEFAULT_EFFECTS = {
2881
2923
  };
2882
2924
  var NO_GRANTS = {
2883
2925
  role: "participant",
2926
+ standing: null,
2884
2927
  canRecord: false,
2885
2928
  canPublish: false,
2886
2929
  canSubscribe: false
@@ -2927,6 +2970,11 @@ function effectiveRole(snapshot) {
2927
2970
  const fromToken = snapshot.grants.role;
2928
2971
  return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
2929
2972
  }
2973
+ function effectiveStanding(snapshot) {
2974
+ if (snapshot.grants.standing != null) return snapshot.grants.standing;
2975
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
2976
+ return me?.standing ?? null;
2977
+ }
2930
2978
  var REACTION_TTL_MS = 5e3;
2931
2979
  var CHAT_LIMIT = 200;
2932
2980
  function createMeetStore(initial = {}) {
@@ -3419,7 +3467,7 @@ function applyModeration(store, payload, now) {
3419
3467
  return ignored(`a moderation kind this version does not know: ${String(kind)}`);
3420
3468
  }
3421
3469
  }
3422
- var ROLES = ["host", "cohost", "participant", "guest"];
3470
+ var ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
3423
3471
 
3424
3472
  // src/core/devices.ts
3425
3473
  var KINDS = ["audioinput", "videoinput", "audiooutput"];
@@ -4614,7 +4662,6 @@ function kindOf(person) {
4614
4662
  if (person.isAgent) return "assistant";
4615
4663
  if (person.role === "host") return "host";
4616
4664
  if (person.role === "cohost") return "cohost";
4617
- if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
4618
4665
  if (person.userId === null) return "guest";
4619
4666
  return "member";
4620
4667
  }
@@ -4899,7 +4946,7 @@ function meetingLength(meeting) {
4899
4946
  if (start === null || end === null || end < start) return null;
4900
4947
  return (0, import_format.formatDurationMs)(end - start, { style: "long" });
4901
4948
  }
4902
- async function loadMeetingChat(api, meetingId, roomToken = null) {
4949
+ async function loadMeetingChat(api, meetingId, meetingPass = null) {
4903
4950
  const operation = "meet.loadMeetingChat";
4904
4951
  let response;
4905
4952
  try {
@@ -4907,8 +4954,8 @@ async function loadMeetingChat(api, meetingId, roomToken = null) {
4907
4954
  path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
4908
4955
  method: "GET",
4909
4956
  operation,
4910
- requireAuth: roomToken === null,
4911
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
4957
+ requireAuth: meetingPass === null,
4958
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
4912
4959
  });
4913
4960
  } catch (error) {
4914
4961
  return {
@@ -5052,7 +5099,12 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
5052
5099
  // src/core/room.ts
5053
5100
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5054
5101
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5055
- var KNOWN_ROLES = ["host", "cohost", "participant", "guest"];
5102
+ var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
5103
+ var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
5104
+ function parseStanding(value) {
5105
+ return KNOWN_STANDINGS.includes(value) ? value : null;
5106
+ }
5107
+ var KNOWN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
5056
5108
  function parseRole(value) {
5057
5109
  return KNOWN_ROLES.includes(value) ? value : "participant";
5058
5110
  }
@@ -5086,6 +5138,9 @@ function projectParticipant(driver, joinedAt) {
5086
5138
  // ATTRIBUTE FIRST, metadata second — see the constants above. Both are the
5087
5139
  // server's; the attribute is simply the one that always arrives.
5088
5140
  role: parseRole(driver.attributes[PARTICIPANT_ROLE_ATTRIBUTE] ?? metadata.role),
5141
+ standing: parseStanding(
5142
+ driver.attributes[PARTICIPANT_STANDING_ATTRIBUTE] ?? metadata.standing
5143
+ ),
5089
5144
  canRecord: driver.attributes[PARTICIPANT_CAN_RECORD_ATTRIBUTE] === "true" || metadata.can_record === true,
5090
5145
  isLocal: driver.isLocal,
5091
5146
  isAgent: metadata.is_agent === true,
@@ -5158,7 +5213,7 @@ function createRoomEngine(options) {
5158
5213
  const normalized = normalizeMeetError(error, operation);
5159
5214
  store.patch({
5160
5215
  phase: "failed",
5161
- failure: { message: normalized.message, remedy: normalized.remedy }
5216
+ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
5162
5217
  });
5163
5218
  report(
5164
5219
  normalized.code === "session-unavailable" ? "warn" : "error",
@@ -5362,6 +5417,7 @@ function createRoomEngine(options) {
5362
5417
  breakoutRoomId: token.breakoutRoomId ?? null,
5363
5418
  grants: {
5364
5419
  role: token.role,
5420
+ standing: token.standing,
5365
5421
  canRecord: token.canRecord,
5366
5422
  canPublish: token.canPublish,
5367
5423
  canSubscribe: token.canSubscribe
@@ -5707,7 +5763,7 @@ function createInRoomClient(options) {
5707
5763
  }
5708
5764
  function headers() {
5709
5765
  const token = options.resolveRoomToken();
5710
- return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
5766
+ return token === null ? void 0 : { [MEETING_PASS_HEADER]: token };
5711
5767
  }
5712
5768
  function refusal2(response, operation, remedy) {
5713
5769
  return refusalFromResponse(response, operation, remedy);
@@ -6188,7 +6244,7 @@ function summarizeMeetingRecordRestrictions(restrictions) {
6188
6244
  async function loadGuestMeetingRecord(args) {
6189
6245
  const { api, repository, meetingId, meeting, roomToken } = args;
6190
6246
  const session = args.session === true;
6191
- if (roomToken === null && !session) {
6247
+ if (roomToken?.meetingPass == null && !session) {
6192
6248
  return refusedBundle(
6193
6249
  meeting,
6194
6250
  "This meeting's record is only readable by the people who were in it.",
@@ -6198,12 +6254,12 @@ async function loadGuestMeetingRecord(args) {
6198
6254
  let response;
6199
6255
  try {
6200
6256
  response = await api.request({
6201
- path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}`,
6257
+ path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}` + (args.sessionId ? `&session_id=${encodeURIComponent(args.sessionId)}` : ""),
6202
6258
  method: "GET",
6203
6259
  // A GUEST HAS NO SESSION AND IS STILL A FIRST-CLASS READER (D6).
6204
6260
  requireAuth: session,
6205
6261
  operation: "meet.record",
6206
- headers: roomToken === null ? {} : { [ROOM_TOKEN_HEADER]: roomToken.token }
6262
+ headers: roomToken?.meetingPass != null ? { [MEETING_PASS_HEADER]: roomToken.meetingPass } : {}
6207
6263
  });
6208
6264
  } catch (error) {
6209
6265
  return refusedBundle(
@@ -6591,6 +6647,10 @@ function takeMoveRequest(store) {
6591
6647
  }
6592
6648
 
6593
6649
  // src/core/observation.ts
6650
+ function observedRole(role, standing) {
6651
+ if (role === "host" || role === "cohost") return role;
6652
+ return isLinkFollower(standing) ? "guest" : "participant";
6653
+ }
6594
6654
  function observedDevice(track) {
6595
6655
  if (track.enabled) return "on";
6596
6656
  const refused = track.unavailable;
@@ -6618,6 +6678,8 @@ function observedPhase(input) {
6618
6678
  const raw = snapshot.phase;
6619
6679
  const live = raw === "connected" || raw === "reconnecting";
6620
6680
  if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
6681
+ const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
6682
+ if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
6621
6683
  switch (raw) {
6622
6684
  case "idle":
6623
6685
  case "failed":
@@ -6638,12 +6700,13 @@ function observe(input) {
6638
6700
  const { snapshot } = input;
6639
6701
  const phase = observedPhase(input);
6640
6702
  const inCall = phase === "in_call";
6641
- const role = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
6642
- const canAdmit = role !== "none" && ADMITTER_ROLES.has(role);
6703
+ const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
6704
+ const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
6705
+ const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
6643
6706
  const deviceInPlay = phase === "joining" || inCall;
6644
6707
  return {
6645
6708
  phase,
6646
- reason: null,
6709
+ reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6647
6710
  role,
6648
6711
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
6649
6712
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
@@ -6709,7 +6772,7 @@ function participantAttributes(p) {
6709
6772
  "data-meet-participant": "",
6710
6773
  "data-meet-participant-identity": p.identity,
6711
6774
  "data-meet-participant-name": p.displayName,
6712
- "data-meet-participant-role": p.role,
6775
+ "data-meet-participant-role": observedRole(p.role, p.standing),
6713
6776
  "data-meet-participant-self": String(p.isLocal)
6714
6777
  };
6715
6778
  const quality = observedQuality(p.quality);