@tinycloud/operations 0.6.1-beta.1 → 0.6.1-beta.10

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/dist/state.cjs CHANGED
@@ -23,8 +23,11 @@ __export(state_exports, {
23
23
  DEFAULT_PROFILE: () => DEFAULT_PROFILE,
24
24
  ProfileDeletedError: () => ProfileDeletedError,
25
25
  ProfileLockTimeoutError: () => ProfileLockTimeoutError,
26
+ ProfileSymlinkError: () => ProfileSymlinkError,
26
27
  additionalDelegationsPath: () => additionalDelegationsPath,
28
+ assertRealProfileDirectory: () => assertRealProfileDirectory,
27
29
  authRequestsPath: () => authRequestsPath,
30
+ namedProfilePath: () => namedProfilePath,
28
31
  profileConfigPath: () => profileConfigPath,
29
32
  profileLockMetadataPath: () => profileLockMetadataPath,
30
33
  profileLockPath: () => profileLockPath,
@@ -48,6 +51,7 @@ __export(state_exports, {
48
51
  tinycloudHomePath: () => tinycloudHomePath,
49
52
  updateProfileStore: () => updateProfileStore,
50
53
  upsertProfileRecord: () => upsertProfileRecord,
54
+ withPinnedProfileDirectory: () => withPinnedProfileDirectory,
51
55
  withProfileLock: () => withProfileLock,
52
56
  withTinyCloudStateRoot: () => withTinyCloudStateRoot,
53
57
  writeJsonAtomic: () => writeJsonAtomic,
@@ -81,14 +85,18 @@ var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIE
81
85
  var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
82
86
  var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
83
87
  function processWideContext(key) {
84
- var _a3;
88
+ var _a4;
85
89
  const registry = globalThis;
86
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new import_node_async_hooks.AsyncLocalStorage());
90
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new import_node_async_hooks.AsyncLocalStorage());
87
91
  }
88
92
  var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
89
93
  var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
94
+ var pinnedProfiles = processWideContext("tinycloud.operations.pinnedProfiles.v2");
95
+ var pinnedHomes = processWideContext("tinycloud.operations.pinnedHomes.v2");
90
96
  var _a;
91
- var processInstanceId = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a] = (0, import_node_crypto.randomUUID)());
97
+ var canonicalHomes = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.canonicalHomes.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Map());
98
+ var _a2;
99
+ var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = (0, import_node_crypto.randomUUID)());
92
100
  var cachedProcessOrigin;
93
101
  function processOrigin() {
94
102
  if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
@@ -276,9 +284,14 @@ function tinycloudConfigPath() {
276
284
  function profilesPath() {
277
285
  return (0, import_node_path.join)(tinycloudHomePath(), "profiles");
278
286
  }
279
- function profilePath(profile) {
287
+ function namedProfilePath(profile) {
280
288
  return (0, import_node_path.join)(profilesPath(), validateProfileName(profile));
281
289
  }
290
+ function profilePath(profile) {
291
+ const named = namedProfilePath(profile);
292
+ const pinned = pinnedProfiles.getStore()?.get(named);
293
+ return pinned ? livePinnedPath(profile, pinned) : named;
294
+ }
282
295
  function profileConfigPath(profile) {
283
296
  return (0, import_node_path.join)(profilePath(profile), "profile.json");
284
297
  }
@@ -298,13 +311,15 @@ function profileStoreMetadataPath(profile, store) {
298
311
  return `${profileStorePath(profile, store)}.metadata.json`;
299
312
  }
300
313
  function profileLockPath(profile) {
301
- return (0, import_node_path.join)(profilePath(profile), ".lock");
314
+ return (0, import_node_path.join)(namedProfilePath(profile), ".lock");
302
315
  }
303
316
  function profileLockMetadataPath(profile) {
304
317
  return (0, import_node_path.join)(profileLockPath(profile), "owner.json");
305
318
  }
306
319
  function profileTurnLockPath(profile) {
307
- return (0, import_node_path.join)(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
320
+ const home = tinycloudHomePath();
321
+ const pinned = pinnedHomes.getStore()?.get(home);
322
+ return (0, import_node_path.join)(pinned ? livePinnedPath(profile, pinned) : home, "profile-locks", validateProfileName(profile));
308
323
  }
309
324
  async function readJson(filePath) {
310
325
  try {
@@ -314,6 +329,44 @@ async function readJson(filePath) {
314
329
  throw error;
315
330
  }
316
331
  }
332
+ async function readProfileJson(profile, filePath) {
333
+ const before = process.platform !== "linux" ? await (0, import_promises.lstat)(filePath, { bigint: true }).catch((error) => {
334
+ if (error.code === "ENOENT") return null;
335
+ throw error;
336
+ }) : null;
337
+ if (process.platform !== "linux" && !before) return null;
338
+ if (before && !before.isFile()) throw new ProfileSymlinkError(profile);
339
+ const dev = before && exactFileId(before.dev);
340
+ const ino = before && exactFileId(before.ino);
341
+ try {
342
+ const handle = await (0, import_promises.open)(filePath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
343
+ try {
344
+ if (before) {
345
+ const opened = await handle.stat({ bigint: true });
346
+ if (!opened.isFile() || dev === null || ino === null || dev !== exactFileId(opened.dev) || ino !== exactFileId(opened.ino)) {
347
+ throw new ProfileSymlinkError(profile, "changed while opening its file");
348
+ }
349
+ }
350
+ const contents = await handle.readFile("utf8");
351
+ if (before) {
352
+ const named = await (0, import_promises.lstat)(filePath, { bigint: true }).catch((error) => {
353
+ if (error.code === "ENOENT") return null;
354
+ throw error;
355
+ });
356
+ if (!named?.isFile() || dev !== exactFileId(named.dev) || ino !== exactFileId(named.ino)) {
357
+ throw new ProfileSymlinkError(profile, "changed while reading its file");
358
+ }
359
+ }
360
+ return JSON.parse(contents);
361
+ } finally {
362
+ await handle.close();
363
+ }
364
+ } catch (error) {
365
+ if (isErrno(error, "ENOENT")) return null;
366
+ if (isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR")) throw new ProfileSymlinkError(profile);
367
+ throw error;
368
+ }
369
+ }
317
370
  async function writeJsonAtomic(filePath, value) {
318
371
  await replaceJson(filePath, value, false);
319
372
  }
@@ -347,11 +400,14 @@ async function writeFileDurably(path, contents) {
347
400
  }
348
401
  }
349
402
  async function readProfile(profile) {
350
- return readJson(profileConfigPath(profile));
403
+ return withPinnedProfileDirectory(profile, (directory) => directory ? readProfileJson(profile, (0, import_node_path.join)(directory, "profile.json")) : Promise.resolve(null));
351
404
  }
352
405
  async function readSession(profile) {
353
- await readStoreMetadata(profile, "session");
354
- return readJson(sessionPath(profile));
406
+ return withPinnedProfileDirectory(profile, async (directory) => {
407
+ if (!directory) return null;
408
+ await readStoreMetadata(profile, "session");
409
+ return readProfileJson(profile, sessionPath(profile));
410
+ });
355
411
  }
356
412
  async function readAdditionalDelegations(profile) {
357
413
  return (await readProfileStore(profile, "additional-delegations")).records;
@@ -360,48 +416,200 @@ async function readAuthRequests(profile) {
360
416
  return (await readProfileStore(profile, "auth-requests")).records;
361
417
  }
362
418
  async function readStoreMetadata(profile, store) {
363
- const metadata = await readJson(profileStoreMetadataPath(profile, store));
364
- if (metadata === null) return { formatVersion: 1 };
365
- if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
366
- throw new TypeError(`Unsupported store format for "${store}".`);
367
- }
368
- return metadata;
419
+ return withPinnedProfileDirectory(profile, async (directory) => {
420
+ const metadata = directory ? await readProfileJson(profile, profileStoreMetadataPath(profile, store)) : null;
421
+ if (metadata === null) return { formatVersion: 1 };
422
+ if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
423
+ throw new TypeError(`Unsupported store format for "${store}".`);
424
+ }
425
+ return metadata;
426
+ });
369
427
  }
370
428
  async function readProfileStore(profile, store) {
371
- const [metadata, raw] = await Promise.all([
372
- readStoreMetadata(profile, store),
373
- readJson(profileStorePath(profile, store))
374
- ]);
375
- return {
376
- formatVersion: metadata.formatVersion,
377
- records: Array.isArray(raw) ? raw : []
378
- };
429
+ return withPinnedProfileDirectory(profile, async (directory) => {
430
+ if (!directory) return { formatVersion: 1, records: [] };
431
+ const [metadata, raw] = await Promise.all([
432
+ readStoreMetadata(profile, store),
433
+ readProfileJson(profile, profileStorePath(profile, store))
434
+ ]);
435
+ return {
436
+ formatVersion: metadata.formatVersion,
437
+ records: Array.isArray(raw) ? raw : []
438
+ };
439
+ });
440
+ }
441
+ var ProfileSymlinkError = class extends Error {
442
+ constructor(profile, detail = "must be a real directory, not a symlink or another file type") {
443
+ super(`Profile "${profile}" ${detail}.`);
444
+ this.code = "PROFILE_SYMLINK";
445
+ this.name = "ProfileSymlinkError";
446
+ }
447
+ };
448
+ function exactFileId(value) {
449
+ if (typeof value === "bigint") return value >= 0n ? value : null;
450
+ return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
451
+ }
452
+ function isPinnedLive(pinned) {
453
+ if (!pinned.active) return false;
454
+ try {
455
+ const stats = (0, import_node_fs.fstatSync)(pinned.fd, { bigint: true });
456
+ return exactFileId(stats.dev) === pinned.dev && exactFileId(stats.ino) === pinned.ino;
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+ function livePinnedPath(profile, pinned) {
462
+ if (isPinnedLive(pinned)) return pinned.path;
463
+ throw new ProfileSymlinkError(profile, "its pinned directory is no longer active or changed during use");
464
+ }
465
+ async function withPinnedProfileDirectory(profile, action, options = {}) {
466
+ const named = namedProfilePath(profile);
467
+ const existing = pinnedProfiles.getStore()?.get(named);
468
+ if (existing) return action(livePinnedPath(profile, existing));
469
+ if (process.platform !== "linux") {
470
+ if (options.create) await (0, import_promises.mkdir)(named, { recursive: true, mode: PRIVATE_DIR_MODE });
471
+ const stats = await (0, import_promises.lstat)(named).catch((error) => {
472
+ if (error.code === "ENOENT") return null;
473
+ throw error;
474
+ });
475
+ if (!stats) return action(null);
476
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new ProfileSymlinkError(profile);
477
+ return action(named);
478
+ }
479
+ const home = tinycloudHomePath();
480
+ const handles = [];
481
+ const fdPath = (fd) => `/proc/self/fd/${fd}`;
482
+ const flags = import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_DIRECTORY | import_node_fs.constants.O_NOFOLLOW;
483
+ try {
484
+ if (options.create) await (0, import_promises.mkdir)(home, { recursive: true, mode: PRIVATE_DIR_MODE });
485
+ const resolved = canonicalHomes.get(home) ?? await (0, import_promises.realpath)(home).catch((error) => {
486
+ if (error.code === "ENOENT" && !options.create) return null;
487
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
488
+ throw error;
489
+ });
490
+ if (!resolved) return await action(null);
491
+ const root = canonicalHomes.get(home) ?? resolved;
492
+ canonicalHomes.set(home, root);
493
+ const segments = [root, "profiles", validateProfileName(profile)];
494
+ for (let index = 0; index < segments.length; index++) {
495
+ const path = handles.length ? (0, import_node_path.join)(fdPath(handles[handles.length - 1].fd), segments[index]) : segments[index];
496
+ if (options.create && handles.length) {
497
+ await (0, import_promises.mkdir)(path, { mode: PRIVATE_DIR_MODE }).catch((error) => {
498
+ if (error.code !== "EEXIST") throw error;
499
+ });
500
+ }
501
+ const handle = await (0, import_promises.open)(path, index === 1 ? flags & ~import_node_fs.constants.O_NOFOLLOW : flags).catch((error) => {
502
+ if (error.code === "ENOENT" && !options.create) return null;
503
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
504
+ throw error;
505
+ });
506
+ if (!handle) return await action(null);
507
+ handles.push(handle);
508
+ if (options.create) await handle.chmod(PRIVATE_DIR_MODE);
509
+ }
510
+ const opened = handles[2];
511
+ const [actual, namedStats] = await Promise.all([opened.stat({ bigint: true }), (0, import_promises.lstat)(named, { bigint: true })]);
512
+ const dev = exactFileId(actual.dev);
513
+ const ino = exactFileId(actual.ino);
514
+ if (!namedStats.isDirectory() || dev === null || ino === null || dev !== exactFileId(namedStats.dev) || ino !== exactFileId(namedStats.ino)) {
515
+ throw new ProfileSymlinkError(profile, "changed while opening its directory");
516
+ }
517
+ const directory = fdPath(opened.fd);
518
+ const pinned = {
519
+ path: directory,
520
+ fd: opened.fd,
521
+ dev,
522
+ ino,
523
+ active: true
524
+ };
525
+ const homeHandle = handles[0];
526
+ const homeStats = await homeHandle.stat({ bigint: true });
527
+ const homeDev = exactFileId(homeStats.dev);
528
+ const homeIno = exactFileId(homeStats.ino);
529
+ if (homeDev === null || homeIno === null) throw new ProfileSymlinkError(profile, "has an unverified home directory identity");
530
+ const homePinned = {
531
+ path: fdPath(homeHandle.fd),
532
+ fd: homeHandle.fd,
533
+ dev: homeDev,
534
+ ino: homeIno,
535
+ active: true
536
+ };
537
+ const context = new Map(pinnedProfiles.getStore());
538
+ context.set(named, pinned);
539
+ const homes = new Map(pinnedHomes.getStore());
540
+ homes.set(home, homePinned);
541
+ try {
542
+ return await pinnedHomes.run(homes, () => pinnedProfiles.run(context, () => action(directory)));
543
+ } finally {
544
+ pinned.active = false;
545
+ homePinned.active = false;
546
+ }
547
+ } finally {
548
+ for (const handle of handles.reverse()) await handle.close();
549
+ }
550
+ }
551
+ async function assertRealProfileDirectory(profile) {
552
+ const named = namedProfilePath(profile);
553
+ const pinned = pinnedProfiles.getStore()?.get(named);
554
+ if (pinned) {
555
+ livePinnedPath(profile, pinned);
556
+ return;
557
+ }
558
+ const stats = await (0, import_promises.lstat)(named).catch((error) => {
559
+ if (error.code === "ENOENT") return null;
560
+ throw error;
561
+ });
562
+ if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) throw new ProfileSymlinkError(profile);
379
563
  }
380
564
  async function withProfileLock(profile, action, options = {}) {
381
565
  const normalizedProfile = validateProfileName(profile);
382
566
  const lockPath = profileLockPath(normalizedProfile);
383
- const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
384
- if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
385
- const acquired = await acquireProfileLock(normalizedProfile, options);
386
- const ownership = {
387
- lockPath,
388
- turnSlot: acquired.turnSlot,
389
- deletedWhileWaiting: acquired.deletedWhileWaiting,
390
- active: true
391
- };
392
- try {
393
- return await heldProfileLocks.run([...held, ownership], action);
394
- } finally {
395
- ownership.active = false;
396
- await acquired.release();
397
- if (acquired.deletedWhileWaiting) await (0, import_promises.rmdir)(profilePath(normalizedProfile)).catch(() => void 0);
567
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);
568
+ if (held.some((ownership) => ownership.lockPath === lockPath)) {
569
+ await assertRealProfileDirectory(normalizedProfile);
570
+ return action();
398
571
  }
572
+ const named = namedProfilePath(normalizedProfile);
573
+ const inherited = pinnedProfiles.getStore()?.get(named);
574
+ if (inherited && !isPinnedLive(inherited)) {
575
+ const profiles = new Map(pinnedProfiles.getStore());
576
+ profiles.delete(named);
577
+ const homes = new Map(pinnedHomes.getStore());
578
+ homes.delete(tinycloudHomePath());
579
+ return pinnedHomes.run(homes, () => pinnedProfiles.run(profiles, () => withProfileLock(normalizedProfile, action, options)));
580
+ }
581
+ return withPinnedProfileDirectory(normalizedProfile, async (directory) => {
582
+ if (!directory) throw new ProfileDeletedError(normalizedProfile);
583
+ const acquired = await acquireProfileLock(normalizedProfile, options);
584
+ const ownership = {
585
+ lockPath,
586
+ turnSlot: acquired.turnSlot,
587
+ deletedWhileWaiting: acquired.deletedWhileWaiting,
588
+ active: true
589
+ };
590
+ try {
591
+ return await heldProfileLocks.run([...held, ownership], action);
592
+ } finally {
593
+ ownership.active = false;
594
+ await acquired.release();
595
+ if (ownership.deletedProfile || acquired.deletedWhileWaiting) {
596
+ const current = await (0, import_promises.lstat)(namedProfilePath(normalizedProfile), { bigint: true }).catch(() => null);
597
+ const original = await (0, import_promises.stat)(directory, { bigint: true }).catch(() => null);
598
+ const currentDev = current && exactFileId(current.dev);
599
+ const currentIno = current && exactFileId(current.ino);
600
+ if (currentDev !== null && currentIno !== null && original && currentDev === exactFileId(original.dev) && currentIno === exactFileId(original.ino)) {
601
+ await (0, import_promises.rmdir)(namedProfilePath(normalizedProfile)).catch(() => void 0);
602
+ }
603
+ }
604
+ }
605
+ }, { create: !options.requireProfile });
399
606
  }
400
607
  async function recordProfileDeletion(profile) {
401
608
  const lockPath = profileLockPath(profile);
402
609
  const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
403
610
  if (!ownership) throw new Error(`Recording the deletion of profile "${profile}" requires holding its lock.`);
404
611
  await replaceJson((0, import_node_path.join)(profileTurnLockPath(profile), "deleted.json"), { slot: ownership.turnSlot }, true);
612
+ ownership.deletedProfile = true;
405
613
  }
406
614
  async function refuseWriteToDeletedProfile(profile) {
407
615
  const lockPath = profileLockPath(profile);
@@ -461,17 +669,15 @@ async function acquireProfileLock(profile, options) {
461
669
  retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
462
670
  };
463
671
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
464
- const lockPath = profileLockPath(profile);
672
+ const lockPath = (0, import_node_path.join)(profilePath(profile), ".lock");
465
673
  const turnDirectory = profileTurnLockPath(profile);
466
674
  const directory = profilePath(profile);
675
+ await assertRealProfileDirectory(profile);
467
676
  if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
468
677
  if (!options.requireProfile) await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
678
+ await assertRealProfileDirectory(profile);
469
679
  await (0, import_promises.mkdir)((0, import_node_path.dirname)(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
470
- for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
471
- await (0, import_promises.chmod)(path, PRIVATE_DIR_MODE).catch((error) => {
472
- if (path !== directory || !isErrno(error, "ENOENT")) throw error;
473
- });
474
- }
680
+ await (0, import_promises.chmod)(directory, PRIVATE_DIR_MODE);
475
681
  const liveness = { directory: turnDirectory, held: void 0 };
476
682
  const closeAcquisitionLiveness = async () => {
477
683
  if (liveness.held) await closeLiveness(liveness.held);
@@ -491,6 +697,7 @@ async function acquireProfileLock(profile, options) {
491
697
  }
492
698
  const deletion = await readJson((0, import_node_path.join)(turnDirectory, "deleted.json")).catch(() => null);
493
699
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
700
+ if (deletedWhileWaiting && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
494
701
  while (true) {
495
702
  const token = (0, import_node_crypto.randomUUID)();
496
703
  if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
@@ -527,8 +734,8 @@ async function waitOrTimeOut(profile, deadline, detail) {
527
734
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
528
735
  }
529
736
  var TURN = /^(?:0|[1-9][0-9]*)$/;
530
- var _a2;
531
- var relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
737
+ var _a3;
738
+ var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
532
739
  var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
533
740
  var SETTLE_RETRY_MS = 1e3;
534
741
  var SETTLE_RETRIES = 300;
@@ -642,7 +849,7 @@ async function takeTurn(profile, directory, latest, liveness) {
642
849
  if (published) await markTurn(path, token, "done");
643
850
  return null;
644
851
  } catch (error) {
645
- if (published) settleOwnTurn(directory, slot, token);
852
+ if (published) await settleOwnTurn(directory, slot, token);
646
853
  throw error;
647
854
  }
648
855
  }
@@ -653,17 +860,25 @@ async function settleAbandonedTurn(directory, abandoned) {
653
860
  }
654
861
  await markTurn(path, abandoned.token, "done");
655
862
  }
656
- function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
863
+ async function settleOwnTurn(directory, slot, token) {
657
864
  relinquishedTurns.add(token);
658
- void (async () => {
659
- try {
660
- const turn = await readTurn(directory, slot);
661
- if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
662
- relinquishedTurns.delete(token);
663
- } catch {
664
- if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
665
- }
666
- })();
865
+ const pinned = await pinDirectory(directory).catch(() => null);
866
+ const path = pinned?.path(".") ?? directory;
867
+ let retries = SETTLE_RETRIES;
868
+ const attempt = () => {
869
+ void (async () => {
870
+ try {
871
+ const turn = await readTurn(path, slot);
872
+ if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(path, turn);
873
+ relinquishedTurns.delete(token);
874
+ await pinned?.close();
875
+ } catch {
876
+ if (retries-- > 0) setTimeout(attempt, SETTLE_RETRY_MS).unref();
877
+ else await pinned?.close();
878
+ }
879
+ })();
880
+ };
881
+ attempt();
667
882
  }
668
883
  async function markTurn(path, token, kind) {
669
884
  for (let attempt = 0; ; attempt++) {
@@ -708,7 +923,7 @@ async function releaseTurn(profile, turn, liveness) {
708
923
  try {
709
924
  await markTurn((0, import_node_path.join)(turn.directory, String(turn.slot)), turn.token, "done");
710
925
  } catch {
711
- settleOwnTurn(turn.directory, turn.slot, turn.token);
926
+ await settleOwnTurn(turn.directory, turn.slot, turn.token);
712
927
  return;
713
928
  }
714
929
  await collectTurns(profile, turn, liveness).catch(() => void 0);
@@ -1001,8 +1216,11 @@ function sleep(milliseconds) {
1001
1216
  DEFAULT_PROFILE,
1002
1217
  ProfileDeletedError,
1003
1218
  ProfileLockTimeoutError,
1219
+ ProfileSymlinkError,
1004
1220
  additionalDelegationsPath,
1221
+ assertRealProfileDirectory,
1005
1222
  authRequestsPath,
1223
+ namedProfilePath,
1006
1224
  profileConfigPath,
1007
1225
  profileLockMetadataPath,
1008
1226
  profileLockPath,
@@ -1026,6 +1244,7 @@ function sleep(milliseconds) {
1026
1244
  tinycloudHomePath,
1027
1245
  updateProfileStore,
1028
1246
  upsertProfileRecord,
1247
+ withPinnedProfileDirectory,
1029
1248
  withProfileLock,
1030
1249
  withTinyCloudStateRoot,
1031
1250
  writeJsonAtomic,