arcane-os 0.1.0-dev.5 → 0.1.0
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/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +86 -0
- package/browser-runtime/dependencies/event-pubsub/index.js +141 -0
- package/browser-runtime/dependencies/event-pubsub/licence +21 -0
- package/browser-runtime/dependencies/event-pubsub/package.json +59 -0
- package/browser-runtime/dependencies/strong-type/index.js +1151 -0
- package/browser-runtime/dependencies/strong-type/licence +21 -0
- package/browser-runtime/dependencies/strong-type/package.json +61 -0
- package/browser-runtime/dom-event-instrumentation.mjs +594 -0
- package/browser-runtime/event-manager.mjs +1342 -0
- package/docs/publishing.md +52 -59
- package/docs/reference/event-manager.md +5 -5
- package/docs/work-amplification.md +4 -3
- package/package.json +11 -7
- package/runtime/ARCANE_RUNTIME_RELEASE.json +1 -1
- package/schemas/arcane-lock.schema.json +86 -1
- package/src/cli/main.mjs +5 -0
- package/src/dev-server.mjs +78 -34
- package/src/doctor.mjs +77 -3
- package/src/import-map.mjs +2328 -0
- package/src/packager/core.mjs +607 -29
- package/src/scaffold.mjs +122 -5
- package/src/sdk-browser-runtime.mjs +585 -0
- package/src/targets/index.mjs +31 -4
- package/src/templates/workspace-template.mjs +135 -23
- package/src/toolchain.mjs +288 -55
- package/src/workspace-operation-lock.mjs +716 -0
- package/src/workspace-runtime.mjs +841 -0
- package/src/workspace.mjs +286 -37
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
|
+
import {lstat,open,readdir,realpath} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {SDK_VERSION} from './constants.mjs';
|
|
7
|
+
|
|
8
|
+
const MANIFEST_NAME='ARCANE_SDK_BROWSER_RELEASE.json';
|
|
9
|
+
const BUILDER='arcane-sdk-browser-runtime-v1';
|
|
10
|
+
const PROTOCOL='arcane-sdk-browser-runtime/1';
|
|
11
|
+
export const SDK_BROWSER_RUNTIME_MANIFEST_SHA256=
|
|
12
|
+
'43baaec850291c28795f6c194001deb5febab88ccab1b033bce6597dd6f6f08f';
|
|
13
|
+
export const SDK_BROWSER_RUNTIME_CONTENT_SHA256=
|
|
14
|
+
'0caa302bc07d4a45f5290504ec62ddce98fdf5e3412f916c10aae3d51b1e5f7c';
|
|
15
|
+
const REPOSITORY='https://github.com/TheWizardNexus/arcane-os-sdk.git';
|
|
16
|
+
const SHA256_PATTERN=/^[a-f0-9]{64}$/u;
|
|
17
|
+
const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
18
|
+
const MAX_VERIFIED_FILE_BYTES=64*1024*1024;
|
|
19
|
+
const packageRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
20
|
+
const defaultRoot=path.join(packageRoot,'browser-runtime');
|
|
21
|
+
const issuedReceipts=new WeakSet();
|
|
22
|
+
|
|
23
|
+
const expectedFiles=Object.freeze([
|
|
24
|
+
['dependencies/event-pubsub/index.js','node_modules/event-pubsub/index.js','vendor-package-identity'],
|
|
25
|
+
['dependencies/event-pubsub/licence','node_modules/event-pubsub/licence','vendor-package-identity'],
|
|
26
|
+
['dependencies/event-pubsub/package.json','node_modules/event-pubsub/package.json','vendor-package-identity'],
|
|
27
|
+
['dependencies/strong-type/index.js','node_modules/strong-type/index.js','vendor-package-identity'],
|
|
28
|
+
['dependencies/strong-type/licence','node_modules/strong-type/licence','vendor-package-identity'],
|
|
29
|
+
['dependencies/strong-type/package.json','node_modules/strong-type/package.json','vendor-package-identity'],
|
|
30
|
+
['dom-event-instrumentation.mjs','src/dom-event-instrumentation.mjs','sdk-source-identity'],
|
|
31
|
+
['event-manager.mjs','src/event-manager.mjs','sdk-source-identity']
|
|
32
|
+
].map(([filePath,sourcePath,provenance])=>Object.freeze({
|
|
33
|
+
path:filePath,sourcePath,provenance
|
|
34
|
+
})));
|
|
35
|
+
|
|
36
|
+
const expectedDirectories=Object.freeze([...new Set(expectedFiles.flatMap(file=>{
|
|
37
|
+
const segments=file.path.split('/');
|
|
38
|
+
return segments.slice(0,-1).map((_,index)=>segments.slice(0,index+1).join('/'));
|
|
39
|
+
}))].sort(compareText));
|
|
40
|
+
|
|
41
|
+
function fail(message,code='ARCANE_SDK_BROWSER_INTEGRITY_FAILED'){
|
|
42
|
+
const error=new Error(message);
|
|
43
|
+
error.code=code;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function throwIfAborted(signal){
|
|
48
|
+
if(!signal?.aborted)return;
|
|
49
|
+
const error=signal.reason instanceof Error?signal.reason:new Error('Operation cancelled.');
|
|
50
|
+
error.code=error.code||'ARCANE_CANCELLED';
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function emit(onEvent,event){
|
|
55
|
+
if(typeof onEvent==='function')await onEvent(Object.freeze(event));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function compareText(left,right){
|
|
59
|
+
const a=String(left);
|
|
60
|
+
const b=String(right);
|
|
61
|
+
return a<b?-1:a>b?1:0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function exactKeys(value,keys){
|
|
65
|
+
return Object.keys(value).sort(compareText).join('\0')===[...keys].sort(compareText).join('\0');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function safeInventoryPath(value,label='manifest'){
|
|
69
|
+
if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
|
|
70
|
+
||value.normalize('NFC')!==value||path.posix.isAbsolute(value)
|
|
71
|
+
||path.posix.normalize(value)!==value||value==='.'||value.startsWith('../')
|
|
72
|
+
||value.includes('/../')){
|
|
73
|
+
fail(`SDK browser runtime ${label} contains an unsafe path: ${String(value)}.`);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function collisionKey(value){
|
|
79
|
+
return value.normalize('NFC').toLocaleLowerCase('en-US');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function immutableInventory(files){
|
|
83
|
+
return Object.freeze(files.map(file=>Object.freeze({...file})));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateSource(source){
|
|
87
|
+
if(!source||typeof source!=='object'||Array.isArray(source)
|
|
88
|
+
||!exactKeys(source,['authority','browserEntry','dependencies','protocol','repository'])
|
|
89
|
+
||source.authority!=='arcane-os-sdk'||source.browserEntry!=='arcane-os/event-manager'
|
|
90
|
+
||source.protocol!==PROTOCOL
|
|
91
|
+
||source.repository!==REPOSITORY||!Array.isArray(source.dependencies)
|
|
92
|
+
||source.dependencies.length!==2){
|
|
93
|
+
fail('SDK browser runtime manifest source authority is invalid.');
|
|
94
|
+
}
|
|
95
|
+
const expectedDependencies=[
|
|
96
|
+
{
|
|
97
|
+
name:'event-pubsub',
|
|
98
|
+
version:'6.1.0',
|
|
99
|
+
resolved:'https://registry.npmjs.org/event-pubsub/-/event-pubsub-6.1.0.tgz',
|
|
100
|
+
integrity:'sha512-FEMlhTxwqGM0hztTixG6FhVFXqp7Eq1ltk5mSreK6Mhy3xWWpLAzEUR6OMvMdNqT3jgSxA8JDhnhyAG3X4Xy7Q=='
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name:'strong-type',
|
|
104
|
+
version:'2.0.0',
|
|
105
|
+
resolved:'https://registry.npmjs.org/strong-type/-/strong-type-2.0.0.tgz',
|
|
106
|
+
integrity:'sha512-HHrY9qYC7yn+5mlewiI3k9RQM9gZqGQsqbomZcd10Ks0h4RlX01nnkWbCe4AsVPCI6KaFvpkWm1nHMD+Ykup6g=='
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
for(const [index,expected] of expectedDependencies.entries()){
|
|
110
|
+
const actual=source.dependencies[index];
|
|
111
|
+
if(!actual||typeof actual!=='object'||Array.isArray(actual)
|
|
112
|
+
||!exactKeys(actual,['integrity','name','resolved','version'])
|
|
113
|
+
||actual.name!==expected.name||actual.version!==expected.version
|
|
114
|
+
||actual.resolved!==expected.resolved||actual.integrity!==expected.integrity){
|
|
115
|
+
fail('SDK browser runtime manifest dependency identity is invalid.');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
...source,
|
|
120
|
+
dependencies:Object.freeze(source.dependencies.map(item=>Object.freeze({...item})))
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateRelease(value){
|
|
125
|
+
if(!value||typeof value!=='object'||Array.isArray(value)){
|
|
126
|
+
fail('SDK browser runtime manifest must be a JSON object.');
|
|
127
|
+
}
|
|
128
|
+
if(!exactKeys(value,[
|
|
129
|
+
'builder','contentSha256','fileCount','files','schemaVersion','sdkVersion','source','totalBytes'
|
|
130
|
+
])){
|
|
131
|
+
fail('SDK browser runtime manifest contains missing or unsupported fields.');
|
|
132
|
+
}
|
|
133
|
+
if(value.schemaVersion!==1||value.builder!==BUILDER){
|
|
134
|
+
fail('SDK browser runtime manifest uses an unsupported schema or builder.');
|
|
135
|
+
}
|
|
136
|
+
if(value.sdkVersion!==SDK_VERSION){
|
|
137
|
+
fail('SDK browser runtime manifest sdkVersion is incompatible with this SDK.');
|
|
138
|
+
}
|
|
139
|
+
const source=validateSource(value.source);
|
|
140
|
+
if(!Array.isArray(value.files)||!Number.isSafeInteger(value.fileCount)
|
|
141
|
+
||value.fileCount!==value.files.length||value.fileCount!==expectedFiles.length
|
|
142
|
+
||!Number.isSafeInteger(value.totalBytes)||value.totalBytes<0
|
|
143
|
+
||!SHA256_PATTERN.test(value.contentSha256)){
|
|
144
|
+
fail('SDK browser runtime manifest inventory summary is invalid.');
|
|
145
|
+
}
|
|
146
|
+
let previous='';
|
|
147
|
+
let totalBytes=0;
|
|
148
|
+
const collisionKeys=new Set();
|
|
149
|
+
const files=value.files.map((entry,index)=>{
|
|
150
|
+
if(!entry||typeof entry!=='object'||Array.isArray(entry)
|
|
151
|
+
||!exactKeys(entry,['bytes','path','provenance','sha256','sourcePath'])){
|
|
152
|
+
fail(`SDK browser runtime manifest file ${index} is invalid.`);
|
|
153
|
+
}
|
|
154
|
+
const relative=safeInventoryPath(entry.path);
|
|
155
|
+
const sourcePath=safeInventoryPath(entry.sourcePath,'source inventory');
|
|
156
|
+
const expected=expectedFiles[index];
|
|
157
|
+
const key=collisionKey(relative);
|
|
158
|
+
if(relative===MANIFEST_NAME||collisionKeys.has(key)
|
|
159
|
+
||(previous&&compareText(previous,relative)>=0)
|
|
160
|
+
||relative!==expected.path||sourcePath!==expected.sourcePath
|
|
161
|
+
||entry.provenance!==expected.provenance){
|
|
162
|
+
fail(`SDK browser runtime manifest inventory is invalid at ${relative}.`);
|
|
163
|
+
}
|
|
164
|
+
if(!Number.isSafeInteger(entry.bytes)||entry.bytes<0||!SHA256_PATTERN.test(entry.sha256)){
|
|
165
|
+
fail(`SDK browser runtime manifest metadata is invalid for ${relative}.`);
|
|
166
|
+
}
|
|
167
|
+
collisionKeys.add(key);
|
|
168
|
+
previous=relative;
|
|
169
|
+
totalBytes+=entry.bytes;
|
|
170
|
+
if(!Number.isSafeInteger(totalBytes)){
|
|
171
|
+
fail('SDK browser runtime manifest total byte count is too large.');
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
path:relative,
|
|
175
|
+
sourcePath,
|
|
176
|
+
provenance:entry.provenance,
|
|
177
|
+
bytes:entry.bytes,
|
|
178
|
+
sha256:entry.sha256
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
if(totalBytes!==value.totalBytes){
|
|
182
|
+
fail('SDK browser runtime manifest totalBytes does not match its inventory.');
|
|
183
|
+
}
|
|
184
|
+
const aggregate=createHash('sha256').update(JSON.stringify(files)).digest('hex');
|
|
185
|
+
if(aggregate!==value.contentSha256){
|
|
186
|
+
fail('SDK browser runtime manifest contentSha256 does not match its inventory.');
|
|
187
|
+
}
|
|
188
|
+
if(value.contentSha256!==SDK_BROWSER_RUNTIME_CONTENT_SHA256){
|
|
189
|
+
fail('SDK browser runtime manifest does not match the trusted SDK browser closure.');
|
|
190
|
+
}
|
|
191
|
+
return Object.freeze({...value,source,files:immutableInventory(files)});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sameIdentity(before,after){
|
|
195
|
+
return before.dev===after.dev&&before.ino===after.ino&&before.size===after.size
|
|
196
|
+
&&before.mtimeNs===after.mtimeNs&&before.ctimeNs===after.ctimeNs
|
|
197
|
+
&&before.nlink===after.nlink;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function fileIdentity(info){
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
device:String(info.dev),
|
|
203
|
+
inode:String(info.ino),
|
|
204
|
+
bytes:Number(info.size),
|
|
205
|
+
modifiedNanoseconds:String(info.mtimeNs),
|
|
206
|
+
changedNanoseconds:String(info.ctimeNs),
|
|
207
|
+
links:String(info.nlink)
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function identityMatches(info,identity){
|
|
212
|
+
return String(info.dev)===identity.device&&String(info.ino)===identity.inode
|
|
213
|
+
&&Number(info.size)===identity.bytes
|
|
214
|
+
&&String(info.mtimeNs)===identity.modifiedNanoseconds
|
|
215
|
+
&&String(info.ctimeNs)===identity.changedNanoseconds
|
|
216
|
+
&&String(info.nlink)===identity.links;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function containedPath(root,relative,label='file'){
|
|
220
|
+
const absolute=path.resolve(root,...safeInventoryPath(relative).split('/'));
|
|
221
|
+
const fromRoot=path.relative(root,absolute);
|
|
222
|
+
if(fromRoot.startsWith('..')||path.isAbsolute(fromRoot)){
|
|
223
|
+
fail(`SDK browser runtime ${label} escapes its root: ${relative}.`);
|
|
224
|
+
}
|
|
225
|
+
return absolute;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function assertCanonicalPath(root,filePath,relative){
|
|
229
|
+
const canonical=await realpath(filePath);
|
|
230
|
+
const fromRoot=path.relative(root,canonical);
|
|
231
|
+
if(fromRoot.startsWith('..')||path.isAbsolute(fromRoot)){
|
|
232
|
+
fail(`SDK browser runtime path left its root: ${relative}.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function scanTree(root,{signal}={}){
|
|
237
|
+
const files=[];
|
|
238
|
+
const directories=[];
|
|
239
|
+
const expectedDirectorySet=new Set(expectedDirectories);
|
|
240
|
+
async function visit(directory,relativeRoot=''){
|
|
241
|
+
throwIfAborted(signal);
|
|
242
|
+
const entries=await readdir(directory,{withFileTypes:true});
|
|
243
|
+
entries.sort((left,right)=>compareText(left.name,right.name));
|
|
244
|
+
for(const entry of entries){
|
|
245
|
+
throwIfAborted(signal);
|
|
246
|
+
const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
|
|
247
|
+
if(relative===MANIFEST_NAME)continue;
|
|
248
|
+
safeInventoryPath(relative,'tree');
|
|
249
|
+
const absolute=path.join(directory,entry.name);
|
|
250
|
+
const info=await lstat(absolute,{bigint:true});
|
|
251
|
+
if(info.isSymbolicLink()){
|
|
252
|
+
fail(`SDK browser runtime contains a symbolic link or junction: ${relative}.`);
|
|
253
|
+
}
|
|
254
|
+
if(info.isDirectory()){
|
|
255
|
+
if(!expectedDirectorySet.has(relative)){
|
|
256
|
+
fail(`SDK browser runtime contains an unexpected directory: ${relative}.`);
|
|
257
|
+
}
|
|
258
|
+
await assertCanonicalPath(root,absolute,relative);
|
|
259
|
+
directories.push(Object.freeze({path:relative,...fileIdentity(info)}));
|
|
260
|
+
await visit(absolute,relative);
|
|
261
|
+
}else if(info.isFile()){
|
|
262
|
+
files.push(relative);
|
|
263
|
+
}else{
|
|
264
|
+
fail(`SDK browser runtime contains a non-file entry: ${relative}.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
await visit(root);
|
|
269
|
+
files.sort(compareText);
|
|
270
|
+
directories.sort((left,right)=>compareText(left.path,right.path));
|
|
271
|
+
if(JSON.stringify(directories.map(item=>item.path))!==JSON.stringify(expectedDirectories)){
|
|
272
|
+
fail('SDK browser runtime directory inventory is incomplete.');
|
|
273
|
+
}
|
|
274
|
+
return {files,directories:Object.freeze(directories)};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function openVerified(filePath,{expectedBytes,expectedIdentity,root,relative,signal}={}){
|
|
278
|
+
throwIfAborted(signal);
|
|
279
|
+
const before=await lstat(filePath,{bigint:true});
|
|
280
|
+
if(before.isSymbolicLink()||!before.isFile()
|
|
281
|
+
||(expectedIdentity&&!identityMatches(before,expectedIdentity))
|
|
282
|
+
||(expectedBytes!==undefined&&before.size!==BigInt(expectedBytes))){
|
|
283
|
+
fail(`SDK browser runtime file changed: ${relative}.`);
|
|
284
|
+
}
|
|
285
|
+
await assertCanonicalPath(root,filePath,relative);
|
|
286
|
+
let handle;
|
|
287
|
+
try{
|
|
288
|
+
handle=await open(filePath,READ_ONLY_NO_FOLLOW);
|
|
289
|
+
}catch(error){
|
|
290
|
+
if(error?.code==='ELOOP'){
|
|
291
|
+
fail(`SDK browser runtime file became a symbolic link: ${relative}.`);
|
|
292
|
+
}
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
try{
|
|
296
|
+
const opened=await handle.stat({bigint:true});
|
|
297
|
+
if(!opened.isFile()||!sameIdentity(before,opened)
|
|
298
|
+
||(expectedIdentity&&!identityMatches(opened,expectedIdentity))){
|
|
299
|
+
fail(`SDK browser runtime file changed while it was being opened: ${relative}.`);
|
|
300
|
+
}
|
|
301
|
+
if(opened.size>BigInt(MAX_VERIFIED_FILE_BYTES)){
|
|
302
|
+
fail(`SDK browser runtime file exceeds the verification limit: ${relative}.`);
|
|
303
|
+
}
|
|
304
|
+
const bytes=await handle.readFile();
|
|
305
|
+
throwIfAborted(signal);
|
|
306
|
+
const after=await handle.stat({bigint:true});
|
|
307
|
+
if(!sameIdentity(opened,after)||bytes.length!==Number(after.size)){
|
|
308
|
+
fail(`SDK browser runtime file changed while it was being read: ${relative}.`);
|
|
309
|
+
}
|
|
310
|
+
const current=await lstat(filePath,{bigint:true});
|
|
311
|
+
if(current.isSymbolicLink()||!current.isFile()||!sameIdentity(after,current)){
|
|
312
|
+
fail(`SDK browser runtime path changed while it was being read: ${relative}.`);
|
|
313
|
+
}
|
|
314
|
+
await assertCanonicalPath(root,filePath,relative);
|
|
315
|
+
return {bytes,identity:fileIdentity(after)};
|
|
316
|
+
}finally{
|
|
317
|
+
await handle.close();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function readRelease(root,signal){
|
|
322
|
+
const manifestPath=path.join(root,MANIFEST_NAME);
|
|
323
|
+
let result;
|
|
324
|
+
try{
|
|
325
|
+
result=await openVerified(manifestPath,{root,relative:MANIFEST_NAME,signal});
|
|
326
|
+
}catch(error){
|
|
327
|
+
if(error?.code==='ENOENT'){
|
|
328
|
+
fail(`SDK browser runtime manifest is missing: ${manifestPath}.`);
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
let value;
|
|
333
|
+
try{
|
|
334
|
+
value=JSON.parse(result.bytes.toString('utf8'));
|
|
335
|
+
}catch(error){
|
|
336
|
+
fail(`SDK browser runtime manifest is not valid JSON: ${error.message}`);
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
release:validateRelease(value),
|
|
340
|
+
manifestPath,
|
|
341
|
+
manifestSha256:createHash('sha256').update(result.bytes).digest('hex'),
|
|
342
|
+
manifestIdentity:result.identity
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function verifiedRoot(browserRuntimeRoot){
|
|
347
|
+
const requested=path.resolve(browserRuntimeRoot);
|
|
348
|
+
let info;
|
|
349
|
+
try{
|
|
350
|
+
info=await lstat(requested,{bigint:true});
|
|
351
|
+
}catch(error){
|
|
352
|
+
if(error?.code==='ENOENT')fail(`SDK browser runtime root does not exist: ${requested}.`);
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
if(info.isSymbolicLink()||!info.isDirectory()){
|
|
356
|
+
fail('SDK browser runtime root must be a real directory.');
|
|
357
|
+
}
|
|
358
|
+
const canonical=await realpath(requested);
|
|
359
|
+
const canonicalInfo=await lstat(canonical,{bigint:true});
|
|
360
|
+
if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()){
|
|
361
|
+
fail('SDK browser runtime root must be a real directory.');
|
|
362
|
+
}
|
|
363
|
+
return {canonical,identity:fileIdentity(canonicalInfo)};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function directoryIdentityMatches(actual,expected){
|
|
367
|
+
return actual.path===expected.path&&actual.device===expected.device
|
|
368
|
+
&&actual.inode===expected.inode&&actual.bytes===expected.bytes
|
|
369
|
+
&&actual.modifiedNanoseconds===expected.modifiedNanoseconds
|
|
370
|
+
&&actual.changedNanoseconds===expected.changedNanoseconds
|
|
371
|
+
&&actual.links===expected.links;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function assertReceiptState(receipt,signal){
|
|
375
|
+
throwIfAborted(signal);
|
|
376
|
+
const rootBefore=await lstat(receipt.canonicalLocation,{bigint:true});
|
|
377
|
+
if(rootBefore.isSymbolicLink()||!rootBefore.isDirectory()
|
|
378
|
+
||!identityMatches(rootBefore,receipt.rootIdentity)){
|
|
379
|
+
fail('SDK browser runtime root changed after verification.');
|
|
380
|
+
}
|
|
381
|
+
const scanned=await scanTree(receipt.canonicalLocation,{signal});
|
|
382
|
+
if(JSON.stringify(scanned.files)!==JSON.stringify(receipt.files.map(file=>file.path))
|
|
383
|
+
||scanned.directories.length!==receipt.directories.length
|
|
384
|
+
||scanned.directories.some((item,index)=>!directoryIdentityMatches(item,receipt.directories[index]))){
|
|
385
|
+
fail('SDK browser runtime inventory changed after verification.');
|
|
386
|
+
}
|
|
387
|
+
const manifestInfo=await lstat(receipt.manifestPath,{bigint:true});
|
|
388
|
+
if(manifestInfo.isSymbolicLink()||!manifestInfo.isFile()
|
|
389
|
+
||!identityMatches(manifestInfo,receipt.manifestIdentity)){
|
|
390
|
+
fail('SDK browser runtime manifest changed after verification.');
|
|
391
|
+
}
|
|
392
|
+
for(const identity of receipt.identities){
|
|
393
|
+
throwIfAborted(signal);
|
|
394
|
+
const absolute=containedPath(receipt.canonicalLocation,identity.path);
|
|
395
|
+
const info=await lstat(absolute,{bigint:true});
|
|
396
|
+
if(info.isSymbolicLink()||!info.isFile()||!identityMatches(info,identity)){
|
|
397
|
+
fail(`SDK browser runtime file changed after verification: ${identity.path}.`);
|
|
398
|
+
}
|
|
399
|
+
await assertCanonicalPath(receipt.canonicalLocation,absolute,identity.path);
|
|
400
|
+
}
|
|
401
|
+
const sourceRoot=path.dirname(receipt.canonicalLocation);
|
|
402
|
+
for(const identity of receipt.sourceIdentities){
|
|
403
|
+
throwIfAborted(signal);
|
|
404
|
+
const file=receipt.files.find(candidate=>candidate.sourcePath===identity.path);
|
|
405
|
+
if(!file)fail(`SDK browser source identity is not declared: ${identity.path}.`);
|
|
406
|
+
const result=await openVerified(containedPath(sourceRoot,identity.path,'source file'),{
|
|
407
|
+
expectedBytes:file.bytes,
|
|
408
|
+
expectedIdentity:identity,
|
|
409
|
+
root:sourceRoot,
|
|
410
|
+
relative:identity.path,
|
|
411
|
+
signal
|
|
412
|
+
});
|
|
413
|
+
if(createHash('sha256').update(result.bytes).digest('hex')!==file.sha256){
|
|
414
|
+
fail(`SDK browser runtime package source changed after verification: ${identity.path}.`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const rootAfter=await lstat(receipt.canonicalLocation,{bigint:true});
|
|
418
|
+
if(rootAfter.isSymbolicLink()||!rootAfter.isDirectory()
|
|
419
|
+
||!identityMatches(rootAfter,receipt.rootIdentity)||!sameIdentity(rootBefore,rootAfter)){
|
|
420
|
+
fail('SDK browser runtime root changed while its receipt was authenticated.');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function getSdkBrowserRuntimeRoot(){
|
|
425
|
+
return defaultRoot;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export async function loadSdkBrowserRuntimeRelease({browserRuntimeRoot=defaultRoot,signal}={}){
|
|
429
|
+
throwIfAborted(signal);
|
|
430
|
+
const {canonical}=await verifiedRoot(browserRuntimeRoot);
|
|
431
|
+
return (await readRelease(canonical,signal)).release;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function authenticateSdkBrowserRuntimeReceipt(receipt,{
|
|
435
|
+
browserRuntimeRoot=defaultRoot,
|
|
436
|
+
signal
|
|
437
|
+
}={}){
|
|
438
|
+
if(!receipt||!issuedReceipts.has(receipt)){
|
|
439
|
+
fail('SDK browser runtime receipt was not issued by this SDK process.');
|
|
440
|
+
}
|
|
441
|
+
const {canonical}=await verifiedRoot(browserRuntimeRoot);
|
|
442
|
+
if(receipt.canonicalLocation!==canonical){
|
|
443
|
+
fail('SDK browser runtime receipt belongs to a different location.');
|
|
444
|
+
}
|
|
445
|
+
await assertReceiptState(receipt,signal);
|
|
446
|
+
return receipt;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function readVerifiedSdkBrowserRuntimeFile(receipt,{
|
|
450
|
+
browserRuntimeRoot=defaultRoot,
|
|
451
|
+
relativePath,
|
|
452
|
+
signal
|
|
453
|
+
}={}){
|
|
454
|
+
if(!receipt||!issuedReceipts.has(receipt)){
|
|
455
|
+
fail('SDK browser runtime receipt was not issued by this SDK process.');
|
|
456
|
+
}
|
|
457
|
+
throwIfAborted(signal);
|
|
458
|
+
const normalized=safeInventoryPath(relativePath);
|
|
459
|
+
const key=value=>process.platform==='win32'?collisionKey(value):value;
|
|
460
|
+
const file=receipt.files.find(candidate=>key(candidate.path)===key(normalized));
|
|
461
|
+
if(!file)fail(`Path is not in the verified SDK browser runtime inventory: ${normalized}.`);
|
|
462
|
+
const identity=receipt.identities.find(candidate=>key(candidate.path)===key(file.path));
|
|
463
|
+
if(!identity)fail(`Verified SDK browser runtime identity is missing for ${file.path}.`);
|
|
464
|
+
const {canonical}=await verifiedRoot(browserRuntimeRoot);
|
|
465
|
+
if(receipt.canonicalLocation!==canonical){
|
|
466
|
+
fail('SDK browser runtime receipt belongs to a different location.');
|
|
467
|
+
}
|
|
468
|
+
const rootInfo=await lstat(canonical,{bigint:true});
|
|
469
|
+
if(!identityMatches(rootInfo,receipt.rootIdentity)){
|
|
470
|
+
fail('SDK browser runtime root changed after verification.');
|
|
471
|
+
}
|
|
472
|
+
for(const directory of receipt.directories){
|
|
473
|
+
if(file.path.startsWith(`${directory.path}/`)){
|
|
474
|
+
const info=await lstat(containedPath(canonical,directory.path),{bigint:true});
|
|
475
|
+
if(info.isSymbolicLink()||!info.isDirectory()||!identityMatches(info,directory)){
|
|
476
|
+
fail(`SDK browser runtime directory changed after verification: ${directory.path}.`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const filePath=containedPath(canonical,file.path);
|
|
481
|
+
const result=await openVerified(filePath,{
|
|
482
|
+
expectedBytes:file.bytes,
|
|
483
|
+
expectedIdentity:identity,
|
|
484
|
+
root:canonical,
|
|
485
|
+
relative:file.path,
|
|
486
|
+
signal
|
|
487
|
+
});
|
|
488
|
+
if(createHash('sha256').update(result.bytes).digest('hex')!==file.sha256){
|
|
489
|
+
fail(`SDK browser runtime file hash changed: ${file.path}.`);
|
|
490
|
+
}
|
|
491
|
+
return result.bytes;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export async function verifySdkBrowserRuntime({
|
|
495
|
+
browserRuntimeRoot=defaultRoot,
|
|
496
|
+
signal,
|
|
497
|
+
onEvent
|
|
498
|
+
}={}){
|
|
499
|
+
throwIfAborted(signal);
|
|
500
|
+
const {canonical,identity:rootIdentity}=await verifiedRoot(browserRuntimeRoot);
|
|
501
|
+
const {release,manifestPath,manifestSha256,manifestIdentity}=await readRelease(canonical,signal);
|
|
502
|
+
if(manifestSha256!==SDK_BROWSER_RUNTIME_MANIFEST_SHA256){
|
|
503
|
+
fail('SDK browser runtime manifest bytes do not match the trusted SDK release.');
|
|
504
|
+
}
|
|
505
|
+
await emit(onEvent,{
|
|
506
|
+
type:'sdk-browser-runtime.verify.started',
|
|
507
|
+
fileCount:release.fileCount,
|
|
508
|
+
totalBytes:release.totalBytes
|
|
509
|
+
});
|
|
510
|
+
const scanned=await scanTree(canonical,{signal});
|
|
511
|
+
if(JSON.stringify(scanned.files)!==JSON.stringify(release.files.map(file=>file.path))){
|
|
512
|
+
fail('SDK browser runtime file inventory does not match ARCANE_SDK_BROWSER_RELEASE.json.');
|
|
513
|
+
}
|
|
514
|
+
const identities=[];
|
|
515
|
+
const sourceIdentities=[];
|
|
516
|
+
const sourceRoot=path.dirname(canonical);
|
|
517
|
+
let verifiedBytes=0;
|
|
518
|
+
for(const [index,file] of release.files.entries()){
|
|
519
|
+
throwIfAborted(signal);
|
|
520
|
+
const absolute=containedPath(canonical,file.path);
|
|
521
|
+
const result=await openVerified(absolute,{
|
|
522
|
+
expectedBytes:file.bytes,
|
|
523
|
+
root:canonical,
|
|
524
|
+
relative:file.path,
|
|
525
|
+
signal
|
|
526
|
+
});
|
|
527
|
+
if(createHash('sha256').update(result.bytes).digest('hex')!==file.sha256){
|
|
528
|
+
fail(`SDK browser runtime integrity check failed for ${file.path}.`);
|
|
529
|
+
}
|
|
530
|
+
const sourceResult=await openVerified(
|
|
531
|
+
containedPath(sourceRoot,file.sourcePath,'source file'),
|
|
532
|
+
{
|
|
533
|
+
expectedBytes:file.bytes,
|
|
534
|
+
root:sourceRoot,
|
|
535
|
+
relative:file.sourcePath,
|
|
536
|
+
signal
|
|
537
|
+
}
|
|
538
|
+
);
|
|
539
|
+
if(!result.bytes.equals(sourceResult.bytes)){
|
|
540
|
+
fail(
|
|
541
|
+
`SDK browser runtime file does not match its declared package source: `
|
|
542
|
+
+`${file.path} (${file.sourcePath}).`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
identities.push(Object.freeze({path:file.path,...result.identity}));
|
|
546
|
+
sourceIdentities.push(Object.freeze({path:file.sourcePath,...sourceResult.identity}));
|
|
547
|
+
verifiedBytes+=file.bytes;
|
|
548
|
+
await emit(onEvent,{
|
|
549
|
+
type:'sdk-browser-runtime.verify.progress',
|
|
550
|
+
current:index+1,
|
|
551
|
+
total:release.fileCount,
|
|
552
|
+
verifiedBytes,
|
|
553
|
+
totalBytes:release.totalBytes,
|
|
554
|
+
path:file.path
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const receipt=Object.freeze({
|
|
558
|
+
schemaVersion:1,
|
|
559
|
+
kind:'arcane-sdk-browser-runtime-verification',
|
|
560
|
+
canonicalLocation:canonical,
|
|
561
|
+
rootIdentity,
|
|
562
|
+
manifestPath,
|
|
563
|
+
manifestSha256,
|
|
564
|
+
manifestIdentity,
|
|
565
|
+
builder:release.builder,
|
|
566
|
+
sdkVersion:release.sdkVersion,
|
|
567
|
+
source:release.source,
|
|
568
|
+
files:release.files,
|
|
569
|
+
fileCount:release.fileCount,
|
|
570
|
+
totalBytes:release.totalBytes,
|
|
571
|
+
contentSha256:release.contentSha256,
|
|
572
|
+
identities:Object.freeze(identities),
|
|
573
|
+
sourceIdentities:Object.freeze(sourceIdentities),
|
|
574
|
+
directories:scanned.directories
|
|
575
|
+
});
|
|
576
|
+
issuedReceipts.add(receipt);
|
|
577
|
+
await assertReceiptState(receipt,signal);
|
|
578
|
+
await emit(onEvent,{
|
|
579
|
+
type:'sdk-browser-runtime.verify.completed',
|
|
580
|
+
contentSha256:receipt.contentSha256,
|
|
581
|
+
fileCount:receipt.fileCount,
|
|
582
|
+
totalBytes:receipt.totalBytes
|
|
583
|
+
});
|
|
584
|
+
return receipt;
|
|
585
|
+
}
|
package/src/targets/index.mjs
CHANGED
|
@@ -163,6 +163,8 @@ const browserAdapter=Object.freeze({
|
|
|
163
163
|
dryRun=false,
|
|
164
164
|
signal,
|
|
165
165
|
onEvent,
|
|
166
|
+
workspaceOperationLease,
|
|
167
|
+
runtimeVerificationState,
|
|
166
168
|
validateSourceState
|
|
167
169
|
}={}){
|
|
168
170
|
await this.plan({workspaceRoot,appId,format,signing,signal});
|
|
@@ -173,17 +175,42 @@ const browserAdapter=Object.freeze({
|
|
|
173
175
|
dryRun,
|
|
174
176
|
signal,
|
|
175
177
|
onEvent,
|
|
178
|
+
workspaceOperationLease,
|
|
179
|
+
runtimeVerificationState,
|
|
176
180
|
validateSourceState
|
|
177
181
|
});
|
|
178
182
|
return {target:'browser',format,signing,release};
|
|
179
183
|
},
|
|
180
|
-
async verify({workspaceRoot,appId,signal,onEvent}={}){
|
|
184
|
+
async verify({workspaceRoot,appId,runtimeVerificationState,signal,onEvent}={}){
|
|
181
185
|
throwIfAborted(signal);
|
|
182
|
-
return {
|
|
186
|
+
return {
|
|
187
|
+
target:'browser',
|
|
188
|
+
release:await verifyApp({
|
|
189
|
+
workspaceRoot,
|
|
190
|
+
appId,
|
|
191
|
+
runtimeVerificationState,
|
|
192
|
+
signal,
|
|
193
|
+
onEvent
|
|
194
|
+
})
|
|
195
|
+
};
|
|
183
196
|
},
|
|
184
|
-
async run({
|
|
197
|
+
async run({
|
|
198
|
+
workspaceRoot,
|
|
199
|
+
appId,
|
|
200
|
+
runtimeVerificationState,
|
|
201
|
+
host='127.0.0.1',
|
|
202
|
+
port=0,
|
|
203
|
+
signal,
|
|
204
|
+
onEvent
|
|
205
|
+
}={}){
|
|
185
206
|
throwIfAborted(signal);
|
|
186
|
-
const verified=await verifyApp({
|
|
207
|
+
const verified=await verifyApp({
|
|
208
|
+
workspaceRoot,
|
|
209
|
+
appId,
|
|
210
|
+
runtimeVerificationState,
|
|
211
|
+
signal,
|
|
212
|
+
onEvent
|
|
213
|
+
});
|
|
187
214
|
const server=await startDevServer({
|
|
188
215
|
workspaceRoot,
|
|
189
216
|
appId,
|