@open-everywhere/adapters 0.4.0 → 0.6.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/agent/meetings/native-http.ts +72 -0
- package/agent/meetings/native-meet.ts +173 -0
- package/agent/meetings/native-sources.ts +14 -0
- package/agent/meetings/native-teams.ts +119 -0
- package/agent/meetings/native-zoom.ts +98 -0
- package/agent/model/gateway.ts +44 -4
- package/agent/model/meeting-gateway.ts +88 -0
- package/agent/model/transcription-routes.ts +347 -0
- package/agent/oe-oauth/config.ts +1 -0
- package/agent/register-defaults.ts +11 -0
- package/agent/sandbox/long-lived-box.ts +464 -0
- package/dist/agent/meetings/native-http.d.ts +22 -0
- package/dist/agent/meetings/native-http.d.ts.map +1 -0
- package/dist/agent/meetings/native-http.js +61 -0
- package/dist/agent/meetings/native-http.js.map +1 -0
- package/dist/agent/meetings/native-meet.d.ts +6 -0
- package/dist/agent/meetings/native-meet.d.ts.map +1 -0
- package/dist/agent/meetings/native-meet.js +176 -0
- package/dist/agent/meetings/native-meet.js.map +1 -0
- package/dist/agent/meetings/native-sources.d.ts +2 -0
- package/dist/agent/meetings/native-sources.d.ts.map +1 -0
- package/dist/agent/meetings/native-sources.js +13 -0
- package/dist/agent/meetings/native-sources.js.map +1 -0
- package/dist/agent/meetings/native-teams.d.ts +6 -0
- package/dist/agent/meetings/native-teams.d.ts.map +1 -0
- package/dist/agent/meetings/native-teams.js +118 -0
- package/dist/agent/meetings/native-teams.js.map +1 -0
- package/dist/agent/meetings/native-zoom.d.ts +7 -0
- package/dist/agent/meetings/native-zoom.d.ts.map +1 -0
- package/dist/agent/meetings/native-zoom.js +98 -0
- package/dist/agent/meetings/native-zoom.js.map +1 -0
- package/dist/agent/model/gateway.d.ts +10 -0
- package/dist/agent/model/gateway.d.ts.map +1 -1
- package/dist/agent/model/gateway.js +43 -5
- package/dist/agent/model/gateway.js.map +1 -1
- package/dist/agent/model/meeting-gateway.d.ts +24 -0
- package/dist/agent/model/meeting-gateway.d.ts.map +1 -0
- package/dist/agent/model/meeting-gateway.js +75 -0
- package/dist/agent/model/meeting-gateway.js.map +1 -0
- package/dist/agent/model/transcription-routes.d.ts +89 -0
- package/dist/agent/model/transcription-routes.d.ts.map +1 -0
- package/dist/agent/model/transcription-routes.js +279 -0
- package/dist/agent/model/transcription-routes.js.map +1 -0
- package/dist/agent/oe-oauth/config.d.ts.map +1 -1
- package/dist/agent/oe-oauth/config.js +1 -0
- package/dist/agent/oe-oauth/config.js.map +1 -1
- package/dist/agent/register-defaults.d.ts.map +1 -1
- package/dist/agent/register-defaults.js +9 -0
- package/dist/agent/register-defaults.js.map +1 -1
- package/dist/agent/sandbox/long-lived-box.d.ts +70 -0
- package/dist/agent/sandbox/long-lived-box.d.ts.map +1 -0
- package/dist/agent/sandbox/long-lived-box.js +414 -0
- package/dist/agent/sandbox/long-lived-box.js.map +1 -0
- package/package.json +38 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// The Google Meet native-transcript reader (OPE-2471): the Meet REST API v2, read through the organisation's
|
|
2
|
+
// service account with domain-wide delegation, acting as the meeting's organiser.
|
|
3
|
+
//
|
|
4
|
+
// 1. token: a JWT signed with the service account's key (RS256), `sub` = the organiser, scope
|
|
5
|
+
// `meetings.space.readonly`, exchanged at oauth2.googleapis.com/token
|
|
6
|
+
// 2. records: GET /v2/conferenceRecords?filter=space.meeting_code="<code>" — one per session
|
|
7
|
+
// 3. transcripts: GET /v2/<record>/transcripts — state STARTED, ENDED or FILE_GENERATED
|
|
8
|
+
// 4. entries: GET /v2/<transcript>/entries — the spoken lines, each naming a participant
|
|
9
|
+
// 5. names: GET /v2/<record>/participants — the participants' display names
|
|
10
|
+
//
|
|
11
|
+
// WHAT THE ADMIN GRANTS: the Google Meet REST API on the service account's Cloud project, and domain-wide
|
|
12
|
+
// delegation of `https://www.googleapis.com/auth/meetings.space.readonly` to the service account's client id
|
|
13
|
+
// in the Workspace Admin console. The meeting needs transcription on. Google keeps transcript entries for 30
|
|
14
|
+
// days after the conference ends, so an older record is `expired`.
|
|
15
|
+
import { createSign } from "node:crypto";
|
|
16
|
+
import { TokenCache, onHost, readJsonLoose, unexpectedStatus, timedFetch } from "./native-http.js";
|
|
17
|
+
const MEET = "https://meet.googleapis.com/v2";
|
|
18
|
+
const TOKEN_URI = "https://oauth2.googleapis.com/token";
|
|
19
|
+
export const MEET_SCOPE = "https://www.googleapis.com/auth/meetings.space.readonly";
|
|
20
|
+
const MAX_PAGES = 100;
|
|
21
|
+
export const MEET_ACCESS_HINT = "In the Google Workspace Admin console, grant the service account's client id domain-wide delegation of https://www.googleapis.com/auth/meetings.space.readonly, and enable the Google Meet REST API in its Cloud project.";
|
|
22
|
+
const b64url = (v) => Buffer.from(v).toString("base64url");
|
|
23
|
+
/** The signed JWT-bearer assertion for one organiser. The test verifies its signature from the token request. */
|
|
24
|
+
function meetAssertion(key, subject, nowMs) {
|
|
25
|
+
const iat = Math.floor(nowMs / 1000);
|
|
26
|
+
const head = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
27
|
+
const claims = b64url(JSON.stringify({ iss: key.client_email, sub: subject, scope: MEET_SCOPE, aud: TOKEN_URI, iat, exp: iat + 3600 }));
|
|
28
|
+
const signer = createSign("RSA-SHA256");
|
|
29
|
+
signer.update(`${head}.${claims}`);
|
|
30
|
+
return `${head}.${claims}.${b64url(signer.sign(key.private_key))}`;
|
|
31
|
+
}
|
|
32
|
+
function str(o, k) {
|
|
33
|
+
const v = o?.[k];
|
|
34
|
+
return typeof v === "string" ? v : undefined;
|
|
35
|
+
}
|
|
36
|
+
export function meetTranscriptSource(fetchImpl = timedFetch, now = Date.now) {
|
|
37
|
+
const tokens = new TokenCache();
|
|
38
|
+
async function token(keyJson, subject) {
|
|
39
|
+
let key;
|
|
40
|
+
try {
|
|
41
|
+
const k = JSON.parse(keyJson);
|
|
42
|
+
if (typeof k.client_email !== "string" || typeof k.private_key !== "string")
|
|
43
|
+
throw new Error("shape");
|
|
44
|
+
key = { client_email: k.client_email, private_key: k.private_key };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { status: "refused", reason: "credential", hint: "The stored service-account key is not a Google JSON key file." };
|
|
48
|
+
}
|
|
49
|
+
const cacheKey = `${key.client_email}\n${subject}`;
|
|
50
|
+
const cached = tokens.get(cacheKey, now());
|
|
51
|
+
if (cached !== undefined)
|
|
52
|
+
return cached;
|
|
53
|
+
let assertion;
|
|
54
|
+
try {
|
|
55
|
+
assertion = meetAssertion(key, subject, now());
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { status: "refused", reason: "credential", hint: "The stored service-account key's private key does not sign." };
|
|
59
|
+
}
|
|
60
|
+
const res = await fetchImpl(TOKEN_URI, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
63
|
+
body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion }).toString(),
|
|
64
|
+
});
|
|
65
|
+
const body = await readJsonLoose(res);
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
// `unauthorized_client`: the delegation for this scope is not granted. Anything else on a 4xx is the key.
|
|
68
|
+
if (body.error === "unauthorized_client")
|
|
69
|
+
return { status: "refused", reason: "access-off", hint: MEET_ACCESS_HINT };
|
|
70
|
+
if (res.status >= 400 && res.status < 500)
|
|
71
|
+
return { status: "refused", reason: "credential", hint: "Google refused the service account's key, or the organiser is not a user in the Workspace." };
|
|
72
|
+
throw unexpectedStatus("the Google token endpoint", res.status);
|
|
73
|
+
}
|
|
74
|
+
if (typeof body.access_token !== "string")
|
|
75
|
+
throw unexpectedStatus("the Google token endpoint", res.status);
|
|
76
|
+
tokens.set(cacheKey, body.access_token, body.expires_in, now());
|
|
77
|
+
return body.access_token;
|
|
78
|
+
}
|
|
79
|
+
async function get(url, bearer, what) {
|
|
80
|
+
if (!onHost(url, "meet.googleapis.com"))
|
|
81
|
+
throw new Error(`${what}: refusing to send the token off meet.googleapis.com`);
|
|
82
|
+
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${bearer}` } });
|
|
83
|
+
if (res.status === 401)
|
|
84
|
+
return { ok: false, result: { status: "refused", reason: "credential", hint: "Google refused the service account's token." } };
|
|
85
|
+
if (res.status === 403)
|
|
86
|
+
return { ok: false, result: { status: "refused", reason: "access-off", hint: MEET_ACCESS_HINT } };
|
|
87
|
+
if (res.status === 404)
|
|
88
|
+
return { ok: false, result: { status: "refused", reason: "expired" } };
|
|
89
|
+
if (!res.ok)
|
|
90
|
+
throw unexpectedStatus(what, res.status);
|
|
91
|
+
return { ok: true, body: await readJsonLoose(res) };
|
|
92
|
+
}
|
|
93
|
+
/** Every page of a list, by `nextPageToken`. A refusal on any page is the answer. */
|
|
94
|
+
async function list(base, field, bearer, what) {
|
|
95
|
+
const out = [];
|
|
96
|
+
let pageToken;
|
|
97
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
98
|
+
const sep = base.includes("?") ? "&" : "?";
|
|
99
|
+
const got = await get(`${base}${sep}pageSize=100${pageToken !== undefined ? `&pageToken=${encodeURIComponent(pageToken)}` : ""}`, bearer, what);
|
|
100
|
+
if (!got.ok)
|
|
101
|
+
return got.result;
|
|
102
|
+
const items = got.body[field];
|
|
103
|
+
if (Array.isArray(items))
|
|
104
|
+
out.push(...items);
|
|
105
|
+
pageToken = str(got.body, "nextPageToken");
|
|
106
|
+
if (pageToken === undefined || pageToken === "")
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
platform: "meet",
|
|
113
|
+
disclosure: "OE reads the transcript Google Meet made, through the Meet REST API, with the organisation's own service account acting as the organiser (domain-wide delegation, read-only).",
|
|
114
|
+
async read(ref, ctx) {
|
|
115
|
+
const m = ctx.policy.meet;
|
|
116
|
+
if (m === undefined)
|
|
117
|
+
return { status: "refused", reason: "credential", hint: "The meeting policy declares no Google Meet credential." };
|
|
118
|
+
const subject = ref.organiserEmail;
|
|
119
|
+
if (subject === undefined || !m.domains.includes(subject.slice(subject.lastIndexOf("@") + 1)))
|
|
120
|
+
return { status: "refused", reason: "not-found" };
|
|
121
|
+
const tok = await token(await ctx.secret(m.serviceAccountKeyRef), subject);
|
|
122
|
+
if (typeof tok !== "string")
|
|
123
|
+
return tok;
|
|
124
|
+
const filter = `space.meeting_code = "${ref.roomId}"`;
|
|
125
|
+
const listed = await list(`${MEET}/conferenceRecords?filter=${encodeURIComponent(filter)}`, "conferenceRecords", tok, "the Meet conference list");
|
|
126
|
+
if (!Array.isArray(listed))
|
|
127
|
+
return listed.status === "refused" && listed.reason === "expired" ? { status: "refused", reason: "not-found" } : listed;
|
|
128
|
+
const records = listed
|
|
129
|
+
.map((r) => ({ name: str(r, "name") ?? "", ...(str(r, "startTime") ? { startTime: str(r, "startTime") } : {}), ...(str(r, "endTime") ? { endTime: str(r, "endTime") } : {}), ...(str(r, "expireTime") ? { expireTime: str(r, "expireTime") } : {}) }))
|
|
130
|
+
.filter((r) => r.name.startsWith("conferenceRecords/") && (ref.startedAfter === undefined || Date.parse(r.startTime ?? "") >= ref.startedAfter))
|
|
131
|
+
.sort((a, b) => Date.parse(b.startTime ?? "") - Date.parse(a.startTime ?? ""));
|
|
132
|
+
const rec = records[0];
|
|
133
|
+
// No session yet, or the session is on now.
|
|
134
|
+
if (rec === undefined || rec.endTime === undefined)
|
|
135
|
+
return { status: "pending" };
|
|
136
|
+
if (rec.expireTime !== undefined && Date.parse(rec.expireTime) <= now())
|
|
137
|
+
return { status: "refused", reason: "expired" };
|
|
138
|
+
const transcripts = await list(`${MEET}/${rec.name}/transcripts`, "transcripts", tok, "the Meet transcript list");
|
|
139
|
+
if (!Array.isArray(transcripts))
|
|
140
|
+
return transcripts;
|
|
141
|
+
if (transcripts.length === 0)
|
|
142
|
+
return { status: "refused", reason: "not-transcribed" };
|
|
143
|
+
const done = transcripts.filter((t) => str(t, "state") === "ENDED" || str(t, "state") === "FILE_GENERATED");
|
|
144
|
+
if (done.length < transcripts.length)
|
|
145
|
+
return { status: "pending" };
|
|
146
|
+
const people = await list(`${MEET}/${rec.name}/participants`, "participants", tok, "the Meet participant list");
|
|
147
|
+
if (!Array.isArray(people))
|
|
148
|
+
return people;
|
|
149
|
+
const names = new Map();
|
|
150
|
+
for (const p of people) {
|
|
151
|
+
const o = p;
|
|
152
|
+
const who = str(o.signedinUser, "displayName") ?? str(o.anonymousUser, "displayName") ?? str(o.phoneUser, "displayName");
|
|
153
|
+
const name = str(o, "name");
|
|
154
|
+
if (name !== undefined && who !== undefined)
|
|
155
|
+
names.set(name, who);
|
|
156
|
+
}
|
|
157
|
+
const start = Date.parse(rec.startTime ?? "");
|
|
158
|
+
const segments = [];
|
|
159
|
+
for (const t of done) {
|
|
160
|
+
const entries = await list(`${MEET}/${str(t, "name")}/entries`, "transcriptEntries", tok, "the Meet transcript entries");
|
|
161
|
+
if (!Array.isArray(entries))
|
|
162
|
+
return entries;
|
|
163
|
+
for (const e of entries) {
|
|
164
|
+
const text = str(e, "text");
|
|
165
|
+
if (text === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
const at = Date.parse(str(e, "startTime") ?? "");
|
|
168
|
+
segments.push({ speaker: names.get(str(e, "participant") ?? "") ?? "", text, ...(Number.isFinite(at) && Number.isFinite(start) ? { startMs: Math.max(0, at - start) } : {}) });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
segments.sort((a, b) => (a.startMs ?? 0) - (b.startMs ?? 0));
|
|
172
|
+
return { status: "ready", segments, ...(rec.startTime !== undefined ? { startedAt: rec.startTime } : {}), endedAt: rec.endTime };
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=native-meet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-meet.js","sourceRoot":"","sources":["../../../agent/meetings/native-meet.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-sources.d.ts","sourceRoot":"","sources":["../../../agent/meetings/native-sources.ts"],"names":[],"mappings":"AASA,wBAAgB,+BAA+B,IAAI,IAAI,CAItD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Boot registration of the three native-transcript readers (OPE-2471). Each one reads only when the meeting
|
|
2
|
+
// policy's `native` block declares its platform and a member approves a `meeting.recap` card, so registering
|
|
3
|
+
// all three is harmless on a deployment that uses none.
|
|
4
|
+
import { registerNativeTranscriptSource } from "@open-everywhere/core/agent/adapter-api.js";
|
|
5
|
+
import { teamsTranscriptSource } from "./native-teams.js";
|
|
6
|
+
import { meetTranscriptSource } from "./native-meet.js";
|
|
7
|
+
import { zoomTranscriptSource } from "./native-zoom.js";
|
|
8
|
+
export function registerNativeTranscriptSources() {
|
|
9
|
+
registerNativeTranscriptSource(teamsTranscriptSource());
|
|
10
|
+
registerNativeTranscriptSource(meetTranscriptSource());
|
|
11
|
+
registerNativeTranscriptSource(zoomTranscriptSource());
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=native-sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-sources.js","sourceRoot":"","sources":["../../../agent/meetings/native-sources.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAC5G,6GAA6G;AAC7G,wDAAwD;AAExD,OAAO,EAAE,8BAA8B,EAAE,MAAM,4CAA4C,CAAC;AAC5F,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,UAAU,+BAA+B;IAC7C,8BAA8B,CAAC,qBAAqB,EAAE,CAAC,CAAC;IACxD,8BAA8B,CAAC,oBAAoB,EAAE,CAAC,CAAC;IACvD,8BAA8B,CAAC,oBAAoB,EAAE,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { NativeTranscriptSource } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
import { type NativeFetch } from "./native-http.js";
|
|
3
|
+
/** The fixed hint for a 403: what an admin grants. Never response text. */
|
|
4
|
+
export declare const TEAMS_ACCESS_HINT = "In Microsoft Entra, grant the app OnlineMeetings.Read.All and OnlineMeetingTranscript.Read.All (application, admin consent), and add an application access policy that covers the organiser (Grant-CsApplicationAccessPolicy). The Teams meeting policy must allow transcription.";
|
|
5
|
+
export declare function teamsTranscriptSource(fetchImpl?: NativeFetch, now?: () => number): NativeTranscriptSource;
|
|
6
|
+
//# sourceMappingURL=native-teams.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-teams.d.ts","sourceRoot":"","sources":["../../../agent/meetings/native-teams.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAA0B,sBAAsB,EAAuC,MAAM,4CAA4C,CAAC;AAEtJ,OAAO,EAAuE,KAAK,WAAW,EAAc,MAAM,kBAAkB,CAAC;AAMrI,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,sRACuP,CAAC;AAQtR,wBAAgB,qBAAqB,CAAC,SAAS,GAAE,WAAwB,EAAE,GAAG,GAAE,MAAM,MAAiB,GAAG,sBAAsB,CAoF/H"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// The Microsoft Teams native-transcript reader (OPE-2471): Microsoft Graph `callTranscript`, read app-only
|
|
2
|
+
// with the organisation's Entra app registration (client credentials).
|
|
3
|
+
//
|
|
4
|
+
// 1. token: POST login.microsoftonline.com/<tenant>/oauth2/v2.0/token (client_credentials, `.default`)
|
|
5
|
+
// 2. meeting: GET /users/<organiser>/onlineMeetings?$filter=JoinWebUrl eq '<link>'
|
|
6
|
+
// 3. transcripts: GET /users/<organiser>/onlineMeetings/<id>/transcripts
|
|
7
|
+
// 4. content: GET …/transcripts/<id>/content?$format=text/vtt — WebVTT with `<v Speaker>` voice spans
|
|
8
|
+
//
|
|
9
|
+
// WHAT THE ADMIN GRANTS (docs/capabilities/meetings.md says it exactly): the application permissions
|
|
10
|
+
// `OnlineMeetings.Read.All` and `OnlineMeetingTranscript.Read.All` with admin consent, and an application
|
|
11
|
+
// access policy that covers the organisers (Graph refuses app-only meeting reads without one). A 403 at any
|
|
12
|
+
// step is the `access-off` refusal: the grant, the policy, or the tenant's transcript switches are off.
|
|
13
|
+
//
|
|
14
|
+
// Graph names a transcript only after the meeting ends, so "no transcript yet" is `pending`, and the watch's
|
|
15
|
+
// deadline turns a meeting that was never transcribed into a plain sentence in the thread.
|
|
16
|
+
import { parseWebVtt } from "@open-everywhere/core/agent/adapter-api.js";
|
|
17
|
+
import { TokenCache, onHost, readCappedText, readJsonLoose, unexpectedStatus, timedFetch } from "./native-http.js";
|
|
18
|
+
const GRAPH = "https://graph.microsoft.com/v1.0";
|
|
19
|
+
const LOGIN = "https://login.microsoftonline.com";
|
|
20
|
+
const MAX_PAGES = 10;
|
|
21
|
+
/** The fixed hint for a 403: what an admin grants. Never response text. */
|
|
22
|
+
export const TEAMS_ACCESS_HINT = "In Microsoft Entra, grant the app OnlineMeetings.Read.All and OnlineMeetingTranscript.Read.All (application, admin consent), and add an application access policy that covers the organiser (Grant-CsApplicationAccessPolicy). The Teams meeting policy must allow transcription.";
|
|
23
|
+
export function teamsTranscriptSource(fetchImpl = timedFetch, now = Date.now) {
|
|
24
|
+
const tokens = new TokenCache();
|
|
25
|
+
async function token(tenant, clientId, secret) {
|
|
26
|
+
const key = `${tenant}\n${clientId}`;
|
|
27
|
+
const cached = tokens.get(key, now());
|
|
28
|
+
if (cached !== undefined)
|
|
29
|
+
return cached;
|
|
30
|
+
const res = await fetchImpl(`${LOGIN}/${encodeURIComponent(tenant)}/oauth2/v2.0/token`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
33
|
+
body: new URLSearchParams({ client_id: clientId, client_secret: secret, scope: "https://graph.microsoft.com/.default", grant_type: "client_credentials" }).toString(),
|
|
34
|
+
});
|
|
35
|
+
const body = await readJsonLoose(res);
|
|
36
|
+
if (res.status === 400 || res.status === 401)
|
|
37
|
+
return { status: "refused", reason: "credential", hint: "Microsoft Entra refused the app's client id or client secret." };
|
|
38
|
+
if (!res.ok || typeof body.access_token !== "string")
|
|
39
|
+
throw unexpectedStatus("the Entra token endpoint", res.status);
|
|
40
|
+
tokens.set(key, body.access_token, body.expires_in, now());
|
|
41
|
+
return body.access_token;
|
|
42
|
+
}
|
|
43
|
+
/** One Graph GET. A refusal comes back as a result; anything unexpected throws (checked again later). */
|
|
44
|
+
async function graph(url, bearer, what, on404) {
|
|
45
|
+
if (!onHost(url, "graph.microsoft.com"))
|
|
46
|
+
throw new Error(`${what}: refusing to send the token off graph.microsoft.com`);
|
|
47
|
+
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${bearer}` } });
|
|
48
|
+
if (res.status === 401)
|
|
49
|
+
return { status: "refused", reason: "credential", hint: "Microsoft Graph refused the app's token." };
|
|
50
|
+
if (res.status === 403)
|
|
51
|
+
return { status: "refused", reason: "access-off", hint: TEAMS_ACCESS_HINT };
|
|
52
|
+
if (res.status === 404)
|
|
53
|
+
return on404;
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw unexpectedStatus(what, res.status);
|
|
56
|
+
return res;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
platform: "teams",
|
|
60
|
+
disclosure: "OE reads the transcript Microsoft Teams made, through Microsoft Graph, with the organisation's own Entra app registration (not any person's account).",
|
|
61
|
+
async read(ref, ctx) {
|
|
62
|
+
const t = ctx.policy.teams;
|
|
63
|
+
if (t === undefined)
|
|
64
|
+
return { status: "refused", reason: "credential", hint: "The meeting policy declares no Teams credential." };
|
|
65
|
+
if (ref.tenantId === undefined || ref.organizerId === undefined || !t.tenants.includes(ref.tenantId))
|
|
66
|
+
return { status: "refused", reason: "not-found" };
|
|
67
|
+
const tok = await token(ref.tenantId, t.clientId, await ctx.secret(t.clientSecretRef));
|
|
68
|
+
if (typeof tok !== "string")
|
|
69
|
+
return tok;
|
|
70
|
+
const user = `${GRAPH}/users/${encodeURIComponent(ref.organizerId)}/onlineMeetings`;
|
|
71
|
+
const filter = `JoinWebUrl eq '${ref.url.replace(/'/g, "''")}'`;
|
|
72
|
+
const found = await graph(`${user}?$filter=${encodeURIComponent(filter)}`, tok, "the Graph meeting lookup", { status: "refused", reason: "not-found" });
|
|
73
|
+
if (!(found instanceof Response))
|
|
74
|
+
return found;
|
|
75
|
+
const meetings = (await readJsonLoose(found)).value;
|
|
76
|
+
const meetingId = Array.isArray(meetings) && typeof meetings[0]?.id === "string" ? meetings[0].id : undefined;
|
|
77
|
+
if (meetingId === undefined)
|
|
78
|
+
return { status: "refused", reason: "not-found" };
|
|
79
|
+
const transcripts = [];
|
|
80
|
+
let next = `${user}/${encodeURIComponent(meetingId)}/transcripts`;
|
|
81
|
+
for (let page = 0; next !== undefined && page < MAX_PAGES; page++) {
|
|
82
|
+
const listed = await graph(next, tok, "the Graph transcript list", { status: "refused", reason: "not-found" });
|
|
83
|
+
if (!(listed instanceof Response))
|
|
84
|
+
return listed;
|
|
85
|
+
const body = await readJsonLoose(listed);
|
|
86
|
+
for (const v of Array.isArray(body.value) ? body.value : []) {
|
|
87
|
+
const o = v;
|
|
88
|
+
if (typeof o.id === "string") {
|
|
89
|
+
transcripts.push({
|
|
90
|
+
id: o.id,
|
|
91
|
+
...(typeof o.createdDateTime === "string" ? { createdDateTime: o.createdDateTime } : {}),
|
|
92
|
+
...(typeof o.endDateTime === "string" ? { endDateTime: o.endDateTime } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
next = typeof body["@odata.nextLink"] === "string" ? body["@odata.nextLink"] : undefined;
|
|
97
|
+
}
|
|
98
|
+
// A recurring meeting keeps one join link, so its transcripts span every occurrence: newest first, and
|
|
99
|
+
// only a session that started inside the ask's window.
|
|
100
|
+
const eligible = transcripts
|
|
101
|
+
.filter((x) => ref.startedAfter === undefined || (x.createdDateTime !== undefined && Date.parse(x.createdDateTime) >= ref.startedAfter))
|
|
102
|
+
.sort((a, b) => Date.parse(b.createdDateTime ?? "") - Date.parse(a.createdDateTime ?? ""));
|
|
103
|
+
const pick = eligible[0];
|
|
104
|
+
// No transcript yet (the meeting has not ended), or one Teams is still writing.
|
|
105
|
+
if (pick === undefined || pick.endDateTime === undefined)
|
|
106
|
+
return { status: "pending" };
|
|
107
|
+
const content = await graph(`${user}/${encodeURIComponent(meetingId)}/transcripts/${encodeURIComponent(pick.id)}/content?$format=text/vtt`, tok, "the Graph transcript content", {
|
|
108
|
+
status: "refused",
|
|
109
|
+
reason: "expired",
|
|
110
|
+
});
|
|
111
|
+
if (!(content instanceof Response))
|
|
112
|
+
return content;
|
|
113
|
+
const segments = parseWebVtt(await readCappedText(content));
|
|
114
|
+
return { status: "ready", segments, ...(pick.createdDateTime !== undefined ? { startedAt: pick.createdDateTime } : {}), endedAt: pick.endDateTime };
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=native-teams.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-teams.js","sourceRoot":"","sources":["../../../agent/meetings/native-teams.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NativeTranscriptSource } from "@open-everywhere/core/agent/adapter-api.js";
|
|
2
|
+
import { type NativeFetch } from "./native-http.js";
|
|
3
|
+
/** After this long with a finished recording and no transcript file, the transcript is not coming. */
|
|
4
|
+
export declare const ZOOM_TRANSCRIPT_GRACE_MS: number;
|
|
5
|
+
export declare const ZOOM_ACCESS_HINT = "In the Zoom App Marketplace, give the Server-to-Server OAuth app the scope cloud_recording:read:list_recording_files:admin (classic: recording:read:admin), and turn on cloud recording with Create audio transcript in the account settings.";
|
|
6
|
+
export declare function zoomTranscriptSource(fetchImpl?: NativeFetch, now?: () => number): NativeTranscriptSource;
|
|
7
|
+
//# sourceMappingURL=native-zoom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-zoom.d.ts","sourceRoot":"","sources":["../../../agent/meetings/native-zoom.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAA0B,sBAAsB,EAAuC,MAAM,4CAA4C,CAAC;AAEtJ,OAAO,EAAuE,KAAK,WAAW,EAAc,MAAM,kBAAkB,CAAC;AAIrI,sGAAsG;AACtG,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AAEzD,eAAO,MAAM,gBAAgB,kPACoN,CAAC;AASlP,wBAAgB,oBAAoB,CAAC,SAAS,GAAE,WAAwB,EAAE,GAAG,GAAE,MAAM,MAAiB,GAAG,sBAAsB,CA6D9H"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The Zoom native-transcript reader (OPE-2471): the cloud recording's audio transcript, read with the
|
|
2
|
+
// organisation's own Server-to-Server OAuth app.
|
|
3
|
+
//
|
|
4
|
+
// 1. token: POST zoom.us/oauth/token?grant_type=account_credentials&account_id=<account> (Basic auth)
|
|
5
|
+
// 2. recording: GET api.zoom.us/v2/meetings/<meeting id>/recordings — the newest session's files
|
|
6
|
+
// 3. transcript: the `TRANSCRIPT` file's `download_url`, with the token — WebVTT whose cues start
|
|
7
|
+
// "Speaker Name: text"
|
|
8
|
+
//
|
|
9
|
+
// WHAT THE ADMIN GRANTS: a Server-to-Server OAuth app on the org's Zoom account with the scope
|
|
10
|
+
// `cloud_recording:read:list_recording_files:admin` (classic scope: `recording:read:admin`), and, in the
|
|
11
|
+
// account settings, cloud recording with "Create audio transcript" on. Zoom needs a paid plan for both.
|
|
12
|
+
//
|
|
13
|
+
// HOW READY IS LEARNED: Zoom has a `recording.transcript_completed` webhook, and it is reported unreliable;
|
|
14
|
+
// OE polls this endpoint instead (meetings/native-recap.ts). Zoom writes the transcript after the recording,
|
|
15
|
+
// so a recording without a transcript file is `pending` until a day after it ended, then `not-transcribed`.
|
|
16
|
+
// The token goes only to zoom.us hosts: a `download_url` anywhere else is refused, never fetched.
|
|
17
|
+
import { parseWebVtt } from "@open-everywhere/core/agent/adapter-api.js";
|
|
18
|
+
import { TokenCache, onHost, readCappedText, readJsonLoose, unexpectedStatus, timedFetch } from "./native-http.js";
|
|
19
|
+
const API = "https://api.zoom.us/v2";
|
|
20
|
+
const TOKEN = "https://zoom.us/oauth/token";
|
|
21
|
+
/** After this long with a finished recording and no transcript file, the transcript is not coming. */
|
|
22
|
+
export const ZOOM_TRANSCRIPT_GRACE_MS = 24 * 60 * 60_000;
|
|
23
|
+
export const ZOOM_ACCESS_HINT = "In the Zoom App Marketplace, give the Server-to-Server OAuth app the scope cloud_recording:read:list_recording_files:admin (classic: recording:read:admin), and turn on cloud recording with Create audio transcript in the account settings.";
|
|
24
|
+
export function zoomTranscriptSource(fetchImpl = timedFetch, now = Date.now) {
|
|
25
|
+
const tokens = new TokenCache();
|
|
26
|
+
async function token(accountId, clientId, secret) {
|
|
27
|
+
const key = `${accountId}\n${clientId}`;
|
|
28
|
+
const cached = tokens.get(key, now());
|
|
29
|
+
if (cached !== undefined)
|
|
30
|
+
return cached;
|
|
31
|
+
const res = await fetchImpl(`${TOKEN}?grant_type=account_credentials&account_id=${encodeURIComponent(accountId)}`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { authorization: `Basic ${Buffer.from(`${clientId}:${secret}`).toString("base64")}` },
|
|
34
|
+
});
|
|
35
|
+
const body = await readJsonLoose(res);
|
|
36
|
+
if (res.status === 400 || res.status === 401)
|
|
37
|
+
return { status: "refused", reason: "credential", hint: "Zoom refused the Server-to-Server app's account id, client id or client secret." };
|
|
38
|
+
if (!res.ok || typeof body.access_token !== "string")
|
|
39
|
+
throw unexpectedStatus("the Zoom token endpoint", res.status);
|
|
40
|
+
tokens.set(key, body.access_token, body.expires_in, now());
|
|
41
|
+
return body.access_token;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
platform: "zoom",
|
|
45
|
+
disclosure: "OE reads the audio transcript of the meeting's Zoom cloud recording, through the Zoom API, with the organisation's own Server-to-Server OAuth app (not any person's account).",
|
|
46
|
+
async read(ref, ctx) {
|
|
47
|
+
const z = ctx.policy.zoom;
|
|
48
|
+
if (z === undefined)
|
|
49
|
+
return { status: "refused", reason: "credential", hint: "The meeting policy declares no Zoom credential." };
|
|
50
|
+
const tok = await token(z.accountId, z.clientId, await ctx.secret(z.clientSecretRef));
|
|
51
|
+
if (typeof tok !== "string")
|
|
52
|
+
return tok;
|
|
53
|
+
const res = await fetchImpl(`${API}/meetings/${encodeURIComponent(ref.roomId)}/recordings`, { headers: { authorization: `Bearer ${tok}` } });
|
|
54
|
+
const body = await readJsonLoose(res);
|
|
55
|
+
if (res.status === 401)
|
|
56
|
+
return { status: "refused", reason: "credential", hint: "Zoom refused the app's token." };
|
|
57
|
+
if (res.status === 403)
|
|
58
|
+
return { status: "refused", reason: "access-off", hint: ZOOM_ACCESS_HINT };
|
|
59
|
+
if (res.status === 404) {
|
|
60
|
+
// 3301 "This recording does not exist": no cloud recording yet (or never). 3001: no such meeting on this account.
|
|
61
|
+
if (body.code === 3001)
|
|
62
|
+
return { status: "refused", reason: "not-found" };
|
|
63
|
+
return { status: "pending" };
|
|
64
|
+
}
|
|
65
|
+
// A missing scope can come back as a 400 (code 4711, "does not contain scopes") as well as a 403; code 200 is "no permission".
|
|
66
|
+
if (res.status === 400 && (body.code === 200 || body.code === 4711))
|
|
67
|
+
return { status: "refused", reason: "access-off", hint: ZOOM_ACCESS_HINT };
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
throw unexpectedStatus("the Zoom recordings endpoint", res.status);
|
|
70
|
+
const startedAt = typeof body.start_time === "string" ? body.start_time : undefined;
|
|
71
|
+
// A recurring meeting answers with its newest session: one from before the ask's window is not the meeting asked for.
|
|
72
|
+
if (ref.startedAfter !== undefined && (startedAt === undefined || Date.parse(startedAt) < ref.startedAfter))
|
|
73
|
+
return { status: "pending" };
|
|
74
|
+
const files = (Array.isArray(body.recording_files) ? body.recording_files : []);
|
|
75
|
+
const transcript = files.find((f) => f.file_type === "TRANSCRIPT");
|
|
76
|
+
if (transcript === undefined || transcript.status !== "completed" || typeof transcript.download_url !== "string") {
|
|
77
|
+
const ends = files.map((f) => Date.parse(f.recording_end ?? "")).filter((t) => Number.isFinite(t));
|
|
78
|
+
const finished = files.length > 0 && files.every((f) => f.status === "completed") && ends.length > 0;
|
|
79
|
+
if (transcript === undefined && finished && now() - Math.max(...ends) > ZOOM_TRANSCRIPT_GRACE_MS)
|
|
80
|
+
return { status: "refused", reason: "not-transcribed" };
|
|
81
|
+
return { status: "pending" };
|
|
82
|
+
}
|
|
83
|
+
if (!onHost(transcript.download_url, "zoom.us"))
|
|
84
|
+
throw new Error("the Zoom transcript download_url is not on zoom.us, so the token is not sent there");
|
|
85
|
+
const dl = await fetchImpl(transcript.download_url, { headers: { authorization: `Bearer ${tok}` } });
|
|
86
|
+
if (dl.status === 404)
|
|
87
|
+
return { status: "refused", reason: "expired" };
|
|
88
|
+
if (dl.status === 401 || dl.status === 403)
|
|
89
|
+
return { status: "refused", reason: "access-off", hint: ZOOM_ACCESS_HINT };
|
|
90
|
+
if (!dl.ok)
|
|
91
|
+
throw unexpectedStatus("the Zoom transcript download", dl.status);
|
|
92
|
+
const segments = parseWebVtt(await readCappedText(dl), { namePrefix: true });
|
|
93
|
+
const ends = files.map((f) => f.recording_end).filter((t) => typeof t === "string").sort();
|
|
94
|
+
return { status: "ready", segments, ...(startedAt !== undefined ? { startedAt } : {}), ...(ends.length > 0 ? { endedAt: ends[ends.length - 1] } : {}) };
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=native-zoom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-zoom.js","sourceRoot":"","sources":["../../../agent/meetings/native-zoom.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,13 @@ type GatewayModel = ReturnType<typeof gateway>;
|
|
|
5
5
|
export declare const DEFAULT_GATEWAY_HOST = "ai-gateway.vercel.sh";
|
|
6
6
|
/** The one host the model call reaches: the retarget's host, else the default. An unparseable retarget falls back to the default host. */
|
|
7
7
|
export declare function gatewayHost(baseUrl: string | undefined): string;
|
|
8
|
+
/** The one host this deployment's gateway calls reach — what every host-plane push must keep declaring. */
|
|
9
|
+
export declare const GATEWAY_TARGET_HOST: string;
|
|
10
|
+
/** Add `hosts` to this process's host plane and return the shared fetch (undefined off the proxy tier). */
|
|
11
|
+
export declare function sharedHostPlaneFetch(hosts: readonly string[]): typeof fetch | undefined;
|
|
12
|
+
/** The gateway provider this backend resolves model ids through — retargeted only when
|
|
13
|
+
* {@link GATEWAY_BASE_URL} names a different host. */
|
|
14
|
+
declare const provider: import("@ai-sdk/gateway").GatewayProvider;
|
|
8
15
|
/** Static defaults for one routed slot — mirrors what a plain string config said. */
|
|
9
16
|
export interface GatewayRoutedDefaults {
|
|
10
17
|
/** Default gateway model id (what unrouted channels use), e.g. "zai/glm-5.2". */
|
|
@@ -23,6 +30,9 @@ export declare function applyGatewayOptions(route: ModelRoute, options: RoutedCa
|
|
|
23
30
|
readonly id: string;
|
|
24
31
|
readonly defaultId: string;
|
|
25
32
|
}, defaults?: Record<string, unknown>): RoutedCallOptions;
|
|
33
|
+
/** A gateway TRANSCRIPTION model, on the same provider (retarget and host-plane fetch included) the turn
|
|
34
|
+
* model uses. The meeting bot's `gateway` route (meeting-gateway.ts) reads it. */
|
|
35
|
+
export declare function gatewayTranscriptionModel(id: string): ReturnType<typeof provider.transcriptionModel>;
|
|
26
36
|
/** The gateway ModelBackend (see core ModelBackend). */
|
|
27
37
|
export declare function gatewayModelBackend(defaults?: Record<string, unknown>): ModelBackend<GatewayModel>;
|
|
28
38
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../agent/model/gateway.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAiB,MAAM,IAAI,CAAC;AAC5C,OAAO,EACL,KAAK,UAAU,EAEf,KAAK,YAAY,EACjB,KAAK,iBAAiB,
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../agent/model/gateway.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAiB,MAAM,IAAI,CAAC;AAC5C,OAAO,EACL,KAAK,UAAU,EAEf,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAKvB,MAAM,4CAA4C,CAAC;AAEpD,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAQ/C,wHAAwH;AACxH,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,0IAA0I;AAC1I,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAO/D;AAED,2GAA2G;AAC3G,eAAO,MAAM,mBAAmB,QAAgC,CAAC;AA8BjE,2GAA2G;AAC3G,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,KAAK,GAAG,SAAS,CAOvF;AAED;uDACuD;AACvD,QAAA,MAAM,QAAQ,2CAGD,CAAC;AAEd,qFAAqF;AACrF,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AAKD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,iBAAiB,EAC1B,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EACxD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,iBAAiB,CAanB;AAED;mFACmF;AACnF,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,kBAAkB,CAAC,CAEpG;AAED,wDAAwD;AACxD,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,CAAC,YAAY,CAAC,CAKlG;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,YAAY,CAAC,CAG5H"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The Vercel AI Gateway model backend — the reference `ModelBackend`. Provider SELECTION is not pinned here:
|
|
2
2
|
// that is a gateway-operator concern, and `ModelRoute.providers` is only an optional per-route preference.
|
|
3
3
|
import { gateway, createGateway } from "ai";
|
|
4
|
-
import { hostPlaneFetch, liveIdentity, resolvePrimaryModel, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
4
|
+
import { hostPlaneFetch, hostPlanePolicy, liveIdentity, resolvePrimaryModel, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
5
5
|
/**
|
|
6
6
|
* THE GATEWAY TARGET — `OE_AI_GATEWAY_URL`, read once at module load: a target that changed mid-process would
|
|
7
7
|
* send two calls in one turn to two providers. Unset ⇒ the plain `gateway` export, byte-identical.
|
|
@@ -20,13 +20,46 @@ export function gatewayHost(baseUrl) {
|
|
|
20
20
|
return DEFAULT_GATEWAY_HOST;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/** The one host this deployment's gateway calls reach — what every host-plane push must keep declaring. */
|
|
24
|
+
export const GATEWAY_TARGET_HOST = gatewayHost(GATEWAY_BASE_URL);
|
|
25
|
+
/**
|
|
26
|
+
* THE HOST-PLANE UNION (OPE-2470). The proxy REPLACES its host plane on every push; it never merges
|
|
27
|
+
* (deploy/egress-proxy/control/handlers.ts `setHostPlane`). So this process keeps ONE host set, and every
|
|
28
|
+
* host-plane call goes through ONE fetch that pushes the whole set: the turn model and the meeting bot's
|
|
29
|
+
* non-gateway transcription routes never push lists that drop each other. A caller adds its hosts with
|
|
30
|
+
* {@link sharedHostPlaneFetch} at boot; a grown set builds a new fetch, which pushes the union on its next call.
|
|
31
|
+
*/
|
|
32
|
+
const HOST_PLANE_HOSTS = [GATEWAY_TARGET_HOST];
|
|
33
|
+
let hostPlaneCurrent = hostPlaneFetch(HOST_PLANE_HOSTS);
|
|
34
|
+
let hostPlaneBuiltFor = HOST_PLANE_HOSTS.length;
|
|
23
35
|
/**
|
|
24
36
|
* THE MODEL CALL'S TRANSPORT ON THE SELF-HOST PROXY TIER (OPE-2406). With an egress proxy wired, the model call goes out
|
|
25
|
-
* on the app's HOST PLANE: the proxy decides it against the gateway host
|
|
26
|
-
*
|
|
27
|
-
*
|
|
37
|
+
* on the app's HOST PLANE: the proxy decides it against the host-plane list (the gateway host, plus the hosts of any
|
|
38
|
+
* non-gateway transcription route the app registers), never against a session's policy. That is what lets a turn reach
|
|
39
|
+
* its model while the session stays default-deny. Off that tier it is `undefined` and the provider keeps its default
|
|
40
|
+
* fetch, byte-identical to before. On it, the fetch always uses the newest union.
|
|
28
41
|
*/
|
|
29
|
-
const HOST_PLANE_FETCH =
|
|
42
|
+
const HOST_PLANE_FETCH = hostPlaneCurrent === undefined
|
|
43
|
+
? undefined
|
|
44
|
+
: (input, init) => {
|
|
45
|
+
if (hostPlaneBuiltFor !== HOST_PLANE_HOSTS.length) {
|
|
46
|
+
hostPlaneCurrent = hostPlaneFetch(HOST_PLANE_HOSTS);
|
|
47
|
+
hostPlaneBuiltFor = HOST_PLANE_HOSTS.length;
|
|
48
|
+
}
|
|
49
|
+
return hostPlaneCurrent(input, init);
|
|
50
|
+
};
|
|
51
|
+
/** Add `hosts` to this process's host plane and return the shared fetch (undefined off the proxy tier). */
|
|
52
|
+
export function sharedHostPlaneFetch(hosts) {
|
|
53
|
+
// Validate BEFORE the list grows: a wildcard host throws here, at registration, and never reaches the shared list,
|
|
54
|
+
// where it would fail every model turn at the next rebuild. A blank name is skipped.
|
|
55
|
+
const named = hosts.map((h) => h.trim()).filter((h) => h !== "");
|
|
56
|
+
if (HOST_PLANE_FETCH !== undefined)
|
|
57
|
+
hostPlanePolicy([...HOST_PLANE_HOSTS, ...named]);
|
|
58
|
+
for (const h of named)
|
|
59
|
+
if (!HOST_PLANE_HOSTS.includes(h))
|
|
60
|
+
HOST_PLANE_HOSTS.push(h);
|
|
61
|
+
return HOST_PLANE_FETCH;
|
|
62
|
+
}
|
|
30
63
|
/** The gateway provider this backend resolves model ids through — retargeted only when
|
|
31
64
|
* {@link GATEWAY_BASE_URL} names a different host. */
|
|
32
65
|
const provider = GATEWAY_BASE_URL || HOST_PLANE_FETCH
|
|
@@ -54,6 +87,11 @@ export function applyGatewayOptions(route, options, ids, defaults) {
|
|
|
54
87
|
return options;
|
|
55
88
|
return { ...options, providerOptions: { ...options.providerOptions, gateway: gatewayOpts } };
|
|
56
89
|
}
|
|
90
|
+
/** A gateway TRANSCRIPTION model, on the same provider (retarget and host-plane fetch included) the turn
|
|
91
|
+
* model uses. The meeting bot's `gateway` route (meeting-gateway.ts) reads it. */
|
|
92
|
+
export function gatewayTranscriptionModel(id) {
|
|
93
|
+
return provider.transcriptionModel(id);
|
|
94
|
+
}
|
|
57
95
|
/** The gateway ModelBackend (see core ModelBackend). */
|
|
58
96
|
export function gatewayModelBackend(defaults) {
|
|
59
97
|
return {
|