@ai-switch/tauri-plugin-runtime 0.1.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +327 -0
  3. package/THIRD_PARTY_NOTICES.md +210 -0
  4. package/dist/bridge/handshake.d.ts +11 -0
  5. package/dist/bridge/json-codec.d.ts +4 -0
  6. package/dist/bridge/rpc-peer.d.ts +7 -0
  7. package/dist/bridge/rpc-types.d.ts +20 -0
  8. package/dist/chunk-CFQSUF5W.js +1794 -0
  9. package/dist/chunk-JSBRDJBE.js +30 -0
  10. package/dist/chunk-K5YWIJN6.js +389 -0
  11. package/dist/chunk-PW7XHHWG.js +403 -0
  12. package/dist/chunk-RKQEK7DY.js +13879 -0
  13. package/dist/chunk-WT62GTMC.js +18 -0
  14. package/dist/chunk-YI6NQ6WR.js +513 -0
  15. package/dist/host/event-router.d.ts +19 -0
  16. package/dist/host/file-handles.d.ts +15 -0
  17. package/dist/host/host.d.ts +2 -0
  18. package/dist/host/index.d.ts +4 -0
  19. package/dist/host/index.js +791 -0
  20. package/dist/host/policy.d.ts +7 -0
  21. package/dist/host/requests.d.ts +13 -0
  22. package/dist/host/types.d.ts +19 -0
  23. package/dist/host/view.d.ts +18 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +9 -0
  26. package/dist/node/buffer.d.ts +7 -0
  27. package/dist/node/buffer.js +8 -0
  28. package/dist/node/events.d.ts +12 -0
  29. package/dist/node/events.js +458 -0
  30. package/dist/node/fs/backend.d.ts +8 -0
  31. package/dist/node/fs/callback-types.d.ts +43 -0
  32. package/dist/node/fs/callbacks.d.ts +3 -0
  33. package/dist/node/fs/client.d.ts +3 -0
  34. package/dist/node/fs/errors.d.ts +5 -0
  35. package/dist/node/fs/lifecycle.d.ts +11 -0
  36. package/dist/node/fs/options.d.ts +7 -0
  37. package/dist/node/fs/promises.d.ts +33 -0
  38. package/dist/node/fs/promises.js +30 -0
  39. package/dist/node/fs/transfer.d.ts +7 -0
  40. package/dist/node/fs/types.d.ts +86 -0
  41. package/dist/node/fs.d.ts +36 -0
  42. package/dist/node/fs.js +92 -0
  43. package/dist/node/path.d.ts +20 -0
  44. package/dist/node/path.js +434 -0
  45. package/dist/plugin/client.d.ts +4 -0
  46. package/dist/plugin/connection.d.ts +6 -0
  47. package/dist/plugin/errors.d.ts +3 -0
  48. package/dist/plugin/index.d.ts +6 -0
  49. package/dist/plugin/index.js +12 -0
  50. package/dist/plugin/types.d.ts +28 -0
  51. package/dist/protocol/capabilities.d.ts +8 -0
  52. package/dist/protocol/errors.d.ts +14 -0
  53. package/dist/protocol/generated/capabilities.generated.d.ts +56 -0
  54. package/dist/protocol/generated/fs.generated.d.ts +178 -0
  55. package/dist/protocol/generated/host-event.generated.d.ts +17 -0
  56. package/dist/protocol/generated/manifest.generated.d.ts +64 -0
  57. package/dist/protocol/generated/session.generated.d.ts +93 -0
  58. package/dist/protocol/generated/types.generated.d.ts +6 -0
  59. package/dist/protocol/generated/validators.generated.d.mts +24 -0
  60. package/dist/protocol/generated/wire.generated.d.ts +63 -0
  61. package/dist/protocol/index.d.ts +18 -0
  62. package/dist/protocol/index.js +746 -0
  63. package/dist/protocol/json-safety.d.ts +3 -0
  64. package/dist/protocol/limits.d.ts +11 -0
  65. package/dist/protocol/manifest.d.ts +5 -0
  66. package/dist/protocol/path-policy.d.ts +2 -0
  67. package/dist/protocol/schema/capabilities.schema.json +301 -0
  68. package/dist/protocol/schema/fs.schema.json +900 -0
  69. package/dist/protocol/schema/host-event.schema.json +116 -0
  70. package/dist/protocol/schema/manifest.schema.json +60 -0
  71. package/dist/protocol/schema/session.schema.json +145 -0
  72. package/dist/protocol/schema/wire.schema.json +358 -0
  73. package/dist/protocol/session.d.ts +3 -0
  74. package/dist/protocol/types.d.ts +27 -0
  75. package/dist/protocol/validation.d.ts +2 -0
  76. package/dist/protocol/wire.d.ts +16 -0
  77. package/package.json +95 -0
@@ -0,0 +1,791 @@
1
+ import {
2
+ appendBootstrapHint,
3
+ createRpcPeer
4
+ } from "../chunk-PW7XHHWG.js";
5
+ import {
6
+ AplgError,
7
+ standardCapabilities,
8
+ validateCapabilityRequest,
9
+ validateCapabilityResult,
10
+ validateHostEvent,
11
+ validateJson,
12
+ validateSessionDescriptor
13
+ } from "../chunk-RKQEK7DY.js";
14
+ import {
15
+ protocolVersion
16
+ } from "../chunk-WT62GTMC.js";
17
+ import "../chunk-JSBRDJBE.js";
18
+
19
+ // src/host/policy.ts
20
+ function checkedTimeout(value) {
21
+ if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647) {
22
+ throw new AplgError("E_INVALID_ARGUMENT", "Host timeouts must be positive timer-safe integers.");
23
+ }
24
+ return value;
25
+ }
26
+ function checkedOrigin(origin) {
27
+ try {
28
+ const url = new URL(origin);
29
+ const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || /^127(?:\.[0-9]{1,3}){3}$/.test(url.hostname);
30
+ if (url.origin !== origin || url.username || url.password || !(url.protocol === "https:" || url.protocol === "http:" && loopback)) throw new Error();
31
+ return origin;
32
+ } catch {
33
+ throw new AplgError("E_INVALID_ARGUMENT", "Asset origins must be explicit HTTPS or loopback HTTP origins.");
34
+ }
35
+ }
36
+ function requireAssetOrigin(assetUrl, origins) {
37
+ if (!origins.includes(new URL(assetUrl).origin)) throw new AplgError("E_PERMISSION_DENIED", "The plugin asset origin is not allowed by this host.");
38
+ }
39
+ function randomId(prefix = "") {
40
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
41
+ return prefix + Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
42
+ }
43
+ function randomNonce() {
44
+ return btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
45
+ }
46
+ function bounded(operation, timeoutMs) {
47
+ return new Promise((resolve, reject) => {
48
+ const timer = setTimeout(() => reject(new AplgError("E_TIMEOUT", "The host operation timed out.")), timeoutMs);
49
+ void Promise.resolve().then(operation).then(resolve, reject).finally(() => clearTimeout(timer));
50
+ });
51
+ }
52
+
53
+ // src/host/requests.ts
54
+ function createSessionRequests(options) {
55
+ const timeoutMs = checkedTimeout(options.timeoutMs);
56
+ const active = /* @__PURE__ */ new Map();
57
+ return {
58
+ async run(operation, args, signal, onLate) {
59
+ if (signal?.aborted) throw new AplgError("E_CANCELLED", "The request was cancelled.");
60
+ if (!validateJson(args).ok || ["sessionId", "requestId", "principal", "pluginId", "token"].some((key) => Object.hasOwn(args, key))) {
61
+ throw new AplgError("E_INVALID_ARGUMENT", "Backend identity must come from the bound host session.");
62
+ }
63
+ if (active.size >= 64) throw new AplgError("E_LIMIT_EXCEEDED", "Too many active host requests.");
64
+ const requestId = randomId("request-");
65
+ const payload = { ...args, sessionId: options.sessionId, requestId };
66
+ return new Promise((resolve, reject) => {
67
+ let settled = false;
68
+ let sent = false;
69
+ const timer = setTimeout(() => stop(new AplgError("E_TIMEOUT", "The host request timed out.")), timeoutMs);
70
+ const abort = () => stop(new AplgError("E_CANCELLED", "The request was cancelled."));
71
+ function cleanup() {
72
+ active.delete(requestId);
73
+ clearTimeout(timer);
74
+ signal?.removeEventListener("abort", abort);
75
+ }
76
+ function stop(reason) {
77
+ if (settled) return;
78
+ settled = true;
79
+ cleanup();
80
+ reject(reason);
81
+ if (sent) void Promise.resolve().then(() => options.transport.call("request.cancel", { sessionId: options.sessionId, requestId })).catch(() => {
82
+ });
83
+ }
84
+ active.set(requestId, stop);
85
+ signal?.addEventListener("abort", abort, { once: true });
86
+ void Promise.resolve().then(() => {
87
+ if (settled) return;
88
+ sent = true;
89
+ return options.transport.call(operation, payload);
90
+ }).then((result) => {
91
+ if (!sent) return;
92
+ if (settled) {
93
+ if (onLate) void Promise.resolve().then(() => onLate(result)).catch(() => {
94
+ });
95
+ return;
96
+ }
97
+ settled = true;
98
+ cleanup();
99
+ if (!validateJson(result).ok) reject(new AplgError("E_INVALID_MESSAGE", "The backend returned an invalid JSON value."));
100
+ else resolve(result);
101
+ }, (error) => {
102
+ if (!settled) {
103
+ settled = true;
104
+ cleanup();
105
+ reject(error);
106
+ }
107
+ });
108
+ });
109
+ },
110
+ abortAll(reason) {
111
+ for (const cancel of [...active.values()]) cancel(reason);
112
+ }
113
+ };
114
+ }
115
+
116
+ // src/host/file-handles.ts
117
+ function createFileHandleGuard(options) {
118
+ const handles = /* @__PURE__ */ new Set();
119
+ const opening = /* @__PURE__ */ new Set();
120
+ const releasing = /* @__PURE__ */ new Map();
121
+ let disposed = false;
122
+ let cleanup;
123
+ function handleOf(value) {
124
+ if (!validateJson(value).ok || !value || typeof value !== "object" || Array.isArray(value)) return void 0;
125
+ const handle = value.handle;
126
+ return typeof handle === "string" && /^[\x21-\x7e]{1,128}$/.test(handle) ? handle : void 0;
127
+ }
128
+ function release(handle) {
129
+ const previous = releasing.get(handle);
130
+ if (previous) return previous;
131
+ const work = bounded(() => options.transport.call("capability.call", {
132
+ sessionId: options.sessionId,
133
+ requestId: randomId("file-cleanup-"),
134
+ capability: "aplg.fs",
135
+ method: "transfer.abort",
136
+ params: { handle }
137
+ }), Math.min(options.timeoutMs, 1e3)).then((result) => {
138
+ if (result === null) handles.delete(handle);
139
+ }).catch(() => {
140
+ }).finally(() => releasing.delete(handle));
141
+ releasing.set(handle, work);
142
+ return work;
143
+ }
144
+ async function releaseUnclaimed(value) {
145
+ const handle = handleOf(value);
146
+ if (handle && !handles.has(handle)) {
147
+ handles.add(handle);
148
+ await release(handle);
149
+ }
150
+ }
151
+ return {
152
+ async call(method, params, signal) {
153
+ if (disposed) throw new AplgError("E_SESSION_CLOSED", "The file transfer session is closed.");
154
+ if (signal.aborted) throw new AplgError("E_CANCELLED", "The file transfer request was cancelled.");
155
+ if (!validateCapabilityRequest("aplg.fs", method, params).ok) throw new AplgError("E_INVALID_ARGUMENT", "Invalid filesystem request.");
156
+ const isOpen = method === "transfer.openRead" || method === "transfer.openWrite";
157
+ const reservation = /* @__PURE__ */ Symbol("file-open");
158
+ let received = false;
159
+ if (isOpen) {
160
+ if (handles.size + opening.size >= options.maxTransfers) throw new AplgError("E_LIMIT_EXCEEDED", "Too many file transfers in this session.");
161
+ opening.add(reservation);
162
+ } else if (method.startsWith("transfer.")) {
163
+ if (!handles.has(params.handle)) throw new AplgError("E_PERMISSION_DENIED", "This session does not own the file transfer handle.");
164
+ }
165
+ try {
166
+ const result = await options.requests.run("capability.call", { capability: "aplg.fs", method, params }, signal, isOpen ? async (late) => {
167
+ opening.delete(reservation);
168
+ await releaseUnclaimed(late);
169
+ } : void 0);
170
+ received = true;
171
+ opening.delete(reservation);
172
+ if (isOpen) {
173
+ const handle = handleOf(result);
174
+ if (handle && handles.has(handle)) throw new AplgError("E_INVALID_MESSAGE", "The backend reused an active file transfer handle.");
175
+ if (!validateCapabilityResult("aplg.fs", method, result).ok || disposed || signal.aborted) {
176
+ await releaseUnclaimed(result);
177
+ throw new AplgError(disposed || signal.aborted ? "E_SESSION_CLOSED" : "E_INVALID_MESSAGE", "The file transfer could not be opened safely.");
178
+ }
179
+ handles.add(handle);
180
+ } else {
181
+ if (!validateCapabilityResult("aplg.fs", method, result).ok) throw new AplgError("E_INVALID_MESSAGE", "The backend returned an invalid filesystem result.");
182
+ if (method === "transfer.finish" || method === "transfer.abort") handles.delete(params.handle);
183
+ }
184
+ return result;
185
+ } catch (error) {
186
+ const code = error && typeof error === "object" ? error.code : void 0;
187
+ if (received || !["E_TIMEOUT", "E_CANCELLED", "E_HOST_UNAVAILABLE", "E_SESSION_CLOSED"].includes(code ?? "")) opening.delete(reservation);
188
+ throw error;
189
+ }
190
+ },
191
+ async restore() {
192
+ if (disposed) return;
193
+ if (opening.size) throw new AplgError("E_HOST_UNAVAILABLE", "An interrupted file open is still unconfirmed.");
194
+ const previous = [...handles];
195
+ await Promise.all(previous.map(release));
196
+ if (!disposed && previous.some((handle) => handles.has(handle))) {
197
+ throw new AplgError("E_HOST_UNAVAILABLE", "Interrupted file transfers could not be released after reconnect.");
198
+ }
199
+ },
200
+ dispose() {
201
+ if (cleanup) return cleanup;
202
+ disposed = true;
203
+ opening.clear();
204
+ cleanup = Promise.allSettled([...handles].map(release)).then(() => {
205
+ });
206
+ return cleanup;
207
+ }
208
+ };
209
+ }
210
+
211
+ // src/host/event-router.ts
212
+ function createEventRouter(options) {
213
+ const subscriptions = /* @__PURE__ */ new Map();
214
+ const backendIds = /* @__PURE__ */ new Map();
215
+ const buffered = [];
216
+ let bufferTimer;
217
+ let online = true;
218
+ let ready = true;
219
+ let disposed = false;
220
+ let generation = 0;
221
+ let opening = 0;
222
+ let cleanup;
223
+ let reconnecting;
224
+ function status(state) {
225
+ if (!disposed) options.send({ protocol: protocolVersion, kind: "connection", state });
226
+ }
227
+ function release(id) {
228
+ return bounded(() => options.transport.call("subscription.close", { sessionId: options.sessionId, subscriptionId: id }), options.timeoutMs).then(() => {
229
+ });
230
+ }
231
+ function resultId(value) {
232
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== 1 || typeof value.subscriptionId !== "string" || !/^[\x21-\x7e]{1,128}$/.test(value.subscriptionId)) {
233
+ throw new AplgError("E_INVALID_MESSAGE", "Invalid backend subscription identifier.");
234
+ }
235
+ return value.subscriptionId;
236
+ }
237
+ function lateRelease(value) {
238
+ try {
239
+ void release(resultId(value)).catch(() => {
240
+ });
241
+ } catch {
242
+ }
243
+ }
244
+ function expire() {
245
+ clearTimeout(bufferTimer);
246
+ bufferTimer = void 0;
247
+ const now = Date.now();
248
+ for (let index = buffered.length - 1; index >= 0; index--) if (buffered[index].expires <= now) buffered.splice(index, 1);
249
+ if (buffered.length) bufferTimer = setTimeout(expire, Math.max(1, buffered[0].expires - now));
250
+ }
251
+ function clearBuffer() {
252
+ buffered.length = 0;
253
+ clearTimeout(bufferTimer);
254
+ bufferTimer = void 0;
255
+ }
256
+ function deliver(subscription, event) {
257
+ if (event.seq <= subscription.backendSeq) return;
258
+ const delta = event.seq - subscription.backendSeq;
259
+ if (!Number.isSafeInteger(subscription.seq + delta)) {
260
+ void router.setConnected(false);
261
+ return;
262
+ }
263
+ if (delta > 1) {
264
+ status("disconnected");
265
+ status("connected");
266
+ }
267
+ subscription.backendSeq = event.seq;
268
+ subscription.seq += delta;
269
+ options.send({ protocol: protocolVersion, kind: "event", subscriptionId: subscription.id, seq: subscription.seq, payload: event.payload });
270
+ }
271
+ function flush() {
272
+ expire();
273
+ if (!ready || disposed) return;
274
+ for (let index = 0; index < buffered.length; ) {
275
+ const item = buffered[index];
276
+ const subscription = backendIds.get(item.event.subscriptionId);
277
+ if (subscription) {
278
+ buffered.splice(index, 1);
279
+ deliver(subscription, item.event);
280
+ } else index++;
281
+ }
282
+ if (opening === 0) clearBuffer();
283
+ }
284
+ async function attach(subscription, signal, restoring = false) {
285
+ const epoch = generation;
286
+ opening++;
287
+ try {
288
+ const value = await options.requests.run("subscription.open", { capability: subscription.capability, topic: subscription.topic }, signal, lateRelease);
289
+ const id = resultId(value);
290
+ if (restoring && !subscriptions.has(subscription.id)) {
291
+ void release(id).catch(() => {
292
+ });
293
+ return;
294
+ }
295
+ if (disposed || !online || epoch !== generation || !subscriptions.has(subscription.id) || signal?.aborted) {
296
+ void release(id).catch(() => {
297
+ });
298
+ throw new AplgError(disposed ? "E_SESSION_CLOSED" : "E_HOST_UNAVAILABLE", "The subscription session changed while opening.");
299
+ }
300
+ if (backendIds.has(id)) throw new AplgError("E_INVALID_MESSAGE", "The backend reused an active subscription identifier.");
301
+ subscription.backend = id;
302
+ subscription.backendSeq = 0;
303
+ backendIds.set(id, subscription);
304
+ } finally {
305
+ opening--;
306
+ }
307
+ flush();
308
+ }
309
+ const router = {
310
+ async open(capability, topic, signal) {
311
+ if (disposed) throw new AplgError("E_SESSION_CLOSED", "The plugin session is closed.");
312
+ if (!ready) throw new AplgError("E_HOST_UNAVAILABLE", "The plugin host is disconnected.");
313
+ if (subscriptions.size >= 64) throw new AplgError("E_LIMIT_EXCEEDED", "Too many subscriptions for this plugin session.");
314
+ const subscription = { id: randomId("subscription-"), capability, topic, backendSeq: 0, seq: 0 };
315
+ subscriptions.set(subscription.id, subscription);
316
+ try {
317
+ await attach(subscription, signal);
318
+ return subscription.id;
319
+ } catch (error) {
320
+ subscriptions.delete(subscription.id);
321
+ if (opening === 0) clearBuffer();
322
+ throw error;
323
+ }
324
+ },
325
+ async close(id) {
326
+ const subscription = subscriptions.get(id);
327
+ if (!subscription) throw new AplgError("E_PERMISSION_DENIED", "This session does not own the requested subscription.");
328
+ subscriptions.delete(id);
329
+ if (subscription.backend) {
330
+ backendIds.delete(subscription.backend);
331
+ await release(subscription.backend);
332
+ }
333
+ },
334
+ accept(event) {
335
+ if (disposed || !online || event.kind !== "capability.event" || event.sessionId !== options.sessionId) return;
336
+ const subscription = backendIds.get(event.subscriptionId);
337
+ if (ready && subscription) {
338
+ deliver(subscription, event);
339
+ return;
340
+ }
341
+ if (opening > 0 && buffered.length < 32) {
342
+ buffered.push({ event, expires: Date.now() + 2e3 });
343
+ if (!bufferTimer) bufferTimer = setTimeout(expire, 2e3);
344
+ }
345
+ },
346
+ isConnected() {
347
+ return ready && !disposed;
348
+ },
349
+ async setConnected(connected) {
350
+ if (disposed) return;
351
+ if (!connected) {
352
+ if (!online && !ready) return;
353
+ online = false;
354
+ ready = false;
355
+ generation++;
356
+ reconnecting = void 0;
357
+ options.requests.abortAll(new AplgError("E_HOST_UNAVAILABLE", "The host transport disconnected."));
358
+ clearBuffer();
359
+ for (const subscription of subscriptions.values()) {
360
+ if (subscription.backend) void release(subscription.backend).catch(() => {
361
+ });
362
+ subscription.backend = void 0;
363
+ subscription.backendSeq = 0;
364
+ }
365
+ backendIds.clear();
366
+ status("disconnected");
367
+ return;
368
+ }
369
+ if (ready) return;
370
+ if (reconnecting) return reconnecting;
371
+ online = true;
372
+ const epoch = generation;
373
+ reconnecting = (async () => {
374
+ try {
375
+ await Promise.all([...subscriptions.values()].map((subscription) => attach(subscription, void 0, true)));
376
+ if (disposed || epoch !== generation || !online) return;
377
+ ready = true;
378
+ status("connected");
379
+ flush();
380
+ } catch (error) {
381
+ if (disposed || epoch !== generation) return;
382
+ await router.setConnected(false);
383
+ throw error;
384
+ } finally {
385
+ if (epoch === generation) reconnecting = void 0;
386
+ }
387
+ })();
388
+ return reconnecting;
389
+ },
390
+ dispose() {
391
+ if (cleanup) return cleanup;
392
+ disposed = true;
393
+ online = false;
394
+ ready = false;
395
+ generation++;
396
+ options.requests.abortAll(new AplgError("E_SESSION_CLOSED", "The plugin session is closed."));
397
+ clearBuffer();
398
+ const closing = [...backendIds.keys()].map((id) => release(id));
399
+ subscriptions.clear();
400
+ backendIds.clear();
401
+ cleanup = Promise.allSettled(closing).then(() => {
402
+ });
403
+ return cleanup;
404
+ }
405
+ };
406
+ return router;
407
+ }
408
+
409
+ // src/host/view.ts
410
+ function createManagedView(options) {
411
+ const { descriptor, container, transport } = options;
412
+ const document = container.ownerDocument;
413
+ const window2 = document.defaultView;
414
+ const iframe = document.createElement("iframe");
415
+ iframe.title = descriptor.manifest.name;
416
+ iframe.setAttribute("sandbox", "allow-scripts");
417
+ iframe.referrerPolicy = "no-referrer";
418
+ iframe.setAttribute("allow", "camera 'none'; microphone 'none'; geolocation 'none'");
419
+ const nonce = randomNonce();
420
+ const requests = createSessionRequests({ transport, sessionId: descriptor.sessionId, timeoutMs: options.requestTimeoutMs });
421
+ const fileHandles = createFileHandleGuard({ transport, requests, sessionId: descriptor.sessionId, timeoutMs: options.requestTimeoutMs, maxTransfers: descriptor.info.limits.fileTransfers });
422
+ let peer;
423
+ let closed = false;
424
+ let established = false;
425
+ let loaded = false;
426
+ let transportGeneration = 0;
427
+ let disposePromise;
428
+ let resolveReady;
429
+ let rejectReady;
430
+ const ready = new Promise((resolve, reject) => {
431
+ resolveReady = resolve;
432
+ rejectReady = reject;
433
+ });
434
+ const events = createEventRouter({
435
+ transport,
436
+ requests,
437
+ sessionId: descriptor.sessionId,
438
+ timeoutMs: options.requestTimeoutMs,
439
+ send(event) {
440
+ if (established && !closed) {
441
+ try {
442
+ peer?.sendEvent(event);
443
+ } catch {
444
+ void dispose(new AplgError("E_INVALID_MESSAGE", "The plugin event could not be delivered."));
445
+ }
446
+ }
447
+ }
448
+ });
449
+ const timer = setTimeout(() => {
450
+ void dispose(new AplgError("E_TIMEOUT", "The plugin handshake timed out."));
451
+ }, options.handshakeTimeoutMs);
452
+ const observer = new MutationObserver(() => {
453
+ if (!container.isConnected || iframe.parentElement !== container || iframe.getAttribute("sandbox") !== "allow-scripts") {
454
+ void dispose(new AplgError("E_SESSION_CLOSED", "The plugin container was removed or changed."));
455
+ }
456
+ });
457
+ const view = { pluginId: descriptor.manifest.id, element: iframe, dispose: () => dispose() };
458
+ function requireCapability(name, method) {
459
+ if (closed || !established) throw new AplgError("E_SESSION_CLOSED", "The plugin session is not active.");
460
+ if (!events.isConnected()) throw new AplgError("E_HOST_UNAVAILABLE", "The host transport is disconnected.");
461
+ const capability = Object.hasOwn(descriptor.info.capabilities, name) ? descriptor.info.capabilities[name] : void 0;
462
+ if (!capability || method !== void 0 && !capability.methods.includes(method)) {
463
+ throw new AplgError("E_CAPABILITY_UNAVAILABLE", "The session does not grant this capability or method.");
464
+ }
465
+ }
466
+ async function handle(request) {
467
+ if (request.operation === "capability.call") {
468
+ const { capability, method, params } = request.args;
469
+ requireCapability(capability, method);
470
+ const standard = Object.hasOwn(standardCapabilities, capability);
471
+ if (standard && !validateCapabilityRequest(capability, method, params).ok) throw new AplgError("E_INVALID_ARGUMENT", "The capability request is invalid.");
472
+ const result = capability === "aplg.fs" ? await fileHandles.call(method, params, request.signal) : await requests.run("capability.call", { capability, method, params }, request.signal);
473
+ if (standard && !validateCapabilityResult(capability, method, result).ok) throw new AplgError("E_INVALID_MESSAGE", "The backend capability result is invalid.");
474
+ return result;
475
+ }
476
+ if (request.operation === "subscription.open") {
477
+ requireCapability(request.args.capability);
478
+ return { subscriptionId: await events.open(request.args.capability, request.args.topic, request.signal) };
479
+ }
480
+ if (request.operation === "subscription.close") {
481
+ if (closed || !established) throw new AplgError("E_SESSION_CLOSED", "The plugin session is closed.");
482
+ await events.close(request.args.subscriptionId);
483
+ return null;
484
+ }
485
+ throw new AplgError("E_PERMISSION_DENIED", "The plugin cannot call this host operation.");
486
+ }
487
+ function receive(event) {
488
+ if (closed || peer) return;
489
+ if (event.source !== iframe.contentWindow || event.origin !== "null") return;
490
+ if (!validateJson(event.data, 4096).ok || !event.data || typeof event.data !== "object") return;
491
+ const data = event.data;
492
+ if (data.nonce !== nonce) return;
493
+ if (Object.keys(data).length !== 4 || data.channel !== "aplg.bootstrap" || data.protocol !== protocolVersion || data.kind !== "ready" || event.ports.length !== 0) {
494
+ event.ports.forEach((port) => port.close());
495
+ void dispose(new AplgError("E_PROTOCOL_MISMATCH", "The plugin bootstrap request is invalid."));
496
+ return;
497
+ }
498
+ const channel = new MessageChannel();
499
+ peer = createRpcPeer(channel.port1, { timeoutMs: options.requestTimeoutMs, maxBytes: descriptor.info.limits.controlBytes });
500
+ peer.onRequest(handle);
501
+ peer.onEvent((message) => {
502
+ if (message.kind !== "connection" || message.state === "disconnected") {
503
+ void dispose(new AplgError("E_INVALID_MESSAGE", "Plugins cannot emit host events."));
504
+ return;
505
+ }
506
+ if (message.state === "closed") {
507
+ void dispose();
508
+ return;
509
+ }
510
+ if (established) {
511
+ void dispose(new AplgError("E_INVALID_MESSAGE", "Duplicate plugin handshake acknowledgement."));
512
+ return;
513
+ }
514
+ if (!events.isConnected()) {
515
+ void dispose(new AplgError("E_HOST_UNAVAILABLE", "The host disconnected during plugin startup."));
516
+ return;
517
+ }
518
+ established = true;
519
+ clearTimeout(timer);
520
+ window2.removeEventListener("message", receive);
521
+ try {
522
+ peer.sendEvent({ protocol: protocolVersion, kind: "connection", state: "connected" });
523
+ } catch {
524
+ void dispose(new AplgError("E_SESSION_CLOSED", "The plugin handshake port closed."));
525
+ return;
526
+ }
527
+ resolveReady(view);
528
+ });
529
+ try {
530
+ iframe.contentWindow.postMessage({ channel: "aplg.bootstrap", protocol: protocolVersion, kind: "connect", nonce, info: descriptor.info }, "*", [channel.port2]);
531
+ } catch {
532
+ channel.port2.close();
533
+ void dispose(new AplgError("E_SESSION_CLOSED", "The plugin bootstrap could not be sent."));
534
+ }
535
+ }
536
+ function load() {
537
+ if (loaded) void dispose(new AplgError("E_SESSION_CLOSED", "The plugin document navigated or reloaded."));
538
+ loaded = true;
539
+ }
540
+ function leave() {
541
+ void dispose();
542
+ }
543
+ function dispose(reason = new AplgError("E_SESSION_CLOSED", "The plugin view closed.")) {
544
+ if (disposePromise) return disposePromise;
545
+ let done;
546
+ disposePromise = new Promise((resolve) => {
547
+ done = resolve;
548
+ });
549
+ closed = true;
550
+ clearTimeout(timer);
551
+ observer.disconnect();
552
+ window2.removeEventListener("message", receive);
553
+ window2.removeEventListener("pagehide", leave);
554
+ iframe.removeEventListener("load", load);
555
+ if (!established) rejectReady(reason);
556
+ requests.abortAll(reason);
557
+ peer?.close(reason);
558
+ iframe.remove();
559
+ options.onDisposed();
560
+ void Promise.allSettled([events.dispose(), fileHandles.dispose(), options.closeSession()]).then(() => done());
561
+ return disposePromise;
562
+ }
563
+ try {
564
+ window2.addEventListener("message", receive);
565
+ window2.addEventListener("pagehide", leave, { once: true });
566
+ iframe.addEventListener("load", load);
567
+ iframe.src = appendBootstrapHint(descriptor.assetUrl, nonce, window2.location.origin);
568
+ container.append(iframe);
569
+ for (let node = container; node; node = node.parentNode) observer.observe(node, { childList: true });
570
+ observer.observe(iframe, { attributes: true, attributeFilter: ["sandbox"] });
571
+ } catch {
572
+ void dispose(new AplgError("E_HOST_UNAVAILABLE", "The plugin container could not be mounted."));
573
+ }
574
+ return {
575
+ view,
576
+ ready,
577
+ dispose,
578
+ accept(event) {
579
+ if (closed) return;
580
+ if (event.kind === "session.closed") {
581
+ if (event.sessionId === descriptor.sessionId) void dispose();
582
+ return;
583
+ }
584
+ if (event.kind === "transport.state") {
585
+ if (event.state === "disconnected") {
586
+ transportGeneration++;
587
+ void events.setConnected(false);
588
+ } else {
589
+ if (events.isConnected()) return;
590
+ const generation = transportGeneration;
591
+ void fileHandles.restore().then(() => {
592
+ if (!closed && generation === transportGeneration) return events.setConnected(true);
593
+ }).catch(() => {
594
+ if (!closed && generation === transportGeneration) void dispose(new AplgError("E_HOST_UNAVAILABLE", "The plugin resources could not be restored."));
595
+ });
596
+ }
597
+ } else events.accept(event);
598
+ }
599
+ };
600
+ }
601
+
602
+ // src/host/host.ts
603
+ function createPluginHost(options) {
604
+ const handshakeTimeoutMs = checkedTimeout(options.handshakeTimeoutMs ?? 1e4);
605
+ const requestTimeoutMs = checkedTimeout(options.requestTimeoutMs ?? 3e4);
606
+ const allowedAssetOrigins = options.allowedAssetOrigins?.map(checkedOrigin);
607
+ const views = /* @__PURE__ */ new Map();
608
+ const sessionOwners = /* @__PURE__ */ new Map();
609
+ const mounts = /* @__PURE__ */ new Map();
610
+ const inUse = /* @__PURE__ */ new Set();
611
+ const recentlyClosed = /* @__PURE__ */ new Set();
612
+ let disposed = false;
613
+ let online = true;
614
+ let subscribed;
615
+ let unsubscribe;
616
+ let subscriptionGeneration = 0;
617
+ let disposing;
618
+ function releaseListenerIfIdle() {
619
+ if (views.size || mounts.size) return;
620
+ subscriptionGeneration++;
621
+ const remove = unsubscribe;
622
+ unsubscribe = void 0;
623
+ subscribed = void 0;
624
+ recentlyClosed.clear();
625
+ try {
626
+ remove?.();
627
+ } catch {
628
+ }
629
+ }
630
+ function receive(input) {
631
+ const validation = validateHostEvent(input);
632
+ if (disposed || !validation.ok) return;
633
+ const event = validation.value;
634
+ if (event.kind === "transport.state") {
635
+ online = event.state === "connected";
636
+ for (const view of views.values()) view.accept(event);
637
+ return;
638
+ }
639
+ if (event.kind === "session.closed" && mounts.size) {
640
+ if (!recentlyClosed.has(event.sessionId) && recentlyClosed.size >= 256) {
641
+ for (const controller of mounts.values()) controller.abort(new AplgError("E_LIMIT_EXCEEDED", "Too many session changes occurred during startup."));
642
+ } else recentlyClosed.add(event.sessionId);
643
+ }
644
+ views.get(event.sessionId)?.accept(event);
645
+ }
646
+ async function ensureSubscribed() {
647
+ if (subscribed) return subscribed;
648
+ online = true;
649
+ const generation = subscriptionGeneration;
650
+ const pending = Promise.resolve().then(() => options.transport.subscribe(receive)).then((remove) => {
651
+ if (typeof remove !== "function") throw new AplgError("E_INVALID_MESSAGE", "The host transport returned an invalid event subscription.");
652
+ if (disposed || generation !== subscriptionGeneration) {
653
+ try {
654
+ remove();
655
+ } catch {
656
+ }
657
+ return;
658
+ }
659
+ unsubscribe = remove;
660
+ });
661
+ subscribed = pending;
662
+ try {
663
+ await pending;
664
+ } catch (error) {
665
+ if (subscribed === pending) subscribed = void 0;
666
+ throw error;
667
+ }
668
+ }
669
+ function closeBackend(sessionId) {
670
+ return bounded(() => options.transport.call("session.close", { sessionId }), requestTimeoutMs).then(() => {
671
+ });
672
+ }
673
+ function rawSessionId(raw) {
674
+ if (!validateJson(raw).ok || !raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
675
+ const id = raw.sessionId;
676
+ return typeof id === "string" && /^[\x21-\x7e]{1,128}$/.test(id) ? id : void 0;
677
+ }
678
+ function waitFor(work, signal) {
679
+ return new Promise((resolve, reject) => {
680
+ if (signal.aborted) {
681
+ reject(signal.reason);
682
+ return;
683
+ }
684
+ const abort = () => reject(signal.reason);
685
+ signal.addEventListener("abort", abort, { once: true });
686
+ void work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
687
+ });
688
+ }
689
+ return {
690
+ async mount({ pluginId, container }) {
691
+ if (disposed) throw new AplgError("E_SESSION_CLOSED", "The plugin host is closed.");
692
+ if (typeof window === "undefined" || !container?.ownerDocument?.defaultView || !container.isConnected || typeof container.append !== "function") {
693
+ throw new AplgError("E_HOST_UNAVAILABLE", "Plugin mounting requires a connected browser container.");
694
+ }
695
+ if (typeof pluginId !== "string" || !/^[a-z0-9]+(?:[.-][a-z0-9]+)+$/.test(pluginId) || pluginId.length > 160) throw new AplgError("E_INVALID_ARGUMENT", "Invalid plugin identifier.");
696
+ if (inUse.has(container)) throw new AplgError("E_INVALID_ARGUMENT", "The container already holds a plugin view.");
697
+ if (views.size + mounts.size >= 64) throw new AplgError("E_LIMIT_EXCEEDED", "Too many plugin views.");
698
+ const origins = allowedAssetOrigins ?? [checkedOrigin(container.ownerDocument.defaultView.location.origin)];
699
+ const controller = new AbortController();
700
+ mounts.set(container, controller);
701
+ inUse.add(container);
702
+ const timer = setTimeout(() => controller.abort(new AplgError("E_TIMEOUT", "Plugin startup timed out.")), handshakeTimeoutMs);
703
+ let sessionId;
704
+ let closePromise;
705
+ let managed;
706
+ const close = () => {
707
+ if (closePromise) return closePromise;
708
+ if (!sessionId || sessionOwners.get(sessionId) !== controller) return Promise.resolve();
709
+ const ownedId = sessionId;
710
+ closePromise = closeBackend(ownedId).catch(() => {
711
+ }).finally(() => {
712
+ if (sessionOwners.get(ownedId) === controller) sessionOwners.delete(ownedId);
713
+ });
714
+ return closePromise;
715
+ };
716
+ try {
717
+ await waitFor(ensureSubscribed(), controller.signal);
718
+ if (!online) throw new AplgError("E_HOST_UNAVAILABLE", "The host transport is disconnected.");
719
+ const opening = Promise.resolve().then(() => {
720
+ if (controller.signal.aborted) throw controller.signal.reason;
721
+ return options.transport.call("session.open", { pluginId });
722
+ }).then((raw2) => {
723
+ const id = rawSessionId(raw2);
724
+ if (id && !sessionOwners.has(id)) {
725
+ sessionOwners.set(id, controller);
726
+ sessionId = id;
727
+ }
728
+ if (controller.signal.aborted || disposed) {
729
+ void close();
730
+ throw controller.signal.reason ?? new AplgError("E_SESSION_CLOSED", "The plugin host closed.");
731
+ }
732
+ return raw2;
733
+ });
734
+ const raw = await waitFor(opening, controller.signal);
735
+ const result = validateSessionDescriptor(raw);
736
+ if (!result.ok) throw new AplgError("E_PROTOCOL_MISMATCH", "The host returned an invalid plugin session.");
737
+ const descriptor = JSON.parse(JSON.stringify(result.value));
738
+ if (sessionOwners.get(descriptor.sessionId) !== controller || views.has(descriptor.sessionId)) throw new AplgError("E_PROTOCOL_MISMATCH", "The backend reused an active session.");
739
+ if (descriptor.manifest.id !== pluginId) throw new AplgError("E_PROTOCOL_MISMATCH", "The backend returned a different plugin identity.");
740
+ if (recentlyClosed.has(descriptor.sessionId)) throw new AplgError("E_SESSION_CLOSED", "The plugin session closed during startup.");
741
+ requireAssetOrigin(descriptor.assetUrl, origins);
742
+ if (controller.signal.aborted || disposed || !container.isConnected || !online) throw controller.signal.reason ?? new AplgError("E_SESSION_CLOSED", "The plugin container is no longer available.");
743
+ managed = createManagedView({
744
+ descriptor,
745
+ container,
746
+ transport: options.transport,
747
+ handshakeTimeoutMs,
748
+ requestTimeoutMs,
749
+ closeSession: close,
750
+ onDisposed() {
751
+ if (views.get(descriptor.sessionId) === managed) views.delete(descriptor.sessionId);
752
+ inUse.delete(container);
753
+ releaseListenerIfIdle();
754
+ }
755
+ });
756
+ views.set(descriptor.sessionId, managed);
757
+ const view = await waitFor(managed.ready, controller.signal);
758
+ if (controller.signal.aborted || disposed) throw controller.signal.reason ?? new AplgError("E_SESSION_CLOSED", "The plugin host closed.");
759
+ return view;
760
+ } catch (error) {
761
+ if (managed) {
762
+ views.delete(sessionId);
763
+ void managed.dispose(error instanceof Error ? error : void 0);
764
+ } else if (sessionId) void close();
765
+ inUse.delete(container);
766
+ throw error;
767
+ } finally {
768
+ clearTimeout(timer);
769
+ mounts.delete(container);
770
+ if (mounts.size === 0) recentlyClosed.clear();
771
+ releaseListenerIfIdle();
772
+ }
773
+ },
774
+ dispose() {
775
+ if (disposing) return disposing;
776
+ disposed = true;
777
+ for (const controller of mounts.values()) controller.abort(new AplgError("E_SESSION_CLOSED", "The plugin host closed."));
778
+ const closing = [...views.values()].map((view) => view.dispose());
779
+ views.clear();
780
+ mounts.clear();
781
+ inUse.clear();
782
+ releaseListenerIfIdle();
783
+ disposing = Promise.allSettled(closing).then(() => {
784
+ });
785
+ return disposing;
786
+ }
787
+ };
788
+ }
789
+ export {
790
+ createPluginHost
791
+ };