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.
Files changed (153) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  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 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  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 +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  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 +27 -67
  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 +109 -1558
  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 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  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 -719
  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 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  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,45 +1,13 @@
1
- import {createHash,randomUUID} from 'node:crypto';
2
- import {constants as FS_CONSTANTS} from 'node:fs';
3
- import {
4
- lstat,
5
- mkdir,
6
- open,
7
- readdir,
8
- realpath,
9
- rename,
10
- rm
11
- } from 'node:fs/promises';
1
+ import {randomUUID} from 'node:crypto';
2
+ import {copyFile,lstat,mkdir,readdir,realpath,rename,rm} from 'node:fs/promises';
12
3
  import path from 'node:path';
13
- import {
14
- authenticateRuntimeReceipt,
15
- getSdkRoot,
16
- readVerifiedRuntimeFile
17
- } from './runtime.mjs';
18
- import {
19
- authenticateSdkBrowserRuntimeReceipt,
20
- getSdkBrowserRuntimeRoot,
21
- readVerifiedSdkBrowserRuntimeFile
22
- } from './sdk-browser-runtime.mjs';
4
+ import {getSdkRoot} from './runtime.mjs';
5
+ import {getSdkBrowserRuntimeRoot} from './sdk-browser-runtime.mjs';
23
6
 
24
- const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
25
- const CREATE_NEW_NO_FOLLOW=FS_CONSTANTS.O_CREAT|FS_CONSTANTS.O_EXCL
26
- |FS_CONSTANTS.O_WRONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
27
- const MAX_VERIFIED_WORKSPACE_RUNTIME_FILE_BYTES=64*1024*1024;
28
- const MAX_WORKSPACE_RUNTIME_RECEIPT_BYTES=4*1024*1024;
29
- const STAGING_PREFIX='.arcane-runtime-stage-';
30
- const BACKUP_PREFIX='.arcane-runtime-backup-';
31
- const MATERIALIZATION_RECEIPT_DIRECTORY='.arcane';
32
- const MATERIALIZATION_RECEIPT_NAME='installed-sdk-runtime.json';
33
- const RECEIPT_STAGING_PREFIX='.installed-sdk-runtime-stage-';
34
- const RECEIPT_BACKUP_PREFIX='.installed-sdk-runtime-backup-';
35
- const MATERIALIZATION_RECEIPT_KIND='arcane-installed-sdk-runtime-projection';
36
- const MATERIALIZATION_RECEIPT_PATH=
37
- `${MATERIALIZATION_RECEIPT_DIRECTORY}/${MATERIALIZATION_RECEIPT_NAME}`;
38
- const SHA256_PATTERN=/^[a-f0-9]{64}$/u;
39
- const UUID_PATTERN=/^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u;
40
- const issuedReceipts=new WeakSet();
7
+ const STAGING_PREFIX='.arcane-runtime-content-staging-';
8
+ const BACKUP_PREFIX='.arcane-runtime-content-backup-';
41
9
 
42
- function fail(message,code='ARCANE_WORKSPACE_RUNTIME_INTEGRITY_FAILED'){
10
+ function fail(message,code='ARCANE_WORKSPACE_RUNTIME_INVALID'){
43
11
  const error=new Error(message);
44
12
  error.code=code;
45
13
  throw error;
@@ -53,7 +21,7 @@ function throwIfAborted(signal){
53
21
  }
54
22
 
55
23
  async function emit(onEvent,event){
56
- if(typeof onEvent==='function')await onEvent(Object.freeze(event));
24
+ if(typeof onEvent==='function')await onEvent(event);
57
25
  }
58
26
 
59
27
  function compareText(left,right){
@@ -62,1562 +30,145 @@ function compareText(left,right){
62
30
  return a<b?-1:a>b?1:0;
63
31
  }
64
32
 
65
- function isRecord(value){
66
- return value!==null&&typeof value==='object'&&!Array.isArray(value);
67
- }
68
-
69
- function exactKeys(value,keys){
70
- return isRecord(value)
71
- &&Object.keys(value).sort(compareText).join('\0')===[...keys].sort(compareText).join('\0');
72
- }
73
-
74
- function canonicalJson(value){
75
- return `${JSON.stringify(value,null,2)}\n`;
76
- }
77
-
78
- function deepFreeze(value){
79
- if(!value||typeof value!=='object'||Object.isFrozen(value))return value;
80
- for(const child of Object.values(value))deepFreeze(child);
81
- return Object.freeze(value);
82
- }
83
-
84
- function inventoryContentSha256(files){
85
- return createHash('sha256').update(JSON.stringify(files.map(file=>({
86
- path:file.path,
87
- bytes:file.bytes,
88
- sha256:file.sha256
89
- })))).digest('hex');
90
- }
91
-
92
- function safeRelativePath(value){
93
- if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
94
- ||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
95
- ||value==='.'||value.startsWith('../')||value.includes('/../')){
96
- fail(`Workspace runtime contains an unsafe path: ${String(value)}.`);
97
- }
98
- return value;
99
- }
100
-
101
- function portableKey(value){
102
- const normalized=value.normalize('NFC');
103
- return process.platform==='win32'?normalized.toLowerCase():normalized;
104
- }
105
-
106
- function resolveContained(root,relativePath){
107
- const safe=safeRelativePath(relativePath);
108
- const absolute=path.resolve(root,...safe.split('/'));
109
- const relative=path.relative(root,absolute);
110
- if(relative.startsWith('..')||path.isAbsolute(relative)){
111
- fail(`Workspace runtime path escapes its root: ${safe}.`);
112
- }
113
- return absolute;
114
- }
115
-
116
- function projectSourcePath(sourcePath){
117
- const safe=safeRelativePath(sourcePath);
118
- if(safe.startsWith('arcane/'))return safe.slice('arcane/'.length);
119
- if(safe.startsWith('strong-type/')){
120
- return `dependencies/strong-type/${safe.slice('strong-type/'.length)}`;
121
- }
122
- fail(`SDK runtime path cannot be projected into a workspace: ${safe}.`);
123
- }
124
-
125
- function projectRuntimeFiles(runtimeReceipt,sdkBrowserRuntimeReceipt){
126
- if(!runtimeReceipt||!Array.isArray(runtimeReceipt.files)){
127
- fail('An authenticated SDK runtime receipt is required for workspace projection.');
128
- }
129
- if(!sdkBrowserRuntimeReceipt||!Array.isArray(sdkBrowserRuntimeReceipt.files)){
130
- fail('An authenticated SDK browser runtime receipt is required for workspace projection.');
131
- }
132
- const seen=new Map();
133
- const files=[];
134
- const add=(sourceFile,projectedPath,authority)=>{
135
- const key=portableKey(projectedPath);
136
- const collision=seen.get(key);
137
- if(collision){
138
- fail(
139
- `SDK runtime paths collide in the workspace projection: ${collision} and ${projectedPath}.`
140
- );
141
- }
142
- seen.set(key,projectedPath);
143
- files.push({
144
- path:projectedPath,
145
- sourcePath:sourceFile.path,
146
- authority,
147
- bytes:sourceFile.bytes,
148
- sha256:sourceFile.sha256
149
- });
150
- };
151
- for(const sourceFile of runtimeReceipt.files){
152
- add(sourceFile,projectSourcePath(sourceFile.path),'arcane-runtime');
153
- }
154
- for(const sourceFile of sdkBrowserRuntimeReceipt.files){
155
- add(sourceFile,`sdk/${safeRelativePath(sourceFile.path)}`,'sdk-browser-runtime');
156
- }
157
- files.sort((left,right)=>compareText(left.path,right.path));
158
- return files;
159
- }
160
-
161
- function projectedDirectories(files){
162
- const directories=new Set();
163
- for(const file of files){
164
- const parts=file.path.split('/');
165
- parts.pop();
166
- let current='';
167
- for(const part of parts){
168
- current=current?`${current}/${part}`:part;
169
- directories.add(current);
170
- }
171
- }
172
- return [...directories].sort(compareText);
173
- }
174
-
175
- function sameIdentity(before,after){
176
- return before.dev===after.dev&&before.ino===after.ino&&before.size===after.size
177
- &&before.mtimeNs===after.mtimeNs&&before.ctimeNs===after.ctimeNs
178
- &&before.nlink===after.nlink;
179
- }
180
-
181
- function fileIdentity(info){
182
- return Object.freeze({
183
- device:String(info.dev),
184
- inode:String(info.ino),
185
- bytes:Number(info.size),
186
- modifiedNanoseconds:String(info.mtimeNs),
187
- changedNanoseconds:String(info.ctimeNs),
188
- links:String(info.nlink)
189
- });
190
- }
191
-
192
- function identityMatches(info,identity){
193
- return String(info.dev)===identity.device
194
- &&String(info.ino)===identity.inode
195
- &&Number(info.size)===identity.bytes
196
- &&String(info.mtimeNs)===identity.modifiedNanoseconds
197
- &&String(info.ctimeNs)===identity.changedNanoseconds
198
- &&String(info.nlink)===identity.links;
199
- }
200
-
201
- function locationIdentityMatches(info,identity){
202
- return String(info.dev)===identity.device&&String(info.ino)===identity.inode;
203
- }
204
-
205
- function locationIdentity(identity){
206
- return Object.freeze({device:identity.device,inode:identity.inode});
207
- }
208
-
209
- function assertPersistentIdentity(value,label,{locationOnly=false}={}){
210
- const keys=locationOnly
211
- ?['device','inode']
212
- :['device','inode','bytes','modifiedNanoseconds','changedNanoseconds','links'];
213
- if(!exactKeys(value,keys)
214
- ||!keys.filter(key=>key!=='bytes')
215
- .every(key=>typeof value[key]==='string'&&/^[0-9]+$/u.test(value[key]))
216
- ||(!locationOnly&&(!Number.isSafeInteger(value.bytes)||value.bytes<0))){
217
- fail(`Installed SDK runtime receipt ${label} is invalid.`,
218
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
219
- }
220
- }
221
-
222
- function assertPersistentSourceReceipt(value,label,{browser=false}={}){
223
- const keys=[
224
- 'canonicalLocation','rootIdentity','manifestPath','manifestSha256',
225
- 'manifestIdentity','sdkVersion','contentSha256','fileCount','totalBytes'
226
- ];
227
- if(browser)keys.push('builder');
228
- if(!exactKeys(value,keys)
229
- ||typeof value.canonicalLocation!=='string'||!path.isAbsolute(value.canonicalLocation)
230
- ||typeof value.manifestPath!=='string'||!path.isAbsolute(value.manifestPath)
231
- ||!SHA256_PATTERN.test(value.manifestSha256)
232
- ||typeof value.sdkVersion!=='string'||!value.sdkVersion
233
- ||!SHA256_PATTERN.test(value.contentSha256)
234
- ||!Number.isSafeInteger(value.fileCount)||value.fileCount<1
235
- ||!Number.isSafeInteger(value.totalBytes)||value.totalBytes<1
236
- ||(browser&&(typeof value.builder!=='string'||!value.builder))){
237
- fail(`Installed SDK runtime receipt ${label} is invalid.`,
238
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
239
- }
240
- assertPersistentIdentity(value.rootIdentity,`${label} root identity`);
241
- assertPersistentIdentity(value.manifestIdentity,`${label} manifest identity`);
242
- }
243
-
244
- function validatePersistentMaterializationReceipt(document){
245
- if(!exactKeys(document,[
246
- 'schemaVersion','kind','generation','workspace','receiptPath',
247
- 'installedPackage','runtimeReceipt','sdkBrowserRuntimeReceipt','projection'
248
- ])||document.schemaVersion!==1||document.kind!==MATERIALIZATION_RECEIPT_KIND
249
- ||!UUID_PATTERN.test(document.generation)
250
- ||document.receiptPath!==MATERIALIZATION_RECEIPT_PATH){
251
- fail('Installed SDK runtime receipt envelope is invalid.',
252
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
253
- }
254
- if(!exactKeys(document.workspace,['canonicalLocation','identity'])
255
- ||typeof document.workspace.canonicalLocation!=='string'
256
- ||!path.isAbsolute(document.workspace.canonicalLocation)){
257
- fail('Installed SDK runtime receipt workspace is invalid.',
258
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
259
- }
260
- assertPersistentIdentity(document.workspace.identity,'workspace identity',{locationOnly:true});
261
- const installed=document.installedPackage;
262
- if(!exactKeys(installed,[
263
- 'dependencyName','dependencyGroup','specifier','packageSource','canonicalLocation',
264
- 'rootIdentity','packageName','packageVersion'
265
- ])||![
266
- installed.dependencyName,installed.dependencyGroup,installed.specifier,
267
- installed.packageSource,installed.canonicalLocation,installed.packageName,
268
- installed.packageVersion
269
- ].every(value=>typeof value==='string'&&value)
270
- ||!path.isAbsolute(installed.canonicalLocation)){
271
- fail('Installed SDK runtime receipt package identity is invalid.',
272
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
273
- }
274
- safeRelativePath(installed.packageSource);
275
- assertPersistentIdentity(installed.rootIdentity,'installed package root identity');
276
- assertPersistentSourceReceipt(document.runtimeReceipt,'runtime source receipt');
277
- assertPersistentSourceReceipt(
278
- document.sdkBrowserRuntimeReceipt,
279
- 'browser runtime source receipt',
280
- {browser:true}
281
- );
282
- if(document.runtimeReceipt.sdkVersion!==installed.packageVersion
283
- ||document.sdkBrowserRuntimeReceipt.sdkVersion!==installed.packageVersion
284
- ||path.resolve(installed.canonicalLocation,'runtime')
285
- !==path.resolve(document.runtimeReceipt.canonicalLocation)
286
- ||path.resolve(installed.canonicalLocation,'browser-runtime')
287
- !==path.resolve(document.sdkBrowserRuntimeReceipt.canonicalLocation)){
288
- fail('Installed SDK runtime receipt source binding is inconsistent.',
289
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
290
- }
291
- const projection=document.projection;
292
- if(!exactKeys(projection,[
293
- 'relativePath','fileCount','totalBytes','contentSha256','files'
294
- ])||projection.relativePath!=='arcane'||!Array.isArray(projection.files)
295
- ||!Number.isSafeInteger(projection.fileCount)||projection.fileCount<1
296
- ||!Number.isSafeInteger(projection.totalBytes)||projection.totalBytes<1
297
- ||!SHA256_PATTERN.test(projection.contentSha256)
298
- ||projection.fileCount!==projection.files.length){
299
- fail('Installed SDK runtime receipt projection is invalid.',
300
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
301
- }
302
- const seen=new Set();
303
- let totalBytes=0;
304
- let previous='';
305
- for(const [index,file] of projection.files.entries()){
306
- if(!exactKeys(file,['path','sourcePath','authority','bytes','sha256'])
307
- ||!['arcane-runtime','sdk-browser-runtime'].includes(file.authority)
308
- ||!Number.isSafeInteger(file.bytes)||file.bytes<0
309
- ||!SHA256_PATTERN.test(file.sha256)){
310
- fail(`Installed SDK runtime receipt projection file ${String(index)} is invalid.`,
311
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
312
- }
313
- const projectedPath=safeRelativePath(file.path);
314
- safeRelativePath(file.sourcePath);
315
- const key=portableKey(projectedPath);
316
- if(seen.has(key)||(index>0&&compareText(previous,projectedPath)>=0)){
317
- fail('Installed SDK runtime receipt projection inventory is not unique and sorted.',
318
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
319
- }
320
- seen.add(key);
321
- previous=projectedPath;
322
- totalBytes+=file.bytes;
323
- if(!Number.isSafeInteger(totalBytes)){
324
- fail('Installed SDK runtime receipt projection byte total is invalid.',
325
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
326
- }
327
- }
328
- if(totalBytes!==projection.totalBytes
329
- ||inventoryContentSha256(projection.files)!==projection.contentSha256){
330
- fail('Installed SDK runtime receipt projection inventory digest is invalid.',
331
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
332
- }
333
- return document;
334
- }
335
-
336
- function persistentSourceReceipt(receipt,{browser=false}={}){
337
- const result={
338
- canonicalLocation:receipt.canonicalLocation,
339
- rootIdentity:receipt.rootIdentity,
340
- manifestPath:receipt.manifestPath,
341
- manifestSha256:receipt.manifestSha256,
342
- manifestIdentity:receipt.manifestIdentity,
343
- sdkVersion:receipt.sdkVersion,
344
- contentSha256:receipt.contentSha256,
345
- fileCount:receipt.fileCount,
346
- totalBytes:receipt.totalBytes
347
- };
348
- if(browser)result.builder=receipt.builder;
349
- return result;
350
- }
351
-
352
- async function createPersistentMaterializationReceipt({
353
- generation,
354
- workspace,
355
- installedSdkAuthority,
356
- runtimeReceipt,
357
- sdkBrowserRuntimeReceipt,
358
- expectedFiles
359
- }){
360
- const {declaration,installation}=installedSdkAuthority??{};
361
- if(!declaration||!installation){
362
- fail('Installed SDK authority is required for a persistent workspace projection receipt.');
363
- }
364
- const packageInfo=await lstat(installation.canonicalPackageRoot,{bigint:true});
365
- if(packageInfo.isSymbolicLink()||!packageInfo.isDirectory()){
366
- fail('Installed SDK package root must remain a real directory.');
367
- }
368
- const canonicalPackageRoot=await realpath(installation.canonicalPackageRoot);
369
- if(canonicalPackageRoot!==installation.canonicalPackageRoot){
370
- fail('Installed SDK package root changed while materialization was active.');
371
- }
372
- if(runtimeReceipt.sdkVersion!==installation.packageVersion
373
- ||sdkBrowserRuntimeReceipt.sdkVersion!==installation.packageVersion){
374
- fail('Installed SDK package and source receipt versions do not match.');
375
- }
376
- const files=expectedFiles.map(file=>({
377
- path:file.path,
378
- sourcePath:file.sourcePath,
379
- authority:file.authority,
380
- bytes:file.bytes,
381
- sha256:file.sha256
382
- }));
383
- const document={
384
- schemaVersion:1,
385
- kind:MATERIALIZATION_RECEIPT_KIND,
386
- generation,
387
- workspace:{
388
- canonicalLocation:workspace.canonicalRoot,
389
- identity:locationIdentity(workspace.identity)
390
- },
391
- receiptPath:MATERIALIZATION_RECEIPT_PATH,
392
- installedPackage:{
393
- dependencyName:declaration.dependencyName,
394
- dependencyGroup:declaration.dependencyGroup,
395
- specifier:declaration.specifier,
396
- packageSource:declaration.packageSource,
397
- canonicalLocation:canonicalPackageRoot,
398
- rootIdentity:fileIdentity(packageInfo),
399
- packageName:installation.packageName,
400
- packageVersion:installation.packageVersion
401
- },
402
- runtimeReceipt:persistentSourceReceipt(runtimeReceipt),
403
- sdkBrowserRuntimeReceipt:persistentSourceReceipt(
404
- sdkBrowserRuntimeReceipt,
405
- {browser:true}
406
- ),
407
- projection:{
408
- relativePath:'arcane',
409
- fileCount:files.length,
410
- totalBytes:files.reduce((total,file)=>total+file.bytes,0),
411
- contentSha256:inventoryContentSha256(files),
412
- files
413
- }
414
- };
415
- validatePersistentMaterializationReceipt(document);
416
- return deepFreeze(document);
417
- }
418
-
419
- async function workspaceLocation(workspaceRoot){
420
- const requestedRoot=path.resolve(workspaceRoot);
33
+ async function realDirectory(location,label){
34
+ const requested=path.resolve(location);
421
35
  let info;
422
- try{info=await lstat(requestedRoot,{bigint:true});}
423
- catch(error){
424
- if(error?.code==='ENOENT')fail(`Workspace root does not exist: ${requestedRoot}.`);
425
- throw error;
426
- }
427
- if(info.isSymbolicLink()||!info.isDirectory()){
428
- fail('Workspace root must be a real directory.');
429
- }
430
- const canonicalRoot=await realpath(requestedRoot);
431
- const canonicalInfo=await lstat(canonicalRoot,{bigint:true});
432
- if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()
433
- ||!sameIdentity(info,canonicalInfo)){
434
- fail('Workspace root changed while its location was being resolved.');
435
- }
436
- return {
437
- canonicalRoot,
438
- identity:fileIdentity(canonicalInfo)
439
- };
440
- }
441
-
442
- async function assertRealDirectoryLocation(directory,identity,label){
443
- let before;
444
- try{before=await lstat(directory,{bigint:true});}
36
+ try{info=await lstat(requested);}
445
37
  catch(error){
446
- if(error?.code==='ENOENT')fail(`${label} is missing.`);
38
+ if(error?.code==='ENOENT')fail(`${label} does not exist: ${requested}.`);
447
39
  throw error;
448
40
  }
449
- if(before.isSymbolicLink()||!before.isDirectory()
450
- ||!locationIdentityMatches(before,identity)){
451
- fail(`${label} changed while workspace runtime materialization was active.`);
452
- }
453
- const canonical=await realpath(directory);
454
- if(canonical!==directory){
455
- fail(`${label} became a symbolic link or junction.`);
456
- }
457
- const after=await lstat(directory,{bigint:true});
458
- if(after.isSymbolicLink()||!after.isDirectory()
459
- ||!locationIdentityMatches(after,identity)
460
- ||!locationIdentityMatches(before,fileIdentity(after))){
461
- fail(`${label} changed while its location was being authenticated.`);
41
+ if(info.isSymbolicLink()||!info.isDirectory())fail(`${label} must be a real directory.`);
42
+ const canonical=await realpath(requested);
43
+ const canonicalInfo=await lstat(canonical);
44
+ if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()){
45
+ fail(`${label} must be a real directory.`);
462
46
  }
47
+ return canonical;
463
48
  }
464
49
 
465
- async function inspectTree(root,{signal}={}){
50
+ async function copyCompleteEntry(source,destination,label,signal){
466
51
  throwIfAborted(signal);
467
- const rootInfo=await lstat(root,{bigint:true});
468
- if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()){
469
- fail('Workspace arcane runtime root must be a real directory.');
470
- }
471
- const files=[];
472
- const directories=[];
473
- async function visit(directory,relativeRoot=''){
52
+ const info=await lstat(source);
53
+ if(info.isSymbolicLink())fail(`${label} must not contain a symbolic link or junction.`);
54
+ if(info.isFile()){
55
+ await copyFile(source,destination);
56
+ return;
57
+ }
58
+ if(!info.isDirectory())fail(`${label} contains a non-file entry.`);
59
+ await mkdir(destination,{recursive:true});
60
+ const entries=await readdir(source,{withFileTypes:true});
61
+ entries.sort((left,right)=>compareText(left.name,right.name));
62
+ for(const entry of entries){
474
63
  throwIfAborted(signal);
475
- const entries=await readdir(directory,{withFileTypes:true});
476
- entries.sort((left,right)=>compareText(left.name,right.name));
477
- for(const entry of entries){
478
- throwIfAborted(signal);
479
- const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
480
- safeRelativePath(relative);
481
- const absolute=path.join(directory,entry.name);
482
- const details=await lstat(absolute,{bigint:true});
483
- if(details.isSymbolicLink()){
484
- fail(`Workspace arcane runtime contains a symbolic link or junction: ${relative}.`);
485
- }
486
- if(details.isDirectory()){
487
- directories.push({path:relative,identity:fileIdentity(details)});
488
- await visit(absolute,relative);
489
- }else if(details.isFile()){
490
- files.push(relative);
491
- }else{
492
- fail(`Workspace arcane runtime contains a non-file entry: ${relative}.`);
493
- }
494
- }
495
- }
496
- await visit(root);
497
- files.sort(compareText);
498
- directories.sort((left,right)=>compareText(left.path,right.path));
499
- return {rootIdentity:fileIdentity(rootInfo),files,directories};
500
- }
501
-
502
- async function hashExactFile(filePath,expectedBytes,signal){
503
- throwIfAborted(signal);
504
- let handle;
505
- try{
506
- handle=await open(filePath,READ_ONLY_NO_FOLLOW);
507
- }catch(error){
508
- if(error?.code==='ELOOP')fail(`Workspace runtime file became a symbolic link: ${filePath}.`);
509
- throw error;
510
- }
511
- const hash=createHash('sha256');
512
- const buffer=Buffer.allocUnsafe(1024*1024);
513
- try{
514
- const before=await handle.stat({bigint:true});
515
- if(!before.isFile()||before.size!==BigInt(expectedBytes)){
516
- fail(`Workspace runtime file size is invalid: ${filePath}.`);
517
- }
518
- while(true){
519
- throwIfAborted(signal);
520
- const {bytesRead}=await handle.read(buffer,0,buffer.length,null);
521
- if(bytesRead===0)break;
522
- hash.update(buffer.subarray(0,bytesRead));
523
- }
524
- const after=await handle.stat({bigint:true});
525
- if(!sameIdentity(before,after)){
526
- fail(`Workspace runtime file changed while it was being verified: ${filePath}.`);
527
- }
528
- return {sha256:hash.digest('hex'),identity:fileIdentity(after)};
529
- }finally{
530
- await handle.close();
531
- }
532
- }
533
-
534
- async function assertIdentityAt(root,entry,{directory}){
535
- const absolute=resolveContained(root,entry.path);
536
- const info=await lstat(absolute,{bigint:true});
537
- if(info.isSymbolicLink()
538
- ||(directory?!info.isDirectory():!info.isFile())
539
- ||!identityMatches(info,entry)){
540
- fail(`Workspace runtime ${directory?'directory':'file'} changed after verification: ${entry.path}.`);
541
- }
542
- }
543
-
544
- async function verifyProjectedTree(root,expectedFiles,{signal,onProgress}={}){
545
- const expectedPaths=expectedFiles.map(file=>file.path);
546
- const expectedDirectoryPaths=projectedDirectories(expectedFiles);
547
- const before=await inspectTree(root,{signal});
548
- if(JSON.stringify(before.files)!==JSON.stringify(expectedPaths)){
549
- fail('Workspace arcane runtime file inventory does not match the authenticated SDK runtime.');
550
- }
551
- if(JSON.stringify(before.directories.map(entry=>entry.path))
552
- !==JSON.stringify(expectedDirectoryPaths)){
553
- fail('Workspace arcane runtime directory inventory does not match the authenticated SDK runtime.');
554
- }
555
-
556
- const identities=[];
557
- let verifiedBytes=0;
558
- for(const [index,file] of expectedFiles.entries()){
559
- throwIfAborted(signal);
560
- const absolute=resolveContained(root,file.path);
561
- const result=await hashExactFile(absolute,file.bytes,signal);
562
- if(result.sha256!==file.sha256){
563
- fail(`Workspace runtime integrity check failed for ${file.path}.`);
564
- }
565
- verifiedBytes+=file.bytes;
566
- identities.push({path:file.path,...result.identity});
567
- if(onProgress){
568
- await onProgress({
569
- current:index+1,
570
- total:expectedFiles.length,
571
- verifiedBytes,
572
- totalBytes:expectedFiles.reduce((total,entry)=>total+entry.bytes,0),
573
- path:file.path
574
- });
575
- }
576
- }
577
-
578
- const rootAfter=await lstat(root,{bigint:true});
579
- if(rootAfter.isSymbolicLink()||!rootAfter.isDirectory()
580
- ||!identityMatches(rootAfter,before.rootIdentity)){
581
- fail('Workspace arcane runtime root changed while it was being verified.');
582
- }
583
- for(const directory of before.directories){
584
- throwIfAborted(signal);
585
- await assertIdentityAt(root,{path:directory.path,...directory.identity},{directory:true});
586
- }
587
- for(const identity of identities){
588
- throwIfAborted(signal);
589
- await assertIdentityAt(root,identity,{directory:false});
590
- }
591
- return {
592
- rootIdentity:before.rootIdentity,
593
- directoryIdentities:before.directories.map(entry=>Object.freeze({
594
- path:entry.path,
595
- ...entry.identity
596
- })),
597
- identities:identities.map(Object.freeze)
598
- };
599
- }
600
-
601
- async function assertRequestedWorkspace(receipt,workspaceRoot){
602
- const requested=await workspaceLocation(workspaceRoot);
603
- if(requested.canonicalRoot!==receipt.canonicalWorkspaceLocation
604
- ||!locationIdentityMatches(
605
- {
606
- dev:BigInt(requested.identity.device),
607
- ino:BigInt(requested.identity.inode)
608
- },
609
- receipt.workspaceIdentity
610
- )){
611
- fail('Workspace runtime receipt belongs to a different workspace location.');
612
- }
613
- const expectedRoot=path.join(requested.canonicalRoot,'arcane');
614
- const canonicalRoot=await realpath(expectedRoot);
615
- if(canonicalRoot!==receipt.canonicalLocation||canonicalRoot!==expectedRoot){
616
- fail('Workspace runtime receipt belongs to a different arcane runtime location.');
617
- }
618
- return {workspace:requested,root:canonicalRoot};
619
- }
620
-
621
- async function assertWorkspaceRuntimeState(receipt,{workspaceRoot,signal}){
622
- throwIfAborted(signal);
623
- const {root}=await assertRequestedWorkspace(receipt,workspaceRoot);
624
- const actual=await inspectTree(root,{signal});
625
- if(!identityMatches(
626
- {
627
- dev:BigInt(actual.rootIdentity.device),
628
- ino:BigInt(actual.rootIdentity.inode),
629
- size:BigInt(actual.rootIdentity.bytes),
630
- mtimeNs:BigInt(actual.rootIdentity.modifiedNanoseconds),
631
- ctimeNs:BigInt(actual.rootIdentity.changedNanoseconds),
632
- nlink:BigInt(actual.rootIdentity.links)
633
- },
634
- receipt.rootIdentity
635
- )){
636
- fail('Workspace arcane runtime root changed after verification.');
637
- }
638
- const actualPaths=actual.files;
639
- const expectedPaths=receipt.files.map(file=>file.path);
640
- if(JSON.stringify(actualPaths)!==JSON.stringify(expectedPaths)){
641
- fail('Workspace arcane runtime file inventory changed after verification.');
642
- }
643
- const actualDirectories=actual.directories.map(entry=>entry.path);
644
- const expectedDirectories=receipt.directoryIdentities.map(entry=>entry.path);
645
- if(JSON.stringify(actualDirectories)!==JSON.stringify(expectedDirectories)){
646
- fail('Workspace arcane runtime directory inventory changed after verification.');
647
- }
648
- for(const directory of receipt.directoryIdentities){
649
- throwIfAborted(signal);
650
- await assertIdentityAt(root,directory,{directory:true});
651
- }
652
- for(const identity of receipt.identities){
653
- throwIfAborted(signal);
654
- await assertIdentityAt(root,identity,{directory:false});
655
- }
656
- return receipt;
657
- }
658
-
659
- async function writeNewFile(filePath,bytes){
660
- let handle;
661
- try{
662
- handle=await open(filePath,CREATE_NEW_NO_FOLLOW,0o644);
663
- }catch(error){
664
- if(error?.code==='ELOOP')fail(`Workspace runtime staging path became a symbolic link: ${filePath}.`);
665
- throw error;
666
- }
667
- try{
668
- await handle.writeFile(bytes);
669
- await handle.sync();
670
- }finally{
671
- await handle.close();
672
- }
673
- }
674
-
675
- async function readPersistentMaterializationReceipt(receiptPath,{signal}={}){
676
- throwIfAborted(signal);
677
- let handle;
678
- try{
679
- handle=await open(receiptPath,READ_ONLY_NO_FOLLOW);
680
- }catch(error){
681
- if(error?.code==='ELOOP'){
682
- fail('Installed SDK runtime receipt must not be a symbolic link.',
683
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
684
- }
685
- throw error;
686
- }
687
- try{
688
- const before=await handle.stat({bigint:true});
689
- if(!before.isFile()||before.size>BigInt(MAX_WORKSPACE_RUNTIME_RECEIPT_BYTES)){
690
- fail('Installed SDK runtime receipt is not a bounded regular file.',
691
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
692
- }
693
- const bytes=await handle.readFile();
694
- throwIfAborted(signal);
695
- const after=await handle.stat({bigint:true});
696
- if(!sameIdentity(before,after)){
697
- fail('Installed SDK runtime receipt changed while it was being read.',
698
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
699
- }
700
- let document;
701
- try{document=JSON.parse(bytes.toString('utf8'));}
702
- catch(error){
703
- fail(`Installed SDK runtime receipt is not valid JSON: ${error.message}`,
704
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
705
- }
706
- validatePersistentMaterializationReceipt(document);
707
- if(!bytes.equals(Buffer.from(canonicalJson(document)))){
708
- fail('Installed SDK runtime receipt bytes are not canonical.',
709
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
710
- }
711
- const named=await lstat(receiptPath,{bigint:true});
712
- if(named.isSymbolicLink()||!named.isFile()||!sameIdentity(named,after)){
713
- fail('Installed SDK runtime receipt path changed while it was being read.',
714
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
715
- }
716
- return Object.freeze({
717
- document:deepFreeze(document),
718
- identity:fileIdentity(after)
719
- });
720
- }finally{
721
- await handle.close();
722
- }
723
- }
724
-
725
- async function assertOwnedFileLocation(filePath,identity,label){
726
- const info=await lstat(filePath,{bigint:true});
727
- if(info.isSymbolicLink()||!info.isFile()||!locationIdentityMatches(info,identity)){
728
- fail(`${label} changed while workspace runtime materialization was active.`);
729
- }
730
- }
731
-
732
- async function removeOwnedFile(filePath,identity,label){
733
- await assertOwnedFileLocation(filePath,identity,label);
734
- await rm(filePath,{force:true});
735
- try{
736
- await lstat(filePath,{bigint:true});
737
- fail(`${label} remained after cleanup.`);
738
- }catch(error){
739
- if(error?.code!=='ENOENT')throw error;
740
- }
741
- }
742
-
743
- async function materializationMetadataLocation(workspace){
744
- const metadataRoot=path.join(workspace.canonicalRoot,MATERIALIZATION_RECEIPT_DIRECTORY);
745
- let info;
746
- try{info=await lstat(metadataRoot,{bigint:true});}
747
- catch(error){
748
- if(error?.code!=='ENOENT')throw error;
749
- await mkdir(metadataRoot,{mode:0o700});
750
- info=await lstat(metadataRoot,{bigint:true});
751
- }
752
- if(info.isSymbolicLink()||!info.isDirectory()){
753
- fail('Workspace materialization metadata root must be a real directory.');
754
- }
755
- const canonical=await realpath(metadataRoot);
756
- if(canonical!==metadataRoot){
757
- fail('Workspace materialization metadata root must not resolve through another path.');
758
- }
759
- return Object.freeze({root:metadataRoot,identity:fileIdentity(info)});
760
- }
761
-
762
- async function optionalPathIdentity(target){
763
- try{return fileIdentity(await lstat(target,{bigint:true}));}
764
- catch(error){
765
- if(error?.code==='ENOENT')return null;
766
- throw error;
767
- }
768
- }
769
-
770
- async function cleanupOwnedTree(ownedRoot,workspace,ownedIdentity,{prefix,label}){
771
- const ownedParent=path.dirname(ownedRoot);
772
- const ownedName=path.basename(ownedRoot);
773
- if(ownedParent!==workspace.canonicalRoot||!ownedName.startsWith(prefix)){
774
- fail(
775
- `Refusing to clean an unowned workspace runtime ${label} path: ${ownedRoot} `
776
- +`(parent ${ownedParent}; expected ${workspace.canonicalRoot}).`
64
+ await copyCompleteEntry(
65
+ path.join(source,entry.name),
66
+ path.join(destination,entry.name),
67
+ `${label}/${entry.name}`,
68
+ signal
777
69
  );
778
70
  }
779
- await assertRealDirectoryLocation(
780
- workspace.canonicalRoot,
781
- workspace.identity,
782
- 'Workspace root'
783
- );
784
- let ownedInfo;
785
- try{ownedInfo=await lstat(ownedRoot,{bigint:true});}
786
- catch(error){
787
- if(error?.code==='ENOENT')return;
788
- throw error;
789
- }
790
- if(ownedInfo.isSymbolicLink()||!ownedInfo.isDirectory()
791
- ||!locationIdentityMatches(ownedInfo,ownedIdentity)){
792
- fail(`Refusing to clean a workspace runtime ${label} path whose identity changed.`);
793
- }
794
- await assertRealDirectoryLocation(
795
- ownedRoot,
796
- ownedIdentity,
797
- `Workspace runtime ${label} directory`
798
- );
799
- try{await inspectTree(ownedRoot);}
800
- catch(error){
801
- fail(`Refusing to clean an unauthenticated workspace runtime ${label} tree: ${error.message}`);
802
- }
803
- await assertRealDirectoryLocation(
804
- workspace.canonicalRoot,
805
- workspace.identity,
806
- 'Workspace root'
807
- );
808
- await assertRealDirectoryLocation(
809
- ownedRoot,
810
- ownedIdentity,
811
- `Workspace runtime ${label} directory`
812
- );
813
- await rm(ownedRoot,{recursive:true,force:true});
814
- try{
815
- await lstat(ownedRoot,{bigint:true});
816
- fail(`Workspace runtime ${label} path remained after cleanup.`);
817
- }catch(error){
818
- if(error?.code!=='ENOENT')throw error;
819
- }
820
- await assertRealDirectoryLocation(
821
- workspace.canonicalRoot,
822
- workspace.identity,
823
- 'Workspace root'
824
- );
825
71
  }
826
72
 
827
- async function cleanupStaging(stagingRoot,workspace,stagingIdentity){
828
- return cleanupOwnedTree(stagingRoot,workspace,stagingIdentity,{
829
- prefix:STAGING_PREFIX,
830
- label:'staging'
831
- });
73
+ async function removeTemporaryTree(location){
74
+ await rm(location,{recursive:true,force:true}).catch(()=>{});
832
75
  }
833
76
 
834
- export async function verifyWorkspaceRuntime({
77
+ export async function materializeWorkspaceRuntimeContent({
835
78
  workspaceRoot,
836
79
  runtimeRoot=path.join(getSdkRoot(),'runtime'),
837
- runtimeReceipt,
838
80
  browserRuntimeRoot=getSdkBrowserRuntimeRoot(),
839
- sdkBrowserRuntimeReceipt,
840
81
  signal,
841
82
  onEvent
842
83
  }={}){
843
- if(!workspaceRoot)fail('workspaceRoot is required to verify a workspace runtime.');
844
- await authenticateRuntimeReceipt(runtimeReceipt,{runtimeRoot,signal});
845
- await authenticateSdkBrowserRuntimeReceipt(sdkBrowserRuntimeReceipt,{
846
- browserRuntimeRoot,
847
- signal
848
- });
849
- const expectedFiles=projectRuntimeFiles(runtimeReceipt,sdkBrowserRuntimeReceipt);
850
- const workspace=await workspaceLocation(workspaceRoot);
851
- const projectedRoot=path.join(workspace.canonicalRoot,'arcane');
852
- let rootInfo;
853
- try{rootInfo=await lstat(projectedRoot,{bigint:true});}
854
- catch(error){
855
- if(error?.code==='ENOENT'){
856
- fail(`Workspace arcane runtime is missing: ${projectedRoot}.`);
857
- }
858
- throw error;
859
- }
860
- if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()){
861
- fail('Workspace arcane runtime root must be a real directory.');
862
- }
863
- const canonicalRoot=await realpath(projectedRoot);
864
- if(canonicalRoot!==projectedRoot){
865
- fail('Workspace arcane runtime root must not resolve outside its direct workspace location.');
866
- }
867
-
868
- const totalBytes=expectedFiles.reduce((total,file)=>total+file.bytes,0);
869
- await emit(onEvent,{
870
- type:'workspace.runtime.verify.started',
871
- fileCount:expectedFiles.length,
872
- totalBytes
873
- });
874
- const verified=await verifyProjectedTree(canonicalRoot,expectedFiles,{
875
- signal,
876
- onProgress:event=>emit(onEvent,{type:'workspace.runtime.verify.progress',...event})
877
- });
878
- const inventory=expectedFiles.map(file=>Object.freeze({...file}));
879
- const contentInventory=expectedFiles.map(file=>({
880
- path:file.path,
881
- bytes:file.bytes,
882
- sha256:file.sha256
883
- }));
884
- const receipt=Object.freeze({
885
- schemaVersion:1,
886
- kind:'arcane-workspace-runtime-verification',
887
- canonicalWorkspaceLocation:workspace.canonicalRoot,
888
- workspaceIdentity:workspace.identity,
889
- canonicalLocation:canonicalRoot,
890
- rootIdentity:verified.rootIdentity,
891
- sourceRuntimeLocation:runtimeReceipt.canonicalLocation,
892
- sourceManifestSha256:runtimeReceipt.manifestSha256,
893
- sourceContentSha256:runtimeReceipt.contentSha256,
894
- sourceBrowserRuntimeLocation:sdkBrowserRuntimeReceipt.canonicalLocation,
895
- sourceBrowserManifestSha256:sdkBrowserRuntimeReceipt.manifestSha256,
896
- sourceBrowserContentSha256:sdkBrowserRuntimeReceipt.contentSha256,
897
- sdkVersion:runtimeReceipt.sdkVersion,
898
- sources:Object.freeze({
899
- arcane:Object.freeze({
900
- authority:'arcane-os-sdk',
901
- location:runtimeReceipt.canonicalLocation,
902
- manifestSha256:runtimeReceipt.manifestSha256,
903
- contentSha256:runtimeReceipt.contentSha256,
904
- source:runtimeReceipt.source
905
- }),
906
- sdkBrowser:Object.freeze({
907
- authority:'arcane-os-sdk',
908
- location:sdkBrowserRuntimeReceipt.canonicalLocation,
909
- manifestSha256:sdkBrowserRuntimeReceipt.manifestSha256,
910
- contentSha256:sdkBrowserRuntimeReceipt.contentSha256,
911
- source:sdkBrowserRuntimeReceipt.source
912
- })
913
- }),
914
- files:Object.freeze(inventory),
915
- fileCount:inventory.length,
916
- totalBytes,
917
- contentSha256:createHash('sha256')
918
- .update(JSON.stringify(contentInventory))
919
- .digest('hex'),
920
- directoryIdentities:Object.freeze(verified.directoryIdentities),
921
- identities:Object.freeze(verified.identities)
922
- });
923
- issuedReceipts.add(receipt);
924
- await emit(onEvent,{
925
- type:'workspace.runtime.verify.completed',
926
- sourceContentSha256:receipt.sourceContentSha256,
927
- sourceBrowserContentSha256:receipt.sourceBrowserContentSha256,
928
- contentSha256:receipt.contentSha256,
929
- fileCount:receipt.fileCount,
930
- totalBytes:receipt.totalBytes
931
- });
932
- return receipt;
933
- }
934
-
935
- function attachMaterialization(receipt,{
936
- status,
937
- persistentReceipt,
938
- receiptPath,
939
- cleanupWarnings=[]
940
- }){
941
- const result=Object.freeze({
942
- ...receipt,
943
- materialization:Object.freeze({
944
- status,
945
- generation:persistentReceipt.generation,
946
- receiptPath,
947
- persistentReceipt,
948
- cleanupWarnings:Object.freeze([...cleanupWarnings])
949
- })
950
- });
951
- issuedReceipts.add(result);
952
- return result;
953
- }
954
-
955
- async function inspectInstalledMaterialization({
956
- workspace,
957
- destinationRoot,
958
- receiptPath,
959
- desiredReceipt,
960
- signal
961
- }){
962
- const destinationIdentity=await optionalPathIdentity(destinationRoot);
963
- const receiptIdentity=await optionalPathIdentity(receiptPath);
964
- if(!destinationIdentity&&!receiptIdentity)return Object.freeze({kind:'absent'});
965
- if(!destinationIdentity){
966
- fail('Installed SDK runtime receipt exists without its workspace projection.',
967
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
968
- }
969
- const destinationInfo=await lstat(destinationRoot,{bigint:true});
970
- if(destinationInfo.isSymbolicLink()||!destinationInfo.isDirectory()){
971
- fail('Existing workspace arcane runtime path must be a real directory.');
972
- }
973
- const canonicalDestination=await realpath(destinationRoot);
974
- if(canonicalDestination!==destinationRoot){
975
- fail('Existing workspace arcane runtime path must not resolve through another path.');
976
- }
977
- if(!receiptIdentity){
978
- await inspectTree(destinationRoot,{signal});
979
- return Object.freeze({
980
- kind:'legacy',
981
- destinationIdentity:fileIdentity(destinationInfo)
982
- });
983
- }
984
- const recorded=await readPersistentMaterializationReceipt(receiptPath,{signal});
985
- if(recorded.document.workspace.canonicalLocation!==workspace.canonicalRoot
986
- ||recorded.document.workspace.identity.device!==workspace.identity.device
987
- ||recorded.document.workspace.identity.inode!==workspace.identity.inode){
988
- fail('Installed SDK runtime receipt belongs to a different workspace.',
989
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
990
- }
991
- if(canonicalJson(recorded.document)===canonicalJson(desiredReceipt)){
992
- return Object.freeze({
993
- kind:'current',
994
- document:recorded.document,
995
- destinationIdentity:fileIdentity(destinationInfo),
996
- receiptIdentity:recorded.identity
997
- });
998
- }
999
- await verifyProjectedTree(
1000
- destinationRoot,
1001
- recorded.document.projection.files,
1002
- {signal}
1003
- );
1004
- return Object.freeze({
1005
- kind:'stale',
1006
- document:recorded.document,
1007
- destinationIdentity:fileIdentity(destinationInfo),
1008
- receiptIdentity:recorded.identity
1009
- });
1010
- }
1011
-
1012
- async function rollbackInstalledReplacement(state){
1013
- const errors=[];
1014
- const attempt=async operation=>{
1015
- try{await operation();}
1016
- catch(error){errors.push(error);}
1017
- };
1018
- if(state.newReceiptMoved){
1019
- await attempt(()=>rename(state.receiptPath,state.receiptStagingPath));
1020
- }
1021
- if(state.newRootMoved){
1022
- await attempt(()=>rename(state.destinationRoot,state.stagingRoot));
1023
- }
1024
- if(state.oldReceiptMoved){
1025
- await attempt(()=>rename(state.receiptBackupPath,state.receiptPath));
1026
- }
1027
- if(state.oldRootMoved){
1028
- await attempt(()=>rename(state.backupRoot,state.destinationRoot));
1029
- }
1030
- if(errors.length){
1031
- throw new AggregateError(
1032
- errors,
1033
- 'Installed SDK runtime replacement failed and its prior projection could not be fully restored.'
1034
- );
1035
- }
1036
- state.newReceiptMoved=false;
1037
- state.newRootMoved=false;
1038
- state.oldReceiptMoved=false;
1039
- state.oldRootMoved=false;
1040
- }
1041
-
1042
- async function materializeInstalledWorkspaceRuntime({
1043
- workspaceRoot,
1044
- runtimeRoot,
1045
- runtimeReceipt,
1046
- browserRuntimeRoot,
1047
- sdkBrowserRuntimeReceipt,
1048
- installedSdkAuthority,
1049
- signal,
1050
- onEvent
1051
- }){
1052
- const expectedFiles=projectRuntimeFiles(runtimeReceipt,sdkBrowserRuntimeReceipt);
1053
- const workspace=await workspaceLocation(workspaceRoot);
1054
- const metadata=await materializationMetadataLocation(workspace);
1055
- const destinationRoot=path.join(workspace.canonicalRoot,'arcane');
1056
- const receiptPath=path.join(metadata.root,MATERIALIZATION_RECEIPT_NAME);
1057
- const generation=randomUUID();
1058
- let desiredReceipt=await createPersistentMaterializationReceipt({
1059
- generation,
1060
- workspace,
1061
- installedSdkAuthority,
1062
- runtimeReceipt,
1063
- sdkBrowserRuntimeReceipt,
1064
- expectedFiles
1065
- });
1066
- let existingReceipt=null;
1067
- try{
1068
- existingReceipt=(await readPersistentMaterializationReceipt(receiptPath,{signal})).document;
1069
- desiredReceipt=await createPersistentMaterializationReceipt({
1070
- generation:existingReceipt.generation,
1071
- workspace,
1072
- installedSdkAuthority,
1073
- runtimeReceipt,
1074
- sdkBrowserRuntimeReceipt,
1075
- expectedFiles
1076
- });
1077
- }catch(error){
1078
- if(error?.code!=='ENOENT')throw error;
1079
- }
1080
- const existing=await inspectInstalledMaterialization({
1081
- workspace,
1082
- destinationRoot,
1083
- receiptPath,
1084
- desiredReceipt,
1085
- signal
1086
- });
1087
- if(existing.kind==='current'){
1088
- const verified=await verifyWorkspaceRuntime({
1089
- workspaceRoot,
1090
- runtimeRoot,
1091
- runtimeReceipt,
1092
- browserRuntimeRoot,
1093
- sdkBrowserRuntimeReceipt,
1094
- signal,
1095
- onEvent
1096
- });
1097
- const result=attachMaterialization(verified,{
1098
- status:'reused',
1099
- persistentReceipt:existing.document,
1100
- receiptPath
1101
- });
1102
- await emit(onEvent,{
1103
- type:'workspace.runtime.materialize.reused',
1104
- status:'reused',
1105
- generation:existing.document.generation,
1106
- receiptPath
1107
- });
1108
- return result;
1109
- }
1110
-
1111
- desiredReceipt=await createPersistentMaterializationReceipt({
1112
- generation,
1113
- workspace,
1114
- installedSdkAuthority,
1115
- runtimeReceipt,
1116
- sdkBrowserRuntimeReceipt,
1117
- expectedFiles
1118
- });
1119
- const operationSuffix=`${String(process.pid)}-${generation}`;
1120
- const stagingRoot=path.join(workspace.canonicalRoot,`${STAGING_PREFIX}${operationSuffix}`);
1121
- const backupRoot=path.join(workspace.canonicalRoot,`${BACKUP_PREFIX}${operationSuffix}`);
1122
- const receiptStagingPath=path.join(
1123
- metadata.root,
1124
- `${RECEIPT_STAGING_PREFIX}${operationSuffix}.json`
1125
- );
1126
- const receiptBackupPath=path.join(
1127
- metadata.root,
1128
- `${RECEIPT_BACKUP_PREFIX}${operationSuffix}.json`
84
+ if(!workspaceRoot)fail('workspaceRoot is required to materialize a workspace runtime.');
85
+ throwIfAborted(signal);
86
+ const workspace=await realDirectory(workspaceRoot,'Workspace root');
87
+ const runtime=await realDirectory(runtimeRoot,'SDK runtime root');
88
+ const browserRuntime=await realDirectory(browserRuntimeRoot,'SDK browser runtime root');
89
+ const runtimeArcane=await realDirectory(path.join(runtime,'arcane'),'SDK Arcane runtime');
90
+ const runtimeStrongType=await realDirectory(
91
+ path.join(runtime,'strong-type'),
92
+ 'SDK strong-type runtime'
1129
93
  );
1130
- await mkdir(stagingRoot,{mode:0o700});
1131
- const stagingInfo=await lstat(stagingRoot,{bigint:true});
1132
- if(stagingInfo.isSymbolicLink()||!stagingInfo.isDirectory()){
1133
- fail('Workspace runtime staging path must be a real directory.');
1134
- }
1135
- const stagingIdentity=fileIdentity(stagingInfo);
1136
- const transaction={
1137
- destinationRoot,
1138
- stagingRoot,
1139
- backupRoot,
1140
- receiptPath,
1141
- receiptStagingPath,
1142
- receiptBackupPath,
1143
- newReceiptMoved:false,
1144
- newRootMoved:false,
1145
- oldReceiptMoved:false,
1146
- oldRootMoved:false
1147
- };
1148
- let stagingCleaned=false;
1149
- let receiptStagingIdentity;
1150
- let uncertainCommit=false;
94
+ const destinationRoot=path.join(workspace,'arcane');
95
+ const token=randomUUID();
96
+ const stagingRoot=path.join(workspace,`${STAGING_PREFIX}${token}`);
97
+ const backupRoot=path.join(workspace,`${BACKUP_PREFIX}${token}`);
98
+ let backedUp=false;
99
+ let promoted=false;
100
+
101
+ await mkdir(stagingRoot);
1151
102
  try{
1152
- const totalBytes=expectedFiles.reduce((total,file)=>total+file.bytes,0);
1153
- const bufferedEvents=[{
1154
- type:'workspace.runtime.materialize.started',
1155
- status:existing.kind==='absent'?'created':'refreshed',
1156
- generation,
1157
- fileCount:expectedFiles.length,
1158
- totalBytes
1159
- }];
1160
- let writtenBytes=0;
1161
- for(const [index,file] of expectedFiles.entries()){
1162
- throwIfAborted(signal);
1163
- const bytes=file.authority==='sdk-browser-runtime'
1164
- ?await readVerifiedSdkBrowserRuntimeFile(sdkBrowserRuntimeReceipt,{
1165
- browserRuntimeRoot,
1166
- relativePath:file.sourcePath,
1167
- signal
1168
- })
1169
- :await readVerifiedRuntimeFile(runtimeReceipt,{
1170
- runtimeRoot,
1171
- relativePath:file.sourcePath,
1172
- signal
1173
- });
1174
- const destination=resolveContained(stagingRoot,file.path);
1175
- await mkdir(path.dirname(destination),{recursive:true,mode:0o755});
1176
- await writeNewFile(destination,bytes);
1177
- writtenBytes+=bytes.length;
1178
- bufferedEvents.push({
1179
- type:'workspace.runtime.materialize.progress',
1180
- current:index+1,
1181
- total:expectedFiles.length,
1182
- writtenBytes,
1183
- totalBytes,
1184
- path:file.path
1185
- });
1186
- }
1187
- await verifyProjectedTree(stagingRoot,expectedFiles,{signal});
1188
- await writeNewFile(receiptStagingPath,Buffer.from(canonicalJson(desiredReceipt)));
1189
- const receiptStagingInfo=await lstat(receiptStagingPath,{bigint:true});
1190
- if(receiptStagingInfo.isSymbolicLink()||!receiptStagingInfo.isFile()){
1191
- fail('Workspace runtime staged receipt must be a real file.');
1192
- }
1193
- receiptStagingIdentity=fileIdentity(receiptStagingInfo);
1194
- await readPersistentMaterializationReceipt(receiptStagingPath,{signal});
1195
-
1196
- for(const event of bufferedEvents)await emit(onEvent,event);
1197
- throwIfAborted(signal);
1198
- await assertRealDirectoryLocation(
1199
- workspace.canonicalRoot,
1200
- workspace.identity,
1201
- 'Workspace root'
1202
- );
1203
- await assertRealDirectoryLocation(
1204
- metadata.root,
1205
- metadata.identity,
1206
- 'Workspace materialization metadata root'
1207
- );
1208
- await assertRealDirectoryLocation(
1209
- stagingRoot,
1210
- stagingIdentity,
1211
- 'Workspace runtime staging directory'
103
+ await copyCompleteEntry(runtimeArcane,stagingRoot,'SDK Arcane runtime',signal);
104
+ await mkdir(path.join(stagingRoot,'dependencies'),{recursive:true});
105
+ await copyCompleteEntry(
106
+ runtimeStrongType,
107
+ path.join(stagingRoot,'dependencies','strong-type'),
108
+ 'SDK strong-type runtime',
109
+ signal
1212
110
  );
1213
- await assertOwnedFileLocation(
1214
- receiptStagingPath,
1215
- receiptStagingIdentity,
1216
- 'Workspace runtime staged receipt'
1217
- );
1218
- await verifyProjectedTree(stagingRoot,expectedFiles,{signal});
1219
- if(existing.destinationIdentity){
1220
- await assertRealDirectoryLocation(
1221
- destinationRoot,
1222
- existing.destinationIdentity,
1223
- 'Existing workspace runtime projection'
1224
- );
1225
- }
1226
- if(existing.receiptIdentity){
1227
- await assertOwnedFileLocation(
1228
- receiptPath,
1229
- existing.receiptIdentity,
1230
- 'Existing workspace runtime receipt'
111
+ const sdkDestination=path.join(stagingRoot,'sdk');
112
+ await mkdir(sdkDestination,{recursive:true});
113
+ const browserEntries=await readdir(browserRuntime,{withFileTypes:true});
114
+ browserEntries.sort((left,right)=>compareText(left.name,right.name));
115
+ for(const entry of browserEntries){
116
+ throwIfAborted(signal);
117
+ await copyCompleteEntry(
118
+ path.join(browserRuntime,entry.name),
119
+ path.join(sdkDestination,entry.name),
120
+ `SDK browser runtime/${entry.name}`,
121
+ signal
1231
122
  );
1232
123
  }
1233
- throwIfAborted(signal);
1234
-
1235
- try{
1236
- if(existing.destinationIdentity){
1237
- await rename(destinationRoot,backupRoot);
1238
- transaction.oldRootMoved=true;
1239
- }
1240
- if(existing.receiptIdentity){
1241
- await rename(receiptPath,receiptBackupPath);
1242
- transaction.oldReceiptMoved=true;
1243
- }
1244
- await rename(stagingRoot,destinationRoot);
1245
- transaction.newRootMoved=true;
1246
- await rename(receiptStagingPath,receiptPath);
1247
- transaction.newReceiptMoved=true;
1248
- }catch(commitError){
1249
- try{await rollbackInstalledReplacement(transaction);}
1250
- catch(rollbackError){
1251
- uncertainCommit=true;
1252
- throw new AggregateError(
1253
- [commitError,rollbackError],
1254
- 'Installed SDK runtime replacement and rollback both failed.',
1255
- {cause:commitError}
1256
- );
1257
- }
1258
- throw commitError;
1259
- }
1260
124
 
1261
- let committedPersistent;
1262
- let verified;
125
+ throwIfAborted(signal);
1263
126
  try{
1264
- committedPersistent=(await readPersistentMaterializationReceipt(receiptPath)).document;
1265
- if(canonicalJson(committedPersistent)!==canonicalJson(desiredReceipt)){
1266
- fail('Committed installed SDK runtime receipt does not match the staged generation.',
1267
- 'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
127
+ const existing=await lstat(destinationRoot);
128
+ if(existing.isSymbolicLink()||!existing.isDirectory()){
129
+ fail('Workspace Arcane runtime destination must be a real directory when present.');
1268
130
  }
1269
- verified=await verifyWorkspaceRuntime({
1270
- workspaceRoot,
1271
- runtimeRoot,
1272
- runtimeReceipt,
1273
- browserRuntimeRoot,
1274
- sdkBrowserRuntimeReceipt
1275
- });
1276
- }catch(verificationError){
1277
- try{await rollbackInstalledReplacement(transaction);}
1278
- catch(rollbackError){
1279
- uncertainCommit=true;
1280
- throw new AggregateError(
1281
- [verificationError,rollbackError],
1282
- 'Installed SDK runtime verification and rollback both failed.',
1283
- {cause:verificationError}
1284
- );
1285
- }
1286
- throw verificationError;
131
+ await rename(destinationRoot,backupRoot);
132
+ backedUp=true;
133
+ }catch(error){
134
+ if(error?.code!=='ENOENT')throw error;
1287
135
  }
1288
136
 
1289
- const cleanupWarnings=[];
1290
- if(existing.destinationIdentity){
1291
- try{
1292
- await cleanupOwnedTree(backupRoot,workspace,existing.destinationIdentity,{
1293
- prefix:BACKUP_PREFIX,
1294
- label:'backup'
1295
- });
1296
- transaction.oldRootMoved=false;
1297
- }catch(error){cleanupWarnings.push(String(error?.message??error));}
1298
- }
1299
- if(existing.receiptIdentity){
1300
- try{
1301
- await removeOwnedFile(
1302
- receiptBackupPath,
1303
- existing.receiptIdentity,
1304
- 'Workspace runtime receipt backup'
1305
- );
1306
- transaction.oldReceiptMoved=false;
1307
- }catch(error){cleanupWarnings.push(String(error?.message??error));}
1308
- }
1309
- transaction.newRootMoved=false;
1310
- transaction.newReceiptMoved=false;
1311
- stagingCleaned=true;
1312
- const status=existing.kind==='absent'?'created':'refreshed';
1313
- const result=attachMaterialization(verified,{
1314
- status,
1315
- persistentReceipt:committedPersistent,
1316
- receiptPath,
1317
- cleanupWarnings
1318
- });
137
+ await rename(stagingRoot,destinationRoot);
138
+ promoted=true;
1319
139
  await emit(onEvent,{
1320
- type:'workspace.runtime.materialize.completed',
1321
- status,
1322
- generation,
1323
- receiptPath,
1324
- contentSha256:result.contentSha256,
1325
- sourceContentSha256:result.sourceContentSha256,
1326
- sourceBrowserContentSha256:result.sourceBrowserContentSha256,
1327
- fileCount:result.fileCount,
1328
- totalBytes:result.totalBytes,
1329
- cleanupWarnings:Object.freeze([...cleanupWarnings])
1330
- });
1331
- return result;
1332
- }finally{
1333
- if(!uncertainCommit&&!stagingCleaned){
1334
- await cleanupStaging(stagingRoot,workspace,stagingIdentity);
1335
- if(receiptStagingIdentity){
1336
- try{
1337
- await removeOwnedFile(
1338
- receiptStagingPath,
1339
- receiptStagingIdentity,
1340
- 'Workspace runtime staged receipt'
1341
- );
1342
- }catch(error){
1343
- if(error?.code!=='ENOENT')throw error;
1344
- }
1345
- }
1346
- }
1347
- }
1348
- }
1349
-
1350
- export async function materializeWorkspaceRuntime({
1351
- workspaceRoot,
1352
- runtimeRoot=path.join(getSdkRoot(),'runtime'),
1353
- runtimeReceipt,
1354
- browserRuntimeRoot=getSdkBrowserRuntimeRoot(),
1355
- sdkBrowserRuntimeReceipt,
1356
- installedSdkAuthority,
1357
- signal,
1358
- onEvent
1359
- }={}){
1360
- if(!workspaceRoot)fail('workspaceRoot is required to materialize a workspace runtime.');
1361
- await authenticateRuntimeReceipt(runtimeReceipt,{runtimeRoot,signal});
1362
- await authenticateSdkBrowserRuntimeReceipt(sdkBrowserRuntimeReceipt,{
1363
- browserRuntimeRoot,
1364
- signal
1365
- });
1366
- if(installedSdkAuthority){
1367
- return materializeInstalledWorkspaceRuntime({
1368
- workspaceRoot,
1369
- runtimeRoot,
1370
- runtimeReceipt,
1371
- browserRuntimeRoot,
1372
- sdkBrowserRuntimeReceipt,
1373
- installedSdkAuthority,
1374
- signal,
1375
- onEvent
1376
- });
1377
- }
1378
- const expectedFiles=projectRuntimeFiles(runtimeReceipt,sdkBrowserRuntimeReceipt);
1379
- const workspace=await workspaceLocation(workspaceRoot);
1380
- const destinationRoot=path.join(workspace.canonicalRoot,'arcane');
1381
- try{
1382
- const existing=await lstat(destinationRoot,{bigint:true});
1383
- if(existing.isSymbolicLink()||!existing.isDirectory()){
1384
- fail('Existing workspace arcane runtime path must be a real directory.');
1385
- }
1386
- await emit(onEvent,{type:'workspace.runtime.materialize.reused'});
1387
- return verifyWorkspaceRuntime({
1388
- workspaceRoot,
1389
- runtimeRoot,
1390
- runtimeReceipt,
1391
- browserRuntimeRoot,
1392
- sdkBrowserRuntimeReceipt,
1393
- signal,
1394
- onEvent
140
+ type:'workspace.runtime.materialized',
141
+ workspaceRoot:workspace,
142
+ runtimeRoot:destinationRoot
1395
143
  });
144
+ if(backedUp){
145
+ await rm(backupRoot,{recursive:true});
146
+ backedUp=false;
147
+ }
148
+ return {
149
+ kind:'arcane-workspace-runtime-content',
150
+ workspaceRoot:workspace,
151
+ runtimeRoot:destinationRoot
152
+ };
1396
153
  }catch(error){
1397
- if(error?.code!=='ENOENT')throw error;
1398
- }
1399
-
1400
- const stagingRoot=path.join(
1401
- workspace.canonicalRoot,
1402
- `${STAGING_PREFIX}${String(process.pid)}-${randomUUID()}`
1403
- );
1404
- await mkdir(stagingRoot,{mode:0o700});
1405
- const stagingInfo=await lstat(stagingRoot,{bigint:true});
1406
- if(stagingInfo.isSymbolicLink()||!stagingInfo.isDirectory()){
1407
- fail('Workspace runtime staging path must be a real directory.');
1408
- }
1409
- const stagingIdentity=fileIdentity(stagingInfo);
1410
- await assertRealDirectoryLocation(
1411
- workspace.canonicalRoot,
1412
- workspace.identity,
1413
- 'Workspace root'
1414
- );
1415
- await assertRealDirectoryLocation(
1416
- stagingRoot,
1417
- stagingIdentity,
1418
- 'Workspace runtime staging directory'
1419
- );
1420
- const bufferedEvents=[];
1421
- let stagingCleaned=false;
1422
- try{
1423
- const totalBytes=expectedFiles.reduce((total,file)=>total+file.bytes,0);
1424
- bufferedEvents.push({
1425
- type:'workspace.runtime.materialize.started',
1426
- fileCount:expectedFiles.length,
1427
- totalBytes
1428
- });
1429
- let writtenBytes=0;
1430
- for(const [index,file] of expectedFiles.entries()){
1431
- throwIfAborted(signal);
1432
- const bytes=file.authority==='sdk-browser-runtime'
1433
- ?await readVerifiedSdkBrowserRuntimeFile(sdkBrowserRuntimeReceipt,{
1434
- browserRuntimeRoot,
1435
- relativePath:file.sourcePath,
1436
- signal
1437
- })
1438
- :await readVerifiedRuntimeFile(runtimeReceipt,{
1439
- runtimeRoot,
1440
- relativePath:file.sourcePath,
1441
- signal
1442
- });
1443
- const destination=resolveContained(stagingRoot,file.path);
1444
- await mkdir(path.dirname(destination),{recursive:true,mode:0o755});
1445
- await writeNewFile(destination,bytes);
1446
- writtenBytes+=bytes.length;
1447
- bufferedEvents.push({
1448
- type:'workspace.runtime.materialize.progress',
1449
- current:index+1,
1450
- total:expectedFiles.length,
1451
- writtenBytes,
1452
- totalBytes,
1453
- path:file.path
1454
- });
1455
- }
1456
- await verifyProjectedTree(stagingRoot,expectedFiles,{signal});
1457
-
1458
- // Materialization callbacks are held until every staged byte authenticates.
1459
- // They can still veto the commit, but no callback runs between staged writes.
1460
- for(const event of bufferedEvents)await emit(onEvent,event);
1461
- throwIfAborted(signal);
1462
- await assertRealDirectoryLocation(
1463
- workspace.canonicalRoot,
1464
- workspace.identity,
1465
- 'Workspace root'
1466
- );
1467
- await assertRealDirectoryLocation(
1468
- stagingRoot,
1469
- stagingIdentity,
1470
- 'Workspace runtime staging directory'
1471
- );
1472
- await verifyProjectedTree(stagingRoot,expectedFiles,{signal});
1473
- await assertRealDirectoryLocation(
1474
- workspace.canonicalRoot,
1475
- workspace.identity,
1476
- 'Workspace root'
1477
- );
1478
- await assertRealDirectoryLocation(
1479
- stagingRoot,
1480
- stagingIdentity,
1481
- 'Workspace runtime staging directory'
1482
- );
1483
-
1484
- throwIfAborted(signal);
1485
- try{
1486
- await rename(stagingRoot,destinationRoot);
1487
- }catch(error){
1488
- let destinationExists=false;
1489
- try{
1490
- const existing=await lstat(destinationRoot,{bigint:true});
1491
- destinationExists=existing.isDirectory()&&!existing.isSymbolicLink();
1492
- }catch(inspectError){
1493
- if(inspectError?.code!=='ENOENT')throw inspectError;
154
+ if(promoted)await removeTemporaryTree(destinationRoot);
155
+ if(backedUp){
156
+ try{await rename(backupRoot,destinationRoot);}
157
+ catch(rollbackError){
158
+ throw new AggregateError(
159
+ [error,rollbackError],
160
+ 'Workspace runtime materialization and rollback both failed.',
161
+ {cause:error}
162
+ );
1494
163
  }
1495
- if(!destinationExists)throw error;
1496
- await cleanupStaging(stagingRoot,workspace,stagingIdentity);
1497
- stagingCleaned=true;
1498
- await emit(onEvent,{type:'workspace.runtime.materialize.reused'});
1499
- return await verifyWorkspaceRuntime({
1500
- workspaceRoot,
1501
- runtimeRoot,
1502
- runtimeReceipt,
1503
- browserRuntimeRoot,
1504
- sdkBrowserRuntimeReceipt,
1505
- signal,
1506
- onEvent
1507
- });
1508
164
  }
1509
- await cleanupStaging(stagingRoot,workspace,stagingIdentity);
1510
- stagingCleaned=true;
1511
- const receipt=await verifyWorkspaceRuntime({
1512
- workspaceRoot,
1513
- runtimeRoot,
1514
- runtimeReceipt,
1515
- browserRuntimeRoot,
1516
- sdkBrowserRuntimeReceipt,
1517
- signal,
1518
- onEvent
1519
- });
1520
- await emit(onEvent,{
1521
- type:'workspace.runtime.materialize.completed',
1522
- contentSha256:receipt.contentSha256,
1523
- sourceContentSha256:receipt.sourceContentSha256,
1524
- sourceBrowserContentSha256:receipt.sourceBrowserContentSha256,
1525
- fileCount:receipt.fileCount,
1526
- totalBytes:receipt.totalBytes
1527
- });
1528
- return receipt;
165
+ throw error;
1529
166
  }finally{
1530
- if(!stagingCleaned)await cleanupStaging(stagingRoot,workspace,stagingIdentity);
167
+ await removeTemporaryTree(stagingRoot);
168
+ if(!backedUp||promoted)await removeTemporaryTree(backupRoot);
1531
169
  }
1532
170
  }
1533
171
 
1534
- export async function authenticateWorkspaceRuntimeReceipt(receipt,{
1535
- workspaceRoot,
1536
- signal
1537
- }={}){
1538
- if(!receipt||!issuedReceipts.has(receipt)){
1539
- fail('Workspace runtime verification receipt was not issued by this SDK process.');
1540
- }
1541
- if(!workspaceRoot)fail('workspaceRoot is required to authenticate a workspace runtime receipt.');
1542
- return assertWorkspaceRuntimeState(receipt,{workspaceRoot,signal});
1543
- }
1544
-
1545
- export async function readVerifiedWorkspaceRuntimeFile(receipt,{
1546
- workspaceRoot,
1547
- relativePath,
1548
- signal
1549
- }={}){
1550
- if(!receipt||!issuedReceipts.has(receipt)){
1551
- fail('Workspace runtime verification receipt was not issued by this SDK process.');
1552
- }
1553
- if(!workspaceRoot)fail('workspaceRoot is required to read a verified workspace runtime file.');
1554
- throwIfAborted(signal);
1555
- const normalized=safeRelativePath(relativePath);
1556
- const file=receipt.files.find(candidate=>portableKey(candidate.path)===portableKey(normalized));
1557
- if(!file)fail(`Path is not in the verified workspace runtime inventory: ${normalized}.`);
1558
- if(file.bytes>MAX_VERIFIED_WORKSPACE_RUNTIME_FILE_BYTES){
1559
- fail(
1560
- `Verified workspace runtime file exceeds the ${MAX_VERIFIED_WORKSPACE_RUNTIME_FILE_BYTES}-byte serving limit: ${file.path}.`
1561
- );
1562
- }
1563
- const identity=receipt.identities.find(
1564
- candidate=>portableKey(candidate.path)===portableKey(file.path)
1565
- );
1566
- if(!identity)fail(`Verified workspace runtime identity is missing for ${file.path}.`);
1567
-
1568
- const {root}=await assertRequestedWorkspace(receipt,workspaceRoot);
1569
- const rootInfo=await lstat(root,{bigint:true});
1570
- if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()
1571
- ||!identityMatches(rootInfo,receipt.rootIdentity)){
1572
- fail('Workspace arcane runtime root changed after verification.');
1573
- }
1574
- const ancestors=[];
1575
- let current='';
1576
- for(const part of file.path.split('/').slice(0,-1)){
1577
- current=current?`${current}/${part}`:part;
1578
- ancestors.push(current);
1579
- }
1580
- for(const ancestor of ancestors){
1581
- const directory=receipt.directoryIdentities.find(entry=>entry.path===ancestor);
1582
- if(!directory)fail(`Verified workspace runtime directory identity is missing for ${ancestor}.`);
1583
- await assertIdentityAt(root,directory,{directory:true});
1584
- }
1585
- await assertIdentityAt(root,identity,{directory:false});
1586
-
1587
- const filePath=resolveContained(root,file.path);
1588
- let handle;
1589
- try{
1590
- handle=await open(filePath,READ_ONLY_NO_FOLLOW);
1591
- }catch(error){
1592
- if(error?.code==='ELOOP')fail(`Workspace runtime file became a symbolic link: ${file.path}.`);
1593
- throw error;
1594
- }
1595
- try{
1596
- const opened=await handle.stat({bigint:true});
1597
- if(!opened.isFile()||!identityMatches(opened,identity)){
1598
- fail(`Workspace runtime file changed while it was being opened: ${file.path}.`);
1599
- }
1600
- const bytes=await handle.readFile();
1601
- throwIfAborted(signal);
1602
- const after=await handle.stat({bigint:true});
1603
- if(!identityMatches(after,identity)||bytes.length!==file.bytes){
1604
- fail(`Workspace runtime file changed while it was being read: ${file.path}.`);
1605
- }
1606
- if(createHash('sha256').update(bytes).digest('hex')!==file.sha256){
1607
- fail(`Workspace runtime file hash changed: ${file.path}.`);
1608
- }
1609
- await assertIdentityAt(root,identity,{directory:false});
1610
- for(const ancestor of ancestors){
1611
- const directory=receipt.directoryIdentities.find(entry=>entry.path===ancestor);
1612
- await assertIdentityAt(root,directory,{directory:true});
1613
- }
1614
- const canonicalFile=await realpath(filePath);
1615
- const canonicalRelative=path.relative(root,canonicalFile);
1616
- if(canonicalRelative.startsWith('..')||path.isAbsolute(canonicalRelative)){
1617
- fail(`Workspace runtime path left its root: ${file.path}.`);
1618
- }
1619
- return bytes;
1620
- }finally{
1621
- await handle.close();
1622
- }
172
+ export async function materializeWorkspaceRuntime(options={}){
173
+ return materializeWorkspaceRuntimeContent(options);
1623
174
  }