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.
- package/CHANGELOG.md +35 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
- package/browser-runtime/ai/browser-speech-providers.mjs +331 -25
- package/docs/architecture.md +2 -2
- package/docs/reference/README.md +79 -13
- package/docs/reference/ai/browser-speech.md +336 -0
- package/docs/reference/ai/browser-wasm.md +207 -82
- package/docs/reference/availability-and-normalization.md +30 -4
- package/docs/reference/behavioral-testing.md +4 -1
- package/docs/reference/cli.md +29 -10
- package/docs/reference/core/arcane-ai-contracts.md +43 -9
- package/docs/reference/inventory/package-api.json +110 -14
- package/docs/reference/inventory/runtime-components.json +19 -6
- package/docs/reference/inventory/runtime-modules.json +113 -9
- package/docs/reference/protocols.md +260 -38
- package/docs/reference/runtime-components.md +108 -15
- package/docs/reference/runtime-modules.md +422 -8
- package/docs/reference/sdk-api.md +626 -85
- package/package.json +1 -1
- package/runtime/ARCANE_RUNTIME_RELEASE.json +19 -19
- package/runtime/arcane/components/chat.html +288 -52
- package/runtime/arcane/components/speech.html +339 -15
- package/runtime/arcane/modules/AI.js +1245 -136
- package/runtime/arcane/modules/AIProviderRuntime.js +299 -30
- package/runtime/arcane/modules/AIRuntimeState.js +23 -4
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +93 -8
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +448 -24
- package/runtime/arcane/modules/LocalAIReadinessController.js +1 -1
- package/schemas/arcane-lock.schema.json +6 -4
- package/src/dev-server.mjs +29 -13
- package/src/doctor.mjs +1 -3
- package/src/import-map.mjs +134 -83
- package/src/packager/core.mjs +311 -39
- package/src/scaffold.mjs +45 -17
- package/src/templates/workspace-template.mjs +23 -4
- package/src/toolchain.mjs +10 -2
- package/src/workspace.mjs +177 -24
package/src/packager/core.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
1124
|
-
browserRuntimeRoot:
|
|
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
|
|
1287
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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,{
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
3017
|
-
|
|
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
|
|
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')
|
|
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:{
|
|
198
|
+
devDependencies:{
|
|
199
|
+
...(existing.devDependencies||{}),
|
|
200
|
+
[sdkDeclaration.dependencyName]:sdkDeclaration.specifier
|
|
201
|
+
},
|
|
196
202
|
engines:{...generated.engines,...(existing.engines||{})}
|
|
197
203
|
};
|
|
198
|
-
if(existing.dependencies?.[
|
|
204
|
+
if(existing.dependencies?.[sdkDeclaration.dependencyName]!==undefined){
|
|
199
205
|
merged.dependencies={...existing.dependencies};
|
|
200
|
-
delete merged.dependencies[
|
|
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(
|
|
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
|
}
|