@tinycloud/operations 0.6.1-beta.3 → 0.6.1-beta.5

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.d.ts CHANGED
@@ -43,6 +43,7 @@ declare function tinycloudHomePath(): string;
43
43
  declare function withTinyCloudStateRoot<T>(stateRoot: string | undefined, action: () => Promise<T>): Promise<T>;
44
44
  declare function tinycloudConfigPath(): string;
45
45
  declare function profilesPath(): string;
46
+ declare function namedProfilePath(profile: string): string;
46
47
  declare function profilePath(profile: string): string;
47
48
  declare function profileConfigPath(profile: string): string;
48
49
  declare function sessionPath(profile: string): string;
@@ -75,6 +76,16 @@ declare function readAdditionalDelegations<T = Record<string, unknown>>(profile:
75
76
  declare function readAuthRequests<T = Record<string, unknown>>(profile: string): Promise<T[]>;
76
77
  declare function readStoreMetadata(profile: string, store: ProfileStoreName): Promise<StoreMetadata>;
77
78
  declare function readProfileStore<T>(profile: string, store: Exclude<ProfileStoreName, "session">): Promise<ProfileStoreContents<T>>;
79
+ declare class ProfileSymlinkError extends Error {
80
+ readonly code = "PROFILE_SYMLINK";
81
+ constructor(profile: string, detail?: string);
82
+ }
83
+ /** Missing profiles remain missing; existing profiles are always opened without following aliases. */
84
+ declare function withPinnedProfileDirectory<T>(profile: string, action: (directory: string | null) => Promise<T>, options?: {
85
+ create?: boolean;
86
+ }): Promise<T>;
87
+ /** Missing profiles retain their existing read/create behavior; aliases never do. */
88
+ declare function assertRealProfileDirectory(profile: string): Promise<void>;
78
89
  /**
79
90
  * Runs a small critical section under the one lock shared by all profile
80
91
  * stores (see acquireProfileLock). Lock release verifies its ownership token
@@ -126,4 +137,4 @@ declare function updateProfileStore<T, Result>(profile: string, store: Exclude<P
126
137
  declare function writeSession<T extends object>(profile: string, session: T, options?: ProfileLockOptions): Promise<void>;
127
138
  declare function removeSession(profile: string, options?: ProfileLockOptions): Promise<void>;
128
139
 
129
- export { DEFAULT_PROFILE, ProfileDeletedError, type ProfileLockOptions, ProfileLockTimeoutError, type ProfileStoreContents, type ProfileStoreName, type StoreMetadata, additionalDelegationsPath, authRequestsPath, profileConfigPath, profileLockMetadataPath, profileLockPath, profilePath, profileStoreMetadataPath, profileStorePath, profileTurnLockPath, profilesPath, readAdditionalDelegations, readAuthRequests, readJson, readProfile, readProfileStore, readSession, readStoreMetadata, recordProfileDeletion, refuseWriteToDeletedProfile, removeSession, sessionPath, tinycloudConfigPath, tinycloudHomePath, updateProfileStore, upsertProfileRecord, withProfileLock, withTinyCloudStateRoot, writeJsonAtomic, writeSession };
140
+ export { DEFAULT_PROFILE, ProfileDeletedError, type ProfileLockOptions, ProfileLockTimeoutError, type ProfileStoreContents, type ProfileStoreName, ProfileSymlinkError, type StoreMetadata, additionalDelegationsPath, assertRealProfileDirectory, authRequestsPath, namedProfilePath, profileConfigPath, profileLockMetadataPath, profileLockPath, profilePath, profileStoreMetadataPath, profileStorePath, profileTurnLockPath, profilesPath, readAdditionalDelegations, readAuthRequests, readJson, readProfile, readProfileStore, readSession, readStoreMetadata, recordProfileDeletion, refuseWriteToDeletedProfile, removeSession, sessionPath, tinycloudConfigPath, tinycloudHomePath, updateProfileStore, upsertProfileRecord, withPinnedProfileDirectory, withProfileLock, withTinyCloudStateRoot, writeJsonAtomic, writeSession };
package/dist/state.js CHANGED
@@ -1,15 +1,16 @@
1
1
  // src/state.ts
2
2
  import { randomUUID } from "crypto";
3
3
  import { AsyncLocalStorage } from "async_hooks";
4
- import { readFileSync, readlinkSync } from "fs";
4
+ import { constants as fsConstants, fstatSync, readFileSync, readlinkSync } from "fs";
5
5
  import {
6
6
  chmod,
7
7
  link,
8
- lstat,
9
8
  mkdir,
9
+ lstat,
10
10
  open,
11
11
  readFile,
12
12
  readdir,
13
+ realpath,
13
14
  rename,
14
15
  rmdir,
15
16
  rm,
@@ -40,14 +41,18 @@ var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIE
40
41
  var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
41
42
  var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
42
43
  function processWideContext(key) {
43
- var _a3;
44
+ var _a4;
44
45
  const registry = globalThis;
45
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new AsyncLocalStorage());
46
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
46
47
  }
47
48
  var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
48
49
  var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
50
+ var pinnedProfiles = processWideContext("tinycloud.operations.pinnedProfiles.v2");
51
+ var pinnedHomes = processWideContext("tinycloud.operations.pinnedHomes.v2");
49
52
  var _a;
50
- var processInstanceId = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a] = randomUUID());
53
+ var canonicalHomes = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.canonicalHomes.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Map());
54
+ var _a2;
55
+ var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
51
56
  var cachedProcessOrigin;
52
57
  function processOrigin() {
53
58
  if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
@@ -235,9 +240,14 @@ function tinycloudConfigPath() {
235
240
  function profilesPath() {
236
241
  return join(tinycloudHomePath(), "profiles");
237
242
  }
238
- function profilePath(profile) {
243
+ function namedProfilePath(profile) {
239
244
  return join(profilesPath(), validateProfileName(profile));
240
245
  }
246
+ function profilePath(profile) {
247
+ const named = namedProfilePath(profile);
248
+ const pinned = pinnedProfiles.getStore()?.get(named);
249
+ return pinned ? livePinnedPath(profile, pinned) : named;
250
+ }
241
251
  function profileConfigPath(profile) {
242
252
  return join(profilePath(profile), "profile.json");
243
253
  }
@@ -257,13 +267,15 @@ function profileStoreMetadataPath(profile, store) {
257
267
  return `${profileStorePath(profile, store)}.metadata.json`;
258
268
  }
259
269
  function profileLockPath(profile) {
260
- return join(profilePath(profile), ".lock");
270
+ return join(namedProfilePath(profile), ".lock");
261
271
  }
262
272
  function profileLockMetadataPath(profile) {
263
273
  return join(profileLockPath(profile), "owner.json");
264
274
  }
265
275
  function profileTurnLockPath(profile) {
266
- return join(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
276
+ const home = tinycloudHomePath();
277
+ const pinned = pinnedHomes.getStore()?.get(home);
278
+ return join(pinned ? livePinnedPath(profile, pinned) : home, "profile-locks", validateProfileName(profile));
267
279
  }
268
280
  async function readJson(filePath) {
269
281
  try {
@@ -273,6 +285,44 @@ async function readJson(filePath) {
273
285
  throw error;
274
286
  }
275
287
  }
288
+ async function readProfileJson(profile, filePath) {
289
+ const before = process.platform !== "linux" ? await lstat(filePath, { bigint: true }).catch((error) => {
290
+ if (error.code === "ENOENT") return null;
291
+ throw error;
292
+ }) : null;
293
+ if (process.platform !== "linux" && !before) return null;
294
+ if (before && !before.isFile()) throw new ProfileSymlinkError(profile);
295
+ const dev = before && exactFileId(before.dev);
296
+ const ino = before && exactFileId(before.ino);
297
+ try {
298
+ const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
299
+ try {
300
+ if (before) {
301
+ const opened = await handle.stat({ bigint: true });
302
+ if (!opened.isFile() || dev === null || ino === null || dev !== exactFileId(opened.dev) || ino !== exactFileId(opened.ino)) {
303
+ throw new ProfileSymlinkError(profile, "changed while opening its file");
304
+ }
305
+ }
306
+ const contents = await handle.readFile("utf8");
307
+ if (before) {
308
+ const named = await lstat(filePath, { bigint: true }).catch((error) => {
309
+ if (error.code === "ENOENT") return null;
310
+ throw error;
311
+ });
312
+ if (!named?.isFile() || dev !== exactFileId(named.dev) || ino !== exactFileId(named.ino)) {
313
+ throw new ProfileSymlinkError(profile, "changed while reading its file");
314
+ }
315
+ }
316
+ return JSON.parse(contents);
317
+ } finally {
318
+ await handle.close();
319
+ }
320
+ } catch (error) {
321
+ if (isErrno(error, "ENOENT")) return null;
322
+ if (isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR")) throw new ProfileSymlinkError(profile);
323
+ throw error;
324
+ }
325
+ }
276
326
  async function writeJsonAtomic(filePath, value) {
277
327
  await replaceJson(filePath, value, false);
278
328
  }
@@ -306,11 +356,14 @@ async function writeFileDurably(path, contents) {
306
356
  }
307
357
  }
308
358
  async function readProfile(profile) {
309
- return readJson(profileConfigPath(profile));
359
+ return withPinnedProfileDirectory(profile, (directory) => directory ? readProfileJson(profile, join(directory, "profile.json")) : Promise.resolve(null));
310
360
  }
311
361
  async function readSession(profile) {
312
- await readStoreMetadata(profile, "session");
313
- return readJson(sessionPath(profile));
362
+ return withPinnedProfileDirectory(profile, async (directory) => {
363
+ if (!directory) return null;
364
+ await readStoreMetadata(profile, "session");
365
+ return readProfileJson(profile, sessionPath(profile));
366
+ });
314
367
  }
315
368
  async function readAdditionalDelegations(profile) {
316
369
  return (await readProfileStore(profile, "additional-delegations")).records;
@@ -319,48 +372,200 @@ async function readAuthRequests(profile) {
319
372
  return (await readProfileStore(profile, "auth-requests")).records;
320
373
  }
321
374
  async function readStoreMetadata(profile, store) {
322
- const metadata = await readJson(profileStoreMetadataPath(profile, store));
323
- if (metadata === null) return { formatVersion: 1 };
324
- if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
325
- throw new TypeError(`Unsupported store format for "${store}".`);
326
- }
327
- return metadata;
375
+ return withPinnedProfileDirectory(profile, async (directory) => {
376
+ const metadata = directory ? await readProfileJson(profile, profileStoreMetadataPath(profile, store)) : null;
377
+ if (metadata === null) return { formatVersion: 1 };
378
+ if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
379
+ throw new TypeError(`Unsupported store format for "${store}".`);
380
+ }
381
+ return metadata;
382
+ });
328
383
  }
329
384
  async function readProfileStore(profile, store) {
330
- const [metadata, raw] = await Promise.all([
331
- readStoreMetadata(profile, store),
332
- readJson(profileStorePath(profile, store))
333
- ]);
334
- return {
335
- formatVersion: metadata.formatVersion,
336
- records: Array.isArray(raw) ? raw : []
337
- };
385
+ return withPinnedProfileDirectory(profile, async (directory) => {
386
+ if (!directory) return { formatVersion: 1, records: [] };
387
+ const [metadata, raw] = await Promise.all([
388
+ readStoreMetadata(profile, store),
389
+ readProfileJson(profile, profileStorePath(profile, store))
390
+ ]);
391
+ return {
392
+ formatVersion: metadata.formatVersion,
393
+ records: Array.isArray(raw) ? raw : []
394
+ };
395
+ });
396
+ }
397
+ var ProfileSymlinkError = class extends Error {
398
+ constructor(profile, detail = "must be a real directory, not a symlink or another file type") {
399
+ super(`Profile "${profile}" ${detail}.`);
400
+ this.code = "PROFILE_SYMLINK";
401
+ this.name = "ProfileSymlinkError";
402
+ }
403
+ };
404
+ function exactFileId(value) {
405
+ if (typeof value === "bigint") return value >= 0n ? value : null;
406
+ return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
407
+ }
408
+ function isPinnedLive(pinned) {
409
+ if (!pinned.active) return false;
410
+ try {
411
+ const stats = fstatSync(pinned.fd, { bigint: true });
412
+ return exactFileId(stats.dev) === pinned.dev && exactFileId(stats.ino) === pinned.ino;
413
+ } catch {
414
+ return false;
415
+ }
416
+ }
417
+ function livePinnedPath(profile, pinned) {
418
+ if (isPinnedLive(pinned)) return pinned.path;
419
+ throw new ProfileSymlinkError(profile, "its pinned directory is no longer active or changed during use");
420
+ }
421
+ async function withPinnedProfileDirectory(profile, action, options = {}) {
422
+ const named = namedProfilePath(profile);
423
+ const existing = pinnedProfiles.getStore()?.get(named);
424
+ if (existing) return action(livePinnedPath(profile, existing));
425
+ if (process.platform !== "linux") {
426
+ if (options.create) await mkdir(named, { recursive: true, mode: PRIVATE_DIR_MODE });
427
+ const stats = await lstat(named).catch((error) => {
428
+ if (error.code === "ENOENT") return null;
429
+ throw error;
430
+ });
431
+ if (!stats) return action(null);
432
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new ProfileSymlinkError(profile);
433
+ return action(named);
434
+ }
435
+ const home = tinycloudHomePath();
436
+ const handles = [];
437
+ const fdPath = (fd) => `/proc/self/fd/${fd}`;
438
+ const flags = fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW;
439
+ try {
440
+ if (options.create) await mkdir(home, { recursive: true, mode: PRIVATE_DIR_MODE });
441
+ const resolved = canonicalHomes.get(home) ?? await realpath(home).catch((error) => {
442
+ if (error.code === "ENOENT" && !options.create) return null;
443
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
444
+ throw error;
445
+ });
446
+ if (!resolved) return await action(null);
447
+ const root = canonicalHomes.get(home) ?? resolved;
448
+ canonicalHomes.set(home, root);
449
+ const segments = [root, "profiles", validateProfileName(profile)];
450
+ for (let index = 0; index < segments.length; index++) {
451
+ const path = handles.length ? join(fdPath(handles[handles.length - 1].fd), segments[index]) : segments[index];
452
+ if (options.create && handles.length) {
453
+ await mkdir(path, { mode: PRIVATE_DIR_MODE }).catch((error) => {
454
+ if (error.code !== "EEXIST") throw error;
455
+ });
456
+ }
457
+ const handle = await open(path, index === 1 ? flags & ~fsConstants.O_NOFOLLOW : flags).catch((error) => {
458
+ if (error.code === "ENOENT" && !options.create) return null;
459
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
460
+ throw error;
461
+ });
462
+ if (!handle) return await action(null);
463
+ handles.push(handle);
464
+ if (options.create) await handle.chmod(PRIVATE_DIR_MODE);
465
+ }
466
+ const opened = handles[2];
467
+ const [actual, namedStats] = await Promise.all([opened.stat({ bigint: true }), lstat(named, { bigint: true })]);
468
+ const dev = exactFileId(actual.dev);
469
+ const ino = exactFileId(actual.ino);
470
+ if (!namedStats.isDirectory() || dev === null || ino === null || dev !== exactFileId(namedStats.dev) || ino !== exactFileId(namedStats.ino)) {
471
+ throw new ProfileSymlinkError(profile, "changed while opening its directory");
472
+ }
473
+ const directory = fdPath(opened.fd);
474
+ const pinned = {
475
+ path: directory,
476
+ fd: opened.fd,
477
+ dev,
478
+ ino,
479
+ active: true
480
+ };
481
+ const homeHandle = handles[0];
482
+ const homeStats = await homeHandle.stat({ bigint: true });
483
+ const homeDev = exactFileId(homeStats.dev);
484
+ const homeIno = exactFileId(homeStats.ino);
485
+ if (homeDev === null || homeIno === null) throw new ProfileSymlinkError(profile, "has an unverified home directory identity");
486
+ const homePinned = {
487
+ path: fdPath(homeHandle.fd),
488
+ fd: homeHandle.fd,
489
+ dev: homeDev,
490
+ ino: homeIno,
491
+ active: true
492
+ };
493
+ const context = new Map(pinnedProfiles.getStore());
494
+ context.set(named, pinned);
495
+ const homes = new Map(pinnedHomes.getStore());
496
+ homes.set(home, homePinned);
497
+ try {
498
+ return await pinnedHomes.run(homes, () => pinnedProfiles.run(context, () => action(directory)));
499
+ } finally {
500
+ pinned.active = false;
501
+ homePinned.active = false;
502
+ }
503
+ } finally {
504
+ for (const handle of handles.reverse()) await handle.close();
505
+ }
506
+ }
507
+ async function assertRealProfileDirectory(profile) {
508
+ const named = namedProfilePath(profile);
509
+ const pinned = pinnedProfiles.getStore()?.get(named);
510
+ if (pinned) {
511
+ livePinnedPath(profile, pinned);
512
+ return;
513
+ }
514
+ const stats = await lstat(named).catch((error) => {
515
+ if (error.code === "ENOENT") return null;
516
+ throw error;
517
+ });
518
+ if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) throw new ProfileSymlinkError(profile);
338
519
  }
339
520
  async function withProfileLock(profile, action, options = {}) {
340
521
  const normalizedProfile = validateProfileName(profile);
341
522
  const lockPath = profileLockPath(normalizedProfile);
342
- const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
343
- if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
344
- const acquired = await acquireProfileLock(normalizedProfile, options);
345
- const ownership = {
346
- lockPath,
347
- turnSlot: acquired.turnSlot,
348
- deletedWhileWaiting: acquired.deletedWhileWaiting,
349
- active: true
350
- };
351
- try {
352
- return await heldProfileLocks.run([...held, ownership], action);
353
- } finally {
354
- ownership.active = false;
355
- await acquired.release();
356
- if (acquired.deletedWhileWaiting) await rmdir(profilePath(normalizedProfile)).catch(() => void 0);
523
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);
524
+ if (held.some((ownership) => ownership.lockPath === lockPath)) {
525
+ await assertRealProfileDirectory(normalizedProfile);
526
+ return action();
527
+ }
528
+ const named = namedProfilePath(normalizedProfile);
529
+ const inherited = pinnedProfiles.getStore()?.get(named);
530
+ if (inherited && !isPinnedLive(inherited)) {
531
+ const profiles = new Map(pinnedProfiles.getStore());
532
+ profiles.delete(named);
533
+ const homes = new Map(pinnedHomes.getStore());
534
+ homes.delete(tinycloudHomePath());
535
+ return pinnedHomes.run(homes, () => pinnedProfiles.run(profiles, () => withProfileLock(normalizedProfile, action, options)));
357
536
  }
537
+ return withPinnedProfileDirectory(normalizedProfile, async (directory) => {
538
+ if (!directory) throw new ProfileDeletedError(normalizedProfile);
539
+ const acquired = await acquireProfileLock(normalizedProfile, options);
540
+ const ownership = {
541
+ lockPath,
542
+ turnSlot: acquired.turnSlot,
543
+ deletedWhileWaiting: acquired.deletedWhileWaiting,
544
+ active: true
545
+ };
546
+ try {
547
+ return await heldProfileLocks.run([...held, ownership], action);
548
+ } finally {
549
+ ownership.active = false;
550
+ await acquired.release();
551
+ if (ownership.deletedProfile || acquired.deletedWhileWaiting) {
552
+ const current = await lstat(namedProfilePath(normalizedProfile), { bigint: true }).catch(() => null);
553
+ const original = await stat(directory, { bigint: true }).catch(() => null);
554
+ const currentDev = current && exactFileId(current.dev);
555
+ const currentIno = current && exactFileId(current.ino);
556
+ if (currentDev !== null && currentIno !== null && original && currentDev === exactFileId(original.dev) && currentIno === exactFileId(original.ino)) {
557
+ await rmdir(namedProfilePath(normalizedProfile)).catch(() => void 0);
558
+ }
559
+ }
560
+ }
561
+ }, { create: !options.requireProfile });
358
562
  }
359
563
  async function recordProfileDeletion(profile) {
360
564
  const lockPath = profileLockPath(profile);
361
565
  const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
362
566
  if (!ownership) throw new Error(`Recording the deletion of profile "${profile}" requires holding its lock.`);
363
567
  await replaceJson(join(profileTurnLockPath(profile), "deleted.json"), { slot: ownership.turnSlot }, true);
568
+ ownership.deletedProfile = true;
364
569
  }
365
570
  async function refuseWriteToDeletedProfile(profile) {
366
571
  const lockPath = profileLockPath(profile);
@@ -420,17 +625,15 @@ async function acquireProfileLock(profile, options) {
420
625
  retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
421
626
  };
422
627
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
423
- const lockPath = profileLockPath(profile);
628
+ const lockPath = join(profilePath(profile), ".lock");
424
629
  const turnDirectory = profileTurnLockPath(profile);
425
630
  const directory = profilePath(profile);
631
+ await assertRealProfileDirectory(profile);
426
632
  if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
427
633
  if (!options.requireProfile) await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
634
+ await assertRealProfileDirectory(profile);
428
635
  await mkdir(dirname(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
429
- for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
430
- await chmod(path, PRIVATE_DIR_MODE).catch((error) => {
431
- if (path !== directory || !isErrno(error, "ENOENT")) throw error;
432
- });
433
- }
636
+ await chmod(directory, PRIVATE_DIR_MODE);
434
637
  const liveness = { directory: turnDirectory, held: void 0 };
435
638
  const closeAcquisitionLiveness = async () => {
436
639
  if (liveness.held) await closeLiveness(liveness.held);
@@ -450,6 +653,7 @@ async function acquireProfileLock(profile, options) {
450
653
  }
451
654
  const deletion = await readJson(join(turnDirectory, "deleted.json")).catch(() => null);
452
655
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
656
+ if (deletedWhileWaiting && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
453
657
  while (true) {
454
658
  const token = randomUUID();
455
659
  if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
@@ -486,8 +690,8 @@ async function waitOrTimeOut(profile, deadline, detail) {
486
690
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
487
691
  }
488
692
  var TURN = /^(?:0|[1-9][0-9]*)$/;
489
- var _a2;
490
- var relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
693
+ var _a3;
694
+ var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
491
695
  var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
492
696
  var SETTLE_RETRY_MS = 1e3;
493
697
  var SETTLE_RETRIES = 300;
@@ -601,7 +805,7 @@ async function takeTurn(profile, directory, latest, liveness) {
601
805
  if (published) await markTurn(path, token, "done");
602
806
  return null;
603
807
  } catch (error) {
604
- if (published) settleOwnTurn(directory, slot, token);
808
+ if (published) await settleOwnTurn(directory, slot, token);
605
809
  throw error;
606
810
  }
607
811
  }
@@ -612,17 +816,25 @@ async function settleAbandonedTurn(directory, abandoned) {
612
816
  }
613
817
  await markTurn(path, abandoned.token, "done");
614
818
  }
615
- function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
819
+ async function settleOwnTurn(directory, slot, token) {
616
820
  relinquishedTurns.add(token);
617
- void (async () => {
618
- try {
619
- const turn = await readTurn(directory, slot);
620
- if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
621
- relinquishedTurns.delete(token);
622
- } catch {
623
- if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
624
- }
625
- })();
821
+ const pinned = await pinDirectory(directory).catch(() => null);
822
+ const path = pinned?.path(".") ?? directory;
823
+ let retries = SETTLE_RETRIES;
824
+ const attempt = () => {
825
+ void (async () => {
826
+ try {
827
+ const turn = await readTurn(path, slot);
828
+ if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(path, turn);
829
+ relinquishedTurns.delete(token);
830
+ await pinned?.close();
831
+ } catch {
832
+ if (retries-- > 0) setTimeout(attempt, SETTLE_RETRY_MS).unref();
833
+ else await pinned?.close();
834
+ }
835
+ })();
836
+ };
837
+ attempt();
626
838
  }
627
839
  async function markTurn(path, token, kind) {
628
840
  for (let attempt = 0; ; attempt++) {
@@ -667,7 +879,7 @@ async function releaseTurn(profile, turn, liveness) {
667
879
  try {
668
880
  await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
669
881
  } catch {
670
- settleOwnTurn(turn.directory, turn.slot, turn.token);
882
+ await settleOwnTurn(turn.directory, turn.slot, turn.token);
671
883
  return;
672
884
  }
673
885
  await collectTurns(profile, turn, liveness).catch(() => void 0);
@@ -959,8 +1171,11 @@ export {
959
1171
  DEFAULT_PROFILE,
960
1172
  ProfileDeletedError,
961
1173
  ProfileLockTimeoutError,
1174
+ ProfileSymlinkError,
962
1175
  additionalDelegationsPath,
1176
+ assertRealProfileDirectory,
963
1177
  authRequestsPath,
1178
+ namedProfilePath,
964
1179
  profileConfigPath,
965
1180
  profileLockMetadataPath,
966
1181
  profileLockPath,
@@ -984,6 +1199,7 @@ export {
984
1199
  tinycloudHomePath,
985
1200
  updateProfileStore,
986
1201
  upsertProfileRecord,
1202
+ withPinnedProfileDirectory,
987
1203
  withProfileLock,
988
1204
  withTinyCloudStateRoot,
989
1205
  writeJsonAtomic,