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