arcane-os 0.3.1 → 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 +14 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
- package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
- 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 -148
- package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
- 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 +1042 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
- 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 +27 -67
- 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 +109 -1558
- 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 -185
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1295
- 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 -719
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2965
- package/docs/reference/sdk-api.md +0 -6698
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
package/src/runtime.mjs
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
|
-
import {lstat,open,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
|
-
export const RUNTIME_MANIFEST_NAME='ARCANE_RUNTIME_RELEASE.json';
|
|
9
|
-
const SHA256_PATTERN=/^[a-f0-9]{64}$/;
|
|
10
|
-
const SDK_RUNTIME_REPOSITORY='https://github.com/TheWizardNexus/arcane-os-sdk.git';
|
|
11
|
-
const LEGACY_ARCANE_REPOSITORY='https://github.com/TheWizardNexus/ARCANE-OS.git';
|
|
12
|
-
const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
13
|
-
const MAX_VERIFIED_RUNTIME_FILE_BYTES=64*1024*1024;
|
|
14
6
|
const sdkRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
15
|
-
const issuedReceipts=new WeakSet();
|
|
16
7
|
|
|
17
|
-
function fail(message,code='
|
|
8
|
+
function fail(message,code='ARCANE_RUNTIME_INVALID'){
|
|
18
9
|
const error=new Error(message);
|
|
19
10
|
error.code=code;
|
|
20
11
|
throw error;
|
|
@@ -27,436 +18,125 @@ function throwIfAborted(signal){
|
|
|
27
18
|
throw error;
|
|
28
19
|
}
|
|
29
20
|
|
|
30
|
-
async function emit(onEvent,event){
|
|
31
|
-
if(typeof onEvent==='function')await onEvent(Object.freeze(event));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
21
|
function compareText(left,right){
|
|
35
22
|
const a=String(left);
|
|
36
23
|
const b=String(right);
|
|
37
24
|
return a<b?-1:a>b?1:0;
|
|
38
25
|
}
|
|
39
26
|
|
|
40
|
-
function
|
|
41
|
-
return Object.freeze(files.map(file=>Object.freeze({...file})));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function safeInventoryPath(value){
|
|
27
|
+
function safeRelativePath(value){
|
|
45
28
|
if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
|
|
46
29
|
||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
|
|
47
30
|
||value==='.'||value.startsWith('../')||value.includes('/../')){
|
|
48
|
-
fail(`
|
|
31
|
+
fail(`SDK runtime contains an unsafe relative path: ${String(value)}.`);
|
|
49
32
|
}
|
|
50
33
|
return value;
|
|
51
34
|
}
|
|
52
35
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if(value.schemaVersion!==1||value.builder!=='arcane-sdk-runtime-v1'){
|
|
60
|
-
fail('Runtime manifest uses an unsupported schema or builder.');
|
|
61
|
-
}
|
|
62
|
-
if(value.sdkVersion!==SDK_VERSION)fail('Runtime manifest sdkVersion is incompatible with this SDK.');
|
|
63
|
-
if(!value.source||typeof value.source!=='object'
|
|
64
|
-
||Object.keys(value.source).sort(compareText).join('\0')!=='authority\0legacyProjection\0path\0protocol\0repository'
|
|
65
|
-
||value.source.authority!=='sdk-canonical'
|
|
66
|
-
||value.source.repository!==SDK_RUNTIME_REPOSITORY
|
|
67
|
-
||value.source.path!=='runtime/arcane'
|
|
68
|
-
||value.source.protocol!=='arcane/1'
|
|
69
|
-
||!value.source.legacyProjection
|
|
70
|
-
||typeof value.source.legacyProjection!=='object'
|
|
71
|
-
||Object.keys(value.source.legacyProjection).sort(compareText).join('\0')!=='bundleVersion\0commit\0repository'
|
|
72
|
-
||value.source.legacyProjection.repository!==LEGACY_ARCANE_REPOSITORY
|
|
73
|
-
||typeof value.source.legacyProjection.commit!=='string'
|
|
74
|
-
||!/^[a-f0-9]{40}$/.test(value.source.legacyProjection.commit)
|
|
75
|
-
||value.source.legacyProjection.bundleVersion!=='0.8.12'){
|
|
76
|
-
fail('Runtime manifest source identity is invalid.');
|
|
77
|
-
}
|
|
78
|
-
if(!Array.isArray(value.files)||!Number.isSafeInteger(value.fileCount)
|
|
79
|
-
||value.fileCount!==value.files.length||!Number.isSafeInteger(value.totalBytes)
|
|
80
|
-
||value.totalBytes<0||!SHA256_PATTERN.test(value.contentSha256)){
|
|
81
|
-
fail('Runtime manifest inventory summary is invalid.');
|
|
36
|
+
function resolveContained(root,relativePath){
|
|
37
|
+
const normalized=safeRelativePath(relativePath);
|
|
38
|
+
const resolved=path.resolve(root,...normalized.split('/'));
|
|
39
|
+
const fromRoot=path.relative(root,resolved);
|
|
40
|
+
if(fromRoot.startsWith('..')||path.isAbsolute(fromRoot)){
|
|
41
|
+
fail(`SDK runtime path escapes its root: ${normalized}.`);
|
|
82
42
|
}
|
|
83
|
-
|
|
84
|
-
let totalBytes=0;
|
|
85
|
-
const seen=new Set();
|
|
86
|
-
const files=value.files.map((entry,index)=>{
|
|
87
|
-
if(!entry||typeof entry!=='object'||Array.isArray(entry)){
|
|
88
|
-
fail(`Runtime manifest file ${index} is invalid.`);
|
|
89
|
-
}
|
|
90
|
-
if(Object.keys(entry).sort(compareText).join('\0')!=='bytes\0path\0sha256'){
|
|
91
|
-
fail(`Runtime manifest file ${index} contains missing or unsupported fields.`);
|
|
92
|
-
}
|
|
93
|
-
const relative=safeInventoryPath(entry.path);
|
|
94
|
-
if(relative===RUNTIME_MANIFEST_NAME||seen.has(relative)
|
|
95
|
-
||(previous&&compareText(previous,relative)>=0)){
|
|
96
|
-
fail(`Runtime manifest inventory is not unique and sorted at ${relative}.`);
|
|
97
|
-
}
|
|
98
|
-
if(!Number.isSafeInteger(entry.bytes)||entry.bytes<0||!SHA256_PATTERN.test(entry.sha256)){
|
|
99
|
-
fail(`Runtime manifest metadata is invalid for ${relative}.`);
|
|
100
|
-
}
|
|
101
|
-
seen.add(relative);
|
|
102
|
-
previous=relative;
|
|
103
|
-
totalBytes+=entry.bytes;
|
|
104
|
-
if(!Number.isSafeInteger(totalBytes))fail('Runtime manifest total byte count is too large.');
|
|
105
|
-
return {path:relative,bytes:entry.bytes,sha256:entry.sha256};
|
|
106
|
-
});
|
|
107
|
-
if(totalBytes!==value.totalBytes)fail('Runtime manifest totalBytes does not match its inventory.');
|
|
108
|
-
const aggregate=createHash('sha256').update(JSON.stringify(files)).digest('hex');
|
|
109
|
-
if(aggregate!==value.contentSha256)fail('Runtime manifest contentSha256 does not match its inventory.');
|
|
110
|
-
return {...value,files};
|
|
43
|
+
return resolved;
|
|
111
44
|
}
|
|
112
45
|
|
|
113
|
-
async function
|
|
114
|
-
const
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if(info.isSymbolicLink()||!info.isFile())fail('SDK runtime manifest must be a real file.');
|
|
120
|
-
const handle=await open(manifestPath,'r');
|
|
121
|
-
try{
|
|
122
|
-
const before=await handle.stat({bigint:true});
|
|
123
|
-
bytes=await handle.readFile();
|
|
124
|
-
const after=await handle.stat({bigint:true});
|
|
125
|
-
if(!sameIdentity(before,after))fail('SDK runtime manifest changed while it was being read.');
|
|
126
|
-
manifestIdentity=fileIdentity(after);
|
|
127
|
-
}finally{
|
|
128
|
-
await handle.close();
|
|
129
|
-
}
|
|
130
|
-
}catch(error){
|
|
131
|
-
if(error?.code==='ENOENT')fail(`SDK runtime manifest is missing: ${manifestPath}.`);
|
|
46
|
+
async function runtimeLocation(runtimeRoot){
|
|
47
|
+
const requested=path.resolve(runtimeRoot);
|
|
48
|
+
let info;
|
|
49
|
+
try{info=await lstat(requested);}
|
|
50
|
+
catch(error){
|
|
51
|
+
if(error?.code==='ENOENT')fail(`SDK runtime root does not exist: ${requested}.`);
|
|
132
52
|
throw error;
|
|
133
53
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
value=JSON.parse(bytes.toString('utf8'));
|
|
137
|
-
}catch(error){
|
|
138
|
-
fail(`SDK runtime manifest is not valid JSON: ${error.message}`);
|
|
54
|
+
if(info.isSymbolicLink()||!info.isDirectory()){
|
|
55
|
+
fail('SDK runtime root must be a real directory.');
|
|
139
56
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
manifestIdentity
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function listRuntimeFiles(root,{signal}={}){
|
|
149
|
-
const found=[];
|
|
150
|
-
async function visit(directory,relativeRoot=''){
|
|
151
|
-
throwIfAborted(signal);
|
|
152
|
-
const entries=await readdir(directory,{withFileTypes:true});
|
|
153
|
-
entries.sort((left,right)=>compareText(left.name,right.name));
|
|
154
|
-
for(const entry of entries){
|
|
155
|
-
throwIfAborted(signal);
|
|
156
|
-
const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
|
|
157
|
-
if(relative===RUNTIME_MANIFEST_NAME)continue;
|
|
158
|
-
const absolute=path.join(directory,entry.name);
|
|
159
|
-
const details=await lstat(absolute);
|
|
160
|
-
if(details.isSymbolicLink())fail(`SDK runtime contains a symbolic link or junction: ${relative}.`);
|
|
161
|
-
if(details.isDirectory())await visit(absolute,relative);
|
|
162
|
-
else if(details.isFile())found.push(relative);
|
|
163
|
-
else fail(`SDK runtime contains a non-file entry: ${relative}.`);
|
|
164
|
-
}
|
|
57
|
+
const canonical=await realpath(requested);
|
|
58
|
+
const canonicalInfo=await lstat(canonical);
|
|
59
|
+
if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()){
|
|
60
|
+
fail('SDK runtime root must be a real directory.');
|
|
165
61
|
}
|
|
166
|
-
|
|
167
|
-
return found.sort(compareText);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function sameIdentity(before,after){
|
|
171
|
-
return before.dev===after.dev&&before.ino===after.ino&&before.size===after.size
|
|
172
|
-
&&before.mtimeNs===after.mtimeNs&&before.ctimeNs===after.ctimeNs
|
|
173
|
-
&&before.nlink===after.nlink;
|
|
62
|
+
return canonical;
|
|
174
63
|
}
|
|
175
64
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function identityMatches(info,identity){
|
|
188
|
-
return String(info.dev)===identity.device
|
|
189
|
-
&&String(info.ino)===identity.inode
|
|
190
|
-
&&Number(info.size)===identity.bytes
|
|
191
|
-
&&String(info.mtimeNs)===identity.modifiedNanoseconds
|
|
192
|
-
&&String(info.ctimeNs)===identity.changedNanoseconds
|
|
193
|
-
&&String(info.nlink)===identity.links;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function assertRuntimeState({
|
|
197
|
-
canonicalRoot,
|
|
198
|
-
rootIdentity,
|
|
199
|
-
manifestPath,
|
|
200
|
-
manifestIdentity,
|
|
201
|
-
identities,
|
|
202
|
-
signal
|
|
203
|
-
}){
|
|
204
|
-
throwIfAborted(signal);
|
|
205
|
-
const rootBefore=await lstat(canonicalRoot,{bigint:true});
|
|
206
|
-
if(rootBefore.isSymbolicLink()||!rootBefore.isDirectory()
|
|
207
|
-
||!identityMatches(rootBefore,rootIdentity)){
|
|
208
|
-
fail('SDK runtime root changed after verification.');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const actualPaths=await listRuntimeFiles(canonicalRoot,{signal});
|
|
212
|
-
const expectedPaths=identities.map(identity=>identity.path);
|
|
213
|
-
if(JSON.stringify(actualPaths)!==JSON.stringify(expectedPaths)){
|
|
214
|
-
fail('SDK runtime file inventory changed after verification.');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const manifestInfo=await lstat(manifestPath,{bigint:true});
|
|
218
|
-
if(manifestInfo.isSymbolicLink()||!manifestInfo.isFile()
|
|
219
|
-
||!identityMatches(manifestInfo,manifestIdentity)){
|
|
220
|
-
fail('SDK runtime manifest changed after verification.');
|
|
221
|
-
}
|
|
222
|
-
for(const identity of identities){
|
|
223
|
-
throwIfAborted(signal);
|
|
224
|
-
const filePath=path.resolve(canonicalRoot,...identity.path.split('/'));
|
|
225
|
-
const relative=path.relative(canonicalRoot,filePath);
|
|
226
|
-
if(relative.startsWith('..')||path.isAbsolute(relative)){
|
|
227
|
-
fail(`Runtime receipt path escapes its root: ${identity.path}.`);
|
|
228
|
-
}
|
|
229
|
-
const info=await lstat(filePath,{bigint:true});
|
|
230
|
-
if(info.isSymbolicLink()||!info.isFile()||!identityMatches(info,identity)){
|
|
231
|
-
fail(`SDK runtime file changed after verification: ${identity.path}.`);
|
|
232
|
-
}
|
|
65
|
+
async function containedFile(root,relativePath){
|
|
66
|
+
const normalized=safeRelativePath(relativePath);
|
|
67
|
+
const filePath=resolveContained(root,normalized);
|
|
68
|
+
let info;
|
|
69
|
+
try{info=await lstat(filePath);}
|
|
70
|
+
catch(error){
|
|
71
|
+
if(error?.code==='ENOENT')fail(`SDK runtime file does not exist: ${normalized}.`);
|
|
72
|
+
throw error;
|
|
233
73
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if(rootAfter.isSymbolicLink()||!rootAfter.isDirectory()
|
|
237
|
-
||!identityMatches(rootAfter,rootIdentity)
|
|
238
|
-
||!sameIdentity(rootBefore,rootAfter)){
|
|
239
|
-
fail('SDK runtime root changed while its receipt was authenticated.');
|
|
74
|
+
if(info.isSymbolicLink()||!info.isFile()){
|
|
75
|
+
fail(`SDK runtime path is not a real file: ${normalized}.`);
|
|
240
76
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const handle=await open(filePath,'r');
|
|
246
|
-
const hash=createHash('sha256');
|
|
247
|
-
const buffer=Buffer.allocUnsafe(1024*1024);
|
|
248
|
-
try{
|
|
249
|
-
const before=await handle.stat({bigint:true});
|
|
250
|
-
if(!before.isFile()||before.size!==BigInt(expectedBytes)){
|
|
251
|
-
fail(`SDK runtime file size changed: ${filePath}.`);
|
|
252
|
-
}
|
|
253
|
-
while(true){
|
|
254
|
-
throwIfAborted(signal);
|
|
255
|
-
const {bytesRead}=await handle.read(buffer,0,buffer.length,null);
|
|
256
|
-
if(bytesRead===0)break;
|
|
257
|
-
hash.update(buffer.subarray(0,bytesRead));
|
|
258
|
-
}
|
|
259
|
-
const after=await handle.stat({bigint:true});
|
|
260
|
-
if(!sameIdentity(before,after))fail(`SDK runtime file changed while it was being verified: ${filePath}.`);
|
|
261
|
-
return {sha256:hash.digest('hex'),identity:fileIdentity(after)};
|
|
262
|
-
}finally{
|
|
263
|
-
await handle.close();
|
|
77
|
+
const canonicalFile=await realpath(filePath);
|
|
78
|
+
const fromRoot=path.relative(root,canonicalFile);
|
|
79
|
+
if(fromRoot.startsWith('..')||path.isAbsolute(fromRoot)){
|
|
80
|
+
fail(`SDK runtime path escapes its root: ${normalized}.`);
|
|
264
81
|
}
|
|
82
|
+
return canonicalFile;
|
|
265
83
|
}
|
|
266
84
|
|
|
267
85
|
export function getSdkRoot(){
|
|
268
86
|
return sdkRoot;
|
|
269
87
|
}
|
|
270
88
|
|
|
271
|
-
export async function
|
|
272
|
-
const resolved=path.resolve(runtimeRoot);
|
|
273
|
-
return (await readRelease(resolved)).release;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export async function authenticateRuntimeReceipt(receipt,{
|
|
89
|
+
export async function listRuntimeFiles({
|
|
277
90
|
runtimeRoot=path.join(sdkRoot,'runtime'),
|
|
278
91
|
signal
|
|
279
92
|
}={}){
|
|
280
|
-
if(!receipt||!issuedReceipts.has(receipt)){
|
|
281
|
-
fail('Runtime verification receipt was not issued by this SDK process.');
|
|
282
|
-
}
|
|
283
93
|
throwIfAborted(signal);
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
94
|
+
const root=await runtimeLocation(runtimeRoot);
|
|
95
|
+
const files=[];
|
|
96
|
+
async function visit(directory,relativeRoot=''){
|
|
97
|
+
throwIfAborted(signal);
|
|
98
|
+
const entries=await readdir(directory,{withFileTypes:true});
|
|
99
|
+
entries.sort((left,right)=>compareText(left.name,right.name));
|
|
100
|
+
for(const entry of entries){
|
|
101
|
+
throwIfAborted(signal);
|
|
102
|
+
const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
|
|
103
|
+
safeRelativePath(relative);
|
|
104
|
+
const absolute=path.join(directory,entry.name);
|
|
105
|
+
const info=await lstat(absolute);
|
|
106
|
+
if(info.isSymbolicLink()){
|
|
107
|
+
fail(`SDK runtime contains a symbolic link or junction: ${relative}.`);
|
|
108
|
+
}
|
|
109
|
+
if(info.isDirectory())await visit(absolute,relative);
|
|
110
|
+
else if(info.isFile())files.push(relative);
|
|
111
|
+
else fail(`SDK runtime contains a non-file entry: ${relative}.`);
|
|
112
|
+
}
|
|
292
113
|
}
|
|
293
|
-
await
|
|
294
|
-
|
|
295
|
-
rootIdentity:receipt.rootIdentity,
|
|
296
|
-
manifestPath:receipt.manifestPath,
|
|
297
|
-
manifestIdentity:receipt.manifestIdentity,
|
|
298
|
-
identities:receipt.identities,
|
|
299
|
-
signal
|
|
300
|
-
});
|
|
301
|
-
return receipt;
|
|
114
|
+
await visit(root);
|
|
115
|
+
return files.sort(compareText);
|
|
302
116
|
}
|
|
303
117
|
|
|
304
|
-
export async function
|
|
118
|
+
export async function readRuntimeFile({
|
|
305
119
|
runtimeRoot=path.join(sdkRoot,'runtime'),
|
|
306
120
|
relativePath,
|
|
307
121
|
signal
|
|
308
122
|
}={}){
|
|
309
|
-
if(!receipt||!issuedReceipts.has(receipt)){
|
|
310
|
-
fail('Runtime verification receipt was not issued by this SDK process.');
|
|
311
|
-
}
|
|
312
123
|
throwIfAborted(signal);
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
fail(
|
|
319
|
-
`Verified runtime file exceeds the ${MAX_VERIFIED_RUNTIME_FILE_BYTES}-byte development serving limit: ${file.path}.`
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
const identity=receipt.identities.find(candidate=>key(candidate.path)===key(file.path));
|
|
323
|
-
if(!identity)fail(`Verified runtime identity is missing for ${file.path}.`);
|
|
324
|
-
|
|
325
|
-
const requestedRoot=path.resolve(runtimeRoot);
|
|
326
|
-
const requestedInfo=await lstat(requestedRoot,{bigint:true});
|
|
327
|
-
if(requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory()){
|
|
328
|
-
fail('SDK runtime root must be a real directory.');
|
|
329
|
-
}
|
|
330
|
-
const canonicalRoot=await realpath(requestedRoot);
|
|
331
|
-
if(receipt.canonicalLocation!==canonicalRoot){
|
|
332
|
-
fail('Runtime verification receipt belongs to a different runtime location.');
|
|
333
|
-
}
|
|
334
|
-
const rootInfo=await lstat(canonicalRoot,{bigint:true});
|
|
335
|
-
if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()
|
|
336
|
-
||!identityMatches(rootInfo,receipt.rootIdentity)){
|
|
337
|
-
fail('SDK runtime root changed after verification.');
|
|
338
|
-
}
|
|
339
|
-
const filePath=path.resolve(canonicalRoot,...file.path.split('/'));
|
|
340
|
-
const relative=path.relative(canonicalRoot,filePath);
|
|
341
|
-
if(relative.startsWith('..')||path.isAbsolute(relative)){
|
|
342
|
-
fail(`Runtime receipt path escapes its root: ${file.path}.`);
|
|
343
|
-
}
|
|
344
|
-
const before=await lstat(filePath,{bigint:true});
|
|
345
|
-
if(before.isSymbolicLink()||!before.isFile()||!identityMatches(before,identity)){
|
|
346
|
-
fail(`SDK runtime file changed after verification: ${file.path}.`);
|
|
347
|
-
}
|
|
348
|
-
let handle;
|
|
349
|
-
try{
|
|
350
|
-
handle=await open(filePath,READ_ONLY_NO_FOLLOW);
|
|
351
|
-
}catch(error){
|
|
352
|
-
if(error?.code==='ELOOP')fail(`SDK runtime file became a symbolic link: ${file.path}.`);
|
|
353
|
-
throw error;
|
|
354
|
-
}
|
|
355
|
-
try{
|
|
356
|
-
const opened=await handle.stat({bigint:true});
|
|
357
|
-
if(!opened.isFile()||!identityMatches(opened,identity)){
|
|
358
|
-
fail(`SDK runtime file changed while it was being opened: ${file.path}.`);
|
|
359
|
-
}
|
|
360
|
-
const bytes=await handle.readFile();
|
|
361
|
-
throwIfAborted(signal);
|
|
362
|
-
const after=await handle.stat({bigint:true});
|
|
363
|
-
if(!identityMatches(after,identity)||bytes.length!==file.bytes){
|
|
364
|
-
fail(`SDK runtime file changed while it was being read: ${file.path}.`);
|
|
365
|
-
}
|
|
366
|
-
if(createHash('sha256').update(bytes).digest('hex')!==file.sha256){
|
|
367
|
-
fail(`SDK runtime file hash changed: ${file.path}.`);
|
|
368
|
-
}
|
|
369
|
-
const current=await lstat(filePath,{bigint:true});
|
|
370
|
-
if(current.isSymbolicLink()||!current.isFile()||!identityMatches(current,identity)){
|
|
371
|
-
fail(`SDK runtime path changed while it was being read: ${file.path}.`);
|
|
372
|
-
}
|
|
373
|
-
const canonicalFile=await realpath(filePath);
|
|
374
|
-
const canonicalRelative=path.relative(canonicalRoot,canonicalFile);
|
|
375
|
-
if(canonicalRelative.startsWith('..')||path.isAbsolute(canonicalRelative)){
|
|
376
|
-
fail(`SDK runtime path left its root: ${file.path}.`);
|
|
377
|
-
}
|
|
378
|
-
return bytes;
|
|
379
|
-
}finally{
|
|
380
|
-
await handle.close();
|
|
381
|
-
}
|
|
124
|
+
const root=await runtimeLocation(runtimeRoot);
|
|
125
|
+
const filePath=await containedFile(root,relativePath);
|
|
126
|
+
const content=await readFile(filePath);
|
|
127
|
+
throwIfAborted(signal);
|
|
128
|
+
return content;
|
|
382
129
|
}
|
|
383
130
|
|
|
384
|
-
export async function
|
|
131
|
+
export async function loadRuntimeRelease({
|
|
385
132
|
runtimeRoot=path.join(sdkRoot,'runtime'),
|
|
386
|
-
signal
|
|
387
|
-
onEvent
|
|
133
|
+
signal
|
|
388
134
|
}={}){
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
if(requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory())fail('SDK runtime root must be a real directory.');
|
|
398
|
-
const canonicalRoot=await realpath(requestedRoot);
|
|
399
|
-
const rootInfo=await lstat(canonicalRoot,{bigint:true});
|
|
400
|
-
if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory())fail('SDK runtime root must be a real directory.');
|
|
401
|
-
const rootIdentity=fileIdentity(rootInfo);
|
|
402
|
-
const {release,manifestPath,manifestSha256,manifestIdentity}=await readRelease(canonicalRoot);
|
|
403
|
-
await emit(onEvent,{type:'runtime.verify.started',fileCount:release.fileCount,totalBytes:release.totalBytes});
|
|
404
|
-
const actualPaths=await listRuntimeFiles(canonicalRoot,{signal});
|
|
405
|
-
const expectedPaths=release.files.map(file=>file.path);
|
|
406
|
-
if(JSON.stringify(actualPaths)!==JSON.stringify(expectedPaths)){
|
|
407
|
-
fail('SDK runtime file inventory does not match ARCANE_RUNTIME_RELEASE.json.');
|
|
408
|
-
}
|
|
409
|
-
const identities=[];
|
|
410
|
-
let verifiedBytes=0;
|
|
411
|
-
for(const [index,file] of release.files.entries()){
|
|
412
|
-
throwIfAborted(signal);
|
|
413
|
-
const absolute=path.resolve(canonicalRoot,...file.path.split('/'));
|
|
414
|
-
const relative=path.relative(canonicalRoot,absolute);
|
|
415
|
-
if(relative.startsWith('..')||path.isAbsolute(relative))fail(`Runtime file escapes its root: ${file.path}.`);
|
|
416
|
-
const result=await hashExactFile(absolute,file.bytes,signal);
|
|
417
|
-
if(result.sha256!==file.sha256)fail(`SDK runtime integrity check failed for ${file.path}.`);
|
|
418
|
-
verifiedBytes+=file.bytes;
|
|
419
|
-
identities.push({path:file.path,...result.identity});
|
|
420
|
-
await emit(onEvent,{
|
|
421
|
-
type:'runtime.verify.progress',
|
|
422
|
-
current:index+1,
|
|
423
|
-
total:release.fileCount,
|
|
424
|
-
verifiedBytes,
|
|
425
|
-
totalBytes:release.totalBytes,
|
|
426
|
-
path:file.path
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
const frozenIdentities=Object.freeze(identities.map(Object.freeze));
|
|
430
|
-
await assertRuntimeState({
|
|
431
|
-
canonicalRoot,
|
|
432
|
-
rootIdentity,
|
|
433
|
-
manifestPath,
|
|
434
|
-
manifestIdentity,
|
|
435
|
-
identities:frozenIdentities,
|
|
436
|
-
signal
|
|
437
|
-
});
|
|
438
|
-
const receipt=Object.freeze({
|
|
439
|
-
schemaVersion:1,
|
|
440
|
-
kind:'arcane-sdk-runtime-verification',
|
|
441
|
-
canonicalLocation:canonicalRoot,
|
|
442
|
-
rootIdentity,
|
|
443
|
-
manifestPath,
|
|
444
|
-
manifestSha256,
|
|
445
|
-
manifestIdentity,
|
|
446
|
-
sdkVersion:release.sdkVersion,
|
|
447
|
-
source:Object.freeze({...release.source}),
|
|
448
|
-
files:immutableFileInventory(release.files),
|
|
449
|
-
fileCount:release.fileCount,
|
|
450
|
-
totalBytes:release.totalBytes,
|
|
451
|
-
contentSha256:release.contentSha256,
|
|
452
|
-
identities:frozenIdentities
|
|
453
|
-
});
|
|
454
|
-
issuedReceipts.add(receipt);
|
|
455
|
-
await emit(onEvent,{
|
|
456
|
-
type:'runtime.verify.completed',
|
|
457
|
-
contentSha256:receipt.contentSha256,
|
|
458
|
-
fileCount:receipt.fileCount,
|
|
459
|
-
totalBytes:receipt.totalBytes
|
|
460
|
-
});
|
|
461
|
-
return receipt;
|
|
135
|
+
const root=await runtimeLocation(runtimeRoot);
|
|
136
|
+
const files=await listRuntimeFiles({runtimeRoot:root,signal});
|
|
137
|
+
return {
|
|
138
|
+
sdkVersion:SDK_VERSION,
|
|
139
|
+
runtimeRoot:root,
|
|
140
|
+
files
|
|
141
|
+
};
|
|
462
142
|
}
|