@memberjunction/ai-bridge-teams 5.44.0 → 5.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -2,5 +2,8 @@ export * from './teams-sdk.js';
2
2
  export * from './teams-meeting-controls.js';
3
3
  export * from './teams-bridge.js';
4
4
  export * from './teams-native-sdk.js';
5
+ export * from './real-teams-bindings.js';
6
+ export * from './teams-graph-acs-client.js';
7
+ export * from './teams-ingress.js';
5
8
  export * from './register-native.js';
6
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@ export * from './teams-sdk.js';
2
2
  export * from './teams-meeting-controls.js';
3
3
  export * from './teams-bridge.js';
4
4
  export * from './teams-native-sdk.js';
5
+ export * from './real-teams-bindings.js';
6
+ export * from './teams-graph-acs-client.js';
7
+ export * from './teams-ingress.js';
5
8
  export * from './register-native.js';
6
9
  import { LoadTeamsBridge } from './teams-bridge.js';
7
10
  import { RegisterTeamsNativeSdk } from './register-native.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,uGAAuG;AACvG,iGAAiG;AACjG,eAAe,EAAE,CAAC;AAClB,iGAAiG;AACjG,sBAAsB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,uGAAuG;AACvG,iGAAiG;AACjG,eAAe,EAAE,CAAC;AAClB,iGAAiG;AACjG,sBAAsB,EAAE,CAAC"}
@@ -0,0 +1,278 @@
1
+ /**
2
+ * @fileoverview `RealTeamsBindings` — a production {@link ITeamsMeetingSdk} implementation over the real
3
+ * **Microsoft Graph cloud-communications API** (`/communications/calls`) + the **ACS application-hosted-media**
4
+ * audio sockets, plus the **pure** join-payload / meeting-URL / roster helpers it (and the MJAPI ingress)
5
+ * build on.
6
+ *
7
+ * ## Why injected client surfaces (not `import { Client } from '@microsoft/microsoft-graph-client'`)
8
+ * To stay buildable + unit-testable WITHOUT the Graph / ACS SDKs installed and WITHOUT any network, this
9
+ * module does NOT import them directly. Instead it depends on two tiny, local structural surfaces:
10
+ * - {@link IGraphCallsLike} — the handful of Graph cloud-communications operations we use: create/join a
11
+ * call (`POST /communications/calls`), read its participants, post a chat message
12
+ * (`POST /chats/{id}/messages`), mute a participant, and hang up the call leg.
13
+ * - {@link IAcsMediaLike} — the application-hosted-media inbound/outbound **audio sockets** (the bot's
14
+ * per-participant hearing + its voice), expressed as raw PCM frame in/out + the roster/lifecycle events
15
+ * the media + signaling plane surfaces.
16
+ *
17
+ * Production wires these over `@microsoft/microsoft-graph-client` (the REST control plane) and the ACS
18
+ * calling-bot media SDK (the audio plane); tests inject fakes. NONE of the Graph/ACS SDK types leak into
19
+ * this package. The packages are declared in `optionalDependencies` (CLAUDE rule 8, category 2 — optional
20
+ * peer SDKs loaded only when the Teams provider is configured) and are resolved by the host's
21
+ * native-adapter wiring, never statically imported here.
22
+ *
23
+ * ## The audio contract (T0 codec)
24
+ * ACS application-hosted media delivers/accepts **raw linear PCM** on the audio sockets (typically 16 kHz
25
+ * mono PCM16). The {@link ITeamsMeetingSdk} seam speaks **PCM16 `ArrayBuffer`** at the model's rate. When
26
+ * the ACS socket rate and the model rate differ, all resampling goes through the shared T0 codec
27
+ * ({@link resamplePcm16Buffer} from `@memberjunction/ai-bridge-base`) — never reimplemented here. Per-frame
28
+ * speaker labels (the ACS source participant id) ride through unchanged for diarization.
29
+ *
30
+ * ## Mapping to the documented binding (see `teams-sdk.ts`)
31
+ * - `join` → Graph `POST /communications/calls` (join by meeting URL/coordinates); returns
32
+ * `{ BotParticipantId, CallId }`.
33
+ * - `leave` → Graph call `DELETE` (the bot's call-leg lifecycle).
34
+ * - `sendAudioFrame` → ACS outbound audio socket (the bot's voice in), transcoded via the T0 codec.
35
+ * - `onAudioFrame` → ACS per-participant inbound audio socket (diarized hearing), transcoded via T0.
36
+ * - `getParticipants`/`onParticipantJoin`/`onParticipantLeave` → the call participants collection +
37
+ * `participantsUpdated` change notifications.
38
+ * - `postChatMessage` → Graph `POST /chats/{id}/messages` against the meeting's chat thread.
39
+ * - `muteParticipant` → the call's `participant:mute` action.
40
+ * - `onHandRaise` → the raised-hand signal — **tolerated as absent**: never throws if a tenant/build
41
+ * does not surface it; the handler simply never fires.
42
+ * - `onMeetingEnded` → the call's `callEnded` / `terminated` status.
43
+ *
44
+ * @module @memberjunction/ai-bridge-teams
45
+ * @author MemberJunction.com
46
+ * @see {@link ITeamsMeetingSdk} — the seam this implements.
47
+ * @see `/plans/realtime/bridges-and-widget/meeting-vendor-bindings-teams-slack.md` §1b, §2 (M1).
48
+ */
49
+ import { ITeamsMeetingSdk, TeamsAudioFrame, TeamsJoinArgs, TeamsJoinResult, TeamsParticipant, TeamsParticipantRole } from './teams-sdk.js';
50
+ /** The Graph create-call meeting-coordinate fields parsed out of a Teams join URL. */
51
+ export interface TeamsMeetingCoordinates {
52
+ /** The meeting **thread id** (the chat/conversation id behind the join URL, e.g. `19:meeting_…@thread.v2`). */
53
+ ThreadId: string;
54
+ /** The organizer's Azure AD object id, when the join URL encodes it (Graph `organizer.identity.user.id`). */
55
+ OrganizerId?: string;
56
+ /** The Azure tenant id, when the join URL encodes it (Graph `tid`). */
57
+ TenantId?: string;
58
+ /** The meeting message/conversation message id (Graph `messageId`), when present. `'0'` for most meetings. */
59
+ MessageId?: string;
60
+ }
61
+ /**
62
+ * **Pure** parse of a Teams `meetup-join` URL into the Graph create-call meeting coordinates. The Teams join
63
+ * link embeds the thread id in its path (URL-encoded) and the organizer/tenant in the `context` query param
64
+ * (URL-encoded JSON), e.g.
65
+ * `https://teams.microsoft.com/l/meetup-join/19%3Ameeting_X%40thread.v2/0?context=%7B%22Tid%22%3A%22…%22%2C%22Oid%22%3A%22…%22%7D`.
66
+ *
67
+ * Decodes the thread id and (best-effort) the `Tid`/`Oid` from `context`. Returns `null` when the URL carries
68
+ * no recognizable thread id, so the caller can fall back to an explicitly-supplied `ThreadId`. Never throws on
69
+ * a malformed `context` blob — the meeting can still be joined by thread id alone.
70
+ *
71
+ * @param joinUrl The Teams meeting join URL.
72
+ * @returns The parsed coordinates, or `null` when no thread id can be extracted.
73
+ */
74
+ export declare function parseTeamsJoinUrl(joinUrl: string): TeamsMeetingCoordinates | null;
75
+ /** The Graph `POST /communications/calls` request body (the subset we construct for a join-by-URL bot call). */
76
+ export interface GraphCreateCallRequest {
77
+ /** Always `'meeting'` — the bot joins an existing scheduled/online meeting (vs a peer-to-peer call). */
78
+ CallType: 'meeting';
79
+ /** The bot's display name in the participant list. */
80
+ BotDisplayName: string;
81
+ /** The original Teams join URL (Graph `meetingInfo.joinWebUrl`). */
82
+ JoinWebUrl: string;
83
+ /** The parsed meeting thread id (the durable meeting coordinate + chat target). */
84
+ ThreadId: string;
85
+ /** The meeting organizer's id, when resolved (helps Graph bind the call to the right meeting). */
86
+ OrganizerId?: string;
87
+ /** The Azure tenant id, when joining cross-tenant. */
88
+ TenantId?: string;
89
+ /** The application-hosted-media flag — this bot owns its media socket (vs service-hosted media). */
90
+ AppHostedMedia: true;
91
+ }
92
+ /**
93
+ * **Pure** construction of the Graph `POST /communications/calls` request body for a join-by-URL,
94
+ * application-hosted-media bot call. Prefers an explicitly-supplied `ThreadId`/`TenantId` (resolved upstream)
95
+ * over what the URL encodes, then fills the rest from {@link parseTeamsJoinUrl}. Throws when neither an
96
+ * explicit thread id nor a parseable join URL is available, so a malformed join fails loud at the seam.
97
+ *
98
+ * Isolated from I/O so the request shape unit-tests directly and the live MJAPI wiring reuses it verbatim.
99
+ *
100
+ * @param args The bridge's join args (join URL, optional thread id, bot name, tenant).
101
+ * @returns The Graph create-call request body.
102
+ * @throws When no thread id can be resolved (neither explicit nor from the URL).
103
+ */
104
+ export declare function buildGraphCreateCallRequest(args: TeamsJoinArgs): GraphCreateCallRequest;
105
+ /** One Graph call participant as the participants collection / `participantsUpdated` notification reports it. */
106
+ export interface GraphCallParticipant {
107
+ /** The Graph participant id (`participant.id`) — stable for the participant's presence in the call. */
108
+ id: string;
109
+ /** The participant's display name (`participant.info.identity.user.displayName`), when known. */
110
+ displayName?: string;
111
+ /** The Graph meeting role string (`'organizer'` / `'presenter'` / `'coorganizer'` / `'attendee'`). */
112
+ role?: string;
113
+ /** Whether this participant is the bot's own call leg (`participant.isInLobby === false && isSelf`). */
114
+ isSelf?: boolean;
115
+ }
116
+ /** Normalizes a Graph meeting role string onto the bridge's {@link TeamsParticipantRole}. */
117
+ export declare function mapGraphRole(role?: string): TeamsParticipantRole;
118
+ /**
119
+ * **Pure** normalization of one Graph call participant onto the bridge's {@link TeamsParticipant}. Isolated
120
+ * from the Graph SDK and from I/O so it unit-tests directly.
121
+ */
122
+ export declare function normalizeGraphParticipant(p: GraphCallParticipant): TeamsParticipant;
123
+ /** **Pure** normalization of a full Graph participants collection onto the bridge roster. */
124
+ export declare function normalizeGraphRoster(participants: GraphCallParticipant[]): TeamsParticipant[];
125
+ /** One raw per-participant audio frame the ACS inbound socket surfaces (PCM at the socket's sample rate). */
126
+ export interface AcsInboundAudioFrame {
127
+ /** Raw little-endian PCM16 bytes for this frame. */
128
+ Pcm: ArrayBuffer;
129
+ /** The ACS source participant id this audio came from (the diarization speaker label). */
130
+ ParticipantId: string;
131
+ /** The participant's display name at capture time, when ACS provides it. */
132
+ DisplayName?: string;
133
+ /** Optional epoch-ms capture timestamp. */
134
+ TimestampMs?: number;
135
+ }
136
+ /**
137
+ * **Pure** transcode of one inbound ACS audio frame to the bridge's diarized {@link TeamsAudioFrame},
138
+ * resampling from the ACS socket rate to the model rate via the T0 codec when they differ. The speaker
139
+ * label and timestamp pass through unchanged. Isolated for direct testing.
140
+ *
141
+ * @param frame The inbound ACS audio frame (PCM16 at `acsRate`).
142
+ * @param acsRate The ACS audio-socket sample rate (Hz).
143
+ * @param modelRate The realtime model's expected inbound sample rate (Hz).
144
+ * @returns The diarized PCM16 frame at the model rate.
145
+ */
146
+ export declare function transcodeInboundAudio(frame: AcsInboundAudioFrame, acsRate: number, modelRate: number): TeamsAudioFrame;
147
+ /**
148
+ * **Pure** transcode of one outbound PCM16 frame (the agent's voice, at the model rate) to the ACS outbound
149
+ * socket rate via the T0 codec when they differ. Returns a fresh buffer the caller can hand straight to the
150
+ * ACS outbound socket.
151
+ *
152
+ * @param pcm The agent's outbound PCM16 audio at `modelRate`.
153
+ * @param modelRate The realtime model's outbound sample rate (Hz).
154
+ * @param acsRate The ACS audio-socket sample rate (Hz).
155
+ * @returns The PCM16 audio at the ACS socket rate.
156
+ */
157
+ export declare function transcodeOutboundAudio(pcm: ArrayBuffer, modelRate: number, acsRate: number): ArrayBuffer;
158
+ /** What {@link IGraphCallsLike.CreateCall} resolves to — the bot's call-leg + participant handles. */
159
+ export interface GraphCreateCallResult {
160
+ /** The Graph call id (`/communications/calls/{id}`) — the durable external connection id. */
161
+ CallId: string;
162
+ /** The bot's own participant id in the joined call. */
163
+ BotParticipantId: string;
164
+ }
165
+ /**
166
+ * The minimal Microsoft Graph cloud-communications surface {@link RealTeamsBindings} drives — just the
167
+ * control-plane operations we use. A production wiring implements this over
168
+ * `@microsoft/microsoft-graph-client` (`Client.api('/communications/calls').post(...)`, etc.); tests inject
169
+ * a fake. NO Graph SDK types leak through this surface.
170
+ */
171
+ export interface IGraphCallsLike {
172
+ /** Creates/joins a meeting call (`POST /communications/calls`); resolves the call + bot participant ids. */
173
+ CreateCall(request: GraphCreateCallRequest): Promise<GraphCreateCallResult>;
174
+ /** Hangs up the bot's call leg (`DELETE /communications/calls/{id}`). */
175
+ DeleteCall(callId: string): Promise<void>;
176
+ /** Reads the call's current participants collection (`GET /communications/calls/{id}/participants`). */
177
+ GetParticipants(callId: string): Promise<GraphCallParticipant[]>;
178
+ /** Posts a chat message to the meeting thread (`POST /chats/{threadId}/messages`). */
179
+ PostChatMessage(threadId: string, text: string): Promise<void>;
180
+ /** Mutes a participant (`POST /communications/calls/{id}/participants/{pid}/mute`). */
181
+ MuteParticipant(callId: string, participantId: string): Promise<void>;
182
+ /** Registers the participants-changed handler (`participantsUpdated` change notifications). */
183
+ OnParticipantsUpdated(callId: string, handler: (participants: GraphCallParticipant[]) => void): void;
184
+ /** Registers the call-ended handler (`callEnded` / `terminated` status notification). */
185
+ OnCallEnded(callId: string, handler: () => void): void;
186
+ }
187
+ /**
188
+ * The ACS application-hosted-media audio plane for a single call — the bot's per-participant inbound
189
+ * **hearing** socket and its outbound **voice** socket, plus the (optionally-absent) raised-hand signal.
190
+ * Production wires this over the ACS calling-bot media SDK; tests inject a fake. Speaks raw PCM16 frames;
191
+ * the T0 transcode happens in {@link RealTeamsBindings} via the pure helpers above.
192
+ */
193
+ export interface IAcsMediaLike {
194
+ /** The ACS audio-socket sample rate in Hz (e.g. 16000) — used to drive T0 resampling. */
195
+ readonly SampleRate: number;
196
+ /** Sends one outbound PCM16 frame on the bot's outbound audio socket (the agent's voice). */
197
+ SendAudioFrame(callId: string, pcm: ArrayBuffer): void;
198
+ /** Registers the inbound per-participant audio handler on the bot's inbound socket (latest wins). */
199
+ OnAudioFrame(callId: string, handler: (frame: AcsInboundAudioFrame) => void): void;
200
+ /**
201
+ * Registers the raised-hand handler, when the platform exposes it. **Optional** — a tenant/build that
202
+ * does not surface a clean per-participant hand-raise event simply omits this method, and the binding
203
+ * tolerates its absence (never throws). "Latest handler wins" when present.
204
+ */
205
+ OnHandRaise?(callId: string, handler: (participantId: string, raised: boolean) => void): void;
206
+ }
207
+ /** Options {@link RealTeamsBindings} needs at construction — the injected client surfaces + audio rates. */
208
+ export interface RealTeamsBindingsOptions {
209
+ /** The Graph cloud-communications control-plane surface (create/join call, roster, chat, mute, hangup). */
210
+ Graph: IGraphCallsLike;
211
+ /** The ACS application-hosted-media audio plane (inbound hearing + outbound voice + hand-raise). */
212
+ Media: IAcsMediaLike;
213
+ /**
214
+ * The realtime model's PCM16 sample rate (Hz) the seam carries on both directions. The binding
215
+ * resamples between this and {@link IAcsMediaLike.SampleRate} via the T0 codec. Defaults to 16000.
216
+ */
217
+ ModelSampleRate?: number;
218
+ }
219
+ /**
220
+ * Production {@link ITeamsMeetingSdk} over the real Graph cloud-communications API + ACS
221
+ * application-hosted-media sockets, expressed against the injected {@link IGraphCallsLike} /
222
+ * {@link IAcsMediaLike} surfaces so it builds and unit-tests with no Graph/ACS install and no network.
223
+ *
224
+ * - `join` → Graph `CreateCall` with the {@link buildGraphCreateCallRequest} body; wires the ACS
225
+ * audio + roster + lifecycle callbacks; returns `{ BotParticipantId, CallId }`.
226
+ * - `leave` → Graph `DeleteCall`.
227
+ * - `sendAudioFrame` → ACS outbound socket, resampled model→ACS rate via T0.
228
+ * - `onAudioFrame` → ACS inbound socket, resampled ACS→model rate via T0, with the speaker label.
229
+ * - `getParticipants` → Graph `GetParticipants`, normalized via {@link normalizeGraphRoster}.
230
+ * - `postChatMessage` → Graph `PostChatMessage` on the meeting thread.
231
+ * - `muteParticipant` → Graph `MuteParticipant`.
232
+ * - `onHandRaise` → ACS `OnHandRaise` when present; **no-op (tolerated) when absent**.
233
+ * - `onMeetingEnded` → Graph `OnCallEnded`.
234
+ */
235
+ export declare class RealTeamsBindings implements ITeamsMeetingSdk {
236
+ private readonly graph;
237
+ private readonly media;
238
+ private readonly modelSampleRate;
239
+ /** The active call id once {@link join} succeeds (the durable external connection id). */
240
+ private callId;
241
+ /** The meeting thread id resolved at join (the chat-post target + meeting coordinate). */
242
+ private threadId;
243
+ private audioHandler?;
244
+ private joinHandler?;
245
+ private leaveHandler?;
246
+ private handRaiseHandler?;
247
+ private endedHandler?;
248
+ /** The last roster snapshot, kept so `participantsUpdated` can diff into join/leave events. */
249
+ private lastRoster;
250
+ constructor(options: RealTeamsBindingsOptions);
251
+ /** @inheritdoc */
252
+ join(args: TeamsJoinArgs): Promise<TeamsJoinResult>;
253
+ /** @inheritdoc */
254
+ leave(): Promise<void>;
255
+ /** @inheritdoc */
256
+ sendAudioFrame(pcm: ArrayBuffer): void;
257
+ /** @inheritdoc */
258
+ onAudioFrame(cb: (frame: TeamsAudioFrame) => void): void;
259
+ /** @inheritdoc */
260
+ onParticipantJoin(cb: (participant: TeamsParticipant) => void): void;
261
+ /** @inheritdoc */
262
+ onParticipantLeave(cb: (participantId: string) => void): void;
263
+ /** @inheritdoc */
264
+ onHandRaise(cb: (participantId: string, raised: boolean) => void): void;
265
+ /** @inheritdoc */
266
+ getParticipants(): Promise<TeamsParticipant[]>;
267
+ /** @inheritdoc */
268
+ postChatMessage(text: string): Promise<void>;
269
+ /** @inheritdoc */
270
+ muteParticipant(participantId: string): Promise<void>;
271
+ /** @inheritdoc */
272
+ onMeetingEnded(cb: () => void): void;
273
+ /** Wires the Graph + ACS callbacks for the joined call to this binding's seam handlers. */
274
+ private wireCallbacks;
275
+ /** Diffs a fresh roster against the last snapshot and emits per-participant join/leave events. */
276
+ private handleRosterUpdate;
277
+ }
278
+ //# sourceMappingURL=real-teams-bindings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"real-teams-bindings.d.ts","sourceRoot":"","sources":["../src/real-teams-bindings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,345 @@
1
+ /**
2
+ * @fileoverview `RealTeamsBindings` — a production {@link ITeamsMeetingSdk} implementation over the real
3
+ * **Microsoft Graph cloud-communications API** (`/communications/calls`) + the **ACS application-hosted-media**
4
+ * audio sockets, plus the **pure** join-payload / meeting-URL / roster helpers it (and the MJAPI ingress)
5
+ * build on.
6
+ *
7
+ * ## Why injected client surfaces (not `import { Client } from '@microsoft/microsoft-graph-client'`)
8
+ * To stay buildable + unit-testable WITHOUT the Graph / ACS SDKs installed and WITHOUT any network, this
9
+ * module does NOT import them directly. Instead it depends on two tiny, local structural surfaces:
10
+ * - {@link IGraphCallsLike} — the handful of Graph cloud-communications operations we use: create/join a
11
+ * call (`POST /communications/calls`), read its participants, post a chat message
12
+ * (`POST /chats/{id}/messages`), mute a participant, and hang up the call leg.
13
+ * - {@link IAcsMediaLike} — the application-hosted-media inbound/outbound **audio sockets** (the bot's
14
+ * per-participant hearing + its voice), expressed as raw PCM frame in/out + the roster/lifecycle events
15
+ * the media + signaling plane surfaces.
16
+ *
17
+ * Production wires these over `@microsoft/microsoft-graph-client` (the REST control plane) and the ACS
18
+ * calling-bot media SDK (the audio plane); tests inject fakes. NONE of the Graph/ACS SDK types leak into
19
+ * this package. The packages are declared in `optionalDependencies` (CLAUDE rule 8, category 2 — optional
20
+ * peer SDKs loaded only when the Teams provider is configured) and are resolved by the host's
21
+ * native-adapter wiring, never statically imported here.
22
+ *
23
+ * ## The audio contract (T0 codec)
24
+ * ACS application-hosted media delivers/accepts **raw linear PCM** on the audio sockets (typically 16 kHz
25
+ * mono PCM16). The {@link ITeamsMeetingSdk} seam speaks **PCM16 `ArrayBuffer`** at the model's rate. When
26
+ * the ACS socket rate and the model rate differ, all resampling goes through the shared T0 codec
27
+ * ({@link resamplePcm16Buffer} from `@memberjunction/ai-bridge-base`) — never reimplemented here. Per-frame
28
+ * speaker labels (the ACS source participant id) ride through unchanged for diarization.
29
+ *
30
+ * ## Mapping to the documented binding (see `teams-sdk.ts`)
31
+ * - `join` → Graph `POST /communications/calls` (join by meeting URL/coordinates); returns
32
+ * `{ BotParticipantId, CallId }`.
33
+ * - `leave` → Graph call `DELETE` (the bot's call-leg lifecycle).
34
+ * - `sendAudioFrame` → ACS outbound audio socket (the bot's voice in), transcoded via the T0 codec.
35
+ * - `onAudioFrame` → ACS per-participant inbound audio socket (diarized hearing), transcoded via T0.
36
+ * - `getParticipants`/`onParticipantJoin`/`onParticipantLeave` → the call participants collection +
37
+ * `participantsUpdated` change notifications.
38
+ * - `postChatMessage` → Graph `POST /chats/{id}/messages` against the meeting's chat thread.
39
+ * - `muteParticipant` → the call's `participant:mute` action.
40
+ * - `onHandRaise` → the raised-hand signal — **tolerated as absent**: never throws if a tenant/build
41
+ * does not surface it; the handler simply never fires.
42
+ * - `onMeetingEnded` → the call's `callEnded` / `terminated` status.
43
+ *
44
+ * @module @memberjunction/ai-bridge-teams
45
+ * @author MemberJunction.com
46
+ * @see {@link ITeamsMeetingSdk} — the seam this implements.
47
+ * @see `/plans/realtime/bridges-and-widget/meeting-vendor-bindings-teams-slack.md` §1b, §2 (M1).
48
+ */
49
+ import { resamplePcm16Buffer } from '@memberjunction/ai-bridge-base';
50
+ /**
51
+ * **Pure** parse of a Teams `meetup-join` URL into the Graph create-call meeting coordinates. The Teams join
52
+ * link embeds the thread id in its path (URL-encoded) and the organizer/tenant in the `context` query param
53
+ * (URL-encoded JSON), e.g.
54
+ * `https://teams.microsoft.com/l/meetup-join/19%3Ameeting_X%40thread.v2/0?context=%7B%22Tid%22%3A%22…%22%2C%22Oid%22%3A%22…%22%7D`.
55
+ *
56
+ * Decodes the thread id and (best-effort) the `Tid`/`Oid` from `context`. Returns `null` when the URL carries
57
+ * no recognizable thread id, so the caller can fall back to an explicitly-supplied `ThreadId`. Never throws on
58
+ * a malformed `context` blob — the meeting can still be joined by thread id alone.
59
+ *
60
+ * @param joinUrl The Teams meeting join URL.
61
+ * @returns The parsed coordinates, or `null` when no thread id can be extracted.
62
+ */
63
+ export function parseTeamsJoinUrl(joinUrl) {
64
+ const threadId = extractThreadId(joinUrl);
65
+ if (!threadId) {
66
+ return null;
67
+ }
68
+ const context = extractJoinContext(joinUrl);
69
+ const messageId = extractMessageId(joinUrl);
70
+ return {
71
+ ThreadId: threadId,
72
+ ...(context.OrganizerId ? { OrganizerId: context.OrganizerId } : {}),
73
+ ...(context.TenantId ? { TenantId: context.TenantId } : {}),
74
+ ...(messageId ? { MessageId: messageId } : {}),
75
+ };
76
+ }
77
+ /**
78
+ * **Pure** construction of the Graph `POST /communications/calls` request body for a join-by-URL,
79
+ * application-hosted-media bot call. Prefers an explicitly-supplied `ThreadId`/`TenantId` (resolved upstream)
80
+ * over what the URL encodes, then fills the rest from {@link parseTeamsJoinUrl}. Throws when neither an
81
+ * explicit thread id nor a parseable join URL is available, so a malformed join fails loud at the seam.
82
+ *
83
+ * Isolated from I/O so the request shape unit-tests directly and the live MJAPI wiring reuses it verbatim.
84
+ *
85
+ * @param args The bridge's join args (join URL, optional thread id, bot name, tenant).
86
+ * @returns The Graph create-call request body.
87
+ * @throws When no thread id can be resolved (neither explicit nor from the URL).
88
+ */
89
+ export function buildGraphCreateCallRequest(args) {
90
+ const parsed = parseTeamsJoinUrl(args.JoinUrl);
91
+ const threadId = args.ThreadId ?? parsed?.ThreadId;
92
+ if (!threadId) {
93
+ throw new Error('buildGraphCreateCallRequest: could not resolve a meeting thread id. Supply TeamsJoinArgs.ThreadId ' +
94
+ `or a join URL containing the meeting thread id. Got join URL: '${args.JoinUrl}'.`);
95
+ }
96
+ const tenantId = args.TenantId ?? parsed?.TenantId;
97
+ const organizerId = parsed?.OrganizerId;
98
+ return {
99
+ CallType: 'meeting',
100
+ BotDisplayName: args.BotDisplayName,
101
+ JoinWebUrl: args.JoinUrl,
102
+ ThreadId: threadId,
103
+ ...(organizerId ? { OrganizerId: organizerId } : {}),
104
+ ...(tenantId ? { TenantId: tenantId } : {}),
105
+ AppHostedMedia: true,
106
+ };
107
+ }
108
+ /** Normalizes a Graph meeting role string onto the bridge's {@link TeamsParticipantRole}. */
109
+ export function mapGraphRole(role) {
110
+ switch ((role ?? '').trim().toLowerCase()) {
111
+ case 'organizer':
112
+ return 'Organizer';
113
+ case 'presenter':
114
+ case 'coorganizer':
115
+ case 'co-organizer':
116
+ return 'Presenter';
117
+ default:
118
+ return 'Attendee';
119
+ }
120
+ }
121
+ /**
122
+ * **Pure** normalization of one Graph call participant onto the bridge's {@link TeamsParticipant}. Isolated
123
+ * from the Graph SDK and from I/O so it unit-tests directly.
124
+ */
125
+ export function normalizeGraphParticipant(p) {
126
+ return {
127
+ ParticipantId: p.id,
128
+ DisplayName: p.displayName,
129
+ Role: mapGraphRole(p.role),
130
+ IsSelf: p.isSelf,
131
+ };
132
+ }
133
+ /** **Pure** normalization of a full Graph participants collection onto the bridge roster. */
134
+ export function normalizeGraphRoster(participants) {
135
+ return participants.map(normalizeGraphParticipant);
136
+ }
137
+ /**
138
+ * **Pure** transcode of one inbound ACS audio frame to the bridge's diarized {@link TeamsAudioFrame},
139
+ * resampling from the ACS socket rate to the model rate via the T0 codec when they differ. The speaker
140
+ * label and timestamp pass through unchanged. Isolated for direct testing.
141
+ *
142
+ * @param frame The inbound ACS audio frame (PCM16 at `acsRate`).
143
+ * @param acsRate The ACS audio-socket sample rate (Hz).
144
+ * @param modelRate The realtime model's expected inbound sample rate (Hz).
145
+ * @returns The diarized PCM16 frame at the model rate.
146
+ */
147
+ export function transcodeInboundAudio(frame, acsRate, modelRate) {
148
+ const pcm = acsRate === modelRate ? frame.Pcm : resamplePcm16Buffer(frame.Pcm, acsRate, modelRate);
149
+ return {
150
+ Pcm: pcm,
151
+ ParticipantId: frame.ParticipantId,
152
+ DisplayName: frame.DisplayName,
153
+ TimestampMs: typeof frame.TimestampMs === 'number' ? frame.TimestampMs : Date.now(),
154
+ };
155
+ }
156
+ /**
157
+ * **Pure** transcode of one outbound PCM16 frame (the agent's voice, at the model rate) to the ACS outbound
158
+ * socket rate via the T0 codec when they differ. Returns a fresh buffer the caller can hand straight to the
159
+ * ACS outbound socket.
160
+ *
161
+ * @param pcm The agent's outbound PCM16 audio at `modelRate`.
162
+ * @param modelRate The realtime model's outbound sample rate (Hz).
163
+ * @param acsRate The ACS audio-socket sample rate (Hz).
164
+ * @returns The PCM16 audio at the ACS socket rate.
165
+ */
166
+ export function transcodeOutboundAudio(pcm, modelRate, acsRate) {
167
+ return modelRate === acsRate ? pcm.slice(0) : resamplePcm16Buffer(pcm, modelRate, acsRate);
168
+ }
169
+ /** Extracts and URL-decodes the `19:meeting_…@thread.v2` thread id from a Teams `meetup-join` URL path. */
170
+ function extractThreadId(joinUrl) {
171
+ const match = joinUrl.match(/meetup-join\/([^/?#]+)/i);
172
+ if (!match) {
173
+ return undefined;
174
+ }
175
+ const decoded = safeDecode(match[1]);
176
+ return decoded.length > 0 ? decoded : undefined;
177
+ }
178
+ /** Best-effort extract of the `0`-or-message-id segment that follows the thread id in the join URL path. */
179
+ function extractMessageId(joinUrl) {
180
+ const match = joinUrl.match(/meetup-join\/[^/?#]+\/([^/?#]+)/i);
181
+ if (!match) {
182
+ return undefined;
183
+ }
184
+ const decoded = safeDecode(match[1]);
185
+ return decoded.length > 0 ? decoded : undefined;
186
+ }
187
+ /** Best-effort parse of the `context` query param (URL-encoded JSON with `Tid`/`Oid`). Never throws. */
188
+ function extractJoinContext(joinUrl) {
189
+ const match = joinUrl.match(/[?&]context=([^&#]+)/i);
190
+ if (!match) {
191
+ return {};
192
+ }
193
+ try {
194
+ const parsed = JSON.parse(safeDecode(match[1]));
195
+ if (parsed && typeof parsed === 'object') {
196
+ const obj = parsed;
197
+ return {
198
+ OrganizerId: readNonEmptyString(obj.Oid),
199
+ TenantId: readNonEmptyString(obj.Tid),
200
+ };
201
+ }
202
+ }
203
+ catch {
204
+ // Malformed context — the meeting is still joinable by thread id; swallow and return nothing.
205
+ }
206
+ return {};
207
+ }
208
+ /** URL-decodes a segment, falling back to the raw value if it is not valid percent-encoding. */
209
+ function safeDecode(value) {
210
+ try {
211
+ return decodeURIComponent(value);
212
+ }
213
+ catch {
214
+ return value;
215
+ }
216
+ }
217
+ /** Reads a value as a non-empty string, or `undefined`. */
218
+ function readNonEmptyString(value) {
219
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
220
+ }
221
+ /**
222
+ * Production {@link ITeamsMeetingSdk} over the real Graph cloud-communications API + ACS
223
+ * application-hosted-media sockets, expressed against the injected {@link IGraphCallsLike} /
224
+ * {@link IAcsMediaLike} surfaces so it builds and unit-tests with no Graph/ACS install and no network.
225
+ *
226
+ * - `join` → Graph `CreateCall` with the {@link buildGraphCreateCallRequest} body; wires the ACS
227
+ * audio + roster + lifecycle callbacks; returns `{ BotParticipantId, CallId }`.
228
+ * - `leave` → Graph `DeleteCall`.
229
+ * - `sendAudioFrame` → ACS outbound socket, resampled model→ACS rate via T0.
230
+ * - `onAudioFrame` → ACS inbound socket, resampled ACS→model rate via T0, with the speaker label.
231
+ * - `getParticipants` → Graph `GetParticipants`, normalized via {@link normalizeGraphRoster}.
232
+ * - `postChatMessage` → Graph `PostChatMessage` on the meeting thread.
233
+ * - `muteParticipant` → Graph `MuteParticipant`.
234
+ * - `onHandRaise` → ACS `OnHandRaise` when present; **no-op (tolerated) when absent**.
235
+ * - `onMeetingEnded` → Graph `OnCallEnded`.
236
+ */
237
+ export class RealTeamsBindings {
238
+ constructor(options) {
239
+ /** The active call id once {@link join} succeeds (the durable external connection id). */
240
+ this.callId = null;
241
+ /** The meeting thread id resolved at join (the chat-post target + meeting coordinate). */
242
+ this.threadId = null;
243
+ /** The last roster snapshot, kept so `participantsUpdated` can diff into join/leave events. */
244
+ this.lastRoster = [];
245
+ this.graph = options.Graph;
246
+ this.media = options.Media;
247
+ this.modelSampleRate = options.ModelSampleRate ?? 16000;
248
+ }
249
+ /** @inheritdoc */
250
+ async join(args) {
251
+ const request = buildGraphCreateCallRequest(args);
252
+ const result = await this.graph.CreateCall(request);
253
+ this.callId = result.CallId;
254
+ this.threadId = request.ThreadId;
255
+ this.wireCallbacks(result.CallId);
256
+ return { BotParticipantId: result.BotParticipantId, CallId: result.CallId };
257
+ }
258
+ /** @inheritdoc */
259
+ async leave() {
260
+ const callId = this.callId;
261
+ this.callId = null;
262
+ this.threadId = null;
263
+ this.lastRoster = [];
264
+ if (callId) {
265
+ await this.graph.DeleteCall(callId);
266
+ }
267
+ }
268
+ /** @inheritdoc */
269
+ sendAudioFrame(pcm) {
270
+ if (!this.callId) {
271
+ return;
272
+ }
273
+ const out = transcodeOutboundAudio(pcm, this.modelSampleRate, this.media.SampleRate);
274
+ this.media.SendAudioFrame(this.callId, out);
275
+ }
276
+ /** @inheritdoc */
277
+ onAudioFrame(cb) {
278
+ this.audioHandler = cb;
279
+ }
280
+ /** @inheritdoc */
281
+ onParticipantJoin(cb) {
282
+ this.joinHandler = cb;
283
+ }
284
+ /** @inheritdoc */
285
+ onParticipantLeave(cb) {
286
+ this.leaveHandler = cb;
287
+ }
288
+ /** @inheritdoc */
289
+ onHandRaise(cb) {
290
+ this.handRaiseHandler = cb;
291
+ }
292
+ /** @inheritdoc */
293
+ async getParticipants() {
294
+ if (!this.callId) {
295
+ return [];
296
+ }
297
+ const roster = normalizeGraphRoster(await this.graph.GetParticipants(this.callId));
298
+ this.lastRoster = roster;
299
+ return roster;
300
+ }
301
+ /** @inheritdoc */
302
+ async postChatMessage(text) {
303
+ if (!this.threadId) {
304
+ return;
305
+ }
306
+ await this.graph.PostChatMessage(this.threadId, text);
307
+ }
308
+ /** @inheritdoc */
309
+ async muteParticipant(participantId) {
310
+ if (!this.callId) {
311
+ return;
312
+ }
313
+ await this.graph.MuteParticipant(this.callId, participantId);
314
+ }
315
+ /** @inheritdoc */
316
+ onMeetingEnded(cb) {
317
+ this.endedHandler = cb;
318
+ }
319
+ /** Wires the Graph + ACS callbacks for the joined call to this binding's seam handlers. */
320
+ wireCallbacks(callId) {
321
+ this.media.OnAudioFrame(callId, (frame) => this.audioHandler?.(transcodeInboundAudio(frame, this.media.SampleRate, this.modelSampleRate)));
322
+ this.graph.OnParticipantsUpdated(callId, (participants) => this.handleRosterUpdate(normalizeGraphRoster(participants)));
323
+ this.graph.OnCallEnded(callId, () => this.endedHandler?.());
324
+ // Hand-raise is partial on Teams — only wire it when the ACS plane exposes the optional method. Its
325
+ // absence is tolerated (no throw); the handler simply never fires on tenants/builds without it.
326
+ this.media.OnHandRaise?.(callId, (participantId, raised) => this.handRaiseHandler?.(participantId, raised));
327
+ }
328
+ /** Diffs a fresh roster against the last snapshot and emits per-participant join/leave events. */
329
+ handleRosterUpdate(roster) {
330
+ const previousIds = new Set(this.lastRoster.map((p) => p.ParticipantId));
331
+ const currentIds = new Set(roster.map((p) => p.ParticipantId));
332
+ for (const p of roster) {
333
+ if (!previousIds.has(p.ParticipantId)) {
334
+ this.joinHandler?.(p);
335
+ }
336
+ }
337
+ for (const id of previousIds) {
338
+ if (!currentIds.has(id)) {
339
+ this.leaveHandler?.(id);
340
+ }
341
+ }
342
+ this.lastRoster = roster;
343
+ }
344
+ }
345
+ //# sourceMappingURL=real-teams-bindings.js.map