arcane-os 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +30 -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 +3242 -391
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1209 -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 +249 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2322 -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 +18 -20
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1205 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +2 -2
  21. package/docs/reference/behavioral-testing.md +28 -5
  22. package/docs/reference/cli.md +115 -7
  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 +86 -37
  27. package/docs/reference/inventory/runtime-modules.json +126 -49
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +185 -33
  30. package/docs/reference/runtime-components.md +192 -80
  31. package/docs/reference/runtime-modules.md +589 -70
  32. package/docs/reference/sdk-api.md +1017 -24
  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 +453 -131
  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 +244 -72
  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 +2117 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +756 -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 +235 -37
  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 +130 -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-runtime.mjs +804 -22
  118. package/src/workspace.mjs +1 -1
@@ -1,11 +1,457 @@
1
1
  export const SPEECH_WORKER_PROTOCOL = "arcane-ai-speech-worker/1";
2
2
 
3
+ const SPEECH_WORKER_ERROR_PROTOCOL = "arcane-ai-speech-worker-error/1";
4
+
5
+ const ARTIFACT_GRAPH_MODULE_GRAPH =
6
+ "browser-speech-authenticated-artifact-graph";
7
+ const GRAPH_GUARD_NAME = "__arcaneBrowserSpeechArtifactGraphGuardsV1";
8
+ const NESTED_WORKER_PROTOCOL =
9
+ "arcane-ai-browser-speech-artifact-module-worker/1";
10
+ const GRAPH_ADMISSIONS = new Set([
11
+ "artifact-graph-network-dbopfs-verified",
12
+ "artifact-graph-dbopfs-cache-verified",
13
+ "artifact-graph-offline-dbopfs-cache-verified",
14
+ "artifact-graph-network-dbopfs-partially-checked",
15
+ "artifact-graph-dbopfs-cache-partially-checked",
16
+ "artifact-graph-offline-dbopfs-cache-partially-checked",
17
+ "artifact-graph-network-dbopfs-unchecked",
18
+ "artifact-graph-dbopfs-cache-unchecked",
19
+ "artifact-graph-offline-dbopfs-cache-unchecked",
20
+ ]);
3
21
  const ADAPTERS = Object.freeze({
4
22
  stt: "transformers-whisper",
5
23
  tts: "kokoro-js",
6
24
  });
25
+ const ONNX_NAMESPACES = Object.freeze({
26
+ stt: "transformers-env-backends-onnx-wasm",
27
+ tts: "kokoro-env-wasm-paths",
28
+ });
29
+ const ARTIFACT_GRAPH_TRANSFORM_KINDS = new Set([
30
+ "function-return-this-to-global-this",
31
+ "typed-array-constructor",
32
+ ]);
33
+ const PUBLIC_WORKER_OPERATIONS = Object.freeze([
34
+ "load",
35
+ "use",
36
+ "status",
37
+ "unload",
38
+ "dispose",
39
+ ]);
40
+ const TRANSPORT_WORKER_OPERATION_SET = new Set([
41
+ ...PUBLIC_WORKER_OPERATIONS,
42
+ "cancel",
43
+ ]);
44
+ const BOTH_WORKER_ROLES = Object.freeze(["stt", "tts"]);
45
+ const LOAD_OPERATION = Object.freeze(["load"]);
46
+ const USE_OPERATION = Object.freeze(["use"]);
47
+ const STATUS_OPERATION = Object.freeze(["status"]);
48
+ const UNLOAD_OPERATION = Object.freeze(["unload"]);
49
+ const DISPOSE_OPERATION = Object.freeze(["dispose"]);
50
+ const UNLOAD_OR_DISPOSE_OPERATIONS = Object.freeze(["unload", "dispose"]);
51
+ const LOAD_OR_USE_OPERATIONS = Object.freeze(["load", "use"]);
52
+ const SDK_WORKER_ERRORS = new WeakSet();
53
+ const WORKER_ERROR_MESSAGES = Object.freeze({
54
+ ARCANE_AI_REQUEST_ABORTED: "The speech worker operation was cancelled.",
55
+ ARCANE_AI_NOT_READY: "The speech worker is not loaded.",
56
+ ARCANE_AI_INVALID_REQUEST: "The speech worker request was rejected.",
57
+ ARCANE_AI_INVALID_PROVIDER_RESULT: "The speech engine result was rejected.",
58
+ ARCANE_AI_UNDECLARED_ARTIFACT: "The speech engine requested an undeclared artifact.",
59
+ ARCANE_AI_PROVIDER_UNAVAILABLE: "The selected speech engine is unavailable.",
60
+ ARCANE_AI_PROVIDER_DISPOSED: "The speech worker is disposed.",
61
+ ARCANE_AI_PROVIDER_REQUEST_FAILED: "The speech engine operation was rejected.",
62
+ ARCANE_AI_WORKER_CRASHED: "The speech Worker stopped unexpectedly.",
63
+ ARCANE_AI_WORKER_MESSAGE_ERROR: "The speech Worker message was rejected.",
64
+ ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID:
65
+ "The authenticated artifact graph Worker configuration was rejected.",
66
+ ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED:
67
+ "The runtime used an undeclared artifact graph fetch edge.",
68
+ ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED:
69
+ "The runtime used an undeclared artifact graph import edge.",
70
+ ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED:
71
+ "The runtime used an undeclared artifact graph cache edge.",
72
+ ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED:
73
+ "The runtime used an undeclared artifact graph module Worker edge.",
74
+ ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE:
75
+ "The Worker cannot install the authenticated artifact graph isolation boundary.",
76
+ });
77
+ const WORKER_ERROR_REASON_ADMISSIONS = new Map();
78
+
79
+ function registerWorkerErrorReasons(code, roles, operations, reasons) {
80
+ const admission = Object.freeze({
81
+ code,
82
+ roles: Object.freeze([...roles]),
83
+ operations: Object.freeze([...operations]),
84
+ });
85
+ for (const reason of reasons) WORKER_ERROR_REASON_ADMISSIONS.set(reason, admission);
86
+ }
87
+
88
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], LOAD_OPERATION, [
89
+ "stt-load-cancelled",
90
+ ]);
91
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], STATUS_OPERATION, [
92
+ "stt-status-cancelled",
93
+ ]);
94
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], UNLOAD_OPERATION, [
95
+ "stt-unload-cancelled",
96
+ ]);
97
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], DISPOSE_OPERATION, [
98
+ "stt-dispose-cancelled",
99
+ ]);
100
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], USE_OPERATION, [
101
+ "stt-transcription-cancelled",
102
+ ]);
103
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], LOAD_OPERATION, [
104
+ "tts-load-cancelled",
105
+ ]);
106
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], STATUS_OPERATION, [
107
+ "tts-status-cancelled",
108
+ ]);
109
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], UNLOAD_OPERATION, [
110
+ "tts-unload-cancelled",
111
+ ]);
112
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], DISPOSE_OPERATION, [
113
+ "tts-dispose-cancelled",
114
+ ]);
115
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], USE_OPERATION, [
116
+ "tts-synthesis-cancelled",
117
+ ]);
118
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], LOAD_OPERATION, [
119
+ "stt-worker-runtime-configuration-rejected",
120
+ "stt-worker-runtime-import-rejected",
121
+ "stt-worker-model-load-rejected",
122
+ ]);
123
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], LOAD_OPERATION, [
124
+ "tts-worker-runtime-configuration-rejected",
125
+ "tts-worker-runtime-import-rejected",
126
+ "tts-worker-model-load-rejected",
127
+ ]);
128
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], USE_OPERATION, [
129
+ "stt-transcription-engine-operation-rejected",
130
+ ]);
131
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], USE_OPERATION, [
132
+ "tts-synthesis-engine-operation-rejected",
133
+ ]);
134
+ registerWorkerErrorReasons(
135
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
136
+ ["stt"],
137
+ UNLOAD_OR_DISPOSE_OPERATIONS,
138
+ ["stt-worker-engine-dispose-rejected"],
139
+ );
140
+ registerWorkerErrorReasons(
141
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
142
+ ["tts"],
143
+ UNLOAD_OR_DISPOSE_OPERATIONS,
144
+ ["tts-worker-engine-dispose-rejected"],
145
+ );
146
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], DISPOSE_OPERATION, [
147
+ "stt-worker-dispose-rejected",
148
+ ]);
149
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], DISPOSE_OPERATION, [
150
+ "tts-worker-dispose-rejected",
151
+ ]);
152
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], STATUS_OPERATION, [
153
+ "stt-worker-status-rejected",
154
+ ]);
155
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], STATUS_OPERATION, [
156
+ "tts-worker-status-rejected",
157
+ ]);
158
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", BOTH_WORKER_ROLES, LOAD_OPERATION, [
159
+ "artifact-graph-entrypoint-not-materialized",
160
+ "artifact-graph-materialized-file-not-object",
161
+ "artifact-graph-materialized-media-type-empty",
162
+ "artifact-graph-materialized-module-url-empty",
163
+ "artifact-graph-materialized-path-ambiguous",
164
+ "artifact-graph-materialized-path-empty",
165
+ "artifact-graph-materialized-source-url-empty",
166
+ "artifact-graph-runtime-request-routes-not-array",
167
+ "speech-model-sample-rate-not-positive-safe-integer",
168
+ "speech-runtime-entrypoint-not-materialized",
169
+ "speech-worker-configuration-missing",
170
+ "speech-worker-materialized-files-missing",
171
+ "speech-worker-model-dtype-empty",
172
+ "speech-worker-model-id-empty",
173
+ "speech-worker-model-repository-empty",
174
+ "speech-worker-model-revision-empty",
175
+ "speech-worker-runtime-entry-empty",
176
+ "speech-worker-runtime-selection-mismatch",
177
+ ]);
178
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", ["stt"], USE_OPERATION, [
179
+ "stt-transcription-audio-not-float32array",
180
+ "stt-transcription-sample-rate-mismatch",
181
+ ]);
182
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", ["tts"], LOAD_OPERATION, [
183
+ "tts-default-voice-empty",
184
+ ]);
185
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", ["tts"], USE_OPERATION, [
186
+ "tts-synthesis-speed-out-of-range",
187
+ "tts-synthesis-text-empty",
188
+ "tts-synthesis-voice-empty",
189
+ "tts-synthesis-voice-not-declared",
190
+ ]);
191
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_PROVIDER_RESULT", ["stt"], USE_OPERATION, [
192
+ "stt-transcription-result-text-not-string",
193
+ ]);
194
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_PROVIDER_RESULT", ["tts"], USE_OPERATION, [
195
+ "tts-synthesis-pcm-result-not-float32array",
196
+ "tts-synthesis-pcm-sample-non-finite",
197
+ "tts-synthesis-result-sample-rate-mismatch",
198
+ ]);
199
+ registerWorkerErrorReasons(
200
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
201
+ BOTH_WORKER_ROLES,
202
+ LOAD_OR_USE_OPERATIONS,
203
+ [
204
+ "artifact-graph-cache-write-rejected",
205
+ "artifact-graph-runtime-request-url-malformed",
206
+ "speech-worker-artifact-request-method-rejected",
207
+ "speech-worker-artifact-request-undeclared",
208
+ "speech-worker-cache-match-rejected",
209
+ "speech-worker-cache-open-rejected",
210
+ ],
211
+ );
212
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_DISPOSED", ["stt"], LOAD_OPERATION, [
213
+ "stt-load-rejected-after-dispose",
214
+ ]);
215
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_DISPOSED", ["tts"], LOAD_OPERATION, [
216
+ "tts-load-rejected-after-dispose",
217
+ ]);
218
+ registerWorkerErrorReasons("ARCANE_AI_NOT_READY", ["stt"], USE_OPERATION, [
219
+ "stt-transcription-rejected-before-load",
220
+ ]);
221
+ registerWorkerErrorReasons("ARCANE_AI_NOT_READY", ["tts"], USE_OPERATION, [
222
+ "tts-synthesis-rejected-before-load",
223
+ ]);
224
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", BOTH_WORKER_ROLES, LOAD_OPERATION, [
225
+ "artifact-graph-fetch-constructor-unavailable",
226
+ "artifact-graph-onnx-wasm-pair-not-materialized",
227
+ "speech-worker-fetch-unavailable",
228
+ ]);
229
+ registerWorkerErrorReasons(
230
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
231
+ BOTH_WORKER_ROLES,
232
+ LOAD_OR_USE_OPERATIONS,
233
+ [
234
+ "artifact-graph-module-worker-constructor-unavailable",
235
+ "artifact-graph-negative-response-constructor-unavailable",
236
+ ],
237
+ );
238
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["tts"], LOAD_OPERATION, [
239
+ "kokoro-env-wasm-paths-assignment-rejected",
240
+ "kokoro-env-wasm-paths-unavailable",
241
+ "kokoro-tts-constructor-export-missing",
242
+ ]);
243
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["stt"], LOAD_OPERATION, [
244
+ "transformers-env-allow-local-models-assignment-rejected",
245
+ "transformers-env-allow-local-models-unavailable",
246
+ "transformers-env-allow-remote-models-assignment-rejected",
247
+ "transformers-env-allow-remote-models-unavailable",
248
+ "transformers-env-backends-onnx-wasm-unavailable",
249
+ "transformers-env-browser-cache-assignment-rejected",
250
+ "transformers-env-browser-cache-unavailable",
251
+ "transformers-env-custom-cache-assignment-rejected",
252
+ "transformers-env-custom-cache-toggle-unavailable",
253
+ "transformers-env-custom-cache-toggle-assignment-rejected",
254
+ "transformers-env-custom-cache-unavailable",
255
+ "transformers-env-fs-cache-assignment-rejected",
256
+ "transformers-env-fs-cache-unavailable",
257
+ "transformers-env-num-threads-assignment-rejected",
258
+ "transformers-env-num-threads-unavailable",
259
+ "transformers-env-wasm-paths-assignment-rejected",
260
+ "transformers-env-wasm-paths-unavailable",
261
+ "transformers-whisper-pipeline-export-missing",
262
+ ]);
263
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["stt"], USE_OPERATION, [
264
+ "stt-transcription-method-unavailable",
265
+ ]);
266
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["tts"], USE_OPERATION, [
267
+ "tts-synthesis-method-unavailable",
268
+ ]);
269
+ registerWorkerErrorReasons(
270
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
271
+ BOTH_WORKER_ROLES,
272
+ LOAD_OPERATION,
273
+ [
274
+ "artifact-graph-kokoro-voice-inventory-missing",
275
+ "artifact-graph-module-worker-target-not-materialized",
276
+ "artifact-graph-negative-request-routes-not-array",
277
+ "artifact-graph-onnx-wasm-configuration-mismatch",
278
+ "artifact-graph-runtime-request-route-ambiguous",
279
+ "artifact-graph-transform-identity-ambiguous",
280
+ "artifact-graph-transform-kind-not-admitted",
281
+ "artifact-graph-transform-module-path-empty",
282
+ "artifact-graph-transform-occurrence-not-positive-safe-integer",
283
+ "artifact-graph-transformers-cache-edge-ambiguous",
284
+ "artifact-graph-worker-configuration-incomplete",
285
+ ],
286
+ );
287
+ registerWorkerErrorReasons(
288
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
289
+ BOTH_WORKER_ROLES,
290
+ LOAD_OR_USE_OPERATIONS,
291
+ ["artifact-graph-typed-array-constructor-transform-undeclared"],
292
+ );
293
+ registerWorkerErrorReasons(
294
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
295
+ ["tts"],
296
+ LOAD_OPERATION,
297
+ [
298
+ "kokoro-env-num-threads-field-not-exposed",
299
+ ],
300
+ );
301
+ registerWorkerErrorReasons(
302
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
303
+ ["stt"],
304
+ LOAD_OPERATION,
305
+ [
306
+ "transformers-env-num-threads-not-positive-safe-integer",
307
+ ],
308
+ );
309
+ for (const label of ["cache-open", "dynamic-import", "fetch", "module-worker"]) {
310
+ registerWorkerErrorReasons(
311
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
312
+ BOTH_WORKER_ROLES,
313
+ LOAD_OPERATION,
314
+ [
315
+ `artifact-graph-${label}-edges-not-array`,
316
+ `artifact-graph-${label}-edge-not-object`,
317
+ `artifact-graph-${label}-edge-module-path-empty`,
318
+ `artifact-graph-${label}-edge-occurrence-not-positive-safe-integer`,
319
+ `artifact-graph-${label}-edge-policy-not-admitted`,
320
+ `artifact-graph-${label}-edge-identity-ambiguous`,
321
+ ],
322
+ );
323
+ }
324
+ registerWorkerErrorReasons(
325
+ "ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED",
326
+ BOTH_WORKER_ROLES,
327
+ LOAD_OR_USE_OPERATIONS,
328
+ [
329
+ "artifact-graph-dynamic-import-edge-undeclared",
330
+ "artifact-graph-dynamic-import-edge-undeclared-inactive-runtime-branch-entered",
331
+ "artifact-graph-dynamic-import-target-undeclared",
332
+ ],
333
+ );
334
+ registerWorkerErrorReasons(
335
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
336
+ BOTH_WORKER_ROLES,
337
+ LOAD_OR_USE_OPERATIONS,
338
+ [
339
+ "artifact-graph-fetch-edge-undeclared",
340
+ "artifact-graph-fetch-edge-undeclared-inactive-runtime-branch-entered",
341
+ "artifact-graph-fetch-guard-bypassed",
342
+ "artifact-graph-fetch-method-undeclared",
343
+ "artifact-graph-fetch-target-undeclared",
344
+ ],
345
+ );
346
+ registerWorkerErrorReasons(
347
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
348
+ BOTH_WORKER_ROLES,
349
+ LOAD_OR_USE_OPERATIONS,
350
+ [
351
+ "artifact-graph-cache-match-guard-bypassed",
352
+ "artifact-graph-cache-name-mismatch",
353
+ "artifact-graph-cache-open-edge-undeclared",
354
+ "artifact-graph-cache-open-edge-undeclared-inactive-runtime-branch-entered",
355
+ "artifact-graph-cache-open-guard-bypassed",
356
+ "artifact-graph-cache-read-target-undeclared",
357
+ ],
358
+ );
359
+ registerWorkerErrorReasons(
360
+ "ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
361
+ BOTH_WORKER_ROLES,
362
+ LOAD_OR_USE_OPERATIONS,
363
+ [
364
+ "artifact-graph-module-worker-edge-undeclared",
365
+ "artifact-graph-module-worker-edge-undeclared-inactive-runtime-branch-entered",
366
+ "artifact-graph-module-worker-target-undeclared",
367
+ "artifact-graph-module-worker-type-mismatch",
368
+ ],
369
+ );
370
+ registerWorkerErrorReasons(
371
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
372
+ BOTH_WORKER_ROLES,
373
+ LOAD_OPERATION,
374
+ [
375
+ "artifact-graph-cache-isolation-unavailable",
376
+ "artifact-graph-dynamic-code-constructor-isolation-unavailable",
377
+ "artifact-graph-fetch-isolation-unavailable",
378
+ "artifact-graph-guard-global-collision",
379
+ "artifact-graph-guard-global-definition-rejected",
380
+ "artifact-graph-indexeddb-isolation-unavailable",
381
+ "artifact-graph-opfs-isolation-unavailable",
382
+ "artifact-graph-private-message-port-missing",
383
+ "artifact-graph-setinterval-isolation-unavailable",
384
+ "artifact-graph-settimeout-isolation-unavailable",
385
+ "artifact-graph-typed-array-validation-unavailable",
386
+ "speech-worker-cache-isolation-unavailable",
387
+ "speech-worker-fetch-isolation-unavailable",
388
+ ],
389
+ );
390
+ registerWorkerErrorReasons(
391
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
392
+ BOTH_WORKER_ROLES,
393
+ LOAD_OR_USE_OPERATIONS,
394
+ [
395
+ "artifact-graph-dynamic-code-constructor-rejected",
396
+ "artifact-graph-guard-capability-mismatch",
397
+ "artifact-graph-setinterval-string-callback-rejected",
398
+ "artifact-graph-settimeout-string-callback-rejected",
399
+ "artifact-graph-typed-array-constructor-intrinsic-mismatch",
400
+ "artifact-graph-typed-array-constructor-receiver-not-typed-array",
401
+ ],
402
+ );
403
+ for (const name of [
404
+ "broadcastchannel",
405
+ "eventsource",
406
+ "function",
407
+ "rtcpeerconnection",
408
+ "shadowrealm",
409
+ "sharedworker",
410
+ "websocket",
411
+ "websocketstream",
412
+ "webtransport",
413
+ "worker",
414
+ "xmlhttprequest",
415
+ "eval",
416
+ "importscripts",
417
+ ]) {
418
+ registerWorkerErrorReasons(
419
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
420
+ BOTH_WORKER_ROLES,
421
+ LOAD_OR_USE_OPERATIONS,
422
+ [`artifact-graph-${name}-capability-undeclared`],
423
+ );
424
+ registerWorkerErrorReasons(
425
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
426
+ BOTH_WORKER_ROLES,
427
+ LOAD_OPERATION,
428
+ [`artifact-graph-${name}-isolation-unavailable`],
429
+ );
430
+ }
431
+ registerWorkerErrorReasons(
432
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
433
+ BOTH_WORKER_ROLES,
434
+ LOAD_OR_USE_OPERATIONS,
435
+ [
436
+ "artifact-graph-module-worker-error-envelope-rejected",
437
+ "artifact-graph-module-worker-initialization-message-rejected",
438
+ ],
439
+ );
440
+
441
+ function operationFailureReason(role, op) {
442
+ if (op === "load") return `${role}-worker-model-load-rejected`;
443
+ if (op === "use") {
444
+ return role === "stt"
445
+ ? "stt-transcription-engine-operation-rejected"
446
+ : "tts-synthesis-engine-operation-rejected";
447
+ }
448
+ if (op === "unload") return `${role}-worker-engine-dispose-rejected`;
449
+ if (op === "dispose") return `${role}-worker-dispose-rejected`;
450
+ if (op === "status") return `${role}-worker-status-rejected`;
451
+ return `${role}-worker-operation-unknown`;
452
+ }
7
453
 
8
- function workerError(code, message, cause) {
454
+ function workerError(code, message, cause, reason) {
9
455
  const error = cause === undefined
10
456
  ? new Error(message)
11
457
  : new Error(message, { cause });
@@ -13,15 +459,35 @@ function workerError(code, message, cause) {
13
459
  ? "AbortError"
14
460
  : "ArcaneSpeechWorkerError";
15
461
  error.code = code;
462
+ if (typeof reason === "string" && reason) error.reason = reason;
463
+ SDK_WORKER_ERRORS.add(error);
16
464
  return error;
17
465
  }
18
466
 
19
- function throwIfAborted(signal) {
467
+ function isSdkWorkerError(value) {
468
+ return value instanceof Error && SDK_WORKER_ERRORS.has(value);
469
+ }
470
+
471
+ function admitWorkerFailure(error, code, message, reason) {
472
+ return isSdkWorkerError(error)
473
+ ? error
474
+ : workerError(code, message, error, reason);
475
+ }
476
+
477
+ function operationReason(role, op, suffix) {
478
+ const subject = op === "use"
479
+ ? role === "stt" ? "stt-transcription" : "tts-synthesis"
480
+ : `${role}-${op}`;
481
+ return `${subject}-${suffix}`;
482
+ }
483
+
484
+ function throwIfAborted(signal, reason) {
20
485
  if (!signal?.aborted) return;
21
486
  throw workerError(
22
487
  "ARCANE_AI_REQUEST_ABORTED",
23
488
  "The speech worker operation was cancelled.",
24
489
  signal.reason,
490
+ reason,
25
491
  );
26
492
  }
27
493
 
@@ -51,119 +517,569 @@ export function collectSpeechTransferables(value) {
51
517
  return transfers;
52
518
  }
53
519
 
54
- function serializedError(error) {
55
- const code = typeof error?.code === "string" && /^ARCANE_AI_[A-Z0-9_]+$/u.test(error.code)
56
- ? error.code
57
- : "ARCANE_AI_PROVIDER_REQUEST_FAILED";
58
- const messages = Object.freeze({
59
- ARCANE_AI_REQUEST_ABORTED: "The speech worker operation was cancelled.",
60
- ARCANE_AI_NOT_READY: "The speech worker is not loaded.",
61
- ARCANE_AI_INVALID_REQUEST: "The speech worker request is invalid.",
62
- ARCANE_AI_INVALID_PROVIDER_RESULT: "The speech engine returned an invalid result.",
63
- ARCANE_AI_UNDECLARED_ARTIFACT: "The speech engine requested an undeclared artifact.",
64
- ARCANE_AI_PROVIDER_UNAVAILABLE: "The selected speech engine is unavailable.",
520
+ function serializedError(error, role, op) {
521
+ const admission = isSdkWorkerError(error)
522
+ ? workerErrorReasonAdmission(error.reason, role, op)
523
+ : null;
524
+ const admittedCode = isSdkWorkerError(error)
525
+ && Object.hasOwn(WORKER_ERROR_MESSAGES, error.code)
526
+ && admission?.code === error.code;
527
+ const code = admittedCode ? error.code : "ARCANE_AI_PROVIDER_REQUEST_FAILED";
528
+ const reason = admittedCode ? error.reason : operationFailureReason(role, op);
529
+ return Object.freeze({
530
+ protocol: SPEECH_WORKER_ERROR_PROTOCOL,
531
+ code,
532
+ message: WORKER_ERROR_MESSAGES[code],
533
+ reason,
65
534
  });
535
+ }
536
+
537
+ function workerErrorReasonAdmission(reason, role, op) {
538
+ const admission = WORKER_ERROR_REASON_ADMISSIONS.get(reason);
539
+ return admission
540
+ && admission.roles.includes(role)
541
+ && admission.operations.includes(op)
542
+ ? admission
543
+ : null;
544
+ }
545
+
546
+ export function normalizeSpeechWorkerErrorEnvelope(value, role, op) {
547
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
548
+ let descriptors;
549
+ try {
550
+ const keys = Reflect.ownKeys(value);
551
+ if (keys.some((key) => typeof key !== "string")
552
+ || keys.sort().join(",") !== "code,message,protocol,reason") return null;
553
+ descriptors = Object.getOwnPropertyDescriptors(value);
554
+ } catch {
555
+ return null;
556
+ }
557
+ for (const key of ["code", "message", "protocol", "reason"]) {
558
+ if (!Object.hasOwn(descriptors[key], "value")) return null;
559
+ }
560
+ const protocol = descriptors.protocol.value;
561
+ const code = descriptors.code.value;
562
+ const message = descriptors.message.value;
563
+ const reason = descriptors.reason.value;
564
+ if (protocol !== SPEECH_WORKER_ERROR_PROTOCOL) return null;
565
+ if (!Object.hasOwn(WORKER_ERROR_MESSAGES, code)) return null;
566
+ if (message !== WORKER_ERROR_MESSAGES[code]) return null;
567
+ const admission = workerErrorReasonAdmission(reason, role, op);
568
+ if (admission?.code !== code) return null;
66
569
  return Object.freeze({
67
570
  code,
68
- message: messages[code] ?? "The speech worker operation failed.",
571
+ message,
572
+ reason,
69
573
  });
70
574
  }
71
575
 
72
- function requiredText(value, label) {
576
+ function requiredText(
577
+ value,
578
+ label,
579
+ reason,
580
+ ) {
73
581
  if (typeof value !== "string" || !value.trim()) {
74
- throw workerError("ARCANE_AI_INVALID_REQUEST", `${label} is required.`);
582
+ throw workerError(
583
+ "ARCANE_AI_INVALID_REQUEST",
584
+ `${label} is required.`,
585
+ undefined,
586
+ reason,
587
+ );
75
588
  }
76
589
  return value.trim();
77
590
  }
78
591
 
592
+ function requiredSampleRate(value, label, fallback) {
593
+ const candidate = value ?? fallback;
594
+ if (!Number.isSafeInteger(candidate) || candidate < 1) {
595
+ throw workerError(
596
+ "ARCANE_AI_INVALID_REQUEST",
597
+ `${label} must be a positive safe integer.`,
598
+ undefined,
599
+ "speech-model-sample-rate-not-positive-safe-integer",
600
+ );
601
+ }
602
+ return candidate;
603
+ }
604
+
605
+ function graphConfiguration(configuration) {
606
+ return configuration?.runtime?.moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH;
607
+ }
608
+
609
+ function validateMaterializedFile(file, label) {
610
+ if (!file || typeof file !== "object" || Array.isArray(file)) {
611
+ throw workerError(
612
+ "ARCANE_AI_INVALID_REQUEST",
613
+ `${label} must be an object.`,
614
+ undefined,
615
+ "artifact-graph-materialized-file-not-object",
616
+ );
617
+ }
618
+ requiredText(file.path, `${label} path`, "artifact-graph-materialized-path-empty");
619
+ requiredText(file.sourceUrl, `${label} sourceUrl`, "artifact-graph-materialized-source-url-empty");
620
+ requiredText(file.moduleUrl, `${label} moduleUrl`, "artifact-graph-materialized-module-url-empty");
621
+ requiredText(file.mediaType, `${label} mediaType`, "artifact-graph-materialized-media-type-empty");
622
+ if (file.runtimeRequestUrls !== undefined && !Array.isArray(file.runtimeRequestUrls)) {
623
+ throw workerError(
624
+ "ARCANE_AI_INVALID_REQUEST",
625
+ `${label} runtimeRequestUrls must be an array.`,
626
+ undefined,
627
+ "artifact-graph-runtime-request-routes-not-array",
628
+ );
629
+ }
630
+ return file;
631
+ }
632
+
79
633
  function validateConfiguration(configuration, role) {
80
634
  if (!configuration || typeof configuration !== "object" || Array.isArray(configuration)) {
81
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Speech worker configuration is required.");
635
+ throw workerError(
636
+ "ARCANE_AI_INVALID_REQUEST",
637
+ "Speech worker configuration is required.",
638
+ undefined,
639
+ "speech-worker-configuration-missing",
640
+ );
82
641
  }
642
+ const moduleGraph = configuration.runtime?.moduleGraph;
83
643
  if (
84
644
  configuration.role !== role
85
645
  || configuration.runtime?.adapter !== ADAPTERS[role]
86
- || configuration.runtime?.moduleGraph !== "self-contained"
646
+ || (moduleGraph !== "self-contained" && moduleGraph !== ARTIFACT_GRAPH_MODULE_GRAPH)
87
647
  ) {
88
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Speech worker role and runtime adapter do not match.");
648
+ throw workerError(
649
+ "ARCANE_AI_INVALID_REQUEST",
650
+ "Speech worker role, runtime adapter, or module graph does not match.",
651
+ undefined,
652
+ "speech-worker-runtime-selection-mismatch",
653
+ );
89
654
  }
90
- requiredText(configuration.model?.id, "Speech model id");
91
- requiredText(configuration.model?.repository, "Speech model repository");
92
- requiredText(configuration.model?.revision, "Speech model revision");
93
- requiredText(configuration.runtime?.entry, "Speech runtime entry");
655
+ requiredText(configuration.model?.id, "Speech model id", "speech-worker-model-id-empty");
656
+ requiredText(configuration.model?.repository, "Speech model repository", "speech-worker-model-repository-empty");
657
+ requiredText(configuration.model?.revision, "Speech model revision", "speech-worker-model-revision-empty");
658
+ requiredText(configuration.runtime?.entry, "Speech runtime entry", "speech-worker-runtime-entry-empty");
94
659
  if (!Array.isArray(configuration.runtime?.files) || !Array.isArray(configuration.model?.files)) {
95
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Speech runtime and model files are required.");
660
+ throw workerError(
661
+ "ARCANE_AI_INVALID_REQUEST",
662
+ "Speech runtime and model files are required.",
663
+ undefined,
664
+ "speech-worker-materialized-files-missing",
665
+ );
96
666
  }
97
667
  const entry = configuration.runtime.files.find((file) =>
98
668
  file.path === configuration.runtime.entry);
99
669
  if (!entry?.moduleUrl) {
100
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Speech runtime entry was not materialized.");
670
+ throw workerError(
671
+ "ARCANE_AI_INVALID_REQUEST",
672
+ "Speech runtime entry was not materialized.",
673
+ undefined,
674
+ moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH
675
+ ? "artifact-graph-entrypoint-not-materialized"
676
+ : "speech-runtime-entrypoint-not-materialized",
677
+ );
678
+ }
679
+ if (moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH) {
680
+ const allFiles = [
681
+ ...configuration.runtime.files,
682
+ ...configuration.model.files,
683
+ ];
684
+ const paths = new Set();
685
+ for (let index = 0; index < allFiles.length; index += 1) {
686
+ const file = validateMaterializedFile(allFiles[index], `Speech file ${String(index)}`);
687
+ if (paths.has(file.path)) {
688
+ throw workerError(
689
+ "ARCANE_AI_INVALID_REQUEST",
690
+ "Speech materialized file paths must be unique.",
691
+ undefined,
692
+ "artifact-graph-materialized-path-ambiguous",
693
+ );
694
+ }
695
+ paths.add(file.path);
696
+ }
697
+ const runtimePaths = new Set(configuration.runtime.files.map((file) => file.path));
698
+ const security = configuration.security;
699
+ const securityChecks = security?.checks;
700
+ if (
701
+ !security
702
+ || typeof security !== "object"
703
+ || Array.isArray(security)
704
+ || typeof security.secure !== "boolean"
705
+ || !securityChecks
706
+ || typeof securityChecks !== "object"
707
+ || Array.isArray(securityChecks)
708
+ || typeof securityChecks.byteLength !== "boolean"
709
+ || typeof securityChecks.sha256 !== "boolean"
710
+ || typeof configuration.runtime.artifactGraphId !== "string"
711
+ || !/^[a-f0-9]{64}$/u.test(configuration.runtime.artifactGraphId)
712
+ || (
713
+ typeof configuration.runtime.guardCapability !== "string"
714
+ || !/^[a-f0-9]{64}$/u.test(configuration.runtime.guardCapability)
715
+ || !GRAPH_ADMISSIONS.has(configuration.runtime.artifactGraphAdmission)
716
+ || !configuration.runtime.edges
717
+ || typeof configuration.runtime.edges !== "object"
718
+ || !Array.isArray(configuration.runtime.transforms)
719
+ )
720
+ ) {
721
+ throw workerError(
722
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
723
+ "Artifact graph security, identity, admission, edges, and transforms are required.",
724
+ undefined,
725
+ "artifact-graph-worker-configuration-incomplete",
726
+ );
727
+ }
728
+ const wasm = configuration.runtime.onnxWasm;
729
+ if (
730
+ !wasm
731
+ || wasm.namespace !== ONNX_NAMESPACES[role]
732
+ || !runtimePaths.has(wasm.mjsPath)
733
+ || !runtimePaths.has(wasm.wasmPath)
734
+ ) {
735
+ throw workerError(
736
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
737
+ "The authenticated ONNX Runtime Web MJS/WASM pair is incomplete or uses the wrong namespace.",
738
+ undefined,
739
+ "artifact-graph-onnx-wasm-configuration-mismatch",
740
+ );
741
+ }
742
+ if (role === "tts" && wasm.numThreads !== undefined) {
743
+ throw workerError(
744
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
745
+ "Kokoro does not expose a verified numThreads configuration field.",
746
+ undefined,
747
+ "kokoro-env-num-threads-field-not-exposed",
748
+ );
749
+ }
750
+ if (
751
+ wasm.numThreads !== undefined
752
+ && (!Number.isSafeInteger(wasm.numThreads) || wasm.numThreads < 1)
753
+ ) {
754
+ throw workerError(
755
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
756
+ "Transformers numThreads must be a positive safe integer.",
757
+ undefined,
758
+ "transformers-env-num-threads-not-positive-safe-integer",
759
+ );
760
+ }
761
+ requiredText(configuration.model?.dtype, "Speech model dtype", "speech-worker-model-dtype-empty");
762
+ if (role === "stt") {
763
+ requiredSampleRate(configuration.model.inputSampleRate, "Whisper inputSampleRate");
764
+ } else {
765
+ requiredSampleRate(configuration.model.outputSampleRate, "Kokoro outputSampleRate");
766
+ requiredText(configuration.model.defaultVoice, "Kokoro defaultVoice", "tts-default-voice-empty");
767
+ if (!Array.isArray(configuration.model.voices) || configuration.model.voices.length < 1) {
768
+ throw workerError(
769
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
770
+ "Kokoro graph configuration requires the caller-owned voice inventory.",
771
+ undefined,
772
+ "artifact-graph-kokoro-voice-inventory-missing",
773
+ );
774
+ }
775
+ }
776
+ if (
777
+ configuration.runtime.negativeRuntimeRequestUrls !== undefined
778
+ && !Array.isArray(configuration.runtime.negativeRuntimeRequestUrls)
779
+ ) {
780
+ throw workerError(
781
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
782
+ "Artifact graph negative runtime request routes must be an array.",
783
+ undefined,
784
+ "artifact-graph-negative-request-routes-not-array",
785
+ );
786
+ }
101
787
  }
102
788
  return configuration;
103
789
  }
104
790
 
105
- function artifactMap(configuration) {
106
- const map = new Map();
107
- const materialized = new Set();
108
- for (const file of [
791
+ function absoluteRequestUrl(value, scope) {
792
+ const input = typeof value === "string" || value instanceof URL
793
+ ? String(value)
794
+ : value?.url;
795
+ try {
796
+ return new URL(input, scope.location?.href).href;
797
+ } catch {
798
+ throw workerError(
799
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
800
+ "The speech engine requested a malformed artifact URL.",
801
+ undefined,
802
+ "artifact-graph-runtime-request-url-malformed",
803
+ );
804
+ }
805
+ }
806
+
807
+ function createArtifactRoutes(scope, configuration) {
808
+ const positive = new Map();
809
+ const negative = new Set();
810
+ const files = [
109
811
  ...configuration.runtime.files,
110
812
  ...configuration.model.files,
111
- ]) {
112
- const sourceUrl = new URL(file.sourceUrl).href;
113
- map.set(sourceUrl, file.moduleUrl);
114
- materialized.add(file.moduleUrl);
813
+ ];
814
+
815
+ function addPositive(route, file) {
816
+ const absolute = absoluteRequestUrl(route, scope);
817
+ const existing = positive.get(absolute);
818
+ if (existing && existing.path !== file.path) {
819
+ throw workerError(
820
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
821
+ "An artifact graph runtime request route names more than one file.",
822
+ undefined,
823
+ "artifact-graph-runtime-request-route-ambiguous",
824
+ );
825
+ }
826
+ positive.set(absolute, file);
827
+ }
828
+
829
+ for (const file of files) {
830
+ addPositive(file.sourceUrl, file);
831
+ addPositive(file.moduleUrl, file);
832
+ for (const route of file.runtimeRequestUrls ?? []) addPositive(route, file);
833
+ }
834
+ for (const route of configuration.runtime.negativeRuntimeRequestUrls ?? []) {
835
+ const absolute = absoluteRequestUrl(route, scope);
836
+ if (positive.has(absolute) || negative.has(absolute)) {
837
+ throw workerError(
838
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
839
+ "Artifact graph positive and negative runtime request routes overlap.",
840
+ undefined,
841
+ "artifact-graph-runtime-request-route-ambiguous",
842
+ );
843
+ }
844
+ negative.add(absolute);
115
845
  }
116
- return Object.freeze({ map, materialized });
846
+ return Object.freeze({ files, positive, negative });
117
847
  }
118
848
 
119
- function installAuthorizedFetch(scope, configuration) {
120
- const original = scope.fetch?.bind(scope);
121
- if (typeof original !== "function") {
122
- throw workerError("ARCANE_AI_PROVIDER_UNAVAILABLE", "Browser fetch is unavailable in the speech worker.");
123
- }
124
- const allowed = artifactMap(configuration);
125
- scope.fetch = async function fetchAuthorizedSpeechArtifact(input, init) {
126
- const requested = typeof Request === "function" && input instanceof Request
127
- ? input.url
128
- : String(input);
129
- let absolute;
849
+ function requestMethod(input, init, scope) {
850
+ const RequestConstructor = scope.Request ?? globalThis.Request;
851
+ const inherited = typeof RequestConstructor === "function" && input instanceof RequestConstructor
852
+ ? input.method
853
+ : "GET";
854
+ return String(init?.method ?? inherited ?? "GET").toUpperCase();
855
+ }
856
+
857
+ function installScopeValue(scope, name, value, reason) {
858
+ const descriptor = Object.getOwnPropertyDescriptor(scope, name);
859
+ if (descriptor?.configurable === false && descriptor.writable !== true) {
860
+ throw workerError(
861
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
862
+ `The speech Worker cannot isolate globalThis.${name}.`,
863
+ undefined,
864
+ reason,
865
+ );
866
+ }
867
+ try {
868
+ if (descriptor?.configurable === false) {
869
+ scope[name] = value;
870
+ } else {
871
+ Object.defineProperty(scope, name, {
872
+ configurable: true,
873
+ enumerable: descriptor?.enumerable ?? true,
874
+ writable: false,
875
+ value,
876
+ });
877
+ }
878
+ } catch (error) {
879
+ throw workerError(
880
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
881
+ `The speech Worker could not isolate globalThis.${name}.`,
882
+ error,
883
+ reason,
884
+ );
885
+ }
886
+ return function restoreScopeValue() {
887
+ if (descriptor) Object.defineProperty(scope, name, descriptor);
888
+ else delete scope[name];
889
+ };
890
+ }
891
+
892
+ function restoreScopeValues(restores) {
893
+ for (const restore of restores.splice(0).reverse()) {
130
894
  try {
131
- absolute = new URL(requested, scope.location?.href).href;
895
+ restore();
132
896
  } catch {
133
- throw workerError("ARCANE_AI_UNDECLARED_ARTIFACT", "The speech engine requested an invalid artifact URL.");
897
+ // Terminating the owning Worker remains the final isolation boundary.
134
898
  }
135
- const replacement = allowed.map.get(absolute);
136
- if (replacement) return original(replacement, init);
137
- if (allowed.materialized.has(absolute)) return original(absolute, init);
899
+ }
900
+ }
901
+
902
+ function installObjectValue(object, name, value, label, reason) {
903
+ const descriptor = Object.getOwnPropertyDescriptor(object, name);
904
+ if (descriptor?.configurable === false && descriptor.writable !== true) {
138
905
  throw workerError(
139
- "ARCANE_AI_UNDECLARED_ARTIFACT",
140
- "The speech engine requested an artifact outside its admitted file map.",
906
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
907
+ `The speech Worker cannot isolate ${label}.`,
908
+ undefined,
909
+ reason,
141
910
  );
911
+ }
912
+ try {
913
+ Object.defineProperty(object, name, {
914
+ configurable: true,
915
+ enumerable: descriptor?.enumerable ?? true,
916
+ writable: false,
917
+ value,
918
+ });
919
+ } catch (error) {
920
+ throw workerError(
921
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
922
+ `The speech Worker could not isolate ${label}.`,
923
+ error,
924
+ reason,
925
+ );
926
+ }
927
+ return function restoreObjectValue() {
928
+ if (descriptor) Object.defineProperty(object, name, descriptor);
929
+ else delete object[name];
142
930
  };
143
- return function restoreSpeechWorkerFetch() {
144
- scope.fetch = original;
145
- };
146
931
  }
147
932
 
148
- function installCacheIsolation(scope) {
149
- const ownDescriptor = Object.getOwnPropertyDescriptor(scope, "caches");
150
- if (ownDescriptor && ownDescriptor.configurable === false) {
933
+ function createRouteReader(scope, configuration, originalFetch) {
934
+ const routes = createArtifactRoutes(scope, configuration);
935
+ const ResponseConstructor = scope.Response ?? globalThis.Response;
936
+
937
+ async function responseFor(resolution, signal) {
938
+ if (resolution.kind === "negative") {
939
+ if (typeof ResponseConstructor !== "function") {
940
+ throw workerError(
941
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
942
+ "Browser Response is unavailable in the speech Worker.",
943
+ undefined,
944
+ "artifact-graph-negative-response-constructor-unavailable",
945
+ );
946
+ }
947
+ return new ResponseConstructor(null, {
948
+ status: 404,
949
+ statusText: "Not Found",
950
+ headers: { "cache-control": "no-store" },
951
+ });
952
+ }
953
+ return originalFetch(resolution.file.moduleUrl, {
954
+ method: "GET",
955
+ credentials: "omit",
956
+ redirect: "error",
957
+ signal,
958
+ });
959
+ }
960
+
961
+ function resolve(input) {
962
+ const absolute = absoluteRequestUrl(input, scope);
963
+ const file = routes.positive.get(absolute);
964
+ if (file) return Object.freeze({ kind: "file", absolute, file });
965
+ if (routes.negative.has(absolute)) {
966
+ return Object.freeze({ kind: "negative", absolute, file: null });
967
+ }
968
+ return null;
969
+ }
970
+
971
+ function cacheForPaths(targetPaths) {
972
+ const admittedPaths = new Set(targetPaths);
973
+ return Object.freeze({
974
+ async match(input) {
975
+ const resolution = resolve(input);
976
+ if (
977
+ !resolution
978
+ || resolution.kind !== "file"
979
+ || !admittedPaths.has(resolution.file.path)
980
+ ) {
981
+ throw workerError(
982
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
983
+ "The runtime cache read target is outside its admitted artifact graph edge.",
984
+ undefined,
985
+ "artifact-graph-cache-read-target-undeclared",
986
+ );
987
+ }
988
+ return responseFor(resolution);
989
+ },
990
+ async put() {
991
+ throw workerError(
992
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
993
+ "Speech runtime cache writes are disabled; DBOPFS is the sole durable artifact store.",
994
+ undefined,
995
+ "artifact-graph-cache-write-rejected",
996
+ );
997
+ },
998
+ async add() {
999
+ throw workerError(
1000
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
1001
+ "Speech runtime cache writes are disabled; DBOPFS is the sole durable artifact store.",
1002
+ undefined,
1003
+ "artifact-graph-cache-write-rejected",
1004
+ );
1005
+ },
1006
+ async addAll() {
1007
+ throw workerError(
1008
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
1009
+ "Speech runtime cache writes are disabled; DBOPFS is the sole durable artifact store.",
1010
+ undefined,
1011
+ "artifact-graph-cache-write-rejected",
1012
+ );
1013
+ },
1014
+ async delete() {
1015
+ return false;
1016
+ },
1017
+ async keys() {
1018
+ return Object.freeze([]);
1019
+ },
1020
+ });
1021
+ }
1022
+
1023
+ const cache = cacheForPaths(routes.files.map((file) => file.path));
1024
+ return Object.freeze({ cache, cacheForPaths, resolve, responseFor, routes });
1025
+ }
1026
+
1027
+ function installLegacyAuthorizedFetch(scope, configuration) {
1028
+ const original = scope.fetch?.bind(scope);
1029
+ if (typeof original !== "function") {
151
1030
  throw workerError(
152
1031
  "ARCANE_AI_PROVIDER_UNAVAILABLE",
153
- "The speech worker cannot isolate the browser cache API.",
1032
+ "Browser fetch is unavailable in the speech Worker.",
1033
+ undefined,
1034
+ "speech-worker-fetch-unavailable",
154
1035
  );
155
1036
  }
1037
+ const reader = createRouteReader(scope, configuration, original);
1038
+ const authorized = async function fetchAuthorizedSpeechArtifact(input, init) {
1039
+ if (requestMethod(input, init, scope) !== "GET") {
1040
+ throw workerError(
1041
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
1042
+ "Legacy speech artifacts admit only GET requests.",
1043
+ undefined,
1044
+ "speech-worker-artifact-request-method-rejected",
1045
+ );
1046
+ }
1047
+ const resolution = reader.resolve(input);
1048
+ if (!resolution) {
1049
+ throw workerError(
1050
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
1051
+ "The speech engine requested an artifact outside its admitted file map.",
1052
+ undefined,
1053
+ "speech-worker-artifact-request-undeclared",
1054
+ );
1055
+ }
1056
+ return reader.responseFor(resolution, init?.signal);
1057
+ };
1058
+ const restore = installScopeValue(
1059
+ scope,
1060
+ "fetch",
1061
+ authorized,
1062
+ "speech-worker-fetch-isolation-unavailable",
1063
+ );
1064
+ return Object.freeze({ cache: null, cleanup: restore });
1065
+ }
1066
+
1067
+ function installDeniedCacheIsolation(scope) {
156
1068
  const denied = Object.freeze({
157
1069
  async open() {
158
1070
  throw workerError(
159
1071
  "ARCANE_AI_UNDECLARED_ARTIFACT",
160
1072
  "Speech runtime cache access is disabled; DBOPFS admission is the sole artifact source.",
1073
+ undefined,
1074
+ "speech-worker-cache-open-rejected",
161
1075
  );
162
1076
  },
163
1077
  async match() {
164
1078
  throw workerError(
165
1079
  "ARCANE_AI_UNDECLARED_ARTIFACT",
166
1080
  "Speech runtime cache access is disabled; DBOPFS admission is the sole artifact source.",
1081
+ undefined,
1082
+ "speech-worker-cache-match-rejected",
167
1083
  );
168
1084
  },
169
1085
  async has() {
@@ -176,54 +1092,915 @@ function installCacheIsolation(scope) {
176
1092
  return false;
177
1093
  },
178
1094
  });
179
- try {
180
- Object.defineProperty(scope, "caches", {
181
- configurable: true,
182
- enumerable: ownDescriptor?.enumerable ?? true,
183
- writable: false,
184
- value: denied,
1095
+ return installScopeValue(
1096
+ scope,
1097
+ "caches",
1098
+ denied,
1099
+ "speech-worker-cache-isolation-unavailable",
1100
+ );
1101
+ }
1102
+
1103
+ function edgeKey(modulePath, occurrence) {
1104
+ return `${modulePath}\u0000${String(occurrence)}`;
1105
+ }
1106
+
1107
+ function edgeMap(edges, label) {
1108
+ if (!Array.isArray(edges)) {
1109
+ throw workerError(
1110
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1111
+ `Artifact graph ${label} edges must be an array.`,
1112
+ undefined,
1113
+ `artifact-graph-${label}-edges-not-array`,
1114
+ );
1115
+ }
1116
+ const map = new Map();
1117
+ for (const edge of edges) {
1118
+ if (!edge || typeof edge !== "object" || Array.isArray(edge)) {
1119
+ throw workerError(
1120
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1121
+ `Artifact graph ${label} edge must be an object.`,
1122
+ undefined,
1123
+ `artifact-graph-${label}-edge-not-object`,
1124
+ );
1125
+ }
1126
+ if (typeof edge.modulePath !== "string" || !edge.modulePath) {
1127
+ throw workerError(
1128
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1129
+ `Artifact graph ${label} edge modulePath must be nonempty.`,
1130
+ undefined,
1131
+ `artifact-graph-${label}-edge-module-path-empty`,
1132
+ );
1133
+ }
1134
+ if (!Number.isSafeInteger(edge.occurrence) || edge.occurrence < 1) {
1135
+ throw workerError(
1136
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1137
+ `Artifact graph ${label} edge occurrence must be a positive safe integer.`,
1138
+ undefined,
1139
+ `artifact-graph-${label}-edge-occurrence-not-positive-safe-integer`,
1140
+ );
1141
+ }
1142
+ if (edge.edgePolicy !== "artifact-targets-admitted"
1143
+ && edge.edgePolicy !== "inactive-runtime-branch-rejected") {
1144
+ throw workerError(
1145
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1146
+ `Artifact graph ${label} edge policy is not admitted.`,
1147
+ undefined,
1148
+ `artifact-graph-${label}-edge-policy-not-admitted`,
1149
+ );
1150
+ }
1151
+ const key = edgeKey(edge.modulePath, edge.occurrence);
1152
+ if (map.has(key)) {
1153
+ throw workerError(
1154
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1155
+ `Artifact graph ${label} edge identity is ambiguous.`,
1156
+ undefined,
1157
+ `artifact-graph-${label}-edge-identity-ambiguous`,
1158
+ );
1159
+ }
1160
+ map.set(key, edge);
1161
+ }
1162
+ return map;
1163
+ }
1164
+
1165
+ function transformSet(transforms, kind) {
1166
+ const result = new Set();
1167
+ for (const transform of transforms) {
1168
+ if (!ARTIFACT_GRAPH_TRANSFORM_KINDS.has(transform?.kind)) {
1169
+ throw workerError(
1170
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1171
+ "Artifact graph Worker configuration contains a transform kind that is not admitted.",
1172
+ undefined,
1173
+ "artifact-graph-transform-kind-not-admitted",
1174
+ );
1175
+ }
1176
+ if (transform?.kind !== kind) continue;
1177
+ if (typeof transform.modulePath !== "string" || !transform.modulePath) {
1178
+ throw workerError(
1179
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1180
+ `Artifact graph ${kind} transform modulePath must be nonempty.`,
1181
+ undefined,
1182
+ "artifact-graph-transform-module-path-empty",
1183
+ );
1184
+ }
1185
+ if (!Number.isSafeInteger(transform.occurrence) || transform.occurrence < 1) {
1186
+ throw workerError(
1187
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1188
+ `Artifact graph ${kind} transform occurrence must be a positive safe integer.`,
1189
+ undefined,
1190
+ "artifact-graph-transform-occurrence-not-positive-safe-integer",
1191
+ );
1192
+ }
1193
+ const key = edgeKey(transform.modulePath, transform.occurrence);
1194
+ if (result.has(key)) {
1195
+ throw workerError(
1196
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1197
+ `Artifact graph ${kind} transform identity is ambiguous.`,
1198
+ undefined,
1199
+ "artifact-graph-transform-identity-ambiguous",
1200
+ );
1201
+ }
1202
+ result.add(key);
1203
+ }
1204
+ return result;
1205
+ }
1206
+
1207
+ function typedArrayIntrinsics(scope) {
1208
+ const constructors = new Map();
1209
+ for (const name of [
1210
+ "BigInt64Array",
1211
+ "BigUint64Array",
1212
+ "Float32Array",
1213
+ "Float64Array",
1214
+ "Int8Array",
1215
+ "Int16Array",
1216
+ "Int32Array",
1217
+ "Uint8Array",
1218
+ "Uint8ClampedArray",
1219
+ "Uint16Array",
1220
+ "Uint32Array",
1221
+ ]) {
1222
+ const Constructor = scope[name] ?? globalThis[name];
1223
+ if (typeof Constructor === "function" && Constructor.prototype) {
1224
+ constructors.set(Constructor.prototype, Constructor);
1225
+ }
1226
+ }
1227
+ const ArrayBufferConstructor = scope.ArrayBuffer ?? globalThis.ArrayBuffer;
1228
+ const DataViewConstructor = scope.DataView ?? globalThis.DataView;
1229
+ if (typeof ArrayBufferConstructor?.isView !== "function") {
1230
+ throw workerError(
1231
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1232
+ "Authenticated artifact graph typed-array validation is unavailable.",
1233
+ undefined,
1234
+ "artifact-graph-typed-array-validation-unavailable",
1235
+ );
1236
+ }
1237
+ return Object.freeze({
1238
+ constructors,
1239
+ dataViewPrototype: DataViewConstructor?.prototype ?? null,
1240
+ getPrototypeOf: Object.getPrototypeOf,
1241
+ hasOwn: Object.hasOwn,
1242
+ isView: ArrayBufferConstructor.isView.bind(ArrayBufferConstructor),
1243
+ });
1244
+ }
1245
+
1246
+ function runtimeFileMap(configuration) {
1247
+ return new Map(configuration.runtime.files.map((file) => [file.path, file]));
1248
+ }
1249
+
1250
+ function targetRecords(edge) {
1251
+ if (Array.isArray(edge.targets)) return edge.targets;
1252
+ if (Array.isArray(edge.targetPaths)) {
1253
+ return edge.targetPaths.map((targetPath) => ({ targetPath }));
1254
+ }
1255
+ return [];
1256
+ }
1257
+
1258
+ function targetForRequest(edge, requested, files, sourceFile) {
1259
+ const value = String(requested);
1260
+ for (const target of targetRecords(edge)) {
1261
+ const file = files.get(target?.targetPath);
1262
+ if (!file) continue;
1263
+ if (target.match === "exact-runtime-specifier") {
1264
+ if (value === target.exactSpecifier) return file;
1265
+ continue;
1266
+ }
1267
+ if (target.match === "self-module-url") {
1268
+ if (file.path === sourceFile?.path && value === sourceFile.moduleUrl) return file;
1269
+ continue;
1270
+ }
1271
+ if (target.match === "materialized-module-url" && value === file.moduleUrl) {
1272
+ return file;
1273
+ }
1274
+ }
1275
+ return null;
1276
+ }
1277
+
1278
+ function nestedWorkerUrl(role) {
1279
+ const url = new URL(
1280
+ role === "stt"
1281
+ ? "./browser-whisper-worker.mjs"
1282
+ : "./browser-kokoro-worker.mjs",
1283
+ import.meta.url,
1284
+ );
1285
+ url.searchParams.set("arcaneSpeechWorkerMode", "artifact-module-worker");
1286
+ return url;
1287
+ }
1288
+
1289
+ function nestedWorkerFailureEvent(scope, error) {
1290
+ const ErrorEventConstructor = scope.ErrorEvent ?? globalThis.ErrorEvent;
1291
+ if (typeof ErrorEventConstructor === "function") {
1292
+ return new ErrorEventConstructor("error", {
1293
+ error,
1294
+ message: error.message,
1295
+ cancelable: true,
185
1296
  });
1297
+ }
1298
+ const EventConstructor = scope.Event ?? globalThis.Event;
1299
+ const event = new EventConstructor("error", { cancelable: true });
1300
+ Object.defineProperties(event, {
1301
+ error: { value: error },
1302
+ message: { value: error.message },
1303
+ });
1304
+ return event;
1305
+ }
1306
+
1307
+ function createArtifactGraphGuard(scope, configuration, role, reader, originalWorker) {
1308
+ const files = runtimeFileMap(configuration);
1309
+ const edges = configuration.runtime.edges;
1310
+ const imports = edgeMap(edges.dynamicImports ?? [], "dynamic-import");
1311
+ const fetches = edgeMap(edges.fetches ?? [], "fetch");
1312
+ const workers = edgeMap(edges.moduleWorkers ?? [], "module-worker");
1313
+ const cacheOpens = edgeMap(edges.cacheOpens ?? [], "cache-open");
1314
+ const typedArrayConstructors = transformSet(
1315
+ configuration.runtime.transforms,
1316
+ "typed-array-constructor",
1317
+ );
1318
+ transformSet(configuration.runtime.transforms, "function-return-this-to-global-this");
1319
+ const typedArrays = typedArrayIntrinsics(scope);
1320
+ const guardCapability = configuration.runtime.guardCapability;
1321
+ const nestedWorkers = new Set();
1322
+
1323
+ function assertGuardCapability(candidate) {
1324
+ if (candidate === guardCapability) return;
1325
+ throw workerError(
1326
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1327
+ "Authenticated artifact graph runtime guard capability did not match.",
1328
+ undefined,
1329
+ "artifact-graph-guard-capability-mismatch",
1330
+ );
1331
+ }
1332
+
1333
+ function declaredEdge(map, modulePath, occurrence, code, reason) {
1334
+ const edge = map.get(edgeKey(modulePath, occurrence));
1335
+ if (edge?.edgePolicy === "artifact-targets-admitted") return edge;
1336
+ if (edge?.edgePolicy === "inactive-runtime-branch-rejected") {
1337
+ throw workerError(
1338
+ code,
1339
+ "The runtime entered an artifact graph branch declared inactive.",
1340
+ undefined,
1341
+ `${reason}-inactive-runtime-branch-entered`,
1342
+ );
1343
+ }
1344
+ throw workerError(
1345
+ code,
1346
+ "The runtime used an undeclared authenticated artifact graph edge.",
1347
+ undefined,
1348
+ reason,
1349
+ );
1350
+ }
1351
+
1352
+ function fetchEdgeAllows(edge, resolution) {
1353
+ if (resolution.kind === "negative") {
1354
+ return (edge.negativeRuntimeRequestUrls ?? [])
1355
+ .map((route) => absoluteRequestUrl(route, scope))
1356
+ .includes(resolution.absolute);
1357
+ }
1358
+ const paths = new Set([
1359
+ ...(edge.targetPaths ?? []),
1360
+ ...targetRecords(edge).map((target) => target?.targetPath),
1361
+ ]);
1362
+ if (!paths.has(resolution.file.path)) return false;
1363
+ if (resolution.absolute === absoluteRequestUrl(resolution.file.moduleUrl, scope)) {
1364
+ return edge.allowMaterializedUrls === true;
1365
+ }
1366
+ return true;
1367
+ }
1368
+
1369
+ const guard = Object.freeze({
1370
+ protocol: "arcane-ai-browser-speech-artifact-graph-runtime/1",
1371
+
1372
+ async dynamicImport(capability, modulePath, occurrence, specifier) {
1373
+ assertGuardCapability(capability);
1374
+ const edge = declaredEdge(
1375
+ imports,
1376
+ modulePath,
1377
+ occurrence,
1378
+ "ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED",
1379
+ "artifact-graph-dynamic-import-edge-undeclared",
1380
+ );
1381
+ const sourceFile = files.get(modulePath);
1382
+ const target = targetForRequest(edge, specifier, files, sourceFile);
1383
+ if (!target) {
1384
+ throw workerError(
1385
+ "ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED",
1386
+ "The runtime dynamic import target is outside its admitted artifact graph edge.",
1387
+ undefined,
1388
+ "artifact-graph-dynamic-import-target-undeclared",
1389
+ );
1390
+ }
1391
+ return import(target.moduleUrl);
1392
+ },
1393
+
1394
+ async fetch(capability, modulePath, occurrence, input, init) {
1395
+ assertGuardCapability(capability);
1396
+ const edge = declaredEdge(
1397
+ fetches,
1398
+ modulePath,
1399
+ occurrence,
1400
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
1401
+ "artifact-graph-fetch-edge-undeclared",
1402
+ );
1403
+ const method = requestMethod(input, init, scope);
1404
+ if (!(edge.methods ?? ["GET"]).includes(method)) {
1405
+ throw workerError(
1406
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
1407
+ "The runtime fetch method is outside its admitted artifact graph edge.",
1408
+ undefined,
1409
+ "artifact-graph-fetch-method-undeclared",
1410
+ );
1411
+ }
1412
+ const resolution = reader.resolve(input);
1413
+ if (!resolution || !fetchEdgeAllows(edge, resolution)) {
1414
+ throw workerError(
1415
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
1416
+ "The runtime fetch target is outside its admitted artifact graph edge.",
1417
+ undefined,
1418
+ "artifact-graph-fetch-target-undeclared",
1419
+ );
1420
+ }
1421
+ return reader.responseFor(resolution, init?.signal);
1422
+ },
1423
+
1424
+ async openCache(capability, modulePath, occurrence, cacheName) {
1425
+ assertGuardCapability(capability);
1426
+ const edge = declaredEdge(
1427
+ cacheOpens,
1428
+ modulePath,
1429
+ occurrence,
1430
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
1431
+ "artifact-graph-cache-open-edge-undeclared",
1432
+ );
1433
+ if (cacheName !== edge.cacheName) {
1434
+ throw workerError(
1435
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
1436
+ "The runtime cache name is outside its admitted artifact graph edge.",
1437
+ undefined,
1438
+ "artifact-graph-cache-name-mismatch",
1439
+ );
1440
+ }
1441
+ return reader.cacheForPaths(edge.targetPaths);
1442
+ },
1443
+
1444
+ createWorker(capability, modulePath, occurrence, specifier, options = {}) {
1445
+ assertGuardCapability(capability);
1446
+ const edge = declaredEdge(
1447
+ workers,
1448
+ modulePath,
1449
+ occurrence,
1450
+ "ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
1451
+ "artifact-graph-module-worker-edge-undeclared",
1452
+ );
1453
+ const sourceFile = files.get(modulePath);
1454
+ const target = targetForRequest(edge, specifier, files, sourceFile);
1455
+ if (!target) {
1456
+ throw workerError(
1457
+ "ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
1458
+ "The runtime module Worker target is outside its admitted artifact graph edge.",
1459
+ undefined,
1460
+ "artifact-graph-module-worker-target-undeclared",
1461
+ );
1462
+ }
1463
+ if (options?.type !== "module") {
1464
+ throw workerError(
1465
+ "ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
1466
+ "Authenticated artifact graph child Workers must be module Workers.",
1467
+ undefined,
1468
+ "artifact-graph-module-worker-type-mismatch",
1469
+ );
1470
+ }
1471
+ if (typeof originalWorker !== "function") {
1472
+ throw workerError(
1473
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1474
+ "Nested browser module Workers are unavailable.",
1475
+ undefined,
1476
+ "artifact-graph-module-worker-constructor-unavailable",
1477
+ );
1478
+ }
1479
+ const worker = new originalWorker(nestedWorkerUrl(role), {
1480
+ type: "module",
1481
+ name: typeof options.name === "string" ? options.name : "arcane-speech-artifact-module",
1482
+ });
1483
+ nestedWorkers.add(worker);
1484
+ const onBootstrapMessage = (event) => {
1485
+ if (event.data?.protocol !== NESTED_WORKER_PROTOCOL
1486
+ || event.data?.event !== "artifact-module-worker-bootstrap-rejected") return;
1487
+ event.stopImmediatePropagation?.();
1488
+ const admitted = normalizeSpeechWorkerErrorEnvelope(event.data.error, role, "load");
1489
+ const failure = admitted
1490
+ ? workerError(admitted.code, admitted.message, undefined, admitted.reason)
1491
+ : workerError(
1492
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
1493
+ "The authenticated artifact module Worker error envelope was rejected.",
1494
+ undefined,
1495
+ "artifact-graph-module-worker-error-envelope-rejected",
1496
+ );
1497
+ worker.dispatchEvent?.(nestedWorkerFailureEvent(scope, failure));
1498
+ };
1499
+ worker.addEventListener?.("message", onBootstrapMessage);
1500
+ try {
1501
+ worker.postMessage({
1502
+ protocol: NESTED_WORKER_PROTOCOL,
1503
+ op: "initialize-authenticated-artifact-module-worker",
1504
+ role,
1505
+ targetPath: target.path,
1506
+ configuration,
1507
+ });
1508
+ } catch (error) {
1509
+ nestedWorkers.delete(worker);
1510
+ worker.terminate();
1511
+ throw workerError(
1512
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
1513
+ "The authenticated artifact module Worker initialization message was rejected.",
1514
+ error,
1515
+ "artifact-graph-module-worker-initialization-message-rejected",
1516
+ );
1517
+ }
1518
+ return worker;
1519
+ },
1520
+
1521
+ typedArrayConstructor(capability, modulePath, occurrence, receiver) {
1522
+ assertGuardCapability(capability);
1523
+ if (!typedArrayConstructors.has(edgeKey(modulePath, occurrence))) {
1524
+ throw workerError(
1525
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1526
+ "The runtime used an undeclared typed-array constructor transform.",
1527
+ undefined,
1528
+ "artifact-graph-typed-array-constructor-transform-undeclared",
1529
+ );
1530
+ }
1531
+ if (
1532
+ !typedArrays.isView(receiver)
1533
+ || typedArrays.getPrototypeOf(receiver) === typedArrays.dataViewPrototype
1534
+ || typedArrays.hasOwn(receiver, "constructor")
1535
+ ) {
1536
+ throw workerError(
1537
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1538
+ "The typed-array constructor transform received an unauthenticated receiver.",
1539
+ undefined,
1540
+ "artifact-graph-typed-array-constructor-receiver-not-typed-array",
1541
+ );
1542
+ }
1543
+ const Constructor = typedArrays.constructors.get(
1544
+ typedArrays.getPrototypeOf(receiver),
1545
+ );
1546
+ if (typeof Constructor !== "function") {
1547
+ throw workerError(
1548
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1549
+ "The typed-array constructor transform receiver does not use an intrinsic prototype.",
1550
+ undefined,
1551
+ "artifact-graph-typed-array-constructor-intrinsic-mismatch",
1552
+ );
1553
+ }
1554
+ return Constructor;
1555
+ },
1556
+ });
1557
+
1558
+ return Object.freeze({
1559
+ guard,
1560
+ transformersCache: (() => {
1561
+ const edgesForTransformers = [...cacheOpens.values()].filter((edge) =>
1562
+ edge.edgePolicy === "artifact-targets-admitted"
1563
+ && edge.cacheName === "transformers-cache");
1564
+ if (edgesForTransformers.length === 0) return null;
1565
+ if (edgesForTransformers.length !== 1) {
1566
+ throw workerError(
1567
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1568
+ "The Transformers custom cache edge is ambiguous.",
1569
+ undefined,
1570
+ "artifact-graph-transformers-cache-edge-ambiguous",
1571
+ );
1572
+ }
1573
+ return reader.cacheForPaths(edgesForTransformers[0].targetPaths);
1574
+ })(),
1575
+ cleanup() {
1576
+ for (const worker of nestedWorkers) {
1577
+ try {
1578
+ worker.terminate();
1579
+ } catch {
1580
+ // The owning speech Worker is already being torn down.
1581
+ }
1582
+ }
1583
+ nestedWorkers.clear();
1584
+ },
1585
+ });
1586
+ }
1587
+
1588
+ function installDynamicCodeConstructorIsolation() {
1589
+ const rejectingConstructor = function rejectArtifactGraphDynamicCodeConstructor() {
1590
+ throw workerError(
1591
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1592
+ "Authenticated artifact graph runtime code cannot construct executable strings.",
1593
+ undefined,
1594
+ "artifact-graph-dynamic-code-constructor-rejected",
1595
+ );
1596
+ };
1597
+ const prototypes = new Set([
1598
+ Object.getPrototypeOf(function artifactGraphFunctionProbe() {}),
1599
+ Object.getPrototypeOf(async function artifactGraphAsyncFunctionProbe() {}),
1600
+ Object.getPrototypeOf(function* artifactGraphGeneratorFunctionProbe() {}),
1601
+ Object.getPrototypeOf(async function* artifactGraphAsyncGeneratorFunctionProbe() {}),
1602
+ ]);
1603
+ const restores = [];
1604
+ try {
1605
+ for (const prototype of prototypes) {
1606
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, "constructor");
1607
+ if (!descriptor || (descriptor.configurable === false && descriptor.writable !== true)) {
1608
+ throw workerError(
1609
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1610
+ "The Worker cannot isolate a dynamic-code constructor prototype.",
1611
+ undefined,
1612
+ "artifact-graph-dynamic-code-constructor-isolation-unavailable",
1613
+ );
1614
+ }
1615
+ Object.defineProperty(prototype, "constructor", {
1616
+ ...descriptor,
1617
+ value: rejectingConstructor,
1618
+ });
1619
+ restores.push(() => Object.defineProperty(prototype, "constructor", descriptor));
1620
+ }
1621
+ } catch (error) {
1622
+ restoreScopeValues(restores);
1623
+ throw error;
1624
+ }
1625
+ return () => restoreScopeValues(restores);
1626
+ }
1627
+
1628
+ function installStringTimerIsolation(scope, name) {
1629
+ const original = scope[name];
1630
+ if (typeof original !== "function") return () => undefined;
1631
+ const invoke = original.bind(scope);
1632
+ return installScopeValue(
1633
+ scope,
1634
+ name,
1635
+ function authenticatedArtifactGraphTimer(callback, ...arguments_) {
1636
+ if (typeof callback !== "function") {
1637
+ throw workerError(
1638
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1639
+ `Authenticated artifact graph ${name} requires a function callback.`,
1640
+ undefined,
1641
+ `artifact-graph-${name.toLowerCase()}-string-callback-rejected`,
1642
+ );
1643
+ }
1644
+ return invoke(callback, ...arguments_);
1645
+ },
1646
+ `artifact-graph-${name.toLowerCase()}-isolation-unavailable`,
1647
+ );
1648
+ }
1649
+
1650
+ function installArtifactGraphEnvironment(scope, configuration, role) {
1651
+ const originalFetch = scope.fetch?.bind(scope);
1652
+ const originalWorker = scope.Worker;
1653
+ const strictSecurity = configuration.security?.secure === true;
1654
+ if (typeof originalFetch !== "function") {
1655
+ throw workerError(
1656
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1657
+ "Browser fetch is unavailable in the speech Worker.",
1658
+ undefined,
1659
+ "artifact-graph-fetch-constructor-unavailable",
1660
+ );
1661
+ }
1662
+ if (GRAPH_GUARD_NAME in scope) {
1663
+ throw workerError(
1664
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1665
+ "The authenticated artifact graph guard global already exists.",
1666
+ undefined,
1667
+ "artifact-graph-guard-global-collision",
1668
+ );
1669
+ }
1670
+ const reader = createRouteReader(scope, configuration, originalFetch);
1671
+ const graphGuard = createArtifactGraphGuard(
1672
+ scope,
1673
+ configuration,
1674
+ role,
1675
+ reader,
1676
+ originalWorker,
1677
+ );
1678
+ const restores = [];
1679
+ try {
1680
+ if (strictSecurity) {
1681
+ restores.push(installDynamicCodeConstructorIsolation());
1682
+ restores.push(installStringTimerIsolation(scope, "setInterval"));
1683
+ restores.push(installStringTimerIsolation(scope, "setTimeout"));
1684
+ if ("indexedDB" in scope) {
1685
+ restores.push(installScopeValue(
1686
+ scope,
1687
+ "indexedDB",
1688
+ undefined,
1689
+ "artifact-graph-indexeddb-isolation-unavailable",
1690
+ ));
1691
+ }
1692
+ if (scope.navigator && "storage" in scope.navigator) {
1693
+ restores.push(installObjectValue(
1694
+ scope.navigator,
1695
+ "storage",
1696
+ undefined,
1697
+ "WorkerNavigator.storage",
1698
+ "artifact-graph-opfs-isolation-unavailable",
1699
+ ));
1700
+ }
1701
+ }
1702
+ restores.push(installScopeValue(
1703
+ scope,
1704
+ GRAPH_GUARD_NAME,
1705
+ graphGuard.guard,
1706
+ "artifact-graph-guard-global-definition-rejected",
1707
+ ));
1708
+ if (strictSecurity) {
1709
+ restores.push(installScopeValue(
1710
+ scope,
1711
+ "fetch",
1712
+ async function rejectUntransformedArtifactGraphFetch() {
1713
+ throw workerError(
1714
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
1715
+ "Artifact graph fetch must use its declared transformed edge.",
1716
+ undefined,
1717
+ "artifact-graph-fetch-guard-bypassed",
1718
+ );
1719
+ },
1720
+ "artifact-graph-fetch-isolation-unavailable",
1721
+ ));
1722
+ const cacheStorage = Object.freeze({
1723
+ async open() {
1724
+ throw workerError(
1725
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
1726
+ "Artifact graph CacheStorage.open must use its declared transformed edge.",
1727
+ undefined,
1728
+ "artifact-graph-cache-open-guard-bypassed",
1729
+ );
1730
+ },
1731
+ async match() {
1732
+ throw workerError(
1733
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
1734
+ "Artifact graph CacheStorage.match requires a declared cache-open edge.",
1735
+ undefined,
1736
+ "artifact-graph-cache-match-guard-bypassed",
1737
+ );
1738
+ },
1739
+ async has() {
1740
+ return true;
1741
+ },
1742
+ async keys() {
1743
+ return Object.freeze([]);
1744
+ },
1745
+ async delete() {
1746
+ return false;
1747
+ },
1748
+ });
1749
+ restores.push(installScopeValue(
1750
+ scope,
1751
+ "caches",
1752
+ cacheStorage,
1753
+ "artifact-graph-cache-isolation-unavailable",
1754
+ ));
1755
+
1756
+ const deniedCapabilities = [
1757
+ "BroadcastChannel",
1758
+ "EventSource",
1759
+ "Function",
1760
+ "RTCPeerConnection",
1761
+ "ShadowRealm",
1762
+ "SharedWorker",
1763
+ "WebSocket",
1764
+ "WebSocketStream",
1765
+ "WebTransport",
1766
+ "Worker",
1767
+ "XMLHttpRequest",
1768
+ "eval",
1769
+ "importScripts",
1770
+ ];
1771
+ for (const name of deniedCapabilities) {
1772
+ if (!(name in scope)) continue;
1773
+ restores.push(installScopeValue(
1774
+ scope,
1775
+ name,
1776
+ function rejectUndeclaredArtifactGraphCapability() {
1777
+ throw workerError(
1778
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1779
+ `The authenticated artifact graph denied raw ${name} access.`,
1780
+ undefined,
1781
+ `artifact-graph-${name.toLowerCase()}-capability-undeclared`,
1782
+ );
1783
+ },
1784
+ `artifact-graph-${name.toLowerCase()}-isolation-unavailable`,
1785
+ ));
1786
+ }
1787
+ }
1788
+ } catch (error) {
1789
+ restoreScopeValues(restores);
1790
+ graphGuard.cleanup();
1791
+ throw error;
1792
+ }
1793
+ return Object.freeze({
1794
+ cache: graphGuard.transformersCache,
1795
+ cleanup() {
1796
+ graphGuard.cleanup();
1797
+ restoreScopeValues(restores);
1798
+ },
1799
+ });
1800
+ }
1801
+
1802
+ function assignSetting(object, name, value, cleanup, {
1803
+ allowCreate = false,
1804
+ assignmentRejectedReason,
1805
+ unavailableReason,
1806
+ } = {}) {
1807
+ if (!object) {
1808
+ throw workerError(
1809
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1810
+ `The selected runtime does not expose the verified ${name} setting.`,
1811
+ undefined,
1812
+ unavailableReason,
1813
+ );
1814
+ }
1815
+ let hadOwn;
1816
+ let previous;
1817
+ let canRestore = false;
1818
+ try {
1819
+ if (!allowCreate && !(name in object)) {
1820
+ throw workerError(
1821
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1822
+ `The selected runtime does not expose the verified ${name} setting.`,
1823
+ undefined,
1824
+ unavailableReason,
1825
+ );
1826
+ }
1827
+ hadOwn = Object.hasOwn(object, name);
1828
+ previous = object[name];
1829
+ canRestore = true;
1830
+ object[name] = value;
1831
+ if (object[name] !== value) {
1832
+ throw new TypeError(`The ${name} assignment was not retained.`);
1833
+ }
186
1834
  } catch (error) {
1835
+ if (isSdkWorkerError(error)) throw error;
1836
+ if (canRestore) {
1837
+ try {
1838
+ if (hadOwn) object[name] = previous;
1839
+ else delete object[name];
1840
+ } catch {
1841
+ // Preserve the exact configuration failure as the public boundary.
1842
+ }
1843
+ }
187
1844
  throw workerError(
188
1845
  "ARCANE_AI_PROVIDER_UNAVAILABLE",
189
- "The speech worker could not isolate the browser cache API.",
1846
+ `The selected runtime rejected the verified ${name} setting.`,
190
1847
  error,
1848
+ assignmentRejectedReason,
191
1849
  );
192
1850
  }
193
- return function restoreSpeechWorkerCaches() {
194
- if (ownDescriptor) Object.defineProperty(scope, "caches", ownDescriptor);
195
- else delete scope.caches;
196
- };
1851
+ cleanup.push(() => {
1852
+ if (hadOwn) object[name] = previous;
1853
+ else delete object[name];
1854
+ });
197
1855
  }
198
1856
 
199
- function configureWasmPaths(namespace, configuration) {
200
- if (namespace?.env) {
201
- namespace.env.allowLocalModels = false;
202
- namespace.env.allowRemoteModels = true;
203
- namespace.env.useBrowserCache = false;
204
- namespace.env.useFSCache = false;
205
- namespace.env.useCustomCache = false;
206
- namespace.env.customCache = null;
207
- }
208
- const wasm = namespace?.env?.backends?.onnx?.wasm;
209
- if (!wasm) return;
210
- const paths = {};
211
- for (const file of configuration.runtime.files) {
212
- if (/\.(?:m?js|wasm)$/iu.test(file.path) && file.path !== configuration.runtime.entry) {
213
- paths[file.path.split("/").pop()] = file.moduleUrl;
1857
+ function configuredWasmPaths(configuration) {
1858
+ if (configuration.runtime.wasmPaths !== undefined) {
1859
+ if (configuration.security?.secure === true) {
1860
+ throw workerError(
1861
+ "ARCANE_AI_INVALID_REQUEST",
1862
+ "Secure browser speech cannot use remote wasmPaths.",
1863
+ undefined,
1864
+ "speech-worker-secure-remote-wasm-paths-rejected",
1865
+ );
214
1866
  }
1867
+ return configuration.runtime.wasmPaths;
1868
+ }
1869
+ const byPath = new Map(configuration.runtime.files.map((file) => [file.path, file]));
1870
+ const declared = configuration.runtime.onnxWasm;
1871
+ let mjs = declared ? byPath.get(declared.mjsPath) : null;
1872
+ let wasm = declared ? byPath.get(declared.wasmPath) : null;
1873
+ if (!declared) {
1874
+ mjs = configuration.runtime.files.find((file) =>
1875
+ file.path !== configuration.runtime.entry && /\.mjs$/iu.test(file.path));
1876
+ wasm = configuration.runtime.files.find((file) => /\.wasm$/iu.test(file.path));
215
1877
  }
216
- if (Object.keys(paths).length > 0) wasm.wasmPaths = Object.freeze(paths);
217
- if (scopeIsCrossOriginIsolated() && Number.isSafeInteger(globalThis.navigator?.hardwareConcurrency)) {
218
- wasm.numThreads = Math.max(1, Math.min(8, globalThis.navigator.hardwareConcurrency));
1878
+ if (!mjs && !wasm) return null;
1879
+ if (!mjs?.moduleUrl || !wasm?.moduleUrl) {
1880
+ throw workerError(
1881
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1882
+ "The selected runtime did not materialize its complete ONNX MJS/WASM pair.",
1883
+ undefined,
1884
+ "artifact-graph-onnx-wasm-pair-not-materialized",
1885
+ );
219
1886
  }
1887
+ return Object.freeze({ mjs: mjs.moduleUrl, wasm: wasm.moduleUrl });
220
1888
  }
221
1889
 
222
- function scopeIsCrossOriginIsolated() {
223
- return globalThis.crossOriginIsolated === true;
1890
+ function configureRuntimeNamespace(namespace, configuration, role, cache) {
1891
+ const cleanup = [];
1892
+ const strictSecurity = configuration.security?.secure === true;
1893
+ const restoreSettings = () => {
1894
+ for (const restore of cleanup.splice(0).reverse()) {
1895
+ try {
1896
+ restore();
1897
+ } catch {
1898
+ // Worker isolation is still released by terminating the owning Worker.
1899
+ }
1900
+ }
1901
+ };
1902
+ const paths = configuredWasmPaths(configuration);
1903
+ if (role === "tts") {
1904
+ const env = namespace?.env;
1905
+ if (!env || !("wasmPaths" in env)) {
1906
+ throw workerError(
1907
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1908
+ "Kokoro does not expose the verified env.wasmPaths configuration field.",
1909
+ undefined,
1910
+ "kokoro-env-wasm-paths-unavailable",
1911
+ );
1912
+ }
1913
+ try {
1914
+ if (paths) {
1915
+ assignSetting(
1916
+ env,
1917
+ "wasmPaths",
1918
+ paths,
1919
+ cleanup,
1920
+ {
1921
+ assignmentRejectedReason: "kokoro-env-wasm-paths-assignment-rejected",
1922
+ unavailableReason: "kokoro-env-wasm-paths-unavailable",
1923
+ },
1924
+ );
1925
+ }
1926
+ } catch (error) {
1927
+ restoreSettings();
1928
+ throw error;
1929
+ }
1930
+ return restoreSettings;
1931
+ }
1932
+
1933
+ const env = namespace?.env;
1934
+ const wasm = env?.backends?.onnx?.wasm;
1935
+ if (!env || !wasm) {
1936
+ throw workerError(
1937
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1938
+ "Transformers does not expose env.backends.onnx.wasm.",
1939
+ undefined,
1940
+ "transformers-env-backends-onnx-wasm-unavailable",
1941
+ );
1942
+ }
1943
+ try {
1944
+ assignSetting(env, "allowLocalModels", false, cleanup, {
1945
+ assignmentRejectedReason: "transformers-env-allow-local-models-assignment-rejected",
1946
+ unavailableReason: "transformers-env-allow-local-models-unavailable",
1947
+ });
1948
+ assignSetting(env, "allowRemoteModels", true, cleanup, {
1949
+ assignmentRejectedReason: "transformers-env-allow-remote-models-assignment-rejected",
1950
+ unavailableReason: "transformers-env-allow-remote-models-unavailable",
1951
+ });
1952
+ if (strictSecurity) {
1953
+ assignSetting(env, "useBrowserCache", false, cleanup, {
1954
+ assignmentRejectedReason: "transformers-env-browser-cache-assignment-rejected",
1955
+ unavailableReason: "transformers-env-browser-cache-unavailable",
1956
+ });
1957
+ assignSetting(env, "useFSCache", false, cleanup, {
1958
+ assignmentRejectedReason: "transformers-env-fs-cache-assignment-rejected",
1959
+ unavailableReason: "transformers-env-fs-cache-unavailable",
1960
+ });
1961
+ assignSetting(env, "useCustomCache", cache !== null, cleanup, {
1962
+ assignmentRejectedReason: "transformers-env-custom-cache-toggle-assignment-rejected",
1963
+ unavailableReason: "transformers-env-custom-cache-toggle-unavailable",
1964
+ });
1965
+ assignSetting(env, "customCache", cache, cleanup, {
1966
+ assignmentRejectedReason: "transformers-env-custom-cache-assignment-rejected",
1967
+ unavailableReason: "transformers-env-custom-cache-unavailable",
1968
+ });
1969
+ }
1970
+ if (paths) {
1971
+ assignSetting(
1972
+ wasm,
1973
+ "wasmPaths",
1974
+ paths,
1975
+ cleanup,
1976
+ {
1977
+ allowCreate: true,
1978
+ assignmentRejectedReason: "transformers-env-wasm-paths-assignment-rejected",
1979
+ unavailableReason: "transformers-env-wasm-paths-unavailable",
1980
+ },
1981
+ );
1982
+ }
1983
+ if (configuration.runtime.onnxWasm?.numThreads !== undefined) {
1984
+ assignSetting(
1985
+ wasm,
1986
+ "numThreads",
1987
+ configuration.runtime.onnxWasm.numThreads,
1988
+ cleanup,
1989
+ {
1990
+ allowCreate: true,
1991
+ assignmentRejectedReason: "transformers-env-num-threads-assignment-rejected",
1992
+ unavailableReason: "transformers-env-num-threads-unavailable",
1993
+ },
1994
+ );
1995
+ }
1996
+ } catch (error) {
1997
+ restoreSettings();
1998
+ throw error;
1999
+ }
2000
+ return restoreSettings;
224
2001
  }
225
2002
 
226
- function workerProgress(send, requestId, phase, completed = 0, total = null, unit = "items") {
2003
+ function workerProgress(send, role, requestId, phase, completed = 0, total = null, unit = "items") {
227
2004
  send({
228
2005
  protocol: SPEECH_WORKER_PROTOCOL,
229
2006
  event: "progress",
@@ -238,6 +2015,24 @@ function workerProgress(send, requestId, phase, completed = 0, total = null, uni
238
2015
  }, []);
239
2016
  }
240
2017
 
2018
+ function upstreamProgress(send, role, requestId) {
2019
+ return function reportSpeechModelProgress(update = {}) {
2020
+ const completed = Number.isFinite(update.loaded)
2021
+ ? update.loaded
2022
+ : Number.isFinite(update.progress) ? update.progress : 0;
2023
+ const total = Number.isFinite(update.total) ? update.total : null;
2024
+ workerProgress(
2025
+ send,
2026
+ role,
2027
+ requestId,
2028
+ `${role}-model-load-progress`,
2029
+ completed,
2030
+ total,
2031
+ Number.isFinite(update.loaded) ? "bytes" : "items",
2032
+ );
2033
+ };
2034
+ }
2035
+
241
2036
  async function disposeEngine(engine) {
242
2037
  if (!engine) return;
243
2038
  if (typeof engine.dispose === "function") {
@@ -254,26 +2049,39 @@ async function disposeEngine(engine) {
254
2049
 
255
2050
  async function createWhisperEngine(namespace, configuration, signal, report) {
256
2051
  if (typeof namespace?.pipeline !== "function") {
257
- throw workerError("ARCANE_AI_PROVIDER_UNAVAILABLE", "The Whisper runtime does not export pipeline().");
2052
+ throw workerError(
2053
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
2054
+ "The Whisper runtime does not export pipeline().",
2055
+ undefined,
2056
+ "transformers-whisper-pipeline-export-missing",
2057
+ );
258
2058
  }
259
- configureWasmPaths(namespace, configuration);
260
- throwIfAborted(signal);
2059
+ throwIfAborted(signal, "stt-load-cancelled");
261
2060
  const transcriber = await namespace.pipeline(
262
2061
  "automatic-speech-recognition",
263
2062
  configuration.model.repository,
264
2063
  {
265
2064
  device: "wasm",
266
- dtype: "fp32",
2065
+ dtype: configuration.model.dtype ?? "fp32",
267
2066
  revision: configuration.model.revision,
268
2067
  progress_callback: report,
269
2068
  },
270
2069
  );
271
- throwIfAborted(signal);
2070
+ try {
2071
+ throwIfAborted(signal, "stt-load-cancelled");
2072
+ } catch (error) {
2073
+ try {
2074
+ await disposeEngine(transcriber);
2075
+ } catch {
2076
+ // Preserve the cancellation boundary while the owning Worker terminates.
2077
+ }
2078
+ throw error;
2079
+ }
272
2080
  return Object.freeze({
273
2081
  async transcribe(input, { signal: requestSignal } = {}) {
274
- throwIfAborted(requestSignal);
2082
+ throwIfAborted(requestSignal, "stt-transcription-cancelled");
275
2083
  const output = await transcriber(input.audio, { signal: requestSignal });
276
- throwIfAborted(requestSignal);
2084
+ throwIfAborted(requestSignal, "stt-transcription-cancelled");
277
2085
  return Object.freeze({ text: String(output?.text ?? "").trim() });
278
2086
  },
279
2087
  dispose: () => disposeEngine(transcriber),
@@ -282,29 +2090,42 @@ async function createWhisperEngine(namespace, configuration, signal, report) {
282
2090
 
283
2091
  async function createKokoroEngine(namespace, configuration, signal, report) {
284
2092
  if (typeof namespace?.KokoroTTS?.from_pretrained !== "function") {
285
- throw workerError("ARCANE_AI_PROVIDER_UNAVAILABLE", "The Kokoro runtime does not export KokoroTTS.");
2093
+ throw workerError(
2094
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
2095
+ "The Kokoro runtime does not export KokoroTTS.",
2096
+ undefined,
2097
+ "kokoro-tts-constructor-export-missing",
2098
+ );
286
2099
  }
287
- configureWasmPaths(namespace, configuration);
288
- throwIfAborted(signal);
2100
+ throwIfAborted(signal, "tts-load-cancelled");
289
2101
  const synthesizer = await namespace.KokoroTTS.from_pretrained(
290
2102
  configuration.model.repository,
291
2103
  {
292
2104
  device: "wasm",
293
- dtype: "q8",
2105
+ dtype: configuration.model.dtype ?? "q8",
294
2106
  revision: configuration.model.revision,
295
2107
  progress_callback: report,
296
2108
  },
297
2109
  );
298
- throwIfAborted(signal);
2110
+ try {
2111
+ throwIfAborted(signal, "tts-load-cancelled");
2112
+ } catch (error) {
2113
+ try {
2114
+ await disposeEngine(synthesizer);
2115
+ } catch {
2116
+ // Preserve the cancellation boundary while the owning Worker terminates.
2117
+ }
2118
+ throw error;
2119
+ }
299
2120
  return Object.freeze({
300
2121
  async synthesize(input, { signal: requestSignal } = {}) {
301
- throwIfAborted(requestSignal);
2122
+ throwIfAborted(requestSignal, "tts-synthesis-cancelled");
302
2123
  const output = await synthesizer.generate(input.text, {
303
2124
  voice: input.voice,
304
2125
  speed: input.speed,
305
2126
  signal: requestSignal,
306
2127
  });
307
- throwIfAborted(requestSignal);
2128
+ throwIfAborted(requestSignal, "tts-synthesis-cancelled");
308
2129
  const audio = output?.audio instanceof Float32Array
309
2130
  ? output.audio
310
2131
  : new Float32Array(output?.audio ?? []);
@@ -318,49 +2139,109 @@ async function createKokoroEngine(namespace, configuration, signal, report) {
318
2139
  });
319
2140
  }
320
2141
 
2142
+ function callerVoiceIds(configuration) {
2143
+ return new Set((configuration.model.voices ?? []).map((voice) =>
2144
+ typeof voice === "string" ? voice : voice?.id));
2145
+ }
2146
+
321
2147
  function validateInput(role, payload, configuration) {
322
2148
  if (role === "stt") {
323
- if (!(payload?.audio instanceof Float32Array) || payload.sampleRate !== 16_000) {
2149
+ const inputSampleRate = requiredSampleRate(
2150
+ configuration.model.inputSampleRate,
2151
+ "Whisper inputSampleRate",
2152
+ 16_000,
2153
+ );
2154
+ if (!(payload?.audio instanceof Float32Array)) {
2155
+ throw workerError(
2156
+ "ARCANE_AI_INVALID_REQUEST",
2157
+ "Whisper requires Float32Array audio.",
2158
+ undefined,
2159
+ "stt-transcription-audio-not-float32array",
2160
+ );
2161
+ }
2162
+ if (payload.sampleRate !== inputSampleRate) {
324
2163
  throw workerError(
325
2164
  "ARCANE_AI_INVALID_REQUEST",
326
- "Whisper requires Float32Array audio sampled at exactly 16000 Hz.",
2165
+ `Whisper requires audio sampled at exactly ${String(inputSampleRate)} Hz.`,
2166
+ undefined,
2167
+ "stt-transcription-sample-rate-mismatch",
327
2168
  );
328
2169
  }
329
- return Object.freeze({ audio: payload.audio, sampleRate: 16_000 });
2170
+ return Object.freeze({ audio: payload.audio, sampleRate: inputSampleRate });
330
2171
  }
331
- const text = requiredText(payload?.text, "Kokoro text");
2172
+ const text = requiredText(payload?.text, "Kokoro text", "tts-synthesis-text-empty");
332
2173
  const voice = requiredText(
333
2174
  payload?.voice ?? configuration.model.defaultVoice,
334
2175
  "Kokoro voice",
2176
+ "tts-synthesis-voice-empty",
335
2177
  );
2178
+ const voices = callerVoiceIds(configuration);
2179
+ if (voices.size > 0 && !voices.has(voice)) {
2180
+ throw workerError(
2181
+ "ARCANE_AI_INVALID_REQUEST",
2182
+ "Kokoro voice is outside the caller-owned voice inventory.",
2183
+ undefined,
2184
+ "tts-synthesis-voice-not-declared",
2185
+ );
2186
+ }
336
2187
  const speed = payload?.speed ?? 1;
337
2188
  if (!Number.isFinite(speed) || speed <= 0 || speed > 4) {
338
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Kokoro speed must be greater than 0 and at most 4.");
2189
+ throw workerError(
2190
+ "ARCANE_AI_INVALID_REQUEST",
2191
+ "Kokoro speed must be greater than 0 and at most 4.",
2192
+ undefined,
2193
+ "tts-synthesis-speed-out-of-range",
2194
+ );
339
2195
  }
340
2196
  return Object.freeze({ text, voice, speed });
341
2197
  }
342
2198
 
343
- function validateResult(role, result) {
2199
+ function validateResult(role, result, configuration) {
344
2200
  if (role === "stt") {
345
2201
  if (!result || typeof result.text !== "string") {
346
- throw workerError("ARCANE_AI_INVALID_PROVIDER_RESULT", "Whisper did not return text.");
2202
+ throw workerError(
2203
+ "ARCANE_AI_INVALID_PROVIDER_RESULT",
2204
+ "Whisper did not return text.",
2205
+ undefined,
2206
+ "stt-transcription-result-text-not-string",
2207
+ );
347
2208
  }
348
2209
  return Object.freeze({ text: result.text.trim() });
349
2210
  }
350
- if (!(result?.audio instanceof Float32Array) || result.sampleRate !== 24_000) {
2211
+ const outputSampleRate = requiredSampleRate(
2212
+ configuration.model.outputSampleRate,
2213
+ "Kokoro outputSampleRate",
2214
+ 24_000,
2215
+ );
2216
+ if (!(result?.audio instanceof Float32Array)) {
351
2217
  throw workerError(
352
2218
  "ARCANE_AI_INVALID_PROVIDER_RESULT",
353
- "Kokoro must return 24000 Hz Float32 PCM.",
2219
+ "Kokoro must return Float32 PCM.",
2220
+ undefined,
2221
+ "tts-synthesis-pcm-result-not-float32array",
2222
+ );
2223
+ }
2224
+ if (result.sampleRate !== outputSampleRate) {
2225
+ throw workerError(
2226
+ "ARCANE_AI_INVALID_PROVIDER_RESULT",
2227
+ `Kokoro must return ${String(outputSampleRate)} Hz PCM.`,
2228
+ undefined,
2229
+ "tts-synthesis-result-sample-rate-mismatch",
354
2230
  );
355
2231
  }
356
2232
  for (const sample of result.audio) {
357
2233
  if (!Number.isFinite(sample)) {
358
- throw workerError("ARCANE_AI_INVALID_PROVIDER_RESULT", "Kokoro returned non-finite PCM.");
2234
+ throw workerError(
2235
+ "ARCANE_AI_INVALID_PROVIDER_RESULT",
2236
+ "Kokoro returned non-finite PCM.",
2237
+ undefined,
2238
+ "tts-synthesis-pcm-sample-non-finite",
2239
+ );
359
2240
  }
360
2241
  }
361
2242
  return Object.freeze({
362
2243
  audio: result.audio,
363
- sampleRate: 24_000,
2244
+ sampleRate: outputSampleRate,
364
2245
  voice: result.voice,
365
2246
  });
366
2247
  }
@@ -374,92 +2255,232 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
374
2255
  }
375
2256
  let configuration = null;
376
2257
  let engine = null;
377
- let restoreFetch = null;
378
- let restoreCaches = null;
2258
+ let environment = null;
2259
+ let restoreNamespace = null;
379
2260
  let disposed = false;
380
2261
  let tail = Promise.resolve();
2262
+ let lifecycleReason = `${role}-worker-created`;
381
2263
  const operations = new Map();
382
2264
 
383
2265
  function status() {
2266
+ const state = disposed ? "disposed" : engine ? "ready" : "unloaded";
2267
+ const security = configuration?.security
2268
+ ? Object.freeze({
2269
+ secure: configuration.security.secure === true,
2270
+ checks: Object.freeze({
2271
+ byteLength: configuration.security.checks?.byteLength === true,
2272
+ sha256: configuration.security.checks?.sha256 === true,
2273
+ }),
2274
+ })
2275
+ : null;
384
2276
  return Object.freeze({
385
- state: disposed ? "disposed" : engine ? "ready" : "unloaded",
2277
+ state,
2278
+ lifecycleStatus: `${role}-worker-${state}`,
2279
+ lifecycleReason,
2280
+ role,
386
2281
  loaded: engine !== null,
387
2282
  busy: operations.size > 0,
2283
+ activeOperation: operations.values().next().value?.publicOperation ?? null,
2284
+ security,
2285
+ artifactGraphId: configuration?.runtime?.artifactGraphId ?? null,
2286
+ artifactGraphAdmission: configuration?.runtime?.artifactGraphAdmission ?? null,
388
2287
  });
389
2288
  }
390
2289
 
391
2290
  async function load(request, signal) {
392
2291
  if (disposed) {
393
- throw workerError("ARCANE_AI_PROVIDER_DISPOSED", "The speech worker is disposed.");
2292
+ throw workerError(
2293
+ "ARCANE_AI_PROVIDER_DISPOSED",
2294
+ "The speech worker is disposed.",
2295
+ undefined,
2296
+ `${role}-load-rejected-after-dispose`,
2297
+ );
394
2298
  }
395
2299
  if (engine) return status();
396
- configuration = validateConfiguration(request.payload?.configuration, role);
397
- const entry = configuration.runtime.files.find((file) =>
398
- file.path === configuration.runtime.entry);
399
- restoreFetch = installAuthorizedFetch(scope, configuration);
2300
+ lifecycleReason = `${role}-load-started`;
2301
+ let loadFailureReason = `${role}-worker-runtime-configuration-rejected`;
400
2302
  try {
401
- restoreCaches = installCacheIsolation(scope);
402
- workerProgress(send, request.id, "runtime-import");
2303
+ configuration = validateConfiguration(request.payload?.configuration, role);
2304
+ const entry = configuration.runtime.files.find((file) =>
2305
+ file.path === configuration.runtime.entry);
2306
+ if (graphConfiguration(configuration)) {
2307
+ environment = installArtifactGraphEnvironment(scope, configuration, role);
2308
+ } else if (configuration.security?.secure === true) {
2309
+ const legacyFetch = installLegacyAuthorizedFetch(scope, configuration);
2310
+ try {
2311
+ const restoreCaches = installDeniedCacheIsolation(scope);
2312
+ environment = Object.freeze({
2313
+ cache: null,
2314
+ cleanup() {
2315
+ try {
2316
+ restoreCaches();
2317
+ } finally {
2318
+ legacyFetch.cleanup();
2319
+ }
2320
+ },
2321
+ });
2322
+ } catch (error) {
2323
+ legacyFetch.cleanup();
2324
+ throw error;
2325
+ }
2326
+ } else {
2327
+ environment = Object.freeze({
2328
+ cache: null,
2329
+ cleanup() {},
2330
+ });
2331
+ }
2332
+ workerProgress(send, role, request.id, `${role}-runtime-import-started`);
2333
+ loadFailureReason = `${role}-worker-runtime-import-rejected`;
403
2334
  const namespace = await import(entry.moduleUrl);
404
- throwIfAborted(signal);
405
- const report = () => workerProgress(send, request.id, "model-load");
2335
+ throwIfAborted(signal, `${role}-load-cancelled`);
2336
+ restoreNamespace = configureRuntimeNamespace(
2337
+ namespace,
2338
+ configuration,
2339
+ role,
2340
+ environment.cache,
2341
+ );
2342
+ workerProgress(send, role, request.id, `${role}-model-load-started`);
2343
+ loadFailureReason = `${role}-worker-model-load-rejected`;
2344
+ const report = upstreamProgress(send, role, request.id);
406
2345
  engine = role === "stt"
407
2346
  ? await createWhisperEngine(namespace, configuration, signal, report)
408
2347
  : await createKokoroEngine(namespace, configuration, signal, report);
409
- workerProgress(send, request.id, "ready", 1, 1);
2348
+ lifecycleReason = `${role}-load-completed`;
2349
+ workerProgress(send, role, request.id, `${role}-provider-ready`, 1, 1);
410
2350
  return status();
411
2351
  } catch (error) {
412
- restoreFetch?.();
413
- restoreFetch = null;
414
- restoreCaches?.();
415
- restoreCaches = null;
2352
+ const failure = admitWorkerFailure(
2353
+ error,
2354
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
2355
+ "The speech Worker load operation was rejected.",
2356
+ loadFailureReason,
2357
+ );
2358
+ const rejectedEngine = engine;
2359
+ engine = null;
2360
+ try {
2361
+ await disposeEngine(rejectedEngine);
2362
+ } catch {
2363
+ // Preserve the load boundary error that caused teardown.
2364
+ }
2365
+ restoreNamespace?.();
2366
+ restoreNamespace = null;
2367
+ try {
2368
+ environment?.cleanup();
2369
+ } catch {
2370
+ // A rejected load is followed by provider-owned Worker termination.
2371
+ }
2372
+ environment = null;
416
2373
  configuration = null;
417
- throw error;
2374
+ lifecycleReason = failure.reason;
2375
+ throw failure;
418
2376
  }
419
2377
  }
420
2378
 
421
2379
  async function use(request, signal) {
422
2380
  if (!engine || !configuration) {
423
- throw workerError("ARCANE_AI_NOT_READY", "The speech worker is not loaded.");
2381
+ throw workerError(
2382
+ "ARCANE_AI_NOT_READY",
2383
+ "The speech worker is not loaded.",
2384
+ undefined,
2385
+ role === "stt"
2386
+ ? "stt-transcription-rejected-before-load"
2387
+ : "tts-synthesis-rejected-before-load",
2388
+ );
424
2389
  }
425
2390
  const input = validateInput(role, request.payload, configuration);
426
2391
  const method = role === "stt" ? engine.transcribe : engine.synthesize;
427
2392
  if (typeof method !== "function") {
428
- throw workerError("ARCANE_AI_PROVIDER_UNAVAILABLE", "The speech engine operation is unavailable.");
2393
+ throw workerError(
2394
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
2395
+ "The speech engine operation is unavailable.",
2396
+ undefined,
2397
+ role === "stt"
2398
+ ? "stt-transcription-method-unavailable"
2399
+ : "tts-synthesis-method-unavailable",
2400
+ );
2401
+ }
2402
+ lifecycleReason = role === "stt"
2403
+ ? "stt-transcription-started"
2404
+ : "tts-synthesis-started";
2405
+ try {
2406
+ const result = validateResult(
2407
+ role,
2408
+ await method(input, { signal }),
2409
+ configuration,
2410
+ );
2411
+ lifecycleReason = role === "stt"
2412
+ ? "stt-transcription-completed"
2413
+ : "tts-synthesis-completed";
2414
+ return result;
2415
+ } catch (error) {
2416
+ const failure = admitWorkerFailure(
2417
+ error,
2418
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
2419
+ "The speech engine operation was rejected.",
2420
+ role === "stt"
2421
+ ? "stt-transcription-engine-operation-rejected"
2422
+ : "tts-synthesis-engine-operation-rejected",
2423
+ );
2424
+ lifecycleReason = failure.reason;
2425
+ throw failure;
429
2426
  }
430
- return validateResult(role, await method(input, { signal }));
431
2427
  }
432
2428
 
433
- async function unload() {
434
- for (const controller of operations.values()) controller.abort();
2429
+ async function unload(reason = `${role}-unload-completed`) {
2430
+ for (const operation of operations.values()) {
2431
+ if (operation.op === "unload" || operation.op === "dispose") continue;
2432
+ operation.controller.abort(operationReason(role, operation.op, "superseded-by-unload"));
2433
+ }
435
2434
  const current = engine;
436
2435
  engine = null;
437
- configuration = null;
2436
+ let disposeFailure = null;
438
2437
  try {
439
2438
  await disposeEngine(current);
2439
+ } catch (error) {
2440
+ const failure = admitWorkerFailure(
2441
+ error,
2442
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
2443
+ "The speech engine dispose operation was rejected.",
2444
+ `${role}-worker-engine-dispose-rejected`,
2445
+ );
2446
+ disposeFailure = failure;
2447
+ lifecycleReason = failure.reason;
2448
+ throw failure;
440
2449
  } finally {
441
- restoreFetch?.();
442
- restoreFetch = null;
443
- restoreCaches?.();
444
- restoreCaches = null;
2450
+ restoreNamespace?.();
2451
+ restoreNamespace = null;
2452
+ try {
2453
+ environment?.cleanup();
2454
+ } catch {
2455
+ // Worker termination remains the final cleanup boundary.
2456
+ }
2457
+ environment = null;
2458
+ configuration = null;
2459
+ if (!disposeFailure) lifecycleReason = reason;
445
2460
  }
446
2461
  return status();
447
2462
  }
448
2463
 
449
- async function dispatch(request, signal) {
450
- if (request.op === "load") return load(request, signal);
451
- if (request.op === "use") return use(request, signal);
452
- if (request.op === "status") return status();
453
- if (request.op === "unload") return unload();
454
- if (request.op === "dispose") {
455
- await unload();
2464
+ async function dispatch(request, signal, op) {
2465
+ if (op === "load") return load(request, signal);
2466
+ if (op === "use") return use(request, signal);
2467
+ if (op === "status") return status();
2468
+ if (op === "unload") return unload(`${role}-unload-completed`);
2469
+ if (op === "dispose") {
2470
+ await unload(`${role}-dispose-unloaded-worker`);
456
2471
  disposed = true;
2472
+ lifecycleReason = `${role}-dispose-completed`;
457
2473
  return status();
458
2474
  }
459
- throw workerError("ARCANE_AI_INVALID_REQUEST", "The speech worker operation is unsupported.");
2475
+ throw workerError(
2476
+ "ARCANE_AI_INVALID_REQUEST",
2477
+ "The speech worker operation is not part of its protocol.",
2478
+ undefined,
2479
+ `${role}-worker-operation-unknown`,
2480
+ );
460
2481
  }
461
2482
 
462
- function respond(request, operation) {
2483
+ function respond(request, operation, op) {
463
2484
  operation.then((result) => send({
464
2485
  protocol: SPEECH_WORKER_PROTOCOL,
465
2486
  id: request.id,
@@ -469,7 +2490,7 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
469
2490
  protocol: SPEECH_WORKER_PROTOCOL,
470
2491
  id: request.id,
471
2492
  ok: false,
472
- error: serializedError(error),
2493
+ error: serializedError(error, role, op),
473
2494
  }, []));
474
2495
  }
475
2496
 
@@ -477,40 +2498,174 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
477
2498
  if (request?.protocol !== SPEECH_WORKER_PROTOCOL
478
2499
  || !Number.isSafeInteger(request.id)
479
2500
  || request.id < 1) {
480
- return Promise.reject(workerError("ARCANE_AI_INVALID_REQUEST", "The speech worker envelope is invalid."));
2501
+ return Promise.reject(workerError(
2502
+ "ARCANE_AI_INVALID_REQUEST",
2503
+ "The speech worker envelope shape was rejected.",
2504
+ undefined,
2505
+ `${role}-worker-message-envelope-shape-rejected`,
2506
+ ));
2507
+ }
2508
+ const op = request.op;
2509
+ if (!TRANSPORT_WORKER_OPERATION_SET.has(op)) {
2510
+ return Promise.reject(workerError(
2511
+ "ARCANE_AI_INVALID_REQUEST",
2512
+ "The speech worker operation is not part of its protocol.",
2513
+ undefined,
2514
+ `${role}-worker-operation-unknown`,
2515
+ ));
481
2516
  }
482
- if (request.op === "cancel") {
2517
+ if (op === "cancel") {
483
2518
  const target = operations.get(request.payload?.targetId);
484
- target?.abort(request.payload?.reason);
485
- const result = Promise.resolve(Object.freeze({ cancelled: Boolean(target) }));
486
- respond(request, result);
2519
+ target?.controller.abort(operationReason(role, target.op, "cancelled"));
2520
+ const result = Promise.resolve(Object.freeze({
2521
+ cancelled: Boolean(target),
2522
+ reason: target
2523
+ ? operationReason(role, target.op, "cancelled")
2524
+ : `${role}-cancel-target-not-active`,
2525
+ }));
2526
+ respond(request, result, op);
487
2527
  return result;
488
2528
  }
489
2529
  const controller = new AbortController();
2530
+ const publicOperation = op === "use"
2531
+ ? role === "stt" ? "stt-transcription" : "tts-synthesis"
2532
+ : `${role}-${op}`;
2533
+ const record = { controller, op, publicOperation };
490
2534
  const execute = tail.catch(() => undefined).then(() => {
491
- throwIfAborted(controller.signal);
492
- return dispatch(request, controller.signal);
2535
+ throwIfAborted(controller.signal, operationReason(role, op, "cancelled"));
2536
+ return dispatch(request, controller.signal, op);
493
2537
  });
494
- operations.set(request.id, controller);
2538
+ operations.set(request.id, record);
495
2539
  const operation = execute.finally(() => operations.delete(request.id));
496
2540
  tail = operation.catch(() => undefined);
497
- respond(request, operation);
2541
+ respond(request, operation, op);
498
2542
  return operation;
499
2543
  }
500
2544
 
501
2545
  return Object.freeze({ handleMessage, status });
502
2546
  }
503
2547
 
2548
+ function privatePort(value) {
2549
+ return value
2550
+ && typeof value.postMessage === "function"
2551
+ && typeof value.addEventListener === "function"
2552
+ ? value
2553
+ : null;
2554
+ }
2555
+
504
2556
  export function installBrowserSpeechWorker(role, scope = globalThis) {
2557
+ let transport = scope;
2558
+ let transportMode = null;
505
2559
  const runtime = createSpeechWorkerRuntime({
506
2560
  role,
507
2561
  scope,
508
- send: (message, transfers) => scope.postMessage(message, transfers),
2562
+ send: (message, transfers) => transport.postMessage(message, transfers),
509
2563
  });
2564
+
2565
+ function receive(request) {
2566
+ void runtime.handleMessage(request).catch(() => undefined);
2567
+ }
2568
+
510
2569
  scope.addEventListener("message", (event) => {
511
- // Valid envelopes own their one response inside handleMessage(). Invalid
512
- // envelopes have no trustworthy request id and are intentionally ignored.
513
- void runtime.handleMessage(event.data).catch(() => undefined);
2570
+ if (transportMode === "private-message-port") return;
2571
+ const requestedPort = privatePort(event.data?.privatePort);
2572
+ const isGraphLoad = event.data?.op === "load"
2573
+ && graphConfiguration(event.data?.payload?.configuration);
2574
+ const isStrictGraphLoad = isGraphLoad
2575
+ && event.data?.payload?.configuration?.security?.secure === true;
2576
+ if (requestedPort) {
2577
+ if (!isStrictGraphLoad || transportMode !== null) return;
2578
+ transportMode = "private-message-port";
2579
+ transport = requestedPort;
2580
+ requestedPort.addEventListener("message", (portEvent) => receive(portEvent.data));
2581
+ requestedPort.start?.();
2582
+ const { privatePort: ignored, ...request } = event.data;
2583
+ void ignored;
2584
+ receive(request);
2585
+ return;
2586
+ }
2587
+ if (isStrictGraphLoad) {
2588
+ scope.postMessage({
2589
+ protocol: SPEECH_WORKER_PROTOCOL,
2590
+ id: event.data.id,
2591
+ ok: false,
2592
+ error: serializedError(workerError(
2593
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
2594
+ "Strict artifact graph loading requires a private MessagePort.",
2595
+ undefined,
2596
+ "artifact-graph-private-message-port-missing",
2597
+ ), role, "load"),
2598
+ });
2599
+ return;
2600
+ }
2601
+ transportMode ??= "worker-global-message";
2602
+ receive(event.data);
514
2603
  });
515
2604
  return runtime;
516
2605
  }
2606
+
2607
+ function replayWorkerMessage(scope, event) {
2608
+ if (typeof scope.dispatchEvent === "function") {
2609
+ scope.dispatchEvent(event);
2610
+ return;
2611
+ }
2612
+ scope.onmessage?.(event);
2613
+ }
2614
+
2615
+ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThis) {
2616
+ if (role !== "stt" && role !== "tts") {
2617
+ throw new TypeError('Speech artifact module Worker role must be "stt" or "tts".');
2618
+ }
2619
+ let initializing = false;
2620
+ const queued = [];
2621
+ const bootstrap = (event) => {
2622
+ if (initializing) {
2623
+ queued.push(event);
2624
+ return;
2625
+ }
2626
+ const request = event.data;
2627
+ if (request?.protocol !== NESTED_WORKER_PROTOCOL
2628
+ || request.op !== "initialize-authenticated-artifact-module-worker"
2629
+ || request.role !== role) return;
2630
+ initializing = true;
2631
+ void (async () => {
2632
+ let environment = null;
2633
+ try {
2634
+ const configuration = validateConfiguration(request.configuration, role);
2635
+ const target = configuration.runtime.files.find((file) =>
2636
+ file.path === request.targetPath);
2637
+ if (!target?.moduleUrl) {
2638
+ throw workerError(
2639
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
2640
+ "The nested module Worker target was not materialized.",
2641
+ undefined,
2642
+ "artifact-graph-module-worker-target-not-materialized",
2643
+ );
2644
+ }
2645
+ environment = installArtifactGraphEnvironment(scope, configuration, role);
2646
+ await import(target.moduleUrl);
2647
+ scope.removeEventListener("message", bootstrap);
2648
+ await new Promise((resolve) => queueMicrotask(resolve));
2649
+ for (const queuedEvent of queued.splice(0)) replayWorkerMessage(scope, queuedEvent);
2650
+ } catch (error) {
2651
+ try {
2652
+ environment?.cleanup();
2653
+ } catch {
2654
+ // Preserve the exact nested Worker bootstrap failure.
2655
+ }
2656
+ scope.postMessage({
2657
+ protocol: NESTED_WORKER_PROTOCOL,
2658
+ event: "artifact-module-worker-bootstrap-rejected",
2659
+ error: serializedError(error, role, "load"),
2660
+ });
2661
+ scope.close?.();
2662
+ }
2663
+ })();
2664
+ };
2665
+ scope.addEventListener("message", bootstrap);
2666
+ return Object.freeze({
2667
+ protocol: NESTED_WORKER_PROTOCOL,
2668
+ role,
2669
+ lifecycleStatus: `${role}-artifact-module-worker-awaiting-initialization`,
2670
+ });
2671
+ }