stackbite 0.5.103 → 0.5.105

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.103",
3
+ "version": "0.5.105",
4
4
  "description": "Verified native Stackbite CLI bootstrapper",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -7,7 +7,7 @@ const { createHash } = require('node:crypto');
7
7
  const { LIMITS, fail, memberPath, inside, hashFile, readSmall, exists } = require('./common');
8
8
  const { download } = require('./download');
9
9
  const { extractArchive } = require('./archive');
10
- const { cacheDirectory, existingCacheDirectory } = require('./cache');
10
+ const { cacheDirectory, existingCacheDirectory, resolveCacheDirectory } = require('./cache');
11
11
  const { parseSetup, stageReporter } = require('./setup');
12
12
  const timing = require('./startup-timing');
13
13
  const packageVersion = require('../../../package.json').version;
@@ -209,12 +209,24 @@ function installationRoot(environment, platform) {
209
209
  ? path.resolve(environment.LOCALAPPDATA || home, 'Stackbite')
210
210
  : path.resolve(environment.XDG_DATA_HOME || path.join(environment.HOME || home, '.local', 'share'), 'stackbite');
211
211
  }
212
+ // Resolve only user-selected directory roots. Managed descendants, including
213
+ // the installation root and update directory, must still be real directories.
214
+ async function selectedDirectory(directory, environment, platform) {
215
+ const home = environment.USERPROFILE || environment.HOME || os.homedir();
216
+ const data = platform === 'win32' ? environment.LOCALAPPDATA || home
217
+ : environment.XDG_DATA_HOME || path.join(environment.HOME || home, '.local', 'share');
218
+ const roots = [data, home].map(root => path.resolve(root)).sort((a, b) => b.length - a.length);
219
+ const selected = roots.find(root => directory === root || inside(root, directory));
220
+ if (!selected) return existingCacheDirectory(directory);
221
+ const resolved = await resolveCacheDirectory(selected);
222
+ return existingCacheDirectory(path.join(resolved, path.relative(selected, directory)));
223
+ }
212
224
  // Before a runtime is available, read the SAME journal without creating any
213
225
  // product directory. The verified bundled engine repeats this under OS locks
214
226
  // and owns cache/locator publication. This read is not a JavaScript flock.
215
227
  async function admitPublication(environment, platform) {
216
228
  const root = installationRoot(environment, platform);
217
- if (!await existingCacheDirectory(root)) return;
229
+ if (!await selectedDirectory(root, environment, platform)) return;
218
230
  const journal = path.join(root, '.stackbite-uninstall.json');
219
231
  if (!await exists(journal)) return;
220
232
  const record = JSON.parse((await readSmall(journal, 8 * 1024 * 1024)).toString('utf8').replace(/^\uFEFF/, ''));
@@ -243,7 +255,7 @@ async function locatorIncarnation(root) {
243
255
  // package can continue it. The digest binds the same request bytes until launch.
244
256
  async function acceptedRemovalWithoutCapsule(environment, platform) {
245
257
  const root = installationRoot(environment, platform);
246
- if (!await existingCacheDirectory(root)) return null;
258
+ if (!await selectedDirectory(root, environment, platform)) return null;
247
259
  const journal = path.join(root, '.stackbite-uninstall.json');
248
260
  let info;
249
261
  try { info = await fs.lstat(journal); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
@@ -262,8 +274,9 @@ async function acceptedRemovalWithoutCapsule(environment, platform) {
262
274
  digest: createHash('sha256').update(bytes).digest('hex') };
263
275
  }
264
276
 
265
- async function baselinePin(file) {
266
- await existingCacheDirectory(path.dirname(file));
277
+ async function baselinePin(file, environment, platform) {
278
+ if (environment) await selectedDirectory(path.dirname(file), environment, platform);
279
+ else await existingCacheDirectory(path.dirname(file));
267
280
  let info;
268
281
  try { info = await fs.lstat(file, { bigint: true }); }
269
282
  catch (error) { if (error.code === 'ENOENT') return null; throw error; }
@@ -296,11 +309,11 @@ async function captureBaseline(environment, platform, cachedHome) {
296
309
  for (const selected of homes) {
297
310
  for (const name of ['portable.json', platform === 'win32' ? 'stackbite.exe' : 'stackbite', 'stackbite.py']) {
298
311
  const file = path.join(selected, name);
299
- if (await baselinePin(file) !== null) paths.push(file);
312
+ if (await baselinePin(file, environment, platform) !== null) paths.push(file);
300
313
  }
301
314
  }
302
315
  const legacy = [];
303
- for (const file of new Set(paths)) legacy.push({ path: file, identity: await baselinePin(file) });
316
+ for (const file of new Set(paths)) legacy.push({ path: file, identity: await baselinePin(file, environment, platform) });
304
317
  return legacy.some(pin => pin.identity !== null) ? { root, incarnation: null, legacy } : null;
305
318
  }
306
319
 
@@ -310,23 +323,23 @@ async function assertBaseline(environment, platform) {
310
323
  if (Buffer.byteLength(raw) > 16384) fail('Invalid installation baseline');
311
324
  const expected = JSON.parse(raw), root = installationRoot(environment, platform);
312
325
  if (expected.preparation !== undefined) {
313
- await assertPreparationBaseline(expected, root, platform);
326
+ await assertPreparationBaseline(expected, root, platform, environment);
314
327
  return;
315
328
  }
316
329
  const current = await locatorInstallationId(root);
317
330
  if (expected.root !== root || expected.incarnation !== current) fail('Installation changed while staging');
318
- if (current === null) await assertLegacyBaseline(expected, platform);
331
+ if (current === null) await assertLegacyBaseline(expected, platform, environment);
319
332
  }
320
333
  // Python-origin restrictions retain FILE_ID_INFO's full volume serial;
321
334
  // libuv reports its 32-bit component. The engine still checks the full pin.
322
335
  function libuvIdentity(platform, actual, identity) {
323
336
  return platform === 'win32' && actual && identity ? { ...identity, device: (BigInt(identity.device) & 0xffffffffn).toString() } : identity;
324
337
  }
325
- async function assertPreparationBaseline(expected, root, platform) {
338
+ async function assertPreparationBaseline(expected, root, platform, environment) {
326
339
  const pin = expected.preparation, file = path.join(root, '.stackbite-update', 'update.lock');
327
340
  if (Object.keys(expected).sort().join(',') !== 'preparation,root' || expected.root !== root
328
341
  || !pin || Object.keys(pin).sort().join(',') !== 'identity,path' || pin.path !== file || !pin.identity) fail('Invalid installation preparation');
329
- const actual = await baselinePin(file);
342
+ const actual = await baselinePin(file, environment, platform);
330
343
  if (JSON.stringify(actual) !== JSON.stringify(libuvIdentity(platform, actual, pin.identity))) fail('Installation changed while staging');
331
344
  }
332
345
  // The first present locator's incarnation; null for a legacy or absent locator.
@@ -339,10 +352,10 @@ async function locatorInstallationId(root) {
339
352
  }
340
353
  return null;
341
354
  }
342
- async function assertLegacyBaseline(expected, platform) {
355
+ async function assertLegacyBaseline(expected, platform, environment) {
343
356
  if (!Array.isArray(expected.legacy) || !expected.legacy.length || expected.legacy.length > 32) fail('Invalid legacy installation baseline');
344
357
  for (const pin of expected.legacy) {
345
- const actual = await baselinePin(pin.path);
358
+ const actual = await baselinePin(pin.path, environment, platform);
346
359
  if (JSON.stringify(actual) !== JSON.stringify(libuvIdentity(platform, actual, pin.identity))) fail('Installation changed while staging');
347
360
  }
348
361
  }
@@ -381,12 +394,12 @@ async function settlePreparation(preparation, environment, platform, source) {
381
394
  if (!preparation.created || !source || !await source.absent()
382
395
  || await captureBaseline(environment, platform, preparation.target)) return;
383
396
  for (const [directory, pin] of preparation.directories) {
384
- await existingCacheDirectory(directory);
397
+ await selectedDirectory(directory, environment, platform);
385
398
  const current = await fs.lstat(directory, { bigint: true });
386
399
  if (current.dev !== pin.dev || current.ino !== pin.ino) return;
387
400
  }
388
401
  const { file, identity, handle } = preparation;
389
- if (JSON.stringify(await baselinePin(file)) !== JSON.stringify(identity)) return;
402
+ if (JSON.stringify(await baselinePin(file, environment, platform)) !== JSON.stringify(identity)) return;
390
403
  const held = await handle.stat({ bigint: true });
391
404
  if (!held.isFile() || held.nlink !== 1n || held.size > 1n) return;
392
405
  const bytes = Buffer.alloc(2);
@@ -395,7 +408,7 @@ async function settlePreparation(preparation, environment, platform, source) {
395
408
  if (BigInt(bytesRead) !== held.size || bytesRead === 1 && bytes[0] !== 0x30) return;
396
409
  await fs.unlink(file);
397
410
  for (const [directory, pin] of preparation.directories) {
398
- await existingCacheDirectory(directory);
411
+ await selectedDirectory(directory, environment, platform);
399
412
  const current = await fs.lstat(directory, { bigint: true });
400
413
  if (current.dev !== pin.dev || current.ino !== pin.ino) break;
401
414
  await fs.rmdir(directory);
@@ -441,21 +454,21 @@ async function reservePreparation(environment, platform, target, source) {
441
454
  // object identity only; the bundled engine still takes the kernel gates.
442
455
  const root = installationRoot(environment, platform), directory = path.join(root, '.stackbite-update');
443
456
  await admitPublication(environment, platform);
444
- await existingCacheDirectory(directory);
457
+ await selectedDirectory(directory, environment, platform);
445
458
  for (let parent = root; ; parent = path.dirname(parent)) {
446
459
  if (await exists(path.join(parent, '.git'))) fail('Refusing installation preparation in a source repository');
447
460
  if (parent === path.dirname(parent)) break;
448
461
  }
449
462
  if (source) await source.validate();
450
463
  const firstCreated = await fs.mkdir(directory, { recursive: true, mode: 0o700 });
451
- await existingCacheDirectory(directory);
464
+ await selectedDirectory(directory, environment, platform);
452
465
  const directories = await createdDirectories(firstCreated, [directory, root]);
453
466
  const file = path.join(directory, 'update.lock');
454
467
  const { handle, created } = await openPreparationFile(file);
455
468
  let preparation;
456
469
  try {
457
470
  const held = await handle.stat({ bigint: true });
458
- const identity = await baselinePin(file);
471
+ const identity = await baselinePin(file, environment, platform);
459
472
  if (!held.isFile() || held.nlink !== 1n || JSON.stringify(identity) !== JSON.stringify({ device: held.dev.toString(), inode: held.ino.toString() })) fail('Invalid installation preparation');
460
473
  preparation = { handle, file, identity, created, directories, target };
461
474
  baseline = { root, preparation: { path: file, identity } };
@@ -496,7 +509,7 @@ async function publishCache(payload, target, spec, options) {
496
509
  async function ensureBinary(spec, { cache = cacheDirectory(), downloadImpl = download, extractImpl = extractArchive,
497
510
  checkOnly = false, reportCached = false, includePhone = false, progress = () => {},
498
511
  environment = process.env, publishImpl = publishCache, registrationSource = null, ...options } = {}) {
499
- cache = path.resolve(cache);
512
+ cache = await resolveCacheDirectory(cache);
500
513
  environment = { ...environment };
501
514
  if (!checkOnly) {
502
515
  await admitPublication(environment, spec.platform);
@@ -20,4 +20,24 @@ async function existingCacheDirectory(directory) {
20
20
  if (!info.isDirectory() || info.isSymbolicLink()) fail('Cache directory contains a link or non-directory');
21
21
  return true;
22
22
  }
23
- module.exports = { cacheDirectory, existingCacheDirectory };
23
+ // Resolve the selected cache path once, including an absent suffix. Internal
24
+ // payload paths still use the link checks and pinned hashes in bootstrap.
25
+ async function resolveCacheDirectory(directory) {
26
+ directory = path.resolve(directory);
27
+ const missing = [];
28
+ let current = directory;
29
+ for (;;) {
30
+ try {
31
+ const resolved = await fs.realpath(current);
32
+ if (!(await fs.stat(resolved)).isDirectory()) fail('Cache directory contains a non-directory');
33
+ return path.join(resolved, ...missing.reverse());
34
+ } catch (error) {
35
+ if (error.code !== 'ENOENT') throw error;
36
+ const parent = path.dirname(current);
37
+ if (parent === current) throw error;
38
+ missing.push(path.basename(current));
39
+ current = parent;
40
+ }
41
+ }
42
+ }
43
+ module.exports = { cacheDirectory, existingCacheDirectory, resolveCacheDirectory };
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "schema": 1,
3
- "version": "0.5.103",
4
- "commit": "47aa8fd060382de0a8da2ad206279067293cbd50",
3
+ "version": "0.5.105",
4
+ "commit": "235f6efe68c2db92ecbda776f27ffc55c28bbac7",
5
5
  "repository": "Shinick-Han/stackbite",
6
6
  "platforms": {
7
7
  "win32-x64": {
8
- "asset": "stackbite-0.5.103-windows-x64-47aa8fd06038.zip",
9
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.103/stackbite-0.5.103-windows-x64-47aa8fd06038.zip",
10
- "sha256": "9a539580a612d930e8624649b07300f7926c43da8cbab21cee281acd22180c1e",
11
- "size": 96002066,
12
- "root": "stackbite-0.5.103-windows-x64-47aa8fd06038",
13
- "manifestSha256": "37eda9211d27e94c0baed4a17f9f1b64d9bb73a7cb5771101c6cb1a4e6af6a9d",
14
- "metadataSha256": "e457c7c510598b36563a7532e5a306451a9b0982e948ef621dd7ed871a36ce5d"
8
+ "asset": "stackbite-0.5.105-windows-x64-235f6efe68c2.zip",
9
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.105/stackbite-0.5.105-windows-x64-235f6efe68c2.zip",
10
+ "sha256": "58b55180386fec481c87f8f95f3b12b066c7cd552f26c2d97908626648fdfb0f",
11
+ "size": 96306228,
12
+ "root": "stackbite-0.5.105-windows-x64-235f6efe68c2",
13
+ "manifestSha256": "90065bde2ddb6092a5f70b6f2d6c263e84db201847e17bd22498d717f2b8b00a",
14
+ "metadataSha256": "fd100b25335a5bd372bca12c30d9077a12571281bc38b98e614cda19aad277b7"
15
15
  },
16
16
  "linux-x64": {
17
- "asset": "stackbite-0.5.103-linux-x86_64-47aa8fd06038.tar.gz",
18
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.103/stackbite-0.5.103-linux-x86_64-47aa8fd06038.tar.gz",
19
- "sha256": "8ece6fdc4fff9946f8d2dcb200ef9d3ef80c87209f64d20c949a0c070adb3d5b",
20
- "size": 77198058,
21
- "root": "stackbite-0.5.103-linux-x86_64-47aa8fd06038",
22
- "manifestSha256": "d5495675ebe13bc89e58d4e03d2b19e9619f7a4c93591079f60e54fd4a5190c2",
23
- "metadataSha256": "c858b20ab6a696eb756d2881d4657731b0f0eb56855fc078a2d6f027c0151de7"
17
+ "asset": "stackbite-0.5.105-linux-x86_64-235f6efe68c2.tar.gz",
18
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.105/stackbite-0.5.105-linux-x86_64-235f6efe68c2.tar.gz",
19
+ "sha256": "26c730fad6c9288d897df8613aa554f0c6e0551e1247bc1caa247335a7f31b8e",
20
+ "size": 77521818,
21
+ "root": "stackbite-0.5.105-linux-x86_64-235f6efe68c2",
22
+ "manifestSha256": "3f22af3a8ee9cf7bf1bc80a2e01c07ba74b1836059309cb2ac063289e5194a2d",
23
+ "metadataSha256": "47feca72039402ff23d697778d24f8b763015a3cda62c1b2e978f7cd8c169dd2"
24
24
  },
25
25
  "darwin-arm64": {
26
- "asset": "stackbite-0.5.103-macos-arm64-47aa8fd06038.tar.gz",
27
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.103/stackbite-0.5.103-macos-arm64-47aa8fd06038.tar.gz",
28
- "sha256": "61a795cddb426f694a7b6a0d11f416151e0dd6d43191ca1303c150be2feb0206",
29
- "size": 33230555,
30
- "root": "stackbite-0.5.103-macos-arm64-47aa8fd06038",
31
- "manifestSha256": "d22dfdd857f2be54f9f97d74305d9de2e0957177e8d3da5b878fdd7160d2c912",
32
- "metadataSha256": "e33be6949b9c047dfbe0f67c2b44cced26faeaa52fc5bd4f1a5380252eb03367"
26
+ "asset": "stackbite-0.5.105-macos-arm64-235f6efe68c2.tar.gz",
27
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.105/stackbite-0.5.105-macos-arm64-235f6efe68c2.tar.gz",
28
+ "sha256": "568e50444ae47669b379aefc8b0c45e3d284ea1db456a344701e2a9ad87e4bd8",
29
+ "size": 33495757,
30
+ "root": "stackbite-0.5.105-macos-arm64-235f6efe68c2",
31
+ "manifestSha256": "632b645611bed771f4d8a47f1a22e9a5952b9ac27c6741dff150409256349083",
32
+ "metadataSha256": "b411b8de8f99ec000c751278dbbd1d5b58193086e4876343e931ab91e1411277"
33
33
  }
34
34
  }
35
35
  }