arcane-os 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/NOTICE +5 -3
  3. package/README.md +73 -24
  4. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +67 -18
  5. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +16 -5
  6. package/browser-runtime/ai/browser-kokoro-worker.mjs +3 -0
  7. package/browser-runtime/ai/browser-speech-artifacts.mjs +1108 -0
  8. package/browser-runtime/ai/browser-speech-providers.mjs +780 -0
  9. package/browser-runtime/ai/browser-speech.mjs +9 -0
  10. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1537 -167
  11. package/browser-runtime/ai/browser-wasm.mjs +46 -1
  12. package/browser-runtime/ai/browser-whisper-worker.mjs +3 -0
  13. package/browser-runtime/ai/browser-wllama-runtime.mjs +677 -132
  14. package/browser-runtime/ai/model-controller.mjs +138 -12
  15. package/browser-runtime/ai/speech-worker-client.mjs +207 -0
  16. package/browser-runtime/ai/speech-worker-runtime.mjs +516 -0
  17. package/browser-runtime/ai/wllama/index.mjs +389 -0
  18. package/docs/architecture.md +132 -22
  19. package/docs/reference/README.md +1 -1
  20. package/docs/reference/ai/browser-wasm.md +101 -42
  21. package/docs/reference/availability-and-normalization.md +19 -5
  22. package/docs/reference/behavioral-testing.md +18 -5
  23. package/docs/reference/cli.md +2 -2
  24. package/docs/reference/inventory/package-api.json +14 -14
  25. package/docs/reference/protocols.md +4 -4
  26. package/docs/reference/sdk-api.md +68 -38
  27. package/docs/work-amplification.md +8 -4
  28. package/package.json +7 -3
  29. package/runtime/ARCANE_RUNTIME_RELEASE.json +50 -20
  30. package/runtime/arcane/components/chat.html +551 -62
  31. package/runtime/arcane/components/speech.html +1113 -265
  32. package/runtime/arcane/entities/Chat.js +246 -43
  33. package/runtime/arcane/modules/AI.js +1394 -162
  34. package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
  35. package/runtime/arcane/modules/AIRuntimeState.js +872 -0
  36. package/runtime/arcane/modules/ConfiguredAIChatSession.js +382 -31
  37. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +1106 -0
  38. package/runtime/arcane/modules/DocumentLexicalSearch.js +292 -0
  39. package/runtime/arcane/modules/PersistentAIChatSession.js +268 -0
  40. package/runtime/arcane/modules/StaticDocumentCatalog.js +25 -206
  41. package/schemas/arcane-lock.schema.json +10 -6
  42. package/src/cli/main.mjs +14 -2
  43. package/src/constants.mjs +1 -1
  44. package/src/dev-server.mjs +273 -26
  45. package/src/doctor.mjs +1 -3
  46. package/src/import-map.mjs +193 -84
  47. package/src/packager/core.mjs +313 -41
  48. package/src/runtime.mjs +14 -4
  49. package/src/scaffold.mjs +45 -17
  50. package/src/sdk-browser-runtime.mjs +28 -75
  51. package/src/templates/workspace-template.mjs +27 -8
  52. package/src/toolchain.mjs +13 -2
  53. package/src/workspace-runtime.mjs +1 -1
  54. package/src/workspace.mjs +178 -25
@@ -34,6 +34,14 @@ export const RELEASE_MANIFEST_NAME='ARCANE_APP_RELEASE.json';
34
34
  export const PACKAGER_VERSION='arcane-app-packager-v1';
35
35
 
36
36
  const RUNTIME_AUTHORITIES_NAME='ARCANE_RUNTIME_AUTHORITIES.json';
37
+ const RUNTIME_PROJECTION_NAME='ARCANE_RUNTIME_PROJECTION.json';
38
+ const RUNTIME_PROJECTION_ERROR='ARCANE_RUNTIME_PROJECTION_INVALID';
39
+ const GENERATED_PACKAGE_ROOT_PATH_KEYS=new Set([
40
+ RELEASE_MANIFEST_NAME,
41
+ RUNTIME_AUTHORITIES_NAME,
42
+ RUNTIME_PROJECTION_NAME,
43
+ 'index.html'
44
+ ].map(pathKey));
37
45
 
38
46
  const RENAME_RETRY_CODES=new Set(['EACCES','EBUSY','EPERM']);
39
47
  const RENAME_RETRY_LIMIT=20;
@@ -513,7 +521,7 @@ function validateSharedRoute(route,label){
513
521
  }
514
522
 
515
523
  if(destination==='apps'||destination.startsWith('apps/')
516
- ||destination===RELEASE_MANIFEST_NAME){
524
+ ||GENERATED_PACKAGE_ROOT_PATH_KEYS.has(pathKey(destination))){
517
525
  fail(`${label}.destination overlaps a reserved package path: ${destination}`);
518
526
  }
519
527
 
@@ -1036,7 +1044,7 @@ function packagedRuntimeAuthorities(receipt){
1036
1044
  ||!Number.isSafeInteger(receipt.fileCount)||receipt.fileCount<1
1037
1045
  ||!Number.isSafeInteger(receipt.totalBytes)||receipt.totalBytes<1
1038
1046
  ||!SHA256_PATTERN.test(receipt.contentSha256??'')
1039
- ||!isPlainObject(arcane)||arcane.authority!=='arcane-os-upstream'
1047
+ ||!isPlainObject(arcane)||arcane.authority!=='arcane-os-sdk'
1040
1048
  ||!SHA256_PATTERN.test(arcane.manifestSha256??'')
1041
1049
  ||!SHA256_PATTERN.test(arcane.contentSha256??'')
1042
1050
  ||!isPlainObject(arcane.source)
@@ -1073,6 +1081,63 @@ function packagedRuntimeAuthorities(receipt){
1073
1081
  });
1074
1082
  }
1075
1083
 
1084
+ function packagedRuntimeProjection(receipt){
1085
+ const authorities=packagedRuntimeAuthorities(receipt);
1086
+ if(!Array.isArray(receipt.files)){
1087
+ fail('The composed workspace runtime receipt is missing its file inventory.',RUNTIME_PROJECTION_ERROR);
1088
+ }
1089
+ const files=[];
1090
+ let previous=null;
1091
+ let totalBytes=0;
1092
+ for(const [index,file] of receipt.files.entries()){
1093
+ if(!isPlainObject(file)){
1094
+ fail(`Workspace runtime projection files[${index}] is invalid.`,RUNTIME_PROJECTION_ERROR);
1095
+ }
1096
+ let relative;
1097
+ try{
1098
+ relative=normalizeRelativePath(file.path,`workspace runtime projection files[${index}].path`);
1099
+ }catch{
1100
+ fail(`Workspace runtime projection files[${index}].path is invalid.`,RUNTIME_PROJECTION_ERROR);
1101
+ }
1102
+ if(relative!==file.path
1103
+ ||!Number.isSafeInteger(file.bytes)||file.bytes<0
1104
+ ||!SHA256_PATTERN.test(file.sha256??'')
1105
+ ||previous!==null&&compareText(previous,relative)>=0){
1106
+ fail(`Workspace runtime projection files[${index}] is invalid.`,RUNTIME_PROJECTION_ERROR);
1107
+ }
1108
+ totalBytes+=file.bytes;
1109
+ if(!Number.isSafeInteger(totalBytes)){
1110
+ fail('Workspace runtime projection byte total is invalid.',RUNTIME_PROJECTION_ERROR);
1111
+ }
1112
+ previous=relative;
1113
+ files.push({path:relative,bytes:file.bytes,sha256:file.sha256});
1114
+ }
1115
+ const contentSha256=createHash('sha256')
1116
+ .update(JSON.stringify(files))
1117
+ .digest('hex');
1118
+ if(files.length!==receipt.fileCount
1119
+ ||totalBytes!==receipt.totalBytes
1120
+ ||contentSha256!==receipt.contentSha256
1121
+ ||files.length!==authorities.projection.fileCount
1122
+ ||totalBytes!==authorities.projection.totalBytes
1123
+ ||contentSha256!==authorities.projection.contentSha256){
1124
+ fail(
1125
+ 'The workspace runtime file inventory does not match its admitted projection authority.',
1126
+ RUNTIME_PROJECTION_ERROR
1127
+ );
1128
+ }
1129
+ return immutableJsonCopy({
1130
+ schemaVersion:1,
1131
+ kind:'arcane-app-runtime-projection',
1132
+ sdkVersion:receipt.sdkVersion,
1133
+ pathPrefix:'arcane/',
1134
+ fileCount:files.length,
1135
+ totalBytes,
1136
+ contentSha256,
1137
+ files
1138
+ });
1139
+ }
1140
+
1076
1141
  async function hasExternalRuntimeAdmission(context){
1077
1142
  const lockPath=path.join(context.workspaceRoot,'arcane.lock.json');
1078
1143
  try{
@@ -1117,15 +1182,30 @@ async function integratedWorkspaceCandidate(context){
1117
1182
  return document.value?.name==='arcane-os'&&document.value?.type==='module';
1118
1183
  }
1119
1184
 
1120
- function packageRuntimeLocations(context){
1121
- const installedRoot=path.join(context.workspaceRoot,'node_modules','arcane-os');
1185
+ function packageRuntimeLocations(context,validation){
1186
+ const installation=validation?.sdkInstallation;
1187
+ const licenseRoute=context.rootConfig.sharedPayloads['browser-runtime']?.find(
1188
+ route=>route.destination==='licenses/arcane-os'
1189
+ );
1190
+ if(validation?.workspaceMode!=='external'||!isPlainObject(installation)
1191
+ ||typeof installation.packageSource!=='string'
1192
+ ||typeof installation.canonicalPackageRoot!=='string'
1193
+ ||typeof installation.runtimeRoot!=='string'
1194
+ ||typeof installation.browserRuntimeRoot!=='string'
1195
+ ||licenseRoute?.source!==installation.packageSource
1196
+ ||path.resolve(installation.runtimeRoot)
1197
+ !==path.join(path.resolve(installation.canonicalPackageRoot),'runtime')
1198
+ ||path.resolve(installation.browserRuntimeRoot)
1199
+ !==path.join(path.resolve(installation.canonicalPackageRoot),'browser-runtime')){
1200
+ fail('External workspace validation did not return its bound SDK installation authority.');
1201
+ }
1122
1202
  return Object.freeze({
1123
- runtimeRoot:path.join(installedRoot,'runtime'),
1124
- browserRuntimeRoot:path.join(installedRoot,'browser-runtime')
1203
+ runtimeRoot:installation.runtimeRoot,
1204
+ browserRuntimeRoot:installation.browserRuntimeRoot
1125
1205
  });
1126
1206
  }
1127
1207
 
1128
- async function authenticatePackageRuntimeVerificationState(context,state,{signal}={}){
1208
+ async function authenticatePackageRuntimeVerificationState(context,state,{signal,validation}={}){
1129
1209
  assertOnlyKeys(
1130
1210
  state,
1131
1211
  new Set(['runtimeReceipt','sdkBrowserRuntimeReceipt','workspaceRuntimeReceipt']),
@@ -1146,7 +1226,7 @@ async function authenticatePackageRuntimeVerificationState(context,state,{signal
1146
1226
  ||!snapshot.workspaceRuntimeReceipt){
1147
1227
  fail('The runtime verification state must contain all three authenticated receipts.');
1148
1228
  }
1149
- const {runtimeRoot,browserRuntimeRoot}=packageRuntimeLocations(context);
1229
+ const {runtimeRoot,browserRuntimeRoot}=packageRuntimeLocations(context,validation);
1150
1230
  await authenticateRuntimeReceipt(snapshot.runtimeReceipt,{runtimeRoot,signal});
1151
1231
  await authenticateSdkBrowserRuntimeReceipt(snapshot.sdkBrowserRuntimeReceipt,{
1152
1232
  browserRuntimeRoot,
@@ -1158,7 +1238,7 @@ async function authenticatePackageRuntimeVerificationState(context,state,{signal
1158
1238
  });
1159
1239
  const workspaceReceipt=snapshot.workspaceRuntimeReceipt;
1160
1240
  const expectedArcaneSource={
1161
- authority:'arcane-os-upstream',
1241
+ authority:'arcane-os-sdk',
1162
1242
  location:snapshot.runtimeReceipt.canonicalLocation,
1163
1243
  manifestSha256:snapshot.runtimeReceipt.manifestSha256,
1164
1244
  contentSha256:snapshot.runtimeReceipt.contentSha256,
@@ -1189,8 +1269,8 @@ async function authenticatePackageRuntimeVerificationState(context,state,{signal
1189
1269
  return snapshot;
1190
1270
  }
1191
1271
 
1192
- async function issuePackageRuntimeVerificationState(context,{signal,onEvent}={}){
1193
- const {runtimeRoot,browserRuntimeRoot}=packageRuntimeLocations(context);
1272
+ async function issuePackageRuntimeVerificationState(context,{signal,onEvent,validation}={}){
1273
+ const {runtimeRoot,browserRuntimeRoot}=packageRuntimeLocations(context,validation);
1194
1274
  const [runtimeReceipt,sdkBrowserRuntimeReceipt]=await Promise.all([
1195
1275
  verifyRuntime({runtimeRoot,signal,onEvent}),
1196
1276
  verifySdkBrowserRuntime({browserRuntimeRoot,signal,onEvent})
@@ -1207,6 +1287,54 @@ async function issuePackageRuntimeVerificationState(context,{signal,onEvent}={})
1207
1287
  return Object.freeze({runtimeReceipt,sdkBrowserRuntimeReceipt,workspaceRuntimeReceipt});
1208
1288
  }
1209
1289
 
1290
+ function orderedBrowserDocumentPaths(entry,paths,label){
1291
+ const selected=new Map();
1292
+ for(const relative of paths){
1293
+ const normalized=normalizeRelativePath(relative,label);
1294
+ const key=pathKey(normalized);
1295
+ const prior=selected.get(key);
1296
+ if(prior!==undefined){
1297
+ fail(`Package destination collision: ${prior} and ${normalized}.`);
1298
+ }
1299
+ selected.set(key,normalized);
1300
+ }
1301
+ const selectedEntry=selected.get(pathKey(entry));
1302
+ if(selectedEntry!==entry){
1303
+ fail(`The configured entry file was not found in the package payload: ${entry}`);
1304
+ }
1305
+ return Object.freeze([
1306
+ entry,
1307
+ ...[...selected.values()]
1308
+ .filter(relative=>relative!==entry
1309
+ &&isHtmlDocument(relative))
1310
+ .sort(compareText)
1311
+ ]);
1312
+ }
1313
+
1314
+ function isHtmlDocument(relative){
1315
+ const extension=path.posix.extname(relative).toLowerCase();
1316
+ return extension==='.html'||extension==='.htm';
1317
+ }
1318
+
1319
+ async function packageImportMapDocuments(context,{signal}={}){
1320
+ const {workspaceRoot,appRoot,config}=context;
1321
+ const files=await enumerateRoute({
1322
+ workspaceRoot,
1323
+ sourceRoot:appRoot,
1324
+ destinationRoot:`apps/${config.id}`,
1325
+ include:config.include,
1326
+ exclude:config.exclude,
1327
+ label:`apps.${config.id}`,
1328
+ appPayload:true,
1329
+ signal
1330
+ });
1331
+ return orderedBrowserDocumentPaths(
1332
+ config.entry,
1333
+ files.map(file=>file.sourceRelative),
1334
+ `apps.${config.id} browser document`
1335
+ );
1336
+ }
1337
+
1210
1338
  async function refreshPackageImportMap(context,{
1211
1339
  runtimeVerificationState,
1212
1340
  workspaceOperationLease,
@@ -1243,19 +1371,21 @@ async function refreshPackageImportMap(context,{
1243
1371
  let authenticatedRuntimeState=null;
1244
1372
  if(validation.workspaceMode==='external'){
1245
1373
  authenticatedRuntimeState=runtimeVerificationState===undefined
1246
- ?await issuePackageRuntimeVerificationState(context,{signal,onEvent})
1374
+ ?await issuePackageRuntimeVerificationState(context,{signal,onEvent,validation})
1247
1375
  :await authenticatePackageRuntimeVerificationState(
1248
1376
  context,
1249
1377
  runtimeVerificationState,
1250
- {signal}
1378
+ {signal,validation}
1251
1379
  );
1252
1380
  workspaceRuntimeReceipt=authenticatedRuntimeState.workspaceRuntimeReceipt;
1253
1381
  }
1382
+ const documents=await packageImportMapDocuments(context,{signal});
1254
1383
  const importMapReceipt=await generateImportMap({
1255
1384
  workspaceRoot:context.workspaceRoot,
1256
1385
  appId:context.config.id,
1257
1386
  appRoot:context.appRoot,
1258
1387
  entry:context.config.entry,
1388
+ documents,
1259
1389
  workspaceRuntimeReceipt,
1260
1390
  workspaceOperationLease,
1261
1391
  signal,
@@ -1283,27 +1413,74 @@ function authenticatedImportMapReceipt(receipt){
1283
1413
 
1284
1414
  function importMapReceiptFiles(context,receipt){
1285
1415
  if(receipt==null||receipt.skipped===true)return Object.freeze([]);
1286
- if(!Array.isArray(receipt.files)||receipt.files.length!==2){
1287
- fail('The generated import-map receipt does not bind its committed artifact and entry files.');
1416
+ if(!Array.isArray(receipt.files)||receipt.files.length<2
1417
+ ||!Number.isSafeInteger(receipt.documentCount)||receipt.documentCount<1
1418
+ ||!Array.isArray(receipt.documentPaths)
1419
+ ||receipt.documentPaths.length!==receipt.documentCount
1420
+ ||receipt.files.length!==receipt.documentCount+1){
1421
+ fail('The generated import-map receipt does not bind its committed artifact and browser documents.');
1288
1422
  }
1289
- const expected=Object.freeze([
1290
- Object.freeze({
1291
- role:'artifact',
1292
- path:`apps/${context.config.id}/modules/arcane.importmap.json`
1293
- }),
1294
- Object.freeze({
1295
- role:'entry',
1296
- path:`apps/${context.config.id}/${context.config.entry}`
1297
- })
1298
- ]);
1299
1423
  const records=[];
1300
- for(const [index,record] of receipt.files.entries()){
1424
+ const artifact=receipt.files[0];
1425
+ assertOnlyKeys(
1426
+ artifact,
1427
+ new Set(['role','path','bytes','sha256']),
1428
+ 'import-map receipt files[0]'
1429
+ );
1430
+ const artifactPath=`apps/${context.config.id}/modules/arcane.importmap.json`;
1431
+ if(artifact.role!=='artifact'
1432
+ ||normalizeRelativePath(artifact.path,'import-map receipt files[0].path')!==artifactPath
1433
+ ||!Number.isSafeInteger(artifact.bytes)||artifact.bytes<1
1434
+ ||!SHA256_PATTERN.test(artifact.sha256??'')){
1435
+ fail('The generated import-map receipt artifact record is invalid.');
1436
+ }
1437
+ records.push(Object.freeze({...artifact}));
1438
+
1439
+ const seen=new Set();
1440
+ let previousDocument=null;
1441
+ for(const [documentIndex,documentPath] of receipt.documentPaths.entries()){
1442
+ const index=documentIndex+1;
1443
+ if(typeof documentPath!=='string'||!path.isAbsolute(documentPath)){
1444
+ fail(
1445
+ `The generated import-map receipt documentPaths[${documentIndex}] is invalid.`
1446
+ );
1447
+ }
1448
+ const resolved=path.resolve(documentPath);
1449
+ if(!isInside(context.appRoot,resolved)){
1450
+ fail(
1451
+ `The generated import-map receipt documentPaths[${documentIndex}] leaves its `
1452
+ +'application root.'
1453
+ );
1454
+ }
1455
+ const relative=normalizeRelativePath(
1456
+ path.relative(context.appRoot,resolved).replaceAll('\\','/'),
1457
+ `import-map receipt documentPaths[${documentIndex}]`
1458
+ );
1459
+ const key=pathKey(relative);
1460
+ if(seen.has(key)){
1461
+ fail(`The generated import-map receipt repeats a browser document: ${relative}.`);
1462
+ }
1463
+ seen.add(key);
1464
+ if(documentIndex===0&&relative!==context.config.entry){
1465
+ fail('The generated import-map receipt does not preserve its configured entry document.');
1466
+ }
1467
+ if(documentIndex>0){
1468
+ if(!isHtmlDocument(relative)
1469
+ ||previousDocument!==null&&compareText(previousDocument,relative)>=0){
1470
+ fail(`The generated import-map receipt browser document order is invalid: ${relative}.`);
1471
+ }
1472
+ previousDocument=relative;
1473
+ }
1474
+ const record=receipt.files[index];
1301
1475
  assertOnlyKeys(
1302
1476
  record,
1303
1477
  new Set(['role','path','bytes','sha256']),
1304
1478
  `import-map receipt files[${index}]`
1305
1479
  );
1306
- const wanted=expected[index];
1480
+ const wanted={
1481
+ role:documentIndex===0?'entry':'document',
1482
+ path:`apps/${context.config.id}/${relative}`
1483
+ };
1307
1484
  if(record.role!==wanted.role
1308
1485
  ||normalizeRelativePath(record.path,`import-map receipt files[${index}].path`)
1309
1486
  !==wanted.path
@@ -1316,7 +1493,7 @@ function importMapReceiptFiles(context,receipt){
1316
1493
  return Object.freeze(records);
1317
1494
  }
1318
1495
 
1319
- async function authenticateImportMapPair(context,receipt,{signal}={}){
1496
+ async function authenticateImportMapFiles(context,receipt,{signal}={}){
1320
1497
  const records=importMapReceiptFiles(context,receipt);
1321
1498
  for(const record of records){
1322
1499
  const filePath=resolveInside(
@@ -1342,7 +1519,21 @@ async function authenticateImportMapPair(context,receipt,{signal}={}){
1342
1519
  return records;
1343
1520
  }
1344
1521
 
1345
- async function authenticateCollectedImportMapPair(files,records,{signal}={}){
1522
+ async function authenticateCollectedImportMapFiles(context,files,records,{signal}={}){
1523
+ if(records.length>0){
1524
+ const appPrefix=`apps/${context.config.id}/`;
1525
+ const expectedDocuments=orderedBrowserDocumentPaths(
1526
+ context.config.entry,
1527
+ files
1528
+ .filter(file=>file.destination.startsWith(appPrefix))
1529
+ .map(file=>file.destination.slice(appPrefix.length)),
1530
+ `apps.${context.config.id} collected browser document`
1531
+ ).map(relative=>`${appPrefix}${relative}`);
1532
+ const committedDocuments=records.slice(1).map(record=>record.path);
1533
+ if(!isDeepStrictEqual(committedDocuments,expectedDocuments)){
1534
+ fail('The generated import-map receipt does not bind every packaged browser document.');
1535
+ }
1536
+ }
1346
1537
  for(const record of records){
1347
1538
  const collected=files.find(file=>file.destination===record.path);
1348
1539
  if(!collected||collected.bytes!==record.bytes){
@@ -1359,7 +1550,7 @@ async function authenticateCollectedImportMapPair(files,records,{signal}={}){
1359
1550
  }
1360
1551
  }
1361
1552
 
1362
- function authenticatePackagedImportMapPair(release,records){
1553
+ function authenticatePackagedImportMapFiles(release,records){
1363
1554
  for(const record of records){
1364
1555
  const packaged=release.files.find(file=>file.path===record.path);
1365
1556
  if(!packaged||packaged.bytes!==record.bytes||packaged.sha256!==record.sha256){
@@ -1380,7 +1571,7 @@ async function prepareRuntimeAuthorityState(context,{
1380
1571
  }
1381
1572
  return null;
1382
1573
  }
1383
- await validateExternalRuntimeAdmission(context,{signal,onEvent});
1574
+ const workspaceValidation=await validateExternalRuntimeAdmission(context,{signal,onEvent});
1384
1575
 
1385
1576
  let verificationState=null;
1386
1577
  let receipt=null;
@@ -1388,7 +1579,7 @@ async function prepareRuntimeAuthorityState(context,{
1388
1579
  verificationState=await authenticatePackageRuntimeVerificationState(
1389
1580
  context,
1390
1581
  runtimeVerificationState,
1391
- {signal}
1582
+ {signal,validation:workspaceValidation}
1392
1583
  );
1393
1584
  receipt=verificationState.workspaceRuntimeReceipt;
1394
1585
  }else if(validation?.kind==='arcane-workspace-runtime-verification'){
@@ -1398,12 +1589,17 @@ async function prepareRuntimeAuthorityState(context,{
1398
1589
  signal
1399
1590
  });
1400
1591
  }else{
1401
- verificationState=await issuePackageRuntimeVerificationState(context,{signal,onEvent});
1592
+ verificationState=await issuePackageRuntimeVerificationState(context,{
1593
+ signal,
1594
+ onEvent,
1595
+ validation:workspaceValidation
1596
+ });
1402
1597
  receipt=verificationState.workspaceRuntimeReceipt;
1403
1598
  }
1404
1599
  return Object.freeze({
1405
1600
  receipt,
1406
1601
  document:packagedRuntimeAuthorities(receipt),
1602
+ projectionDocument:packagedRuntimeProjection(receipt),
1407
1603
  verificationState
1408
1604
  });
1409
1605
  }
@@ -1418,12 +1614,12 @@ async function authenticateRuntimeAuthorityState(context,state,{signal,onEvent}=
1418
1614
  if(!await hasExternalRuntimeAdmission(context)){
1419
1615
  fail('External runtime authority admission disappeared during package verification.');
1420
1616
  }
1421
- await validateExternalRuntimeAdmission(context,{signal,onEvent});
1617
+ const workspaceValidation=await validateExternalRuntimeAdmission(context,{signal,onEvent});
1422
1618
  if(state.verificationState){
1423
1619
  await authenticatePackageRuntimeVerificationState(
1424
1620
  context,
1425
1621
  state.verificationState,
1426
- {signal}
1622
+ {signal,validation:workspaceValidation}
1427
1623
  );
1428
1624
  }else{
1429
1625
  await authenticateWorkspaceRuntimeReceipt(state.receipt,{
@@ -1434,6 +1630,12 @@ async function authenticateRuntimeAuthorityState(context,state,{signal,onEvent}=
1434
1630
  if(!isDeepStrictEqual(packagedRuntimeAuthorities(state.receipt),state.document)){
1435
1631
  fail('External runtime source authorities changed during package verification.');
1436
1632
  }
1633
+ if(!isDeepStrictEqual(packagedRuntimeProjection(state.receipt),state.projectionDocument)){
1634
+ fail(
1635
+ 'External runtime projection inventory changed during package verification.',
1636
+ RUNTIME_PROJECTION_ERROR
1637
+ );
1638
+ }
1437
1639
  }
1438
1640
 
1439
1641
  async function getAppContext({
@@ -1873,7 +2075,7 @@ async function collectPackageFiles(context,{signal,sharedPayloadState}={}){
1873
2075
 
1874
2076
  for(const file of files){
1875
2077
  throwIfAborted(signal);
1876
- if(file.destination===RELEASE_MANIFEST_NAME||file.destination==='index.html'){
2078
+ if(GENERATED_PACKAGE_ROOT_PATH_KEYS.has(pathKey(file.destination))){
1877
2079
  fail(`${file.label} collides with generated package path: ${file.destination}`);
1878
2080
  }
1879
2081
 
@@ -2424,6 +2626,18 @@ async function writeRuntimeAuthorities(root,state){
2424
2626
  await authority.handle.close();
2425
2627
  }
2426
2628
 
2629
+ async function writeRuntimeProjection(root,state){
2630
+ if(state==null)return;
2631
+ const projectionPath=path.join(root,RUNTIME_PROJECTION_NAME);
2632
+ await writeFile(
2633
+ projectionPath,
2634
+ `${JSON.stringify(state.projectionDocument,null,2)}\n`,
2635
+ {encoding:'utf8',flag:'wx'}
2636
+ );
2637
+ const projection=await openStableRegularFile(projectionPath,RUNTIME_PROJECTION_NAME);
2638
+ await projection.handle.close();
2639
+ }
2640
+
2427
2641
  async function verifyRuntimeAuthorities(root,state){
2428
2642
  const authorityPath=path.join(root,RUNTIME_AUTHORITIES_NAME);
2429
2643
  if(state==null){
@@ -2441,6 +2655,57 @@ async function verifyRuntimeAuthorities(root,state){
2441
2655
  }
2442
2656
  }
2443
2657
 
2658
+ async function verifyRuntimeProjection(root,state,release){
2659
+ const projectionPath=path.join(root,RUNTIME_PROJECTION_NAME);
2660
+ const releaseRecords=release.files.filter(file=>file.path===RUNTIME_PROJECTION_NAME);
2661
+ if(state==null){
2662
+ if(releaseRecords.length!==0){
2663
+ fail(
2664
+ `${RUNTIME_PROJECTION_NAME} is not allowed without an external runtime authority.`,
2665
+ RUNTIME_PROJECTION_ERROR
2666
+ );
2667
+ }
2668
+ try{
2669
+ await lstat(projectionPath);
2670
+ fail(
2671
+ `${RUNTIME_PROJECTION_NAME} is not allowed without an external runtime authority.`,
2672
+ RUNTIME_PROJECTION_ERROR
2673
+ );
2674
+ }catch(error){
2675
+ if(error?.code!=='ENOENT')throw error;
2676
+ }
2677
+ return;
2678
+ }
2679
+
2680
+ let document;
2681
+ try{
2682
+ document=await readJsonDocument(projectionPath,RUNTIME_PROJECTION_NAME);
2683
+ }catch(error){
2684
+ fail(
2685
+ `${RUNTIME_PROJECTION_NAME} could not be authenticated: ${error.message}`,
2686
+ RUNTIME_PROJECTION_ERROR
2687
+ );
2688
+ }
2689
+ const expectedBytes=Buffer.from(`${JSON.stringify(state.projectionDocument,null,2)}\n`,'utf8');
2690
+ if(!document.bytes.equals(expectedBytes)
2691
+ ||!isDeepStrictEqual(document.value,state.projectionDocument)){
2692
+ fail(
2693
+ `${RUNTIME_PROJECTION_NAME} does not match the admitted workspace runtime inventory.`,
2694
+ RUNTIME_PROJECTION_ERROR
2695
+ );
2696
+ }
2697
+ const expectedSha256=createHash('sha256').update(expectedBytes).digest('hex');
2698
+ const record=releaseRecords[0];
2699
+ if(releaseRecords.length!==1
2700
+ ||record.bytes!==expectedBytes.length
2701
+ ||record.sha256!==expectedSha256){
2702
+ fail(
2703
+ `${RUNTIME_PROJECTION_NAME} is not authenticated by the packaged release inventory.`,
2704
+ RUNTIME_PROJECTION_ERROR
2705
+ );
2706
+ }
2707
+ }
2708
+
2444
2709
  function verifyRuntimeProjectionAuthority(release,state){
2445
2710
  if(state==null)return;
2446
2711
  const projection=release.files
@@ -2509,6 +2774,7 @@ async function verifyFreshStaticRelease(root,context,version,releaseState,{
2509
2774
  }
2510
2775
 
2511
2776
  await verifyRuntimeAuthorities(root,runtimeAuthorityState);
2777
+ await verifyRuntimeProjection(root,runtimeAuthorityState,release);
2512
2778
  verifyRuntimeProjectionAuthority(release,runtimeAuthorityState);
2513
2779
 
2514
2780
  return releaseState;
@@ -2558,6 +2824,7 @@ async function verifyGenericRelease(root,context,version,{
2558
2824
  }
2559
2825
 
2560
2826
  await verifyRuntimeAuthorities(root,runtimeAuthorityState);
2827
+ await verifyRuntimeProjection(root,runtimeAuthorityState,release);
2561
2828
  verifyRuntimeProjectionAuthority(release,runtimeAuthorityState);
2562
2829
 
2563
2830
  return {
@@ -2888,13 +3155,13 @@ async function packageAppUnlocked({
2888
3155
  const version=resolveTargetVersion(currentVersion,{bump,exactVersion,preid});
2889
3156
  const importMapFiles=dryRun
2890
3157
  ?Object.freeze([])
2891
- :await authenticateImportMapPair(context,importMapReceipt,{signal});
3158
+ :await authenticateImportMapFiles(context,importMapReceipt,{signal});
2892
3159
  const files=await collectPackageFiles(context,{
2893
3160
  signal,
2894
3161
  sharedPayloadState:authenticatedSharedPayloadState
2895
3162
  });
2896
3163
  if(!dryRun){
2897
- await authenticateCollectedImportMapPair(files,importMapFiles,{signal});
3164
+ await authenticateCollectedImportMapFiles(context,files,importMapFiles,{signal});
2898
3165
  }
2899
3166
  const preview={
2900
3167
  app:appId,
@@ -2984,6 +3251,7 @@ async function packageAppUnlocked({
2984
3251
  onEvent
2985
3252
  });
2986
3253
  await writeRuntimeAuthorities(staging,runtimeAuthorityState);
3254
+ await writeRuntimeProjection(staging,runtimeAuthorityState);
2987
3255
  const releaseState=await writeReleaseManifest(staging,context,version,{signal,onEvent});
2988
3256
  const verifiedRelease=adapter
2989
3257
  ?await verifyBuiltPackage(context,staging,version,adapter,{
@@ -3013,8 +3281,8 @@ async function packageAppUnlocked({
3013
3281
  signal
3014
3282
  });
3015
3283
  }
3016
- await authenticateImportMapPair(context,importMapReceipt,{signal});
3017
- authenticatePackagedImportMapPair(verifiedRelease.release,importMapFiles);
3284
+ await authenticateImportMapFiles(context,importMapReceipt,{signal});
3285
+ authenticatePackagedImportMapFiles(verifiedRelease.release,importMapFiles);
3018
3286
  await assertArtifactState(staging,verifiedRelease.identities,{signal});
3019
3287
  throwIfAborted(signal);
3020
3288
 
@@ -3117,10 +3385,14 @@ export async function packageApp(options){
3117
3385
  if(!await hasExternalRuntimeAdmission(context)){
3118
3386
  fail('A runtime verification state cannot be supplied to an integrated workspace.');
3119
3387
  }
3388
+ const validation=await validateExternalRuntimeAdmission(context,{
3389
+ signal:options?.signal,
3390
+ onEvent:options?.onEvent
3391
+ });
3120
3392
  await authenticatePackageRuntimeVerificationState(
3121
3393
  context,
3122
3394
  options.runtimeVerificationState,
3123
- {signal:options?.signal}
3395
+ {signal:options?.signal,validation}
3124
3396
  );
3125
3397
  }
3126
3398
  return packageAppUnlocked({...options,context,authenticatedSharedPayloadState});
package/src/runtime.mjs CHANGED
@@ -7,6 +7,8 @@ import {SDK_VERSION} from './constants.mjs';
7
7
 
8
8
  export const RUNTIME_MANIFEST_NAME='ARCANE_RUNTIME_RELEASE.json';
9
9
  const SHA256_PATTERN=/^[a-f0-9]{64}$/;
10
+ const SDK_RUNTIME_REPOSITORY='https://github.com/TheWizardNexus/arcane-os-sdk.git';
11
+ const LEGACY_ARCANE_REPOSITORY='https://github.com/TheWizardNexus/ARCANE-OS.git';
10
12
  const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
11
13
  const MAX_VERIFIED_RUNTIME_FILE_BYTES=64*1024*1024;
12
14
  const sdkRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
@@ -59,10 +61,18 @@ function validateRelease(value){
59
61
  }
60
62
  if(value.sdkVersion!==SDK_VERSION)fail('Runtime manifest sdkVersion is incompatible with this SDK.');
61
63
  if(!value.source||typeof value.source!=='object'
62
- ||Object.keys(value.source).sort(compareText).join('\0')!=='bundleVersion\0commit\0protocol\0repository'
63
- ||value.source.repository!=='https://github.com/TheWizardNexus/ARCANE-OS.git'
64
- ||typeof value.source.commit!=='string'||!/^[a-f0-9]{40}$/.test(value.source.commit)
65
- ||value.source.bundleVersion!=='0.8.12'||value.source.protocol!=='arcane/1'){
64
+ ||Object.keys(value.source).sort(compareText).join('\0')!=='authority\0legacyProjection\0path\0protocol\0repository'
65
+ ||value.source.authority!=='sdk-canonical'
66
+ ||value.source.repository!==SDK_RUNTIME_REPOSITORY
67
+ ||value.source.path!=='runtime/arcane'
68
+ ||value.source.protocol!=='arcane/1'
69
+ ||!value.source.legacyProjection
70
+ ||typeof value.source.legacyProjection!=='object'
71
+ ||Object.keys(value.source.legacyProjection).sort(compareText).join('\0')!=='bundleVersion\0commit\0repository'
72
+ ||value.source.legacyProjection.repository!==LEGACY_ARCANE_REPOSITORY
73
+ ||typeof value.source.legacyProjection.commit!=='string'
74
+ ||!/^[a-f0-9]{40}$/.test(value.source.legacyProjection.commit)
75
+ ||value.source.legacyProjection.bundleVersion!=='0.8.12'){
66
76
  fail('Runtime manifest source identity is invalid.');
67
77
  }
68
78
  if(!Array.isArray(value.files)||!Number.isSafeInteger(value.fileCount)