@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
@@ -10,6 +10,14 @@
10
10
  * @module @memberjunction/livekit-room-server
11
11
  */
12
12
  import { AccessToken } from 'livekit-server-sdk';
13
+ /**
14
+ * The LiveKit participant identity a signed-in MJ user joins a room with. Everything that mints a token for a user, and
15
+ * everything that later looks for that user in a room (the handoff engine watching for a person to appear), derives it
16
+ * here so the two can never disagree.
17
+ */
18
+ export function LiveKitUserIdentity(userID) {
19
+ return `user-${userID}`.toLowerCase();
20
+ }
13
21
  /** Mints scoped LiveKit access tokens. Stateless apart from its resolved credentials. */
14
22
  export class LiveKitTokenService {
15
23
  /**
@@ -39,12 +47,13 @@ export class LiveKitTokenService {
39
47
  * @throws {Error} when credentials are not configured.
40
48
  */
41
49
  async MintToken(params) {
42
- this.assertConfigured();
50
+ this.AssertConfigured();
43
51
  const at = new AccessToken(this.config.ApiKey, this.config.ApiSecret, {
44
52
  identity: params.Identity,
45
53
  name: params.DisplayName,
46
54
  ttl: params.TtlSeconds ?? 3600,
47
55
  metadata: this.buildMetadata(params),
56
+ attributes: params.Attributes,
48
57
  });
49
58
  at.addGrant(this.buildGrant(params));
50
59
  const token = await at.toJwt();
@@ -63,16 +72,18 @@ export class LiveKitTokenService {
63
72
  }
64
73
  /**
65
74
  * Mints a token for the AGENT bot (publish + subscribe + data, `agent` role metadata so the UI badges
66
- * it correctly). The bridge passes this in its session `Configuration.AccessToken`.
75
+ * it correctly, and leave to update its own attributes, so it can say when its avatar was taken down). The bridge
76
+ * passes this in its session `Configuration.AccessToken`.
67
77
  *
68
78
  * @param roomName The room to join.
69
79
  * @param identity The bot identity.
70
80
  * @param displayName The bot display name.
81
+ * @param attributes Participant attributes the bot carries from the moment it joins (e.g. `mj.agentWatches`).
71
82
  */
72
- MintBotToken(roomName, identity, displayName) {
73
- return this.MintToken({ RoomName: roomName, Identity: identity, DisplayName: displayName, Role: 'agent' });
83
+ MintBotToken(roomName, identity, displayName, attributes) {
84
+ return this.MintToken({ RoomName: roomName, Identity: identity, DisplayName: displayName, Role: 'agent', Attributes: attributes, CanUpdateOwnMetadata: true });
74
85
  }
75
- /** Builds the LiveKit video grant from the params (defaults: publish + subscribe + data all on). */
86
+ /** Builds the LiveKit video grant from the params (defaults: publish + subscribe + data all on; own metadata off). */
76
87
  buildGrant(params) {
77
88
  return {
78
89
  roomJoin: true,
@@ -80,14 +91,18 @@ export class LiveKitTokenService {
80
91
  canPublish: params.CanPublish ?? true,
81
92
  canSubscribe: params.CanSubscribe ?? true,
82
93
  canPublishData: params.CanPublishData ?? true,
94
+ ...(params.CanUpdateOwnMetadata ? { canUpdateOwnMetadata: true } : {}),
83
95
  };
84
96
  }
85
97
  /** Builds the participant metadata JSON string, always stamping the role for UI resolution. */
86
98
  buildMetadata(params) {
87
99
  return JSON.stringify({ mjRole: params.Role ?? 'participant', ...(params.Metadata ?? {}) });
88
100
  }
89
- /** Throws a clear error when credentials are missing. */
90
- assertConfigured() {
101
+ /**
102
+ * Throws a clear error when credentials are missing. Public so a caller that must fail before doing costly work (the
103
+ * room coordinator, before it opens a model session) gets the same error minting would.
104
+ */
105
+ AssertConfigured() {
91
106
  if (!this.IsConfigured) {
92
107
  throw new Error('LiveKitTokenService is not configured. Set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET ' +
93
108
  '(or pass them to the constructor) before minting tokens.');
@@ -1 +1 @@
1
- {"version":3,"file":"livekit-token-service.js","sourceRoot":"","sources":["../src/livekit-token-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"livekit-token-service.js","sourceRoot":"","sources":["../src/livekit-token-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @fileoverview {@link LiveKitWebhookParser}: verifies and reads LiveKit's server webhooks.
3
+ *
4
+ * LiveKit posts room and participant events (`room_started`, `participant_joined`, `participant_left`, …) to a URL
5
+ * configured on the project, signed with the project's API secret. For phone calls the event MJ acts on is a **SIP
6
+ * participant joining a room**: it is how MJ learns that a call arrived and which number was dialed. This parser checks
7
+ * the signature (a forged event must never make MJ start an agent) and reduces the SDK's protobuf event to a plain value.
8
+ *
9
+ * @module @memberjunction/livekit-room-server
10
+ */
11
+ import { type LiveKitServerConfig } from './livekit-token-service.js';
12
+ /** A LiveKit webhook event reduced to what MJ looks at. */
13
+ export interface LiveKitRoomWebhookEvent {
14
+ /** The event name, e.g. `participant_joined`. */
15
+ Event: string;
16
+ RoomName: string;
17
+ ParticipantIdentity: string;
18
+ /** True when the participant is a phone (SIP) participant. */
19
+ IsSipParticipant: boolean;
20
+ /** The caller's number, for a SIP participant (`sip.phoneNumber`). */
21
+ CallerNumber?: string;
22
+ /** The number that was dialed, for a SIP participant (`sip.trunkPhoneNumber`). */
23
+ DialedNumber?: string;
24
+ /** LiveKit's call id, for a SIP participant. */
25
+ SipCallID?: string;
26
+ }
27
+ /** The part of the SDK receiver the parser uses (so tests inject a fake). */
28
+ export interface WebhookReceiverLike {
29
+ receive(body: string, authHeader?: string): Promise<{
30
+ event: string;
31
+ room?: {
32
+ name: string;
33
+ };
34
+ participant?: {
35
+ identity: string;
36
+ kind: number;
37
+ attributes: {
38
+ [key: string]: string;
39
+ };
40
+ };
41
+ }>;
42
+ }
43
+ /** Verifies and parses LiveKit webhooks. */
44
+ export declare class LiveKitWebhookParser {
45
+ private readonly receiver;
46
+ /**
47
+ * @param config Explicit credentials; omitted fields fall back to the `LIVEKIT_*` environment variables.
48
+ * @param receiver Injectable receiver (tests); defaults to the SDK's `WebhookReceiver`.
49
+ * @throws {Error} when no receiver is injected and the API key and secret are not configured.
50
+ */
51
+ constructor(config?: Partial<LiveKitServerConfig>, receiver?: WebhookReceiverLike);
52
+ /**
53
+ * Verifies the request's signature and returns the event.
54
+ *
55
+ * One event is also acted on here, because every LiveKit webhook MJ receives passes through this method: on a
56
+ * verified `room_finished` the room's {@link RoomAudioPlayer} playbacks (hold music) are stopped, so a bot never
57
+ * outlives its room. That cleanup runs in the background and never throws.
58
+ *
59
+ * @param body The raw request body (the signature covers the exact bytes, so do not re-serialise parsed JSON).
60
+ * @param authHeader The request's `Authorization` header.
61
+ * @throws {Error} when the signature is missing or does not verify.
62
+ */
63
+ Parse(body: string, authHeader?: string): Promise<LiveKitRoomWebhookEvent>;
64
+ }
65
+ //# sourceMappingURL=livekit-webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"livekit-webhook.d.ts","sourceRoot":"","sources":["../src/livekit-webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAIxF,2DAA2D;AAC3D,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,gBAAgB,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAClD,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QACxB,WAAW,CAAC,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE;gBAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;aAAE,CAAA;SAAE,CAAC;KACzF,CAAC,CAAC;CACJ;AAED,4CAA4C;AAC5C,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAE/C;;;;OAIG;gBACS,MAAM,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,EAAE,QAAQ,CAAC,EAAE,mBAAmB;IAajF;;;;;;;;;;OAUG;IACU,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;CAmBxF"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @fileoverview {@link LiveKitWebhookParser}: verifies and reads LiveKit's server webhooks.
3
+ *
4
+ * LiveKit posts room and participant events (`room_started`, `participant_joined`, `participant_left`, …) to a URL
5
+ * configured on the project, signed with the project's API secret. For phone calls the event MJ acts on is a **SIP
6
+ * participant joining a room**: it is how MJ learns that a call arrived and which number was dialed. This parser checks
7
+ * the signature (a forged event must never make MJ start an agent) and reduces the SDK's protobuf event to a plain value.
8
+ *
9
+ * @module @memberjunction/livekit-room-server
10
+ */
11
+ import { WebhookReceiver } from 'livekit-server-sdk';
12
+ import { LiveKitTokenService } from './livekit-token-service.js';
13
+ import { LIVEKIT_PARTICIPANT_KIND_SIP, LIVEKIT_SIP_ATTRIBUTES } from './livekit-sip-service.js';
14
+ import { RoomAudioPlayer } from './room-audio/room-audio-player.js';
15
+ /** Verifies and parses LiveKit webhooks. */
16
+ export class LiveKitWebhookParser {
17
+ /**
18
+ * @param config Explicit credentials; omitted fields fall back to the `LIVEKIT_*` environment variables.
19
+ * @param receiver Injectable receiver (tests); defaults to the SDK's `WebhookReceiver`.
20
+ * @throws {Error} when no receiver is injected and the API key and secret are not configured.
21
+ */
22
+ constructor(config, receiver) {
23
+ if (receiver) {
24
+ this.receiver = receiver;
25
+ return;
26
+ }
27
+ const apiKey = config?.ApiKey ?? process.env.LIVEKIT_API_KEY ?? '';
28
+ const apiSecret = config?.ApiSecret ?? process.env.LIVEKIT_API_SECRET ?? '';
29
+ if (!new LiveKitTokenService(config).IsConfigured) {
30
+ throw new Error('LiveKit webhooks cannot be verified: LIVEKIT_URL, LIVEKIT_API_KEY and LIVEKIT_API_SECRET are not all set.');
31
+ }
32
+ this.receiver = new WebhookReceiver(apiKey, apiSecret);
33
+ }
34
+ /**
35
+ * Verifies the request's signature and returns the event.
36
+ *
37
+ * One event is also acted on here, because every LiveKit webhook MJ receives passes through this method: on a
38
+ * verified `room_finished` the room's {@link RoomAudioPlayer} playbacks (hold music) are stopped, so a bot never
39
+ * outlives its room. That cleanup runs in the background and never throws.
40
+ *
41
+ * @param body The raw request body (the signature covers the exact bytes, so do not re-serialise parsed JSON).
42
+ * @param authHeader The request's `Authorization` header.
43
+ * @throws {Error} when the signature is missing or does not verify.
44
+ */
45
+ async Parse(body, authHeader) {
46
+ const event = await this.receiver.receive(body, authHeader);
47
+ const participant = event.participant;
48
+ const isSip = participant?.kind === LIVEKIT_PARTICIPANT_KIND_SIP;
49
+ const attributes = participant?.attributes ?? {};
50
+ const roomName = event.room?.name ?? '';
51
+ if (event.event === 'room_finished' && roomName) {
52
+ void RoomAudioPlayer.Instance.StopAllInRoom(roomName);
53
+ }
54
+ return {
55
+ Event: event.event,
56
+ RoomName: roomName,
57
+ ParticipantIdentity: participant?.identity ?? '',
58
+ IsSipParticipant: isSip,
59
+ CallerNumber: isSip ? attributes[LIVEKIT_SIP_ATTRIBUTES.CallerNumber] : undefined,
60
+ DialedNumber: isSip ? attributes[LIVEKIT_SIP_ATTRIBUTES.DialedNumber] : undefined,
61
+ SipCallID: isSip ? attributes[LIVEKIT_SIP_ATTRIBUTES.CallID] : undefined,
62
+ };
63
+ }
64
+ }
65
+ //# sourceMappingURL=livekit-webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"livekit-webhook.js","sourceRoot":"","sources":["../src/livekit-webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAA4B,MAAM,yBAAyB,CAAC;AACxF,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AA2BjE,4CAA4C;AAC5C,MAAM,OAAO,oBAAoB;IAG/B;;;;OAIG;IACH,YAAY,MAAqC,EAAE,QAA8B;QAC/E,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;YACzB,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,EAAE,CAAC;QACnE,MAAM,SAAS,GAAG,MAAM,EAAE,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC;QAC5E,IAAI,CAAC,IAAI,mBAAmB,CAAC,MAAM,CAAC,CAAC,YAAY,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,2GAA2G,CAAC,CAAC;QAC/H,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACzD,CAAC;IAED;;;;;;;;;;OAUG;IACI,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,UAAmB;QAClD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QAC5D,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;QACtC,MAAM,KAAK,GAAG,WAAW,EAAE,IAAI,KAAK,4BAA4B,CAAC;QACjE,MAAM,UAAU,GAAG,WAAW,EAAE,UAAU,IAAI,EAAE,CAAC;QACjD,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC;QACxC,IAAI,KAAK,CAAC,KAAK,KAAK,eAAe,IAAI,QAAQ,EAAE,CAAC;YAChD,KAAK,eAAe,CAAC,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACxD,CAAC;QACD,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,QAAQ,EAAE,QAAQ;YAClB,mBAAmB,EAAE,WAAW,EAAE,QAAQ,IAAI,EAAE;YAChD,gBAAgB,EAAE,KAAK;YACvB,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,sBAAsB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS;YACjF,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,sBAAsB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS;YACjF,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;SACzE,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @fileoverview Server-side service for verifying and generating phone dial-in codes for meetings.
3
+ *
4
+ * Security architecture:
5
+ * - Dial-in codes are NOT verified through a public GraphQL mutation.
6
+ * - Verification is performed server-side by the LiveKit SIP inbound path upon receiving verified webhooks.
7
+ * - Dual rate-limiting:
8
+ * 1. Keyed on the caller's verified phone number (`CallerNumber`).
9
+ * 2. Keyed on the dialed trunk number (`DialedNumber`) to prevent distributed brute-forcing against a single line.
10
+ * - Code generation uses `crypto.randomInt` (cryptographically secure pseudo-random number generator), never `Math.random`.
11
+ *
12
+ * @module @memberjunction/livekit-room-server
13
+ */
14
+ import { BaseSingleton } from '@memberjunction/global';
15
+ import { type IMetadataProvider, type UserInfo } from '@memberjunction/core';
16
+ /** Rate limiter with configurable attempt limits and lockout windows. */
17
+ export declare class DialInRateLimiter {
18
+ private readonly maxAttempts;
19
+ private readonly windowMs;
20
+ private readonly lockoutMs;
21
+ private readonly attempts;
22
+ constructor(maxAttempts?: number, windowMs?: number, lockoutMs?: number);
23
+ CheckAllowed(key: string): {
24
+ allowed: boolean;
25
+ remainingMs?: number;
26
+ };
27
+ /** Above this many tracked keys, RecordFailure sweeps expired entries (keys are caller-supplied, so unbounded otherwise). */
28
+ private static readonly sweepThreshold;
29
+ /** Hard ceiling; if a sweep can't get below it (a flood of live entries), the oldest entries are dropped. */
30
+ private static readonly maxEntries;
31
+ /** Removes entries whose window and lockout have both elapsed. */
32
+ private sweepExpired;
33
+ /** Number of tracked keys (diagnostics/tests). */
34
+ get Size(): number;
35
+ RecordFailure(key: string): void;
36
+ RecordSuccess(key: string): void;
37
+ Clear(): void;
38
+ }
39
+ /** Input parameters for verifying a dial-in code. */
40
+ export interface VerifyDialInCodeParams {
41
+ /** The verified caller number from the SIP carrier webhook. */
42
+ CallerNumber: string;
43
+ /** The dialed phone number or Phone Numbers row ID. */
44
+ DialedNumber: string;
45
+ /** The DTMF dial-in code entered by the caller. */
46
+ DialInCode: string;
47
+ /** Optional authenticated context user. */
48
+ ContextUser?: UserInfo;
49
+ /** Optional metadata provider. */
50
+ Provider?: IMetadataProvider;
51
+ }
52
+ /** Result of dial-in code verification. */
53
+ export interface VerifyDialInCodeOutcome {
54
+ Success: boolean;
55
+ ErrorMessage?: string;
56
+ RoomName?: string;
57
+ MeetingID?: string;
58
+ }
59
+ /**
60
+ * Server-side singleton service for meeting dial-in verification and code generation.
61
+ */
62
+ export declare class MeetingDialInService extends BaseSingleton<MeetingDialInService> {
63
+ private readonly callerLimiter;
64
+ private readonly dialedLimiter;
65
+ constructor();
66
+ static get Instance(): MeetingDialInService;
67
+ /**
68
+ * Generates a cryptographically secure 6-digit numeric dial-in code.
69
+ */
70
+ GenerateDialInCode(): string;
71
+ /**
72
+ * Generates a unique 6-digit dial-in code for a given phone number among active/scheduled meetings.
73
+ */
74
+ GenerateUniqueDialInCode(phoneNumberID: string, user: UserInfo, provider?: IMetadataProvider): Promise<string | undefined>;
75
+ /**
76
+ * Verifies a DTMF dial-in code against active/scheduled meetings on the dialed line.
77
+ * Rate limits on both the verified caller number and the dialed number.
78
+ */
79
+ VerifyDialInCode(params: VerifyDialInCodeParams): Promise<VerifyDialInCodeOutcome>;
80
+ /** Clears rate limiter entries (test harness). */
81
+ ClearRateLimitsForTesting(): void;
82
+ }
83
+ //# sourceMappingURL=meeting-dial-in-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meeting-dial-in-service.d.ts","sourceRoot":"","sources":["../src/meeting-dial-in-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,aAAa,EAAmB,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAqB,KAAK,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAUhG,yEAAyE;AACzE,qBAAa,iBAAiB;IAI1B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAL5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;gBAG3C,WAAW,GAAE,MAAU,EACvB,QAAQ,GAAE,MAAuB,EACjC,SAAS,GAAE,MAAuB;IAG9C,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;IAoB5E,6HAA6H;IAC7H,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAQ;IAC9C,6GAA6G;IAC7G,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAS;IAE3C,kEAAkE;IAClE,OAAO,CAAC,YAAY;IAgBpB,kDAAkD;IAClD,IAAW,IAAI,IAAI,MAAM,CAExB;IAEM,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAgBhC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIhC,KAAK,IAAI,IAAI;CAGrB;AAED,qDAAqD;AACrD,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,2CAA2C;AAC3C,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,aAAa,CAAC,oBAAoB,CAAC;IAE3E,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4D;IAE1F,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6D;;IAM3F,WAAkB,QAAQ,IAAI,oBAAoB,CAEjD;IAED;;OAEG;IACI,kBAAkB,IAAI,MAAM;IAInC;;OAEG;IACU,wBAAwB,CACnC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,QAAQ,EACd,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAqB9B;;;OAGG;IACU,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IA0E/F,kDAAkD;IAC3C,yBAAyB,IAAI,IAAI;CAIzC"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @fileoverview Server-side service for verifying and generating phone dial-in codes for meetings.
3
+ *
4
+ * Security architecture:
5
+ * - Dial-in codes are NOT verified through a public GraphQL mutation.
6
+ * - Verification is performed server-side by the LiveKit SIP inbound path upon receiving verified webhooks.
7
+ * - Dual rate-limiting:
8
+ * 1. Keyed on the caller's verified phone number (`CallerNumber`).
9
+ * 2. Keyed on the dialed trunk number (`DialedNumber`) to prevent distributed brute-forcing against a single line.
10
+ * - Code generation uses `crypto.randomInt` (cryptographically secure pseudo-random number generator), never `Math.random`.
11
+ *
12
+ * @module @memberjunction/livekit-room-server
13
+ */
14
+ import { randomInt } from 'node:crypto';
15
+ import { BaseSingleton, EscapeSQLString } from '@memberjunction/global';
16
+ import { LogError, RunView } from '@memberjunction/core';
17
+ /** Rate limiter with configurable attempt limits and lockout windows. */
18
+ export class DialInRateLimiter {
19
+ constructor(maxAttempts = 5, windowMs = 15 * 60 * 1000, lockoutMs = 15 * 60 * 1000) {
20
+ this.maxAttempts = maxAttempts;
21
+ this.windowMs = windowMs;
22
+ this.lockoutMs = lockoutMs;
23
+ this.attempts = new Map();
24
+ }
25
+ CheckAllowed(key) {
26
+ const entry = this.attempts.get(key);
27
+ if (!entry) {
28
+ return { allowed: true };
29
+ }
30
+ const now = Date.now();
31
+ if (entry.lockedUntil && entry.lockedUntil > now) {
32
+ return { allowed: false, remainingMs: entry.lockedUntil - now };
33
+ }
34
+ if (now - entry.firstAttemptAt > this.windowMs) {
35
+ this.attempts.delete(key);
36
+ return { allowed: true };
37
+ }
38
+ if (entry.attempts >= this.maxAttempts) {
39
+ entry.lockedUntil = now + this.lockoutMs;
40
+ return { allowed: false, remainingMs: this.lockoutMs };
41
+ }
42
+ return { allowed: true };
43
+ }
44
+ /** Above this many tracked keys, RecordFailure sweeps expired entries (keys are caller-supplied, so unbounded otherwise). */
45
+ static { this.sweepThreshold = 1000; }
46
+ /** Hard ceiling; if a sweep can't get below it (a flood of live entries), the oldest entries are dropped. */
47
+ static { this.maxEntries = 10000; }
48
+ /** Removes entries whose window and lockout have both elapsed. */
49
+ sweepExpired(now) {
50
+ for (const [key, entry] of this.attempts) {
51
+ const lockExpired = !entry.lockedUntil || entry.lockedUntil <= now;
52
+ if (lockExpired && now - entry.firstAttemptAt > this.windowMs) {
53
+ this.attempts.delete(key);
54
+ }
55
+ }
56
+ // Map iterates in insertion order, so the first keys are the oldest. Leave room for the entry about to be inserted.
57
+ for (const key of this.attempts.keys()) {
58
+ if (this.attempts.size < DialInRateLimiter.maxEntries) {
59
+ break;
60
+ }
61
+ this.attempts.delete(key);
62
+ }
63
+ }
64
+ /** Number of tracked keys (diagnostics/tests). */
65
+ get Size() {
66
+ return this.attempts.size;
67
+ }
68
+ RecordFailure(key) {
69
+ const now = Date.now();
70
+ if (this.attempts.size >= DialInRateLimiter.sweepThreshold) {
71
+ this.sweepExpired(now);
72
+ }
73
+ const entry = this.attempts.get(key);
74
+ if (!entry || now - entry.firstAttemptAt > this.windowMs) {
75
+ this.attempts.set(key, { attempts: 1, firstAttemptAt: now });
76
+ }
77
+ else {
78
+ entry.attempts++;
79
+ if (entry.attempts >= this.maxAttempts) {
80
+ entry.lockedUntil = now + this.lockoutMs;
81
+ }
82
+ }
83
+ }
84
+ RecordSuccess(key) {
85
+ this.attempts.delete(key);
86
+ }
87
+ Clear() {
88
+ this.attempts.clear();
89
+ }
90
+ }
91
+ /**
92
+ * Server-side singleton service for meeting dial-in verification and code generation.
93
+ */
94
+ export class MeetingDialInService extends BaseSingleton {
95
+ constructor() {
96
+ super();
97
+ // Caller-based limiter: 5 attempts per 15 minutes
98
+ this.callerLimiter = new DialInRateLimiter(5, 15 * 60 * 1000, 15 * 60 * 1000);
99
+ // Dialed-number limiter: 20 failed attempts per 15 minutes across all callers
100
+ this.dialedLimiter = new DialInRateLimiter(20, 15 * 60 * 1000, 15 * 60 * 1000);
101
+ }
102
+ static get Instance() {
103
+ return MeetingDialInService.getInstance();
104
+ }
105
+ /**
106
+ * Generates a cryptographically secure 6-digit numeric dial-in code.
107
+ */
108
+ GenerateDialInCode() {
109
+ return randomInt(100000, 1000000).toString();
110
+ }
111
+ /**
112
+ * Generates a unique 6-digit dial-in code for a given phone number among active/scheduled meetings.
113
+ */
114
+ async GenerateUniqueDialInCode(phoneNumberID, user, provider) {
115
+ const rv = provider ? RunView.FromMetadataProvider(provider) : new RunView();
116
+ const escapedPhoneID = EscapeSQLString(phoneNumberID);
117
+ for (let attempt = 0; attempt < 10; attempt++) {
118
+ const candidate = this.GenerateDialInCode();
119
+ const res = await rv.RunView({
120
+ EntityName: 'MJ: Meetings',
121
+ ExtraFilter: `DialInPhoneNumberID = '${escapedPhoneID}' AND DialInCode = '${candidate}' AND Status IN ('Live', 'Scheduled')`,
122
+ ResultType: 'entity_object',
123
+ }, user);
124
+ if (res.Success && (!res.Results || res.Results.length === 0)) {
125
+ return candidate;
126
+ }
127
+ }
128
+ return undefined;
129
+ }
130
+ /**
131
+ * Verifies a DTMF dial-in code against active/scheduled meetings on the dialed line.
132
+ * Rate limits on both the verified caller number and the dialed number.
133
+ */
134
+ async VerifyDialInCode(params) {
135
+ try {
136
+ const callerNumber = (params.CallerNumber ?? '').trim().toLowerCase();
137
+ const dialedNumber = (params.DialedNumber ?? '').trim();
138
+ const dialInCode = (params.DialInCode ?? '').trim();
139
+ if (!callerNumber) {
140
+ return { Success: false, ErrorMessage: 'Caller number is required.' };
141
+ }
142
+ if (!dialedNumber) {
143
+ return { Success: false, ErrorMessage: 'Dialed number is required.' };
144
+ }
145
+ if (!dialInCode) {
146
+ return { Success: false, ErrorMessage: 'Dial-in code is required.' };
147
+ }
148
+ // Check caller-based rate limit
149
+ const callerLimit = this.callerLimiter.CheckAllowed(callerNumber);
150
+ if (!callerLimit.allowed) {
151
+ const remainingSeconds = Math.ceil((callerLimit.remainingMs ?? 0) / 1000);
152
+ return {
153
+ Success: false,
154
+ ErrorMessage: `Too many invalid attempts from this caller. Please try again in ${remainingSeconds} seconds.`,
155
+ };
156
+ }
157
+ // Check dialed-number-based rate limit
158
+ const dialedKey = dialedNumber.toLowerCase();
159
+ const dialedLimit = this.dialedLimiter.CheckAllowed(dialedKey);
160
+ if (!dialedLimit.allowed) {
161
+ const remainingSeconds = Math.ceil((dialedLimit.remainingMs ?? 0) / 1000);
162
+ return {
163
+ Success: false,
164
+ ErrorMessage: `Too many invalid attempts on this line. Please try again in ${remainingSeconds} seconds.`,
165
+ };
166
+ }
167
+ const escapedDialed = EscapeSQLString(dialedNumber);
168
+ const escapedCode = EscapeSQLString(dialInCode);
169
+ const rv = params.Provider ? RunView.FromMetadataProvider(params.Provider) : new RunView();
170
+ const filter = `(DialInPhoneNumberID = '${escapedDialed}' OR DialInPhoneNumber = '${escapedDialed}') AND DialInCode = '${escapedCode}' AND Status IN ('Live', 'Scheduled')`;
171
+ const result = await rv.RunView({
172
+ EntityName: 'MJ: Meetings',
173
+ ExtraFilter: filter,
174
+ ResultType: 'entity_object',
175
+ }, params.ContextUser);
176
+ if (!result.Success || !result.Results || result.Results.length === 0) {
177
+ this.callerLimiter.RecordFailure(callerNumber);
178
+ this.dialedLimiter.RecordFailure(dialedKey);
179
+ return { Success: false, ErrorMessage: 'Invalid dial-in code.' };
180
+ }
181
+ this.callerLimiter.RecordSuccess(callerNumber);
182
+ this.dialedLimiter.RecordSuccess(dialedKey);
183
+ const meeting = result.Results[0];
184
+ return {
185
+ Success: true,
186
+ RoomName: meeting.RoomName,
187
+ MeetingID: meeting.ID,
188
+ };
189
+ }
190
+ catch (error) {
191
+ const msg = error instanceof Error ? error.message : String(error);
192
+ LogError(`[MeetingDialInService] VerifyDialInCode exception: ${msg}`);
193
+ return { Success: false, ErrorMessage: msg };
194
+ }
195
+ }
196
+ /** Clears rate limiter entries (test harness). */
197
+ ClearRateLimitsForTesting() {
198
+ this.callerLimiter.Clear();
199
+ this.dialedLimiter.Clear();
200
+ }
201
+ }
202
+ //# sourceMappingURL=meeting-dial-in-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meeting-dial-in-service.js","sourceRoot":"","sources":["../src/meeting-dial-in-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @fileoverview {@link DecodeAudio} — turns an audio file's bytes (WAV or MP3) into mono PCM16, the one shape the
3
+ * room audio player plays. WAV is parsed here (RIFF chunk walk, integer PCM 8/16/24/32-bit and IEEE float 32/64-bit,
4
+ * any channel count downmixed); MP3 is decoded by `mpg123-decoder`, a WebAssembly build of libmpg123 with no native
5
+ * build step. Anything else is refused with an error that names the format it saw.
6
+ *
7
+ * @module @memberjunction/livekit-room-server
8
+ */
9
+ /** Decoded audio: mono 16-bit PCM and its sample rate. */
10
+ export interface DecodedAudio {
11
+ /** Mono PCM16 samples. */
12
+ Pcm: Int16Array;
13
+ /** The rate of {@link Pcm}, in Hz. */
14
+ SampleRate: number;
15
+ }
16
+ /** What the caller knows about the bytes, used only when the bytes themselves do not say what they are. */
17
+ export interface AudioFormatHint {
18
+ /** The MIME type (e.g. `audio/mpeg`, `audio/wav`). */
19
+ MimeType?: string;
20
+ /** The file name (its extension is read). */
21
+ FileName?: string;
22
+ }
23
+ /** The longest audio {@link DecodeAudio} accepts, in seconds (15 minutes). Hold music loops; a longer file is a mistake. */
24
+ export declare const MAX_DECODED_AUDIO_SECONDS: number;
25
+ /**
26
+ * Decodes WAV or MP3 bytes into mono PCM16. The format is read from the bytes' own signature first and from the hint
27
+ * only when the bytes carry none (a bare MP3 frame stream).
28
+ *
29
+ * @param bytes The file's bytes.
30
+ * @param hint The MIME type and/or file name, when known.
31
+ * @returns Mono PCM16 at the file's own sample rate.
32
+ * @throws {Error} when the format is not WAV or MP3, the file is malformed or empty, or it is longer than
33
+ * {@link MAX_DECODED_AUDIO_SECONDS}.
34
+ */
35
+ export declare function DecodeAudio(bytes: Uint8Array, hint?: AudioFormatHint): Promise<DecodedAudio>;
36
+ /** Throws when `sampleCount` samples at `sampleRate` run longer than {@link MAX_DECODED_AUDIO_SECONDS}. */
37
+ export declare function AssertAudioDurationWithinCap(sampleCount: number, sampleRate: number): void;
38
+ /** Converts one [-1, 1] float sample to PCM16, clamping anything outside the range. */
39
+ export declare function FloatToPcm16(sample: number): number;
40
+ //# sourceMappingURL=audio-decoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-decoder.d.ts","sourceRoot":"","sources":["../../src/room-audio/audio-decoder.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,0BAA0B;IAC1B,GAAG,EAAE,UAAU,CAAC;IAChB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,2GAA2G;AAC3G,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,4HAA4H;AAC5H,eAAO,MAAM,yBAAyB,QAAU,CAAC;AAoBjD;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAQlG;AAED,2GAA2G;AAC3G,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAQ1F;AAqID,uFAAuF;AACvF,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGnD"}