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
@@ -1,72 +1,12 @@
1
- import {createHash} from 'node:crypto';
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='ARCANE_SDK_BROWSER_INTEGRITY_FAILED'){
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 exactKeys(value,keys){
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
- ||value.normalize('NFC')!==value||path.posix.isAbsolute(value)
99
- ||path.posix.normalize(value)!==value||value==='.'||value.startsWith('../')
100
- ||value.includes('/../')
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 collisionKey(value){
108
- return value.normalize('NFC').toLocaleLowerCase('en-US');
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 left its root: ${relative}.`);
42
+ fail(`SDK browser runtime path escapes its root: ${normalized}.`);
337
43
  }
44
+ return resolved;
338
45
  }
339
46
 
340
- async function scanTree(root,{signal}={}){
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
- info=await lstat(requested,{bigint:true});
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,{bigint:true});
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 {canonical,identity:fileIdentity(canonicalInfo)};
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 assertReceiptState(receipt,signal){
479
- throwIfAborted(signal);
480
- const rootBefore=await lstat(receipt.canonicalLocation,{bigint:true});
481
- if(rootBefore.isSymbolicLink()||!rootBefore.isDirectory()
482
- ||!identityMatches(rootBefore,receipt.rootIdentity)){
483
- fail('SDK browser runtime root changed after verification.');
484
- }
485
- const scanned=await scanTree(receipt.canonicalLocation,{signal});
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
- const sourceRoot=path.dirname(receipt.canonicalLocation);
506
- for(const identity of receipt.sourceIdentities){
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 rootAfter=await lstat(receipt.canonicalLocation,{bigint:true});
522
- if(rootAfter.isSymbolicLink()||!rootAfter.isDirectory()
523
- ||!identityMatches(rootAfter,receipt.rootIdentity)||!sameIdentity(rootBefore,rootAfter)){
524
- fail('SDK browser runtime root changed while its receipt was authenticated.');
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 loadSdkBrowserRuntimeRelease({browserRuntimeRoot=defaultRoot,signal}={}){
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
- if(!receipt||!issuedReceipts.has(receipt)){
543
- fail('SDK browser runtime receipt was not issued by this SDK process.');
544
- }
545
- const {canonical}=await verifiedRoot(browserRuntimeRoot);
546
- if(receipt.canonicalLocation!==canonical){
547
- fail('SDK browser runtime receipt belongs to a different location.');
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 assertReceiptState(receipt,signal);
550
- return receipt;
115
+ await visit(root);
116
+ return files.sort(compareText);
551
117
  }
552
118
 
553
- export async function readVerifiedSdkBrowserRuntimeFile(receipt,{
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 normalized=safeInventoryPath(relativePath);
563
- const key=value=>process.platform==='win32'?collisionKey(value):value;
564
- const file=receipt.files.find(candidate=>key(candidate.path)===key(normalized));
565
- if(!file)fail(`Path is not in the verified SDK browser runtime inventory: ${normalized}.`);
566
- const identity=receipt.identities.find(candidate=>key(candidate.path)===key(file.path));
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 verifySdkBrowserRuntime({
132
+ export async function loadSdkBrowserRuntimeRelease({
599
133
  browserRuntimeRoot=defaultRoot,
600
- signal,
601
- onEvent
134
+ signal
602
135
  }={}){
603
- throwIfAborted(signal);
604
- const {canonical,identity:rootIdentity}=await verifiedRoot(browserRuntimeRoot);
605
- const {release,manifestPath,manifestSha256,manifestIdentity}=await readRelease(canonical,signal);
606
- if(manifestSha256!==SDK_BROWSER_RUNTIME_MANIFEST_SHA256){
607
- fail('SDK browser runtime manifest bytes do not match the trusted SDK release.');
608
- }
609
- await emit(onEvent,{
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
  }