arcane-os 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -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 +3242 -391
- package/browser-runtime/ai/browser-speech-providers.mjs +1209 -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 +249 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2322 -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 +18 -20
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1205 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +2 -2
- package/docs/reference/behavioral-testing.md +28 -5
- package/docs/reference/cli.md +115 -7
- 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 +86 -37
- package/docs/reference/inventory/runtime-modules.json +126 -49
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +185 -33
- package/docs/reference/runtime-components.md +192 -80
- package/docs/reference/runtime-modules.md +589 -70
- package/docs/reference/sdk-api.md +1017 -24
- 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 +453 -131
- 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 +244 -72
- 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 +2117 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +756 -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 +235 -37
- 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 +130 -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-runtime.mjs +804 -22
- 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,224 +556,1660 @@ 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",
|
|
1305
|
+
);
|
|
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
|
+
}
|
|
1404
|
+
|
|
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
|
+
}
|
|
1434
|
+
|
|
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,
|
|
331
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
|
+
}
|
|
2184
|
+
|
|
2185
|
+
scanCode();
|
|
2186
|
+
assertStaticLiteralChains();
|
|
332
2187
|
}
|
|
333
2188
|
|
|
334
|
-
function
|
|
335
|
-
|
|
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
|
+
) {
|
|
2196
|
+
throw speechError(
|
|
2197
|
+
"ARCANE_AI_RUNTIME_MODULE_GRAPH_UNDECLARED",
|
|
2198
|
+
"Browser speech requires exactly one admitted self-contained runtime module.",
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
const [{ descriptor, file }] = javascriptFiles;
|
|
2202
|
+
if (security?.secure === true) {
|
|
2203
|
+
assertSelfContainedModuleSource(await file.text(), descriptor.path);
|
|
2204
|
+
}
|
|
336
2205
|
}
|
|
337
2206
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// which the Worker replaces with its admitted object-URL map before import.
|
|
341
|
-
// Executable strings, child execution contexts, script loaders, and alternate
|
|
342
|
-
// network transports are outside the grammar. Literal escape sequences are
|
|
343
|
-
// decoded before the same capability tokens are evaluated.
|
|
344
|
-
const CLOSED_MODULE_OUT_OF_GRAMMAR_IDENTIFIERS = new Set([
|
|
345
|
-
"AsyncFunction",
|
|
346
|
-
"AsyncGeneratorFunction",
|
|
347
|
-
"EventSource",
|
|
348
|
-
"Function",
|
|
349
|
-
"GeneratorFunction",
|
|
350
|
-
"RTCPeerConnection",
|
|
351
|
-
"SharedWorker",
|
|
352
|
-
"WebSocket",
|
|
353
|
-
"WebTransport",
|
|
354
|
-
"Worker",
|
|
355
|
-
"XMLHttpRequest",
|
|
356
|
-
"eval",
|
|
357
|
-
"importScripts",
|
|
358
|
-
]);
|
|
359
|
-
const CLOSED_MODULE_OUT_OF_GRAMMAR_LITERAL =
|
|
360
|
-
/(?:^|[^A-Za-z0-9_$])(?:AsyncFunction|AsyncGeneratorFunction|EventSource|Function|GeneratorFunction|RTCPeerConnection|SharedWorker|WebSocket|WebTransport|Worker|XMLHttpRequest|constructor|eval|importScripts)(?:$|[^A-Za-z0-9_$])|(?:^|[^A-Za-z0-9_$])import\s*\(/u;
|
|
361
|
-
|
|
362
|
-
function assertSelfContainedModuleSource(source, label) {
|
|
2207
|
+
function tokenizeArtifactGraphModule(source, modulePath) {
|
|
2208
|
+
const tokens = [];
|
|
363
2209
|
let index = 0;
|
|
364
|
-
let nextTemplateId = 1;
|
|
365
|
-
const templateStack = [];
|
|
366
|
-
const literalFragments = [];
|
|
367
2210
|
|
|
368
|
-
function fail() {
|
|
369
|
-
throw
|
|
370
|
-
"ARCANE_AI_RUNTIME_MODULE_GRAPH_UNDECLARED",
|
|
371
|
-
`${label} must be one self-contained JavaScript module without imports or re-exports.`,
|
|
372
|
-
);
|
|
2211
|
+
function fail(reason, message) {
|
|
2212
|
+
throw artifactGraphError(reason, `${modulePath}: ${message}`);
|
|
373
2213
|
}
|
|
374
2214
|
|
|
375
2215
|
function identifierStart(character) {
|
|
@@ -380,92 +2220,38 @@ function assertSelfContainedModuleSource(source, label) {
|
|
|
380
2220
|
return /[A-Za-z0-9_$]/u.test(character ?? "");
|
|
381
2221
|
}
|
|
382
2222
|
|
|
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
|
-
}));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function stripJoinerTrivia(value) {
|
|
405
|
-
return value
|
|
406
|
-
.replace(/\/\*[\s\S]*?\*\//gu, "")
|
|
407
|
-
.replace(/\/\/[^\r\n]*(?:\r?\n|$)/gu, "")
|
|
408
|
-
.replace(/\s+/gu, "");
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function sharesTemplate(left, right) {
|
|
412
|
-
return left.templateIds.some((id) => right.templateIds.includes(id));
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function staticallyJoins(left, right) {
|
|
416
|
-
const separator = stripJoinerTrivia(source.slice(left.end, right.start));
|
|
417
|
-
const usesConcat = separator.includes(".concat");
|
|
418
|
-
const withoutConcat = separator.replace(/\.concat/gu, "");
|
|
419
|
-
const usesPlus = withoutConcat.includes("+");
|
|
420
|
-
const usesTemplate = sharesTemplate(left, right)
|
|
421
|
-
&& (withoutConcat.includes("${") || withoutConcat.includes("}"));
|
|
422
|
-
if (!usesConcat && !usesPlus && !usesTemplate) return false;
|
|
423
|
-
return (usesTemplate ? /^[+()${}]*$/u : /^[+()]*$/u).test(withoutConcat);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function assertStaticLiteralChains() {
|
|
427
|
-
let chain = [];
|
|
428
|
-
function flushChain() {
|
|
429
|
-
if (chain.length > 1) {
|
|
430
|
-
assertComputedLiteral(chain.map((fragment) => fragment.value).join(""));
|
|
431
|
-
}
|
|
432
|
-
chain = [];
|
|
433
|
-
}
|
|
434
|
-
for (const fragment of literalFragments) {
|
|
435
|
-
const previous = chain[chain.length - 1];
|
|
436
|
-
if (previous && staticallyJoins(previous, fragment)) {
|
|
437
|
-
chain.push(fragment);
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
flushChain();
|
|
441
|
-
chain.push(fragment);
|
|
442
|
-
}
|
|
443
|
-
flushChain();
|
|
444
|
-
}
|
|
445
|
-
|
|
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,63 +2300,48 @@ 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
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
if (character === "`") {
|
|
554
|
-
recordLiteral(fragmentStart, index, value);
|
|
555
|
-
templateStack.pop();
|
|
2331
|
+
readEscape();
|
|
2332
|
+
} else if (character === "`") {
|
|
556
2333
|
return;
|
|
557
|
-
}
|
|
558
|
-
if (character === "$" && source[index] === "{") {
|
|
559
|
-
recordLiteral(fragmentStart, index - 1, value);
|
|
560
|
-
value = "";
|
|
2334
|
+
} else if (character === "$" && source[index] === "{") {
|
|
561
2335
|
index += 1;
|
|
562
2336
|
scanCode(true);
|
|
563
|
-
fragmentStart = index;
|
|
564
|
-
continue;
|
|
565
2337
|
}
|
|
566
|
-
value += character;
|
|
567
2338
|
}
|
|
568
|
-
fail();
|
|
2339
|
+
fail("artifact-graph-javascript-template-literal-unterminated", "unterminated template literal.");
|
|
569
2340
|
}
|
|
570
2341
|
|
|
571
2342
|
function scanCode(stopAtTemplateBrace = false) {
|
|
572
2343
|
let nestedBraces = 0;
|
|
573
|
-
let lastToken = null;
|
|
2344
|
+
let lastToken = tokens[tokens.length - 1] ?? null;
|
|
574
2345
|
while (index < source.length) {
|
|
575
2346
|
const character = source[index];
|
|
576
2347
|
const next = source[index + 1];
|
|
@@ -578,93 +2349,862 @@ function assertSelfContainedModuleSource(source, label) {
|
|
|
578
2349
|
index += 1;
|
|
579
2350
|
continue;
|
|
580
2351
|
}
|
|
581
|
-
if (character === "/" && next === "/") {
|
|
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];
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
if (character === "`") {
|
|
2371
|
+
readTemplate();
|
|
2372
|
+
lastToken = Object.freeze({ type: "template", value: "template" });
|
|
2373
|
+
continue;
|
|
2374
|
+
}
|
|
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;
|
|
2382
|
+
index += 1;
|
|
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");
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
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;
|
|
2648
|
+
}
|
|
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
|
+
);
|
|
2667
|
+
}
|
|
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
|
+
const admittedByPath = new Map(admitted.files.map((entry) => [entry.descriptor.path, entry]));
|
|
2867
|
+
const plans = new Map();
|
|
2868
|
+
for (const descriptor of metadata.runtimeFiles) {
|
|
2869
|
+
throwIfAborted(signal);
|
|
2870
|
+
if (!ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(descriptor.kind)) continue;
|
|
2871
|
+
const file = admittedByPath.get(descriptor.path)?.file;
|
|
2872
|
+
if (!file) {
|
|
2873
|
+
throw artifactGraphError(
|
|
2874
|
+
"artifact-graph-runtime-javascript-file-missing",
|
|
2875
|
+
`Artifact graph runtime JavaScript file ${descriptor.path} is unavailable.`,
|
|
2876
|
+
);
|
|
2877
|
+
}
|
|
2878
|
+
let source;
|
|
2879
|
+
try {
|
|
2880
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
2881
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
2882
|
+
} catch (error) {
|
|
2883
|
+
throw artifactGraphError(
|
|
2884
|
+
"artifact-graph-runtime-javascript-utf8-decode-rejected",
|
|
2885
|
+
`Artifact graph runtime JavaScript file ${descriptor.path} is not valid UTF-8.`,
|
|
2886
|
+
error,
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
plans.set(
|
|
2890
|
+
descriptor.path,
|
|
2891
|
+
inspectArtifactGraphModuleSource(source, descriptor.path, metadata, {
|
|
2892
|
+
strict: security?.secure === true,
|
|
2893
|
+
}),
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
const order = assertArtifactGraphStaticImportClosure(metadata);
|
|
2897
|
+
const warnings = [...new Set([...plans.values()].flatMap((plan) => plan.warnings))]
|
|
2898
|
+
.sort();
|
|
2899
|
+
throwIfAborted(signal);
|
|
2900
|
+
return Object.freeze({ plans, order, warnings: Object.freeze(warnings) });
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
function assertArtifactGraphRuntimeInspection(inspection) {
|
|
657
2904
|
if (
|
|
658
|
-
|
|
659
|
-
||
|
|
2905
|
+
!inspection
|
|
2906
|
+
|| !(inspection.plans instanceof Map)
|
|
2907
|
+
|| !Array.isArray(inspection.order)
|
|
2908
|
+
|| !Array.isArray(inspection.warnings)
|
|
660
2909
|
) {
|
|
661
|
-
throw
|
|
662
|
-
"
|
|
663
|
-
"
|
|
2910
|
+
throw artifactGraphError(
|
|
2911
|
+
"artifact-graph-runtime-inspection-missing",
|
|
2912
|
+
"Artifact graph runtime inspection is required before executable materialization.",
|
|
664
2913
|
);
|
|
665
2914
|
}
|
|
666
|
-
|
|
667
|
-
|
|
2915
|
+
return inspection;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
function applyArtifactGraphModuleTransforms(plan, materializedByPath, guardCapability) {
|
|
2919
|
+
const replacements = [];
|
|
2920
|
+
for (let index = 0; index < plan.staticImports.length; index += 1) {
|
|
2921
|
+
const observed = plan.staticImports[index];
|
|
2922
|
+
const declared = plan.declarations.staticImports[index];
|
|
2923
|
+
const targetUrl = materializedByPath.get(declared.targetPath)?.moduleUrl;
|
|
2924
|
+
if (!targetUrl) {
|
|
2925
|
+
throw artifactGraphError(
|
|
2926
|
+
"artifact-graph-static-import-target-unmaterialized",
|
|
2927
|
+
`Static import target ${declared.targetPath} was not materialized before ${declared.modulePath}.`,
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
replacements.push({
|
|
2931
|
+
start: observed.start,
|
|
2932
|
+
end: observed.end,
|
|
2933
|
+
value: JSON.stringify(targetUrl),
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
for (let index = 0; index < plan.dynamicImports.length; index += 1) {
|
|
2937
|
+
const observed = plan.dynamicImports[index];
|
|
2938
|
+
const declared = plan.declarations.dynamicImports[index];
|
|
2939
|
+
replacements.push({
|
|
2940
|
+
start: observed.start,
|
|
2941
|
+
end: observed.end,
|
|
2942
|
+
value: `globalThis.${ARTIFACT_GRAPH_GUARDS}.dynamicImport(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},`,
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
for (let index = 0; index < plan.fetches.length; index += 1) {
|
|
2946
|
+
const observed = plan.fetches[index];
|
|
2947
|
+
const declared = plan.declarations.fetches[index];
|
|
2948
|
+
replacements.push({
|
|
2949
|
+
start: observed.start,
|
|
2950
|
+
end: observed.end,
|
|
2951
|
+
value: `globalThis.${ARTIFACT_GRAPH_GUARDS}.fetch(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},`,
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
for (let index = 0; index < plan.moduleWorkers.length; index += 1) {
|
|
2955
|
+
const observed = plan.moduleWorkers[index];
|
|
2956
|
+
const declared = plan.declarations.moduleWorkers[index];
|
|
2957
|
+
replacements.push({
|
|
2958
|
+
start: observed.start,
|
|
2959
|
+
end: observed.end,
|
|
2960
|
+
value: `globalThis.${ARTIFACT_GRAPH_GUARDS}.createWorker(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},`,
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
for (let index = 0; index < plan.cacheOpens.length; index += 1) {
|
|
2964
|
+
const observed = plan.cacheOpens[index];
|
|
2965
|
+
const declared = plan.declarations.cacheOpens[index];
|
|
2966
|
+
replacements.push({
|
|
2967
|
+
start: observed.start,
|
|
2968
|
+
end: observed.end,
|
|
2969
|
+
value: `globalThis.${ARTIFACT_GRAPH_GUARDS}.openCache(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},`,
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
for (const observed of plan.returnThisTransforms) {
|
|
2973
|
+
replacements.push({ start: observed.start, end: observed.end, value: "globalThis" });
|
|
2974
|
+
}
|
|
2975
|
+
for (let index = 0; index < plan.typedArrayConstructors.length; index += 1) {
|
|
2976
|
+
const observed = plan.typedArrayConstructors[index];
|
|
2977
|
+
const declared = plan.declarations.typedArrayConstructors[index];
|
|
2978
|
+
replacements.push({
|
|
2979
|
+
start: observed.start,
|
|
2980
|
+
end: observed.end,
|
|
2981
|
+
value: `(globalThis.${ARTIFACT_GRAPH_GUARDS}.typedArrayConstructor(${JSON.stringify(guardCapability)},${JSON.stringify(declared.modulePath)},${String(declared.occurrence)},${observed.receiver}))`,
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
2985
|
+
let source = plan.source;
|
|
2986
|
+
let previousStart = source.length;
|
|
2987
|
+
for (const replacement of replacements) {
|
|
2988
|
+
if (replacement.end > previousStart) {
|
|
2989
|
+
throw artifactGraphError(
|
|
2990
|
+
"artifact-graph-module-transform-overlap",
|
|
2991
|
+
"Artifact graph module transforms overlap and cannot be applied deterministically.",
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2994
|
+
source = `${source.slice(0, replacement.start)}${replacement.value}${source.slice(replacement.end)}`;
|
|
2995
|
+
previousStart = replacement.start;
|
|
2996
|
+
}
|
|
2997
|
+
return source;
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
function artifactGraphGuardCapability() {
|
|
3001
|
+
const crypto = globalThis.crypto;
|
|
3002
|
+
if (typeof crypto?.getRandomValues !== "function") {
|
|
3003
|
+
throw artifactGraphError(
|
|
3004
|
+
"artifact-graph-guard-capability-unavailable",
|
|
3005
|
+
"Authenticated artifact graph materialization requires cryptographic random values.",
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
const bytes = new Uint8Array(32);
|
|
3009
|
+
crypto.getRandomValues(bytes);
|
|
3010
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
async function blobDigest(blob) {
|
|
3014
|
+
const digest = createStreamingSha256();
|
|
3015
|
+
let bytes = 0;
|
|
3016
|
+
for await (const chunk of byteChunks(blob.stream())) {
|
|
3017
|
+
digest.update(chunk);
|
|
3018
|
+
bytes += chunk.byteLength;
|
|
3019
|
+
}
|
|
3020
|
+
return Object.freeze({ bytes, sha256: digest.digestHex() });
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
async function createArtifactGraphObjectUrls(admitted, metadata, inspection, security) {
|
|
3024
|
+
if (
|
|
3025
|
+
typeof PLATFORM_CREATE_OBJECT_URL !== "function"
|
|
3026
|
+
|| typeof PLATFORM_REVOKE_OBJECT_URL !== "function"
|
|
3027
|
+
|| typeof PLATFORM_FETCH !== "function"
|
|
3028
|
+
) {
|
|
3029
|
+
throw artifactGraphError(
|
|
3030
|
+
"artifact-graph-object-url-platform-unavailable",
|
|
3031
|
+
"Authenticated artifact graph materialization requires native Blob URL creation, revocation, and fetch.",
|
|
3032
|
+
);
|
|
3033
|
+
}
|
|
3034
|
+
const admittedByPath = new Map(admitted.files.map((entry) => [entry.descriptor.path, entry]));
|
|
3035
|
+
const materializedByPath = new Map();
|
|
3036
|
+
const created = [];
|
|
3037
|
+
const createdIdentities = new Set();
|
|
3038
|
+
const guardCapability = artifactGraphGuardCapability();
|
|
3039
|
+
const runtimeInspection = assertArtifactGraphRuntimeInspection(inspection);
|
|
3040
|
+
async function materialize(descriptor, body) {
|
|
3041
|
+
const blob = body instanceof Blob && body.type === descriptor.mediaType
|
|
3042
|
+
? body
|
|
3043
|
+
: new Blob([body], { type: descriptor.mediaType });
|
|
3044
|
+
const source = await blobDigest(blob);
|
|
3045
|
+
const transformed = ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(descriptor.kind);
|
|
3046
|
+
const expected = Object.freeze({
|
|
3047
|
+
bytes: transformed || security.checks.byteLength !== true
|
|
3048
|
+
? source.bytes
|
|
3049
|
+
: descriptor.bytes,
|
|
3050
|
+
sha256: transformed || security.checks.sha256 !== true
|
|
3051
|
+
? source.sha256
|
|
3052
|
+
: descriptor.sha256,
|
|
3053
|
+
});
|
|
3054
|
+
const moduleUrl = PLATFORM_CREATE_OBJECT_URL(blob);
|
|
3055
|
+
if (typeof moduleUrl !== "string" || !moduleUrl.startsWith("blob:")) {
|
|
3056
|
+
throw artifactGraphError(
|
|
3057
|
+
"artifact-graph-object-url-scheme-not-blob",
|
|
3058
|
+
`Materialized artifact graph file ${descriptor.path} did not produce a Blob URL.`,
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
if (createdIdentities.has(moduleUrl)) {
|
|
3062
|
+
throw artifactGraphError(
|
|
3063
|
+
"artifact-graph-object-url-identity-ambiguous",
|
|
3064
|
+
`Materialized artifact graph file ${descriptor.path} reused another file's Blob URL.`,
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
createdIdentities.add(moduleUrl);
|
|
3068
|
+
created.push(moduleUrl);
|
|
3069
|
+
let response;
|
|
3070
|
+
try {
|
|
3071
|
+
response = await PLATFORM_FETCH(moduleUrl, {
|
|
3072
|
+
method: "GET",
|
|
3073
|
+
credentials: "omit",
|
|
3074
|
+
redirect: "error",
|
|
3075
|
+
});
|
|
3076
|
+
} catch (error) {
|
|
3077
|
+
throw artifactGraphError(
|
|
3078
|
+
"artifact-graph-object-url-readback-unavailable",
|
|
3079
|
+
`Materialized artifact graph file ${descriptor.path} could not be read back from its Blob URL.`,
|
|
3080
|
+
error,
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
if (!response.ok) {
|
|
3084
|
+
throw artifactGraphError(
|
|
3085
|
+
"artifact-graph-object-url-readback-http-status-rejected",
|
|
3086
|
+
`Materialized artifact graph file ${descriptor.path} returned a non-success Blob URL response.`,
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
if (response.redirected || response.url !== moduleUrl) {
|
|
3090
|
+
throw artifactGraphError(
|
|
3091
|
+
"artifact-graph-object-url-readback-identity-mismatch",
|
|
3092
|
+
`Materialized artifact graph file ${descriptor.path} did not retain its exact Blob URL identity.`,
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
const observedMediaType = response.headers.get("content-type")?.split(";", 1)[0].trim() ?? "";
|
|
3096
|
+
if (observedMediaType !== descriptor.mediaType) {
|
|
3097
|
+
throw artifactGraphError(
|
|
3098
|
+
"artifact-graph-object-url-media-type-mismatch",
|
|
3099
|
+
`Materialized artifact graph file ${descriptor.path} did not retain its declared media type.`,
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
const observedBlob = await response.blob();
|
|
3103
|
+
const observed = await blobDigest(observedBlob);
|
|
3104
|
+
if (observed.bytes !== expected.bytes) {
|
|
3105
|
+
throw artifactGraphError(
|
|
3106
|
+
"artifact-graph-object-url-byte-length-mismatch",
|
|
3107
|
+
`Materialized artifact graph file ${descriptor.path} did not retain its exact byte length.`,
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
if (observed.sha256 !== expected.sha256) {
|
|
3111
|
+
throw artifactGraphError(
|
|
3112
|
+
"artifact-graph-object-url-sha256-mismatch",
|
|
3113
|
+
`Materialized artifact graph file ${descriptor.path} did not retain its exact bytes.`,
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
materializedByPath.set(descriptor.path, Object.freeze({
|
|
3117
|
+
kind: descriptor.kind,
|
|
3118
|
+
path: descriptor.path,
|
|
3119
|
+
sourceUrl: descriptor.sourceUrl,
|
|
3120
|
+
revision: descriptor.revision,
|
|
3121
|
+
license: descriptor.license,
|
|
3122
|
+
moduleUrl,
|
|
3123
|
+
mediaType: descriptor.mediaType,
|
|
3124
|
+
...(descriptor.sourceMediaType === descriptor.mediaType
|
|
3125
|
+
? {}
|
|
3126
|
+
: { sourceMediaType: descriptor.sourceMediaType }),
|
|
3127
|
+
bytes: descriptor.bytes,
|
|
3128
|
+
sha256: descriptor.sha256,
|
|
3129
|
+
runtimeRequestUrls: descriptor.runtimeRequestUrls,
|
|
3130
|
+
...(descriptor.redirectFinalOrigins.length < 1
|
|
3131
|
+
? {}
|
|
3132
|
+
: { redirectFinalOrigins: descriptor.redirectFinalOrigins }),
|
|
3133
|
+
}));
|
|
3134
|
+
}
|
|
3135
|
+
try {
|
|
3136
|
+
for (const descriptor of metadata.files) {
|
|
3137
|
+
if (ARTIFACT_GRAPH_JAVASCRIPT_KINDS.has(descriptor.kind)) continue;
|
|
3138
|
+
const admittedFile = admittedByPath.get(descriptor.path)?.file;
|
|
3139
|
+
if (!admittedFile) {
|
|
3140
|
+
throw artifactGraphError(
|
|
3141
|
+
"artifact-graph-materialized-source-file-missing",
|
|
3142
|
+
`Artifact graph file ${descriptor.path} is unavailable for materialization.`,
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
await materialize(descriptor, admittedFile);
|
|
3146
|
+
}
|
|
3147
|
+
for (const path of runtimeInspection.order) {
|
|
3148
|
+
const descriptor = metadata.filesByPath.get(path);
|
|
3149
|
+
const plan = runtimeInspection.plans.get(path);
|
|
3150
|
+
if (!descriptor || !plan) {
|
|
3151
|
+
throw artifactGraphError(
|
|
3152
|
+
"artifact-graph-runtime-inspection-incomplete",
|
|
3153
|
+
`Artifact graph runtime inspection is incomplete for ${path}.`,
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
await materialize(
|
|
3157
|
+
descriptor,
|
|
3158
|
+
applyArtifactGraphModuleTransforms(plan, materializedByPath, guardCapability),
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
3161
|
+
const files = metadata.files.map((descriptor) => {
|
|
3162
|
+
const materialized = materializedByPath.get(descriptor.path);
|
|
3163
|
+
if (!materialized) {
|
|
3164
|
+
throw artifactGraphError(
|
|
3165
|
+
"artifact-graph-materialization-incomplete",
|
|
3166
|
+
`Artifact graph file ${descriptor.path} was not materialized.`,
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
return materialized;
|
|
3170
|
+
});
|
|
3171
|
+
return Object.freeze({
|
|
3172
|
+
guardCapability,
|
|
3173
|
+
files: Object.freeze(files),
|
|
3174
|
+
release() {
|
|
3175
|
+
for (const url of created.splice(0).reverse()) {
|
|
3176
|
+
try {
|
|
3177
|
+
PLATFORM_REVOKE_OBJECT_URL(url);
|
|
3178
|
+
} catch {
|
|
3179
|
+
// Object URL revocation follows worker termination and is best effort.
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
},
|
|
3183
|
+
});
|
|
3184
|
+
} catch (error) {
|
|
3185
|
+
for (const url of created.splice(0).reverse()) {
|
|
3186
|
+
try {
|
|
3187
|
+
PLATFORM_REVOKE_OBJECT_URL(url);
|
|
3188
|
+
} catch {
|
|
3189
|
+
// Preserve the graph inspection or materialization error.
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
throw error;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function artifactGraphAdmissionStatus(cache, offline, security) {
|
|
3197
|
+
const verification = security.checks.byteLength && security.checks.sha256
|
|
3198
|
+
? "verified"
|
|
3199
|
+
: security.checks.byteLength || security.checks.sha256
|
|
3200
|
+
? "partially-checked"
|
|
3201
|
+
: "unchecked";
|
|
3202
|
+
const source = cache === "installed"
|
|
3203
|
+
? "network-dbopfs"
|
|
3204
|
+
: offline
|
|
3205
|
+
? "offline-dbopfs-cache"
|
|
3206
|
+
: "dbopfs-cache";
|
|
3207
|
+
return `artifact-graph-${source}-${verification}`;
|
|
668
3208
|
}
|
|
669
3209
|
|
|
670
3210
|
function createObjectUrls(files, factory) {
|
|
@@ -815,10 +3355,10 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
815
3355
|
}
|
|
816
3356
|
|
|
817
3357
|
async function removeUnlocked(authority) {
|
|
818
|
-
if (!
|
|
3358
|
+
if (!isSpeechArtifactAuthority(authority)) {
|
|
819
3359
|
throw new TypeError("Speech artifact removal requires an SDK-created authority.");
|
|
820
3360
|
}
|
|
821
|
-
const metadata =
|
|
3361
|
+
const metadata = artifactMetadata(authority);
|
|
822
3362
|
const names = storageNames(authority, metadata.files);
|
|
823
3363
|
const results = await Promise.all([
|
|
824
3364
|
removeEntry(names.manifest),
|
|
@@ -854,6 +3394,17 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
854
3394
|
return completed === file.size && digest.digestHex() === descriptor.sha256;
|
|
855
3395
|
}
|
|
856
3396
|
|
|
3397
|
+
function graphFileReason(descriptor, boundary) {
|
|
3398
|
+
const subject = descriptor.kind === "runtime-entrypoint-javascript"
|
|
3399
|
+
? "entrypoint"
|
|
3400
|
+
: descriptor.kind;
|
|
3401
|
+
return `artifact-graph-${subject}-${boundary}`;
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
function graphVerificationError(descriptor, boundary, message) {
|
|
3405
|
+
return artifactGraphError(graphFileReason(descriptor, boundary), message);
|
|
3406
|
+
}
|
|
3407
|
+
|
|
857
3408
|
function assertSecurityDescriptors(files, security) {
|
|
858
3409
|
for (const descriptor of files) {
|
|
859
3410
|
if (security.checks.byteLength && descriptor.bytes === null) {
|
|
@@ -870,7 +3421,8 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
870
3421
|
}
|
|
871
3422
|
|
|
872
3423
|
async function openCached(authority, { signal, onProgress, security } = {}) {
|
|
873
|
-
const
|
|
3424
|
+
const graph = ARTIFACT_GRAPHS.has(authority);
|
|
3425
|
+
const metadata = artifactMetadata(authority);
|
|
874
3426
|
const names = storageNames(authority, metadata.files);
|
|
875
3427
|
const manifest = await readManifest(names.manifest);
|
|
876
3428
|
if (!manifestMatches(manifest, authority, metadata.files)) {
|
|
@@ -883,7 +3435,12 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
883
3435
|
const descriptor = metadata.files[index];
|
|
884
3436
|
const file = await readFile(names.files[index]);
|
|
885
3437
|
const observed = manifest.files[index];
|
|
886
|
-
if (
|
|
3438
|
+
if (
|
|
3439
|
+
!file
|
|
3440
|
+
|| observed?.path !== descriptor.path
|
|
3441
|
+
|| observed?.bytes !== file.size
|
|
3442
|
+
|| (graph && observed?.sha256 !== descriptor.sha256)
|
|
3443
|
+
) {
|
|
887
3444
|
await removeUnlocked(authority);
|
|
888
3445
|
return null;
|
|
889
3446
|
}
|
|
@@ -893,7 +3450,11 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
893
3450
|
security,
|
|
894
3451
|
signal,
|
|
895
3452
|
onProgress,
|
|
896
|
-
|
|
3453
|
+
graph
|
|
3454
|
+
? security.checks.sha256
|
|
3455
|
+
? "artifact-graph-dbopfs-cache-rehash"
|
|
3456
|
+
: "artifact-graph-dbopfs-cache-readback"
|
|
3457
|
+
: "verify-cache",
|
|
897
3458
|
)) {
|
|
898
3459
|
await removeUnlocked(authority);
|
|
899
3460
|
return null;
|
|
@@ -901,17 +3462,24 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
901
3462
|
files.push({ descriptor, file });
|
|
902
3463
|
}
|
|
903
3464
|
try {
|
|
904
|
-
|
|
3465
|
+
const inspection = graph
|
|
3466
|
+
? await inspectArtifactGraphRuntime({ files }, metadata, signal, security)
|
|
3467
|
+
: (await assertSelfContainedRuntime({ files }, metadata, security), null);
|
|
905
3468
|
throwIfAborted(signal);
|
|
3469
|
+
return Object.freeze({
|
|
3470
|
+
files: Object.freeze(files),
|
|
3471
|
+
cache: "cached",
|
|
3472
|
+
inspection,
|
|
3473
|
+
});
|
|
906
3474
|
} catch (error) {
|
|
907
3475
|
await removeUnlocked(authority);
|
|
908
3476
|
throw error;
|
|
909
3477
|
}
|
|
910
|
-
return Object.freeze({ files: Object.freeze(files), cache: "cached" });
|
|
911
3478
|
}
|
|
912
3479
|
|
|
913
3480
|
async function install(authority, { signal, onProgress, security } = {}) {
|
|
914
|
-
const
|
|
3481
|
+
const graph = ARTIFACT_GRAPHS.has(authority);
|
|
3482
|
+
const metadata = artifactMetadata(authority);
|
|
915
3483
|
const names = storageNames(authority, metadata.files);
|
|
916
3484
|
const fetchFunction = fetchImpl ?? globalThis.fetch?.bind(globalThis);
|
|
917
3485
|
if (typeof fetchFunction !== "function") {
|
|
@@ -923,96 +3491,295 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
923
3491
|
for (let index = 0; index < metadata.files.length; index += 1) {
|
|
924
3492
|
throwIfAborted(signal);
|
|
925
3493
|
const descriptor = metadata.files[index];
|
|
3494
|
+
const sourceUrl = graph ? descriptor.sourceUrl : descriptor.url;
|
|
3495
|
+
const redirectFinalOrigins = graph
|
|
3496
|
+
? descriptor.redirectFinalOrigins
|
|
3497
|
+
: Object.freeze([]);
|
|
926
3498
|
let response;
|
|
927
3499
|
try {
|
|
928
|
-
response = await fetchFunction(
|
|
3500
|
+
response = await fetchFunction(sourceUrl, {
|
|
929
3501
|
cache: "no-store",
|
|
930
3502
|
credentials: "omit",
|
|
931
3503
|
mode: "cors",
|
|
932
|
-
redirect: "error",
|
|
3504
|
+
redirect: redirectFinalOrigins.length < 1 ? "error" : "follow",
|
|
933
3505
|
referrerPolicy: "no-referrer",
|
|
934
3506
|
signal,
|
|
935
3507
|
});
|
|
936
3508
|
} catch (error) {
|
|
937
3509
|
if (signal?.aborted || error?.name === "AbortError") throwIfAborted(signal);
|
|
938
|
-
|
|
3510
|
+
if (graph) {
|
|
3511
|
+
throw artifactGraphError(
|
|
3512
|
+
"artifact-graph-source-fetch-rejected",
|
|
3513
|
+
`Artifact graph source fetch was rejected for ${descriptor.path}.`,
|
|
3514
|
+
error,
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
throw speechError("ARCANE_AI_ARTIFACT_DOWNLOAD_FAILED", "A speech artifact source fetch was rejected.", error);
|
|
939
3518
|
}
|
|
940
|
-
|
|
941
|
-
|
|
3519
|
+
let responseBody;
|
|
3520
|
+
let responseHeaders;
|
|
3521
|
+
let responseOk;
|
|
3522
|
+
let responseRedirected;
|
|
3523
|
+
let responseStatus;
|
|
3524
|
+
let responseUrl;
|
|
3525
|
+
try {
|
|
3526
|
+
if (!response || typeof response !== "object") {
|
|
3527
|
+
throw new TypeError("The artifact fetch result is not an object.");
|
|
3528
|
+
}
|
|
3529
|
+
responseBody = response.body;
|
|
3530
|
+
responseHeaders = response.headers;
|
|
3531
|
+
responseOk = response.ok;
|
|
3532
|
+
responseRedirected = response.redirected;
|
|
3533
|
+
responseStatus = response.status;
|
|
3534
|
+
responseUrl = response.url;
|
|
3535
|
+
if (
|
|
3536
|
+
typeof responseOk !== "boolean"
|
|
3537
|
+
|| typeof responseRedirected !== "boolean"
|
|
3538
|
+
|| !Number.isInteger(responseStatus)
|
|
3539
|
+
|| typeof responseUrl !== "string"
|
|
3540
|
+
|| !responseHeaders
|
|
3541
|
+
|| typeof responseHeaders.get !== "function"
|
|
3542
|
+
|| (responseBody !== null && typeof responseBody?.getReader !== "function")
|
|
3543
|
+
) {
|
|
3544
|
+
throw new TypeError("The artifact fetch result is not a readable Fetch Response.");
|
|
3545
|
+
}
|
|
3546
|
+
} catch (error) {
|
|
3547
|
+
if (graph) {
|
|
3548
|
+
throw artifactGraphError(
|
|
3549
|
+
"artifact-graph-source-http-response-rejected",
|
|
3550
|
+
`Artifact graph source fetch for ${descriptor.path} did not return a readable Fetch Response.`,
|
|
3551
|
+
error,
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
942
3554
|
throw speechError(
|
|
943
3555
|
"ARCANE_AI_ARTIFACT_DOWNLOAD_FAILED",
|
|
944
|
-
|
|
3556
|
+
"A speech artifact source fetch did not return a readable Fetch Response.",
|
|
3557
|
+
error,
|
|
945
3558
|
);
|
|
946
3559
|
}
|
|
947
3560
|
let finalUrl = null;
|
|
3561
|
+
let finalUrlRecord = null;
|
|
948
3562
|
try {
|
|
949
|
-
|
|
950
|
-
? new URL(
|
|
3563
|
+
finalUrlRecord = responseUrl
|
|
3564
|
+
? new URL(responseUrl)
|
|
951
3565
|
: null;
|
|
3566
|
+
finalUrl = finalUrlRecord?.href ?? null;
|
|
952
3567
|
} catch {
|
|
3568
|
+
finalUrlRecord = null;
|
|
953
3569
|
finalUrl = null;
|
|
954
3570
|
}
|
|
955
|
-
if (
|
|
956
|
-
await
|
|
3571
|
+
if (graph && responseRedirected && !finalUrlRecord) {
|
|
3572
|
+
await responseBody?.cancel?.().catch(() => undefined);
|
|
3573
|
+
throw artifactGraphError(
|
|
3574
|
+
"artifact-graph-source-response-url-unreadable",
|
|
3575
|
+
`Artifact graph redirected source response for ${descriptor.path} did not expose a readable final URL.`,
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
if (graph && responseRedirected && redirectFinalOrigins.length < 1) {
|
|
3579
|
+
await responseBody?.cancel?.().catch(() => undefined);
|
|
3580
|
+
throw artifactGraphError(
|
|
3581
|
+
"artifact-graph-source-redirected",
|
|
3582
|
+
`Artifact graph source response for ${descriptor.path} did not match its immutable URL.`,
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
if (graph && responseRedirected && finalUrlRecord.protocol !== "https:") {
|
|
3586
|
+
await responseBody?.cancel?.().catch(() => undefined);
|
|
3587
|
+
throw artifactGraphError(
|
|
3588
|
+
"artifact-graph-source-response-url-protocol-not-https",
|
|
3589
|
+
`Artifact graph redirected source response for ${descriptor.path} did not end at HTTPS.`,
|
|
3590
|
+
);
|
|
3591
|
+
}
|
|
3592
|
+
if (
|
|
3593
|
+
graph
|
|
3594
|
+
&& responseRedirected
|
|
3595
|
+
&& (finalUrlRecord.username || finalUrlRecord.password)
|
|
3596
|
+
) {
|
|
3597
|
+
await responseBody?.cancel?.().catch(() => undefined);
|
|
3598
|
+
throw artifactGraphError(
|
|
3599
|
+
"artifact-graph-source-response-url-credentials-rejected",
|
|
3600
|
+
`Artifact graph redirected source response for ${descriptor.path} exposed credentials in its final URL.`,
|
|
3601
|
+
);
|
|
3602
|
+
}
|
|
3603
|
+
if (graph && responseRedirected && finalUrlRecord.hash) {
|
|
3604
|
+
await responseBody?.cancel?.().catch(() => undefined);
|
|
3605
|
+
throw artifactGraphError(
|
|
3606
|
+
"artifact-graph-source-response-url-fragment-rejected",
|
|
3607
|
+
`Artifact graph redirected source response for ${descriptor.path} exposed a fragment in its final URL.`,
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
if (
|
|
3611
|
+
graph
|
|
3612
|
+
&& responseRedirected
|
|
3613
|
+
&& !redirectFinalOrigins.includes(finalUrlRecord.origin)
|
|
3614
|
+
) {
|
|
3615
|
+
await responseBody?.cancel?.().catch(() => undefined);
|
|
3616
|
+
throw artifactGraphError(
|
|
3617
|
+
"artifact-graph-source-redirect-final-origin-mismatch",
|
|
3618
|
+
`Artifact graph redirected source response for ${descriptor.path} ended at an undeclared final origin.`,
|
|
3619
|
+
);
|
|
3620
|
+
}
|
|
3621
|
+
if (graph && !responseRedirected && finalUrl !== sourceUrl) {
|
|
3622
|
+
await responseBody?.cancel?.().catch(() => undefined);
|
|
3623
|
+
throw artifactGraphError(
|
|
3624
|
+
"artifact-graph-source-response-url-mismatch",
|
|
3625
|
+
`Artifact graph non-redirected source response for ${descriptor.path} did not retain its immutable URL.`,
|
|
3626
|
+
);
|
|
3627
|
+
}
|
|
3628
|
+
if (!graph && (responseRedirected || finalUrl !== sourceUrl)) {
|
|
3629
|
+
await responseBody?.cancel?.().catch(() => undefined);
|
|
957
3630
|
throw speechError(
|
|
958
3631
|
"ARCANE_AI_ARTIFACT_SOURCE_CHANGED",
|
|
959
3632
|
"A speech artifact response did not match its admitted URL.",
|
|
960
3633
|
);
|
|
961
3634
|
}
|
|
962
|
-
|
|
3635
|
+
if (!responseOk || !responseBody) {
|
|
3636
|
+
await responseBody?.cancel?.().catch(() => undefined);
|
|
3637
|
+
if (graph) {
|
|
3638
|
+
throw artifactGraphError(
|
|
3639
|
+
"artifact-graph-source-http-response-rejected",
|
|
3640
|
+
`Artifact graph source for ${descriptor.path} returned HTTP ${String(responseStatus)}.`,
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
throw speechError(
|
|
3644
|
+
"ARCANE_AI_ARTIFACT_DOWNLOAD_FAILED",
|
|
3645
|
+
`A speech artifact server returned HTTP ${String(responseStatus)}.`,
|
|
3646
|
+
);
|
|
3647
|
+
}
|
|
3648
|
+
const header = responseHeaders.get("content-length");
|
|
963
3649
|
const reportedBytes = header ? Number(header) : null;
|
|
3650
|
+
const contentEncoding = responseHeaders.get("content-encoding")?.trim() ?? "";
|
|
3651
|
+
if (graph) {
|
|
3652
|
+
const reportedMediaType = responseHeaders.get("content-type")
|
|
3653
|
+
?.split(";", 1)[0]
|
|
3654
|
+
?.trim()
|
|
3655
|
+
?.toLowerCase() ?? null;
|
|
3656
|
+
if (reportedMediaType !== descriptor.sourceMediaType) {
|
|
3657
|
+
await responseBody.cancel?.().catch(() => undefined);
|
|
3658
|
+
throw graphVerificationError(
|
|
3659
|
+
descriptor,
|
|
3660
|
+
descriptor.sourceMediaType === descriptor.mediaType
|
|
3661
|
+
? "media-type-mismatch"
|
|
3662
|
+
: "source-media-type-mismatch",
|
|
3663
|
+
`Artifact graph source media type for ${descriptor.path} did not match ${descriptor.sourceMediaType}.`,
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
964
3667
|
if (
|
|
965
3668
|
security.checks.byteLength
|
|
966
3669
|
&& Number.isSafeInteger(reportedBytes)
|
|
3670
|
+
&& (!graph || !contentEncoding)
|
|
967
3671
|
&& reportedBytes !== descriptor.bytes
|
|
968
3672
|
) {
|
|
969
|
-
await
|
|
3673
|
+
await responseBody.cancel?.().catch(() => undefined);
|
|
3674
|
+
if (graph) {
|
|
3675
|
+
throw graphVerificationError(
|
|
3676
|
+
descriptor,
|
|
3677
|
+
"byte-length-mismatch",
|
|
3678
|
+
`Artifact graph source Content-Length for ${descriptor.path} changed.`,
|
|
3679
|
+
);
|
|
3680
|
+
}
|
|
970
3681
|
throw speechError("ARCANE_AI_ARTIFACT_SIZE_MISMATCH", "Speech artifact Content-Length changed.");
|
|
971
3682
|
}
|
|
972
3683
|
const digest = security.checks.sha256
|
|
973
3684
|
? createStreamingSha256()
|
|
974
3685
|
: null;
|
|
975
|
-
const written = await writeFile(names.files[index],
|
|
3686
|
+
const written = await writeFile(names.files[index], responseBody, {
|
|
976
3687
|
signal,
|
|
977
3688
|
onChunk(chunk, completed) {
|
|
978
3689
|
digest?.update(chunk);
|
|
979
3690
|
if (security.checks.byteLength && completed > descriptor.bytes) {
|
|
3691
|
+
if (graph) {
|
|
3692
|
+
throw graphVerificationError(
|
|
3693
|
+
descriptor,
|
|
3694
|
+
"byte-length-mismatch",
|
|
3695
|
+
`Artifact graph source ${descriptor.path} exceeded its declared byte length.`,
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
980
3698
|
throw speechError("ARCANE_AI_ARTIFACT_SIZE_MISMATCH", "A speech artifact exceeded its expected size.");
|
|
981
3699
|
}
|
|
982
3700
|
onProgress?.(providerProgress(
|
|
983
|
-
"download",
|
|
3701
|
+
graph ? "artifact-graph-network-download" : "download",
|
|
984
3702
|
completed,
|
|
985
3703
|
security.checks.byteLength ? descriptor.bytes : null,
|
|
986
3704
|
));
|
|
987
3705
|
},
|
|
988
3706
|
});
|
|
989
3707
|
if (security.checks.byteLength && written !== descriptor.bytes) {
|
|
3708
|
+
if (graph) {
|
|
3709
|
+
throw graphVerificationError(
|
|
3710
|
+
descriptor,
|
|
3711
|
+
"byte-length-mismatch",
|
|
3712
|
+
`Artifact graph source ${descriptor.path} byte length changed.`,
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
990
3715
|
throw speechError("ARCANE_AI_ARTIFACT_SIZE_MISMATCH", "A speech artifact byte count changed.");
|
|
991
3716
|
}
|
|
992
3717
|
if (digest && digest.digestHex() !== descriptor.sha256) {
|
|
3718
|
+
if (graph) {
|
|
3719
|
+
throw graphVerificationError(
|
|
3720
|
+
descriptor,
|
|
3721
|
+
"sha256-mismatch",
|
|
3722
|
+
`Artifact graph source ${descriptor.path} SHA-256 changed.`,
|
|
3723
|
+
);
|
|
3724
|
+
}
|
|
993
3725
|
throw speechError("ARCANE_AI_ARTIFACT_DIGEST_MISMATCH", "A speech artifact SHA-256 changed.");
|
|
994
3726
|
}
|
|
995
3727
|
const file = await readFile(names.files[index]);
|
|
996
3728
|
if (!file || file.size !== written) {
|
|
3729
|
+
if (graph) {
|
|
3730
|
+
throw graphVerificationError(
|
|
3731
|
+
descriptor,
|
|
3732
|
+
"dbopfs-persisted-byte-length-mismatch",
|
|
3733
|
+
`DBOPFS did not preserve artifact graph file ${descriptor.path}.`,
|
|
3734
|
+
);
|
|
3735
|
+
}
|
|
997
3736
|
throw speechError("ARCANE_AI_ARTIFACT_CACHE_REJECTED", "DBOPFS did not preserve a speech artifact.");
|
|
998
3737
|
}
|
|
3738
|
+
if (!await verifyFile(
|
|
3739
|
+
file,
|
|
3740
|
+
descriptor,
|
|
3741
|
+
security,
|
|
3742
|
+
signal,
|
|
3743
|
+
onProgress,
|
|
3744
|
+
graph
|
|
3745
|
+
? security.checks.sha256
|
|
3746
|
+
? "artifact-graph-dbopfs-persisted-rehash"
|
|
3747
|
+
: "artifact-graph-dbopfs-persisted-readback"
|
|
3748
|
+
: "verify-cache",
|
|
3749
|
+
)) {
|
|
3750
|
+
if (graph) {
|
|
3751
|
+
throw graphVerificationError(
|
|
3752
|
+
descriptor,
|
|
3753
|
+
"dbopfs-persisted-sha256-mismatch",
|
|
3754
|
+
`DBOPFS persisted bytes for artifact graph file ${descriptor.path} were rejected during re-verification.`,
|
|
3755
|
+
);
|
|
3756
|
+
}
|
|
3757
|
+
throw speechError("ARCANE_AI_ARTIFACT_CACHE_REJECTED", "DBOPFS persisted bytes were rejected during verification.");
|
|
3758
|
+
}
|
|
999
3759
|
installed.push({ descriptor, file });
|
|
1000
3760
|
}
|
|
1001
|
-
|
|
3761
|
+
const inspection = graph
|
|
3762
|
+
? await inspectArtifactGraphRuntime({ files: installed }, metadata, signal, security)
|
|
3763
|
+
: (await assertSelfContainedRuntime({ files: installed }, metadata, security), null);
|
|
1002
3764
|
throwIfAborted(signal);
|
|
1003
3765
|
const manifest = Object.freeze({
|
|
1004
|
-
schema: MANIFEST_SCHEMA,
|
|
3766
|
+
schema: graph ? ARTIFACT_GRAPH_MANIFEST_SCHEMA : MANIFEST_SCHEMA,
|
|
1005
3767
|
complete: true,
|
|
1006
|
-
authority:
|
|
3768
|
+
authority: storedArtifactProjection(authority),
|
|
1007
3769
|
files: Object.freeze(installed.map(({ descriptor, file }) => Object.freeze({
|
|
1008
3770
|
path: descriptor.path,
|
|
1009
3771
|
bytes: file.size,
|
|
3772
|
+
...(graph ? { sha256: descriptor.sha256 } : {}),
|
|
1010
3773
|
}))),
|
|
1011
3774
|
completedAt: new Date().toISOString(),
|
|
1012
3775
|
});
|
|
1013
3776
|
const encoded = new TextEncoder().encode(`${JSON.stringify(manifest)}\n`);
|
|
1014
3777
|
await writeFile(names.manifest, encoded, { signal });
|
|
1015
|
-
return Object.freeze({
|
|
3778
|
+
return Object.freeze({
|
|
3779
|
+
files: Object.freeze(installed),
|
|
3780
|
+
cache: "installed",
|
|
3781
|
+
inspection,
|
|
3782
|
+
});
|
|
1016
3783
|
} catch (error) {
|
|
1017
3784
|
await removeUnlocked(authority).catch(() => undefined);
|
|
1018
3785
|
throw error;
|
|
@@ -1025,12 +3792,25 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1025
3792
|
offline = false,
|
|
1026
3793
|
security,
|
|
1027
3794
|
} = {}) {
|
|
1028
|
-
if (!
|
|
3795
|
+
if (!isSpeechArtifactAuthority(authority)) {
|
|
1029
3796
|
throw new TypeError("Speech artifact preparation requires an SDK-created authority.");
|
|
1030
3797
|
}
|
|
1031
3798
|
throwIfAborted(signal);
|
|
1032
|
-
const
|
|
1033
|
-
|
|
3799
|
+
const graph = ARTIFACT_GRAPHS.has(authority);
|
|
3800
|
+
let effectiveSecurity;
|
|
3801
|
+
try {
|
|
3802
|
+
effectiveSecurity = resolveModelSecurity({ load: security });
|
|
3803
|
+
} catch (error) {
|
|
3804
|
+
if (graph) {
|
|
3805
|
+
throw artifactGraphTypeError(
|
|
3806
|
+
"artifact-graph-load-security-contract-rejected",
|
|
3807
|
+
"Browser speech artifact graph load security does not satisfy the required contract.",
|
|
3808
|
+
error,
|
|
3809
|
+
);
|
|
3810
|
+
}
|
|
3811
|
+
throw error;
|
|
3812
|
+
}
|
|
3813
|
+
const metadata = artifactMetadata(authority);
|
|
1034
3814
|
assertSecurityDescriptors(metadata.files, effectiveSecurity);
|
|
1035
3815
|
const cached = await openCached(authority, {
|
|
1036
3816
|
signal,
|
|
@@ -1045,8 +3825,53 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1045
3825
|
security: effectiveSecurity,
|
|
1046
3826
|
}));
|
|
1047
3827
|
if (!admitted) {
|
|
3828
|
+
if (graph) {
|
|
3829
|
+
throw artifactGraphError(
|
|
3830
|
+
"artifact-graph-offline-cache-miss",
|
|
3831
|
+
"No complete admitted offline artifact graph cache is available.",
|
|
3832
|
+
);
|
|
3833
|
+
}
|
|
1048
3834
|
throw speechError("ARCANE_AI_ARTIFACT_OFFLINE_MISS", "No admitted offline speech cache is available.");
|
|
1049
3835
|
}
|
|
3836
|
+
if (graph) {
|
|
3837
|
+
const artifactGraphAdmission = artifactGraphAdmissionStatus(
|
|
3838
|
+
admitted.cache,
|
|
3839
|
+
offline,
|
|
3840
|
+
effectiveSecurity,
|
|
3841
|
+
);
|
|
3842
|
+
const materialized = await createArtifactGraphObjectUrls(
|
|
3843
|
+
admitted,
|
|
3844
|
+
metadata,
|
|
3845
|
+
admitted.inspection,
|
|
3846
|
+
effectiveSecurity,
|
|
3847
|
+
);
|
|
3848
|
+
const runtimeFiles = Object.freeze(materialized.files.filter((file) =>
|
|
3849
|
+
file.kind.startsWith("runtime-")));
|
|
3850
|
+
const modelFiles = Object.freeze(materialized.files.filter((file) =>
|
|
3851
|
+
!file.kind.startsWith("runtime-")));
|
|
3852
|
+
return Object.freeze({
|
|
3853
|
+
cache: artifactGraphAdmission,
|
|
3854
|
+
artifactGraphId: authority.identitySha256,
|
|
3855
|
+
artifactGraphAdmission,
|
|
3856
|
+
warnings: admitted.inspection.warnings,
|
|
3857
|
+
security: effectiveSecurity,
|
|
3858
|
+
runtime: Object.freeze({
|
|
3859
|
+
...metadata.runtime,
|
|
3860
|
+
files: runtimeFiles,
|
|
3861
|
+
edges: metadata.edges,
|
|
3862
|
+
transforms: metadata.transforms,
|
|
3863
|
+
warnings: admitted.inspection.warnings,
|
|
3864
|
+
guardCapability: materialized.guardCapability,
|
|
3865
|
+
artifactGraphId: authority.identitySha256,
|
|
3866
|
+
artifactGraphAdmission,
|
|
3867
|
+
}),
|
|
3868
|
+
model: Object.freeze({
|
|
3869
|
+
...metadata.model,
|
|
3870
|
+
files: modelFiles,
|
|
3871
|
+
}),
|
|
3872
|
+
release: materialized.release,
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
1050
3875
|
const materialized = createObjectUrls(admitted.files, objectUrlFactory);
|
|
1051
3876
|
const runtimeFiles = materialized.files.filter((file) => file.kind === "runtime");
|
|
1052
3877
|
const modelFiles = materialized.files.filter((file) => file.kind === "model");
|
|
@@ -1057,6 +3882,9 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1057
3882
|
version: metadata.runtime.version,
|
|
1058
3883
|
revision: metadata.runtime.revision,
|
|
1059
3884
|
entry: metadata.runtime.entry,
|
|
3885
|
+
...(metadata.runtime.wasmPaths === undefined
|
|
3886
|
+
? {}
|
|
3887
|
+
: { wasmPaths: metadata.runtime.wasmPaths }),
|
|
1060
3888
|
moduleGraph: "self-contained",
|
|
1061
3889
|
files: Object.freeze(runtimeFiles),
|
|
1062
3890
|
}),
|
|
@@ -1072,16 +3900,31 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1072
3900
|
}
|
|
1073
3901
|
|
|
1074
3902
|
function prepare(authority, options = {}) {
|
|
1075
|
-
if (!
|
|
3903
|
+
if (!isSpeechArtifactAuthority(authority)) {
|
|
1076
3904
|
return Promise.reject(new TypeError(
|
|
1077
3905
|
"Speech artifact preparation requires an SDK-created authority.",
|
|
1078
3906
|
));
|
|
1079
3907
|
}
|
|
1080
|
-
return serializeAuthority(authority, () =>
|
|
3908
|
+
return serializeAuthority(authority, async () => {
|
|
3909
|
+
try {
|
|
3910
|
+
return await prepareUnlocked(authority, options);
|
|
3911
|
+
} catch (error) {
|
|
3912
|
+
if (ARTIFACT_GRAPHS.has(authority) && options.signal?.aborted) {
|
|
3913
|
+
const cancelled = artifactGraphError(
|
|
3914
|
+
"artifact-graph-preparation-cancelled",
|
|
3915
|
+
"Artifact graph preparation was cancelled.",
|
|
3916
|
+
options.signal.reason ?? error,
|
|
3917
|
+
);
|
|
3918
|
+
cancelled.name = "AbortError";
|
|
3919
|
+
throw cancelled;
|
|
3920
|
+
}
|
|
3921
|
+
throw error;
|
|
3922
|
+
}
|
|
3923
|
+
});
|
|
1081
3924
|
}
|
|
1082
3925
|
|
|
1083
3926
|
function remove(authority) {
|
|
1084
|
-
if (!
|
|
3927
|
+
if (!isSpeechArtifactAuthority(authority)) {
|
|
1085
3928
|
return Promise.reject(new TypeError(
|
|
1086
3929
|
"Speech artifact removal requires an SDK-created authority.",
|
|
1087
3930
|
));
|
|
@@ -1103,6 +3946,14 @@ export function isBrowserSpeechAuthority(value) {
|
|
|
1103
3946
|
return AUTHORITIES.has(value);
|
|
1104
3947
|
}
|
|
1105
3948
|
|
|
3949
|
+
export function isBrowserSpeechArtifactGraph(value) {
|
|
3950
|
+
return ARTIFACT_GRAPHS.has(value);
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
export function isBrowserSpeechArtifactError(value) {
|
|
3954
|
+
return ARTIFACT_ERRORS.has(value);
|
|
3955
|
+
}
|
|
3956
|
+
|
|
1106
3957
|
export function isDbopfsSpeechArtifactStore(value) {
|
|
1107
3958
|
return STORES.has(value);
|
|
1108
3959
|
}
|