@opengeni/sdk 3.3.2 → 3.7.0-canary.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/README.md +80 -6
- package/dist/artifact-client.d.ts +4 -2
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +6 -6
- package/dist/browser.d.ts +5 -2
- package/dist/browser.js +14 -4
- package/dist/{chunk-YXQX7RQT.js → chunk-2E2BFEEN.js} +2 -2
- package/dist/{chunk-JUS2UYRP.js → chunk-3QDQTYOE.js} +3 -1
- package/dist/chunk-3QDQTYOE.js.map +1 -0
- package/dist/{chunk-C2H7HBNP.js → chunk-7S2MZ65L.js} +6 -6
- package/dist/chunk-FMLDFJ2Q.js +246 -0
- package/dist/chunk-FMLDFJ2Q.js.map +1 -0
- package/dist/{chunk-TRNI7KEO.js → chunk-IQTEPOI3.js} +228 -26
- package/dist/chunk-IQTEPOI3.js.map +1 -0
- package/dist/chunk-IRAEPLXG.js +350 -0
- package/dist/chunk-IRAEPLXG.js.map +1 -0
- package/dist/{chunk-QT3KFVDP.js → chunk-MPGVC7EO.js} +10 -2
- package/dist/chunk-MPGVC7EO.js.map +1 -0
- package/dist/{chunk-C4DJPZRU.js → chunk-OO5LPTO7.js} +14 -1
- package/dist/chunk-OO5LPTO7.js.map +1 -0
- package/dist/{chunk-KKRO2VCP.js → chunk-QTBAMHEF.js} +2 -2
- package/dist/client.d.ts +51 -11
- package/dist/codex-realtime-controller.js +2 -2
- package/dist/core.d.ts +5 -2
- package/dist/core.js +17 -7
- package/dist/document-authority.js +5 -5
- package/dist/editable-artifacts.js +4 -4
- package/dist/errors.d.ts +12 -0
- package/dist/github-repositories.d.ts +10 -0
- package/dist/github-repositories.js +36 -0
- package/dist/github-repositories.js.map +1 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +99 -52
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +2 -2
- package/dist/model-context.d.ts +49 -0
- package/dist/model-picker-order.d.ts +1 -0
- package/dist/model-picker-order.js +10 -0
- package/dist/model-picker-order.js.map +1 -0
- package/dist/proxy.d.ts +7 -5
- package/dist/realtime.js +2 -2
- package/dist/session-titles.d.ts +2 -28
- package/dist/site.d.ts +93 -0
- package/dist/site.js +35 -0
- package/dist/site.js.map +1 -0
- package/dist/stream.d.ts +2 -0
- package/dist/tools-FSTY4FAY.js +12 -0
- package/dist/tools-FSTY4FAY.js.map +1 -0
- package/dist/tools.d.ts +61 -0
- package/dist/types.d.ts +231 -9
- package/dist/workspace-artifacts.d.ts +26 -1
- package/package.json +14 -2
- package/src/artifact-client.ts +16 -1
- package/src/artifacts.ts +1 -0
- package/src/browser.ts +16 -0
- package/src/client.ts +332 -22
- package/src/core.ts +16 -0
- package/src/errors.ts +23 -0
- package/src/github-repositories.ts +60 -0
- package/src/index.ts +94 -1
- package/src/model-context.ts +69 -0
- package/src/model-picker-order.ts +6 -0
- package/src/proxy.ts +17 -6
- package/src/session-titles.ts +8 -82
- package/src/site.ts +544 -0
- package/src/stream.ts +29 -8
- package/src/tools.ts +396 -0
- package/src/types.ts +287 -10
- package/src/workspace-artifacts.ts +33 -2
- package/dist/chunk-C4DJPZRU.js.map +0 -1
- package/dist/chunk-JUS2UYRP.js.map +0 -1
- package/dist/chunk-QT3KFVDP.js.map +0 -1
- package/dist/chunk-TRNI7KEO.js.map +0 -1
- /package/dist/{chunk-YXQX7RQT.js.map → chunk-2E2BFEEN.js.map} +0 -0
- /package/dist/{chunk-C2H7HBNP.js.map → chunk-7S2MZ65L.js.map} +0 -0
- /package/dist/{chunk-KKRO2VCP.js.map → chunk-QTBAMHEF.js.map} +0 -0
package/src/site.ts
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OpenGeniToolsClient,
|
|
3
|
+
type OpenGeniToolTransport,
|
|
4
|
+
type OpenGeniWorkspaceTools,
|
|
5
|
+
} from "./tools";
|
|
6
|
+
import type {
|
|
7
|
+
ToolGatewayCallRequest,
|
|
8
|
+
ToolGatewayCallResponse,
|
|
9
|
+
ToolGatewayCatalog,
|
|
10
|
+
ToolGatewayDeclarationsResponse,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export const OPENGENI_SITE_BRIDGE_VERSION = 2 as const;
|
|
14
|
+
export const OPENGENI_SITE_BRIDGE_CONNECT = "opengeni.site.connect" as const;
|
|
15
|
+
export const OPENGENI_SITE_BRIDGE_READY = "opengeni.site.ready" as const;
|
|
16
|
+
export const OPENGENI_SITE_BRIDGE_REQUEST = "opengeni.site.request" as const;
|
|
17
|
+
export const OPENGENI_SITE_BRIDGE_RESPONSE = "opengeni.site.response" as const;
|
|
18
|
+
export const OPENGENI_SITE_BRIDGE_CANCEL = "opengeni.site.cancel" as const;
|
|
19
|
+
export const OPENGENI_SITE_BRIDGE_BOOTSTRAP_GLOBAL = "__opengeniSiteBridgeBootstrapV2" as const;
|
|
20
|
+
export const OPENGENI_SITE_LOCAL_CODEMODE_PATH = "/__opengeni/site-tools" as const;
|
|
21
|
+
|
|
22
|
+
export type OpenGeniSiteBridgeConnectMessage = {
|
|
23
|
+
type: typeof OPENGENI_SITE_BRIDGE_CONNECT;
|
|
24
|
+
version: typeof OPENGENI_SITE_BRIDGE_VERSION;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type OpenGeniSiteBridgeReadyMessage = {
|
|
28
|
+
type: typeof OPENGENI_SITE_BRIDGE_READY;
|
|
29
|
+
version: typeof OPENGENI_SITE_BRIDGE_VERSION;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Site-originated calls cannot carry host-only approval tokens or Site-version authority. */
|
|
33
|
+
export type OpenGeniSiteToolCallRequest = Omit<
|
|
34
|
+
ToolGatewayCallRequest,
|
|
35
|
+
"approvalToken" | "siteArtifactId" | "siteVersionId"
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
/** Host-filtered catalog projection. Tenant routing context never enters Site code. */
|
|
39
|
+
export type OpenGeniSiteToolCatalog = Omit<ToolGatewayCatalog, "accountId" | "workspaceId">;
|
|
40
|
+
|
|
41
|
+
export type OpenGeniSiteWorkspaceTools = OpenGeniWorkspaceTools<OpenGeniSiteToolCatalog>;
|
|
42
|
+
|
|
43
|
+
export type OpenGeniSiteBridgeRequestMessage = {
|
|
44
|
+
type: typeof OPENGENI_SITE_BRIDGE_REQUEST;
|
|
45
|
+
version: typeof OPENGENI_SITE_BRIDGE_VERSION;
|
|
46
|
+
requestId: string;
|
|
47
|
+
} & (
|
|
48
|
+
| { method: "catalog" }
|
|
49
|
+
| { method: "call"; payload: OpenGeniSiteToolCallRequest }
|
|
50
|
+
| { method: "declarations" }
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export type OpenGeniSiteBridgeCancelMessage = {
|
|
54
|
+
type: typeof OPENGENI_SITE_BRIDGE_CANCEL;
|
|
55
|
+
version: typeof OPENGENI_SITE_BRIDGE_VERSION;
|
|
56
|
+
requestId: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type OpenGeniSiteBridgeResponseMessage = {
|
|
60
|
+
type: typeof OPENGENI_SITE_BRIDGE_RESPONSE;
|
|
61
|
+
version: typeof OPENGENI_SITE_BRIDGE_VERSION;
|
|
62
|
+
requestId: string;
|
|
63
|
+
} & (
|
|
64
|
+
| {
|
|
65
|
+
ok: true;
|
|
66
|
+
value: OpenGeniSiteToolCatalog | ToolGatewayCallResponse | ToolGatewayDeclarationsResponse;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
ok: false;
|
|
70
|
+
error: {
|
|
71
|
+
code: string;
|
|
72
|
+
message: string;
|
|
73
|
+
retryable: boolean;
|
|
74
|
+
outcomeUnknown?: boolean;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export type OpenGeniSiteClient = {
|
|
80
|
+
/** Workspace-bound typed facade. The parent host owns workspace identity and credentials. */
|
|
81
|
+
readonly tools: OpenGeniSiteWorkspaceTools;
|
|
82
|
+
close(): void;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type OpenGeniSiteClientOptions = {
|
|
86
|
+
/** Advanced embedding seam; ordinary Sites accept bootstrap only from their exact parent. */
|
|
87
|
+
parentWindow?: MessageEventSource;
|
|
88
|
+
/** Advanced embedding seam for listening to the host's document-bound bootstrap message. */
|
|
89
|
+
siteWindow?: Pick<Window, "parent" | "addEventListener" | "removeEventListener">;
|
|
90
|
+
/** Test/embedding seam for an already document-bound host bootstrap port. */
|
|
91
|
+
bootstrapPort?: MessagePort;
|
|
92
|
+
connectTimeoutMs?: number;
|
|
93
|
+
requestTimeoutMs?: number;
|
|
94
|
+
createMessageChannel?: () => MessageChannel;
|
|
95
|
+
/** Local-preview seam. Top-level pages use this same-origin Codemode proxy automatically. */
|
|
96
|
+
localCodemodePath?: string | false;
|
|
97
|
+
/** Test/embedding seam for local-preview requests. */
|
|
98
|
+
fetch?: typeof globalThis.fetch;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export class OpenGeniSiteBridgeError extends Error {
|
|
102
|
+
constructor(
|
|
103
|
+
readonly code: string,
|
|
104
|
+
message: string,
|
|
105
|
+
readonly retryable = false,
|
|
106
|
+
readonly outcomeUnknown = false,
|
|
107
|
+
) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.name = "OpenGeniSiteBridgeError";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create the browser client used by a Site. Published iframes use the parent
|
|
115
|
+
* MessagePort; top-level sandbox previews use the same-origin Codemode adapter.
|
|
116
|
+
* Credentials remain in the selected host and never enter Site code.
|
|
117
|
+
*/
|
|
118
|
+
export function createOpenGeniSiteClient(
|
|
119
|
+
options: OpenGeniSiteClientOptions = {},
|
|
120
|
+
): OpenGeniSiteClient {
|
|
121
|
+
const transport = shouldUseLocalCodemode(options)
|
|
122
|
+
? new OpenGeniSiteLocalCodemodeTransport(options)
|
|
123
|
+
: new OpenGeniSiteBridgeTransport(options);
|
|
124
|
+
return {
|
|
125
|
+
tools: new OpenGeniToolsClient(transport).forWorkspace("site-host"),
|
|
126
|
+
close: () => transport.close(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class OpenGeniSiteLocalCodemodeTransport implements OpenGeniToolTransport {
|
|
131
|
+
private readonly fetchImpl: typeof globalThis.fetch;
|
|
132
|
+
private readonly basePath: string;
|
|
133
|
+
|
|
134
|
+
constructor(options: OpenGeniSiteClientOptions) {
|
|
135
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
136
|
+
const configured = options.localCodemodePath;
|
|
137
|
+
this.basePath = (
|
|
138
|
+
typeof configured === "string" ? configured : OPENGENI_SITE_LOCAL_CODEMODE_PATH
|
|
139
|
+
).replace(/\/+$/u, "");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
close(): void {}
|
|
143
|
+
|
|
144
|
+
async requestJson<T>(
|
|
145
|
+
method: string,
|
|
146
|
+
path: string,
|
|
147
|
+
body?: unknown,
|
|
148
|
+
_query?: Record<string, string>,
|
|
149
|
+
options: { signal?: AbortSignal } = {},
|
|
150
|
+
): Promise<T> {
|
|
151
|
+
const suffix = localCodemodeSuffix(method, path);
|
|
152
|
+
const response = await this.fetchImpl(`${this.basePath}${suffix}`, {
|
|
153
|
+
method,
|
|
154
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
155
|
+
...(body === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: {
|
|
158
|
+
headers: { "content-type": "application/json" },
|
|
159
|
+
body: JSON.stringify(body),
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
const value = (await response.json()) as unknown;
|
|
163
|
+
if (response.ok) return value as T;
|
|
164
|
+
const envelope = isRecord(value) && isRecord(value.error) ? value.error : {};
|
|
165
|
+
throw new OpenGeniSiteBridgeError(
|
|
166
|
+
typeof envelope.code === "string" ? envelope.code : "local_codemode_error",
|
|
167
|
+
typeof envelope.message === "string"
|
|
168
|
+
? envelope.message
|
|
169
|
+
: `Local Site tool request failed with HTTP ${response.status}`,
|
|
170
|
+
envelope.retryable === true,
|
|
171
|
+
envelope.outcomeUnknown === true,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function shouldUseLocalCodemode(options: OpenGeniSiteClientOptions): boolean {
|
|
177
|
+
if (options.localCodemodePath === false || options.bootstrapPort) return false;
|
|
178
|
+
if (typeof options.localCodemodePath === "string") return true;
|
|
179
|
+
const siteWindow = options.siteWindow ?? globalThis.window;
|
|
180
|
+
return Boolean(siteWindow && siteWindow.parent === siteWindow);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function localCodemodeSuffix(method: string, path: string): string {
|
|
184
|
+
if (method === "GET" && path.endsWith("/tools/catalog")) return "/catalog";
|
|
185
|
+
if (method === "GET" && path.endsWith("/tools/declarations")) return "/declarations";
|
|
186
|
+
if (method === "POST" && path.endsWith("/tools/calls")) return "/calls";
|
|
187
|
+
throw new OpenGeniSiteBridgeError("unsupported_request", "Unsupported Site tool request");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function isOpenGeniSiteBridgeConnectMessage(
|
|
191
|
+
value: unknown,
|
|
192
|
+
): value is OpenGeniSiteBridgeConnectMessage {
|
|
193
|
+
return (
|
|
194
|
+
isRecord(value) &&
|
|
195
|
+
value.type === OPENGENI_SITE_BRIDGE_CONNECT &&
|
|
196
|
+
value.version === OPENGENI_SITE_BRIDGE_VERSION
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function isOpenGeniSiteBridgeRequestMessage(
|
|
201
|
+
value: unknown,
|
|
202
|
+
): value is OpenGeniSiteBridgeRequestMessage {
|
|
203
|
+
if (
|
|
204
|
+
!isRecord(value) ||
|
|
205
|
+
value.type !== OPENGENI_SITE_BRIDGE_REQUEST ||
|
|
206
|
+
value.version !== OPENGENI_SITE_BRIDGE_VERSION ||
|
|
207
|
+
typeof value.requestId !== "string" ||
|
|
208
|
+
!value.requestId
|
|
209
|
+
) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
if (value.method === "catalog" || value.method === "declarations") return true;
|
|
213
|
+
return value.method === "call" && isRecord(value.payload);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function isOpenGeniSiteBridgeCancelMessage(
|
|
217
|
+
value: unknown,
|
|
218
|
+
): value is OpenGeniSiteBridgeCancelMessage {
|
|
219
|
+
return (
|
|
220
|
+
isRecord(value) &&
|
|
221
|
+
value.type === OPENGENI_SITE_BRIDGE_CANCEL &&
|
|
222
|
+
value.version === OPENGENI_SITE_BRIDGE_VERSION &&
|
|
223
|
+
typeof value.requestId === "string" &&
|
|
224
|
+
Boolean(value.requestId)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Strip every host-only field before a call crosses the Site boundary. */
|
|
229
|
+
export function sanitizeOpenGeniSiteToolCallRequest(
|
|
230
|
+
value: ToolGatewayCallRequest,
|
|
231
|
+
): OpenGeniSiteToolCallRequest {
|
|
232
|
+
return {
|
|
233
|
+
...(value.operationId === undefined ? {} : { operationId: value.operationId }),
|
|
234
|
+
catalogDigest: value.catalogDigest,
|
|
235
|
+
identity: value.identity,
|
|
236
|
+
arguments: value.arguments,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
class OpenGeniSiteBridgeTransport implements OpenGeniToolTransport {
|
|
241
|
+
private readonly options: OpenGeniSiteClientOptions;
|
|
242
|
+
private readonly bootstrap: SiteBridgeBootstrap;
|
|
243
|
+
private port: MessagePort | null = null;
|
|
244
|
+
private connecting: Promise<MessagePort> | null = null;
|
|
245
|
+
private closed = false;
|
|
246
|
+
|
|
247
|
+
constructor(options: OpenGeniSiteClientOptions) {
|
|
248
|
+
this.options = options;
|
|
249
|
+
this.bootstrap = createSiteBridgeBootstrap(options);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async requestJson<T>(
|
|
253
|
+
method: string,
|
|
254
|
+
path: string,
|
|
255
|
+
body?: unknown,
|
|
256
|
+
_query?: Record<string, string>,
|
|
257
|
+
requestOptions: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
258
|
+
): Promise<T> {
|
|
259
|
+
if (method === "GET" && path.endsWith("/tools/catalog")) {
|
|
260
|
+
return (await this.request({ method: "catalog" }, requestOptions)) as T;
|
|
261
|
+
}
|
|
262
|
+
if (method === "GET" && path.endsWith("/tools/declarations")) {
|
|
263
|
+
return (await this.request({ method: "declarations" }, requestOptions)) as T;
|
|
264
|
+
}
|
|
265
|
+
if (method === "POST" && path.endsWith("/tools/calls")) {
|
|
266
|
+
if (!isRecord(body)) throw new TypeError("Site tool call payload is required");
|
|
267
|
+
return (await this.request(
|
|
268
|
+
{
|
|
269
|
+
method: "call",
|
|
270
|
+
payload: sanitizeOpenGeniSiteToolCallRequest(body as ToolGatewayCallRequest),
|
|
271
|
+
},
|
|
272
|
+
requestOptions,
|
|
273
|
+
)) as T;
|
|
274
|
+
}
|
|
275
|
+
throw new OpenGeniSiteBridgeError("unsupported_request", "Unsupported Site bridge request");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
close(): void {
|
|
279
|
+
if (this.closed) return;
|
|
280
|
+
this.closed = true;
|
|
281
|
+
this.bootstrap.close();
|
|
282
|
+
this.port?.close();
|
|
283
|
+
this.port = null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async request(
|
|
287
|
+
request:
|
|
288
|
+
| { method: "catalog" }
|
|
289
|
+
| { method: "call"; payload: OpenGeniSiteToolCallRequest }
|
|
290
|
+
| { method: "declarations" },
|
|
291
|
+
options: { signal?: AbortSignal; timeoutMs?: number },
|
|
292
|
+
): Promise<unknown> {
|
|
293
|
+
const port = await this.connect();
|
|
294
|
+
const requestId = crypto.randomUUID();
|
|
295
|
+
const timeoutMs = positiveTimeout(
|
|
296
|
+
options.timeoutMs ?? this.options.requestTimeoutMs ?? 120_000,
|
|
297
|
+
"requestTimeoutMs",
|
|
298
|
+
);
|
|
299
|
+
return await new Promise((resolve, reject) => {
|
|
300
|
+
const timeout = setTimeout(() => {
|
|
301
|
+
cleanup();
|
|
302
|
+
cancel(port, requestId);
|
|
303
|
+
reject(
|
|
304
|
+
request.method === "call"
|
|
305
|
+
? new OpenGeniSiteBridgeError(
|
|
306
|
+
"timeout",
|
|
307
|
+
"Site tool request timed out after execution may have started",
|
|
308
|
+
false,
|
|
309
|
+
true,
|
|
310
|
+
)
|
|
311
|
+
: new OpenGeniSiteBridgeError("timeout", "Site tool request timed out", true),
|
|
312
|
+
);
|
|
313
|
+
}, timeoutMs);
|
|
314
|
+
const abort = () => {
|
|
315
|
+
cleanup();
|
|
316
|
+
cancel(port, requestId);
|
|
317
|
+
reject(options.signal?.reason ?? new DOMException("Request aborted", "AbortError"));
|
|
318
|
+
};
|
|
319
|
+
const onMessage = (event: MessageEvent<unknown>) => {
|
|
320
|
+
const response = event.data;
|
|
321
|
+
if (
|
|
322
|
+
!isRecord(response) ||
|
|
323
|
+
response.type !== OPENGENI_SITE_BRIDGE_RESPONSE ||
|
|
324
|
+
response.version !== OPENGENI_SITE_BRIDGE_VERSION ||
|
|
325
|
+
response.requestId !== requestId ||
|
|
326
|
+
typeof response.ok !== "boolean"
|
|
327
|
+
) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
cleanup();
|
|
331
|
+
if (response.ok) {
|
|
332
|
+
resolve(response.value);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const error = isRecord(response.error) ? response.error : {};
|
|
336
|
+
reject(
|
|
337
|
+
new OpenGeniSiteBridgeError(
|
|
338
|
+
typeof error.code === "string" ? error.code : "bridge_error",
|
|
339
|
+
typeof error.message === "string" ? error.message : "Site tool request failed",
|
|
340
|
+
error.retryable === true,
|
|
341
|
+
error.outcomeUnknown === true,
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
};
|
|
345
|
+
const cleanup = () => {
|
|
346
|
+
clearTimeout(timeout);
|
|
347
|
+
options.signal?.removeEventListener("abort", abort);
|
|
348
|
+
port.removeEventListener("message", onMessage);
|
|
349
|
+
};
|
|
350
|
+
if (options.signal?.aborted) {
|
|
351
|
+
abort();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
355
|
+
port.addEventListener("message", onMessage);
|
|
356
|
+
port.postMessage({
|
|
357
|
+
type: OPENGENI_SITE_BRIDGE_REQUEST,
|
|
358
|
+
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
359
|
+
requestId,
|
|
360
|
+
...request,
|
|
361
|
+
} satisfies OpenGeniSiteBridgeRequestMessage);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async connect(): Promise<MessagePort> {
|
|
366
|
+
if (this.closed) throw new OpenGeniSiteBridgeError("closed", "Site bridge is closed");
|
|
367
|
+
if (this.port) return this.port;
|
|
368
|
+
if (this.connecting) return await this.connecting;
|
|
369
|
+
const channel = (this.options.createMessageChannel ?? (() => new MessageChannel()))();
|
|
370
|
+
const timeoutMs = positiveTimeout(this.options.connectTimeoutMs ?? 10_000, "connectTimeoutMs");
|
|
371
|
+
this.connecting = new Promise<MessagePort>((resolve, reject) => {
|
|
372
|
+
let active = true;
|
|
373
|
+
const timeout = setTimeout(() => {
|
|
374
|
+
cleanup();
|
|
375
|
+
channel.port1.close();
|
|
376
|
+
reject(new OpenGeniSiteBridgeError("host_timeout", "OpenGeni Site host did not respond"));
|
|
377
|
+
}, timeoutMs);
|
|
378
|
+
const onMessage = (event: MessageEvent<unknown>) => {
|
|
379
|
+
const message = event.data;
|
|
380
|
+
if (
|
|
381
|
+
!isRecord(message) ||
|
|
382
|
+
message.type !== OPENGENI_SITE_BRIDGE_READY ||
|
|
383
|
+
message.version !== OPENGENI_SITE_BRIDGE_VERSION
|
|
384
|
+
) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
cleanup();
|
|
388
|
+
this.port = channel.port1;
|
|
389
|
+
resolve(channel.port1);
|
|
390
|
+
};
|
|
391
|
+
const cleanup = () => {
|
|
392
|
+
active = false;
|
|
393
|
+
clearTimeout(timeout);
|
|
394
|
+
channel.port1.removeEventListener("message", onMessage);
|
|
395
|
+
};
|
|
396
|
+
channel.port1.addEventListener("message", onMessage);
|
|
397
|
+
channel.port1.start();
|
|
398
|
+
void this.bootstrap
|
|
399
|
+
.port()
|
|
400
|
+
.then((bootstrapPort) => {
|
|
401
|
+
if (!active) return;
|
|
402
|
+
if (this.closed) throw new OpenGeniSiteBridgeError("closed", "Site bridge is closed");
|
|
403
|
+
bootstrapPort.postMessage(
|
|
404
|
+
{
|
|
405
|
+
type: OPENGENI_SITE_BRIDGE_CONNECT,
|
|
406
|
+
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
407
|
+
} satisfies OpenGeniSiteBridgeConnectMessage,
|
|
408
|
+
[channel.port2],
|
|
409
|
+
);
|
|
410
|
+
})
|
|
411
|
+
.catch((error) => {
|
|
412
|
+
cleanup();
|
|
413
|
+
channel.port1.close();
|
|
414
|
+
reject(error);
|
|
415
|
+
});
|
|
416
|
+
}).finally(() => {
|
|
417
|
+
this.connecting = null;
|
|
418
|
+
});
|
|
419
|
+
return await this.connecting;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
type SiteBridgeBootstrap = {
|
|
424
|
+
port: () => Promise<MessagePort>;
|
|
425
|
+
close: () => void;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
function createSiteBridgeBootstrap(options: OpenGeniSiteClientOptions): SiteBridgeBootstrap {
|
|
429
|
+
if (options.bootstrapPort) {
|
|
430
|
+
return {
|
|
431
|
+
port: async () => options.bootstrapPort!,
|
|
432
|
+
close: () => options.bootstrapPort?.close(),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const siteWindow = options.siteWindow ?? globalThis.window;
|
|
436
|
+
const parentWindow = options.parentWindow ?? siteWindow?.parent;
|
|
437
|
+
if (!siteWindow || !parentWindow || parentWindow === siteWindow) {
|
|
438
|
+
return {
|
|
439
|
+
port: async () => {
|
|
440
|
+
throw new OpenGeniSiteBridgeError(
|
|
441
|
+
"host_unavailable",
|
|
442
|
+
"OpenGeni Site client must run inside a hosted iframe",
|
|
443
|
+
);
|
|
444
|
+
},
|
|
445
|
+
close: () => undefined,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const retainedPort = retainedSiteBridgeBootstrapPort(siteWindow);
|
|
449
|
+
if (retainedPort) {
|
|
450
|
+
return {
|
|
451
|
+
port: async () => retainedPort,
|
|
452
|
+
close: () => undefined,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
let settledPort: MessagePort | null = null;
|
|
456
|
+
let rejectBootstrap: ((reason?: unknown) => void) | null = null;
|
|
457
|
+
const onMessage = (event: MessageEvent<unknown>) => {
|
|
458
|
+
if (
|
|
459
|
+
event.source !== parentWindow ||
|
|
460
|
+
!isOpenGeniSiteBridgeReadyMessage(event.data) ||
|
|
461
|
+
event.ports.length !== 1
|
|
462
|
+
) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
cleanup();
|
|
466
|
+
settledPort = event.ports[0]!;
|
|
467
|
+
settledPort.start();
|
|
468
|
+
retainSiteBridgeBootstrapPort(siteWindow, settledPort);
|
|
469
|
+
resolveBootstrap?.(settledPort);
|
|
470
|
+
};
|
|
471
|
+
let resolveBootstrap: ((port: MessagePort) => void) | null = null;
|
|
472
|
+
const cleanup = () => siteWindow.removeEventListener("message", onMessage as EventListener);
|
|
473
|
+
const port = new Promise<MessagePort>((resolve, reject) => {
|
|
474
|
+
resolveBootstrap = resolve;
|
|
475
|
+
rejectBootstrap = reject;
|
|
476
|
+
siteWindow.addEventListener("message", onMessage as EventListener);
|
|
477
|
+
});
|
|
478
|
+
void port.catch(() => undefined);
|
|
479
|
+
return {
|
|
480
|
+
port: async () => await port,
|
|
481
|
+
close: () => {
|
|
482
|
+
cleanup();
|
|
483
|
+
rejectBootstrap?.(new OpenGeniSiteBridgeError("closed", "Site bridge is closed"));
|
|
484
|
+
resolveBootstrap = null;
|
|
485
|
+
rejectBootstrap = null;
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function retainedSiteBridgeBootstrapPort(
|
|
491
|
+
siteWindow: OpenGeniSiteClientOptions["siteWindow"],
|
|
492
|
+
): MessagePort | null {
|
|
493
|
+
const state = (siteWindow as unknown as Record<string, unknown>)[
|
|
494
|
+
OPENGENI_SITE_BRIDGE_BOOTSTRAP_GLOBAL
|
|
495
|
+
];
|
|
496
|
+
if (!isRecord(state)) return null;
|
|
497
|
+
const candidate = state.port as Partial<MessagePort> | undefined;
|
|
498
|
+
return candidate &&
|
|
499
|
+
typeof candidate.postMessage === "function" &&
|
|
500
|
+
typeof candidate.addEventListener === "function" &&
|
|
501
|
+
typeof candidate.start === "function"
|
|
502
|
+
? (candidate as MessagePort)
|
|
503
|
+
: null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function retainSiteBridgeBootstrapPort(
|
|
507
|
+
siteWindow: OpenGeniSiteClientOptions["siteWindow"],
|
|
508
|
+
port: MessagePort,
|
|
509
|
+
): void {
|
|
510
|
+
try {
|
|
511
|
+
Object.defineProperty(siteWindow, OPENGENI_SITE_BRIDGE_BOOTSTRAP_GLOBAL, {
|
|
512
|
+
configurable: true,
|
|
513
|
+
value: { port },
|
|
514
|
+
});
|
|
515
|
+
} catch {
|
|
516
|
+
// The bootstrap event still serves the current client when the embedding
|
|
517
|
+
// window refuses extension; only late construction loses this fallback.
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function isOpenGeniSiteBridgeReadyMessage(value: unknown): value is OpenGeniSiteBridgeReadyMessage {
|
|
522
|
+
return (
|
|
523
|
+
isRecord(value) &&
|
|
524
|
+
value.type === OPENGENI_SITE_BRIDGE_READY &&
|
|
525
|
+
value.version === OPENGENI_SITE_BRIDGE_VERSION
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function cancel(port: MessagePort, requestId: string): void {
|
|
530
|
+
port.postMessage({
|
|
531
|
+
type: OPENGENI_SITE_BRIDGE_CANCEL,
|
|
532
|
+
version: OPENGENI_SITE_BRIDGE_VERSION,
|
|
533
|
+
requestId,
|
|
534
|
+
} satisfies OpenGeniSiteBridgeCancelMessage);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function positiveTimeout(value: number, label: string): number {
|
|
538
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${label} must be positive`);
|
|
539
|
+
return value;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function isRecord(value: unknown): value is Record<string, any> {
|
|
543
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
544
|
+
}
|
package/src/stream.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { isAbortError, isRetryableStreamError, OpenGeniStreamError } from "./err
|
|
|
2
2
|
import { parseSseStream } from "./sse";
|
|
3
3
|
import type { SessionEvent } from "./types";
|
|
4
4
|
|
|
5
|
+
const SESSION_EVENT_STREAM_COVERAGE = Symbol.for(
|
|
6
|
+
"@opengeni/sdk/session-event-stream-covered-through",
|
|
7
|
+
);
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Transport boundary for the streaming core. The client implements it with
|
|
7
11
|
* `fetch`; unit tests script it directly.
|
|
@@ -111,7 +115,8 @@ export async function* streamSessionEvents(
|
|
|
111
115
|
if (!event) {
|
|
112
116
|
continue;
|
|
113
117
|
}
|
|
114
|
-
const coveredSequence =
|
|
118
|
+
const coveredSequence = trustedSseSequence(message.id, event.sequence);
|
|
119
|
+
markSessionEventStreamCoverage(event, coveredSequence);
|
|
115
120
|
if (coveredSequence <= cursor) continue;
|
|
116
121
|
if (event.sequence > cursor + 1) {
|
|
117
122
|
for await (const missed of backfillEvents(transport, cursor, event.sequence - 1)) {
|
|
@@ -283,13 +288,29 @@ function parseSessionEvent(data: string): SessionEvent | null {
|
|
|
283
288
|
return parsed as SessionEvent;
|
|
284
289
|
}
|
|
285
290
|
|
|
286
|
-
/**
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
/** Trusted SSE coverage attached non-enumerably to a streamed event. */
|
|
292
|
+
export function sessionEventStreamCoveredThrough(event: SessionEvent): number | null {
|
|
293
|
+
const covered = (event as SessionEvent & { [SESSION_EVENT_STREAM_COVERAGE]?: unknown })[
|
|
294
|
+
SESSION_EVENT_STREAM_COVERAGE
|
|
295
|
+
];
|
|
296
|
+
return typeof covered === "number" && Number.isSafeInteger(covered) && covered >= event.sequence
|
|
297
|
+
? covered
|
|
298
|
+
: null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function trustedSseSequence(id: string | undefined, sequence: number): number {
|
|
302
|
+
if (id === undefined || !/^\d+$/.test(id)) return sequence;
|
|
303
|
+
const covered = Number(id);
|
|
304
|
+
return Number.isSafeInteger(covered) && covered >= sequence ? covered : sequence;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function markSessionEventStreamCoverage(event: SessionEvent, covered: number): void {
|
|
308
|
+
Object.defineProperty(event, SESSION_EVENT_STREAM_COVERAGE, {
|
|
309
|
+
value: covered,
|
|
310
|
+
enumerable: false,
|
|
311
|
+
configurable: false,
|
|
312
|
+
writable: false,
|
|
313
|
+
});
|
|
293
314
|
}
|
|
294
315
|
|
|
295
316
|
async function sleep(delayMs: number, signal: AbortSignal | undefined): Promise<void> {
|