@memberjunction/livekit-room-server 6.2.0-edge.2 → 6.2.0-edge.4

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.
Files changed (82) hide show
  1. package/README.md +85 -0
  2. package/dist/index.d.ts +18 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +18 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/livekit-agent-room-coordinator.d.ts +220 -13
  7. package/dist/livekit-agent-room-coordinator.d.ts.map +1 -1
  8. package/dist/livekit-agent-room-coordinator.js +409 -65
  9. package/dist/livekit-agent-room-coordinator.js.map +1 -1
  10. package/dist/livekit-egress-service.d.ts +7 -0
  11. package/dist/livekit-egress-service.d.ts.map +1 -1
  12. package/dist/livekit-egress-service.js +11 -0
  13. package/dist/livekit-egress-service.js.map +1 -1
  14. package/dist/livekit-native-module.d.ts +18 -0
  15. package/dist/livekit-native-module.d.ts.map +1 -0
  16. package/dist/livekit-native-module.js +27 -0
  17. package/dist/livekit-native-module.js.map +1 -0
  18. package/dist/livekit-participant-service.d.ts +47 -0
  19. package/dist/livekit-participant-service.d.ts.map +1 -0
  20. package/dist/livekit-participant-service.js +64 -0
  21. package/dist/livekit-participant-service.js.map +1 -0
  22. package/dist/livekit-sip-service.d.ts +141 -0
  23. package/dist/livekit-sip-service.d.ts.map +1 -0
  24. package/dist/livekit-sip-service.js +169 -0
  25. package/dist/livekit-sip-service.js.map +1 -0
  26. package/dist/livekit-token-service.d.ts +26 -5
  27. package/dist/livekit-token-service.d.ts.map +1 -1
  28. package/dist/livekit-token-service.js +22 -7
  29. package/dist/livekit-token-service.js.map +1 -1
  30. package/dist/livekit-webhook.d.ts +65 -0
  31. package/dist/livekit-webhook.d.ts.map +1 -0
  32. package/dist/livekit-webhook.js +65 -0
  33. package/dist/livekit-webhook.js.map +1 -0
  34. package/dist/meeting-dial-in-service.d.ts +83 -0
  35. package/dist/meeting-dial-in-service.d.ts.map +1 -0
  36. package/dist/meeting-dial-in-service.js +202 -0
  37. package/dist/meeting-dial-in-service.js.map +1 -0
  38. package/dist/room-audio/audio-decoder.d.ts +40 -0
  39. package/dist/room-audio/audio-decoder.d.ts.map +1 -0
  40. package/dist/room-audio/audio-decoder.js +205 -0
  41. package/dist/room-audio/audio-decoder.js.map +1 -0
  42. package/dist/room-audio/comfort-tone.d.ts +23 -0
  43. package/dist/room-audio/comfort-tone.d.ts.map +1 -0
  44. package/dist/room-audio/comfort-tone.js +70 -0
  45. package/dist/room-audio/comfort-tone.js.map +1 -0
  46. package/dist/room-audio/pcm-frame-pump.d.ts +134 -0
  47. package/dist/room-audio/pcm-frame-pump.d.ts.map +1 -0
  48. package/dist/room-audio/pcm-frame-pump.js +253 -0
  49. package/dist/room-audio/pcm-frame-pump.js.map +1 -0
  50. package/dist/room-audio/room-audio-decode-cache.d.ts +37 -0
  51. package/dist/room-audio/room-audio-decode-cache.d.ts.map +1 -0
  52. package/dist/room-audio/room-audio-decode-cache.js +75 -0
  53. package/dist/room-audio/room-audio-decode-cache.js.map +1 -0
  54. package/dist/room-audio/room-audio-player.d.ts +178 -0
  55. package/dist/room-audio/room-audio-player.d.ts.map +1 -0
  56. package/dist/room-audio/room-audio-player.js +404 -0
  57. package/dist/room-audio/room-audio-player.js.map +1 -0
  58. package/dist/room-audio/room-audio-sources.d.ts +47 -0
  59. package/dist/room-audio/room-audio-sources.d.ts.map +1 -0
  60. package/dist/room-audio/room-audio-sources.js +116 -0
  61. package/dist/room-audio/room-audio-sources.js.map +1 -0
  62. package/dist/room-audio/room-speech-synthesizer.d.ts +55 -0
  63. package/dist/room-audio/room-speech-synthesizer.d.ts.map +1 -0
  64. package/dist/room-audio/room-speech-synthesizer.js +56 -0
  65. package/dist/room-audio/room-speech-synthesizer.js.map +1 -0
  66. package/dist/room-authorization.d.ts +58 -0
  67. package/dist/room-authorization.d.ts.map +1 -0
  68. package/dist/room-authorization.js +212 -0
  69. package/dist/room-authorization.js.map +1 -0
  70. package/dist/room-handoff/handoff-offer-registry.d.ts +147 -0
  71. package/dist/room-handoff/handoff-offer-registry.d.ts.map +1 -0
  72. package/dist/room-handoff/handoff-offer-registry.js +548 -0
  73. package/dist/room-handoff/handoff-offer-registry.js.map +1 -0
  74. package/dist/room-handoff/handoff-types.d.ts +172 -0
  75. package/dist/room-handoff/handoff-types.d.ts.map +1 -0
  76. package/dist/room-handoff/handoff-types.js +16 -0
  77. package/dist/room-handoff/handoff-types.js.map +1 -0
  78. package/dist/room-handoff/room-handoff-engine.d.ts +149 -0
  79. package/dist/room-handoff/room-handoff-engine.d.ts.map +1 -0
  80. package/dist/room-handoff/room-handoff-engine.js +563 -0
  81. package/dist/room-handoff/room-handoff-engine.js.map +1 -0
  82. package/package.json +15 -9
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-speech-synthesizer.d.ts","sourceRoot":"","sources":["../../src/room-audio/room-speech-synthesizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEjE,yGAAyG;AACzG,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CAC/E;AAED,mDAAmD;AACnD,MAAM,WAAW,8BAA8B;IAC7C,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd,0GAA0G;IAC1G,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wHAAwH;IACxH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8HAA8H;IAC9H,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,+FAA+F;AAC/F,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,oBAAoB,EAAE,iBAAiB,CAAC,CAAC;AAEnF,6HAA6H;AAC7H,qBAAa,uBAAwB,YAAW,sBAAsB;IACpE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyB;IAEhD;;;;OAIG;gBACS,OAAO,EAAE,8BAA8B,EAAE,MAAM,CAAC,EAAE,sBAAsB;IAQpF,oFAAoF;IACvE,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;CAqB3F"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @fileoverview The text-to-speech port the room audio player announces through ({@link IRoomSpeechSynthesizer}),
3
+ * and {@link MJRoomSpeechSynthesizer}, an implementation over MJ's own `AITextToSpeechRunner` (model selection,
4
+ * credentials, failover and an `MJ: AI Prompt Runs` row, all handled there).
5
+ *
6
+ * The player ships with NO synthesizer installed, because a voice is vendor-specific and only the host knows which
7
+ * one its callers should hear. A host opts in once at startup:
8
+ *
9
+ * ```ts
10
+ * RoomAudioPlayer.Instance.SetSpeechSynthesizer(new MJRoomSpeechSynthesizer({ Voice: 'alloy' }));
11
+ * ```
12
+ *
13
+ * @module @memberjunction/livekit-room-server
14
+ */
15
+ import { LogError } from '@memberjunction/core';
16
+ import { AITextToSpeechRunner } from '@memberjunction/ai-prompts';
17
+ import { DecodeAudio } from './audio-decoder.js';
18
+ /** An {@link IRoomSpeechSynthesizer} backed by MJ's `AITextToSpeechRunner`. Never throws: failures log and return `null`. */
19
+ export class MJRoomSpeechSynthesizer {
20
+ /**
21
+ * @param options The voice and model selection.
22
+ * @param runner The runner (tests); defaults to a new `AITextToSpeechRunner`.
23
+ * @throws {Error} when no voice is given.
24
+ */
25
+ constructor(options, runner) {
26
+ if (!options.Voice?.trim()) {
27
+ throw new Error('MJRoomSpeechSynthesizer needs a Voice (a provider-native voice id or name).');
28
+ }
29
+ this.options = options;
30
+ this.runner = runner ?? new AITextToSpeechRunner();
31
+ }
32
+ /** Speaks `text` through the configured TTS model and decodes the result to PCM. */
33
+ async Synthesize(text, contextUser) {
34
+ try {
35
+ const result = await this.runner.RunTextToSpeech({
36
+ text,
37
+ voice: this.options.Voice,
38
+ output_format: this.options.OutputFormat,
39
+ ModelID: this.options.ModelID,
40
+ PromptID: this.options.PromptID,
41
+ ContextUser: contextUser,
42
+ });
43
+ const audio = result.SpeechResult?.data;
44
+ if (!result.Success || !audio || audio.byteLength === 0) {
45
+ LogError(`[MJRoomSpeechSynthesizer] text-to-speech failed for voice '${this.options.Voice}': ${result.ErrorMessage ?? 'no audio returned'}`);
46
+ return null;
47
+ }
48
+ return await DecodeAudio(new Uint8Array(audio.buffer, audio.byteOffset, audio.byteLength));
49
+ }
50
+ catch (err) {
51
+ LogError(`[MJRoomSpeechSynthesizer] could not synthesize an announcement: ${err instanceof Error ? err.message : String(err)}`);
52
+ return null;
53
+ }
54
+ }
55
+ }
56
+ //# sourceMappingURL=room-speech-synthesizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-speech-synthesizer.js","sourceRoot":"","sources":["../../src/room-audio/room-speech-synthesizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,QAAQ,EAAiB,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,WAAW,EAAqB,MAAM,iBAAiB,CAAC;AA6BjE,6HAA6H;AAC7H,MAAM,OAAO,uBAAuB;IAIlC;;;;OAIG;IACH,YAAY,OAAuC,EAAE,MAA+B;QAClF,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;QACjG,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,IAAI,oBAAoB,EAAE,CAAC;IACrD,CAAC;IAED,oFAAoF;IAC7E,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,WAAqB;QACzD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC;gBAC/C,IAAI;gBACJ,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK;gBACzB,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY;gBACxC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO;gBAC7B,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;gBAC/B,WAAW,EAAE,WAAW;aACzB,CAAC,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,EAAE,IAAI,CAAC;YACxC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;gBACxD,QAAQ,CAAC,8DAA8D,IAAI,CAAC,OAAO,CAAC,KAAK,MAAM,MAAM,CAAC,YAAY,IAAI,mBAAmB,EAAE,CAAC,CAAC;gBAC7I,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,MAAM,WAAW,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;QAC7F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,QAAQ,CAAC,mEAAmE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAChI,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,58 @@
1
+ import { BaseSingleton } from '@memberjunction/global';
2
+ import { type IMetadataProvider, type UserInfo } from '@memberjunction/core';
3
+ export declare const DEFAULT_SIP_CALL_ROOM_PREFIX = "call-";
4
+ export declare const SIP_CALL_ROOM_PREFIX = "call-";
5
+ export interface RoomAuthorizationResult {
6
+ Authorized: boolean;
7
+ Reason?: string;
8
+ }
9
+ export type RoomAuthorizerFn = (roomName: string, user: UserInfo, provider?: IMetadataProvider) => Promise<RoomAuthorizationResult>;
10
+ /**
11
+ * Service enforcing per-room authorization for LiveKit rooms.
12
+ *
13
+ * Rules:
14
+ * 1. Rooms tied to an `MJ: Meetings` record:
15
+ * - Cancelled meetings refuse access.
16
+ * - Host user (`meeting.HostUserID`) is granted access.
17
+ * - Meeting participants (`MJ: Meeting Participants` where `UserID = user.ID`):
18
+ * - If `InviteStatus === 'Declined'`, access is refused.
19
+ * - If `InviteStatus` is `'Invited'`, `'Accepted'`, or `'Tentative'`, access is granted.
20
+ * - Non-participants are refused access.
21
+ *
22
+ * 2. Rooms tied to an `MJ: Interactions` record OR starting with the SIP call prefix (`call-`):
23
+ * - Never ad-hoc.
24
+ * - User who has an `Accepted` offer for this room is granted access.
25
+ * - User who started it (the Interaction's agent-session owner) is granted access.
26
+ * - Otherwise access is refused.
27
+ *
28
+ * 3. Rooms tied to an `MJ: Interaction Offers` record:
29
+ * - User must be the `TargetUserID` AND have `Status === 'Accepted'`.
30
+ * - Otherwise access is refused.
31
+ *
32
+ * 4. Ad-hoc rooms:
33
+ * - Only rooms with NO Meeting, NO Interaction, and NO Offer row (and not starting with `call-`)
34
+ * are ad-hoc and open to authenticated users.
35
+ */
36
+ export declare class RoomAuthorizationService extends BaseSingleton<RoomAuthorizationService> {
37
+ private authorizerOverride?;
38
+ private sipRoomPrefix;
39
+ constructor();
40
+ static get Instance(): RoomAuthorizationService;
41
+ /**
42
+ * Sets an authorizer override for testing or specialized harnesses.
43
+ */
44
+ SetAuthorizerForTesting(fn?: RoomAuthorizerFn): void;
45
+ /**
46
+ * Sets or resets the SIP call room prefix (e.g. to match LiveKitSipTelephonyService.roomPrefix).
47
+ */
48
+ SetSipRoomPrefix(prefix?: string): void;
49
+ /**
50
+ * Gets the active SIP call room prefix.
51
+ */
52
+ get SipRoomPrefix(): string;
53
+ /**
54
+ * Authorizes whether the given user may access the specified LiveKit room.
55
+ */
56
+ AuthorizeRoomAccess(roomName: string, user: UserInfo, provider?: IMetadataProvider): Promise<RoomAuthorizationResult>;
57
+ }
58
+ //# sourceMappingURL=room-authorization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-authorization.d.ts","sourceRoot":"","sources":["../src/room-authorization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAA+B,MAAM,wBAAwB,CAAC;AACpF,OAAO,EAAqB,KAAK,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhG,eAAO,MAAM,4BAA4B,UAAU,CAAC;AACpD,eAAO,MAAM,oBAAoB,UAA+B,CAAC;AAEjE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC7B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,QAAQ,EACd,QAAQ,CAAC,EAAE,iBAAiB,KACzB,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAoCtC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,wBAAyB,SAAQ,aAAa,CAAC,wBAAwB,CAAC;IACnF,OAAO,CAAC,kBAAkB,CAAC,CAAmB;IAC9C,OAAO,CAAC,aAAa,CAAwH;;IAM7I,WAAkB,QAAQ,IAAI,wBAAwB,CAErD;IAED;;OAEG;IACI,uBAAuB,CAAC,EAAE,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAI3D;;OAEG;IACI,gBAAgB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAI9C;;OAEG;IACH,IAAW,aAAa,IAAI,MAAM,CAEjC;IAED;;OAEG;IACU,mBAAmB,CAC9B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,QAAQ,EACd,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,OAAO,CAAC,uBAAuB,CAAC;CAyLpC"}
@@ -0,0 +1,212 @@
1
+ import { BaseSingleton, EscapeSQLString, UUIDsEqual } from '@memberjunction/global';
2
+ import { LogError, RunView } from '@memberjunction/core';
3
+ import { LiveKitAgentRoomCoordinator } from './livekit-agent-room-coordinator.js';
4
+ export const DEFAULT_SIP_CALL_ROOM_PREFIX = 'call-';
5
+ export const SIP_CALL_ROOM_PREFIX = DEFAULT_SIP_CALL_ROOM_PREFIX;
6
+ /**
7
+ * Service enforcing per-room authorization for LiveKit rooms.
8
+ *
9
+ * Rules:
10
+ * 1. Rooms tied to an `MJ: Meetings` record:
11
+ * - Cancelled meetings refuse access.
12
+ * - Host user (`meeting.HostUserID`) is granted access.
13
+ * - Meeting participants (`MJ: Meeting Participants` where `UserID = user.ID`):
14
+ * - If `InviteStatus === 'Declined'`, access is refused.
15
+ * - If `InviteStatus` is `'Invited'`, `'Accepted'`, or `'Tentative'`, access is granted.
16
+ * - Non-participants are refused access.
17
+ *
18
+ * 2. Rooms tied to an `MJ: Interactions` record OR starting with the SIP call prefix (`call-`):
19
+ * - Never ad-hoc.
20
+ * - User who has an `Accepted` offer for this room is granted access.
21
+ * - User who started it (the Interaction's agent-session owner) is granted access.
22
+ * - Otherwise access is refused.
23
+ *
24
+ * 3. Rooms tied to an `MJ: Interaction Offers` record:
25
+ * - User must be the `TargetUserID` AND have `Status === 'Accepted'`.
26
+ * - Otherwise access is refused.
27
+ *
28
+ * 4. Ad-hoc rooms:
29
+ * - Only rooms with NO Meeting, NO Interaction, and NO Offer row (and not starting with `call-`)
30
+ * are ad-hoc and open to authenticated users.
31
+ */
32
+ export class RoomAuthorizationService extends BaseSingleton {
33
+ constructor() {
34
+ super();
35
+ this.sipRoomPrefix = (process.env.LIVEKIT_SIP_ROOM_PREFIX ?? DEFAULT_SIP_CALL_ROOM_PREFIX).trim() || DEFAULT_SIP_CALL_ROOM_PREFIX;
36
+ }
37
+ static get Instance() {
38
+ return RoomAuthorizationService.getInstance();
39
+ }
40
+ /**
41
+ * Sets an authorizer override for testing or specialized harnesses.
42
+ */
43
+ SetAuthorizerForTesting(fn) {
44
+ this.authorizerOverride = fn;
45
+ }
46
+ /**
47
+ * Sets or resets the SIP call room prefix (e.g. to match LiveKitSipTelephonyService.roomPrefix).
48
+ */
49
+ SetSipRoomPrefix(prefix) {
50
+ this.sipRoomPrefix = (prefix ?? process.env.LIVEKIT_SIP_ROOM_PREFIX ?? DEFAULT_SIP_CALL_ROOM_PREFIX).trim() || DEFAULT_SIP_CALL_ROOM_PREFIX;
51
+ }
52
+ /**
53
+ * Gets the active SIP call room prefix.
54
+ */
55
+ get SipRoomPrefix() {
56
+ return this.sipRoomPrefix;
57
+ }
58
+ /**
59
+ * Authorizes whether the given user may access the specified LiveKit room.
60
+ */
61
+ async AuthorizeRoomAccess(roomName, user, provider) {
62
+ if (!user || !user.ID) {
63
+ return { Authorized: false, Reason: 'User is not authenticated.' };
64
+ }
65
+ if (this.authorizerOverride) {
66
+ return await this.authorizerOverride(roomName, user, provider);
67
+ }
68
+ const trimmedRoom = roomName.trim();
69
+ if (!trimmedRoom) {
70
+ return { Authorized: false, Reason: 'Room name cannot be empty.' };
71
+ }
72
+ const escapedRoom = EscapeSQLString(trimmedRoom);
73
+ const escapedUserID = EscapeSQLString(user.ID);
74
+ const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();
75
+ try {
76
+ // Batch initial lookups for Meetings, Interactions, Interaction Offers, and Agent Session Bridges
77
+ const [meetingResult, interactionResult, offerResult, bridgeResult] = await rv.RunViews([
78
+ {
79
+ EntityName: 'MJ: Meetings',
80
+ ExtraFilter: `RoomName = '${escapedRoom}'`,
81
+ Fields: ['ID', 'Status', 'HostUserID'],
82
+ ResultType: 'simple',
83
+ },
84
+ {
85
+ EntityName: 'MJ: Interactions',
86
+ ExtraFilter: `RoomName = '${escapedRoom}'`,
87
+ Fields: ['ID', 'Status', 'AgentSessionID'],
88
+ ResultType: 'simple',
89
+ },
90
+ {
91
+ EntityName: 'MJ: Interaction Offers',
92
+ ExtraFilter: `RoomName = '${escapedRoom}'`,
93
+ Fields: ['ID', 'Status', 'TargetUserID'],
94
+ ResultType: 'simple',
95
+ },
96
+ {
97
+ EntityName: 'MJ: AI Agent Session Bridges',
98
+ ExtraFilter: `ExternalConnectionID = '${escapedRoom}' AND Status = 'Active'`,
99
+ Fields: ['ID', 'AgentSessionID'],
100
+ ResultType: 'simple',
101
+ },
102
+ ], user);
103
+ if (!meetingResult.Success) {
104
+ LogError(`[RoomAuthorizationService] Error querying MJ: Meetings for room '${trimmedRoom}': ${meetingResult.ErrorMessage}`);
105
+ return { Authorized: false, Reason: 'Error verifying meeting authorization.' };
106
+ }
107
+ if (!interactionResult.Success) {
108
+ LogError(`[RoomAuthorizationService] Error querying MJ: Interactions for room '${trimmedRoom}': ${interactionResult.ErrorMessage}`);
109
+ return { Authorized: false, Reason: 'Error verifying interaction authorization.' };
110
+ }
111
+ if (!offerResult.Success) {
112
+ LogError(`[RoomAuthorizationService] Error querying MJ: Interaction Offers for room '${trimmedRoom}': ${offerResult.ErrorMessage}`);
113
+ return { Authorized: false, Reason: 'Error verifying interaction offer authorization.' };
114
+ }
115
+ if (!bridgeResult.Success) {
116
+ LogError(`[RoomAuthorizationService] Error querying MJ: AI Agent Session Bridges for room '${trimmedRoom}': ${bridgeResult.ErrorMessage}`);
117
+ return { Authorized: false, Reason: 'Error verifying agent bridge authorization.' };
118
+ }
119
+ const meetings = (meetingResult.Results ?? []);
120
+ const interactions = (interactionResult.Results ?? []);
121
+ const offers = (offerResult.Results ?? []);
122
+ const bridges = (bridgeResult.Results ?? []);
123
+ const activeRoster = LiveKitAgentRoomCoordinator.Instance.GetAgentsInRoom(trimmedRoom);
124
+ const agentSessionIDs = new Set();
125
+ for (const i of interactions) {
126
+ if (i.AgentSessionID)
127
+ agentSessionIDs.add(i.AgentSessionID);
128
+ }
129
+ for (const b of bridges) {
130
+ if (b.AgentSessionID)
131
+ agentSessionIDs.add(b.AgentSessionID);
132
+ }
133
+ for (const r of activeRoster) {
134
+ if (r.AgentSessionID)
135
+ agentSessionIDs.add(r.AgentSessionID);
136
+ }
137
+ // 1. Check if room is tied to an MJ: Meetings record
138
+ if (meetings.length > 0) {
139
+ const meeting = meetings[0];
140
+ if (meeting.Status === 'Cancelled') {
141
+ return { Authorized: false, Reason: 'Meeting is cancelled.' };
142
+ }
143
+ if (UUIDsEqual(meeting.HostUserID, user.ID)) {
144
+ return { Authorized: true };
145
+ }
146
+ const participantResult = await rv.RunView({
147
+ EntityName: 'MJ: Meeting Participants',
148
+ ExtraFilter: `MeetingID = '${EscapeSQLString(meeting.ID)}' AND UserID = '${escapedUserID}'`,
149
+ Fields: ['ID', 'InviteStatus', 'UserID'],
150
+ ResultType: 'simple',
151
+ }, user);
152
+ if (!participantResult.Success) {
153
+ LogError(`[RoomAuthorizationService] Error querying MJ: Meeting Participants for meeting '${meeting.ID}': ${participantResult.ErrorMessage}`);
154
+ return { Authorized: false, Reason: 'Error verifying participant authorization.' };
155
+ }
156
+ const participants = (participantResult.Results ?? []);
157
+ if (participants.length > 0) {
158
+ const participant = participants[0];
159
+ if (participant.InviteStatus === 'Declined') {
160
+ return { Authorized: false, Reason: 'User declined the invitation to this meeting.' };
161
+ }
162
+ return { Authorized: true };
163
+ }
164
+ return { Authorized: false, Reason: 'User is not a host or participant in this meeting.' };
165
+ }
166
+ // 2. Check if room is tied to an Interaction, has active agent sessions (e.g. web agent rooms), or has a SIP call prefix (never ad-hoc)
167
+ const isSipCallRoom = trimmedRoom.startsWith(this.sipRoomPrefix);
168
+ if (interactions.length > 0 || isSipCallRoom || agentSessionIDs.size > 0) {
169
+ // User with an Accepted offer for it
170
+ const hasAcceptedOffer = offers.some((o) => UUIDsEqual(o.TargetUserID, user.ID) && o.Status === 'Accepted');
171
+ if (hasAcceptedOffer) {
172
+ return { Authorized: true };
173
+ }
174
+ // User who started it (the Interaction's or Agent Session's owner)
175
+ if (agentSessionIDs.size > 0) {
176
+ const filter = [...agentSessionIDs].map((id) => `ID = '${EscapeSQLString(id)}'`).join(' OR ');
177
+ const sessionResult = await rv.RunView({
178
+ EntityName: 'MJ: AI Agent Sessions',
179
+ ExtraFilter: filter,
180
+ Fields: ['ID', 'UserID'],
181
+ ResultType: 'simple',
182
+ }, user);
183
+ if (!sessionResult.Success) {
184
+ LogError(`[RoomAuthorizationService] Error querying MJ: AI Agent Sessions for room '${trimmedRoom}': ${sessionResult.ErrorMessage}`);
185
+ return { Authorized: false, Reason: 'Error verifying session authorization.' };
186
+ }
187
+ const sessions = (sessionResult.Results ?? []);
188
+ if (sessions.some((s) => UUIDsEqual(s.UserID, user.ID))) {
189
+ return { Authorized: true };
190
+ }
191
+ }
192
+ return { Authorized: false, Reason: 'User is not authorized to access this call or agent session room.' };
193
+ }
194
+ // 3. Rooms tied to an MJ: Interaction Offers record
195
+ if (offers.length > 0) {
196
+ const acceptedOffer = offers.find((o) => UUIDsEqual(o.TargetUserID, user.ID) && o.Status === 'Accepted');
197
+ if (acceptedOffer) {
198
+ return { Authorized: true };
199
+ }
200
+ return { Authorized: false, Reason: 'User has not accepted a handoff offer for this room.' };
201
+ }
202
+ // 4. Ad-hoc rooms: only rooms with no Meeting, no Interaction, no Agent Session, and no Offer row
203
+ return { Authorized: true };
204
+ }
205
+ catch (err) {
206
+ const msg = err instanceof Error ? err.message : String(err);
207
+ LogError(`[RoomAuthorizationService] Unexpected error authorizing room '${trimmedRoom}': ${msg}`);
208
+ return { Authorized: false, Reason: `Internal authorization error: ${msg}` };
209
+ }
210
+ }
211
+ }
212
+ //# sourceMappingURL=room-authorization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-authorization.js","sourceRoot":"","sources":["../src/room-authorization.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @fileoverview Durable registry of handoff offers backed by `MJ: Interaction Offers`.
3
+ *
4
+ * An offer is a short-lived question put to one person: "a caller is waiting, will you take the conversation?".
5
+ * It lives in `MJ: Interaction Offers` from the moment the AI asks until it is accepted, declined, expired or cancelled,
6
+ * and is kept so a console that was open when it resolved can still show what happened.
7
+ *
8
+ * **Durable and cluster-safe.** Offers are stored in the database (`MJ: Interaction Offers`). Accept and decline
9
+ * execute a compare-and-set on `Status = 'Pending'` inside `RunInEntityTransaction` so multiple server instances
10
+ * cannot both accept or race. Expired offers are updated on read and through a periodic sweep. Live changes are
11
+ * published locally and across instances via cluster pub/sub.
12
+ *
13
+ * An accept or decline is authorised by comparing the offer's target user with the caller, and a mismatch answers
14
+ * exactly as a missing offer does: someone who does not own an offer cannot learn that it exists.
15
+ *
16
+ * @module @memberjunction/livekit-room-server
17
+ */
18
+ import { IMetadataProvider, UserInfo } from '@memberjunction/core';
19
+ import { BaseSingleton } from '@memberjunction/global';
20
+ import type { HandoffMode, HandoffOfferStatus, HandoffOfferView } from './handoff-types.js';
21
+ /** How long a person has to accept an offer before it expires. */
22
+ export declare const HANDOFF_OFFER_TIMEOUT_MS = 45000;
23
+ /** How long a resolved offer stays listed (so a console that was open still sees what happened). */
24
+ export declare const HANDOFF_OFFER_RETENTION_MS: number;
25
+ /** The most pending offers one person can have at once; a flood of offers is refused rather than queued. */
26
+ export declare const MAX_PENDING_OFFERS_PER_USER = 10;
27
+ /** What the registry stores for one offer. */
28
+ export interface HandoffOfferRecord {
29
+ OfferID: string;
30
+ RoomName: string;
31
+ /** The person the offer was made to. Server-side only. */
32
+ TargetUserID: string;
33
+ Mode: HandoffMode;
34
+ Summary: string;
35
+ CallerLabel: string;
36
+ AgentName: string;
37
+ Status: HandoffOfferStatus;
38
+ CreatedAtMs: number;
39
+ ExpiresAtMs: number;
40
+ /** When the offer left `Pending`, for retention. */
41
+ ResolvedAtMs?: number;
42
+ InteractionID?: string;
43
+ OfferedByAgentID?: string | null;
44
+ }
45
+ /** What it takes to create an offer. */
46
+ export interface CreateOfferInput {
47
+ RoomName: string;
48
+ TargetUserID: string;
49
+ Mode: HandoffMode;
50
+ Summary: string;
51
+ CallerLabel: string;
52
+ AgentName: string;
53
+ InteractionID?: string;
54
+ OfferedByAgentID?: string | null;
55
+ ContextUser?: UserInfo;
56
+ Provider?: IMetadataProvider;
57
+ }
58
+ /** The result of trying to resolve an offer on a person's behalf. */
59
+ export type ResolveOfferResult = {
60
+ Ok: true;
61
+ Offer: HandoffOfferRecord;
62
+ } | {
63
+ Ok: false;
64
+ Reason: string;
65
+ };
66
+ /** The one answer for "no such offer", "not yours" and "too late", so an offer's existence is not leaked. */
67
+ export declare const OFFER_UNAVAILABLE = "This conversation offer is no longer available.";
68
+ /** Converts a stored offer to what a console may see. */
69
+ export declare function ToOfferView(offer: HandoffOfferRecord): HandoffOfferView;
70
+ export interface HandoffOfferRegistryDeps {
71
+ Provider?: IMetadataProvider;
72
+ ContextUser?: UserInfo;
73
+ /**
74
+ * Lazily resolves a server identity when no `ContextUser` was given (e.g. the system user, which only exists once the
75
+ * host's user cache has loaded). Consulted on every call, so a late-loading cache is picked up.
76
+ */
77
+ ResolveContextUser?: () => UserInfo | undefined;
78
+ Publisher?: (event: {
79
+ UserID: string;
80
+ Kind: 'offered' | 'updated';
81
+ Offer: HandoffOfferView;
82
+ }) => void;
83
+ }
84
+ /** Holds handoff offers in `MJ: Interaction Offers` with in-memory caching and compare-and-set concurrency guards. */
85
+ export declare class HandoffOfferRegistry extends BaseSingleton<HandoffOfferRegistry> {
86
+ private deps;
87
+ private readonly offers;
88
+ private now;
89
+ private sweepTimer?;
90
+ protected constructor();
91
+ /** The process-wide registry. */
92
+ static get Instance(): HandoffOfferRegistry;
93
+ /** Configures collaborator dependencies. */
94
+ Configure(deps: HandoffOfferRegistryDeps): void;
95
+ /** Stops the periodic background sweep timer (shutdown, tests). */
96
+ StopPeriodicSweep(): void;
97
+ /** Replaces the clock (tests). */
98
+ SetClock(now: () => number): void;
99
+ /** Forgets every cached offer (tests, shutdown). */
100
+ Clear(): void;
101
+ /**
102
+ * Creates a pending offer. Refuses when the person already has {@link MAX_PENDING_OFFERS_PER_USER} pending.
103
+ *
104
+ * @returns The new offer, or `undefined` when the person's pending-offer cap is reached.
105
+ */
106
+ Create(input: CreateOfferInput, timeoutMs?: number): Promise<HandoffOfferRecord | undefined>;
107
+ /** The offer with this id, or `undefined`. */
108
+ Get(offerID: string, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<HandoffOfferRecord | undefined>;
109
+ /** The person's offers that can still be accepted, soonest to expire first. */
110
+ PendingForUser(userID: string, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<HandoffOfferRecord[]>;
111
+ /** The person's offers to show: pending ones, plus ones resolved recently enough to still be retained. */
112
+ ListForUser(userID: string, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<HandoffOfferRecord[]>;
113
+ /** The offers made from one room, any status. */
114
+ ForRoom(roomName: string, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<HandoffOfferRecord[]>;
115
+ /**
116
+ * Moves a pending offer to a terminal status on behalf of the person it was made to. The caller must be the
117
+ * offer's target and the offer must still be pending and unexpired; anything else answers the same way.
118
+ *
119
+ * Compares and sets atomically in a transaction on `Status = 'Pending'` so concurrent requests across
120
+ * multiple instances cannot both succeed.
121
+ */
122
+ ResolveForUser(offerID: string, userID: string, status: 'Accepted' | 'Declined', contextUser?: UserInfo, provider?: IMetadataProvider): Promise<ResolveOfferResult>;
123
+ /**
124
+ * Moves a pending offer to `Expired` or `Cancelled` (a system decision, not the person's).
125
+ *
126
+ * @returns The offer when it was pending and has now changed; `undefined` when it was already resolved or unknown.
127
+ */
128
+ Close(offerID: string, status: 'Expired' | 'Cancelled', contextUser?: UserInfo, provider?: IMetadataProvider): Promise<HandoffOfferRecord | undefined>;
129
+ /**
130
+ * Sweeps expired pending offers and purges retained resolved offers.
131
+ *
132
+ * @returns How many pending offers were expired by this sweep.
133
+ */
134
+ Sweep(contextUser?: UserInfo, provider?: IMetadataProvider): Promise<number>;
135
+ private sweep;
136
+ private finish;
137
+ private entityToRecord;
138
+ private notifyPublisher;
139
+ private warnedNoUser;
140
+ /** The caller's user, else the configured server identity, else the lazily resolved one. */
141
+ private resolveUser;
142
+ /** Logs once (not per tick) that database queries are being skipped because no user identity is available yet. */
143
+ private noteNoUser;
144
+ private getProvider;
145
+ private getRunView;
146
+ }
147
+ //# sourceMappingURL=handoff-offer-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-offer-registry.d.ts","sourceRoot":"","sources":["../../src/room-handoff/handoff-offer-registry.ts"],"names":[],"mappings":"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"}