arcane-os 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
  4. package/browser-runtime/ai/browser-speech-providers.mjs +331 -25
  5. package/docs/architecture.md +2 -2
  6. package/docs/reference/README.md +79 -13
  7. package/docs/reference/ai/browser-speech.md +336 -0
  8. package/docs/reference/ai/browser-wasm.md +207 -82
  9. package/docs/reference/availability-and-normalization.md +30 -4
  10. package/docs/reference/behavioral-testing.md +4 -1
  11. package/docs/reference/cli.md +29 -10
  12. package/docs/reference/core/arcane-ai-contracts.md +43 -9
  13. package/docs/reference/inventory/package-api.json +110 -14
  14. package/docs/reference/inventory/runtime-components.json +19 -6
  15. package/docs/reference/inventory/runtime-modules.json +113 -9
  16. package/docs/reference/protocols.md +260 -38
  17. package/docs/reference/runtime-components.md +108 -15
  18. package/docs/reference/runtime-modules.md +422 -8
  19. package/docs/reference/sdk-api.md +626 -85
  20. package/package.json +1 -1
  21. package/runtime/ARCANE_RUNTIME_RELEASE.json +19 -19
  22. package/runtime/arcane/components/chat.html +288 -52
  23. package/runtime/arcane/components/speech.html +339 -15
  24. package/runtime/arcane/modules/AI.js +1245 -136
  25. package/runtime/arcane/modules/AIProviderRuntime.js +299 -30
  26. package/runtime/arcane/modules/AIRuntimeState.js +23 -4
  27. package/runtime/arcane/modules/ConfiguredAIChatSession.js +93 -8
  28. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +448 -24
  29. package/runtime/arcane/modules/LocalAIReadinessController.js +1 -1
  30. package/schemas/arcane-lock.schema.json +6 -4
  31. package/src/dev-server.mjs +29 -13
  32. package/src/doctor.mjs +1 -3
  33. package/src/import-map.mjs +134 -83
  34. package/src/packager/core.mjs +311 -39
  35. package/src/scaffold.mjs +45 -17
  36. package/src/templates/workspace-template.mjs +23 -4
  37. package/src/toolchain.mjs +10 -2
  38. package/src/workspace.mjs +177 -24
@@ -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
 
@@ -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,
@@ -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/scaffold.mjs CHANGED
@@ -7,12 +7,11 @@ import {materializeWorkspaceRuntime} from './workspace-runtime.mjs';
7
7
  import {generateImportMap} from './import-map.mjs';
8
8
  import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
9
9
  import {SDK_NAME,SDK_VERSION,workspaceTemplate} from './templates/workspace-template.mjs';
10
- import {inspectWorkspaceProfile} from './workspace.mjs';
10
+ import {inspectWorkspaceProfile,resolveSdkPackageDeclaration} from './workspace.mjs';
11
11
  import {parseSemver} from './packager/core.mjs';
12
12
 
13
13
  const APP_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
14
14
  const DISPLAY_CONTROL_PATTERN=/[\x00-\x1f\x7f]/;
15
- const LOCAL_TARBALL_PATTERN=/^file:.+\.tgz$/iu;
16
15
 
17
16
  function fail(message,code='ARCANE_WORKSPACE_INVALID'){
18
17
  const error=new Error(message);
@@ -152,31 +151,35 @@ async function assertNoLinkedAncestors(workspaceRoot,relative){
152
151
  }
153
152
  }
154
153
 
155
- function isSupportedSdkDeclaration(value){
156
- return value===SDK_VERSION||(typeof value==='string'&&LOCAL_TARBALL_PATTERN.test(value)
157
- &&!DISPLAY_CONTROL_PATTERN.test(value));
158
- }
159
-
160
- async function prepareExistingPackage(workspaceRoot,files){
154
+ async function readExistingPackage(workspaceRoot){
161
155
  const packagePath=path.join(workspaceRoot,'package.json');
162
156
  let source;
163
157
  try{source=await readFile(packagePath,'utf8');}
164
158
  catch(error){
165
- if(error?.code==='ENOENT')return Object.freeze({exists:false,updated:false});
159
+ if(error?.code==='ENOENT'){
160
+ return Object.freeze({exists:false,packagePath,source:null,document:null});
161
+ }
166
162
  throw error;
167
163
  }
168
164
  let existing;
169
165
  try{existing=JSON.parse(source);}
170
166
  catch(error){fail(`Existing package.json is not valid JSON: ${error.message}.`);}
171
167
  if(!existing||typeof existing!=='object'||Array.isArray(existing))fail('Existing package.json must be a JSON object.');
168
+ return Object.freeze({exists:true,packagePath,source,document:existing});
169
+ }
170
+
171
+ function defaultSdkDeclaration(){
172
+ return resolveSdkPackageDeclaration({devDependencies:{[SDK_NAME]:SDK_VERSION}});
173
+ }
174
+
175
+ async function prepareExistingPackage(workspaceRoot,files,existingPackage,sdkDeclaration){
176
+ const packageState=existingPackage??await readExistingPackage(workspaceRoot);
177
+ if(!packageState.exists)return Object.freeze({exists:false,updated:false});
178
+ const {packagePath,source,document:existing}=packageState;
172
179
  const generated=JSON.parse(files.get('package.json'));
173
180
  const conflicts=[];
174
181
  if(existing.private!==undefined&&existing.private!==true)conflicts.push('private must be true');
175
182
  if(existing.type!==undefined&&existing.type!=='module')conflicts.push('type must be "module"');
176
- const declared=existing.devDependencies?.[SDK_NAME]??existing.dependencies?.[SDK_NAME];
177
- if(declared!==undefined&&!isSupportedSdkDeclaration(declared)){
178
- conflicts.push(`${SDK_NAME} must be ${SDK_VERSION} or a local file: tarball`);
179
- }
180
183
  const appSelectionScripts=new Set(['build','run']);
181
184
  for(const [name,command] of Object.entries(generated.scripts)){
182
185
  if(appSelectionScripts.has(name))continue;
@@ -192,12 +195,15 @@ async function prepareExistingPackage(workspaceRoot,files){
192
195
  private:true,
193
196
  type:'module',
194
197
  scripts:{...generated.scripts,...(existing.scripts||{})},
195
- devDependencies:{...(existing.devDependencies||{}),[SDK_NAME]:declared??SDK_VERSION},
198
+ devDependencies:{
199
+ ...(existing.devDependencies||{}),
200
+ [sdkDeclaration.dependencyName]:sdkDeclaration.specifier
201
+ },
196
202
  engines:{...generated.engines,...(existing.engines||{})}
197
203
  };
198
- if(existing.dependencies?.[SDK_NAME]!==undefined){
204
+ if(existing.dependencies?.[sdkDeclaration.dependencyName]!==undefined){
199
205
  merged.dependencies={...existing.dependencies};
200
- delete merged.dependencies[SDK_NAME];
206
+ delete merged.dependencies[sdkDeclaration.dependencyName];
201
207
  if(Object.keys(merged.dependencies).length===0)delete merged.dependencies;
202
208
  }
203
209
  if(JSON.stringify(existing)===JSON.stringify(merged)){
@@ -422,6 +428,20 @@ export async function initWorkspace({
422
428
  const sdkBrowserRuntimeReceipt=workspaceMode==='external'
423
429
  ?await verifySdkBrowserRuntime({signal,onEvent})
424
430
  :null;
431
+ const existingPackage=workspaceMode==='external'
432
+ ?await readExistingPackage(resolvedRoot)
433
+ :null;
434
+ const sdkDeclaration=workspaceMode==='external'
435
+ ?profile?.config.sdkPackageSource!==undefined
436
+ ?resolveSdkPackageDeclaration(existingPackage.document??{}, {
437
+ allowMissing:true,
438
+ packageSource:profile.config.sdkPackageSource
439
+ })
440
+ :(existingPackage.exists
441
+ ?resolveSdkPackageDeclaration(existingPackage.document,{allowMissing:true})
442
+ :null)
443
+ ??defaultSdkDeclaration()
444
+ :null;
425
445
  const template=workspaceMode==='integrated'
426
446
  ?workspaceTemplate({
427
447
  appId,
@@ -437,12 +457,20 @@ export async function initWorkspace({
437
457
  displayName,
438
458
  runtimeRelease:runtimeReceipt,
439
459
  sdkBrowserRuntimeRelease:sdkBrowserRuntimeReceipt,
460
+ sdkDependencyName:sdkDeclaration.dependencyName,
461
+ sdkDependencySpecifier:sdkDeclaration.specifier,
462
+ sdkPackageSource:sdkDeclaration.packageSource,
440
463
  target,
441
464
  appIcon:await scaffoldIcon(target)
442
465
  });
443
466
  const packagePlan=workspaceMode==='integrated'
444
467
  ?Object.freeze({exists:true,updated:false})
445
- :await prepareExistingPackage(resolvedRoot,template.files);
468
+ :await prepareExistingPackage(
469
+ resolvedRoot,
470
+ template.files,
471
+ existingPackage,
472
+ sdkDeclaration
473
+ );
446
474
  if(workspaceMode==='external'){
447
475
  await assertExistingLockAdmission(resolvedRoot,template.files);
448
476
  }