stackbite 0.5.97 → 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 +1 -1
- package/packaging/npm/lib/bootstrap.js +101 -26
- package/packaging/npm/release.json +23 -23
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
223
|
-
break;
|
|
228
|
+
return value.installation_id ?? null;
|
|
224
229
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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))
|
|
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)
|
|
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
|
|
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.
|
|
4
|
-
"commit": "
|
|
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.
|
|
9
|
-
"url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.
|
|
10
|
-
"sha256": "
|
|
11
|
-
"size":
|
|
12
|
-
"root": "stackbite-0.5.
|
|
13
|
-
"manifestSha256": "
|
|
14
|
-
"metadataSha256": "
|
|
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.
|
|
18
|
-
"url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.
|
|
19
|
-
"sha256": "
|
|
20
|
-
"size":
|
|
21
|
-
"root": "stackbite-0.5.
|
|
22
|
-
"manifestSha256": "
|
|
23
|
-
"metadataSha256": "
|
|
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.
|
|
27
|
-
"url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.
|
|
28
|
-
"sha256": "
|
|
29
|
-
"size":
|
|
30
|
-
"root": "stackbite-0.5.
|
|
31
|
-
"manifestSha256": "
|
|
32
|
-
"metadataSha256": "
|
|
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
|
}
|