arcane-os 0.3.0 → 0.3.2
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 +27 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
- package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -146
- package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1050 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +40 -62
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +112 -779
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -187
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1252
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -677
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2960
- package/docs/reference/sdk-api.md +0 -6694
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
|
@@ -1,72 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
|
-
import {lstat,open,readFile,readdir,realpath} from 'node:fs/promises';
|
|
1
|
+
import {lstat,readFile,readdir,realpath} from 'node:fs/promises';
|
|
4
2
|
import path from 'node:path';
|
|
5
3
|
import {fileURLToPath} from 'node:url';
|
|
6
4
|
import {SDK_VERSION} from './constants.mjs';
|
|
7
5
|
|
|
8
|
-
const MANIFEST_NAME='ARCANE_SDK_BROWSER_RELEASE.json';
|
|
9
|
-
const BUILDER='arcane-sdk-browser-runtime-v1';
|
|
10
|
-
const PROTOCOL='arcane-sdk-browser-runtime/1';
|
|
11
|
-
const REPOSITORY='https://github.com/TheWizardNexus/arcane-os-sdk.git';
|
|
12
|
-
const SHA256_PATTERN=/^[a-f0-9]{64}$/u;
|
|
13
|
-
const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
14
|
-
const MAX_VERIFIED_FILE_BYTES=64*1024*1024;
|
|
15
6
|
const packageRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
16
7
|
const defaultRoot=path.join(packageRoot,'browser-runtime');
|
|
17
|
-
const canonicalManifestBytes=await readFile(path.join(defaultRoot,MANIFEST_NAME));
|
|
18
|
-
const canonicalRelease=JSON.parse(canonicalManifestBytes.toString('utf8'));
|
|
19
|
-
if(
|
|
20
|
-
canonicalRelease?.schemaVersion!==1
|
|
21
|
-
||canonicalRelease.builder!==BUILDER
|
|
22
|
-
||canonicalRelease.sdkVersion!==SDK_VERSION
|
|
23
|
-
||!SHA256_PATTERN.test(canonicalRelease.contentSha256)
|
|
24
|
-
){
|
|
25
|
-
throw new Error('The canonical SDK browser runtime receipt is invalid.');
|
|
26
|
-
}
|
|
27
|
-
export const SDK_BROWSER_RUNTIME_MANIFEST_SHA256=
|
|
28
|
-
createHash('sha256').update(canonicalManifestBytes).digest('hex');
|
|
29
|
-
export const SDK_BROWSER_RUNTIME_CONTENT_SHA256=canonicalRelease.contentSha256;
|
|
30
|
-
const issuedReceipts=new WeakSet();
|
|
31
|
-
const canonicalAiComponentsBytes=await readFile(
|
|
32
|
-
path.join(defaultRoot,'ai','ARCANE_AI_BROWSER_WASM_COMPONENTS.json')
|
|
33
|
-
);
|
|
34
|
-
const expectedAiComponents=Object.freeze(
|
|
35
|
-
JSON.parse(canonicalAiComponentsBytes.toString('utf8'))
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const admittedDependencyFiles=Object.freeze({
|
|
39
|
-
'event-pubsub':Object.freeze(['index.js','licence','package.json']),
|
|
40
|
-
'strong-type':Object.freeze(['index.js','licence','package.json'])
|
|
41
|
-
});
|
|
42
|
-
const exactRuntimeFileAuthorities=Object.freeze({
|
|
43
|
-
'ai/wllama/LICENCE':Object.freeze({
|
|
44
|
-
sourcePath:'node_modules/@wllama/wllama/LICENCE',
|
|
45
|
-
provenance:'vendor-package-identity'
|
|
46
|
-
}),
|
|
47
|
-
'ai/wllama/index.mjs':Object.freeze({
|
|
48
|
-
sourcePath:'browser-runtime/ai/wllama/index.mjs',
|
|
49
|
-
provenance:'deterministic-derived-vendor'
|
|
50
|
-
}),
|
|
51
|
-
'ai/wllama/llama.cpp-LICENSE':Object.freeze({
|
|
52
|
-
sourcePath:'browser-runtime/ai/wllama/llama.cpp-LICENSE',
|
|
53
|
-
provenance:'vendor-source-identity'
|
|
54
|
-
}),
|
|
55
|
-
'ai/wllama/wllama.wasm':Object.freeze({
|
|
56
|
-
sourcePath:'node_modules/@wllama/wllama/esm/wasm/wllama.wasm',
|
|
57
|
-
provenance:'vendor-package-identity'
|
|
58
|
-
}),
|
|
59
|
-
'dom-event-instrumentation.mjs':Object.freeze({
|
|
60
|
-
sourcePath:'src/dom-event-instrumentation.mjs',
|
|
61
|
-
provenance:'sdk-source-identity'
|
|
62
|
-
}),
|
|
63
|
-
'event-manager.mjs':Object.freeze({
|
|
64
|
-
sourcePath:'src/event-manager.mjs',
|
|
65
|
-
provenance:'sdk-source-identity'
|
|
66
|
-
})
|
|
67
|
-
});
|
|
68
8
|
|
|
69
|
-
function fail(message,code='
|
|
9
|
+
function fail(message,code='ARCANE_SDK_BROWSER_RUNTIME_INVALID'){
|
|
70
10
|
const error=new Error(message);
|
|
71
11
|
error.code=code;
|
|
72
12
|
throw error;
|
|
@@ -79,380 +19,36 @@ function throwIfAborted(signal){
|
|
|
79
19
|
throw error;
|
|
80
20
|
}
|
|
81
21
|
|
|
82
|
-
async function emit(onEvent,event){
|
|
83
|
-
if(typeof onEvent==='function')await onEvent(Object.freeze(event));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
22
|
function compareText(left,right){
|
|
87
23
|
const a=String(left);
|
|
88
24
|
const b=String(right);
|
|
89
25
|
return a<b?-1:a>b?1:0;
|
|
90
26
|
}
|
|
91
27
|
|
|
92
|
-
function
|
|
93
|
-
return Object.keys(value).sort(compareText).join('\0')===[...keys].sort(compareText).join('\0');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function safeInventoryPath(value,label='manifest'){
|
|
28
|
+
function safeRelativePath(value){
|
|
97
29
|
if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
|
|
98
|
-
||
|
|
99
|
-
||
|
|
100
|
-
|
|
101
|
-
||value.split('/').some(segment=>!segment||segment.startsWith('.'))){
|
|
102
|
-
fail(`SDK browser runtime ${label} contains an unsafe path: ${String(value)}.`);
|
|
30
|
+
||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
|
|
31
|
+
||value==='.'||value.startsWith('../')||value.includes('/../')){
|
|
32
|
+
fail(`SDK browser runtime contains an unsafe relative path: ${String(value)}.`);
|
|
103
33
|
}
|
|
104
34
|
return value;
|
|
105
35
|
}
|
|
106
36
|
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
function runtimeFileAuthority(value){
|
|
112
|
-
const relative=safeInventoryPath(value);
|
|
113
|
-
const exact=Object.hasOwn(exactRuntimeFileAuthorities,relative)
|
|
114
|
-
?exactRuntimeFileAuthorities[relative]
|
|
115
|
-
:null;
|
|
116
|
-
if(exact)return Object.freeze({path:relative,...exact});
|
|
117
|
-
|
|
118
|
-
if(relative.startsWith('ai/wllama/')){
|
|
119
|
-
fail(`SDK browser runtime Wllama asset is not admitted: ${relative}.`);
|
|
120
|
-
}
|
|
121
|
-
if(relative.startsWith('ai/')&&['.mjs','.json'].includes(path.posix.extname(relative))){
|
|
122
|
-
return Object.freeze({
|
|
123
|
-
path:relative,
|
|
124
|
-
sourcePath:`browser-runtime/${relative}`,
|
|
125
|
-
provenance:'sdk-source-identity'
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
if(relative.startsWith('dependencies/')){
|
|
129
|
-
const [directory,packageName,fileName,...extra]=relative.split('/');
|
|
130
|
-
const admitted=Object.hasOwn(admittedDependencyFiles,packageName)
|
|
131
|
-
?admittedDependencyFiles[packageName]
|
|
132
|
-
:null;
|
|
133
|
-
if(directory==='dependencies'&&extra.length===0&&admitted?.includes(fileName)){
|
|
134
|
-
return Object.freeze({
|
|
135
|
-
path:relative,
|
|
136
|
-
sourcePath:`node_modules/${packageName}/${fileName}`,
|
|
137
|
-
provenance:'vendor-package-identity'
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
fail(`SDK browser runtime path has no admitted source authority: ${relative}.`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function inventoryDirectories(files){
|
|
145
|
-
return Object.freeze([...new Set(files.flatMap(file=>{
|
|
146
|
-
const segments=file.path.split('/');
|
|
147
|
-
return segments.slice(0,-1).map((_,index)=>segments.slice(0,index+1).join('/'));
|
|
148
|
-
}))].sort(compareText));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function immutableInventory(files){
|
|
152
|
-
return Object.freeze(files.map(file=>Object.freeze({...file})));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function validateAiComponents(bytes,releaseFiles){
|
|
156
|
-
let receipt;
|
|
157
|
-
try{receipt=JSON.parse(bytes.toString('utf8'));}
|
|
158
|
-
catch(error){fail(`Arcane AI browser WASM component receipt is invalid JSON: ${error.message}`);}
|
|
159
|
-
if(JSON.stringify(receipt)!==JSON.stringify(expectedAiComponents)){
|
|
160
|
-
fail('Arcane AI browser WASM component authority is invalid.');
|
|
161
|
-
}
|
|
162
|
-
for(const component of receipt.components){
|
|
163
|
-
for(const file of component.files){
|
|
164
|
-
const declared=releaseFiles.find(candidate=>candidate.path===file.path);
|
|
165
|
-
if(!declared||declared.bytes!==file.bytes||declared.sha256!==file.sha256
|
|
166
|
-
||('sourcePath' in file&&declared.sourcePath!==file.sourcePath)){
|
|
167
|
-
fail(`Arcane AI browser WASM component projection drifted for ${file.path}.`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function requiresInstalledPackageSource(file){
|
|
174
|
-
return !file.sourcePath.startsWith('node_modules/@wllama/wllama/');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function validateSource(source){
|
|
178
|
-
if(!source||typeof source!=='object'||Array.isArray(source)
|
|
179
|
-
||!exactKeys(source,['authority','browserEntry','dependencies','protocol','repository'])
|
|
180
|
-
||source.authority!=='arcane-os-sdk'||source.browserEntry!=='arcane-os/event-manager'
|
|
181
|
-
||source.protocol!==PROTOCOL
|
|
182
|
-
||source.repository!==REPOSITORY||!Array.isArray(source.dependencies)
|
|
183
|
-
||source.dependencies.length!==3){
|
|
184
|
-
fail('SDK browser runtime manifest source authority is invalid.');
|
|
185
|
-
}
|
|
186
|
-
const expectedDependencies=[
|
|
187
|
-
{
|
|
188
|
-
name:'event-pubsub',
|
|
189
|
-
version:'6.1.0',
|
|
190
|
-
resolved:'https://registry.npmjs.org/event-pubsub/-/event-pubsub-6.1.0.tgz',
|
|
191
|
-
integrity:'sha512-FEMlhTxwqGM0hztTixG6FhVFXqp7Eq1ltk5mSreK6Mhy3xWWpLAzEUR6OMvMdNqT3jgSxA8JDhnhyAG3X4Xy7Q=='
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
name:'strong-type',
|
|
195
|
-
version:'2.0.0',
|
|
196
|
-
resolved:'https://registry.npmjs.org/strong-type/-/strong-type-2.0.0.tgz',
|
|
197
|
-
integrity:'sha512-HHrY9qYC7yn+5mlewiI3k9RQM9gZqGQsqbomZcd10Ks0h4RlX01nnkWbCe4AsVPCI6KaFvpkWm1nHMD+Ykup6g=='
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
name:'@wllama/wllama',
|
|
201
|
-
version:'3.6.0',
|
|
202
|
-
resolved:'https://registry.npmjs.org/@wllama/wllama/-/wllama-3.6.0.tgz',
|
|
203
|
-
integrity:'sha512-NN3ZBXqaaUwGXTQubkNvsCaLPjN2XVa0bVS40OYCE8zquYmRc2W3oHYEgwvuSWWDB8aUqTLyMioySCXNkcnD1w=='
|
|
204
|
-
}
|
|
205
|
-
];
|
|
206
|
-
for(const [index,expected] of expectedDependencies.entries()){
|
|
207
|
-
const actual=source.dependencies[index];
|
|
208
|
-
if(!actual||typeof actual!=='object'||Array.isArray(actual)
|
|
209
|
-
||!exactKeys(actual,['integrity','name','resolved','version'])
|
|
210
|
-
||actual.name!==expected.name||actual.version!==expected.version
|
|
211
|
-
||actual.resolved!==expected.resolved||actual.integrity!==expected.integrity){
|
|
212
|
-
fail('SDK browser runtime manifest dependency identity is invalid.');
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return Object.freeze({
|
|
216
|
-
...source,
|
|
217
|
-
dependencies:Object.freeze(source.dependencies.map(item=>Object.freeze({...item})))
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function validateRelease(value,{expectedContentSha256=SDK_BROWSER_RUNTIME_CONTENT_SHA256}={}){
|
|
222
|
-
if(!value||typeof value!=='object'||Array.isArray(value)){
|
|
223
|
-
fail('SDK browser runtime manifest must be a JSON object.');
|
|
224
|
-
}
|
|
225
|
-
if(!exactKeys(value,[
|
|
226
|
-
'builder','contentSha256','fileCount','files','schemaVersion','sdkVersion','source','totalBytes'
|
|
227
|
-
])){
|
|
228
|
-
fail('SDK browser runtime manifest contains missing or unsupported fields.');
|
|
229
|
-
}
|
|
230
|
-
if(value.schemaVersion!==1||value.builder!==BUILDER){
|
|
231
|
-
fail('SDK browser runtime manifest uses an unsupported schema or builder.');
|
|
232
|
-
}
|
|
233
|
-
if(value.sdkVersion!==SDK_VERSION){
|
|
234
|
-
fail('SDK browser runtime manifest sdkVersion is incompatible with this SDK.');
|
|
235
|
-
}
|
|
236
|
-
const source=validateSource(value.source);
|
|
237
|
-
if(!Array.isArray(value.files)||value.files.length===0||!Number.isSafeInteger(value.fileCount)
|
|
238
|
-
||value.fileCount!==value.files.length
|
|
239
|
-
||!Number.isSafeInteger(value.totalBytes)||value.totalBytes<0
|
|
240
|
-
||!SHA256_PATTERN.test(value.contentSha256)){
|
|
241
|
-
fail('SDK browser runtime manifest inventory summary is invalid.');
|
|
242
|
-
}
|
|
243
|
-
let previous='';
|
|
244
|
-
let totalBytes=0;
|
|
245
|
-
const collisionKeys=new Set();
|
|
246
|
-
const sourceCollisionKeys=new Set();
|
|
247
|
-
const files=value.files.map((entry,index)=>{
|
|
248
|
-
if(!entry||typeof entry!=='object'||Array.isArray(entry)
|
|
249
|
-
||!exactKeys(entry,['bytes','path','provenance','sha256','sourcePath'])){
|
|
250
|
-
fail(`SDK browser runtime manifest file ${index} is invalid.`);
|
|
251
|
-
}
|
|
252
|
-
const relative=safeInventoryPath(entry.path);
|
|
253
|
-
const sourcePath=safeInventoryPath(entry.sourcePath,'source inventory');
|
|
254
|
-
const expected=runtimeFileAuthority(relative);
|
|
255
|
-
const key=collisionKey(relative);
|
|
256
|
-
const sourceKey=collisionKey(sourcePath);
|
|
257
|
-
if(relative===MANIFEST_NAME||collisionKeys.has(key)
|
|
258
|
-
||sourceCollisionKeys.has(sourceKey)
|
|
259
|
-
||(previous&&compareText(previous,relative)>=0)
|
|
260
|
-
||sourcePath!==expected.sourcePath
|
|
261
|
-
||entry.provenance!==expected.provenance){
|
|
262
|
-
fail(`SDK browser runtime manifest inventory is invalid at ${relative}.`);
|
|
263
|
-
}
|
|
264
|
-
if(!Number.isSafeInteger(entry.bytes)||entry.bytes<0||!SHA256_PATTERN.test(entry.sha256)){
|
|
265
|
-
fail(`SDK browser runtime manifest metadata is invalid for ${relative}.`);
|
|
266
|
-
}
|
|
267
|
-
collisionKeys.add(key);
|
|
268
|
-
sourceCollisionKeys.add(sourceKey);
|
|
269
|
-
previous=relative;
|
|
270
|
-
totalBytes+=entry.bytes;
|
|
271
|
-
if(!Number.isSafeInteger(totalBytes)){
|
|
272
|
-
fail('SDK browser runtime manifest total byte count is too large.');
|
|
273
|
-
}
|
|
274
|
-
return {
|
|
275
|
-
path:relative,
|
|
276
|
-
sourcePath,
|
|
277
|
-
provenance:entry.provenance,
|
|
278
|
-
bytes:entry.bytes,
|
|
279
|
-
sha256:entry.sha256
|
|
280
|
-
};
|
|
281
|
-
});
|
|
282
|
-
if(totalBytes!==value.totalBytes){
|
|
283
|
-
fail('SDK browser runtime manifest totalBytes does not match its inventory.');
|
|
284
|
-
}
|
|
285
|
-
const aggregate=createHash('sha256').update(JSON.stringify(files)).digest('hex');
|
|
286
|
-
if(aggregate!==value.contentSha256){
|
|
287
|
-
fail('SDK browser runtime manifest contentSha256 does not match its inventory.');
|
|
288
|
-
}
|
|
289
|
-
if(expectedContentSha256!==null&&value.contentSha256!==expectedContentSha256){
|
|
290
|
-
fail('SDK browser runtime manifest does not match the trusted SDK browser closure.');
|
|
291
|
-
}
|
|
292
|
-
return Object.freeze({...value,source,files:immutableInventory(files)});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const validatedCanonicalRelease=validateRelease(canonicalRelease,{expectedContentSha256:null});
|
|
296
|
-
const expectedDirectories=inventoryDirectories(validatedCanonicalRelease.files);
|
|
297
|
-
|
|
298
|
-
function sameIdentity(before,after){
|
|
299
|
-
return before.dev===after.dev&&before.ino===after.ino&&before.size===after.size
|
|
300
|
-
&&before.mtimeNs===after.mtimeNs&&before.ctimeNs===after.ctimeNs
|
|
301
|
-
&&before.nlink===after.nlink;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function fileIdentity(info){
|
|
305
|
-
return Object.freeze({
|
|
306
|
-
device:String(info.dev),
|
|
307
|
-
inode:String(info.ino),
|
|
308
|
-
bytes:Number(info.size),
|
|
309
|
-
modifiedNanoseconds:String(info.mtimeNs),
|
|
310
|
-
changedNanoseconds:String(info.ctimeNs),
|
|
311
|
-
links:String(info.nlink)
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function identityMatches(info,identity){
|
|
316
|
-
return String(info.dev)===identity.device&&String(info.ino)===identity.inode
|
|
317
|
-
&&Number(info.size)===identity.bytes
|
|
318
|
-
&&String(info.mtimeNs)===identity.modifiedNanoseconds
|
|
319
|
-
&&String(info.ctimeNs)===identity.changedNanoseconds
|
|
320
|
-
&&String(info.nlink)===identity.links;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function containedPath(root,relative,label='file'){
|
|
324
|
-
const absolute=path.resolve(root,...safeInventoryPath(relative).split('/'));
|
|
325
|
-
const fromRoot=path.relative(root,absolute);
|
|
326
|
-
if(fromRoot.startsWith('..')||path.isAbsolute(fromRoot)){
|
|
327
|
-
fail(`SDK browser runtime ${label} escapes its root: ${relative}.`);
|
|
328
|
-
}
|
|
329
|
-
return absolute;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function assertCanonicalPath(root,filePath,relative){
|
|
333
|
-
const canonical=await realpath(filePath);
|
|
334
|
-
const fromRoot=path.relative(root,canonical);
|
|
37
|
+
function resolveContained(root,relativePath){
|
|
38
|
+
const normalized=safeRelativePath(relativePath);
|
|
39
|
+
const resolved=path.resolve(root,...normalized.split('/'));
|
|
40
|
+
const fromRoot=path.relative(root,resolved);
|
|
335
41
|
if(fromRoot.startsWith('..')||path.isAbsolute(fromRoot)){
|
|
336
|
-
fail(`SDK browser runtime path
|
|
42
|
+
fail(`SDK browser runtime path escapes its root: ${normalized}.`);
|
|
337
43
|
}
|
|
44
|
+
return resolved;
|
|
338
45
|
}
|
|
339
46
|
|
|
340
|
-
async function
|
|
341
|
-
const files=[];
|
|
342
|
-
const directories=[];
|
|
343
|
-
const expectedDirectorySet=new Set(expectedDirectories);
|
|
344
|
-
async function visit(directory,relativeRoot=''){
|
|
345
|
-
throwIfAborted(signal);
|
|
346
|
-
const entries=await readdir(directory,{withFileTypes:true});
|
|
347
|
-
entries.sort((left,right)=>compareText(left.name,right.name));
|
|
348
|
-
for(const entry of entries){
|
|
349
|
-
throwIfAborted(signal);
|
|
350
|
-
const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
|
|
351
|
-
if(relative===MANIFEST_NAME)continue;
|
|
352
|
-
safeInventoryPath(relative,'tree');
|
|
353
|
-
const absolute=path.join(directory,entry.name);
|
|
354
|
-
const info=await lstat(absolute,{bigint:true});
|
|
355
|
-
if(info.isSymbolicLink()){
|
|
356
|
-
fail(`SDK browser runtime contains a symbolic link or junction: ${relative}.`);
|
|
357
|
-
}
|
|
358
|
-
if(info.isDirectory()){
|
|
359
|
-
if(!expectedDirectorySet.has(relative)){
|
|
360
|
-
fail(`SDK browser runtime contains an unexpected directory: ${relative}.`);
|
|
361
|
-
}
|
|
362
|
-
await assertCanonicalPath(root,absolute,relative);
|
|
363
|
-
directories.push(Object.freeze({path:relative,...fileIdentity(info)}));
|
|
364
|
-
await visit(absolute,relative);
|
|
365
|
-
}else if(info.isFile()){
|
|
366
|
-
files.push(relative);
|
|
367
|
-
}else{
|
|
368
|
-
fail(`SDK browser runtime contains a non-file entry: ${relative}.`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
await visit(root);
|
|
373
|
-
files.sort(compareText);
|
|
374
|
-
directories.sort((left,right)=>compareText(left.path,right.path));
|
|
375
|
-
if(JSON.stringify(directories.map(item=>item.path))!==JSON.stringify(expectedDirectories)){
|
|
376
|
-
fail('SDK browser runtime directory inventory is incomplete.');
|
|
377
|
-
}
|
|
378
|
-
return {files,directories:Object.freeze(directories)};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async function openVerified(filePath,{expectedBytes,expectedIdentity,root,relative,signal}={}){
|
|
382
|
-
throwIfAborted(signal);
|
|
383
|
-
const before=await lstat(filePath,{bigint:true});
|
|
384
|
-
if(before.isSymbolicLink()||!before.isFile()
|
|
385
|
-
||(expectedIdentity&&!identityMatches(before,expectedIdentity))
|
|
386
|
-
||(expectedBytes!==undefined&&before.size!==BigInt(expectedBytes))){
|
|
387
|
-
fail(`SDK browser runtime file changed: ${relative}.`);
|
|
388
|
-
}
|
|
389
|
-
await assertCanonicalPath(root,filePath,relative);
|
|
390
|
-
let handle;
|
|
391
|
-
try{
|
|
392
|
-
handle=await open(filePath,READ_ONLY_NO_FOLLOW);
|
|
393
|
-
}catch(error){
|
|
394
|
-
if(error?.code==='ELOOP'){
|
|
395
|
-
fail(`SDK browser runtime file became a symbolic link: ${relative}.`);
|
|
396
|
-
}
|
|
397
|
-
throw error;
|
|
398
|
-
}
|
|
399
|
-
try{
|
|
400
|
-
const opened=await handle.stat({bigint:true});
|
|
401
|
-
if(!opened.isFile()||!sameIdentity(before,opened)
|
|
402
|
-
||(expectedIdentity&&!identityMatches(opened,expectedIdentity))){
|
|
403
|
-
fail(`SDK browser runtime file changed while it was being opened: ${relative}.`);
|
|
404
|
-
}
|
|
405
|
-
if(opened.size>BigInt(MAX_VERIFIED_FILE_BYTES)){
|
|
406
|
-
fail(`SDK browser runtime file exceeds the verification limit: ${relative}.`);
|
|
407
|
-
}
|
|
408
|
-
const bytes=await handle.readFile();
|
|
409
|
-
throwIfAborted(signal);
|
|
410
|
-
const after=await handle.stat({bigint:true});
|
|
411
|
-
if(!sameIdentity(opened,after)||bytes.length!==Number(after.size)){
|
|
412
|
-
fail(`SDK browser runtime file changed while it was being read: ${relative}.`);
|
|
413
|
-
}
|
|
414
|
-
const current=await lstat(filePath,{bigint:true});
|
|
415
|
-
if(current.isSymbolicLink()||!current.isFile()||!sameIdentity(after,current)){
|
|
416
|
-
fail(`SDK browser runtime path changed while it was being read: ${relative}.`);
|
|
417
|
-
}
|
|
418
|
-
await assertCanonicalPath(root,filePath,relative);
|
|
419
|
-
return {bytes,identity:fileIdentity(after)};
|
|
420
|
-
}finally{
|
|
421
|
-
await handle.close();
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
async function readRelease(root,signal){
|
|
426
|
-
const manifestPath=path.join(root,MANIFEST_NAME);
|
|
427
|
-
let result;
|
|
428
|
-
try{
|
|
429
|
-
result=await openVerified(manifestPath,{root,relative:MANIFEST_NAME,signal});
|
|
430
|
-
}catch(error){
|
|
431
|
-
if(error?.code==='ENOENT'){
|
|
432
|
-
fail(`SDK browser runtime manifest is missing: ${manifestPath}.`);
|
|
433
|
-
}
|
|
434
|
-
throw error;
|
|
435
|
-
}
|
|
436
|
-
let value;
|
|
437
|
-
try{
|
|
438
|
-
value=JSON.parse(result.bytes.toString('utf8'));
|
|
439
|
-
}catch(error){
|
|
440
|
-
fail(`SDK browser runtime manifest is not valid JSON: ${error.message}`);
|
|
441
|
-
}
|
|
442
|
-
return {
|
|
443
|
-
release:validateRelease(value),
|
|
444
|
-
manifestPath,
|
|
445
|
-
manifestSha256:createHash('sha256').update(result.bytes).digest('hex'),
|
|
446
|
-
manifestIdentity:result.identity
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
async function verifiedRoot(browserRuntimeRoot){
|
|
47
|
+
async function browserRuntimeLocation(browserRuntimeRoot){
|
|
451
48
|
const requested=path.resolve(browserRuntimeRoot);
|
|
452
49
|
let info;
|
|
453
|
-
try{
|
|
454
|
-
|
|
455
|
-
}catch(error){
|
|
50
|
+
try{info=await lstat(requested);}
|
|
51
|
+
catch(error){
|
|
456
52
|
if(error?.code==='ENOENT')fail(`SDK browser runtime root does not exist: ${requested}.`);
|
|
457
53
|
throw error;
|
|
458
54
|
}
|
|
@@ -460,240 +56,88 @@ async function verifiedRoot(browserRuntimeRoot){
|
|
|
460
56
|
fail('SDK browser runtime root must be a real directory.');
|
|
461
57
|
}
|
|
462
58
|
const canonical=await realpath(requested);
|
|
463
|
-
const canonicalInfo=await lstat(canonical
|
|
59
|
+
const canonicalInfo=await lstat(canonical);
|
|
464
60
|
if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()){
|
|
465
61
|
fail('SDK browser runtime root must be a real directory.');
|
|
466
62
|
}
|
|
467
|
-
return
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function directoryIdentityMatches(actual,expected){
|
|
471
|
-
return actual.path===expected.path&&actual.device===expected.device
|
|
472
|
-
&&actual.inode===expected.inode&&actual.bytes===expected.bytes
|
|
473
|
-
&&actual.modifiedNanoseconds===expected.modifiedNanoseconds
|
|
474
|
-
&&actual.changedNanoseconds===expected.changedNanoseconds
|
|
475
|
-
&&actual.links===expected.links;
|
|
63
|
+
return canonical;
|
|
476
64
|
}
|
|
477
65
|
|
|
478
|
-
async function
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if(JSON.stringify(scanned.files)!==JSON.stringify(receipt.files.map(file=>file.path))
|
|
487
|
-
||scanned.directories.length!==receipt.directories.length
|
|
488
|
-
||scanned.directories.some((item,index)=>!directoryIdentityMatches(item,receipt.directories[index]))){
|
|
489
|
-
fail('SDK browser runtime inventory changed after verification.');
|
|
490
|
-
}
|
|
491
|
-
const manifestInfo=await lstat(receipt.manifestPath,{bigint:true});
|
|
492
|
-
if(manifestInfo.isSymbolicLink()||!manifestInfo.isFile()
|
|
493
|
-
||!identityMatches(manifestInfo,receipt.manifestIdentity)){
|
|
494
|
-
fail('SDK browser runtime manifest changed after verification.');
|
|
495
|
-
}
|
|
496
|
-
for(const identity of receipt.identities){
|
|
497
|
-
throwIfAborted(signal);
|
|
498
|
-
const absolute=containedPath(receipt.canonicalLocation,identity.path);
|
|
499
|
-
const info=await lstat(absolute,{bigint:true});
|
|
500
|
-
if(info.isSymbolicLink()||!info.isFile()||!identityMatches(info,identity)){
|
|
501
|
-
fail(`SDK browser runtime file changed after verification: ${identity.path}.`);
|
|
502
|
-
}
|
|
503
|
-
await assertCanonicalPath(receipt.canonicalLocation,absolute,identity.path);
|
|
66
|
+
async function containedFile(root,relativePath){
|
|
67
|
+
const normalized=safeRelativePath(relativePath);
|
|
68
|
+
const filePath=resolveContained(root,normalized);
|
|
69
|
+
let info;
|
|
70
|
+
try{info=await lstat(filePath);}
|
|
71
|
+
catch(error){
|
|
72
|
+
if(error?.code==='ENOENT')fail(`SDK browser runtime file does not exist: ${normalized}.`);
|
|
73
|
+
throw error;
|
|
504
74
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
throwIfAborted(signal);
|
|
508
|
-
const file=receipt.files.find(candidate=>candidate.sourcePath===identity.path);
|
|
509
|
-
if(!file)fail(`SDK browser source identity is not declared: ${identity.path}.`);
|
|
510
|
-
const result=await openVerified(containedPath(sourceRoot,identity.path,'source file'),{
|
|
511
|
-
expectedBytes:file.bytes,
|
|
512
|
-
expectedIdentity:identity,
|
|
513
|
-
root:sourceRoot,
|
|
514
|
-
relative:identity.path,
|
|
515
|
-
signal
|
|
516
|
-
});
|
|
517
|
-
if(createHash('sha256').update(result.bytes).digest('hex')!==file.sha256){
|
|
518
|
-
fail(`SDK browser runtime package source changed after verification: ${identity.path}.`);
|
|
519
|
-
}
|
|
75
|
+
if(info.isSymbolicLink()||!info.isFile()){
|
|
76
|
+
fail(`SDK browser runtime path is not a real file: ${normalized}.`);
|
|
520
77
|
}
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
fail(
|
|
78
|
+
const canonicalFile=await realpath(filePath);
|
|
79
|
+
const fromRoot=path.relative(root,canonicalFile);
|
|
80
|
+
if(fromRoot.startsWith('..')||path.isAbsolute(fromRoot)){
|
|
81
|
+
fail(`SDK browser runtime path escapes its root: ${normalized}.`);
|
|
525
82
|
}
|
|
83
|
+
return canonicalFile;
|
|
526
84
|
}
|
|
527
85
|
|
|
528
86
|
export function getSdkBrowserRuntimeRoot(){
|
|
529
87
|
return defaultRoot;
|
|
530
88
|
}
|
|
531
89
|
|
|
532
|
-
export async function
|
|
533
|
-
throwIfAborted(signal);
|
|
534
|
-
const {canonical}=await verifiedRoot(browserRuntimeRoot);
|
|
535
|
-
return (await readRelease(canonical,signal)).release;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
export async function authenticateSdkBrowserRuntimeReceipt(receipt,{
|
|
90
|
+
export async function listSdkBrowserRuntimeFiles({
|
|
539
91
|
browserRuntimeRoot=defaultRoot,
|
|
540
92
|
signal
|
|
541
93
|
}={}){
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
94
|
+
throwIfAborted(signal);
|
|
95
|
+
const root=await browserRuntimeLocation(browserRuntimeRoot);
|
|
96
|
+
const files=[];
|
|
97
|
+
async function visit(directory,relativeRoot=''){
|
|
98
|
+
throwIfAborted(signal);
|
|
99
|
+
const entries=await readdir(directory,{withFileTypes:true});
|
|
100
|
+
entries.sort((left,right)=>compareText(left.name,right.name));
|
|
101
|
+
for(const entry of entries){
|
|
102
|
+
throwIfAborted(signal);
|
|
103
|
+
const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
|
|
104
|
+
safeRelativePath(relative);
|
|
105
|
+
const absolute=path.join(directory,entry.name);
|
|
106
|
+
const info=await lstat(absolute);
|
|
107
|
+
if(info.isSymbolicLink()){
|
|
108
|
+
fail(`SDK browser runtime contains a symbolic link or junction: ${relative}.`);
|
|
109
|
+
}
|
|
110
|
+
if(info.isDirectory())await visit(absolute,relative);
|
|
111
|
+
else if(info.isFile())files.push(relative);
|
|
112
|
+
else fail(`SDK browser runtime contains a non-file entry: ${relative}.`);
|
|
113
|
+
}
|
|
548
114
|
}
|
|
549
|
-
await
|
|
550
|
-
return
|
|
115
|
+
await visit(root);
|
|
116
|
+
return files.sort(compareText);
|
|
551
117
|
}
|
|
552
118
|
|
|
553
|
-
export async function
|
|
119
|
+
export async function readSdkBrowserRuntimeFile({
|
|
554
120
|
browserRuntimeRoot=defaultRoot,
|
|
555
121
|
relativePath,
|
|
556
122
|
signal
|
|
557
123
|
}={}){
|
|
558
|
-
if(!receipt||!issuedReceipts.has(receipt)){
|
|
559
|
-
fail('SDK browser runtime receipt was not issued by this SDK process.');
|
|
560
|
-
}
|
|
561
124
|
throwIfAborted(signal);
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if(!identity)fail(`Verified SDK browser runtime identity is missing for ${file.path}.`);
|
|
568
|
-
const {canonical}=await verifiedRoot(browserRuntimeRoot);
|
|
569
|
-
if(receipt.canonicalLocation!==canonical){
|
|
570
|
-
fail('SDK browser runtime receipt belongs to a different location.');
|
|
571
|
-
}
|
|
572
|
-
const rootInfo=await lstat(canonical,{bigint:true});
|
|
573
|
-
if(!identityMatches(rootInfo,receipt.rootIdentity)){
|
|
574
|
-
fail('SDK browser runtime root changed after verification.');
|
|
575
|
-
}
|
|
576
|
-
for(const directory of receipt.directories){
|
|
577
|
-
if(file.path.startsWith(`${directory.path}/`)){
|
|
578
|
-
const info=await lstat(containedPath(canonical,directory.path),{bigint:true});
|
|
579
|
-
if(info.isSymbolicLink()||!info.isDirectory()||!identityMatches(info,directory)){
|
|
580
|
-
fail(`SDK browser runtime directory changed after verification: ${directory.path}.`);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
const filePath=containedPath(canonical,file.path);
|
|
585
|
-
const result=await openVerified(filePath,{
|
|
586
|
-
expectedBytes:file.bytes,
|
|
587
|
-
expectedIdentity:identity,
|
|
588
|
-
root:canonical,
|
|
589
|
-
relative:file.path,
|
|
590
|
-
signal
|
|
591
|
-
});
|
|
592
|
-
if(createHash('sha256').update(result.bytes).digest('hex')!==file.sha256){
|
|
593
|
-
fail(`SDK browser runtime file hash changed: ${file.path}.`);
|
|
594
|
-
}
|
|
595
|
-
return result.bytes;
|
|
125
|
+
const root=await browserRuntimeLocation(browserRuntimeRoot);
|
|
126
|
+
const filePath=await containedFile(root,relativePath);
|
|
127
|
+
const content=await readFile(filePath);
|
|
128
|
+
throwIfAborted(signal);
|
|
129
|
+
return content;
|
|
596
130
|
}
|
|
597
131
|
|
|
598
|
-
export async function
|
|
132
|
+
export async function loadSdkBrowserRuntimeRelease({
|
|
599
133
|
browserRuntimeRoot=defaultRoot,
|
|
600
|
-
signal
|
|
601
|
-
onEvent
|
|
134
|
+
signal
|
|
602
135
|
}={}){
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
type:'sdk-browser-runtime.verify.started',
|
|
611
|
-
fileCount:release.fileCount,
|
|
612
|
-
totalBytes:release.totalBytes
|
|
613
|
-
});
|
|
614
|
-
const scanned=await scanTree(canonical,{signal});
|
|
615
|
-
if(JSON.stringify(scanned.files)!==JSON.stringify(release.files.map(file=>file.path))){
|
|
616
|
-
fail('SDK browser runtime file inventory does not match ARCANE_SDK_BROWSER_RELEASE.json.');
|
|
617
|
-
}
|
|
618
|
-
const identities=[];
|
|
619
|
-
const sourceIdentities=[];
|
|
620
|
-
const sourceRoot=path.dirname(canonical);
|
|
621
|
-
let aiComponentsBytes;
|
|
622
|
-
let verifiedBytes=0;
|
|
623
|
-
for(const [index,file] of release.files.entries()){
|
|
624
|
-
throwIfAborted(signal);
|
|
625
|
-
const absolute=containedPath(canonical,file.path);
|
|
626
|
-
const result=await openVerified(absolute,{
|
|
627
|
-
expectedBytes:file.bytes,
|
|
628
|
-
root:canonical,
|
|
629
|
-
relative:file.path,
|
|
630
|
-
signal
|
|
631
|
-
});
|
|
632
|
-
if(createHash('sha256').update(result.bytes).digest('hex')!==file.sha256){
|
|
633
|
-
fail(`SDK browser runtime integrity check failed for ${file.path}.`);
|
|
634
|
-
}
|
|
635
|
-
if(file.path==='ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json'){
|
|
636
|
-
aiComponentsBytes=result.bytes;
|
|
637
|
-
}
|
|
638
|
-
if(requiresInstalledPackageSource(file)){
|
|
639
|
-
const sourceResult=await openVerified(
|
|
640
|
-
containedPath(sourceRoot,file.sourcePath,'source file'),
|
|
641
|
-
{
|
|
642
|
-
expectedBytes:file.bytes,
|
|
643
|
-
root:sourceRoot,
|
|
644
|
-
relative:file.sourcePath,
|
|
645
|
-
signal
|
|
646
|
-
}
|
|
647
|
-
);
|
|
648
|
-
if(!result.bytes.equals(sourceResult.bytes)){
|
|
649
|
-
fail(
|
|
650
|
-
`SDK browser runtime file does not match its declared package source: `
|
|
651
|
-
+`${file.path} (${file.sourcePath}).`
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
sourceIdentities.push(Object.freeze({path:file.sourcePath,...sourceResult.identity}));
|
|
655
|
-
}
|
|
656
|
-
identities.push(Object.freeze({path:file.path,...result.identity}));
|
|
657
|
-
verifiedBytes+=file.bytes;
|
|
658
|
-
await emit(onEvent,{
|
|
659
|
-
type:'sdk-browser-runtime.verify.progress',
|
|
660
|
-
current:index+1,
|
|
661
|
-
total:release.fileCount,
|
|
662
|
-
verifiedBytes,
|
|
663
|
-
totalBytes:release.totalBytes,
|
|
664
|
-
path:file.path
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
if(!aiComponentsBytes){
|
|
668
|
-
fail('Arcane AI browser WASM component receipt is missing.');
|
|
669
|
-
}
|
|
670
|
-
validateAiComponents(aiComponentsBytes,release.files);
|
|
671
|
-
const receipt=Object.freeze({
|
|
672
|
-
schemaVersion:1,
|
|
673
|
-
kind:'arcane-sdk-browser-runtime-verification',
|
|
674
|
-
canonicalLocation:canonical,
|
|
675
|
-
rootIdentity,
|
|
676
|
-
manifestPath,
|
|
677
|
-
manifestSha256,
|
|
678
|
-
manifestIdentity,
|
|
679
|
-
builder:release.builder,
|
|
680
|
-
sdkVersion:release.sdkVersion,
|
|
681
|
-
source:release.source,
|
|
682
|
-
files:release.files,
|
|
683
|
-
fileCount:release.fileCount,
|
|
684
|
-
totalBytes:release.totalBytes,
|
|
685
|
-
contentSha256:release.contentSha256,
|
|
686
|
-
identities:Object.freeze(identities),
|
|
687
|
-
sourceIdentities:Object.freeze(sourceIdentities),
|
|
688
|
-
directories:scanned.directories
|
|
689
|
-
});
|
|
690
|
-
issuedReceipts.add(receipt);
|
|
691
|
-
await assertReceiptState(receipt,signal);
|
|
692
|
-
await emit(onEvent,{
|
|
693
|
-
type:'sdk-browser-runtime.verify.completed',
|
|
694
|
-
contentSha256:receipt.contentSha256,
|
|
695
|
-
fileCount:receipt.fileCount,
|
|
696
|
-
totalBytes:receipt.totalBytes
|
|
697
|
-
});
|
|
698
|
-
return receipt;
|
|
136
|
+
const root=await browserRuntimeLocation(browserRuntimeRoot);
|
|
137
|
+
const files=await listSdkBrowserRuntimeFiles({browserRuntimeRoot:root,signal});
|
|
138
|
+
return {
|
|
139
|
+
sdkVersion:SDK_VERSION,
|
|
140
|
+
browserRuntimeRoot:root,
|
|
141
|
+
files
|
|
142
|
+
};
|
|
699
143
|
}
|