stackbite 0.5.96 → 0.5.98

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackbite",
3
- "version": "0.5.96",
3
+ "version": "0.5.98",
4
4
  "description": "Verified native Stackbite CLI bootstrapper",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -212,20 +212,45 @@ async function admitPublication(environment, platform) {
212
212
  if (record.schema !== 'stackbite.uninstall.v1' || !['prepared', 'stopped', 'deferred', 'complete'].includes(record.phase)) {
213
213
  fail('Stackbite removal is in progress');
214
214
  }
215
- let current = null;
215
+ const current = await locatorIncarnation(root);
216
+ const saved = record.install_identity;
217
+ if (saved != null && (saved.root !== root || !/^[a-f0-9]{32}$/.test(saved.incarnation))) fail('Invalid removal installation identity');
218
+ if (current && (!saved || current !== saved.incarnation)) return;
219
+ fail('Stackbite removal is in progress');
220
+ }
221
+ async function locatorIncarnation(root) {
216
222
  for (const name of ['default-install.json', 'install.json']) {
217
223
  const locator = path.join(root, name);
218
224
  if (!await exists(locator)) continue;
219
225
  const value = JSON.parse((await readSmall(locator, 16384)).toString('utf8').replace(/^\uFEFF/, ''));
220
226
  if (!value || Array.isArray(value) || typeof value !== 'object'
221
227
  || (value.installation_id != null && !/^[a-f0-9]{32}$/.test(value.installation_id))) fail('Invalid Stackbite installation locator');
222
- current = value.installation_id;
223
- break;
228
+ return value.installation_id ?? null;
224
229
  }
225
- const saved = record.install_identity;
226
- if (saved != null && (saved.root !== root || !/^[a-f0-9]{32}$/.test(saved.incarnation))) fail('Invalid removal installation identity');
227
- if (current && (!saved || current !== saved.incarnation)) return;
228
- fail('Stackbite removal is in progress');
230
+ return null;
231
+ }
232
+ // An accepted removal of the CURRENT installation that stopped before its
233
+ // capsule existed (no recovery_command). Only a release-pinned runtime from this
234
+ // package can continue it. The digest binds the same request bytes until launch.
235
+ async function acceptedRemovalWithoutCapsule(environment, platform) {
236
+ const root = installationRoot(environment, platform);
237
+ if (!await existingCacheDirectory(root)) return null;
238
+ const journal = path.join(root, '.stackbite-uninstall.json');
239
+ let info;
240
+ try { info = await fs.lstat(journal); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
241
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) fail('Unsafe removal journal');
242
+ const bytes = await readSmall(journal, 8 * 1024 * 1024);
243
+ const record = JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/, ''));
244
+ const saved = record?.install_identity;
245
+ if (record?.schema !== 'stackbite.uninstall.v1' || record.phase !== 'prepared' || record.accepted !== true
246
+ || record.removal_requested !== true || record.recovery_command != null
247
+ || typeof record.request_id !== 'string' || !record.request_id
248
+ || saved?.root !== root || !/^[a-f0-9]{32}$/.test(saved.incarnation)) return null;
249
+ // A reinstall after this request (another incarnation) is not this removal's target.
250
+ const current = await locatorIncarnation(root);
251
+ if (current !== null && current !== saved.incarnation) return null;
252
+ return { journal, request: record.request_id, incarnation: saved.incarnation,
253
+ digest: createHash('sha256').update(bytes).digest('hex') };
229
254
  }
230
255
 
231
256
  async function baselinePin(file) {
@@ -455,7 +480,7 @@ async function ensureBinary(spec, { cache = cacheDirectory(), downloadImpl = dow
455
480
  await existingCacheDirectory(cache); // Validate existing ancestors without creating a cache.
456
481
  }
457
482
  if (checkOnly) {
458
- if (!await existingCacheDirectory(cache)) fail('Pinned runtime is not cached; run stackbite setup.');
483
+ if (!await existingCacheDirectory(cache)) throw notCached();
459
484
  }
460
485
  // Full identity remains SHA-256 addressed while avoiding Windows MAX_PATH.
461
486
  const cacheKey = createHash('sha256').update(JSON.stringify([spec.version, spec.commit, spec.key, spec.sha256])).digest('hex');
@@ -471,7 +496,7 @@ async function ensureBinary(spec, { cache = cacheDirectory(), downloadImpl = dow
471
496
  if (checkOnly || reportCached) progress('ready');
472
497
  return installed;
473
498
  }
474
- if (checkOnly) fail('Pinned runtime is not cached; run stackbite setup.');
499
+ if (checkOnly) throw notCached();
475
500
  // An owned system-temp stage keeps downloads out of product caches. Only the
476
501
  // verified engine below can create/publish a canonical cache installation.
477
502
  await admitPublication(environment, spec.platform);
@@ -479,18 +504,7 @@ async function ensureBinary(spec, { cache = cacheDirectory(), downloadImpl = dow
479
504
  if (registrationSource) await registrationSource.validate();
480
505
  const staging = await fs.mkdtemp(path.join(os.tmpdir(), 'stackbite-npm-'));
481
506
  try {
482
- const archive = path.join(staging, spec.asset), extraction = path.join(staging, 'extract');
483
- await fs.mkdir(extraction, { mode: 0o700 });
484
- progress('download');
485
- await downloadImpl(spec, archive);
486
- // Recheck at the extraction boundary, including for injected test transports.
487
- progress('archive');
488
- if ((await fs.stat(archive)).size !== spec.size || await hashFile(archive, LIMITS.download) !== spec.sha256) fail('Pinned archive SHA-256 or size mismatch');
489
- progress('extract');
490
- await extractImpl(archive, extraction, spec);
491
- const payload = path.join(extraction, spec.root);
492
- progress('validate');
493
- await validatePayload(payload, spec);
507
+ const { payload } = await stageRelease(spec, staging, { downloadImpl, extractImpl, progress });
494
508
  progress('publish');
495
509
  await admitPublication(environment, spec.platform);
496
510
  await assertBaseline(environment, spec.platform);
@@ -501,15 +515,71 @@ async function ensureBinary(spec, { cache = cacheDirectory(), downloadImpl = dow
501
515
  const installed = { home: target, executable: await validatePayload(target, spec) };
502
516
  progress('ready');
503
517
  return installed;
504
- } finally {
505
- // Only this mkdtemp handle is owned by this invocation.
506
- if (path.dirname(staging) !== path.resolve(os.tmpdir()) || !path.basename(staging).startsWith('stackbite-npm-')) fail('Unsafe staging cleanup path');
507
- await fs.rm(staging, { recursive: true, force: true });
508
- }
518
+ } finally { await removeStage(staging); }
509
519
  } finally {
510
520
  if (preparation) await settlePreparation(preparation, environment, spec.platform, registrationSource);
511
521
  }
512
522
  }
523
+ function notCached() {
524
+ return Object.assign(new Error('Pinned runtime is not cached; run stackbite setup.'), { code: 'STACKBITE_NOT_CACHED' });
525
+ }
526
+ // Download, size/hash, extract and validate the pinned archive inside an owned stage.
527
+ async function stageRelease(spec, staging, { downloadImpl, extractImpl, progress }) {
528
+ const archive = path.join(staging, spec.asset), extraction = path.join(staging, 'extract');
529
+ await fs.mkdir(extraction, { mode: 0o700 });
530
+ progress('download');
531
+ await downloadImpl(spec, archive);
532
+ // Recheck at the extraction boundary, including for injected test transports.
533
+ progress('archive');
534
+ if ((await fs.stat(archive)).size !== spec.size || await hashFile(archive, LIMITS.download) !== spec.sha256) fail('Pinned archive SHA-256 or size mismatch');
535
+ progress('extract');
536
+ await extractImpl(archive, extraction, spec);
537
+ const payload = path.join(extraction, spec.root);
538
+ progress('validate');
539
+ return { payload, executable: await validatePayload(payload, spec) };
540
+ }
541
+ async function removeStage(staging) {
542
+ // Only this invocation's mkdtemp directory is owned.
543
+ if (path.dirname(staging) !== path.resolve(os.tmpdir()) || !path.basename(staging).startsWith('stackbite-npm-')) fail('Unsafe staging cleanup path');
544
+ await fs.rm(staging, { recursive: true, force: true });
545
+ }
546
+ // The fixed engine may hand removal to a helper started from its own image
547
+ // (parent_resume.runtime). While the same journal still names that image, the
548
+ // stage is the helper's runtime and stays; finalization removes the journal.
549
+ async function stageStillNeeded(pending, staging) {
550
+ let record;
551
+ try { record = JSON.parse((await readSmall(pending.journal, 8 * 1024 * 1024)).toString('utf8').replace(/^\uFEFF/, '')); }
552
+ catch (error) { if (error.code === 'ENOENT') return false; return true; }
553
+ const runtime = record?.parent_resume?.runtime?.path;
554
+ if (record?.request_id !== pending.request || typeof runtime !== 'string' || !path.isAbsolute(runtime)) return false;
555
+ const roots = [staging, await fs.realpath(staging)];
556
+ return roots.some(root => inside(root, path.resolve(runtime)));
557
+ }
558
+ // Continue an accepted removal with a private, verified copy of this package's
559
+ // pinned runtime. Nothing is published: no cache, locator, preparation or
560
+ // installation identity is created, and only the original argv runs.
561
+ async function continueAcceptedRemoval(spec, argv, environment, options) {
562
+ const pending = await acceptedRemovalWithoutCapsule(environment, spec.platform);
563
+ if (!pending) return null;
564
+ const staging = await fs.mkdtemp(path.join(os.tmpdir(), 'stackbite-npm-'));
565
+ let retained = false;
566
+ try {
567
+ const { payload, executable } = await stageRelease(spec, staging, {
568
+ downloadImpl: options.downloadImpl || download, extractImpl: options.extractImpl || extractArchive,
569
+ progress: options.progress || stageReporter(options.stderr || process.stderr) });
570
+ if (JSON.stringify(await acceptedRemovalWithoutCapsule(environment, spec.platform)) !== JSON.stringify(pending)) {
571
+ fail('Stackbite removal changed while staging its runtime; nothing was run');
572
+ }
573
+ const npm = await require('./uninstall-registration').descriptor(options);
574
+ const result = await launchNative(executable, argv, { ...environment,
575
+ ...(npm ? { STACKBITE_NPM_REGISTRATION: JSON.stringify(npm) } : {}),
576
+ STACKBITE_HOME: path.resolve(payload) }, options);
577
+ retained = await stageStillNeeded(pending, staging);
578
+ return result;
579
+ } finally {
580
+ if (!retained) await removeStage(staging);
581
+ }
582
+ }
513
583
  async function launchNative(executable, argv, environment, { spawnImpl = spawn, signalSource = process } = {}) {
514
584
  return new Promise((resolve, reject) => {
515
585
  const child = spawnImpl(executable, argv, { env: environment, stdio: 'inherit', shell: false });
@@ -553,6 +623,11 @@ async function main(argv = process.argv.slice(2), options = {}) {
553
623
  const { resumeWithoutRuntime, registrationOnly } = require('./uninstall-registration');
554
624
  const recovered = await resumeWithoutRuntime(argv, environment, launchNative, options);
555
625
  if (recovered) return recovered;
626
+ // Only an absent runtime qualifies; a corrupt or foreign cache stays an error.
627
+ if (error.code === 'STACKBITE_NOT_CACHED' && explicitApply(argv)) {
628
+ const continued = await continueAcceptedRemoval(spec, argv, environment, options);
629
+ if (continued) return continued;
630
+ }
556
631
  const registration = await registrationOnly(argv, environment, launchNative, options);
557
632
  if (registration) return registration;
558
633
  throw error;
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "schema": 1,
3
- "version": "0.5.96",
4
- "commit": "7f23d4a4e4d940e0147d841992e93dd23befc944",
3
+ "version": "0.5.98",
4
+ "commit": "4c23b408dfb8e87b7bb93154b895769fa17c4af3",
5
5
  "repository": "Shinick-Han/stackbite",
6
6
  "platforms": {
7
7
  "win32-x64": {
8
- "asset": "stackbite-0.5.96-windows-x64-7f23d4a4e4d9.zip",
9
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.96/stackbite-0.5.96-windows-x64-7f23d4a4e4d9.zip",
10
- "sha256": "ea9b75271f5fbb2ba082eb323adecbacf53a0b049330be2e99fefe865fc6dba0",
11
- "size": 95635099,
12
- "root": "stackbite-0.5.96-windows-x64-7f23d4a4e4d9",
13
- "manifestSha256": "464afb4d132173090c27e4fb2546801cf469b4659e1df68b51fad92555ceaac4",
14
- "metadataSha256": "5d741796ed25420cb01d0ba736a3d54a94609369702f58c0770c1dc23241aa75"
8
+ "asset": "stackbite-0.5.98-windows-x64-4c23b408dfb8.zip",
9
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.98/stackbite-0.5.98-windows-x64-4c23b408dfb8.zip",
10
+ "sha256": "3b587ba43f67158a14e55ee4d4c9431f93cc0b576360d42e281ecc4736ae6f6d",
11
+ "size": 95796531,
12
+ "root": "stackbite-0.5.98-windows-x64-4c23b408dfb8",
13
+ "manifestSha256": "6e5c387b3af706690f372b831dab60ca923211ffb02d481c9bda3c40a599fd1c",
14
+ "metadataSha256": "152b7c85dccafc2cedb9ce8ea498f4a26e43834c534f6dd086d047d7539b05c1"
15
15
  },
16
16
  "linux-x64": {
17
- "asset": "stackbite-0.5.96-linux-x86_64-7f23d4a4e4d9.tar.gz",
18
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.96/stackbite-0.5.96-linux-x86_64-7f23d4a4e4d9.tar.gz",
19
- "sha256": "2cd509a933634d270da4e1729022beb7b7a803880617088c94327d2ad4631e38",
20
- "size": 75907031,
21
- "root": "stackbite-0.5.96-linux-x86_64-7f23d4a4e4d9",
22
- "manifestSha256": "d59dd6704b0ada8a426f5fab557728d2fef7008cecf076c45662c6292546b6b7",
23
- "metadataSha256": "d1afa491f2d0d1f3f128e9b8bef24b62c2adbd3a3ea84fef5692552f303063b2"
17
+ "asset": "stackbite-0.5.98-linux-x86_64-4c23b408dfb8.tar.gz",
18
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.98/stackbite-0.5.98-linux-x86_64-4c23b408dfb8.tar.gz",
19
+ "sha256": "eaf429b2521cbbb7f7f3675379f855ec5f62510f10c1ab2f1d3f0dec413b6eb3",
20
+ "size": 76072178,
21
+ "root": "stackbite-0.5.98-linux-x86_64-4c23b408dfb8",
22
+ "manifestSha256": "1542d61e5aaee722b99bfefe04274b11e6287caa6924dc4c5f2fc7157c5842d3",
23
+ "metadataSha256": "5d0a1ec2ca0c26f3119bfc68e70bd102890313099e961ee7ba65f3da661eee8b"
24
24
  },
25
25
  "darwin-arm64": {
26
- "asset": "stackbite-0.5.96-macos-arm64-7f23d4a4e4d9.tar.gz",
27
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.96/stackbite-0.5.96-macos-arm64-7f23d4a4e4d9.tar.gz",
28
- "sha256": "822879f121af836b47152d7def5e59e50fcb1a8ed987090d1efff4c3688e3ed2",
29
- "size": 30871909,
30
- "root": "stackbite-0.5.96-macos-arm64-7f23d4a4e4d9",
31
- "manifestSha256": "e25fc4c20e124d2db018d8402cb5590368e634afc0b6bc035db665ee0a54578c",
32
- "metadataSha256": "e6ef71691f4d36ad244864d2cbfb2f916e7afafda8c59787926399b6539e2562"
26
+ "asset": "stackbite-0.5.98-macos-arm64-4c23b408dfb8.tar.gz",
27
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.98/stackbite-0.5.98-macos-arm64-4c23b408dfb8.tar.gz",
28
+ "sha256": "56e4f20164b0163cde0be464349165944a7b333e223b1e4091ed60ba1476b1c2",
29
+ "size": 31031692,
30
+ "root": "stackbite-0.5.98-macos-arm64-4c23b408dfb8",
31
+ "manifestSha256": "b833a1fbc7bbe6a8bdfa3aa69c9c2f8e35dc8be5a8bf9ed7d3fa2756e9316478",
32
+ "metadataSha256": "93c06432dffb83f1afa72cda84e7f441482314fc67c2d511855b50ab4f014629"
33
33
  }
34
34
  }
35
35
  }