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

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 (78) hide show
  1. package/README.md +85 -0
  2. package/dist/index.d.ts +17 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +17 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/livekit-agent-room-coordinator.d.ts +105 -8
  7. package/dist/livekit-agent-room-coordinator.d.ts.map +1 -1
  8. package/dist/livekit-agent-room-coordinator.js +125 -25
  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 +20 -0
  17. package/dist/livekit-native-module.js.map +1 -0
  18. package/dist/livekit-sip-service.d.ts +141 -0
  19. package/dist/livekit-sip-service.d.ts.map +1 -0
  20. package/dist/livekit-sip-service.js +169 -0
  21. package/dist/livekit-sip-service.js.map +1 -0
  22. package/dist/livekit-token-service.d.ts +6 -0
  23. package/dist/livekit-token-service.d.ts.map +1 -1
  24. package/dist/livekit-token-service.js +8 -0
  25. package/dist/livekit-token-service.js.map +1 -1
  26. package/dist/livekit-webhook.d.ts +65 -0
  27. package/dist/livekit-webhook.d.ts.map +1 -0
  28. package/dist/livekit-webhook.js +65 -0
  29. package/dist/livekit-webhook.js.map +1 -0
  30. package/dist/meeting-dial-in-service.d.ts +75 -0
  31. package/dist/meeting-dial-in-service.d.ts.map +1 -0
  32. package/dist/meeting-dial-in-service.js +175 -0
  33. package/dist/meeting-dial-in-service.js.map +1 -0
  34. package/dist/room-audio/audio-decoder.d.ts +40 -0
  35. package/dist/room-audio/audio-decoder.d.ts.map +1 -0
  36. package/dist/room-audio/audio-decoder.js +205 -0
  37. package/dist/room-audio/audio-decoder.js.map +1 -0
  38. package/dist/room-audio/comfort-tone.d.ts +23 -0
  39. package/dist/room-audio/comfort-tone.d.ts.map +1 -0
  40. package/dist/room-audio/comfort-tone.js +70 -0
  41. package/dist/room-audio/comfort-tone.js.map +1 -0
  42. package/dist/room-audio/pcm-frame-pump.d.ts +134 -0
  43. package/dist/room-audio/pcm-frame-pump.d.ts.map +1 -0
  44. package/dist/room-audio/pcm-frame-pump.js +253 -0
  45. package/dist/room-audio/pcm-frame-pump.js.map +1 -0
  46. package/dist/room-audio/room-audio-decode-cache.d.ts +37 -0
  47. package/dist/room-audio/room-audio-decode-cache.d.ts.map +1 -0
  48. package/dist/room-audio/room-audio-decode-cache.js +75 -0
  49. package/dist/room-audio/room-audio-decode-cache.js.map +1 -0
  50. package/dist/room-audio/room-audio-player.d.ts +178 -0
  51. package/dist/room-audio/room-audio-player.d.ts.map +1 -0
  52. package/dist/room-audio/room-audio-player.js +404 -0
  53. package/dist/room-audio/room-audio-player.js.map +1 -0
  54. package/dist/room-audio/room-audio-sources.d.ts +47 -0
  55. package/dist/room-audio/room-audio-sources.d.ts.map +1 -0
  56. package/dist/room-audio/room-audio-sources.js +116 -0
  57. package/dist/room-audio/room-audio-sources.js.map +1 -0
  58. package/dist/room-audio/room-speech-synthesizer.d.ts +55 -0
  59. package/dist/room-audio/room-speech-synthesizer.d.ts.map +1 -0
  60. package/dist/room-audio/room-speech-synthesizer.js +56 -0
  61. package/dist/room-audio/room-speech-synthesizer.js.map +1 -0
  62. package/dist/room-authorization.d.ts +58 -0
  63. package/dist/room-authorization.d.ts.map +1 -0
  64. package/dist/room-authorization.js +212 -0
  65. package/dist/room-authorization.js.map +1 -0
  66. package/dist/room-handoff/handoff-offer-registry.d.ts +137 -0
  67. package/dist/room-handoff/handoff-offer-registry.d.ts.map +1 -0
  68. package/dist/room-handoff/handoff-offer-registry.js +521 -0
  69. package/dist/room-handoff/handoff-offer-registry.js.map +1 -0
  70. package/dist/room-handoff/handoff-types.d.ts +170 -0
  71. package/dist/room-handoff/handoff-types.d.ts.map +1 -0
  72. package/dist/room-handoff/handoff-types.js +16 -0
  73. package/dist/room-handoff/handoff-types.js.map +1 -0
  74. package/dist/room-handoff/room-handoff-engine.d.ts +141 -0
  75. package/dist/room-handoff/room-handoff-engine.d.ts.map +1 -0
  76. package/dist/room-handoff/room-handoff-engine.js +530 -0
  77. package/dist/room-handoff/room-handoff-engine.js.map +1 -0
  78. package/package.json +14 -9
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-audio-sources.d.ts","sourceRoot":"","sources":["../../src/room-audio/room-audio-sources.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAsB,KAAK,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAKjG,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC,wBAAwB;IACxB,KAAK,EAAE,UAAU,CAAC;IAClB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qGAAqG;AACrG,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEzI,mGAAmG;AACnG,MAAM,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEjF,8EAA8E;AAC9E,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AAEzD,sEAAsE;AACtE,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAoBxI;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAWpD;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAclF"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @fileoverview Where the room audio player gets an audio file's bytes: an `MJ: Files` row (read from its storage
3
+ * provider through `@memberjunction/storage`), or an https URL (fetched through the SSRF-safe `SafeFetch`, with a
4
+ * timeout and a byte cap). Both return {@link RoomAudioFileContent}, which `DecodeAudio` turns into PCM.
5
+ *
6
+ * @module @memberjunction/livekit-room-server
7
+ */
8
+ import { LogError, Metadata } from '@memberjunction/core';
9
+ import { FileStorageEngine } from '@memberjunction/storage';
10
+ import { SafeFetch } from '@memberjunction/network-utils';
11
+ /** The largest file the player will download from a URL, in bytes (25 MB). */
12
+ export const MAX_ROOM_AUDIO_URL_BYTES = 25 * 1024 * 1024;
13
+ /** How long a URL download may take before it is abandoned, in ms. */
14
+ export const ROOM_AUDIO_URL_TIMEOUT_MS = 20_000;
15
+ /**
16
+ * Reads an `MJ: Files` row's bytes from its storage provider, as the given user (so the user's read permission on
17
+ * the file applies).
18
+ *
19
+ * @throws {Error} when the row cannot be loaded, no storage account serves its provider, or the download fails.
20
+ */
21
+ export async function ReadMJFileAudio(fileID, contextUser, provider) {
22
+ const md = provider ?? Metadata.Provider;
23
+ const file = await md.GetEntityObject('MJ: Files', contextUser);
24
+ const loaded = await file.Load(fileID);
25
+ if (!loaded) {
26
+ const detail = file.LatestResult?.CompleteMessage || 'not found, or not readable by this user';
27
+ throw new Error(`Hold audio file ${fileID} could not be loaded from MJ: Files: ${detail}.`);
28
+ }
29
+ await FileStorageEngine.Instance.Config(false, contextUser, provider);
30
+ const accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
31
+ if (accounts.length === 0) {
32
+ throw new Error(`Hold audio file ${fileID} ('${file.Name}') is on storage provider ${file.Provider}, which has no configured storage account.`);
33
+ }
34
+ const driver = await FileStorageEngine.Instance.GetDriver(accounts[0].ID, contextUser);
35
+ const buffer = await driver.GetObject({ fullPath: file.ProviderKey ?? file.Name });
36
+ return {
37
+ Bytes: new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength),
38
+ MimeType: file.ContentType ?? undefined,
39
+ FileName: file.Name,
40
+ };
41
+ }
42
+ /**
43
+ * Checks that `url` is an absolute https URL. The player refuses plain http: hold audio is fetched by the server, and
44
+ * a cleartext fetch would let anyone on the path substitute what callers hear.
45
+ *
46
+ * @throws {Error} when the URL is malformed or not https.
47
+ */
48
+ export function AssertHttpsAudioUrl(url) {
49
+ let parsed;
50
+ try {
51
+ parsed = new URL(url);
52
+ }
53
+ catch {
54
+ throw new Error(`Hold audio URL is not a valid URL: '${url}'.`);
55
+ }
56
+ if (parsed.protocol !== 'https:') {
57
+ throw new Error(`Hold audio URL must use https; got '${parsed.protocol}' in '${url}'.`);
58
+ }
59
+ return parsed;
60
+ }
61
+ /**
62
+ * Downloads an https audio URL through `SafeFetch` (every redirect hop re-checked against private/reserved
63
+ * addresses), abandoning it after {@link ROOM_AUDIO_URL_TIMEOUT_MS} or once it passes {@link MAX_ROOM_AUDIO_URL_BYTES}.
64
+ *
65
+ * @throws {Error} on a non-https URL, an HTTP error status, a timeout, or a body over the byte cap.
66
+ */
67
+ export async function FetchRoomAudioUrl(url) {
68
+ const parsed = AssertHttpsAudioUrl(url);
69
+ const response = await SafeFetch(parsed.href, { signal: AbortSignal.timeout(ROOM_AUDIO_URL_TIMEOUT_MS) });
70
+ if (!response.ok) {
71
+ void response.body?.cancel();
72
+ throw new Error(`Hold audio URL returned HTTP ${response.status}: '${url}'.`);
73
+ }
74
+ const declared = Number(response.headers.get('content-length') ?? '');
75
+ if (Number.isFinite(declared) && declared > MAX_ROOM_AUDIO_URL_BYTES) {
76
+ void response.body?.cancel();
77
+ throw new Error(`Hold audio URL is ${declared} bytes; the limit is ${MAX_ROOM_AUDIO_URL_BYTES} bytes: '${url}'.`);
78
+ }
79
+ const bytes = await readBodyWithCap(response, url);
80
+ return { Bytes: bytes, MimeType: response.headers.get('content-type') ?? undefined, FileName: parsed.pathname.split('/').pop() };
81
+ }
82
+ /** Reads a response body, stopping (and cancelling the stream) the moment it passes the byte cap. */
83
+ async function readBodyWithCap(response, url) {
84
+ if (!response.body) {
85
+ return new Uint8Array(0);
86
+ }
87
+ const reader = response.body.getReader();
88
+ const chunks = [];
89
+ let total = 0;
90
+ try {
91
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
92
+ total += chunk.value.byteLength;
93
+ if (total > MAX_ROOM_AUDIO_URL_BYTES) {
94
+ throw new Error(`Hold audio URL exceeded the ${MAX_ROOM_AUDIO_URL_BYTES}-byte limit: '${url}'.`);
95
+ }
96
+ chunks.push(chunk.value);
97
+ }
98
+ }
99
+ finally {
100
+ if (total > MAX_ROOM_AUDIO_URL_BYTES) {
101
+ await reader.cancel().catch((err) => LogError(`[RoomAudioPlayer] cancelling the oversize download of '${url}' failed: ${err.message}`));
102
+ }
103
+ reader.releaseLock();
104
+ }
105
+ return concatChunks(chunks, total);
106
+ }
107
+ function concatChunks(chunks, total) {
108
+ const out = new Uint8Array(total);
109
+ let offset = 0;
110
+ for (const chunk of chunks) {
111
+ out.set(chunk, offset);
112
+ offset += chunk.byteLength;
113
+ }
114
+ return out;
115
+ }
116
+ //# sourceMappingURL=room-audio-sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-audio-sources.js","sourceRoot":"","sources":["../../src/room-audio/room-audio-sources.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
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 { type UserInfo } from '@memberjunction/core';
16
+ import { AITextToSpeechRunner } from '@memberjunction/ai-prompts';
17
+ import { type DecodedAudio } from './audio-decoder.js';
18
+ /** Turns announcement text into audio. Return `null` when speech could not be produced (and log why). */
19
+ export interface IRoomSpeechSynthesizer {
20
+ /**
21
+ * Speaks `text`.
22
+ *
23
+ * @param text What to say.
24
+ * @param contextUser The user the synthesis runs (and is logged) as.
25
+ * @returns The audio, or `null` when it could not be produced.
26
+ */
27
+ Synthesize(text: string, contextUser: UserInfo): Promise<DecodedAudio | null>;
28
+ }
29
+ /** Options for {@link MJRoomSpeechSynthesizer}. */
30
+ export interface MJRoomSpeechSynthesizerOptions {
31
+ /** The provider-native voice id or name (required: voices belong to a vendor). */
32
+ Voice: string;
33
+ /** Pins the TTS model by ID. Recommended whenever the voice matters, since voices are vendor-specific. */
34
+ ModelID?: string;
35
+ /** The carrier `MJ: AI Prompts` row whose bindings choose the model. Default: the runner's `Default Text To Speech`. */
36
+ PromptID?: string;
37
+ /** The vendor's output format string (e.g. `mp3_44100_128`). Default: the vendor's default (MP3 or WAV decode either way). */
38
+ OutputFormat?: string;
39
+ }
40
+ /** The part of `AITextToSpeechRunner` the synthesizer uses — a seam so tests inject a fake. */
41
+ export type TextToSpeechRunnerLike = Pick<AITextToSpeechRunner, 'RunTextToSpeech'>;
42
+ /** An {@link IRoomSpeechSynthesizer} backed by MJ's `AITextToSpeechRunner`. Never throws: failures log and return `null`. */
43
+ export declare class MJRoomSpeechSynthesizer implements IRoomSpeechSynthesizer {
44
+ private readonly options;
45
+ private readonly runner;
46
+ /**
47
+ * @param options The voice and model selection.
48
+ * @param runner The runner (tests); defaults to a new `AITextToSpeechRunner`.
49
+ * @throws {Error} when no voice is given.
50
+ */
51
+ constructor(options: MJRoomSpeechSynthesizerOptions, runner?: TextToSpeechRunnerLike);
52
+ /** Speaks `text` through the configured TTS model and decodes the result to PCM. */
53
+ Synthesize(text: string, contextUser: UserInfo): Promise<DecodedAudio | null>;
54
+ }
55
+ //# sourceMappingURL=room-speech-synthesizer.d.ts.map
@@ -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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"}