@memberjunction/ai-bridge-teams 5.44.0 → 5.45.1

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.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @fileoverview Microsoft Graph change-notification **ingress** pure helpers — the validation-token
3
+ * handshake, the `clientState` shared-secret check, and the call/participant notification → normalized
4
+ * state mapper, plus a `buildJoinByUrlRequest` convenience. These are the framework-free pieces of the
5
+ * MJAPI Teams ingress: no Express, no network, no DB, so they unit-test directly and the MJAPI router can
6
+ * call them verbatim once the live wiring lands.
7
+ *
8
+ * The remaining **live** ingress — the `POST /meetings/teams/notifications` endpoint, the
9
+ * `Meeting.JoinByUrl(agentIdentityId, joinUrl)` mutation, and the Azure AD app registration (Graph
10
+ * cloud-communications + chat permissions with tenant admin consent) — is documented in
11
+ * `plans/realtime/bridges-and-widget/spikes/M1-teams-binding-notes.md` and is gated on real Teams/Azure
12
+ * credentials + a publicly reachable Graph webhook URL.
13
+ *
14
+ * @module @memberjunction/ai-bridge-teams
15
+ * @author MemberJunction.com
16
+ * @see `/plans/realtime/bridges-and-widget/meeting-vendor-bindings-teams-slack.md` §2 (M1 B/C).
17
+ */
18
+ import { timingSafeEqual } from 'node:crypto';
19
+ import { buildGraphCreateCallRequest } from './real-teams-bindings.js';
20
+ /**
21
+ * Performs the Graph change-notification gate, pure + exported so the MJAPI router calls it on every
22
+ * `POST /meetings/teams/notifications` request (these can't carry an MJ JWT — the handshake + `clientState`
23
+ * shared secret are the gate):
24
+ *
25
+ * 1. If the request carries a `validationToken` query param, it's the subscription-validation handshake →
26
+ * return `{ Kind: 'validation', ValidationToken }` (the router echoes it back as `text/plain` 200). A
27
+ * blank token rejects.
28
+ * 2. Otherwise it's a real notification. Every notification in the batch carries a `clientState`; verify
29
+ * each (constant-time) against the secret we set when creating the subscription. Any mismatch rejects the
30
+ * whole batch (an attacker who can't produce the secret must not drive bot behavior).
31
+ *
32
+ * @param validationToken The `?validationToken=…` query param value, when present (the handshake).
33
+ * @param expectedClientState The shared secret set on subscription creation (resolved upstream — never inlined).
34
+ * @param notificationClientStates The `clientState` of each notification in the POST body (empty for handshake).
35
+ * @returns The discriminated validation result.
36
+ */
37
+ export function validateGraphNotification(validationToken, expectedClientState, notificationClientStates) {
38
+ if (validationToken !== undefined) {
39
+ return validationToken.length > 0
40
+ ? { Kind: 'validation', ValidationToken: validationToken }
41
+ : { Kind: 'reject', Reason: 'empty-validation-token' };
42
+ }
43
+ for (const state of notificationClientStates) {
44
+ if (!constantTimeEquals(state ?? '', expectedClientState)) {
45
+ return { Kind: 'reject', Reason: 'client-state-mismatch' };
46
+ }
47
+ }
48
+ return { Kind: 'notification' };
49
+ }
50
+ /**
51
+ * **Pure** mapping of a Graph call/participant change notification to a {@link NormalizedCallNotification}
52
+ * (`{ callId, state, participants }`). Resolves the call id from `resourceData.id` or the `resource` path,
53
+ * normalizes the lifecycle state, and passes the participants snapshot through unchanged. Exported so the
54
+ * MJAPI router maps each notification → engine session lifecycle without re-parsing Graph's resource shapes.
55
+ *
56
+ * @param notification One Graph change-notification item.
57
+ * @returns The normalized call notification.
58
+ * @throws When no call id can be resolved (a notification with no identifiable call is unactionable).
59
+ */
60
+ export function parseCallNotification(notification) {
61
+ const callId = notification.resourceData?.id ?? extractCallIdFromResource(notification.resource);
62
+ if (!callId) {
63
+ throw new Error('parseCallNotification: could not resolve a call id from the Graph notification ' +
64
+ `(resourceData.id and resource path both absent). resource='${notification.resource ?? ''}'.`);
65
+ }
66
+ return {
67
+ callId,
68
+ state: normalizeCallState(notification.resourceData?.state),
69
+ participants: notification.resourceData?.participants ?? [],
70
+ };
71
+ }
72
+ /**
73
+ * Builds the Graph `POST /communications/calls` request body for an on-demand **join-by-URL** trigger, the
74
+ * payload the live `Meeting.JoinByUrl(agentIdentityId, joinUrl)` mutation hands the engine. Thin wrapper over
75
+ * {@link buildGraphCreateCallRequest} that constructs the minimal {@link TeamsJoinArgs} from a join URL + bot
76
+ * name; pure so the router and tests share one request shape.
77
+ *
78
+ * @param joinUrl The Teams meeting join URL to join.
79
+ * @param botDisplayName The bot's display name in the participant list (defaults to `'AI Agent'`).
80
+ * @param tenantId The Azure tenant id, when joining cross-tenant.
81
+ * @returns The Graph create-call request body.
82
+ * @throws When the join URL carries no resolvable meeting thread id.
83
+ */
84
+ export function buildJoinByUrlRequest(joinUrl, botDisplayName = 'AI Agent', tenantId) {
85
+ const args = {
86
+ JoinUrl: joinUrl,
87
+ BotDisplayName: botDisplayName,
88
+ ...(tenantId ? { TenantId: tenantId } : {}),
89
+ };
90
+ return buildGraphCreateCallRequest(args);
91
+ }
92
+ /** Maps a free-form Graph call-state string onto the {@link NormalizedCallState} union. */
93
+ function normalizeCallState(state) {
94
+ switch ((state ?? '').trim().toLowerCase()) {
95
+ case 'establishing':
96
+ return 'establishing';
97
+ case 'established':
98
+ return 'established';
99
+ case 'terminating':
100
+ return 'terminating';
101
+ case 'terminated':
102
+ return 'terminated';
103
+ default:
104
+ return 'unknown';
105
+ }
106
+ }
107
+ /** Extracts the `{id}` from a `communications/calls/{id}[/...]` Graph resource path, or `undefined`. */
108
+ function extractCallIdFromResource(resource) {
109
+ if (!resource) {
110
+ return undefined;
111
+ }
112
+ const match = resource.match(/communications\/calls\/([^/?#]+)/i);
113
+ return match ? match[1] : undefined;
114
+ }
115
+ /** Constant-time string compare, length-safe (mismatched lengths return false) — for `clientState`. */
116
+ function constantTimeEquals(a, b) {
117
+ const bufA = new Uint8Array(Buffer.from(a, 'utf8'));
118
+ const bufB = new Uint8Array(Buffer.from(b, 'utf8'));
119
+ if (bufA.length !== bufB.length) {
120
+ return false;
121
+ }
122
+ return timingSafeEqual(bufA, bufB);
123
+ }
124
+ //# sourceMappingURL=teams-ingress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams-ingress.js","sourceRoot":"","sources":["../src/teams-ingress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,2BAA2B,EAAgD,MAAM,uBAAuB,CAAC;AA4BlH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,yBAAyB,CACrC,eAAmC,EACnC,mBAA2B,EAC3B,wBAA2D;IAE3D,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,eAAe,CAAC,MAAM,GAAG,CAAC;YAC7B,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE;YAC1D,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAC;IAC/D,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,wBAAwB,EAAE,CAAC;QAC3C,IAAI,CAAC,kBAAkB,CAAC,KAAK,IAAI,EAAE,EAAE,mBAAmB,CAAC,EAAE,CAAC;YACxD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;QAC/D,CAAC;IACL,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;AACpC,CAAC;AAmCD;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,YAAqC;IACvE,MAAM,MAAM,GAAG,YAAY,CAAC,YAAY,EAAE,EAAE,IAAI,yBAAyB,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IACjG,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACX,iFAAiF;YAC7E,8DAA8D,YAAY,CAAC,QAAQ,IAAI,EAAE,IAAI,CACpG,CAAC;IACN,CAAC;IACD,OAAO;QACH,MAAM;QACN,KAAK,EAAE,kBAAkB,CAAC,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC;QAC3D,YAAY,EAAE,YAAY,CAAC,YAAY,EAAE,YAAY,IAAI,EAAE;KAC9D,CAAC;AACN,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CACjC,OAAe,EACf,cAAc,GAAG,UAAU,EAC3B,QAAiB;IAEjB,MAAM,IAAI,GAAkB;QACxB,OAAO,EAAE,OAAO;QAChB,cAAc,EAAE,cAAc;QAC9B,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9C,CAAC;IACF,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,2FAA2F;AAC3F,SAAS,kBAAkB,CAAC,KAAc;IACtC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;QACzC,KAAK,cAAc;YACf,OAAO,cAAc,CAAC;QAC1B,KAAK,aAAa;YACd,OAAO,aAAa,CAAC;QACzB,KAAK,aAAa;YACd,OAAO,aAAa,CAAC;QACzB,KAAK,YAAY;YACb,OAAO,YAAY,CAAC;QACxB;YACI,OAAO,SAAS,CAAC;IACzB,CAAC;AACL,CAAC;AAED,wGAAwG;AACxG,SAAS,yBAAyB,CAAC,QAAiB;IAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAClE,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACxC,CAAC;AAED,uGAAuG;AACvG,SAAS,kBAAkB,CAAC,CAAS,EAAE,CAAS;IAC5C,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,OAAO,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@memberjunction/ai-bridge-teams",
3
3
  "type": "module",
4
- "version": "5.44.0",
4
+ "version": "5.45.1",
5
5
  "description": "MemberJunction: Microsoft Teams Realtime Bridge driver. Connects the realtime agent engine to a Teams meeting (audio in/out, diarized roster, participant mute, Teams meeting chat) via an injectable Teams calling-bot SDK seam (Azure Communication Services / Microsoft Graph cloud-communications), and contributes a Meeting Controls facilitator channel.",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -17,10 +17,15 @@
17
17
  "author": "MemberJunction.com",
18
18
  "license": "ISC",
19
19
  "dependencies": {
20
- "@memberjunction/core": "5.44.0",
21
- "@memberjunction/global": "5.44.0",
22
- "@memberjunction/core-entities": "5.44.0",
23
- "@memberjunction/ai-bridge-base": "5.44.0"
20
+ "@memberjunction/core": "5.45.1",
21
+ "@memberjunction/global": "5.45.1",
22
+ "@memberjunction/core-entities": "5.45.1",
23
+ "@memberjunction/ai-bridge-base": "5.45.1"
24
+ },
25
+ "//optionalDependencies": "@microsoft/microsoft-graph-client and @azure/communication-call-automation are OPTIONAL PEER SDKs (CLAUDE rule 8, category 2): the package never statically imports either. @microsoft/microsoft-graph-client is the Graph cloud-communications CONTROL plane — production wires RealTeamsBindings.IGraphCallsLike over Client.api('/communications/calls').post(...) for join/roster/chat/mute/hangup. @azure/communication-call-automation is the ACS application-hosted-media AUDIO plane — production wires RealTeamsBindings.IAcsMediaLike (inbound per-participant + outbound PCM sockets) over it. Both are injected behind structural surfaces so the package builds and unit-tests with no install and no network, and are loaded only when the Teams provider is configured at deployment.",
26
+ "optionalDependencies": {
27
+ "@microsoft/microsoft-graph-client": "^3.0.7",
28
+ "@azure/communication-call-automation": "^1.3.0"
24
29
  },
25
30
  "devDependencies": {
26
31
  "@types/node": "24.10.11",