@volter/world-runtime 2.0.33 → 2.0.35

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.
@@ -1585,8 +1585,16 @@ export async function upWorld(configId, options = {}, abort) {
1585
1585
  });
1586
1586
  }
1587
1587
  catch (error) {
1588
- if (resourcesClaimed)
1589
- releaseWorldLifecycle(root, name);
1588
+ // the failure that stopped the boot leads: a release that also fails is added to it, never put in its place
1589
+ if (resourcesClaimed) {
1590
+ try {
1591
+ releaseWorldLifecycle(root, name);
1592
+ }
1593
+ catch (release) {
1594
+ if (error instanceof Error)
1595
+ error.message += `; releasing the World's claim failed too: ${release instanceof Error ? release.message : String(release)}`;
1596
+ }
1597
+ }
1590
1598
  throw error;
1591
1599
  }
1592
1600
  // From here on this call owns the claim; the finally below releases it however the boot
@@ -1785,7 +1793,7 @@ export async function upWorld(configId, options = {}, abort) {
1785
1793
  if (surviving.length)
1786
1794
  rollbackErrors.push('owned processes remain alive or uncertain');
1787
1795
  if (error instanceof CommandRetirementError)
1788
- rollbackErrors.push(error.message);
1796
+ rollbackErrors.push(error.retirement ?? error.message);
1789
1797
  if (rollbackErrors.length > 0) {
1790
1798
  cleanupUncertain = true;
1791
1799
  try {
@@ -3475,7 +3483,10 @@ function startProxyDaemonOwned(name, root, caCertPath, envFile, boot, caller = p
3475
3483
  }
3476
3484
  }
3477
3485
  catch (error) {
3478
- rmSync(marker, { force: true }); // partial pre-spawn writes own no child
3486
+ try {
3487
+ rmSync(marker, { force: true });
3488
+ }
3489
+ catch { /* the write's failure is the one to report */ } // partial pre-spawn writes own no child
3479
3490
  throw error;
3480
3491
  }
3481
3492
  let child;
@@ -3494,7 +3505,10 @@ function startProxyDaemonOwned(name, root, caCertPath, envFile, boot, caller = p
3494
3505
  }
3495
3506
  }
3496
3507
  catch (error) {
3497
- rmSync(marker, { force: true }); // no child was returned by spawn
3508
+ try {
3509
+ rmSync(marker, { force: true });
3510
+ }
3511
+ catch { /* the spawn's failure is the one to report */ } // no child was returned by spawn
3498
3512
  throw error;
3499
3513
  }
3500
3514
  child.on('error', () => { }); // a failed spawn can emit after this synchronous operation throws
@@ -3547,8 +3561,24 @@ function startProxyDaemonOwned(name, root, caCertPath, envFile, boot, caller = p
3547
3561
  return { url: '', caCertPath, env: {} };
3548
3562
  }
3549
3563
  catch (error) {
3550
- if (!retirementAttempted)
3551
- retire();
3564
+ // the failure that stopped the start leads; a retirement that also fails is added to it, never put in its place
3565
+ if (!retirementAttempted) {
3566
+ // a retirement error is still what is thrown (its type keeps the survivor's ownership), with the cause it hid leading its
3567
+ // message; any other cleanup failure is added to the original, which is thrown
3568
+ try {
3569
+ retire();
3570
+ }
3571
+ catch (retirement) {
3572
+ const cause = error instanceof Error ? error.message : String(error);
3573
+ if (retirement instanceof CommandRetirementError) {
3574
+ Object.assign(retirement, { retirement: retirement.message, cause: error });
3575
+ retirement.message = `${cause}; ${retirement.message}`;
3576
+ throw retirement;
3577
+ }
3578
+ if (error instanceof Error)
3579
+ error.message += `; retiring the proxy failed too: ${retirement instanceof Error ? retirement.message : String(retirement)}`;
3580
+ }
3581
+ }
3552
3582
  throw error;
3553
3583
  }
3554
3584
  }
@@ -1189,6 +1189,15 @@ export class WorldDoors {
1189
1189
  }
1190
1190
  if (kind === 'links' && !name && request.method === 'GET')
1191
1191
  return Response.json({ links: this.links() });
1192
+ // OPENING A SEAL — pointing this World at a vendor (a link, a twin's root) or sealing the credential it uses there — is
1193
+ // the World token's act, or a person's at its page: never a key's, which an app or a script holds (an app that could
1194
+ // open its own seal would be unsealed by itself, not deliberately)
1195
+ const opensSeal = (kind === 'links' && name && (request.method === 'PUT' || request.method === 'DELETE'))
1196
+ || (kind === 'twins' && name && (verb === 'root' || verb === 'credential') && request.method === 'PUT')
1197
+ // a check runs in a local World's own process, with all it can reach: putting or removing one is the same act
1198
+ || (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'));
1199
+ if (opensSeal && via === 'key')
1200
+ return Response.json({ error: 'opening a seal is the World\'s token\'s act, or a person\'s at its page: a key cannot point this World at a vendor, seal a credential, or change its checks' }, { status: 403 });
1192
1201
  if (kind === 'links' && name && request.method === 'PUT') {
1193
1202
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) || this.up().services[name])
1194
1203
  return Response.json({ error: 'a link is named [a-z0-9_-] and is not a twin of this World' }, { status: 400 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.33",
3
+ "version": "2.0.35",
4
4
  "description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
5
5
  "keywords": [
6
6
  "twin",
package/src/runtime.ts CHANGED
@@ -1567,7 +1567,11 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
1567
1567
  writeFileSync(bootingFile(root, name), `${JSON.stringify({ pid: process.pid, at: new Date().toISOString() } satisfies BootingClaim)}\n`, { mode: 0o600 });
1568
1568
  });
1569
1569
  } catch (error) {
1570
- if (resourcesClaimed) releaseWorldLifecycle(root, name);
1570
+ // the failure that stopped the boot leads: a release that also fails is added to it, never put in its place
1571
+ if (resourcesClaimed) {
1572
+ try { releaseWorldLifecycle(root, name); }
1573
+ catch (release) { if (error instanceof Error) error.message += `; releasing the World's claim failed too: ${release instanceof Error ? release.message : String(release)}`; }
1574
+ }
1571
1575
  throw error;
1572
1576
  }
1573
1577
 
@@ -1755,7 +1759,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
1755
1759
  recordWorldStop(Object.values(services).filter((service) => rollbackKilled.includes(service.pid) && service.log).map((service) => service.log), 'world.service.killed', 'boot rollback', 'SIGKILL');
1756
1760
  const surviving = undeadPids(pids);
1757
1761
  if (surviving.length) rollbackErrors.push('owned processes remain alive or uncertain');
1758
- if (error instanceof CommandRetirementError) rollbackErrors.push(error.message);
1762
+ if (error instanceof CommandRetirementError) rollbackErrors.push((error as CommandRetirementError & { retirement?: string }).retirement ?? error.message);
1759
1763
 
1760
1764
  if (rollbackErrors.length > 0) {
1761
1765
  cleanupUncertain = true;
@@ -3341,7 +3345,7 @@ function startProxyDaemonOwned(name: string, root: string, caCertPath: string, e
3341
3345
  try { writeFileSync(markerFd, JSON.stringify({ token: randomUUID(), ownerPid: process.pid, createdAt })); }
3342
3346
  finally { closeSync(markerFd); }
3343
3347
  } catch (error) {
3344
- rmSync(marker, { force: true }); // partial pre-spawn writes own no child
3348
+ try { rmSync(marker, { force: true }); } catch { /* the write's failure is the one to report */ } // partial pre-spawn writes own no child
3345
3349
  throw error;
3346
3350
  }
3347
3351
  let child: ReturnType<typeof spawn>;
@@ -3356,7 +3360,7 @@ function startProxyDaemonOwned(name: string, root: string, caCertPath: string, e
3356
3360
  });
3357
3361
  } finally { closeSync(proxyOut); }
3358
3362
  } catch (error) {
3359
- rmSync(marker, { force: true }); // no child was returned by spawn
3363
+ try { rmSync(marker, { force: true }); } catch { /* the spawn's failure is the one to report */ } // no child was returned by spawn
3360
3364
  throw error;
3361
3365
  }
3362
3366
  child.on('error', () => {}); // a failed spawn can emit after this synchronous operation throws
@@ -3399,7 +3403,20 @@ function startProxyDaemonOwned(name: string, root: string, caCertPath: string, e
3399
3403
  process.stderr.write(`!! WARN: ambient proxy daemon for world "${name}" did not report within ${deadlineMs}ms — world is NOT proxy-sealed; unmodified CLIs will bypass the twins (env-only redirect still active).\n`);
3400
3404
  return { url: '', caCertPath, env: {} };
3401
3405
  } catch (error) {
3402
- if (!retirementAttempted) retire();
3406
+ // the failure that stopped the start leads; a retirement that also fails is added to it, never put in its place
3407
+ if (!retirementAttempted) {
3408
+ // a retirement error is still what is thrown (its type keeps the survivor's ownership), with the cause it hid leading its
3409
+ // message; any other cleanup failure is added to the original, which is thrown
3410
+ try { retire(); } catch (retirement) {
3411
+ const cause = error instanceof Error ? error.message : String(error);
3412
+ if (retirement instanceof CommandRetirementError) {
3413
+ Object.assign(retirement, { retirement: retirement.message, cause: error });
3414
+ retirement.message = `${cause}; ${retirement.message}`;
3415
+ throw retirement;
3416
+ }
3417
+ if (error instanceof Error) error.message += `; retiring the proxy failed too: ${retirement instanceof Error ? retirement.message : String(retirement)}`;
3418
+ }
3419
+ }
3403
3420
  throw error;
3404
3421
  }
3405
3422
  }
@@ -1037,6 +1037,14 @@ export class WorldDoors {
1037
1037
  return Response.json(approveWorldChangeset(name, { root: this.worldRoot, world: this.name, principal, ...(body.note ? { note: body.note } : {}) }));
1038
1038
  }
1039
1039
  if (kind === 'links' && !name && request.method === 'GET') return Response.json({ links: this.links() });
1040
+ // OPENING A SEAL — pointing this World at a vendor (a link, a twin's root) or sealing the credential it uses there — is
1041
+ // the World token's act, or a person's at its page: never a key's, which an app or a script holds (an app that could
1042
+ // open its own seal would be unsealed by itself, not deliberately)
1043
+ const opensSeal = (kind === 'links' && name && (request.method === 'PUT' || request.method === 'DELETE'))
1044
+ || (kind === 'twins' && name && (verb === 'root' || verb === 'credential') && request.method === 'PUT')
1045
+ // a check runs in a local World's own process, with all it can reach: putting or removing one is the same act
1046
+ || (kind === 'checks' && name && (request.method === 'PUT' || request.method === 'DELETE'));
1047
+ if (opensSeal && via === 'key') return Response.json({ error: 'opening a seal is the World\'s token\'s act, or a person\'s at its page: a key cannot point this World at a vendor, seal a credential, or change its checks' }, { status: 403 });
1040
1048
  if (kind === 'links' && name && request.method === 'PUT') {
1041
1049
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) || this.up().services[name]) return Response.json({ error: 'a link is named [a-z0-9_-] and is not a twin of this World' }, { status: 400 });
1042
1050
  const body = (await request.json()) as { origin?: string };