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.
Files changed (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
package/src/runtime.mjs CHANGED
@@ -1,20 +1,11 @@
1
- import {createHash} from 'node:crypto';
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='ARCANE_INTEGRITY_FAILED'){
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 immutableFileInventory(files){
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(`Runtime manifest contains an unsafe path: ${String(value)}.`);
31
+ fail(`SDK runtime contains an unsafe relative path: ${String(value)}.`);
49
32
  }
50
33
  return value;
51
34
  }
52
35
 
53
- function validateRelease(value){
54
- if(!value||typeof value!=='object'||Array.isArray(value))fail('Runtime manifest must be a JSON object.');
55
- const releaseKeys=['builder','contentSha256','fileCount','files','schemaVersion','sdkVersion','source','totalBytes'];
56
- if(Object.keys(value).sort(compareText).join('\0')!==releaseKeys.sort(compareText).join('\0')){
57
- fail('Runtime manifest contains missing or unsupported fields.');
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
- let previous='';
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 readRelease(runtimeRoot){
114
- const manifestPath=path.join(runtimeRoot,RUNTIME_MANIFEST_NAME);
115
- let bytes;
116
- let manifestIdentity;
117
- try{
118
- const info=await lstat(manifestPath);
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
- let value;
135
- try{
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
- return {
141
- release:validateRelease(value),
142
- manifestPath,
143
- manifestSha256:createHash('sha256').update(bytes).digest('hex'),
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
- await visit(root);
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 fileIdentity(info){
177
- return Object.freeze({
178
- device:String(info.dev),
179
- inode:String(info.ino),
180
- bytes:Number(info.size),
181
- modifiedNanoseconds:String(info.mtimeNs),
182
- changedNanoseconds:String(info.ctimeNs),
183
- links:String(info.nlink)
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
- const rootAfter=await lstat(canonicalRoot,{bigint:true});
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
- async function hashExactFile(filePath,expectedBytes,signal){
244
- throwIfAborted(signal);
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 loadRuntimeRelease({runtimeRoot=path.join(sdkRoot,'runtime')}={}){
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 requestedRoot=path.resolve(runtimeRoot);
285
- const requestedInfo=await lstat(requestedRoot,{bigint:true});
286
- if(requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory()){
287
- fail('SDK runtime root must be a real directory.');
288
- }
289
- const canonicalRoot=await realpath(requestedRoot);
290
- if(receipt.canonicalLocation!==canonicalRoot){
291
- fail('Runtime verification receipt belongs to a different runtime location.');
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 assertRuntimeState({
294
- canonicalRoot,
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 readVerifiedRuntimeFile(receipt,{
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 normalized=safeInventoryPath(relativePath);
314
- const key=value=>process.platform==='win32'?value.toLowerCase():value;
315
- const file=receipt.files.find(candidate=>key(candidate.path)===key(normalized));
316
- if(!file)fail(`Path is not in the verified runtime inventory: ${normalized}.`);
317
- if(file.bytes>MAX_VERIFIED_RUNTIME_FILE_BYTES){
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 verifyRuntime({
131
+ export async function loadRuntimeRelease({
385
132
  runtimeRoot=path.join(sdkRoot,'runtime'),
386
- signal,
387
- onEvent
133
+ signal
388
134
  }={}){
389
- throwIfAborted(signal);
390
- const requestedRoot=path.resolve(runtimeRoot);
391
- let requestedInfo;
392
- try{requestedInfo=await lstat(requestedRoot,{bigint:true});}
393
- catch(error){
394
- if(error?.code==='ENOENT')fail(`SDK runtime root does not exist: ${requestedRoot}.`);
395
- throw error;
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
  }