arcane-os 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
- package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -148
- package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1042 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +27 -67
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +109 -1558
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -185
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1295
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -719
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2965
- package/docs/reference/sdk-api.md +0 -6698
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
|
@@ -1,45 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
25
|
-
const
|
|
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='
|
|
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(
|
|
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
|
|
66
|
-
|
|
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(
|
|
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}
|
|
38
|
+
if(error?.code==='ENOENT')fail(`${label} does not exist: ${requested}.`);
|
|
447
39
|
throw error;
|
|
448
40
|
}
|
|
449
|
-
if(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
|
50
|
+
async function copyCompleteEntry(source,destination,label,signal){
|
|
466
51
|
throwIfAborted(signal);
|
|
467
|
-
const
|
|
468
|
-
if(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
828
|
-
|
|
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
|
|
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
|
|
844
|
-
|
|
845
|
-
await
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
)
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
1262
|
-
let verified;
|
|
125
|
+
throwIfAborted(signal);
|
|
1263
126
|
try{
|
|
1264
|
-
|
|
1265
|
-
if(
|
|
1266
|
-
fail('
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
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.
|
|
1321
|
-
|
|
1322
|
-
|
|
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(
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
+
await removeTemporaryTree(stagingRoot);
|
|
168
|
+
if(!backedUp||promoted)await removeTemporaryTree(backupRoot);
|
|
1531
169
|
}
|
|
1532
170
|
}
|
|
1533
171
|
|
|
1534
|
-
export async function
|
|
1535
|
-
|
|
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
|
}
|