arcane-os 0.2.2 → 0.3.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 (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +462 -384
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +639 -98
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +470 -0
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. package/src/workspace.mjs +1 -1
@@ -6,24 +6,112 @@ import {
6
6
 
7
7
  export const BROWSER_SPEECH_ARTIFACT_PROTOCOL =
8
8
  "arcane-ai-browser-speech-artifacts/1";
9
+ export const BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL =
10
+ "arcane-ai-browser-speech-artifact-graph/1";
9
11
 
10
12
  const MODEL_AUTHORITY_PROTOCOL = "arcane-ai-model-authority/1";
11
13
  const MANIFEST_SCHEMA = "arcane.ai.browser-speech.assets.v1";
14
+ const ARTIFACT_GRAPH_MANIFEST_SCHEMA =
15
+ "arcane.ai.browser-speech.authenticated-artifact-graph.v1";
16
+ const ARTIFACT_GRAPH_KIND = "browser-speech-authenticated-artifact-graph";
17
+ const ARTIFACT_GRAPH_MODULE_KIND =
18
+ "browser-speech-authenticated-artifact-graph";
19
+ const ARTIFACT_GRAPH_GUARDS = "__arcaneBrowserSpeechArtifactGraphGuardsV1";
12
20
  const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
13
21
  const MUTABLE_PATH_PATTERN = /\/(?:resolve\/)?(?:main|master|latest)(?:\/|$)/iu;
22
+ const ARTIFACT_GRAPH_MUTABLE_SOURCE_PATTERN =
23
+ /\/(?:refs\/heads\/(?:main|master)|resolve\/(?:main|master)|(?:main|master|latest))(?:\/|$)|@(?:latest|next)(?:\/|$)/iu;
14
24
  const AUTHORITIES = new WeakSet();
15
25
  const AUTHORITY_METADATA = new WeakMap();
26
+ const ARTIFACT_GRAPHS = new WeakSet();
27
+ const ARTIFACT_GRAPH_METADATA = new WeakMap();
28
+ const ARTIFACT_ERRORS = new WeakSet();
16
29
  const STORES = new WeakSet();
30
+ const PLATFORM_CREATE_OBJECT_URL = typeof globalThis.URL?.createObjectURL === "function"
31
+ ? globalThis.URL.createObjectURL.bind(globalThis.URL)
32
+ : null;
33
+ const PLATFORM_REVOKE_OBJECT_URL = typeof globalThis.URL?.revokeObjectURL === "function"
34
+ ? globalThis.URL.revokeObjectURL.bind(globalThis.URL)
35
+ : null;
36
+ const PLATFORM_FETCH = typeof globalThis.fetch === "function"
37
+ ? globalThis.fetch.bind(globalThis)
38
+ : null;
39
+ const LEGACY_ARTIFACT_ERROR_REASONS = Object.freeze({
40
+ ARCANE_AI_REQUEST_ABORTED: "browser-speech-artifact-preparation-cancelled",
41
+ ARCANE_AI_STORAGE_BUSY: "browser-speech-artifact-dbopfs-write-lock-unavailable",
42
+ ARCANE_AI_STORAGE_UNAVAILABLE: "browser-speech-artifact-dbopfs-table-unavailable",
43
+ ARCANE_AI_STORAGE_DELETE_FAILED: "browser-speech-artifact-dbopfs-delete-rejected",
44
+ ARCANE_AI_STORAGE_READ_FAILED: "browser-speech-artifact-dbopfs-read-rejected",
45
+ ARCANE_AI_ARTIFACT_SOURCE_INVALID: "browser-speech-artifact-source-body-unreadable",
46
+ ARCANE_AI_RUNTIME_MODULE_GRAPH_UNDECLARED: "browser-speech-runtime-module-graph-undeclared",
47
+ ARCANE_AI_ARTIFACT_SOURCE_UNAVAILABLE: "browser-speech-artifact-fetch-unavailable",
48
+ ARCANE_AI_ARTIFACT_DOWNLOAD_FAILED: "browser-speech-artifact-fetch-rejected",
49
+ ARCANE_AI_ARTIFACT_SOURCE_CHANGED: "browser-speech-artifact-source-redirected",
50
+ ARCANE_AI_ARTIFACT_SIZE_MISMATCH: "browser-speech-artifact-byte-length-mismatch",
51
+ ARCANE_AI_ARTIFACT_DIGEST_MISMATCH: "browser-speech-artifact-sha256-mismatch",
52
+ ARCANE_AI_ARTIFACT_CACHE_REJECTED: "browser-speech-artifact-dbopfs-cache-rejected",
53
+ ARCANE_AI_ARTIFACT_OFFLINE_MISS: "browser-speech-artifact-offline-cache-miss",
54
+ });
17
55
 
18
- function speechError(code, message, cause) {
56
+ const ARTIFACT_GRAPH_FILE_KINDS = new Set([
57
+ "model-configuration-json",
58
+ "model-generation-configuration-json",
59
+ "model-onnx-binary",
60
+ "model-onnx-external-data",
61
+ "model-opaque-data",
62
+ "model-preprocessor-json",
63
+ "model-tokenizer-json",
64
+ "runtime-auxiliary-javascript",
65
+ "runtime-entrypoint-javascript",
66
+ "runtime-opaque-data",
67
+ "runtime-wasm-binary",
68
+ "voice-style-binary",
69
+ ]);
70
+ const ARTIFACT_GRAPH_JAVASCRIPT_KINDS = new Set([
71
+ "runtime-auxiliary-javascript",
72
+ "runtime-entrypoint-javascript",
73
+ ]);
74
+ const ARTIFACT_GRAPH_ONNX_NAMESPACES = new Set([
75
+ "kokoro-env-wasm-paths",
76
+ "transformers-env-backends-onnx-wasm",
77
+ ]);
78
+ const ARTIFACT_GRAPH_EDGE_POLICIES = new Set([
79
+ "artifact-targets-admitted",
80
+ "inactive-runtime-branch-rejected",
81
+ ]);
82
+ const ARTIFACT_GRAPH_IMPORT_MATCHES = new Set([
83
+ "exact-runtime-specifier",
84
+ "materialized-module-url",
85
+ ]);
86
+
87
+ function speechError(code, message, cause, reason = LEGACY_ARTIFACT_ERROR_REASONS[code]) {
19
88
  const error = cause === undefined
20
89
  ? new Error(message)
21
90
  : new Error(message, { cause });
22
91
  error.name = "ArcaneBrowserSpeechError";
23
92
  error.code = code;
93
+ if (typeof reason === "string" && reason) error.reason = reason;
94
+ ARTIFACT_ERRORS.add(error);
95
+ return error;
96
+ }
97
+
98
+ function artifactGraphError(reason, message, cause, Type = Error) {
99
+ const error = cause === undefined
100
+ ? new Type(message)
101
+ : new Type(message, { cause });
102
+ error.name = Type === TypeError
103
+ ? "TypeError"
104
+ : "ArcaneBrowserSpeechArtifactGraphError";
105
+ error.code = `ARCANE_AI_${reason.toUpperCase().replaceAll("-", "_")}`;
106
+ error.reason = reason;
107
+ ARTIFACT_ERRORS.add(error);
24
108
  return error;
25
109
  }
26
110
 
111
+ function artifactGraphTypeError(reason, message, cause) {
112
+ return artifactGraphError(reason, message, cause, TypeError);
113
+ }
114
+
27
115
  function throwIfAborted(signal) {
28
116
  if (!signal?.aborted) return;
29
117
  const error = speechError(
@@ -50,6 +138,26 @@ function identifier(value, label) {
50
138
  return result;
51
139
  }
52
140
 
141
+ function artifactGraphText(value, label, reason = "artifact-graph-field-text-required") {
142
+ if (typeof value !== "string" || !value.trim()) {
143
+ throw artifactGraphTypeError(reason, `${label} must be a nonempty string.`);
144
+ }
145
+ return value.trim();
146
+ }
147
+
148
+ function artifactGraphIdentifier(
149
+ value,
150
+ label,
151
+ missingReason = "artifact-graph-identifier-missing",
152
+ lengthReason = "artifact-graph-identifier-length-exceeded",
153
+ ) {
154
+ const result = artifactGraphText(value, label, missingReason);
155
+ if (result.length > 128) {
156
+ throw artifactGraphTypeError(lengthReason, `${label} must not exceed 128 characters.`);
157
+ }
158
+ return result;
159
+ }
160
+
53
161
  function immutableUrl(value, label) {
54
162
  let result;
55
163
  try {
@@ -73,6 +181,298 @@ function immutableUrl(value, label) {
73
181
  return result.href;
74
182
  }
75
183
 
184
+ function canonicalArtifactPath(
185
+ value,
186
+ label,
187
+ missingReason = "artifact-graph-file-path-missing",
188
+ formatReason = "artifact-graph-file-path-noncanonical",
189
+ ) {
190
+ const path = artifactGraphText(
191
+ value,
192
+ label,
193
+ missingReason,
194
+ );
195
+ if (
196
+ path !== value
197
+ || path !== path.normalize("NFC")
198
+ || path.startsWith("/")
199
+ || path.endsWith("/")
200
+ || path.includes("\\")
201
+ || /[%?#\u0000-\u001f\u007f]/u.test(path)
202
+ || path.split("/").some((part) => !part || part === "." || part === "..")
203
+ ) {
204
+ throw artifactGraphTypeError(
205
+ formatReason,
206
+ `${label} must be one NFC-normalized relative path without escapes, empty segments, or URL delimiters.`,
207
+ );
208
+ }
209
+ return path;
210
+ }
211
+
212
+ function graphSha256(
213
+ value,
214
+ label,
215
+ missingReason = "artifact-graph-file-sha256-missing",
216
+ formatReason = "artifact-graph-file-sha256-format-mismatch",
217
+ ) {
218
+ const sha256 = artifactGraphText(
219
+ value,
220
+ label,
221
+ missingReason,
222
+ );
223
+ if (sha256 !== value || !SHA256_PATTERN.test(sha256)) {
224
+ throw artifactGraphTypeError(
225
+ formatReason,
226
+ `${label} must contain exactly 64 lowercase hexadecimal characters.`,
227
+ );
228
+ }
229
+ return sha256;
230
+ }
231
+
232
+ function graphPositiveInteger(
233
+ value,
234
+ label,
235
+ reason = "artifact-graph-positive-safe-integer-required",
236
+ ) {
237
+ if (!Number.isSafeInteger(value) || value < 1) {
238
+ throw artifactGraphTypeError(
239
+ reason,
240
+ `${label} must be a positive safe integer.`,
241
+ );
242
+ }
243
+ return value;
244
+ }
245
+
246
+ function graphOptionalSampleRate(value, label, required = false) {
247
+ if (value === undefined || value === null) {
248
+ if (!required) return null;
249
+ throw artifactGraphTypeError(
250
+ "artifact-graph-sample-rate-missing",
251
+ `${label} is required.`,
252
+ );
253
+ }
254
+ return graphPositiveInteger(
255
+ value,
256
+ label,
257
+ "artifact-graph-sample-rate-positive-safe-integer-required",
258
+ );
259
+ }
260
+
261
+ function exactMediaType(value, label) {
262
+ const mediaType = artifactGraphText(
263
+ value,
264
+ label,
265
+ "artifact-graph-file-media-type-missing",
266
+ ).toLowerCase();
267
+ if (
268
+ mediaType !== value
269
+ || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/u.test(mediaType)
270
+ ) {
271
+ throw artifactGraphTypeError(
272
+ "artifact-graph-file-media-type-format-mismatch",
273
+ `${label} must be one lowercase media type without parameters.`,
274
+ );
275
+ }
276
+ return mediaType;
277
+ }
278
+
279
+ function exactSourceMediaType(value, label) {
280
+ const mediaType = artifactGraphText(
281
+ value,
282
+ label,
283
+ "artifact-graph-file-source-media-type-missing",
284
+ ).toLowerCase();
285
+ if (
286
+ mediaType !== value
287
+ || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/u.test(mediaType)
288
+ ) {
289
+ throw artifactGraphTypeError(
290
+ "artifact-graph-file-source-media-type-format-mismatch",
291
+ `${label} must be one lowercase media type without parameters.`,
292
+ );
293
+ }
294
+ return mediaType;
295
+ }
296
+
297
+ function graphRuntimeRequestUrl(value, label) {
298
+ const url = artifactGraphText(
299
+ value,
300
+ label,
301
+ "artifact-graph-runtime-request-url-text-required",
302
+ );
303
+ let result;
304
+ try {
305
+ result = new URL(url);
306
+ } catch (error) {
307
+ throw artifactGraphTypeError(
308
+ "artifact-graph-runtime-request-url-not-absolute",
309
+ `${label} must be an absolute HTTPS URL.`,
310
+ error,
311
+ );
312
+ }
313
+ if (result.protocol !== "https:") {
314
+ throw artifactGraphTypeError(
315
+ "artifact-graph-runtime-request-url-protocol-not-https",
316
+ `${label} must use HTTPS.`,
317
+ );
318
+ }
319
+ if (result.username || result.password) {
320
+ throw artifactGraphTypeError(
321
+ "artifact-graph-runtime-request-url-credentials-rejected",
322
+ `${label} must not contain credentials.`,
323
+ );
324
+ }
325
+ if (result.hash) {
326
+ throw artifactGraphTypeError(
327
+ "artifact-graph-runtime-request-url-fragment-rejected",
328
+ `${label} must not contain a fragment.`,
329
+ );
330
+ }
331
+ return result.href;
332
+ }
333
+
334
+ function graphRedirectFinalOrigin(value, label) {
335
+ const text = artifactGraphText(
336
+ value,
337
+ label,
338
+ "artifact-graph-source-redirect-final-origin-text-required",
339
+ );
340
+ if (text !== value) {
341
+ throw artifactGraphTypeError(
342
+ "artifact-graph-source-redirect-final-origin-whitespace-rejected",
343
+ `${label} must not contain surrounding whitespace.`,
344
+ );
345
+ }
346
+ let result;
347
+ try {
348
+ result = new URL(text);
349
+ } catch (error) {
350
+ throw artifactGraphTypeError(
351
+ "artifact-graph-source-redirect-final-origin-not-absolute",
352
+ `${label} must be an absolute HTTPS origin.`,
353
+ error,
354
+ );
355
+ }
356
+ if (result.protocol !== "https:") {
357
+ throw artifactGraphTypeError(
358
+ "artifact-graph-source-redirect-final-origin-protocol-not-https",
359
+ `${label} must use HTTPS.`,
360
+ );
361
+ }
362
+ if (result.username || result.password) {
363
+ throw artifactGraphTypeError(
364
+ "artifact-graph-source-redirect-final-origin-credentials-rejected",
365
+ `${label} must not contain credentials.`,
366
+ );
367
+ }
368
+ if (result.pathname !== "/") {
369
+ throw artifactGraphTypeError(
370
+ "artifact-graph-source-redirect-final-origin-path-rejected",
371
+ `${label} must not contain a path.`,
372
+ );
373
+ }
374
+ if (result.search) {
375
+ throw artifactGraphTypeError(
376
+ "artifact-graph-source-redirect-final-origin-query-rejected",
377
+ `${label} must not contain a query.`,
378
+ );
379
+ }
380
+ if (result.hash) {
381
+ throw artifactGraphTypeError(
382
+ "artifact-graph-source-redirect-final-origin-fragment-rejected",
383
+ `${label} must not contain a fragment.`,
384
+ );
385
+ }
386
+ return result.origin;
387
+ }
388
+
389
+ function normalizeGraphRedirectFinalOrigins(value, path) {
390
+ if (value === undefined) return Object.freeze([]);
391
+ if (!Array.isArray(value)) {
392
+ throw artifactGraphTypeError(
393
+ "artifact-graph-source-redirect-final-origins-not-array",
394
+ `Artifact graph file ${path} redirectFinalOrigins must be an array.`,
395
+ );
396
+ }
397
+ if (value.length < 1) {
398
+ throw artifactGraphTypeError(
399
+ "artifact-graph-source-redirect-final-origin-inventory-empty",
400
+ `Artifact graph file ${path} redirectFinalOrigins must contain at least one final origin when supplied.`,
401
+ );
402
+ }
403
+ const origins = value.map((origin, index) => graphRedirectFinalOrigin(
404
+ origin,
405
+ `Artifact graph file ${path} redirectFinalOrigins[${String(index)}]`,
406
+ ));
407
+ const unique = new Set(origins);
408
+ if (unique.size !== origins.length) {
409
+ throw artifactGraphTypeError(
410
+ "artifact-graph-source-redirect-final-origin-duplicate",
411
+ `Artifact graph file ${path} redirectFinalOrigins must be unique after canonicalization.`,
412
+ );
413
+ }
414
+ return Object.freeze([...origins].sort(lexicalCompare));
415
+ }
416
+
417
+ function graphImmutableUrl(value, label, revision, sha256) {
418
+ let url;
419
+ try {
420
+ url = immutableUrl(
421
+ artifactGraphText(
422
+ value,
423
+ label,
424
+ "artifact-graph-source-url-missing",
425
+ ),
426
+ label,
427
+ );
428
+ } catch (error) {
429
+ if (ARTIFACT_ERRORS.has(error)) throw error;
430
+ throw artifactGraphTypeError(
431
+ "artifact-graph-source-url-mutable",
432
+ `${label} must identify an immutable HTTPS or same-origin source authority.`,
433
+ error,
434
+ );
435
+ }
436
+ const identityUrl = url.toLowerCase();
437
+ if (ARTIFACT_GRAPH_MUTABLE_SOURCE_PATTERN.test(new URL(url).pathname)) {
438
+ throw artifactGraphTypeError(
439
+ "artifact-graph-source-url-mutable",
440
+ `${label} names a mutable branch, channel, or release alias.`,
441
+ );
442
+ }
443
+ if (
444
+ !identityUrl.includes(revision.toLowerCase())
445
+ && !identityUrl.includes(sha256)
446
+ ) {
447
+ throw artifactGraphTypeError(
448
+ "artifact-graph-source-revision-unbound",
449
+ `${label} must contain the file revision or SHA-256 identity.`,
450
+ );
451
+ }
452
+ return url;
453
+ }
454
+
455
+ function canonicalJson(value) {
456
+ if (Array.isArray(value)) {
457
+ return `[${value.map(canonicalJson).join(",")}]`;
458
+ }
459
+ if (value && typeof value === "object") {
460
+ return `{${Object.keys(value).sort().map((key) =>
461
+ `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
462
+ }
463
+ return JSON.stringify(value);
464
+ }
465
+
466
+ function lexicalCompare(left, right) {
467
+ return left < right ? -1 : left > right ? 1 : 0;
468
+ }
469
+
470
+ function sha256Text(value) {
471
+ const digest = createStreamingSha256();
472
+ digest.update(new TextEncoder().encode(value));
473
+ return digest.digestHex();
474
+ }
475
+
76
476
  function normalizeFile(value, kind, index, revision) {
77
477
  if (!value || typeof value !== "object" || Array.isArray(value)) {
78
478
  throw new TypeError(`${kind} file ${String(index)} must be an object.`);
@@ -124,9 +524,13 @@ function normalizeFile(value, kind, index, revision) {
124
524
  });
125
525
  }
126
526
 
127
- function uniqueFiles(files, label, kind, revision) {
128
- if (!Array.isArray(files) || files.length < 1) {
129
- throw new TypeError(`${label} requires a nonempty files array.`);
527
+ function uniqueFiles(files, label, kind, revision, { allowEmpty = false } = {}) {
528
+ if (!Array.isArray(files) || (!allowEmpty && files.length < 1)) {
529
+ throw new TypeError(
530
+ allowEmpty
531
+ ? `${label} files must be an array.`
532
+ : `${label} requires a nonempty files array.`,
533
+ );
130
534
  }
131
535
  const paths = new Set();
132
536
  const urls = new Set();
@@ -152,320 +556,1702 @@ function publicFile(file) {
152
556
  return Object.freeze(result);
153
557
  }
154
558
 
155
- /**
156
- * Admits one caller-owned browser speech model/runtime description. The SDK
157
- * supplies no model URL or profile; applications choose every immutable byte.
158
- */
159
- export function createBrowserSpeechAuthority({
160
- providerId,
161
- role,
162
- model,
163
- runtime,
164
- security,
165
- } = {}) {
166
- const normalizedProviderId = identifier(providerId, "Browser speech providerId");
167
- if (role !== "stt" && role !== "tts") {
168
- throw new TypeError('Browser speech role must be "stt" or "tts".');
169
- }
170
- if (!model || typeof model !== "object" || Array.isArray(model)) {
171
- throw new TypeError("Browser speech model descriptor is required.");
559
+ function normalizeArtifactGraphFile(value, index) {
560
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
561
+ throw artifactGraphTypeError(
562
+ "artifact-graph-file-descriptor-not-object",
563
+ `Artifact graph file ${String(index)} must be an object.`,
564
+ );
172
565
  }
173
- if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
174
- throw new TypeError("Browser speech runtime descriptor is required.");
566
+ const kind = artifactGraphText(
567
+ value.kind,
568
+ `Artifact graph file ${String(index)} kind`,
569
+ "artifact-graph-file-kind-missing",
570
+ );
571
+ if (!ARTIFACT_GRAPH_FILE_KINDS.has(kind)) {
572
+ throw artifactGraphTypeError(
573
+ "artifact-graph-file-kind-not-admitted",
574
+ `Artifact graph file ${String(index)} kind is not supported.`,
575
+ );
175
576
  }
176
- const normalizedSecurity = normalizeModelSecurity(
177
- security,
178
- "Browser speech provider security",
577
+ const path = canonicalArtifactPath(
578
+ value.path,
579
+ `Artifact graph file ${String(index)} path`,
179
580
  );
180
- const modelId = identifier(model.id, "Browser speech model id");
181
- const modelRevision = identifier(model.revision, "Browser speech model revision");
182
- const repository = identifier(model.repository, "Browser speech model repository");
183
- const modelFiles = uniqueFiles(
184
- model.files,
185
- "Browser speech model",
186
- "model",
187
- modelRevision,
581
+ const revision = artifactGraphIdentifier(
582
+ value.revision,
583
+ `Artifact graph file ${path} revision`,
584
+ "artifact-graph-file-revision-missing",
585
+ "artifact-graph-file-revision-length-exceeded",
188
586
  );
189
- const runtimeAdapter = requiredText(runtime.adapter, "Browser speech runtime adapter");
190
- const expectedAdapter = role === "stt"
191
- ? "transformers-whisper"
192
- : "kokoro-js";
193
- if (runtimeAdapter !== expectedAdapter) {
194
- throw new TypeError(`Browser ${role} runtime adapter must equal ${expectedAdapter}.`);
587
+ const bytes = graphPositiveInteger(
588
+ value.bytes,
589
+ `Artifact graph file ${path} bytes`,
590
+ "artifact-graph-file-byte-length-positive-safe-integer-required",
591
+ );
592
+ const sha256 = graphSha256(
593
+ value.sha256,
594
+ `Artifact graph file ${path} sha256`,
595
+ );
596
+ const sourceUrl = graphImmutableUrl(
597
+ value.sourceUrl ?? value.url,
598
+ `Artifact graph file ${path} sourceUrl`,
599
+ revision,
600
+ sha256,
601
+ );
602
+ const redirectFinalOrigins = normalizeGraphRedirectFinalOrigins(
603
+ value.redirectFinalOrigins,
604
+ path,
605
+ );
606
+ const license = artifactGraphText(
607
+ value.license,
608
+ `Artifact graph file ${path} license`,
609
+ "artifact-graph-file-license-missing",
610
+ );
611
+ if (license !== value.license) {
612
+ throw artifactGraphTypeError(
613
+ "artifact-graph-file-license-whitespace-rejected",
614
+ `Artifact graph file ${path} license must not contain surrounding whitespace.`,
615
+ );
195
616
  }
196
- const runtimeVersion = identifier(runtime.version, "Browser speech runtime version");
197
- const runtimeRevision = identifier(runtime.revision, "Browser speech runtime revision");
198
- const runtimeFiles = uniqueFiles(
199
- runtime.files,
200
- "Browser speech runtime",
201
- "runtime",
202
- runtimeRevision,
617
+ if (license !== license.normalize("NFC")) {
618
+ throw artifactGraphTypeError(
619
+ "artifact-graph-file-license-not-nfc",
620
+ `Artifact graph file ${path} license must be NFC-normalized.`,
621
+ );
622
+ }
623
+ if (/[\u0000-\u001f\u007f]/u.test(license)) {
624
+ throw artifactGraphTypeError(
625
+ "artifact-graph-file-license-control-character-rejected",
626
+ `Artifact graph file ${path} license must not contain control characters.`,
627
+ );
628
+ }
629
+ if (license.length > 256) {
630
+ throw artifactGraphTypeError(
631
+ "artifact-graph-file-license-length-exceeded",
632
+ `Artifact graph file ${path} license must not exceed 256 characters.`,
633
+ );
634
+ }
635
+ const mediaType = exactMediaType(
636
+ value.mediaType,
637
+ `Artifact graph file ${path} mediaType`,
203
638
  );
204
- const entry = requiredText(runtime.entry, "Browser speech runtime entry");
205
- const entryFile = runtimeFiles.find((file) => file.path === entry);
206
- if (!entryFile) {
207
- throw new TypeError("Browser speech runtime entry must name one runtime file path.");
639
+ const sourceMediaType = value.sourceMediaType === undefined
640
+ ? mediaType
641
+ : exactSourceMediaType(
642
+ value.sourceMediaType,
643
+ `Artifact graph file ${path} sourceMediaType`,
644
+ );
645
+ if (
646
+ ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(kind)
647
+ && mediaType !== "application/javascript"
648
+ && mediaType !== "text/javascript"
649
+ ) {
650
+ throw artifactGraphTypeError(
651
+ "artifact-graph-javascript-media-type-mismatch",
652
+ `Artifact graph JavaScript file ${path} must use application/javascript or text/javascript.`,
653
+ );
208
654
  }
209
- if (!/\.(?:m?js)$/iu.test(entryFile.path) || entryFile.mediaType !== "text/javascript") {
210
- throw new TypeError("Browser speech runtime entry must be a JavaScript module.");
655
+ if (kind === "runtime-wasm-binary" && mediaType !== "application/wasm") {
656
+ throw artifactGraphTypeError(
657
+ "artifact-graph-wasm-media-type-mismatch",
658
+ `Artifact graph WebAssembly file ${path} must use application/wasm.`,
659
+ );
211
660
  }
212
- const normalizedModel = Object.freeze({
213
- id: modelId,
214
- repository,
215
- revision: modelRevision,
216
- defaultVoice: role === "tts"
217
- ? identifier(model.defaultVoice, "Browser Kokoro defaultVoice")
218
- : null,
219
- files: modelFiles,
661
+ if (kind.endsWith("-json") && mediaType !== "application/json") {
662
+ throw artifactGraphTypeError(
663
+ "artifact-graph-json-media-type-mismatch",
664
+ `Artifact graph JSON file ${path} must use application/json.`,
665
+ );
666
+ }
667
+ const requestUrls = value.runtimeRequestUrls ?? [];
668
+ if (!Array.isArray(requestUrls)) {
669
+ throw artifactGraphTypeError(
670
+ "artifact-graph-runtime-request-routes-not-array",
671
+ `Artifact graph file ${path} runtimeRequestUrls must be an array.`,
672
+ );
673
+ }
674
+ const normalizedRequestUrls = [...new Set(requestUrls.map((url, requestIndex) =>
675
+ graphRuntimeRequestUrl(
676
+ url,
677
+ `Artifact graph file ${path} runtimeRequestUrls[${String(requestIndex)}]`,
678
+ )))].sort();
679
+ if (normalizedRequestUrls.length !== requestUrls.length) {
680
+ throw artifactGraphTypeError(
681
+ "artifact-graph-runtime-request-route-duplicate",
682
+ `Artifact graph file ${path} runtimeRequestUrls must be unique.`,
683
+ );
684
+ }
685
+ return Object.freeze({
686
+ kind,
687
+ index,
688
+ path,
689
+ sourceUrl,
690
+ revision,
691
+ license,
692
+ mediaType,
693
+ sourceMediaType,
694
+ bytes,
695
+ sha256,
696
+ runtimeRequestUrls: Object.freeze(normalizedRequestUrls),
697
+ redirectFinalOrigins,
220
698
  });
221
- const normalizedRuntime = Object.freeze({
222
- adapter: runtimeAdapter,
223
- version: runtimeVersion,
224
- revision: runtimeRevision,
225
- entry,
226
- files: runtimeFiles,
699
+ }
700
+
701
+ function publicArtifactGraphFile(file) {
702
+ return Object.freeze({
703
+ kind: file.kind,
704
+ path: file.path,
705
+ sourceUrl: file.sourceUrl,
706
+ revision: file.revision,
707
+ license: file.license,
708
+ mediaType: file.mediaType,
709
+ ...(file.sourceMediaType === file.mediaType
710
+ ? {}
711
+ : { sourceMediaType: file.sourceMediaType }),
712
+ bytes: file.bytes,
713
+ sha256: file.sha256,
714
+ runtimeRequestUrls: file.runtimeRequestUrls,
715
+ ...(file.redirectFinalOrigins.length < 1
716
+ ? {}
717
+ : { redirectFinalOrigins: file.redirectFinalOrigins }),
227
718
  });
228
- const files = Object.freeze([...runtimeFiles, ...modelFiles]);
229
- const allPaths = new Set();
230
- const allUrls = new Set();
231
- for (const file of files) {
232
- if (allPaths.has(file.path) || allUrls.has(file.url)) {
233
- throw new TypeError("Browser speech runtime and model file identities must not overlap.");
234
- }
235
- allPaths.add(file.path);
236
- allUrls.add(file.url);
719
+ }
720
+
721
+ function normalizeGraphOccurrence(value, label) {
722
+ return graphPositiveInteger(
723
+ value,
724
+ `${label} occurrence`,
725
+ "artifact-graph-edge-occurrence-positive-safe-integer-required",
726
+ );
727
+ }
728
+
729
+ function normalizeGraphModulePath(value, label, filesByPath) {
730
+ const path = canonicalArtifactPath(
731
+ value,
732
+ `${label} modulePath`,
733
+ "artifact-graph-edge-module-path-missing",
734
+ "artifact-graph-edge-module-path-noncanonical",
735
+ );
736
+ if (!ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(filesByPath.get(path)?.kind)) {
737
+ throw artifactGraphTypeError(
738
+ "artifact-graph-edge-module-path-not-runtime-javascript",
739
+ `${label} modulePath must name one declared runtime JavaScript file.`,
740
+ );
237
741
  }
238
- const authority = Object.freeze({
239
- protocol: MODEL_AUTHORITY_PROTOCOL,
240
- providerId: normalizedProviderId,
241
- modelId,
242
- admitted: true,
243
- role,
244
- repository,
245
- revision: modelRevision,
246
- defaultVoice: normalizedModel.defaultVoice,
247
- runtime: Object.freeze({
248
- adapter: normalizedRuntime.adapter,
249
- version: normalizedRuntime.version,
250
- revision: normalizedRuntime.revision,
251
- entry: normalizedRuntime.entry,
252
- files: Object.freeze(runtimeFiles.map(publicFile)),
253
- }),
254
- files: Object.freeze(modelFiles.map(publicFile)),
255
- security: normalizedSecurity,
742
+ return path;
743
+ }
744
+
745
+ function normalizeGraphTargetPath(value, label, filesByPath, javascript = false) {
746
+ const path = canonicalArtifactPath(
747
+ value,
748
+ `${label} targetPath`,
749
+ "artifact-graph-edge-target-path-missing",
750
+ "artifact-graph-edge-target-path-noncanonical",
751
+ );
752
+ const target = filesByPath.get(path);
753
+ if (!target || (javascript && !ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(target.kind))) {
754
+ throw artifactGraphTypeError(
755
+ "artifact-graph-edge-target-path-undeclared",
756
+ `${label} targetPath must name a compatible declared graph file.`,
757
+ );
758
+ }
759
+ return path;
760
+ }
761
+
762
+ function normalizeGraphPolicy(value, label) {
763
+ const policy = value ?? "artifact-targets-admitted";
764
+ if (!ARTIFACT_GRAPH_EDGE_POLICIES.has(policy)) {
765
+ throw artifactGraphTypeError(
766
+ "artifact-graph-edge-policy-not-admitted",
767
+ `${label} edgePolicy must identify an admitted artifact target or an inactive rejected runtime branch.`,
768
+ );
769
+ }
770
+ return policy;
771
+ }
772
+
773
+ function normalizeGraphTargets(value, label, filesByPath, {
774
+ javascript = false,
775
+ worker = false,
776
+ } = {}) {
777
+ if (!Array.isArray(value)) {
778
+ throw artifactGraphTypeError(
779
+ "artifact-graph-edge-targets-not-array",
780
+ `${label} targets must be an array.`,
781
+ );
782
+ }
783
+ const allowedMatches = worker
784
+ ? new Set([...ARTIFACT_GRAPH_IMPORT_MATCHES, "self-module-url"])
785
+ : ARTIFACT_GRAPH_IMPORT_MATCHES;
786
+ const targets = value.map((target, index) => {
787
+ if (!target || typeof target !== "object" || Array.isArray(target)) {
788
+ throw artifactGraphTypeError(
789
+ "artifact-graph-edge-target-not-object",
790
+ `${label} target ${String(index)} must be an object.`,
791
+ );
792
+ }
793
+ const match = artifactGraphText(
794
+ target.match,
795
+ `${label} target ${String(index)} match`,
796
+ "artifact-graph-edge-target-match-missing",
797
+ );
798
+ if (!allowedMatches.has(match)) {
799
+ throw artifactGraphTypeError(
800
+ "artifact-graph-edge-target-match-not-admitted",
801
+ `${label} target ${String(index)} match is not admitted.`,
802
+ );
803
+ }
804
+ const targetPath = normalizeGraphTargetPath(
805
+ target.targetPath,
806
+ `${label} target ${String(index)}`,
807
+ filesByPath,
808
+ javascript,
809
+ );
810
+ const exactSpecifier = match === "exact-runtime-specifier"
811
+ ? artifactGraphText(
812
+ target.exactSpecifier ?? target.specifier,
813
+ `${label} target ${String(index)} exactSpecifier`,
814
+ "artifact-graph-edge-target-specifier-missing",
815
+ )
816
+ : null;
817
+ return Object.freeze({ match, targetPath, exactSpecifier });
256
818
  });
257
- AUTHORITIES.add(authority);
258
- AUTHORITY_METADATA.set(authority, Object.freeze({
259
- model: normalizedModel,
260
- runtime: normalizedRuntime,
261
- files,
262
- }));
263
- return authority;
819
+ targets.sort((left, right) => lexicalCompare(canonicalJson(left), canonicalJson(right)));
820
+ const identities = new Set(targets.map(canonicalJson));
821
+ if (identities.size !== targets.length) {
822
+ throw artifactGraphTypeError(
823
+ "artifact-graph-edge-target-duplicate",
824
+ `${label} targets must be unique.`,
825
+ );
826
+ }
827
+ return Object.freeze(targets);
264
828
  }
265
829
 
266
- function authorityProjection(authority) {
830
+ function normalizeArtifactGraphEdges(value, filesByPath, negativeRuntimeRequestUrls) {
831
+ const edges = value ?? {};
832
+ if (!edges || typeof edges !== "object" || Array.isArray(edges)) {
833
+ throw artifactGraphTypeError(
834
+ "artifact-graph-edges-not-object",
835
+ "Artifact graph edges must be an object.",
836
+ );
837
+ }
838
+ const allowedEdgeNames = new Set([
839
+ "cacheOpens",
840
+ "dynamicImports",
841
+ "fetches",
842
+ "moduleWorkers",
843
+ "staticImports",
844
+ ]);
845
+ if (Reflect.ownKeys(edges).some((name) =>
846
+ typeof name !== "string" || !allowedEdgeNames.has(name))) {
847
+ throw artifactGraphTypeError(
848
+ "artifact-graph-edge-kind-not-admitted",
849
+ "Artifact graph edges contain an edge kind that is not admitted.",
850
+ );
851
+ }
852
+
853
+ function normalizeArray(name, normalize) {
854
+ const values = edges[name] ?? [];
855
+ if (!Array.isArray(values)) {
856
+ throw artifactGraphTypeError(
857
+ "artifact-graph-edge-list-not-array",
858
+ `Artifact graph ${name} must be an array.`,
859
+ );
860
+ }
861
+ const normalized = values.map((edge, index) => {
862
+ if (!edge || typeof edge !== "object" || Array.isArray(edge)) {
863
+ throw artifactGraphTypeError(
864
+ "artifact-graph-edge-not-object",
865
+ `Artifact graph ${name}[${String(index)}] must be an object.`,
866
+ );
867
+ }
868
+ const label = `Artifact graph ${name}[${String(index)}]`;
869
+ const modulePath = normalizeGraphModulePath(edge.modulePath, label, filesByPath);
870
+ const occurrence = normalizeGraphOccurrence(edge.occurrence, label);
871
+ return normalize(edge, label, modulePath, occurrence);
872
+ });
873
+ normalized.sort((left, right) => lexicalCompare(canonicalJson(left), canonicalJson(right)));
874
+ const occurrences = new Set();
875
+ for (const edge of normalized) {
876
+ const key = `${edge.modulePath}\u0000${String(edge.occurrence)}`;
877
+ if (occurrences.has(key)) {
878
+ throw artifactGraphTypeError(
879
+ "artifact-graph-edge-occurrence-duplicate",
880
+ `Artifact graph ${name} contains a duplicate module occurrence.`,
881
+ );
882
+ }
883
+ occurrences.add(key);
884
+ }
885
+ return Object.freeze(normalized);
886
+ }
887
+
888
+ const staticImports = normalizeArray(
889
+ "staticImports",
890
+ (edge, label, modulePath, occurrence) => Object.freeze({
891
+ modulePath,
892
+ occurrence,
893
+ specifier: artifactGraphText(
894
+ edge.specifier,
895
+ `${label} specifier`,
896
+ "artifact-graph-static-import-specifier-missing",
897
+ ),
898
+ targetPath: normalizeGraphTargetPath(
899
+ edge.targetPath,
900
+ label,
901
+ filesByPath,
902
+ true,
903
+ ),
904
+ }),
905
+ );
906
+ const dynamicImports = normalizeArray(
907
+ "dynamicImports",
908
+ (edge, label, modulePath, occurrence) => {
909
+ const edgePolicy = normalizeGraphPolicy(edge.edgePolicy, label);
910
+ const targets = normalizeGraphTargets(edge.targets ?? [], label, filesByPath, {
911
+ javascript: true,
912
+ });
913
+ if (
914
+ (edgePolicy === "artifact-targets-admitted") !== (targets.length > 0)
915
+ ) {
916
+ throw artifactGraphTypeError(
917
+ "artifact-graph-dynamic-import-policy-target-mismatch",
918
+ `${label} must have targets exactly when its edgePolicy admits artifact targets.`,
919
+ );
920
+ }
921
+ return Object.freeze({ modulePath, occurrence, edgePolicy, targets });
922
+ },
923
+ );
924
+ const moduleWorkers = normalizeArray(
925
+ "moduleWorkers",
926
+ (edge, label, modulePath, occurrence) => {
927
+ const edgePolicy = normalizeGraphPolicy(edge.edgePolicy, label);
928
+ const targets = normalizeGraphTargets(edge.targets ?? [], label, filesByPath, {
929
+ javascript: true,
930
+ worker: true,
931
+ });
932
+ if (
933
+ (edgePolicy === "artifact-targets-admitted") !== (targets.length > 0)
934
+ ) {
935
+ throw artifactGraphTypeError(
936
+ "artifact-graph-module-worker-policy-target-mismatch",
937
+ `${label} must have targets exactly when its edgePolicy admits artifact targets.`,
938
+ );
939
+ }
940
+ if (targets.some((target) =>
941
+ target.match === "self-module-url" && target.targetPath !== modulePath)) {
942
+ throw artifactGraphTypeError(
943
+ "artifact-graph-module-worker-self-target-mismatch",
944
+ `${label} self-module-url target must equal modulePath.`,
945
+ );
946
+ }
947
+ return Object.freeze({ modulePath, occurrence, edgePolicy, targets });
948
+ },
949
+ );
950
+ const fetches = normalizeArray(
951
+ "fetches",
952
+ (edge, label, modulePath, occurrence) => {
953
+ const edgePolicy = normalizeGraphPolicy(edge.edgePolicy, label);
954
+ const methods = edge.methods ?? ["GET"];
955
+ if (
956
+ !Array.isArray(methods)
957
+ || methods.length !== 1
958
+ || methods[0] !== "GET"
959
+ ) {
960
+ throw artifactGraphTypeError(
961
+ "artifact-graph-fetch-method-not-get",
962
+ `${label} methods must be exactly ["GET"].`,
963
+ );
964
+ }
965
+ const targetPaths = edge.targetPaths ?? [];
966
+ if (!Array.isArray(targetPaths)) {
967
+ throw artifactGraphTypeError(
968
+ "artifact-graph-fetch-targets-not-array",
969
+ `${label} targetPaths must be an array.`,
970
+ );
971
+ }
972
+ const normalizedTargetPaths = [...new Set(targetPaths.map((path) =>
973
+ normalizeGraphTargetPath(path, label, filesByPath)))].sort();
974
+ if (normalizedTargetPaths.length !== targetPaths.length) {
975
+ throw artifactGraphTypeError(
976
+ "artifact-graph-fetch-target-duplicate",
977
+ `${label} targetPaths must be unique.`,
978
+ );
979
+ }
980
+ if (normalizedTargetPaths.some((path) =>
981
+ ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(filesByPath.get(path)?.kind))) {
982
+ throw artifactGraphTypeError(
983
+ "artifact-graph-fetch-javascript-target-rejected",
984
+ `${label} must not expose authenticated JavaScript bytes through a fetch edge.`,
985
+ );
986
+ }
987
+ const negativeUrls = edge.negativeRuntimeRequestUrls ?? [];
988
+ if (!Array.isArray(negativeUrls)) {
989
+ throw artifactGraphTypeError(
990
+ "artifact-graph-fetch-negative-routes-not-array",
991
+ `${label} negativeRuntimeRequestUrls must be an array.`,
992
+ );
993
+ }
994
+ const normalizedNegativeUrls = [...new Set(negativeUrls.map((url, index) =>
995
+ graphRuntimeRequestUrl(url, `${label} negativeRuntimeRequestUrls[${String(index)}]`)))].sort();
996
+ if (
997
+ normalizedNegativeUrls.length !== negativeUrls.length
998
+ || normalizedNegativeUrls.some((url) => !negativeRuntimeRequestUrls.has(url))
999
+ ) {
1000
+ throw artifactGraphTypeError(
1001
+ "artifact-graph-fetch-negative-route-undeclared",
1002
+ `${label} negative runtime request routes must be unique graph-level declarations.`,
1003
+ );
1004
+ }
1005
+ if (
1006
+ edgePolicy === "artifact-targets-admitted"
1007
+ && normalizedTargetPaths.length === 0
1008
+ && normalizedNegativeUrls.length === 0
1009
+ ) {
1010
+ throw artifactGraphTypeError(
1011
+ "artifact-graph-fetch-targets-incomplete",
1012
+ `${label} must admit at least one artifact or authenticated negative route.`,
1013
+ );
1014
+ }
1015
+ if (
1016
+ edgePolicy === "inactive-runtime-branch-rejected"
1017
+ && (normalizedTargetPaths.length > 0 || normalizedNegativeUrls.length > 0)
1018
+ ) {
1019
+ throw artifactGraphTypeError(
1020
+ "artifact-graph-fetch-policy-target-mismatch",
1021
+ `${label} rejected inactive branch must not name fetch targets.`,
1022
+ );
1023
+ }
1024
+ return Object.freeze({
1025
+ modulePath,
1026
+ occurrence,
1027
+ edgePolicy,
1028
+ methods: Object.freeze(["GET"]),
1029
+ targetPaths: Object.freeze(normalizedTargetPaths),
1030
+ negativeRuntimeRequestUrls: Object.freeze(normalizedNegativeUrls),
1031
+ allowMaterializedUrls: edge.allowMaterializedUrls === true,
1032
+ });
1033
+ },
1034
+ );
1035
+ const cacheOpens = normalizeArray(
1036
+ "cacheOpens",
1037
+ (edge, label, modulePath, occurrence) => {
1038
+ const edgePolicy = normalizeGraphPolicy(edge.edgePolicy, label);
1039
+ const cacheName = artifactGraphText(
1040
+ edge.cacheName,
1041
+ `${label} cacheName`,
1042
+ "artifact-graph-cache-name-missing",
1043
+ );
1044
+ const targetPaths = edge.targetPaths ?? [];
1045
+ if (!Array.isArray(targetPaths)) {
1046
+ throw artifactGraphTypeError(
1047
+ "artifact-graph-cache-targets-not-array",
1048
+ `${label} targetPaths must be an array.`,
1049
+ );
1050
+ }
1051
+ const normalizedTargetPaths = [...new Set(targetPaths.map((path) =>
1052
+ normalizeGraphTargetPath(path, label, filesByPath)))].sort();
1053
+ if (normalizedTargetPaths.length !== targetPaths.length) {
1054
+ throw artifactGraphTypeError(
1055
+ "artifact-graph-cache-target-duplicate",
1056
+ `${label} targetPaths must be unique.`,
1057
+ );
1058
+ }
1059
+ if (normalizedTargetPaths.some((path) =>
1060
+ ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(filesByPath.get(path)?.kind))) {
1061
+ throw artifactGraphTypeError(
1062
+ "artifact-graph-cache-javascript-target-rejected",
1063
+ `${label} must not expose authenticated JavaScript bytes through a cache edge.`,
1064
+ );
1065
+ }
1066
+ if (
1067
+ (edgePolicy === "artifact-targets-admitted") !== (normalizedTargetPaths.length > 0)
1068
+ ) {
1069
+ throw artifactGraphTypeError(
1070
+ "artifact-graph-cache-policy-target-mismatch",
1071
+ `${label} must have targets exactly when its edgePolicy admits authenticated cache reads.`,
1072
+ );
1073
+ }
1074
+ return Object.freeze({
1075
+ modulePath,
1076
+ occurrence,
1077
+ edgePolicy,
1078
+ cacheName,
1079
+ targetPaths: Object.freeze(normalizedTargetPaths),
1080
+ });
1081
+ },
1082
+ );
267
1083
  return Object.freeze({
268
- protocol: authority.protocol,
269
- providerId: authority.providerId,
270
- modelId: authority.modelId,
271
- role: authority.role,
272
- repository: authority.repository,
273
- revision: authority.revision,
274
- runtime: authority.runtime,
275
- files: authority.files,
1084
+ staticImports,
1085
+ dynamicImports,
1086
+ moduleWorkers,
1087
+ fetches,
1088
+ cacheOpens,
276
1089
  });
277
1090
  }
278
1091
 
279
- function storageKey(authority) {
280
- const digest = createStreamingSha256();
281
- digest.update(new TextEncoder().encode(JSON.stringify(authorityProjection(authority))));
282
- return digest.digestHex();
1092
+ function normalizeArtifactGraphTransforms(value, filesByPath) {
1093
+ const transforms = value ?? [];
1094
+ if (!Array.isArray(transforms)) {
1095
+ throw artifactGraphTypeError(
1096
+ "artifact-graph-transforms-not-array",
1097
+ "Artifact graph transforms must be an array.",
1098
+ );
1099
+ }
1100
+ const normalized = transforms.map((transform, index) => {
1101
+ if (!transform || typeof transform !== "object" || Array.isArray(transform)) {
1102
+ throw artifactGraphTypeError(
1103
+ "artifact-graph-transform-not-object",
1104
+ `Artifact graph transform ${String(index)} must be an object.`,
1105
+ );
1106
+ }
1107
+ const label = `Artifact graph transform ${String(index)}`;
1108
+ if (!["function-return-this-to-global-this", "typed-array-constructor"].includes(
1109
+ transform.kind,
1110
+ )) {
1111
+ throw artifactGraphTypeError(
1112
+ "artifact-graph-transform-kind-not-admitted",
1113
+ `${label} kind is not admitted.`,
1114
+ );
1115
+ }
1116
+ return Object.freeze({
1117
+ kind: transform.kind,
1118
+ modulePath: normalizeGraphModulePath(transform.modulePath, label, filesByPath),
1119
+ occurrence: normalizeGraphOccurrence(transform.occurrence, label),
1120
+ });
1121
+ });
1122
+ normalized.sort((left, right) => lexicalCompare(canonicalJson(left), canonicalJson(right)));
1123
+ const identities = new Set(normalized.map(canonicalJson));
1124
+ if (identities.size !== normalized.length) {
1125
+ throw artifactGraphTypeError(
1126
+ "artifact-graph-transform-occurrence-duplicate",
1127
+ "Artifact graph transform occurrences must be unique.",
1128
+ );
1129
+ }
1130
+ return Object.freeze(normalized);
283
1131
  }
284
1132
 
285
- function storageNames(authority, files) {
286
- const prefix = `arcane-speech-${storageKey(authority)}`;
287
- return Object.freeze({
288
- key: prefix,
289
- manifest: `${prefix}.complete.json`,
290
- files: Object.freeze(files.map((_, index) =>
291
- `${prefix}.${String(index).padStart(4, "0")}.artifact`)),
1133
+ function normalizeArtifactGraphVoices(value, defaultVoice, filesByPath) {
1134
+ if (!Array.isArray(value) || value.length < 1) {
1135
+ throw artifactGraphTypeError(
1136
+ "artifact-graph-voice-inventory-missing",
1137
+ "A TTS artifact graph requires a nonempty voices array.",
1138
+ );
1139
+ }
1140
+ const voices = value.map((voice, index) => {
1141
+ if (!voice || typeof voice !== "object" || Array.isArray(voice)) {
1142
+ throw artifactGraphTypeError(
1143
+ "artifact-graph-voice-descriptor-not-object",
1144
+ `Artifact graph voice ${String(index)} must be an object.`,
1145
+ );
1146
+ }
1147
+ const id = artifactGraphIdentifier(
1148
+ voice.id,
1149
+ `Artifact graph voice ${String(index)} id`,
1150
+ "artifact-graph-voice-id-missing",
1151
+ "artifact-graph-voice-id-length-exceeded",
1152
+ );
1153
+ const path = normalizeGraphTargetPath(
1154
+ voice.path,
1155
+ `Artifact graph voice ${id}`,
1156
+ filesByPath,
1157
+ );
1158
+ if (filesByPath.get(path).kind !== "voice-style-binary") {
1159
+ throw artifactGraphTypeError(
1160
+ "artifact-graph-voice-file-kind-mismatch",
1161
+ `Artifact graph voice ${id} must name a voice-style-binary file.`,
1162
+ );
1163
+ }
1164
+ return Object.freeze({ id, path });
292
1165
  });
1166
+ voices.sort((left, right) => lexicalCompare(left.id, right.id));
1167
+ const ids = new Set(voices.map(({ id }) => id));
1168
+ const paths = new Set(voices.map(({ path }) => path));
1169
+ if (ids.size !== voices.length || paths.size !== voices.length) {
1170
+ throw artifactGraphTypeError(
1171
+ "artifact-graph-voice-inventory-ambiguous",
1172
+ "Artifact graph voice ids and file paths must be unique.",
1173
+ );
1174
+ }
1175
+ if (!ids.has(defaultVoice)) {
1176
+ throw artifactGraphTypeError(
1177
+ "artifact-graph-default-voice-undeclared",
1178
+ "Artifact graph defaultVoice must name one declared voice.",
1179
+ );
1180
+ }
1181
+ return Object.freeze(voices);
293
1182
  }
294
1183
 
295
- function manifestMatches(manifest, authority, files) {
296
- return manifest?.schema === MANIFEST_SCHEMA
297
- && manifest.complete === true
298
- && JSON.stringify(manifest.authority) === JSON.stringify(authorityProjection(authority))
299
- && Array.isArray(manifest.files)
300
- && manifest.files.length === files.length;
1184
+ function artifactGraphIdentityProjection({
1185
+ providerId,
1186
+ role,
1187
+ model,
1188
+ runtime,
1189
+ files,
1190
+ edges,
1191
+ transforms,
1192
+ }) {
1193
+ return Object.freeze({
1194
+ protocol: BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL,
1195
+ kind: ARTIFACT_GRAPH_KIND,
1196
+ providerId,
1197
+ role,
1198
+ model,
1199
+ runtime,
1200
+ files: Object.freeze(files.map(publicArtifactGraphFile)),
1201
+ edges,
1202
+ transforms,
1203
+ });
301
1204
  }
302
1205
 
303
- async function* byteChunks(body, signal) {
304
- if (body instanceof Uint8Array || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
305
- throwIfAborted(signal);
306
- yield body instanceof Uint8Array
307
- ? body
308
- : new Uint8Array(body.buffer ?? body, body.byteOffset ?? 0, body.byteLength);
309
- return;
1206
+ /**
1207
+ * Creates one closed, caller-selected browser speech artifact graph. Every
1208
+ * executable and data byte is immutable, content-addressed, and reachable only
1209
+ * through an explicit graph edge or exact local runtime request route.
1210
+ */
1211
+ export function createBrowserSpeechArtifactGraph({
1212
+ kind = ARTIFACT_GRAPH_KIND,
1213
+ identitySha256,
1214
+ providerId = null,
1215
+ role,
1216
+ model,
1217
+ runtime,
1218
+ files,
1219
+ edges,
1220
+ transforms,
1221
+ } = {}) {
1222
+ if (kind !== ARTIFACT_GRAPH_KIND) {
1223
+ throw artifactGraphTypeError(
1224
+ "artifact-graph-kind-mismatch",
1225
+ `Browser speech artifact graph kind must equal ${ARTIFACT_GRAPH_KIND}.`,
1226
+ );
310
1227
  }
311
- if (body && typeof body.getReader === "function") {
312
- const reader = body.getReader();
313
- const abort = () => void reader.cancel(signal?.reason).catch(() => undefined);
314
- signal?.addEventListener?.("abort", abort, { once: true });
315
- try {
316
- while (true) {
317
- throwIfAborted(signal);
318
- const { done, value } = await reader.read();
319
- if (done) return;
320
- yield value instanceof Uint8Array ? value : new Uint8Array(value);
1228
+ if (role !== "stt" && role !== "tts") {
1229
+ throw artifactGraphTypeError(
1230
+ "artifact-graph-role-not-stt-or-tts",
1231
+ 'Browser speech artifact graph role must be "stt" or "tts".',
1232
+ );
1233
+ }
1234
+ const normalizedProviderId = providerId === null || providerId === undefined
1235
+ ? null
1236
+ : artifactGraphIdentifier(
1237
+ providerId,
1238
+ "Browser speech artifact graph providerId",
1239
+ "artifact-graph-provider-id-missing",
1240
+ "artifact-graph-provider-id-length-exceeded",
1241
+ );
1242
+ if (!model || typeof model !== "object" || Array.isArray(model)) {
1243
+ throw artifactGraphTypeError(
1244
+ "artifact-graph-model-descriptor-missing",
1245
+ "Browser speech artifact graph model descriptor is required.",
1246
+ );
1247
+ }
1248
+ if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
1249
+ throw artifactGraphTypeError(
1250
+ "artifact-graph-runtime-descriptor-missing",
1251
+ "Browser speech artifact graph runtime descriptor is required.",
1252
+ );
1253
+ }
1254
+ if (!Array.isArray(files) || files.length < 1) {
1255
+ throw artifactGraphTypeError(
1256
+ "artifact-graph-file-inventory-missing",
1257
+ "Browser speech artifact graph requires a nonempty files array.",
1258
+ );
1259
+ }
1260
+ const normalizedFiles = files.map(normalizeArtifactGraphFile);
1261
+ normalizedFiles.sort((left, right) => lexicalCompare(left.path, right.path));
1262
+ const filesByPath = new Map();
1263
+ const lowercasePaths = new Set();
1264
+ const sourceUrls = new Set();
1265
+ const requestUrls = new Set();
1266
+ for (const file of normalizedFiles) {
1267
+ const lowercasePath = file.path.toLowerCase();
1268
+ if (
1269
+ filesByPath.has(file.path)
1270
+ || lowercasePaths.has(lowercasePath)
1271
+ || sourceUrls.has(file.sourceUrl)
1272
+ ) {
1273
+ throw artifactGraphTypeError(
1274
+ "artifact-graph-file-identity-ambiguous",
1275
+ "Artifact graph file paths (including case-folded paths) and source URLs must be unique.",
1276
+ );
1277
+ }
1278
+ filesByPath.set(file.path, file);
1279
+ lowercasePaths.add(lowercasePath);
1280
+ sourceUrls.add(file.sourceUrl);
1281
+ for (const url of file.runtimeRequestUrls) {
1282
+ if (requestUrls.has(url)) {
1283
+ throw artifactGraphTypeError(
1284
+ "artifact-graph-runtime-request-route-ambiguous",
1285
+ `Artifact graph runtime request URL ${url} maps to more than one file.`,
1286
+ );
321
1287
  }
322
- } finally {
323
- signal?.removeEventListener?.("abort", abort);
324
- if (signal?.aborted) await reader.cancel(signal.reason).catch(() => undefined);
325
- reader.releaseLock?.();
1288
+ requestUrls.add(url);
326
1289
  }
327
1290
  }
328
- throw speechError(
329
- "ARCANE_AI_ARTIFACT_SOURCE_INVALID",
330
- "A browser speech artifact did not provide readable bytes.",
1291
+ for (const url of requestUrls) {
1292
+ if (sourceUrls.has(url)) {
1293
+ throw artifactGraphTypeError(
1294
+ "artifact-graph-runtime-request-route-ambiguous",
1295
+ `Artifact graph runtime request URL ${url} overlaps an immutable source URL.`,
1296
+ );
1297
+ }
1298
+ }
1299
+
1300
+ const entrypoint = canonicalArtifactPath(
1301
+ runtime.entrypoint ?? runtime.entry,
1302
+ "Browser speech artifact graph runtime entrypoint",
1303
+ "artifact-graph-entrypoint-path-missing",
1304
+ "artifact-graph-entrypoint-path-noncanonical",
331
1305
  );
332
- }
1306
+ const entrypointFile = filesByPath.get(entrypoint);
1307
+ if (entrypointFile?.kind !== "runtime-entrypoint-javascript") {
1308
+ throw artifactGraphTypeError(
1309
+ "artifact-graph-entrypoint-file-kind-mismatch",
1310
+ "Browser speech artifact graph entrypoint must name one runtime-entrypoint-javascript file.",
1311
+ );
1312
+ }
1313
+ if (normalizedFiles.filter((file) =>
1314
+ file.kind === "runtime-entrypoint-javascript").length !== 1) {
1315
+ throw artifactGraphTypeError(
1316
+ "artifact-graph-entrypoint-count-mismatch",
1317
+ "Browser speech artifact graph requires exactly one runtime-entrypoint-javascript file.",
1318
+ );
1319
+ }
1320
+ const runtimeAdapter = artifactGraphText(
1321
+ runtime.adapter,
1322
+ "Browser speech artifact graph runtime adapter",
1323
+ "artifact-graph-runtime-adapter-missing",
1324
+ );
1325
+ const expectedAdapter = role === "stt" ? "transformers-whisper" : "kokoro-js";
1326
+ if (runtimeAdapter !== expectedAdapter) {
1327
+ throw artifactGraphTypeError(
1328
+ "artifact-graph-runtime-adapter-role-mismatch",
1329
+ `Browser ${role} artifact graph runtime adapter must equal ${expectedAdapter}.`,
1330
+ );
1331
+ }
1332
+ const runtimeVersion = artifactGraphIdentifier(
1333
+ runtime.version,
1334
+ "Browser speech artifact graph runtime version",
1335
+ "artifact-graph-runtime-version-missing",
1336
+ "artifact-graph-runtime-version-length-exceeded",
1337
+ );
1338
+ const runtimeRevision = artifactGraphIdentifier(
1339
+ runtime.revision,
1340
+ "Browser speech artifact graph runtime revision",
1341
+ "artifact-graph-runtime-revision-missing",
1342
+ "artifact-graph-runtime-revision-length-exceeded",
1343
+ );
1344
+ if (entrypointFile.revision !== runtimeRevision) {
1345
+ throw artifactGraphTypeError(
1346
+ "artifact-graph-entrypoint-revision-mismatch",
1347
+ "Browser speech artifact graph entrypoint revision must equal the runtime revision.",
1348
+ );
1349
+ }
1350
+ const onnxWasm = runtime.onnxWasm;
1351
+ if (!onnxWasm || typeof onnxWasm !== "object" || Array.isArray(onnxWasm)) {
1352
+ throw artifactGraphTypeError(
1353
+ "artifact-graph-onnx-wasm-descriptor-missing",
1354
+ "Browser speech artifact graph runtime onnxWasm descriptor is required.",
1355
+ );
1356
+ }
1357
+ const namespace = artifactGraphText(
1358
+ onnxWasm.namespace,
1359
+ "Browser speech artifact graph ONNX namespace",
1360
+ "artifact-graph-onnx-wasm-namespace-missing",
1361
+ );
1362
+ const expectedNamespace = role === "stt"
1363
+ ? "transformers-env-backends-onnx-wasm"
1364
+ : "kokoro-env-wasm-paths";
1365
+ if (!ARTIFACT_GRAPH_ONNX_NAMESPACES.has(namespace) || namespace !== expectedNamespace) {
1366
+ throw artifactGraphTypeError(
1367
+ "artifact-graph-onnx-wasm-namespace-role-mismatch",
1368
+ `Browser ${role} artifact graph ONNX namespace must equal ${expectedNamespace}.`,
1369
+ );
1370
+ }
1371
+ const mjsPath = normalizeGraphTargetPath(
1372
+ onnxWasm.mjsPath,
1373
+ "Browser speech artifact graph ONNX module",
1374
+ filesByPath,
1375
+ true,
1376
+ );
1377
+ const wasmPath = normalizeGraphTargetPath(
1378
+ onnxWasm.wasmPath,
1379
+ "Browser speech artifact graph ONNX WebAssembly",
1380
+ filesByPath,
1381
+ );
1382
+ if (
1383
+ filesByPath.get(mjsPath).kind !== "runtime-auxiliary-javascript"
1384
+ || filesByPath.get(wasmPath).kind !== "runtime-wasm-binary"
1385
+ ) {
1386
+ throw artifactGraphTypeError(
1387
+ "artifact-graph-onnx-wasm-file-kind-mismatch",
1388
+ "Browser speech artifact graph ONNX paths must name auxiliary JavaScript and WebAssembly runtime files.",
1389
+ );
1390
+ }
1391
+ const numThreads = onnxWasm.numThreads === undefined
1392
+ ? null
1393
+ : graphPositiveInteger(
1394
+ onnxWasm.numThreads,
1395
+ "Browser speech artifact graph ONNX numThreads",
1396
+ "artifact-graph-onnx-wasm-num-threads-positive-safe-integer-required",
1397
+ );
1398
+ if (role === "tts" && numThreads !== null) {
1399
+ throw artifactGraphTypeError(
1400
+ "kokoro-env-num-threads-field-not-exposed",
1401
+ "Kokoro does not expose a verified numThreads configuration field.",
1402
+ );
1403
+ }
333
1404
 
334
- function providerProgress(phase, completed, total, heartbeat = false) {
335
- return Object.freeze({ phase, completed, total, unit: "bytes", heartbeat });
336
- }
1405
+ const negativeRoutes = runtime.negativeRuntimeRequestUrls ?? [];
1406
+ if (!Array.isArray(negativeRoutes)) {
1407
+ throw artifactGraphTypeError(
1408
+ "artifact-graph-negative-runtime-routes-not-array",
1409
+ "Browser speech artifact graph runtime negativeRuntimeRequestUrls must be an array.",
1410
+ );
1411
+ }
1412
+ const normalizedNegativeRoutes = [...new Set(negativeRoutes.map((url, index) =>
1413
+ graphRuntimeRequestUrl(
1414
+ url,
1415
+ `Browser speech artifact graph negativeRuntimeRequestUrls[${String(index)}]`,
1416
+ )))].sort();
1417
+ if (normalizedNegativeRoutes.length !== negativeRoutes.length) {
1418
+ throw artifactGraphTypeError(
1419
+ "artifact-graph-negative-runtime-route-duplicate",
1420
+ "Browser speech artifact graph negative runtime request routes must be unique.",
1421
+ );
1422
+ }
1423
+ for (const url of normalizedNegativeRoutes) {
1424
+ if (
1425
+ requestUrls.has(url)
1426
+ || sourceUrls.has(url)
1427
+ ) {
1428
+ throw artifactGraphTypeError(
1429
+ "artifact-graph-negative-runtime-route-ambiguous",
1430
+ `Artifact graph negative runtime request URL ${url} must not overlap a positive graph route.`,
1431
+ );
1432
+ }
1433
+ }
337
1434
 
338
- // Runtime entry bytes use one deliberately closed capability grammar. The only
339
- // module reference is import.meta and the only artifact transport is fetch(),
340
- // which the Worker replaces with its admitted object-URL map before import.
341
- // Executable strings, child execution contexts, script loaders, and alternate
342
- // network transports are outside the grammar. Literal escape sequences are
343
- // decoded before the same capability tokens are evaluated.
344
- const CLOSED_MODULE_OUT_OF_GRAMMAR_IDENTIFIERS = new Set([
345
- "AsyncFunction",
346
- "AsyncGeneratorFunction",
347
- "EventSource",
348
- "Function",
349
- "GeneratorFunction",
350
- "RTCPeerConnection",
351
- "SharedWorker",
352
- "WebSocket",
353
- "WebTransport",
354
- "Worker",
355
- "XMLHttpRequest",
356
- "eval",
357
- "importScripts",
358
- ]);
359
- const CLOSED_MODULE_OUT_OF_GRAMMAR_LITERAL =
360
- /(?:^|[^A-Za-z0-9_$])(?:AsyncFunction|AsyncGeneratorFunction|EventSource|Function|GeneratorFunction|RTCPeerConnection|SharedWorker|WebSocket|WebTransport|Worker|XMLHttpRequest|constructor|eval|importScripts)(?:$|[^A-Za-z0-9_$])|(?:^|[^A-Za-z0-9_$])import\s*\(/u;
1435
+ const modelId = artifactGraphIdentifier(
1436
+ model.id,
1437
+ "Browser speech artifact graph model id",
1438
+ "artifact-graph-model-id-missing",
1439
+ "artifact-graph-model-id-length-exceeded",
1440
+ );
1441
+ const repository = artifactGraphIdentifier(
1442
+ model.repository,
1443
+ "Browser speech artifact graph model repository",
1444
+ "artifact-graph-model-repository-missing",
1445
+ "artifact-graph-model-repository-length-exceeded",
1446
+ );
1447
+ const modelRevision = artifactGraphIdentifier(
1448
+ model.revision,
1449
+ "Browser speech artifact graph model revision",
1450
+ "artifact-graph-model-revision-missing",
1451
+ "artifact-graph-model-revision-length-exceeded",
1452
+ );
1453
+ const dtype = artifactGraphIdentifier(
1454
+ model.dtype,
1455
+ "Browser speech artifact graph model dtype",
1456
+ "artifact-graph-model-dtype-missing",
1457
+ "artifact-graph-model-dtype-length-exceeded",
1458
+ );
1459
+ const inputSampleRate = graphOptionalSampleRate(
1460
+ model.inputSampleRate,
1461
+ "Browser speech artifact graph model inputSampleRate",
1462
+ role === "stt",
1463
+ );
1464
+ const outputSampleRate = graphOptionalSampleRate(
1465
+ model.outputSampleRate,
1466
+ "Browser speech artifact graph model outputSampleRate",
1467
+ role === "tts",
1468
+ );
1469
+ const defaultVoice = role === "tts"
1470
+ ? artifactGraphIdentifier(
1471
+ model.defaultVoice,
1472
+ "Browser speech artifact graph defaultVoice",
1473
+ "artifact-graph-default-voice-missing",
1474
+ "artifact-graph-default-voice-length-exceeded",
1475
+ )
1476
+ : null;
1477
+ if (
1478
+ role === "stt"
1479
+ && (model.defaultVoice !== undefined || model.voices !== undefined)
1480
+ ) {
1481
+ throw artifactGraphTypeError(
1482
+ "artifact-graph-stt-voice-authority-declared",
1483
+ "An STT artifact graph must not declare TTS voice authority.",
1484
+ );
1485
+ }
1486
+ const voices = role === "tts"
1487
+ ? normalizeArtifactGraphVoices(model.voices, defaultVoice, filesByPath)
1488
+ : Object.freeze([]);
1489
+
1490
+ const negativeRuntimeRequestUrlSet = new Set(normalizedNegativeRoutes);
1491
+ const normalizedEdges = normalizeArtifactGraphEdges(
1492
+ edges,
1493
+ filesByPath,
1494
+ negativeRuntimeRequestUrlSet,
1495
+ );
1496
+ const normalizedTransforms = normalizeArtifactGraphTransforms(transforms, filesByPath);
1497
+ const referencedPaths = new Set([entrypoint, mjsPath, wasmPath]);
1498
+ const referencedNegativeRoutes = new Set();
1499
+ for (const edge of normalizedEdges.staticImports) referencedPaths.add(edge.targetPath);
1500
+ for (const edge of normalizedEdges.dynamicImports) {
1501
+ for (const target of edge.targets) referencedPaths.add(target.targetPath);
1502
+ }
1503
+ for (const edge of normalizedEdges.moduleWorkers) {
1504
+ for (const target of edge.targets) referencedPaths.add(target.targetPath);
1505
+ }
1506
+ for (const edge of normalizedEdges.fetches) {
1507
+ for (const path of edge.targetPaths) referencedPaths.add(path);
1508
+ for (const url of edge.negativeRuntimeRequestUrls) referencedNegativeRoutes.add(url);
1509
+ }
1510
+ for (const edge of normalizedEdges.cacheOpens) {
1511
+ for (const path of edge.targetPaths) referencedPaths.add(path);
1512
+ }
1513
+ for (const voice of voices) referencedPaths.add(voice.path);
1514
+ for (const file of normalizedFiles) {
1515
+ if (
1516
+ file.kind !== "runtime-entrypoint-javascript"
1517
+ && !referencedPaths.has(file.path)
1518
+ ) {
1519
+ throw artifactGraphTypeError(
1520
+ "artifact-graph-file-unreachable",
1521
+ `Artifact graph file ${file.path} is not reachable from a declared runtime, model, or voice capability.`,
1522
+ );
1523
+ }
1524
+ if (
1525
+ file.runtimeRequestUrls.length > 0
1526
+ && !referencedPaths.has(file.path)
1527
+ ) {
1528
+ throw artifactGraphTypeError(
1529
+ "artifact-graph-runtime-request-route-unreachable",
1530
+ `Artifact graph runtime request routes for ${file.path} have no declared edge.`,
1531
+ );
1532
+ }
1533
+ }
1534
+ for (const url of normalizedNegativeRoutes) {
1535
+ if (!referencedNegativeRoutes.has(url)) {
1536
+ throw artifactGraphTypeError(
1537
+ "artifact-graph-negative-runtime-route-unreachable",
1538
+ `Artifact graph negative runtime request URL ${url} has no declared fetch edge.`,
1539
+ );
1540
+ }
1541
+ }
1542
+
1543
+ const runtimeFiles = Object.freeze(normalizedFiles.filter((file) =>
1544
+ file.kind.startsWith("runtime-")));
1545
+ const modelFiles = Object.freeze(normalizedFiles.filter((file) =>
1546
+ !file.kind.startsWith("runtime-")));
1547
+ if (modelFiles.length < 1) {
1548
+ throw artifactGraphTypeError(
1549
+ "artifact-graph-model-file-inventory-missing",
1550
+ "Browser speech artifact graph requires at least one model or voice file.",
1551
+ );
1552
+ }
1553
+ if (role === "stt" && modelFiles.some((file) => file.kind === "voice-style-binary")) {
1554
+ throw artifactGraphTypeError(
1555
+ "artifact-graph-stt-voice-file-declared",
1556
+ "An STT artifact graph must not contain voice-style-binary files.",
1557
+ );
1558
+ }
1559
+ if (role === "tts") {
1560
+ const declaredVoicePaths = new Set(voices.map(({ path }) => path));
1561
+ if (modelFiles.some((file) =>
1562
+ file.kind === "voice-style-binary" && !declaredVoicePaths.has(file.path))) {
1563
+ throw artifactGraphTypeError(
1564
+ "artifact-graph-voice-file-undeclared",
1565
+ "Every voice-style-binary file must belong to the caller-declared voice inventory.",
1566
+ );
1567
+ }
1568
+ }
1569
+ if (modelFiles.some((file) => file.revision !== modelRevision)) {
1570
+ throw artifactGraphTypeError(
1571
+ "artifact-graph-model-file-revision-mismatch",
1572
+ "Every artifact graph model and voice file revision must equal the model revision.",
1573
+ );
1574
+ }
1575
+ const publicRuntimeFiles = Object.freeze(runtimeFiles.map(publicArtifactGraphFile));
1576
+ const publicModelFiles = Object.freeze(modelFiles.map(publicArtifactGraphFile));
1577
+ const normalizedRuntime = Object.freeze({
1578
+ adapter: runtimeAdapter,
1579
+ version: runtimeVersion,
1580
+ revision: runtimeRevision,
1581
+ entry: entrypoint,
1582
+ moduleGraph: ARTIFACT_GRAPH_MODULE_KIND,
1583
+ files: publicRuntimeFiles,
1584
+ onnxWasm: Object.freeze({
1585
+ namespace,
1586
+ mjsPath,
1587
+ wasmPath,
1588
+ ...(numThreads === null ? {} : { numThreads }),
1589
+ }),
1590
+ negativeRuntimeRequestUrls: Object.freeze(normalizedNegativeRoutes),
1591
+ });
1592
+ const normalizedModel = Object.freeze({
1593
+ id: modelId,
1594
+ repository,
1595
+ revision: modelRevision,
1596
+ dtype,
1597
+ ...(inputSampleRate === null ? {} : { inputSampleRate }),
1598
+ ...(outputSampleRate === null ? {} : { outputSampleRate }),
1599
+ ...(role === "tts" ? { defaultVoice, voices } : {}),
1600
+ files: publicModelFiles,
1601
+ });
1602
+ const projection = artifactGraphIdentityProjection({
1603
+ providerId: normalizedProviderId,
1604
+ role,
1605
+ model: normalizedModel,
1606
+ runtime: normalizedRuntime,
1607
+ files: normalizedFiles,
1608
+ edges: normalizedEdges,
1609
+ transforms: normalizedTransforms,
1610
+ });
1611
+ const computedIdentitySha256 = sha256Text(canonicalJson(projection));
1612
+ if (
1613
+ identitySha256 !== undefined
1614
+ && graphSha256(
1615
+ identitySha256,
1616
+ "Browser speech artifact graph identitySha256",
1617
+ "artifact-graph-identity-sha256-text-required",
1618
+ "artifact-graph-identity-sha256-format-mismatch",
1619
+ )
1620
+ !== computedIdentitySha256
1621
+ ) {
1622
+ throw artifactGraphTypeError(
1623
+ "artifact-graph-identity-sha256-mismatch",
1624
+ "Browser speech artifact graph identitySha256 does not match its canonical descriptor.",
1625
+ );
1626
+ }
1627
+ const graph = Object.freeze({
1628
+ ...projection,
1629
+ identitySha256: computedIdentitySha256,
1630
+ artifactGraphStatus: "artifact-graph-descriptor-verified",
1631
+ });
1632
+ ARTIFACT_GRAPHS.add(graph);
1633
+ ARTIFACT_GRAPH_METADATA.set(graph, Object.freeze({
1634
+ graph,
1635
+ files: Object.freeze(normalizedFiles),
1636
+ filesByPath,
1637
+ runtimeFiles,
1638
+ modelFiles,
1639
+ model: normalizedModel,
1640
+ runtime: normalizedRuntime,
1641
+ edges: normalizedEdges,
1642
+ transforms: normalizedTransforms,
1643
+ }));
1644
+ return graph;
1645
+ }
1646
+
1647
+ /**
1648
+ * Admits one caller-owned browser speech model/runtime description. The SDK
1649
+ * supplies no model URL or profile; applications choose every immutable byte.
1650
+ */
1651
+ export function createBrowserSpeechAuthority({
1652
+ providerId,
1653
+ role,
1654
+ model,
1655
+ runtime,
1656
+ security,
1657
+ } = {}) {
1658
+ const normalizedProviderId = identifier(providerId, "Browser speech providerId");
1659
+ if (role !== "stt" && role !== "tts") {
1660
+ throw new TypeError('Browser speech role must be "stt" or "tts".');
1661
+ }
1662
+ if (!model || typeof model !== "object" || Array.isArray(model)) {
1663
+ throw new TypeError("Browser speech model descriptor is required.");
1664
+ }
1665
+ if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
1666
+ throw new TypeError("Browser speech runtime descriptor is required.");
1667
+ }
1668
+ const normalizedSecurity = normalizeModelSecurity(
1669
+ security,
1670
+ "Browser speech provider security",
1671
+ );
1672
+ const modelId = identifier(model.id, "Browser speech model id");
1673
+ const modelRevision = identifier(model.revision, "Browser speech model revision");
1674
+ const repository = identifier(model.repository, "Browser speech model repository");
1675
+ const modelFiles = uniqueFiles(
1676
+ model.files ?? [],
1677
+ "Browser speech model",
1678
+ "model",
1679
+ modelRevision,
1680
+ { allowEmpty: normalizedSecurity.secure !== true },
1681
+ );
1682
+ const runtimeAdapter = requiredText(runtime.adapter, "Browser speech runtime adapter");
1683
+ const expectedAdapter = role === "stt"
1684
+ ? "transformers-whisper"
1685
+ : "kokoro-js";
1686
+ if (runtimeAdapter !== expectedAdapter) {
1687
+ throw new TypeError(`Browser ${role} runtime adapter must equal ${expectedAdapter}.`);
1688
+ }
1689
+ const runtimeVersion = identifier(runtime.version, "Browser speech runtime version");
1690
+ const runtimeRevision = identifier(runtime.revision, "Browser speech runtime revision");
1691
+ const runtimeFiles = uniqueFiles(
1692
+ runtime.files,
1693
+ "Browser speech runtime",
1694
+ "runtime",
1695
+ runtimeRevision,
1696
+ );
1697
+ const wasmPaths = runtime.wasmPaths === undefined
1698
+ ? null
1699
+ : immutableUrl(runtime.wasmPaths, "Browser speech runtime wasmPaths");
1700
+ if (normalizedSecurity.secure === true && wasmPaths !== null) {
1701
+ throw new TypeError(
1702
+ "Secure browser speech must materialize its ONNX runtime files instead of using remote wasmPaths.",
1703
+ );
1704
+ }
1705
+ const entry = requiredText(runtime.entry, "Browser speech runtime entry");
1706
+ const entryFile = runtimeFiles.find((file) => file.path === entry);
1707
+ if (!entryFile) {
1708
+ throw new TypeError("Browser speech runtime entry must name one runtime file path.");
1709
+ }
1710
+ if (!/\.(?:m?js)$/iu.test(entryFile.path) || entryFile.mediaType !== "text/javascript") {
1711
+ throw new TypeError("Browser speech runtime entry must be a JavaScript module.");
1712
+ }
1713
+ const normalizedModel = Object.freeze({
1714
+ id: modelId,
1715
+ repository,
1716
+ revision: modelRevision,
1717
+ defaultVoice: role === "tts"
1718
+ ? identifier(model.defaultVoice, "Browser Kokoro defaultVoice")
1719
+ : null,
1720
+ files: modelFiles,
1721
+ });
1722
+ const normalizedRuntime = Object.freeze({
1723
+ adapter: runtimeAdapter,
1724
+ version: runtimeVersion,
1725
+ revision: runtimeRevision,
1726
+ entry,
1727
+ ...(wasmPaths === null ? {} : { wasmPaths }),
1728
+ files: runtimeFiles,
1729
+ });
1730
+ const files = Object.freeze([...runtimeFiles, ...modelFiles]);
1731
+ const allPaths = new Set();
1732
+ const allUrls = new Set();
1733
+ for (const file of files) {
1734
+ if (allPaths.has(file.path) || allUrls.has(file.url)) {
1735
+ throw new TypeError("Browser speech runtime and model file identities must not overlap.");
1736
+ }
1737
+ allPaths.add(file.path);
1738
+ allUrls.add(file.url);
1739
+ }
1740
+ const authority = Object.freeze({
1741
+ protocol: MODEL_AUTHORITY_PROTOCOL,
1742
+ providerId: normalizedProviderId,
1743
+ modelId,
1744
+ admitted: true,
1745
+ role,
1746
+ repository,
1747
+ revision: modelRevision,
1748
+ defaultVoice: normalizedModel.defaultVoice,
1749
+ runtime: Object.freeze({
1750
+ adapter: normalizedRuntime.adapter,
1751
+ version: normalizedRuntime.version,
1752
+ revision: normalizedRuntime.revision,
1753
+ entry: normalizedRuntime.entry,
1754
+ ...(wasmPaths === null ? {} : { wasmPaths }),
1755
+ files: Object.freeze(runtimeFiles.map(publicFile)),
1756
+ }),
1757
+ files: Object.freeze(modelFiles.map(publicFile)),
1758
+ security: normalizedSecurity,
1759
+ });
1760
+ AUTHORITIES.add(authority);
1761
+ AUTHORITY_METADATA.set(authority, Object.freeze({
1762
+ model: normalizedModel,
1763
+ runtime: normalizedRuntime,
1764
+ files,
1765
+ }));
1766
+ return authority;
1767
+ }
1768
+
1769
+ function authorityProjection(authority) {
1770
+ return Object.freeze({
1771
+ protocol: authority.protocol,
1772
+ providerId: authority.providerId,
1773
+ modelId: authority.modelId,
1774
+ role: authority.role,
1775
+ repository: authority.repository,
1776
+ revision: authority.revision,
1777
+ runtime: authority.runtime,
1778
+ files: authority.files,
1779
+ });
1780
+ }
1781
+
1782
+ function storedArtifactProjection(authority) {
1783
+ if (ARTIFACT_GRAPHS.has(authority)) {
1784
+ return Object.freeze({
1785
+ protocol: authority.protocol,
1786
+ kind: authority.kind,
1787
+ identitySha256: authority.identitySha256,
1788
+ providerId: authority.providerId,
1789
+ role: authority.role,
1790
+ model: authority.model,
1791
+ runtime: authority.runtime,
1792
+ files: authority.files,
1793
+ edges: authority.edges,
1794
+ transforms: authority.transforms,
1795
+ });
1796
+ }
1797
+ return authorityProjection(authority);
1798
+ }
1799
+
1800
+ function artifactMetadata(authority) {
1801
+ return ARTIFACT_GRAPH_METADATA.get(authority)
1802
+ ?? AUTHORITY_METADATA.get(authority)
1803
+ ?? null;
1804
+ }
1805
+
1806
+ function isSpeechArtifactAuthority(authority) {
1807
+ return AUTHORITIES.has(authority) || ARTIFACT_GRAPHS.has(authority);
1808
+ }
1809
+
1810
+ function storageKey(authority) {
1811
+ if (ARTIFACT_GRAPHS.has(authority)) return authority.identitySha256;
1812
+ const digest = createStreamingSha256();
1813
+ digest.update(new TextEncoder().encode(JSON.stringify(authorityProjection(authority))));
1814
+ return digest.digestHex();
1815
+ }
1816
+
1817
+ function storageNames(authority, files) {
1818
+ const prefix = `arcane-speech-${storageKey(authority)}`;
1819
+ return Object.freeze({
1820
+ key: prefix,
1821
+ manifest: `${prefix}.complete.json`,
1822
+ files: Object.freeze(files.map((_, index) =>
1823
+ `${prefix}.${String(index).padStart(4, "0")}.artifact`)),
1824
+ });
1825
+ }
1826
+
1827
+ function manifestMatches(manifest, authority, files) {
1828
+ const expectedSchema = ARTIFACT_GRAPHS.has(authority)
1829
+ ? ARTIFACT_GRAPH_MANIFEST_SCHEMA
1830
+ : MANIFEST_SCHEMA;
1831
+ return manifest?.schema === expectedSchema
1832
+ && manifest.complete === true
1833
+ && JSON.stringify(manifest.authority) === JSON.stringify(storedArtifactProjection(authority))
1834
+ && Array.isArray(manifest.files)
1835
+ && manifest.files.length === files.length;
1836
+ }
1837
+
1838
+ async function* byteChunks(body, signal) {
1839
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
1840
+ throwIfAborted(signal);
1841
+ yield body instanceof Uint8Array
1842
+ ? body
1843
+ : new Uint8Array(body.buffer ?? body, body.byteOffset ?? 0, body.byteLength);
1844
+ return;
1845
+ }
1846
+ if (body && typeof body.getReader === "function") {
1847
+ const reader = body.getReader();
1848
+ const abort = () => void reader.cancel(signal?.reason).catch(() => undefined);
1849
+ signal?.addEventListener?.("abort", abort, { once: true });
1850
+ try {
1851
+ while (true) {
1852
+ throwIfAborted(signal);
1853
+ const { done, value } = await reader.read();
1854
+ if (done) return;
1855
+ yield value instanceof Uint8Array ? value : new Uint8Array(value);
1856
+ }
1857
+ } finally {
1858
+ signal?.removeEventListener?.("abort", abort);
1859
+ if (signal?.aborted) await reader.cancel(signal.reason).catch(() => undefined);
1860
+ reader.releaseLock?.();
1861
+ }
1862
+ }
1863
+ throw speechError(
1864
+ "ARCANE_AI_ARTIFACT_SOURCE_INVALID",
1865
+ "A browser speech artifact did not provide readable bytes.",
1866
+ );
1867
+ }
1868
+
1869
+ function providerProgress(phase, completed, total, heartbeat = false) {
1870
+ return Object.freeze({ phase, completed, total, unit: "bytes", heartbeat });
1871
+ }
1872
+
1873
+ // Runtime entry bytes use one deliberately closed capability grammar. The only
1874
+ // module reference is import.meta and the only artifact transport is fetch(),
1875
+ // which the Worker replaces with its admitted object-URL map before import.
1876
+ // Executable strings, child execution contexts, script loaders, and alternate
1877
+ // network transports are outside the grammar. Literal escape sequences are
1878
+ // decoded before the same capability tokens are evaluated.
1879
+ const CLOSED_MODULE_OUT_OF_GRAMMAR_IDENTIFIERS = new Set([
1880
+ "AsyncFunction",
1881
+ "AsyncGeneratorFunction",
1882
+ "EventSource",
1883
+ "Function",
1884
+ "GeneratorFunction",
1885
+ "RTCPeerConnection",
1886
+ "SharedWorker",
1887
+ "WebSocket",
1888
+ "WebTransport",
1889
+ "Worker",
1890
+ "XMLHttpRequest",
1891
+ "eval",
1892
+ "importScripts",
1893
+ ]);
1894
+ const CLOSED_MODULE_OUT_OF_GRAMMAR_LITERAL =
1895
+ /(?:^|[^A-Za-z0-9_$])(?:AsyncFunction|AsyncGeneratorFunction|EventSource|Function|GeneratorFunction|RTCPeerConnection|SharedWorker|WebSocket|WebTransport|Worker|XMLHttpRequest|constructor|eval|importScripts)(?:$|[^A-Za-z0-9_$])|(?:^|[^A-Za-z0-9_$])import\s*\(/u;
1896
+
1897
+ function assertSelfContainedModuleSource(source, label) {
1898
+ let index = 0;
1899
+ let nextTemplateId = 1;
1900
+ const templateStack = [];
1901
+ const literalFragments = [];
1902
+
1903
+ function fail() {
1904
+ throw speechError(
1905
+ "ARCANE_AI_RUNTIME_MODULE_GRAPH_UNDECLARED",
1906
+ `${label} must be one self-contained JavaScript module without imports or re-exports.`,
1907
+ );
1908
+ }
1909
+
1910
+ function identifierStart(character) {
1911
+ return /[A-Za-z_$]/u.test(character ?? "");
1912
+ }
1913
+
1914
+ function identifierPart(character) {
1915
+ return /[A-Za-z0-9_$]/u.test(character ?? "");
1916
+ }
1917
+
1918
+ function assertLiteral(value) {
1919
+ if (CLOSED_MODULE_OUT_OF_GRAMMAR_LITERAL.test(value)) fail();
1920
+ }
1921
+
1922
+ function assertComputedLiteral(value) {
1923
+ if (value.includes("constructor") || /import\s*\(/u.test(value)) fail();
1924
+ for (const identifier of CLOSED_MODULE_OUT_OF_GRAMMAR_IDENTIFIERS) {
1925
+ if (value.includes(identifier)) fail();
1926
+ }
1927
+ }
1928
+
1929
+ function recordLiteral(start, end, value) {
1930
+ assertLiteral(value);
1931
+ literalFragments.push(Object.freeze({
1932
+ start,
1933
+ end,
1934
+ value,
1935
+ templateIds: Object.freeze([...templateStack]),
1936
+ }));
1937
+ }
1938
+
1939
+ function stripJoinerTrivia(value) {
1940
+ return value
1941
+ .replace(/\/\*[\s\S]*?\*\//gu, "")
1942
+ .replace(/\/\/[^\r\n]*(?:\r?\n|$)/gu, "")
1943
+ .replace(/\s+/gu, "");
1944
+ }
1945
+
1946
+ function sharesTemplate(left, right) {
1947
+ return left.templateIds.some((id) => right.templateIds.includes(id));
1948
+ }
1949
+
1950
+ function staticallyJoins(left, right) {
1951
+ const separator = stripJoinerTrivia(source.slice(left.end, right.start));
1952
+ const usesConcat = separator.includes(".concat");
1953
+ const withoutConcat = separator.replace(/\.concat/gu, "");
1954
+ const usesPlus = withoutConcat.includes("+");
1955
+ const usesTemplate = sharesTemplate(left, right)
1956
+ && (withoutConcat.includes("${") || withoutConcat.includes("}"));
1957
+ if (!usesConcat && !usesPlus && !usesTemplate) return false;
1958
+ return (usesTemplate ? /^[+()${}]*$/u : /^[+()]*$/u).test(withoutConcat);
1959
+ }
1960
+
1961
+ function assertStaticLiteralChains() {
1962
+ let chain = [];
1963
+ function flushChain() {
1964
+ if (chain.length > 1) {
1965
+ assertComputedLiteral(chain.map((fragment) => fragment.value).join(""));
1966
+ }
1967
+ chain = [];
1968
+ }
1969
+ for (const fragment of literalFragments) {
1970
+ const previous = chain[chain.length - 1];
1971
+ if (previous && staticallyJoins(previous, fragment)) {
1972
+ chain.push(fragment);
1973
+ continue;
1974
+ }
1975
+ flushChain();
1976
+ chain.push(fragment);
1977
+ }
1978
+ flushChain();
1979
+ }
1980
+
1981
+ function readEscape() {
1982
+ if (index >= source.length) fail();
1983
+ const character = source[index];
1984
+ index += 1;
1985
+ if (character === "x") {
1986
+ const hex = source.slice(index, index + 2);
1987
+ if (!/^[a-f0-9]{2}$/iu.test(hex)) fail();
1988
+ index += 2;
1989
+ return String.fromCodePoint(Number.parseInt(hex, 16));
1990
+ }
1991
+ if (character === "u") {
1992
+ if (source[index] === "{") {
1993
+ const end = source.indexOf("}", index + 1);
1994
+ if (end < 0) fail();
1995
+ const hex = source.slice(index + 1, end);
1996
+ if (!/^[a-f0-9]{1,6}$/iu.test(hex)) fail();
1997
+ const codePoint = Number.parseInt(hex, 16);
1998
+ if (codePoint > 0x10ffff) fail();
1999
+ index = end + 1;
2000
+ return String.fromCodePoint(codePoint);
2001
+ }
2002
+ const hex = source.slice(index, index + 4);
2003
+ if (!/^[a-f0-9]{4}$/iu.test(hex)) fail();
2004
+ index += 4;
2005
+ return String.fromCodePoint(Number.parseInt(hex, 16));
2006
+ }
2007
+ if (character === "\n") return "";
2008
+ if (character === "\r") {
2009
+ if (source[index] === "\n") index += 1;
2010
+ return "";
2011
+ }
2012
+ return Object.freeze({
2013
+ "0": "\0",
2014
+ b: "\b",
2015
+ f: "\f",
2016
+ n: "\n",
2017
+ r: "\r",
2018
+ t: "\t",
2019
+ v: "\v",
2020
+ })[character] ?? character;
2021
+ }
2022
+
2023
+ function readQuoted(quote) {
2024
+ const start = index;
2025
+ let value = "";
2026
+ index += 1;
2027
+ while (index < source.length) {
2028
+ const character = source[index];
2029
+ index += 1;
2030
+ if (character === "\\") {
2031
+ value += readEscape();
2032
+ continue;
2033
+ }
2034
+ if (character === quote) {
2035
+ recordLiteral(start, index, value);
2036
+ return;
2037
+ }
2038
+ if (character === "\n" || character === "\r") fail();
2039
+ value += character;
2040
+ }
2041
+ fail();
2042
+ }
2043
+
2044
+ function skipRegex() {
2045
+ index += 1;
2046
+ let inClass = false;
2047
+ while (index < source.length) {
2048
+ const character = source[index];
2049
+ index += 1;
2050
+ if (character === "\\") {
2051
+ index += 1;
2052
+ continue;
2053
+ }
2054
+ if (character === "[") inClass = true;
2055
+ else if (character === "]") inClass = false;
2056
+ else if (character === "/" && !inClass) {
2057
+ while (/[A-Za-z]/u.test(source[index] ?? "")) index += 1;
2058
+ return;
2059
+ } else if (character === "\n" || character === "\r") {
2060
+ fail();
2061
+ }
2062
+ }
2063
+ fail();
2064
+ }
2065
+
2066
+ function canStartRegex(lastToken) {
2067
+ return lastToken === null
2068
+ || [
2069
+ "(", "[", "{", "=", ":", ",", ";", "!", "?",
2070
+ "&&", "||", "=>", "return", "case", "throw",
2071
+ ].includes(lastToken);
2072
+ }
2073
+
2074
+ function readTemplate() {
2075
+ const templateId = nextTemplateId;
2076
+ nextTemplateId += 1;
2077
+ templateStack.push(templateId);
2078
+ let fragmentStart = index;
2079
+ let value = "";
2080
+ index += 1;
2081
+ while (index < source.length) {
2082
+ const character = source[index];
2083
+ index += 1;
2084
+ if (character === "\\") {
2085
+ value += readEscape();
2086
+ continue;
2087
+ }
2088
+ if (character === "`") {
2089
+ recordLiteral(fragmentStart, index, value);
2090
+ templateStack.pop();
2091
+ return;
2092
+ }
2093
+ if (character === "$" && source[index] === "{") {
2094
+ recordLiteral(fragmentStart, index - 1, value);
2095
+ value = "";
2096
+ index += 1;
2097
+ scanCode(true);
2098
+ fragmentStart = index;
2099
+ continue;
2100
+ }
2101
+ value += character;
2102
+ }
2103
+ fail();
2104
+ }
2105
+
2106
+ function scanCode(stopAtTemplateBrace = false) {
2107
+ let nestedBraces = 0;
2108
+ let lastToken = null;
2109
+ while (index < source.length) {
2110
+ const character = source[index];
2111
+ const next = source[index + 1];
2112
+ if (/\s/u.test(character)) {
2113
+ index += 1;
2114
+ continue;
2115
+ }
2116
+ if (character === "/" && next === "/") {
2117
+ index += 2;
2118
+ while (index < source.length && source[index] !== "\n") index += 1;
2119
+ continue;
2120
+ }
2121
+ if (character === "/" && next === "*") {
2122
+ const end = source.indexOf("*/", index + 2);
2123
+ if (end < 0) fail();
2124
+ index = end + 2;
2125
+ continue;
2126
+ }
2127
+ if (character === "'" || character === '"') {
2128
+ if (lastToken === "from") fail();
2129
+ readQuoted(character);
2130
+ lastToken = "literal";
2131
+ continue;
2132
+ }
2133
+ if (character === "`") {
2134
+ if (lastToken === "from") fail();
2135
+ readTemplate();
2136
+ lastToken = "literal";
2137
+ continue;
2138
+ }
2139
+ if (character === "/" && canStartRegex(lastToken)) {
2140
+ skipRegex();
2141
+ lastToken = "literal";
2142
+ continue;
2143
+ }
2144
+ if (identifierStart(character)) {
2145
+ const start = index;
2146
+ index += 1;
2147
+ while (identifierPart(source[index])) index += 1;
2148
+ const word = source.slice(start, index);
2149
+ if (CLOSED_MODULE_OUT_OF_GRAMMAR_IDENTIFIERS.has(word)) fail();
2150
+ if (word === "constructor" && lastToken === ".") fail();
2151
+ if (word === "import") {
2152
+ while (/\s/u.test(source[index] ?? "")) index += 1;
2153
+ if (source[index] === "." && source.slice(index + 1, index + 5) === "meta") {
2154
+ index += 5;
2155
+ lastToken = "import.meta";
2156
+ continue;
2157
+ }
2158
+ fail();
2159
+ }
2160
+ lastToken = word;
2161
+ continue;
2162
+ }
2163
+ if (character === "\\") fail();
2164
+ if (stopAtTemplateBrace && character === "}") {
2165
+ if (nestedBraces === 0) {
2166
+ index += 1;
2167
+ return;
2168
+ }
2169
+ nestedBraces -= 1;
2170
+ } else if (stopAtTemplateBrace && character === "{") {
2171
+ nestedBraces += 1;
2172
+ }
2173
+ const twoCharacters = `${character}${next ?? ""}`;
2174
+ if (["&&", "||", "=>"].includes(twoCharacters)) {
2175
+ lastToken = twoCharacters;
2176
+ index += 2;
2177
+ } else {
2178
+ lastToken = character;
2179
+ index += 1;
2180
+ }
2181
+ }
2182
+ if (stopAtTemplateBrace) fail();
2183
+ }
361
2184
 
362
- function assertSelfContainedModuleSource(source, label) {
363
- let index = 0;
364
- let nextTemplateId = 1;
365
- const templateStack = [];
366
- const literalFragments = [];
2185
+ scanCode();
2186
+ assertStaticLiteralChains();
2187
+ }
367
2188
 
368
- function fail() {
2189
+ async function assertSelfContainedRuntime(admitted, metadata, security) {
2190
+ const javascriptFiles = admitted.files.filter(({ descriptor }) =>
2191
+ descriptor.kind === "runtime" && descriptor.mediaType === "text/javascript");
2192
+ if (
2193
+ javascriptFiles.length !== 1
2194
+ || javascriptFiles[0].descriptor.path !== metadata.runtime.entry
2195
+ ) {
369
2196
  throw speechError(
370
2197
  "ARCANE_AI_RUNTIME_MODULE_GRAPH_UNDECLARED",
371
- `${label} must be one self-contained JavaScript module without imports or re-exports.`,
2198
+ "Browser speech requires exactly one admitted self-contained runtime module.",
372
2199
  );
373
2200
  }
374
-
375
- function identifierStart(character) {
376
- return /[A-Za-z_$]/u.test(character ?? "");
377
- }
378
-
379
- function identifierPart(character) {
380
- return /[A-Za-z0-9_$]/u.test(character ?? "");
381
- }
382
-
383
- function assertLiteral(value) {
384
- if (CLOSED_MODULE_OUT_OF_GRAMMAR_LITERAL.test(value)) fail();
385
- }
386
-
387
- function assertComputedLiteral(value) {
388
- if (value.includes("constructor") || /import\s*\(/u.test(value)) fail();
389
- for (const identifier of CLOSED_MODULE_OUT_OF_GRAMMAR_IDENTIFIERS) {
390
- if (value.includes(identifier)) fail();
391
- }
392
- }
393
-
394
- function recordLiteral(start, end, value) {
395
- assertLiteral(value);
396
- literalFragments.push(Object.freeze({
397
- start,
398
- end,
399
- value,
400
- templateIds: Object.freeze([...templateStack]),
401
- }));
2201
+ const [{ descriptor, file }] = javascriptFiles;
2202
+ if (security?.secure === true) {
2203
+ assertSelfContainedModuleSource(await file.text(), descriptor.path);
402
2204
  }
2205
+ }
403
2206
 
404
- function stripJoinerTrivia(value) {
405
- return value
406
- .replace(/\/\*[\s\S]*?\*\//gu, "")
407
- .replace(/\/\/[^\r\n]*(?:\r?\n|$)/gu, "")
408
- .replace(/\s+/gu, "");
409
- }
2207
+ function tokenizeArtifactGraphModule(source, modulePath) {
2208
+ const tokens = [];
2209
+ let index = 0;
410
2210
 
411
- function sharesTemplate(left, right) {
412
- return left.templateIds.some((id) => right.templateIds.includes(id));
2211
+ function fail(reason, message) {
2212
+ throw artifactGraphError(reason, `${modulePath}: ${message}`);
413
2213
  }
414
2214
 
415
- function staticallyJoins(left, right) {
416
- const separator = stripJoinerTrivia(source.slice(left.end, right.start));
417
- const usesConcat = separator.includes(".concat");
418
- const withoutConcat = separator.replace(/\.concat/gu, "");
419
- const usesPlus = withoutConcat.includes("+");
420
- const usesTemplate = sharesTemplate(left, right)
421
- && (withoutConcat.includes("${") || withoutConcat.includes("}"));
422
- if (!usesConcat && !usesPlus && !usesTemplate) return false;
423
- return (usesTemplate ? /^[+()${}]*$/u : /^[+()]*$/u).test(withoutConcat);
2215
+ function identifierStart(character) {
2216
+ return /[A-Za-z_$]/u.test(character ?? "");
424
2217
  }
425
2218
 
426
- function assertStaticLiteralChains() {
427
- let chain = [];
428
- function flushChain() {
429
- if (chain.length > 1) {
430
- assertComputedLiteral(chain.map((fragment) => fragment.value).join(""));
431
- }
432
- chain = [];
433
- }
434
- for (const fragment of literalFragments) {
435
- const previous = chain[chain.length - 1];
436
- if (previous && staticallyJoins(previous, fragment)) {
437
- chain.push(fragment);
438
- continue;
439
- }
440
- flushChain();
441
- chain.push(fragment);
442
- }
443
- flushChain();
2219
+ function identifierPart(character) {
2220
+ return /[A-Za-z0-9_$]/u.test(character ?? "");
444
2221
  }
445
2222
 
446
2223
  function readEscape() {
447
- if (index >= source.length) fail();
2224
+ if (index >= source.length) {
2225
+ fail("artifact-graph-javascript-escape-unterminated", "unterminated escape sequence.");
2226
+ }
448
2227
  const character = source[index];
449
2228
  index += 1;
450
2229
  if (character === "x") {
451
2230
  const hex = source.slice(index, index + 2);
452
- if (!/^[a-f0-9]{2}$/iu.test(hex)) fail();
2231
+ if (!/^[a-f0-9]{2}$/iu.test(hex)) {
2232
+ fail("artifact-graph-javascript-hexadecimal-escape-malformed", "malformed hexadecimal escape sequence.");
2233
+ }
453
2234
  index += 2;
454
2235
  return String.fromCodePoint(Number.parseInt(hex, 16));
455
2236
  }
456
2237
  if (character === "u") {
457
2238
  if (source[index] === "{") {
458
2239
  const end = source.indexOf("}", index + 1);
459
- if (end < 0) fail();
460
- const hex = source.slice(index + 1, end);
461
- if (!/^[a-f0-9]{1,6}$/iu.test(hex)) fail();
2240
+ const hex = end < 0 ? "" : source.slice(index + 1, end);
2241
+ if (!/^[a-f0-9]{1,6}$/iu.test(hex)) {
2242
+ fail("artifact-graph-javascript-unicode-code-point-escape-malformed", "malformed Unicode escape sequence.");
2243
+ }
462
2244
  const codePoint = Number.parseInt(hex, 16);
463
- if (codePoint > 0x10ffff) fail();
2245
+ if (codePoint > 0x10ffff) {
2246
+ fail("artifact-graph-javascript-unicode-code-point-out-of-range", "Unicode escape exceeds the valid range.");
2247
+ }
464
2248
  index = end + 1;
465
2249
  return String.fromCodePoint(codePoint);
466
2250
  }
467
2251
  const hex = source.slice(index, index + 4);
468
- if (!/^[a-f0-9]{4}$/iu.test(hex)) fail();
2252
+ if (!/^[a-f0-9]{4}$/iu.test(hex)) {
2253
+ fail("artifact-graph-javascript-unicode-escape-malformed", "malformed Unicode escape sequence.");
2254
+ }
469
2255
  index += 4;
470
2256
  return String.fromCodePoint(Number.parseInt(hex, 16));
471
2257
  }
@@ -494,16 +2280,16 @@ function assertSelfContainedModuleSource(source, label) {
494
2280
  index += 1;
495
2281
  if (character === "\\") {
496
2282
  value += readEscape();
497
- continue;
498
- }
499
- if (character === quote) {
500
- recordLiteral(start, index, value);
2283
+ } else if (character === quote) {
2284
+ tokens.push(Object.freeze({ type: "string", value, start, end: index }));
501
2285
  return;
2286
+ } else if (character === "\n" || character === "\r") {
2287
+ fail("artifact-graph-javascript-quoted-string-line-break-rejected", "quoted string contains a line break.");
2288
+ } else {
2289
+ value += character;
502
2290
  }
503
- if (character === "\n" || character === "\r") fail();
504
- value += character;
505
2291
  }
506
- fail();
2292
+ fail("artifact-graph-javascript-quoted-string-unterminated", "unterminated quoted string.");
507
2293
  }
508
2294
 
509
2295
  function skipRegex() {
@@ -514,157 +2300,885 @@ function assertSelfContainedModuleSource(source, label) {
514
2300
  index += 1;
515
2301
  if (character === "\\") {
516
2302
  index += 1;
517
- continue;
518
- }
519
- if (character === "[") inClass = true;
520
- else if (character === "]") inClass = false;
521
- else if (character === "/" && !inClass) {
2303
+ } else if (character === "[") {
2304
+ inClass = true;
2305
+ } else if (character === "]") {
2306
+ inClass = false;
2307
+ } else if (character === "/" && !inClass) {
522
2308
  while (/[A-Za-z]/u.test(source[index] ?? "")) index += 1;
523
2309
  return;
524
2310
  } else if (character === "\n" || character === "\r") {
525
- fail();
2311
+ fail("artifact-graph-javascript-regexp-line-break-rejected", "regular expression contains a line break.");
526
2312
  }
527
2313
  }
528
- fail();
2314
+ fail("artifact-graph-javascript-regexp-unterminated", "unterminated regular expression.");
529
2315
  }
530
2316
 
531
2317
  function canStartRegex(lastToken) {
532
- return lastToken === null
2318
+ return !lastToken
533
2319
  || [
534
- "(", "[", "{", "=", ":", ",", ";", "!", "?",
535
- "&&", "||", "=>", "return", "case", "throw",
536
- ].includes(lastToken);
2320
+ "(", "[", "{", "=", ":", ",", ";", "!", "?", "&&", "||",
2321
+ "=>", "return", "case", "throw", "else", "do", "in", "of",
2322
+ ].includes(lastToken.value);
537
2323
  }
538
2324
 
539
2325
  function readTemplate() {
540
- const templateId = nextTemplateId;
541
- nextTemplateId += 1;
542
- templateStack.push(templateId);
543
- let fragmentStart = index;
544
- let value = "";
545
2326
  index += 1;
546
2327
  while (index < source.length) {
547
2328
  const character = source[index];
548
2329
  index += 1;
549
2330
  if (character === "\\") {
550
- value += readEscape();
2331
+ readEscape();
2332
+ } else if (character === "`") {
2333
+ return;
2334
+ } else if (character === "$" && source[index] === "{") {
2335
+ index += 1;
2336
+ scanCode(true);
2337
+ }
2338
+ }
2339
+ fail("artifact-graph-javascript-template-literal-unterminated", "unterminated template literal.");
2340
+ }
2341
+
2342
+ function scanCode(stopAtTemplateBrace = false) {
2343
+ let nestedBraces = 0;
2344
+ let lastToken = tokens[tokens.length - 1] ?? null;
2345
+ while (index < source.length) {
2346
+ const character = source[index];
2347
+ const next = source[index + 1];
2348
+ if (/\s/u.test(character)) {
2349
+ index += 1;
2350
+ continue;
2351
+ }
2352
+ if (character === "/" && next === "/") {
2353
+ index += 2;
2354
+ while (index < source.length && source[index] !== "\n") index += 1;
2355
+ continue;
2356
+ }
2357
+ if (character === "/" && next === "*") {
2358
+ const end = source.indexOf("*/", index + 2);
2359
+ if (end < 0) {
2360
+ fail("artifact-graph-javascript-block-comment-unterminated", "unterminated block comment.");
2361
+ }
2362
+ index = end + 2;
2363
+ continue;
2364
+ }
2365
+ if (character === "'" || character === '"') {
2366
+ readQuoted(character);
2367
+ lastToken = tokens[tokens.length - 1];
551
2368
  continue;
552
2369
  }
553
2370
  if (character === "`") {
554
- recordLiteral(fragmentStart, index, value);
555
- templateStack.pop();
556
- return;
2371
+ readTemplate();
2372
+ lastToken = Object.freeze({ type: "template", value: "template" });
2373
+ continue;
557
2374
  }
558
- if (character === "$" && source[index] === "{") {
559
- recordLiteral(fragmentStart, index - 1, value);
560
- value = "";
2375
+ if (character === "/" && canStartRegex(lastToken)) {
2376
+ skipRegex();
2377
+ lastToken = Object.freeze({ type: "regexp", value: "regexp" });
2378
+ continue;
2379
+ }
2380
+ if (identifierStart(character)) {
2381
+ const start = index;
561
2382
  index += 1;
562
- scanCode(true);
563
- fragmentStart = index;
2383
+ while (identifierPart(source[index])) index += 1;
2384
+ const token = Object.freeze({
2385
+ type: "identifier",
2386
+ value: source.slice(start, index),
2387
+ start,
2388
+ end: index,
2389
+ });
2390
+ tokens.push(token);
2391
+ lastToken = token;
2392
+ continue;
2393
+ }
2394
+ if (character === "\\") {
2395
+ fail("artifact-graph-javascript-escaped-identifier-rejected", "escaped identifier is not admitted.");
2396
+ }
2397
+ if (stopAtTemplateBrace && character === "}") {
2398
+ if (nestedBraces === 0) {
2399
+ index += 1;
2400
+ return;
2401
+ }
2402
+ nestedBraces -= 1;
2403
+ } else if (stopAtTemplateBrace && character === "{") {
2404
+ nestedBraces += 1;
2405
+ }
2406
+ const twoCharacters = `${character}${next ?? ""}`;
2407
+ const value = [
2408
+ "&&", "||", "=>", "?.", "??", "==", "!=", "<=", ">=", "++", "--",
2409
+ ].includes(twoCharacters)
2410
+ ? twoCharacters
2411
+ : character;
2412
+ const token = Object.freeze({
2413
+ type: "punctuation",
2414
+ value,
2415
+ start: index,
2416
+ end: index + value.length,
2417
+ });
2418
+ tokens.push(token);
2419
+ index += value.length;
2420
+ lastToken = token;
2421
+ }
2422
+ if (stopAtTemplateBrace) {
2423
+ fail("artifact-graph-javascript-template-expression-unterminated", "unterminated template expression.");
2424
+ }
2425
+ }
2426
+
2427
+ scanCode();
2428
+ return Object.freeze(tokens);
2429
+ }
2430
+
2431
+ function artifactGraphDeclarationsByModule(values) {
2432
+ const result = new Map();
2433
+ for (const value of values) {
2434
+ const entries = result.get(value.modulePath) ?? [];
2435
+ entries.push(value);
2436
+ result.set(value.modulePath, entries);
2437
+ }
2438
+ for (const entries of result.values()) {
2439
+ entries.sort((left, right) => left.occurrence - right.occurrence);
2440
+ }
2441
+ return result;
2442
+ }
2443
+
2444
+ function assertArtifactGraphOccurrences(observed, declared, modulePath, subject) {
2445
+ if (observed.length !== declared.length) {
2446
+ throw artifactGraphError(
2447
+ observed.length > declared.length
2448
+ ? "artifact-graph-runtime-edge-undeclared"
2449
+ : "artifact-graph-runtime-edge-declaration-unmatched",
2450
+ `${modulePath} exposes ${String(observed.length)} ${subject} occurrence(s), but the graph declares ${String(declared.length)}.`,
2451
+ );
2452
+ }
2453
+ for (let index = 0; index < declared.length; index += 1) {
2454
+ if (declared[index].occurrence !== index + 1) {
2455
+ throw artifactGraphError(
2456
+ "artifact-graph-runtime-edge-occurrence-noncanonical",
2457
+ `${modulePath} ${subject} declarations must use contiguous one-based occurrence values.`,
2458
+ );
2459
+ }
2460
+ }
2461
+ }
2462
+
2463
+ function inspectArtifactGraphModuleSource(source, modulePath, metadata, {
2464
+ strict = false,
2465
+ } = {}) {
2466
+ const tokens = tokenizeArtifactGraphModule(source, modulePath);
2467
+ const staticImports = [];
2468
+ const dynamicImports = [];
2469
+ const fetches = [];
2470
+ const moduleWorkers = [];
2471
+ const cacheOpens = [];
2472
+ const returnThisTransforms = [];
2473
+ const typedArrayConstructors = [];
2474
+ const warnings = new Set();
2475
+ const forbiddenDynamicCode = new Set([
2476
+ "AsyncFunction",
2477
+ "AsyncGeneratorFunction",
2478
+ "BroadcastChannel",
2479
+ "GeneratorFunction",
2480
+ "RTCPeerConnection",
2481
+ "ShadowRealm",
2482
+ "eval",
2483
+ "importScripts",
2484
+ "WebSocketStream",
2485
+ ]);
2486
+
2487
+ function next(index, offset = 1) {
2488
+ return tokens[index + offset] ?? null;
2489
+ }
2490
+
2491
+ function previous(index, offset = 1) {
2492
+ return tokens[index - offset] ?? null;
2493
+ }
2494
+
2495
+ function recordGuardCall(target, index, kind) {
2496
+ const token = tokens[index];
2497
+ const opening = next(index);
2498
+ if (opening?.value !== "(") {
2499
+ throw artifactGraphError(
2500
+ `artifact-graph-runtime-${kind.toLowerCase()}-direct-call-required`,
2501
+ `${modulePath} references ${kind} outside an admitted direct call boundary.`,
2502
+ );
2503
+ }
2504
+ let start = token.start;
2505
+ if (previous(index)?.value === ".") {
2506
+ if (!["globalThis", "self"].includes(previous(index, 2)?.value)) {
2507
+ throw artifactGraphError(
2508
+ `artifact-graph-runtime-${kind.toLowerCase()}-receiver-not-global`,
2509
+ `${modulePath} calls ${kind} through a non-global receiver.`,
2510
+ );
2511
+ }
2512
+ start = previous(index, 2).start;
2513
+ }
2514
+ target.push(Object.freeze({ start, end: opening.end }));
2515
+ }
2516
+
2517
+ function typedArrayConstructor(index) {
2518
+ const opening = next(index);
2519
+ if (opening?.value !== "(") return null;
2520
+ const property = previous(index, 2);
2521
+ if (property?.type !== "identifier") return null;
2522
+ if (previous(index, 3)?.value === "new") {
2523
+ return Object.freeze({
2524
+ start: property.start,
2525
+ end: tokens[index].end,
2526
+ receiver: source.slice(property.start, property.end),
2527
+ });
2528
+ }
2529
+ if (
2530
+ previous(index, 3)?.value === "."
2531
+ && previous(index, 4)?.value === "]"
2532
+ && previous(index, 5)?.value === "0"
2533
+ && previous(index, 6)?.value === "["
2534
+ && previous(index, 7)?.type === "identifier"
2535
+ && previous(index, 8)?.value === "new"
2536
+ ) {
2537
+ return Object.freeze({
2538
+ start: previous(index, 7).start,
2539
+ end: tokens[index].end,
2540
+ receiver: source.slice(previous(index, 7).start, property.end),
2541
+ });
2542
+ }
2543
+ return null;
2544
+ }
2545
+
2546
+ for (let index = 0; index < tokens.length; index += 1) {
2547
+ const token = tokens[index];
2548
+ if (
2549
+ (token.type === "identifier" || token.type === "string")
2550
+ && token.value === ARTIFACT_GRAPH_GUARDS
2551
+ ) {
2552
+ throw artifactGraphError(
2553
+ "artifact-graph-runtime-guard-reference-reserved",
2554
+ `${modulePath} references the SDK-owned artifact graph runtime guard name.`,
2555
+ );
2556
+ }
2557
+ if (
2558
+ token.type === "string"
2559
+ && [
2560
+ "Function",
2561
+ "BroadcastChannel",
2562
+ "RTCPeerConnection",
2563
+ "ShadowRealm",
2564
+ "SharedWorker",
2565
+ "Worker",
2566
+ "WebSocketStream",
2567
+ "XMLHttpRequest",
2568
+ "constructor",
2569
+ "eval",
2570
+ "fetch",
2571
+ "importScripts",
2572
+ ].includes(token.value)
2573
+ && previous(index)?.value === "["
2574
+ && next(index)?.value === "]"
2575
+ ) {
2576
+ if (strict) {
2577
+ throw artifactGraphError(
2578
+ "artifact-graph-runtime-computed-dynamic-code-undeclared",
2579
+ `${modulePath} contains computed access to dynamic code capability ${token.value}.`,
2580
+ );
2581
+ }
2582
+ warnings.add(token.value);
2583
+ continue;
2584
+ }
2585
+ if (token.type !== "identifier") continue;
2586
+ if (forbiddenDynamicCode.has(token.value)) {
2587
+ if (strict) {
2588
+ throw artifactGraphError(
2589
+ "artifact-graph-runtime-dynamic-code-undeclared",
2590
+ `${modulePath} contains dynamic code capability ${token.value}, which is not admitted.`,
2591
+ );
2592
+ }
2593
+ warnings.add(token.value);
2594
+ continue;
2595
+ }
2596
+ if (token.value === "Function") {
2597
+ const sequence = [
2598
+ next(index)?.value,
2599
+ next(index, 2)?.value,
2600
+ next(index, 3)?.value,
2601
+ next(index, 4)?.value,
2602
+ next(index, 5)?.value,
2603
+ ];
2604
+ if (
2605
+ [".", "?."].includes(previous(index)?.value)
2606
+ || previous(index)?.value === "new"
2607
+ || sequence[0] !== "("
2608
+ || next(index, 2)?.type !== "string"
2609
+ || sequence[1].trim() !== "return this"
2610
+ || sequence[2] !== ")"
2611
+ || sequence[3] !== "("
2612
+ || sequence[4] !== ")"
2613
+ ) {
2614
+ if (strict) {
2615
+ throw artifactGraphError(
2616
+ "artifact-graph-runtime-dynamic-code-undeclared",
2617
+ `${modulePath} contains a Function constructor outside the sole supported global-object transform.`,
2618
+ );
2619
+ }
2620
+ warnings.add("Function");
564
2621
  continue;
565
2622
  }
566
- value += character;
2623
+ returnThisTransforms.push(Object.freeze({
2624
+ start: token.start,
2625
+ end: next(index, 5).end,
2626
+ }));
2627
+ index += 5;
2628
+ continue;
2629
+ }
2630
+ if (
2631
+ token.value === "caches"
2632
+ && next(index)?.value === "."
2633
+ && next(index, 2)?.value === "open"
2634
+ && next(index, 3)?.value === "("
2635
+ ) {
2636
+ let start = token.start;
2637
+ if (previous(index)?.value === ".") {
2638
+ if (!["globalThis", "self"].includes(previous(index, 2)?.value)) {
2639
+ throw artifactGraphError(
2640
+ "artifact-graph-runtime-cache-open-receiver-not-global",
2641
+ `${modulePath} calls CacheStorage.open through a non-global receiver.`,
2642
+ );
2643
+ }
2644
+ start = previous(index, 2).start;
2645
+ }
2646
+ cacheOpens.push(Object.freeze({ start, end: next(index, 3).end }));
2647
+ continue;
567
2648
  }
568
- fail();
569
- }
570
-
571
- function scanCode(stopAtTemplateBrace = false) {
572
- let nestedBraces = 0;
573
- let lastToken = null;
574
- while (index < source.length) {
575
- const character = source[index];
576
- const next = source[index + 1];
577
- if (/\s/u.test(character)) {
578
- index += 1;
579
- continue;
2649
+ if (token.value === "caches") {
2650
+ if (previous(index)?.value === "typeof") continue;
2651
+ throw artifactGraphError(
2652
+ "artifact-graph-runtime-cache-open-direct-call-required",
2653
+ `${modulePath} references CacheStorage outside an admitted direct open call.`,
2654
+ );
2655
+ }
2656
+ if (
2657
+ token.value === "constructor"
2658
+ && [".", "?."].includes(previous(index)?.value)
2659
+ && next(index)?.value === "("
2660
+ ) {
2661
+ const observed = previous(index)?.value === "." ? typedArrayConstructor(index) : null;
2662
+ if (!observed) {
2663
+ throw artifactGraphError(
2664
+ "artifact-graph-runtime-constructor-dynamic-code-undeclared",
2665
+ `${modulePath} calls a constructor property outside the declared typed-array constructor transform.`,
2666
+ );
580
2667
  }
581
- if (character === "/" && next === "/") {
2668
+ typedArrayConstructors.push(observed);
2669
+ continue;
2670
+ }
2671
+ if (token.value === "import") {
2672
+ if (next(index)?.value === "." && next(index, 2)?.value === "meta") {
582
2673
  index += 2;
583
- while (index < source.length && source[index] !== "\n") index += 1;
584
2674
  continue;
585
2675
  }
586
- if (character === "/" && next === "*") {
587
- const end = source.indexOf("*/", index + 2);
588
- if (end < 0) fail();
589
- index = end + 2;
2676
+ if (next(index)?.value === "(") {
2677
+ dynamicImports.push(Object.freeze({ start: token.start, end: next(index).end }));
590
2678
  continue;
591
2679
  }
592
- if (character === "'" || character === '"') {
593
- if (lastToken === "from") fail();
594
- readQuoted(character);
595
- lastToken = "literal";
596
- continue;
2680
+ let specifier = next(index)?.type === "string" ? next(index) : null;
2681
+ if (!specifier) {
2682
+ for (let cursor = index + 1; cursor < tokens.length; cursor += 1) {
2683
+ if (tokens[cursor].value === ";") break;
2684
+ if (tokens[cursor].value === "from" && next(cursor)?.type === "string") {
2685
+ specifier = next(cursor);
2686
+ break;
2687
+ }
2688
+ }
597
2689
  }
598
- if (character === "`") {
599
- if (lastToken === "from") fail();
600
- readTemplate();
601
- lastToken = "literal";
602
- continue;
2690
+ if (!specifier) {
2691
+ throw artifactGraphError(
2692
+ "artifact-graph-static-import-specifier-unresolved",
2693
+ `${modulePath} contains a static import without one literal specifier.`,
2694
+ );
603
2695
  }
604
- if (character === "/" && canStartRegex(lastToken)) {
605
- skipRegex();
606
- lastToken = "literal";
607
- continue;
2696
+ staticImports.push(Object.freeze({
2697
+ start: specifier.start,
2698
+ end: specifier.end,
2699
+ specifier: specifier.value,
2700
+ }));
2701
+ continue;
2702
+ }
2703
+ if (token.value === "export") {
2704
+ if (!["*", "{"].includes(next(index)?.value)) continue;
2705
+ let specifier = null;
2706
+ for (let cursor = index + 1; cursor < tokens.length; cursor += 1) {
2707
+ if (tokens[cursor].value === ";") break;
2708
+ if (tokens[cursor].value === "from" && next(cursor)?.type === "string") {
2709
+ specifier = next(cursor);
2710
+ break;
2711
+ }
2712
+ if (["export", "import"].includes(tokens[cursor].value)) break;
608
2713
  }
609
- if (identifierStart(character)) {
610
- const start = index;
611
- index += 1;
612
- while (identifierPart(source[index])) index += 1;
613
- const word = source.slice(start, index);
614
- if (CLOSED_MODULE_OUT_OF_GRAMMAR_IDENTIFIERS.has(word)) fail();
615
- if (word === "constructor" && lastToken === ".") fail();
616
- if (word === "import") {
617
- while (/\s/u.test(source[index] ?? "")) index += 1;
618
- if (source[index] === "." && source.slice(index + 1, index + 5) === "meta") {
619
- index += 5;
620
- lastToken = "import.meta";
621
- continue;
622
- }
623
- fail();
2714
+ if (specifier) {
2715
+ staticImports.push(Object.freeze({
2716
+ start: specifier.start,
2717
+ end: specifier.end,
2718
+ specifier: specifier.value,
2719
+ }));
2720
+ }
2721
+ continue;
2722
+ }
2723
+ if (token.value === "fetch") {
2724
+ recordGuardCall(fetches, index, "fetch");
2725
+ continue;
2726
+ }
2727
+ if (token.value === "Worker") {
2728
+ const opening = next(index);
2729
+ if (opening?.value !== "(") {
2730
+ if (strict) {
2731
+ throw artifactGraphError(
2732
+ "artifact-graph-runtime-worker-constructor-call-required",
2733
+ `${modulePath} references Worker outside an admitted constructor boundary.`,
2734
+ );
624
2735
  }
625
- lastToken = word;
2736
+ warnings.add("Worker");
626
2737
  continue;
627
2738
  }
628
- if (character === "\\") fail();
629
- if (stopAtTemplateBrace && character === "}") {
630
- if (nestedBraces === 0) {
631
- index += 1;
632
- return;
2739
+ let start = token.start;
2740
+ if (previous(index)?.value === "new") {
2741
+ start = previous(index).start;
2742
+ } else if (previous(index)?.value === ".") {
2743
+ if (!["globalThis", "self"].includes(previous(index, 2)?.value)) {
2744
+ if (strict) {
2745
+ throw artifactGraphError(
2746
+ "artifact-graph-runtime-worker-receiver-not-global",
2747
+ `${modulePath} constructs Worker through a non-global receiver.`,
2748
+ );
2749
+ }
2750
+ warnings.add("Worker");
2751
+ continue;
633
2752
  }
634
- nestedBraces -= 1;
635
- } else if (stopAtTemplateBrace && character === "{") {
636
- nestedBraces += 1;
637
- }
638
- const twoCharacters = `${character}${next ?? ""}`;
639
- if (["&&", "||", "=>"].includes(twoCharacters)) {
640
- lastToken = twoCharacters;
641
- index += 2;
642
- } else {
643
- lastToken = character;
644
- index += 1;
2753
+ start = previous(index, 2).start;
2754
+ if (previous(index, 3)?.value === "new") start = previous(index, 3).start;
645
2755
  }
2756
+ moduleWorkers.push(Object.freeze({ start, end: opening.end }));
646
2757
  }
647
- if (stopAtTemplateBrace) fail();
648
2758
  }
649
2759
 
650
- scanCode();
651
- assertStaticLiteralChains();
2760
+ const moduleTransforms = artifactGraphDeclarationsByModule(metadata.transforms)
2761
+ .get(modulePath) ?? [];
2762
+ const declarations = {
2763
+ staticImports: artifactGraphDeclarationsByModule(metadata.edges.staticImports)
2764
+ .get(modulePath) ?? [],
2765
+ dynamicImports: artifactGraphDeclarationsByModule(metadata.edges.dynamicImports)
2766
+ .get(modulePath) ?? [],
2767
+ fetches: artifactGraphDeclarationsByModule(metadata.edges.fetches)
2768
+ .get(modulePath) ?? [],
2769
+ moduleWorkers: artifactGraphDeclarationsByModule(metadata.edges.moduleWorkers)
2770
+ .get(modulePath) ?? [],
2771
+ cacheOpens: artifactGraphDeclarationsByModule(metadata.edges.cacheOpens)
2772
+ .get(modulePath) ?? [],
2773
+ returnThisTransforms: moduleTransforms.filter((transform) =>
2774
+ transform.kind === "function-return-this-to-global-this"),
2775
+ typedArrayConstructors: moduleTransforms.filter((transform) =>
2776
+ transform.kind === "typed-array-constructor"),
2777
+ };
2778
+ assertArtifactGraphOccurrences(
2779
+ staticImports,
2780
+ declarations.staticImports,
2781
+ modulePath,
2782
+ "static import or re-export",
2783
+ );
2784
+ assertArtifactGraphOccurrences(
2785
+ dynamicImports,
2786
+ declarations.dynamicImports,
2787
+ modulePath,
2788
+ "dynamic import",
2789
+ );
2790
+ assertArtifactGraphOccurrences(fetches, declarations.fetches, modulePath, "fetch");
2791
+ assertArtifactGraphOccurrences(
2792
+ moduleWorkers,
2793
+ declarations.moduleWorkers,
2794
+ modulePath,
2795
+ "module Worker",
2796
+ );
2797
+ assertArtifactGraphOccurrences(
2798
+ cacheOpens,
2799
+ declarations.cacheOpens,
2800
+ modulePath,
2801
+ "CacheStorage open",
2802
+ );
2803
+ assertArtifactGraphOccurrences(
2804
+ returnThisTransforms,
2805
+ declarations.returnThisTransforms,
2806
+ modulePath,
2807
+ "Function return-this transform",
2808
+ );
2809
+ assertArtifactGraphOccurrences(
2810
+ typedArrayConstructors,
2811
+ declarations.typedArrayConstructors,
2812
+ modulePath,
2813
+ "typed-array constructor transform",
2814
+ );
2815
+ for (let index = 0; index < staticImports.length; index += 1) {
2816
+ if (staticImports[index].specifier !== declarations.staticImports[index].specifier) {
2817
+ throw artifactGraphError(
2818
+ "artifact-graph-static-import-specifier-mismatch",
2819
+ `${modulePath} static import occurrence ${String(index + 1)} does not match its declared specifier.`,
2820
+ );
2821
+ }
2822
+ }
2823
+ return Object.freeze({
2824
+ source,
2825
+ staticImports: Object.freeze(staticImports),
2826
+ dynamicImports: Object.freeze(dynamicImports),
2827
+ fetches: Object.freeze(fetches),
2828
+ moduleWorkers: Object.freeze(moduleWorkers),
2829
+ cacheOpens: Object.freeze(cacheOpens),
2830
+ returnThisTransforms: Object.freeze(returnThisTransforms),
2831
+ typedArrayConstructors: Object.freeze(typedArrayConstructors),
2832
+ warnings: Object.freeze([...warnings].sort()),
2833
+ declarations,
2834
+ });
652
2835
  }
653
2836
 
654
- async function assertSelfContainedRuntime(admitted, metadata) {
655
- const javascriptFiles = admitted.files.filter(({ descriptor }) =>
656
- descriptor.kind === "runtime" && descriptor.mediaType === "text/javascript");
2837
+ function assertArtifactGraphStaticImportClosure(metadata) {
2838
+ const dependencies = new Map(metadata.runtimeFiles
2839
+ .filter((file) => ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(file.kind))
2840
+ .map((file) => [file.path, []]));
2841
+ for (const edge of metadata.edges.staticImports) {
2842
+ dependencies.get(edge.modulePath).push(edge.targetPath);
2843
+ }
2844
+ const visiting = new Set();
2845
+ const visited = new Set();
2846
+ const order = [];
2847
+ function visit(path) {
2848
+ if (visiting.has(path)) {
2849
+ throw artifactGraphError(
2850
+ "artifact-graph-runtime-static-import-cycle",
2851
+ `Artifact graph static imports contain a cycle at ${path}.`,
2852
+ );
2853
+ }
2854
+ if (visited.has(path)) return;
2855
+ visiting.add(path);
2856
+ for (const dependency of dependencies.get(path) ?? []) visit(dependency);
2857
+ visiting.delete(path);
2858
+ visited.add(path);
2859
+ order.push(path);
2860
+ }
2861
+ for (const path of [...dependencies.keys()].sort()) visit(path);
2862
+ return Object.freeze(order);
2863
+ }
2864
+
2865
+ async function inspectArtifactGraphRuntime(admitted, metadata, signal, security) {
2866
+ if (security?.secure !== true) {
2867
+ return Object.freeze({
2868
+ plans: new Map(),
2869
+ order: Object.freeze([]),
2870
+ warnings: Object.freeze(["artifact-graph-runtime-unchecked"]),
2871
+ });
2872
+ }
2873
+ const admittedByPath = new Map(admitted.files.map((entry) => [entry.descriptor.path, entry]));
2874
+ const plans = new Map();
2875
+ for (const descriptor of metadata.runtimeFiles) {
2876
+ throwIfAborted(signal);
2877
+ if (!ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(descriptor.kind)) continue;
2878
+ const file = admittedByPath.get(descriptor.path)?.file;
2879
+ if (!file) {
2880
+ throw artifactGraphError(
2881
+ "artifact-graph-runtime-javascript-file-missing",
2882
+ `Artifact graph runtime JavaScript file ${descriptor.path} is unavailable.`,
2883
+ );
2884
+ }
2885
+ let source;
2886
+ try {
2887
+ const bytes = new Uint8Array(await file.arrayBuffer());
2888
+ source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2889
+ } catch (error) {
2890
+ throw artifactGraphError(
2891
+ "artifact-graph-runtime-javascript-utf8-decode-rejected",
2892
+ `Artifact graph runtime JavaScript file ${descriptor.path} is not valid UTF-8.`,
2893
+ error,
2894
+ );
2895
+ }
2896
+ plans.set(
2897
+ descriptor.path,
2898
+ inspectArtifactGraphModuleSource(source, descriptor.path, metadata, {
2899
+ strict: security?.secure === true,
2900
+ }),
2901
+ );
2902
+ }
2903
+ const order = assertArtifactGraphStaticImportClosure(metadata);
2904
+ const warnings = [...new Set([...plans.values()].flatMap((plan) => plan.warnings))]
2905
+ .sort();
2906
+ if (security?.secure !== true && warnings.length > 0) {
2907
+ globalThis.console?.warn?.(
2908
+ `Arcane browser speech warn-first mode allowed runtime capabilities: ${warnings.join(", ")}.`,
2909
+ );
2910
+ }
2911
+ throwIfAborted(signal);
2912
+ return Object.freeze({ plans, order, warnings: Object.freeze(warnings) });
2913
+ }
2914
+
2915
+ function applyArtifactGraphModuleTransforms(plan, materializedByPath, guardCapability) {
2916
+ const replacements = [];
2917
+ for (let index = 0; index < plan.staticImports.length; index += 1) {
2918
+ const observed = plan.staticImports[index];
2919
+ const declared = plan.declarations.staticImports[index];
2920
+ const targetUrl = materializedByPath.get(declared.targetPath)?.moduleUrl;
2921
+ if (!targetUrl) {
2922
+ throw artifactGraphError(
2923
+ "artifact-graph-static-import-target-unmaterialized",
2924
+ `Static import target ${declared.targetPath} was not materialized before ${declared.modulePath}.`,
2925
+ );
2926
+ }
2927
+ replacements.push({
2928
+ start: observed.start,
2929
+ end: observed.end,
2930
+ value: JSON.stringify(targetUrl),
2931
+ });
2932
+ }
2933
+ for (let index = 0; index < plan.dynamicImports.length; index += 1) {
2934
+ const observed = plan.dynamicImports[index];
2935
+ const declared = plan.declarations.dynamicImports[index];
2936
+ replacements.push({
2937
+ start: observed.start,
2938
+ end: observed.end,
2939
+ value: `globalThis.${ARTIFACT_GRAPH_GUARDS}.dynamicImport(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},`,
2940
+ });
2941
+ }
2942
+ for (let index = 0; index < plan.fetches.length; index += 1) {
2943
+ const observed = plan.fetches[index];
2944
+ const declared = plan.declarations.fetches[index];
2945
+ replacements.push({
2946
+ start: observed.start,
2947
+ end: observed.end,
2948
+ value: `globalThis.${ARTIFACT_GRAPH_GUARDS}.fetch(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},`,
2949
+ });
2950
+ }
2951
+ for (let index = 0; index < plan.moduleWorkers.length; index += 1) {
2952
+ const observed = plan.moduleWorkers[index];
2953
+ const declared = plan.declarations.moduleWorkers[index];
2954
+ replacements.push({
2955
+ start: observed.start,
2956
+ end: observed.end,
2957
+ value: `globalThis.${ARTIFACT_GRAPH_GUARDS}.createWorker(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},`,
2958
+ });
2959
+ }
2960
+ for (let index = 0; index < plan.cacheOpens.length; index += 1) {
2961
+ const observed = plan.cacheOpens[index];
2962
+ const declared = plan.declarations.cacheOpens[index];
2963
+ replacements.push({
2964
+ start: observed.start,
2965
+ end: observed.end,
2966
+ value: `globalThis.${ARTIFACT_GRAPH_GUARDS}.openCache(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},`,
2967
+ });
2968
+ }
2969
+ for (const observed of plan.returnThisTransforms) {
2970
+ replacements.push({ start: observed.start, end: observed.end, value: "globalThis" });
2971
+ }
2972
+ for (let index = 0; index < plan.typedArrayConstructors.length; index += 1) {
2973
+ const observed = plan.typedArrayConstructors[index];
2974
+ const declared = plan.declarations.typedArrayConstructors[index];
2975
+ replacements.push({
2976
+ start: observed.start,
2977
+ end: observed.end,
2978
+ value: `(globalThis.${ARTIFACT_GRAPH_GUARDS}.typedArrayConstructor(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},${observed.receiver}))`,
2979
+ });
2980
+ }
2981
+ replacements.sort((left, right) => right.start - left.start);
2982
+ let source = plan.source;
2983
+ let previousStart = source.length;
2984
+ for (const replacement of replacements) {
2985
+ if (replacement.end > previousStart) {
2986
+ throw artifactGraphError(
2987
+ "artifact-graph-module-transform-overlap",
2988
+ "Artifact graph module transforms overlap and cannot be applied deterministically.",
2989
+ );
2990
+ }
2991
+ source = `${source.slice(0, replacement.start)}${replacement.value}${source.slice(replacement.end)}`;
2992
+ previousStart = replacement.start;
2993
+ }
2994
+ return source;
2995
+ }
2996
+
2997
+ function artifactGraphGuardCapability() {
2998
+ const crypto = globalThis.crypto;
2999
+ if (typeof crypto?.getRandomValues !== "function") {
3000
+ throw artifactGraphError(
3001
+ "artifact-graph-guard-capability-unavailable",
3002
+ "Authenticated artifact graph materialization requires cryptographic random values.",
3003
+ );
3004
+ }
3005
+ const bytes = new Uint8Array(32);
3006
+ crypto.getRandomValues(bytes);
3007
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
3008
+ }
3009
+
3010
+ async function blobDigest(blob) {
3011
+ const digest = createStreamingSha256();
3012
+ let bytes = 0;
3013
+ for await (const chunk of byteChunks(blob.stream())) {
3014
+ digest.update(chunk);
3015
+ bytes += chunk.byteLength;
3016
+ }
3017
+ return Object.freeze({ bytes, sha256: digest.digestHex() });
3018
+ }
3019
+
3020
+ async function createArtifactGraphObjectUrls(admitted, metadata, inspection, security) {
657
3021
  if (
658
- javascriptFiles.length !== 1
659
- || javascriptFiles[0].descriptor.path !== metadata.runtime.entry
3022
+ typeof PLATFORM_CREATE_OBJECT_URL !== "function"
3023
+ || typeof PLATFORM_REVOKE_OBJECT_URL !== "function"
3024
+ || typeof PLATFORM_FETCH !== "function"
660
3025
  ) {
661
- throw speechError(
662
- "ARCANE_AI_RUNTIME_MODULE_GRAPH_UNDECLARED",
663
- "Browser speech requires exactly one admitted self-contained runtime module.",
3026
+ throw artifactGraphError(
3027
+ "artifact-graph-object-url-platform-unavailable",
3028
+ "Authenticated artifact graph materialization requires native Blob URL creation, revocation, and fetch.",
664
3029
  );
665
3030
  }
666
- const [{ descriptor, file }] = javascriptFiles;
667
- assertSelfContainedModuleSource(await file.text(), descriptor.path);
3031
+ const admittedByPath = new Map(admitted.files.map((entry) => [entry.descriptor.path, entry]));
3032
+ const materializedByPath = new Map();
3033
+ const created = [];
3034
+ const createdIdentities = new Set();
3035
+ const guardCapability = artifactGraphGuardCapability();
3036
+ async function materialize(descriptor, body) {
3037
+ const blob = body instanceof Blob && body.type === descriptor.mediaType
3038
+ ? body
3039
+ : new Blob([body], { type: descriptor.mediaType });
3040
+ const source = await blobDigest(blob);
3041
+ const transformed = ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(descriptor.kind);
3042
+ const expected = Object.freeze({
3043
+ bytes: transformed || security.checks.byteLength !== true
3044
+ ? source.bytes
3045
+ : descriptor.bytes,
3046
+ sha256: transformed || security.checks.sha256 !== true
3047
+ ? source.sha256
3048
+ : descriptor.sha256,
3049
+ });
3050
+ const moduleUrl = PLATFORM_CREATE_OBJECT_URL(blob);
3051
+ if (typeof moduleUrl !== "string" || !moduleUrl.startsWith("blob:")) {
3052
+ throw artifactGraphError(
3053
+ "artifact-graph-object-url-scheme-not-blob",
3054
+ `Materialized artifact graph file ${descriptor.path} did not produce a Blob URL.`,
3055
+ );
3056
+ }
3057
+ if (createdIdentities.has(moduleUrl)) {
3058
+ throw artifactGraphError(
3059
+ "artifact-graph-object-url-identity-ambiguous",
3060
+ `Materialized artifact graph file ${descriptor.path} reused another file's Blob URL.`,
3061
+ );
3062
+ }
3063
+ createdIdentities.add(moduleUrl);
3064
+ created.push(moduleUrl);
3065
+ let response;
3066
+ try {
3067
+ response = await PLATFORM_FETCH(moduleUrl, {
3068
+ method: "GET",
3069
+ credentials: "omit",
3070
+ redirect: "error",
3071
+ });
3072
+ } catch (error) {
3073
+ throw artifactGraphError(
3074
+ "artifact-graph-object-url-readback-unavailable",
3075
+ `Materialized artifact graph file ${descriptor.path} could not be read back from its Blob URL.`,
3076
+ error,
3077
+ );
3078
+ }
3079
+ if (!response.ok) {
3080
+ throw artifactGraphError(
3081
+ "artifact-graph-object-url-readback-http-status-rejected",
3082
+ `Materialized artifact graph file ${descriptor.path} returned a non-success Blob URL response.`,
3083
+ );
3084
+ }
3085
+ if (response.redirected || response.url !== moduleUrl) {
3086
+ throw artifactGraphError(
3087
+ "artifact-graph-object-url-readback-identity-mismatch",
3088
+ `Materialized artifact graph file ${descriptor.path} did not retain its exact Blob URL identity.`,
3089
+ );
3090
+ }
3091
+ const observedMediaType = response.headers.get("content-type")?.split(";", 1)[0].trim() ?? "";
3092
+ if (observedMediaType !== descriptor.mediaType) {
3093
+ throw artifactGraphError(
3094
+ "artifact-graph-object-url-media-type-mismatch",
3095
+ `Materialized artifact graph file ${descriptor.path} did not retain its declared media type.`,
3096
+ );
3097
+ }
3098
+ const observedBlob = await response.blob();
3099
+ const observed = await blobDigest(observedBlob);
3100
+ if (observed.bytes !== expected.bytes) {
3101
+ throw artifactGraphError(
3102
+ "artifact-graph-object-url-byte-length-mismatch",
3103
+ `Materialized artifact graph file ${descriptor.path} did not retain its exact byte length.`,
3104
+ );
3105
+ }
3106
+ if (observed.sha256 !== expected.sha256) {
3107
+ throw artifactGraphError(
3108
+ "artifact-graph-object-url-sha256-mismatch",
3109
+ `Materialized artifact graph file ${descriptor.path} did not retain its exact bytes.`,
3110
+ );
3111
+ }
3112
+ materializedByPath.set(descriptor.path, Object.freeze({
3113
+ kind: descriptor.kind,
3114
+ path: descriptor.path,
3115
+ sourceUrl: descriptor.sourceUrl,
3116
+ revision: descriptor.revision,
3117
+ license: descriptor.license,
3118
+ moduleUrl,
3119
+ mediaType: descriptor.mediaType,
3120
+ ...(descriptor.sourceMediaType === descriptor.mediaType
3121
+ ? {}
3122
+ : { sourceMediaType: descriptor.sourceMediaType }),
3123
+ bytes: descriptor.bytes,
3124
+ sha256: descriptor.sha256,
3125
+ runtimeRequestUrls: descriptor.runtimeRequestUrls,
3126
+ ...(descriptor.redirectFinalOrigins.length < 1
3127
+ ? {}
3128
+ : { redirectFinalOrigins: descriptor.redirectFinalOrigins }),
3129
+ }));
3130
+ }
3131
+ try {
3132
+ for (const descriptor of metadata.files) {
3133
+ if (ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(descriptor.kind)) continue;
3134
+ await materialize(descriptor, admittedByPath.get(descriptor.path).file);
3135
+ }
3136
+ for (const path of inspection.order) {
3137
+ const descriptor = metadata.filesByPath.get(path);
3138
+ const plan = inspection.plans.get(path);
3139
+ await materialize(
3140
+ descriptor,
3141
+ applyArtifactGraphModuleTransforms(plan, materializedByPath, guardCapability),
3142
+ );
3143
+ }
3144
+ return Object.freeze({
3145
+ guardCapability,
3146
+ files: Object.freeze(metadata.files.map((descriptor) =>
3147
+ materializedByPath.get(descriptor.path))),
3148
+ release() {
3149
+ for (const url of created.splice(0).reverse()) {
3150
+ try {
3151
+ PLATFORM_REVOKE_OBJECT_URL(url);
3152
+ } catch {
3153
+ // Object URL revocation follows worker termination and is best effort.
3154
+ }
3155
+ }
3156
+ },
3157
+ });
3158
+ } catch (error) {
3159
+ for (const url of created.splice(0).reverse()) {
3160
+ try {
3161
+ PLATFORM_REVOKE_OBJECT_URL(url);
3162
+ } catch {
3163
+ // Preserve the graph inspection or materialization error.
3164
+ }
3165
+ }
3166
+ throw error;
3167
+ }
3168
+ }
3169
+
3170
+ function artifactGraphAdmissionStatus(cache, offline, security) {
3171
+ const verification = security.checks.byteLength && security.checks.sha256
3172
+ ? "verified"
3173
+ : security.checks.byteLength || security.checks.sha256
3174
+ ? "partially-checked"
3175
+ : "unchecked";
3176
+ const source = cache === "installed"
3177
+ ? "network-dbopfs"
3178
+ : offline
3179
+ ? "offline-dbopfs-cache"
3180
+ : "dbopfs-cache";
3181
+ return `artifact-graph-${source}-${verification}`;
668
3182
  }
669
3183
 
670
3184
  function createObjectUrls(files, factory) {
@@ -815,10 +3329,10 @@ export function createDbopfsSpeechArtifactStore({
815
3329
  }
816
3330
 
817
3331
  async function removeUnlocked(authority) {
818
- if (!AUTHORITIES.has(authority)) {
3332
+ if (!isSpeechArtifactAuthority(authority)) {
819
3333
  throw new TypeError("Speech artifact removal requires an SDK-created authority.");
820
3334
  }
821
- const metadata = AUTHORITY_METADATA.get(authority);
3335
+ const metadata = artifactMetadata(authority);
822
3336
  const names = storageNames(authority, metadata.files);
823
3337
  const results = await Promise.all([
824
3338
  removeEntry(names.manifest),
@@ -854,6 +3368,17 @@ export function createDbopfsSpeechArtifactStore({
854
3368
  return completed === file.size && digest.digestHex() === descriptor.sha256;
855
3369
  }
856
3370
 
3371
+ function graphFileReason(descriptor, boundary) {
3372
+ const subject = descriptor.kind === "runtime-entrypoint-javascript"
3373
+ ? "entrypoint"
3374
+ : descriptor.kind;
3375
+ return `artifact-graph-${subject}-${boundary}`;
3376
+ }
3377
+
3378
+ function graphVerificationError(descriptor, boundary, message) {
3379
+ return artifactGraphError(graphFileReason(descriptor, boundary), message);
3380
+ }
3381
+
857
3382
  function assertSecurityDescriptors(files, security) {
858
3383
  for (const descriptor of files) {
859
3384
  if (security.checks.byteLength && descriptor.bytes === null) {
@@ -870,7 +3395,8 @@ export function createDbopfsSpeechArtifactStore({
870
3395
  }
871
3396
 
872
3397
  async function openCached(authority, { signal, onProgress, security } = {}) {
873
- const metadata = AUTHORITY_METADATA.get(authority);
3398
+ const graph = ARTIFACT_GRAPHS.has(authority);
3399
+ const metadata = artifactMetadata(authority);
874
3400
  const names = storageNames(authority, metadata.files);
875
3401
  const manifest = await readManifest(names.manifest);
876
3402
  if (!manifestMatches(manifest, authority, metadata.files)) {
@@ -883,7 +3409,12 @@ export function createDbopfsSpeechArtifactStore({
883
3409
  const descriptor = metadata.files[index];
884
3410
  const file = await readFile(names.files[index]);
885
3411
  const observed = manifest.files[index];
886
- if (!file || observed?.path !== descriptor.path || observed?.bytes !== file.size) {
3412
+ if (
3413
+ !file
3414
+ || observed?.path !== descriptor.path
3415
+ || observed?.bytes !== file.size
3416
+ || (graph && observed?.sha256 !== descriptor.sha256)
3417
+ ) {
887
3418
  await removeUnlocked(authority);
888
3419
  return null;
889
3420
  }
@@ -893,7 +3424,11 @@ export function createDbopfsSpeechArtifactStore({
893
3424
  security,
894
3425
  signal,
895
3426
  onProgress,
896
- "verify-cache",
3427
+ graph
3428
+ ? security.checks.sha256
3429
+ ? "artifact-graph-dbopfs-cache-rehash"
3430
+ : "artifact-graph-dbopfs-cache-readback"
3431
+ : "verify-cache",
897
3432
  )) {
898
3433
  await removeUnlocked(authority);
899
3434
  return null;
@@ -901,17 +3436,26 @@ export function createDbopfsSpeechArtifactStore({
901
3436
  files.push({ descriptor, file });
902
3437
  }
903
3438
  try {
904
- await assertSelfContainedRuntime({ files }, metadata);
3439
+ const inspection = graph && security.secure
3440
+ ? await inspectArtifactGraphRuntime({ files }, metadata, signal, security)
3441
+ : graph
3442
+ ? null
3443
+ : (await assertSelfContainedRuntime({ files }, metadata, security), null);
905
3444
  throwIfAborted(signal);
3445
+ return Object.freeze({
3446
+ files: Object.freeze(files),
3447
+ cache: "cached",
3448
+ inspection,
3449
+ });
906
3450
  } catch (error) {
907
3451
  await removeUnlocked(authority);
908
3452
  throw error;
909
3453
  }
910
- return Object.freeze({ files: Object.freeze(files), cache: "cached" });
911
3454
  }
912
3455
 
913
3456
  async function install(authority, { signal, onProgress, security } = {}) {
914
- const metadata = AUTHORITY_METADATA.get(authority);
3457
+ const graph = ARTIFACT_GRAPHS.has(authority);
3458
+ const metadata = artifactMetadata(authority);
915
3459
  const names = storageNames(authority, metadata.files);
916
3460
  const fetchFunction = fetchImpl ?? globalThis.fetch?.bind(globalThis);
917
3461
  if (typeof fetchFunction !== "function") {
@@ -923,96 +3467,297 @@ export function createDbopfsSpeechArtifactStore({
923
3467
  for (let index = 0; index < metadata.files.length; index += 1) {
924
3468
  throwIfAborted(signal);
925
3469
  const descriptor = metadata.files[index];
3470
+ const sourceUrl = graph ? descriptor.sourceUrl : descriptor.url;
3471
+ const redirectFinalOrigins = graph
3472
+ ? descriptor.redirectFinalOrigins
3473
+ : Object.freeze([]);
926
3474
  let response;
927
3475
  try {
928
- response = await fetchFunction(descriptor.url, {
3476
+ response = await fetchFunction(sourceUrl, {
929
3477
  cache: "no-store",
930
3478
  credentials: "omit",
931
3479
  mode: "cors",
932
- redirect: "error",
3480
+ redirect: redirectFinalOrigins.length < 1 ? "error" : "follow",
933
3481
  referrerPolicy: "no-referrer",
934
3482
  signal,
935
3483
  });
936
3484
  } catch (error) {
937
3485
  if (signal?.aborted || error?.name === "AbortError") throwIfAborted(signal);
938
- throw speechError("ARCANE_AI_ARTIFACT_DOWNLOAD_FAILED", "A speech artifact download failed.", error);
3486
+ if (graph) {
3487
+ throw artifactGraphError(
3488
+ "artifact-graph-source-fetch-rejected",
3489
+ `Artifact graph source fetch was rejected for ${descriptor.path}.`,
3490
+ error,
3491
+ );
3492
+ }
3493
+ throw speechError("ARCANE_AI_ARTIFACT_DOWNLOAD_FAILED", "A speech artifact source fetch was rejected.", error);
939
3494
  }
940
- if (!response?.ok || !response.body) {
941
- await response?.body?.cancel?.().catch(() => undefined);
3495
+ let responseBody;
3496
+ let responseHeaders;
3497
+ let responseOk;
3498
+ let responseRedirected;
3499
+ let responseStatus;
3500
+ let responseUrl;
3501
+ try {
3502
+ if (!response || typeof response !== "object") {
3503
+ throw new TypeError("The artifact fetch result is not an object.");
3504
+ }
3505
+ responseBody = response.body;
3506
+ responseHeaders = response.headers;
3507
+ responseOk = response.ok;
3508
+ responseRedirected = response.redirected;
3509
+ responseStatus = response.status;
3510
+ responseUrl = response.url;
3511
+ if (
3512
+ typeof responseOk !== "boolean"
3513
+ || typeof responseRedirected !== "boolean"
3514
+ || !Number.isInteger(responseStatus)
3515
+ || typeof responseUrl !== "string"
3516
+ || !responseHeaders
3517
+ || typeof responseHeaders.get !== "function"
3518
+ || (responseBody !== null && typeof responseBody?.getReader !== "function")
3519
+ ) {
3520
+ throw new TypeError("The artifact fetch result is not a readable Fetch Response.");
3521
+ }
3522
+ } catch (error) {
3523
+ if (graph) {
3524
+ throw artifactGraphError(
3525
+ "artifact-graph-source-http-response-rejected",
3526
+ `Artifact graph source fetch for ${descriptor.path} did not return a readable Fetch Response.`,
3527
+ error,
3528
+ );
3529
+ }
942
3530
  throw speechError(
943
3531
  "ARCANE_AI_ARTIFACT_DOWNLOAD_FAILED",
944
- `A speech artifact server returned HTTP ${response?.status ?? "unknown"}.`,
3532
+ "A speech artifact source fetch did not return a readable Fetch Response.",
3533
+ error,
945
3534
  );
946
3535
  }
947
3536
  let finalUrl = null;
3537
+ let finalUrlRecord = null;
948
3538
  try {
949
- finalUrl = typeof response.url === "string" && response.url
950
- ? new URL(response.url).href
3539
+ finalUrlRecord = responseUrl
3540
+ ? new URL(responseUrl)
951
3541
  : null;
3542
+ finalUrl = finalUrlRecord?.href ?? null;
952
3543
  } catch {
3544
+ finalUrlRecord = null;
953
3545
  finalUrl = null;
954
3546
  }
955
- if (response.redirected === true || finalUrl !== descriptor.url) {
956
- await response.body.cancel?.().catch(() => undefined);
3547
+ if (graph && responseRedirected && !finalUrlRecord) {
3548
+ await responseBody?.cancel?.().catch(() => undefined);
3549
+ throw artifactGraphError(
3550
+ "artifact-graph-source-response-url-unreadable",
3551
+ `Artifact graph redirected source response for ${descriptor.path} did not expose a readable final URL.`,
3552
+ );
3553
+ }
3554
+ if (graph && responseRedirected && redirectFinalOrigins.length < 1) {
3555
+ await responseBody?.cancel?.().catch(() => undefined);
3556
+ throw artifactGraphError(
3557
+ "artifact-graph-source-redirected",
3558
+ `Artifact graph source response for ${descriptor.path} did not match its immutable URL.`,
3559
+ );
3560
+ }
3561
+ if (graph && responseRedirected && finalUrlRecord.protocol !== "https:") {
3562
+ await responseBody?.cancel?.().catch(() => undefined);
3563
+ throw artifactGraphError(
3564
+ "artifact-graph-source-response-url-protocol-not-https",
3565
+ `Artifact graph redirected source response for ${descriptor.path} did not end at HTTPS.`,
3566
+ );
3567
+ }
3568
+ if (
3569
+ graph
3570
+ && responseRedirected
3571
+ && (finalUrlRecord.username || finalUrlRecord.password)
3572
+ ) {
3573
+ await responseBody?.cancel?.().catch(() => undefined);
3574
+ throw artifactGraphError(
3575
+ "artifact-graph-source-response-url-credentials-rejected",
3576
+ `Artifact graph redirected source response for ${descriptor.path} exposed credentials in its final URL.`,
3577
+ );
3578
+ }
3579
+ if (graph && responseRedirected && finalUrlRecord.hash) {
3580
+ await responseBody?.cancel?.().catch(() => undefined);
3581
+ throw artifactGraphError(
3582
+ "artifact-graph-source-response-url-fragment-rejected",
3583
+ `Artifact graph redirected source response for ${descriptor.path} exposed a fragment in its final URL.`,
3584
+ );
3585
+ }
3586
+ if (
3587
+ graph
3588
+ && responseRedirected
3589
+ && !redirectFinalOrigins.includes(finalUrlRecord.origin)
3590
+ ) {
3591
+ await responseBody?.cancel?.().catch(() => undefined);
3592
+ throw artifactGraphError(
3593
+ "artifact-graph-source-redirect-final-origin-mismatch",
3594
+ `Artifact graph redirected source response for ${descriptor.path} ended at an undeclared final origin.`,
3595
+ );
3596
+ }
3597
+ if (graph && !responseRedirected && finalUrl !== sourceUrl) {
3598
+ await responseBody?.cancel?.().catch(() => undefined);
3599
+ throw artifactGraphError(
3600
+ "artifact-graph-source-response-url-mismatch",
3601
+ `Artifact graph non-redirected source response for ${descriptor.path} did not retain its immutable URL.`,
3602
+ );
3603
+ }
3604
+ if (!graph && (responseRedirected || finalUrl !== sourceUrl)) {
3605
+ await responseBody?.cancel?.().catch(() => undefined);
957
3606
  throw speechError(
958
3607
  "ARCANE_AI_ARTIFACT_SOURCE_CHANGED",
959
3608
  "A speech artifact response did not match its admitted URL.",
960
3609
  );
961
3610
  }
962
- const header = response.headers?.get?.("content-length");
3611
+ if (!responseOk || !responseBody) {
3612
+ await responseBody?.cancel?.().catch(() => undefined);
3613
+ if (graph) {
3614
+ throw artifactGraphError(
3615
+ "artifact-graph-source-http-response-rejected",
3616
+ `Artifact graph source for ${descriptor.path} returned HTTP ${String(responseStatus)}.`,
3617
+ );
3618
+ }
3619
+ throw speechError(
3620
+ "ARCANE_AI_ARTIFACT_DOWNLOAD_FAILED",
3621
+ `A speech artifact server returned HTTP ${String(responseStatus)}.`,
3622
+ );
3623
+ }
3624
+ const header = responseHeaders.get("content-length");
963
3625
  const reportedBytes = header ? Number(header) : null;
3626
+ const contentEncoding = responseHeaders.get("content-encoding")?.trim() ?? "";
3627
+ if (graph) {
3628
+ const reportedMediaType = responseHeaders.get("content-type")
3629
+ ?.split(";", 1)[0]
3630
+ ?.trim()
3631
+ ?.toLowerCase() ?? null;
3632
+ if (reportedMediaType !== descriptor.sourceMediaType) {
3633
+ await responseBody.cancel?.().catch(() => undefined);
3634
+ throw graphVerificationError(
3635
+ descriptor,
3636
+ descriptor.sourceMediaType === descriptor.mediaType
3637
+ ? "media-type-mismatch"
3638
+ : "source-media-type-mismatch",
3639
+ `Artifact graph source media type for ${descriptor.path} did not match ${descriptor.sourceMediaType}.`,
3640
+ );
3641
+ }
3642
+ }
964
3643
  if (
965
3644
  security.checks.byteLength
966
3645
  && Number.isSafeInteger(reportedBytes)
3646
+ && (!graph || !contentEncoding)
967
3647
  && reportedBytes !== descriptor.bytes
968
3648
  ) {
969
- await response.body.cancel?.().catch(() => undefined);
3649
+ await responseBody.cancel?.().catch(() => undefined);
3650
+ if (graph) {
3651
+ throw graphVerificationError(
3652
+ descriptor,
3653
+ "byte-length-mismatch",
3654
+ `Artifact graph source Content-Length for ${descriptor.path} changed.`,
3655
+ );
3656
+ }
970
3657
  throw speechError("ARCANE_AI_ARTIFACT_SIZE_MISMATCH", "Speech artifact Content-Length changed.");
971
3658
  }
972
3659
  const digest = security.checks.sha256
973
3660
  ? createStreamingSha256()
974
3661
  : null;
975
- const written = await writeFile(names.files[index], response.body, {
3662
+ const written = await writeFile(names.files[index], responseBody, {
976
3663
  signal,
977
3664
  onChunk(chunk, completed) {
978
3665
  digest?.update(chunk);
979
3666
  if (security.checks.byteLength && completed > descriptor.bytes) {
3667
+ if (graph) {
3668
+ throw graphVerificationError(
3669
+ descriptor,
3670
+ "byte-length-mismatch",
3671
+ `Artifact graph source ${descriptor.path} exceeded its declared byte length.`,
3672
+ );
3673
+ }
980
3674
  throw speechError("ARCANE_AI_ARTIFACT_SIZE_MISMATCH", "A speech artifact exceeded its expected size.");
981
3675
  }
982
3676
  onProgress?.(providerProgress(
983
- "download",
3677
+ graph ? "artifact-graph-network-download" : "download",
984
3678
  completed,
985
3679
  security.checks.byteLength ? descriptor.bytes : null,
986
3680
  ));
987
3681
  },
988
3682
  });
989
3683
  if (security.checks.byteLength && written !== descriptor.bytes) {
3684
+ if (graph) {
3685
+ throw graphVerificationError(
3686
+ descriptor,
3687
+ "byte-length-mismatch",
3688
+ `Artifact graph source ${descriptor.path} byte length changed.`,
3689
+ );
3690
+ }
990
3691
  throw speechError("ARCANE_AI_ARTIFACT_SIZE_MISMATCH", "A speech artifact byte count changed.");
991
3692
  }
992
3693
  if (digest && digest.digestHex() !== descriptor.sha256) {
3694
+ if (graph) {
3695
+ throw graphVerificationError(
3696
+ descriptor,
3697
+ "sha256-mismatch",
3698
+ `Artifact graph source ${descriptor.path} SHA-256 changed.`,
3699
+ );
3700
+ }
993
3701
  throw speechError("ARCANE_AI_ARTIFACT_DIGEST_MISMATCH", "A speech artifact SHA-256 changed.");
994
3702
  }
995
3703
  const file = await readFile(names.files[index]);
996
3704
  if (!file || file.size !== written) {
3705
+ if (graph) {
3706
+ throw graphVerificationError(
3707
+ descriptor,
3708
+ "dbopfs-persisted-byte-length-mismatch",
3709
+ `DBOPFS did not preserve artifact graph file ${descriptor.path}.`,
3710
+ );
3711
+ }
997
3712
  throw speechError("ARCANE_AI_ARTIFACT_CACHE_REJECTED", "DBOPFS did not preserve a speech artifact.");
998
3713
  }
3714
+ if (!await verifyFile(
3715
+ file,
3716
+ descriptor,
3717
+ security,
3718
+ signal,
3719
+ onProgress,
3720
+ graph
3721
+ ? security.checks.sha256
3722
+ ? "artifact-graph-dbopfs-persisted-rehash"
3723
+ : "artifact-graph-dbopfs-persisted-readback"
3724
+ : "verify-cache",
3725
+ )) {
3726
+ if (graph) {
3727
+ throw graphVerificationError(
3728
+ descriptor,
3729
+ "dbopfs-persisted-sha256-mismatch",
3730
+ `DBOPFS persisted bytes for artifact graph file ${descriptor.path} were rejected during re-verification.`,
3731
+ );
3732
+ }
3733
+ throw speechError("ARCANE_AI_ARTIFACT_CACHE_REJECTED", "DBOPFS persisted bytes were rejected during verification.");
3734
+ }
999
3735
  installed.push({ descriptor, file });
1000
3736
  }
1001
- await assertSelfContainedRuntime({ files: installed }, metadata);
3737
+ const inspection = graph && security.secure
3738
+ ? await inspectArtifactGraphRuntime({ files: installed }, metadata, signal, security)
3739
+ : graph
3740
+ ? null
3741
+ : (await assertSelfContainedRuntime({ files: installed }, metadata, security), null);
1002
3742
  throwIfAborted(signal);
1003
3743
  const manifest = Object.freeze({
1004
- schema: MANIFEST_SCHEMA,
3744
+ schema: graph ? ARTIFACT_GRAPH_MANIFEST_SCHEMA : MANIFEST_SCHEMA,
1005
3745
  complete: true,
1006
- authority: authorityProjection(authority),
3746
+ authority: storedArtifactProjection(authority),
1007
3747
  files: Object.freeze(installed.map(({ descriptor, file }) => Object.freeze({
1008
3748
  path: descriptor.path,
1009
3749
  bytes: file.size,
3750
+ ...(graph ? { sha256: descriptor.sha256 } : {}),
1010
3751
  }))),
1011
3752
  completedAt: new Date().toISOString(),
1012
3753
  });
1013
3754
  const encoded = new TextEncoder().encode(`${JSON.stringify(manifest)}\n`);
1014
3755
  await writeFile(names.manifest, encoded, { signal });
1015
- return Object.freeze({ files: Object.freeze(installed), cache: "installed" });
3756
+ return Object.freeze({
3757
+ files: Object.freeze(installed),
3758
+ cache: "installed",
3759
+ inspection,
3760
+ });
1016
3761
  } catch (error) {
1017
3762
  await removeUnlocked(authority).catch(() => undefined);
1018
3763
  throw error;
@@ -1025,12 +3770,31 @@ export function createDbopfsSpeechArtifactStore({
1025
3770
  offline = false,
1026
3771
  security,
1027
3772
  } = {}) {
1028
- if (!AUTHORITIES.has(authority)) {
3773
+ if (!isSpeechArtifactAuthority(authority)) {
1029
3774
  throw new TypeError("Speech artifact preparation requires an SDK-created authority.");
1030
3775
  }
1031
3776
  throwIfAborted(signal);
1032
- const effectiveSecurity = resolveModelSecurity({ load: security });
1033
- const metadata = AUTHORITY_METADATA.get(authority);
3777
+ const graph = ARTIFACT_GRAPHS.has(authority);
3778
+ let effectiveSecurity;
3779
+ try {
3780
+ effectiveSecurity = resolveModelSecurity({ load: security });
3781
+ } catch (error) {
3782
+ if (graph) {
3783
+ throw artifactGraphTypeError(
3784
+ "artifact-graph-load-security-contract-rejected",
3785
+ "Browser speech artifact graph load security does not satisfy the required contract.",
3786
+ error,
3787
+ );
3788
+ }
3789
+ throw error;
3790
+ }
3791
+ const metadata = artifactMetadata(authority);
3792
+ if (graph && effectiveSecurity.secure !== true) {
3793
+ throw artifactGraphError(
3794
+ "artifact-graph-secure-mode-required",
3795
+ "Browser speech artifact graphs require explicit secure:true.",
3796
+ );
3797
+ }
1034
3798
  assertSecurityDescriptors(metadata.files, effectiveSecurity);
1035
3799
  const cached = await openCached(authority, {
1036
3800
  signal,
@@ -1045,8 +3809,51 @@ export function createDbopfsSpeechArtifactStore({
1045
3809
  security: effectiveSecurity,
1046
3810
  }));
1047
3811
  if (!admitted) {
3812
+ if (graph) {
3813
+ throw artifactGraphError(
3814
+ "artifact-graph-offline-cache-miss",
3815
+ "No complete verified offline artifact graph cache is available.",
3816
+ );
3817
+ }
1048
3818
  throw speechError("ARCANE_AI_ARTIFACT_OFFLINE_MISS", "No admitted offline speech cache is available.");
1049
3819
  }
3820
+ if (graph) {
3821
+ const artifactGraphAdmission = artifactGraphAdmissionStatus(
3822
+ admitted.cache,
3823
+ offline,
3824
+ effectiveSecurity,
3825
+ );
3826
+ const materialized = await createArtifactGraphObjectUrls(
3827
+ admitted,
3828
+ metadata,
3829
+ admitted.inspection,
3830
+ effectiveSecurity,
3831
+ );
3832
+ const runtimeFiles = Object.freeze(materialized.files.filter((file) =>
3833
+ file.kind.startsWith("runtime-")));
3834
+ const modelFiles = Object.freeze(materialized.files.filter((file) =>
3835
+ !file.kind.startsWith("runtime-")));
3836
+ return Object.freeze({
3837
+ cache: artifactGraphAdmission,
3838
+ artifactGraphId: authority.identitySha256,
3839
+ artifactGraphAdmission,
3840
+ security: effectiveSecurity,
3841
+ runtime: Object.freeze({
3842
+ ...metadata.runtime,
3843
+ files: runtimeFiles,
3844
+ edges: metadata.edges,
3845
+ transforms: metadata.transforms,
3846
+ guardCapability: materialized.guardCapability,
3847
+ artifactGraphId: authority.identitySha256,
3848
+ artifactGraphAdmission,
3849
+ }),
3850
+ model: Object.freeze({
3851
+ ...metadata.model,
3852
+ files: modelFiles,
3853
+ }),
3854
+ release: materialized.release,
3855
+ });
3856
+ }
1050
3857
  const materialized = createObjectUrls(admitted.files, objectUrlFactory);
1051
3858
  const runtimeFiles = materialized.files.filter((file) => file.kind === "runtime");
1052
3859
  const modelFiles = materialized.files.filter((file) => file.kind === "model");
@@ -1057,6 +3864,9 @@ export function createDbopfsSpeechArtifactStore({
1057
3864
  version: metadata.runtime.version,
1058
3865
  revision: metadata.runtime.revision,
1059
3866
  entry: metadata.runtime.entry,
3867
+ ...(metadata.runtime.wasmPaths === undefined
3868
+ ? {}
3869
+ : { wasmPaths: metadata.runtime.wasmPaths }),
1060
3870
  moduleGraph: "self-contained",
1061
3871
  files: Object.freeze(runtimeFiles),
1062
3872
  }),
@@ -1072,16 +3882,31 @@ export function createDbopfsSpeechArtifactStore({
1072
3882
  }
1073
3883
 
1074
3884
  function prepare(authority, options = {}) {
1075
- if (!AUTHORITIES.has(authority)) {
3885
+ if (!isSpeechArtifactAuthority(authority)) {
1076
3886
  return Promise.reject(new TypeError(
1077
3887
  "Speech artifact preparation requires an SDK-created authority.",
1078
3888
  ));
1079
3889
  }
1080
- return serializeAuthority(authority, () => prepareUnlocked(authority, options));
3890
+ return serializeAuthority(authority, async () => {
3891
+ try {
3892
+ return await prepareUnlocked(authority, options);
3893
+ } catch (error) {
3894
+ if (ARTIFACT_GRAPHS.has(authority) && options.signal?.aborted) {
3895
+ const cancelled = artifactGraphError(
3896
+ "artifact-graph-preparation-cancelled",
3897
+ "Artifact graph preparation was cancelled.",
3898
+ options.signal.reason ?? error,
3899
+ );
3900
+ cancelled.name = "AbortError";
3901
+ throw cancelled;
3902
+ }
3903
+ throw error;
3904
+ }
3905
+ });
1081
3906
  }
1082
3907
 
1083
3908
  function remove(authority) {
1084
- if (!AUTHORITIES.has(authority)) {
3909
+ if (!isSpeechArtifactAuthority(authority)) {
1085
3910
  return Promise.reject(new TypeError(
1086
3911
  "Speech artifact removal requires an SDK-created authority.",
1087
3912
  ));
@@ -1103,6 +3928,14 @@ export function isBrowserSpeechAuthority(value) {
1103
3928
  return AUTHORITIES.has(value);
1104
3929
  }
1105
3930
 
3931
+ export function isBrowserSpeechArtifactGraph(value) {
3932
+ return ARTIFACT_GRAPHS.has(value);
3933
+ }
3934
+
3935
+ export function isBrowserSpeechArtifactError(value) {
3936
+ return ARTIFACT_ERRORS.has(value);
3937
+ }
3938
+
1106
3939
  export function isDbopfsSpeechArtifactStore(value) {
1107
3940
  return STORES.has(value);
1108
3941
  }