@tinycloud/operations 0.3.4-beta.5 → 0.4.0-beta.7

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/state.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport {\n chmod,\n link,\n mkdir,\n readFile,\n readdir,\n rename,\n rmdir,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, relative, resolve } from \"node:path\";\n\nconst DEFAULT_PROFILE = \"default\";\nconst DEFAULT_LOCK_TIMEOUT_MS = 2_000;\nconst DEFAULT_LOCK_RETRY_MS = 25;\nconst DEFAULT_STALE_LOCK_MS = 30_000;\nconst TEST_LOCK_CONTENTION_SIGNAL_PATH = \"TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH\";\n// Profile state holds keys, sessions and delegations: owner-only access.\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIR_MODE = 0o700;\nconst TEST_LOCK_RECOVERY_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR\";\nconst TEST_LOCK_OWNERLESS_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR\";\nconst TEST_LOCK_PUBLISH_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR\";\nconst TEST_LOCK_RELEASE_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR\";\nconst TEST_LOCK_CLAIM_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR\";\nconst TEST_LOCK_CLAIMED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR\";\nconst TEST_LOCK_FENCED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR\";\nconst TEST_LOCK_VERIFIED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR\";\nconst invocationStateRoot = new AsyncLocalStorage<string>();\n/** One acquisition of a profile lock; `active` is cleared before release. */\ninterface HeldProfileLock {\n readonly lockPath: string;\n active: boolean;\n}\n/** Profile lock acquisitions held by the current async call chain (see withProfileLock). */\nconst heldProfileLocks = new AsyncLocalStorage<readonly HeldProfileLock[]>();\n\nexport type ProfileStoreName =\n | \"session\"\n | \"additional-delegations\"\n | \"auth-requests\";\n\nexport interface StoreMetadata {\n formatVersion: number;\n}\n\nexport interface ProfileStoreContents<T> {\n formatVersion: number;\n records: T[];\n}\n\nexport interface ProfileLockOptions {\n timeoutMs?: number;\n retryMs?: number;\n staleAfterMs?: number;\n}\n\nexport class ProfileLockTimeoutError extends Error {\n readonly code = \"PROFILE_LOCK_TIMEOUT\";\n\n constructor(profile: string, timeoutMs: number) {\n super(`Timed out waiting for the profile lock for \"${profile}\" after ${timeoutMs}ms.`);\n this.name = \"ProfileLockTimeoutError\";\n }\n}\n\n/**\n * The CLI's delegated-secret path treats TC_HOME as a home directory, rather\n * than a direct TinyCloud directory. Keep the shared store on that convention.\n */\nexport function tinycloudHomePath(): string {\n const home = invocationStateRoot.getStore() ??\n process.env.TC_HOME ??\n process.env.HOME ??\n process.env.USERPROFILE ??\n homedir();\n return join(home, \".tinycloud\");\n}\n\n/** Run one operation against an isolated TinyCloud home directory. */\nexport function withTinyCloudStateRoot<T>(\n stateRoot: string | undefined,\n action: () => Promise<T>,\n): Promise<T> {\n if (stateRoot === undefined) return action();\n if (!isAbsolute(stateRoot) || stateRoot.includes(\"\\0\")) {\n throw new TypeError(\"The TinyCloud state root must be an absolute path.\");\n }\n return invocationStateRoot.run(resolve(stateRoot), action);\n}\n\nexport function tinycloudConfigPath(): string {\n return join(tinycloudHomePath(), \"config.json\");\n}\n\nexport function profilesPath(): string {\n return join(tinycloudHomePath(), \"profiles\");\n}\n\nexport function profilePath(profile: string): string {\n return join(profilesPath(), validateProfileName(profile));\n}\n\nexport function profileConfigPath(profile: string): string {\n return join(profilePath(profile), \"profile.json\");\n}\n\nexport function sessionPath(profile: string): string {\n return profileStorePath(profile, \"session\");\n}\n\nexport function additionalDelegationsPath(profile: string): string {\n return profileStorePath(profile, \"additional-delegations\");\n}\n\nexport function authRequestsPath(profile: string): string {\n return profileStorePath(profile, \"auth-requests\");\n}\n\nexport function profileStorePath(profile: string, store: ProfileStoreName): string {\n return join(profilePath(profile), `${store}.json`);\n}\n\n/** The format record is deliberately separate from each legacy JSON payload. */\nexport function profileStoreMetadataPath(profile: string, store: ProfileStoreName): string {\n return `${profileStorePath(profile, store)}.metadata.json`;\n}\n\nexport function profileLockPath(profile: string): string {\n return join(profilePath(profile), \".lock\");\n}\n\nexport function profileLockMetadataPath(profile: string): string {\n return join(profileLockPath(profile), \"owner.json\");\n}\n\n/**\n * Reads JSON without hiding malformed or inaccessible files. A missing file is\n * the only condition represented as null.\n */\nexport async function readJson<T>(filePath: string): Promise<T | null> {\n try {\n return JSON.parse(await readFile(filePath, \"utf8\")) as T;\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return null;\n throw error;\n }\n}\n\n/**\n * Writes the same pretty-printed, trailing-newline JSON shape used by the CLI,\n * but publishes it with an atomic rename from the same directory.\n */\nexport async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n const directory = dirname(filePath);\n const temporaryPath = join(\n directory,\n `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`,\n );\n const contents = `${JSON.stringify(value, null, 2)}\\n`;\n\n await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });\n try {\n await writeFile(temporaryPath, contents, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE });\n await rename(temporaryPath, filePath);\n } catch (error) {\n await rm(temporaryPath, { force: true }).catch(() => undefined);\n throw error;\n }\n}\n\nexport async function readProfile<T extends object = Record<string, unknown>>(\n profile: string,\n): Promise<T | null> {\n return readJson<T>(profileConfigPath(profile));\n}\n\nexport async function readSession<T extends object = Record<string, unknown>>(\n profile: string,\n): Promise<T | null> {\n await readStoreMetadata(profile, \"session\");\n return readJson<T>(sessionPath(profile));\n}\n\nexport async function readAdditionalDelegations<T = Record<string, unknown>>(\n profile: string,\n): Promise<T[]> {\n return (await readProfileStore<T>(profile, \"additional-delegations\")).records;\n}\n\nexport async function readAuthRequests<T = Record<string, unknown>>(\n profile: string,\n): Promise<T[]> {\n return (await readProfileStore<T>(profile, \"auth-requests\")).records;\n}\n\nexport async function readStoreMetadata(\n profile: string,\n store: ProfileStoreName,\n): Promise<StoreMetadata> {\n const metadata = await readJson<StoreMetadata>(profileStoreMetadataPath(profile, store));\n if (metadata === null) return { formatVersion: 1 };\n if (\n typeof metadata !== \"object\" ||\n metadata.formatVersion !== 1\n ) {\n throw new TypeError(`Unsupported store format for \"${store}\".`);\n }\n return metadata;\n}\n\nexport async function readProfileStore<T>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n): Promise<ProfileStoreContents<T>> {\n const [metadata, raw] = await Promise.all([\n readStoreMetadata(profile, store),\n readJson<unknown>(profileStorePath(profile, store)),\n ]);\n return {\n formatVersion: metadata.formatVersion,\n records: Array.isArray(raw) ? raw as T[] : [],\n };\n}\n\n/**\n * Runs a small critical section under the one advisory lock shared by all\n * profile stores. Lock release verifies its ownership token before removing\n * the exact metadata instance it acquired.\n *\n * Reentrant within one async call chain: a critical section that already\n * holds a profile's lock (for example a login's compare-and-commit) can call\n * store writers that take the same lock without deadlocking. Ownership is\n * keyed by the resolved lock path (so switching state roots inside a lock\n * still takes the other root's lock) and lasts exactly as long as the\n * acquisition: it is revoked before release, so work deferred past the\n * critical section waits for the lock like any other caller.\n */\nexport async function withProfileLock<T>(\n profile: string,\n action: () => Promise<T>,\n options: ProfileLockOptions = {},\n): Promise<T> {\n const normalizedProfile = validateProfileName(profile);\n const lockPath = profileLockPath(normalizedProfile);\n const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);\n if (held.some((ownership) => ownership.lockPath === lockPath)) return action();\n const release = await acquireProfileLock(normalizedProfile, options);\n const ownership: HeldProfileLock = { lockPath, active: true };\n try {\n return await heldProfileLocks.run([...held, ownership], action);\n } finally {\n ownership.active = false;\n await release();\n }\n}\n\n/**\n * Appends a record or replaces the existing record with the supplied explicit\n * key. The extractor is supplied by the owning store so state.ts never needs\n * to know permission-request or delegation record shapes.\n */\nexport async function upsertProfileRecord<T>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n key: string,\n record: T,\n getKey: (candidate: T) => string | undefined,\n options: ProfileLockOptions = {},\n): Promise<T[]> {\n if (!key) throw new TypeError(\"A non-empty record key is required.\");\n\n return withProfileLock(profile, async () => {\n const current = (await readProfileStore<T>(profile, store)).records;\n const next = current.filter((candidate) => getKey(candidate) !== key);\n next.push(record);\n await writeJsonAtomic(profileStorePath(profile, store), next);\n await writeFormatOneMetadata(profile, store);\n return next;\n }, options);\n}\n\n/**\n * Performs a typed read-modify-write of a legacy array store under the one\n * per-profile lock. The payload stays an array and the sibling metadata stays\n * format 1, so existing CLI readers keep their byte/layout contract.\n */\nexport async function updateProfileStore<T, Result>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n update: (\n records: readonly T[],\n ) => Promise<{ readonly records: readonly T[]; readonly result: Result }> | {\n readonly records: readonly T[];\n readonly result: Result;\n },\n options: ProfileLockOptions = {},\n): Promise<Result> {\n return withProfileLock(\n profile,\n () => updateProfileStoreWhileLocked(profile, store, update),\n options,\n );\n}\n\n/** The caller must already own this profile's lock. */\nasync function updateProfileStoreWhileLocked<T, Result>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n update: (\n records: readonly T[],\n ) => Promise<{ readonly records: readonly T[]; readonly result: Result }> | {\n readonly records: readonly T[];\n readonly result: Result;\n },\n): Promise<Result> {\n const current = await readProfileStore<T>(profile, store);\n const next = await update(current.records);\n await writeJsonAtomic(profileStorePath(profile, store), next.records);\n await writeFormatOneMetadata(profile, store);\n return next.result;\n}\n\nexport async function writeSession<T extends object>(\n profile: string,\n session: T,\n options: ProfileLockOptions = {},\n): Promise<void> {\n await withProfileLock(profile, async () => {\n await readStoreMetadata(profile, \"session\");\n await writeJsonAtomic(sessionPath(profile), session);\n await writeFormatOneMetadata(profile, \"session\");\n }, options);\n}\n\nexport async function removeSession(\n profile: string,\n options: ProfileLockOptions = {},\n): Promise<void> {\n await withProfileLock(profile, async () => {\n await readStoreMetadata(profile, \"session\");\n await rm(sessionPath(profile), { force: true });\n }, options);\n}\n\nasync function writeFormatOneMetadata(profile: string, store: ProfileStoreName): Promise<void> {\n await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });\n}\n\nasync function acquireProfileLock(\n profile: string,\n options: ProfileLockOptions,\n): Promise<() => Promise<void>> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n const retryMs = options.retryMs ?? DEFAULT_LOCK_RETRY_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;\n const startedAt = Date.now();\n const lockPath = profileLockPath(profile);\n\n const directory = profilePath(profile);\n await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });\n // Every store write takes this lock: tighten the tree older releases\n // created 0775 before writing sessions or delegations into it.\n for (const path of [tinycloudHomePath(), profilesPath(), directory]) {\n await chmod(path, PRIVATE_DIR_MODE).catch((error: unknown) => {\n // A profile deletion can remove the profile directory right after the\n // mkdir above; the lock attempt below recreates it 0700.\n if (path !== directory || !isErrno(error, \"ENOENT\")) throw error;\n });\n }\n\n while (true) {\n const token = randomUUID();\n if (await publishProfileLock(profile, lockPath, token)) {\n return async () => {\n await releaseProfileLock(profile, lockPath, token);\n };\n }\n await signalTestLockContention(profile);\n\n if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;\n\n const elapsedMs = Date.now() - startedAt;\n if (elapsedMs >= timeoutMs) {\n throw new ProfileLockTimeoutError(profile, timeoutMs);\n }\n await sleep(Math.min(retryMs, timeoutMs - elapsedMs));\n }\n}\n\n/**\n * One attempt to take the lock. Nothing here replaces an existing path:\n *\n * 1. `mkdir(.lock)` (never recursive) is exclusive, and is the primitive\n * older releases use too, so they exclude each other.\n * 2. Ownership is published by `link`ing a fully written owner file (staged\n * beside `.lock`, on the same filesystem) to `.lock/owner.json`. `link`\n * fails with EEXIST if an owner exists, and with ENOENT if the directory\n * was removed meanwhile (an aged ownerless-lock reclaim). Only a\n * successful link holds the lock; on either failure nothing is released,\n * since nothing was acquired, and the caller retries.\n *\n * A profile deletion removes the profile directory once it has released its\n * lock, so `mkdir(.lock)` can see ENOENT; the directory is recreated (0700)\n * and the caller retries within its deadline, as for a first write to a new\n * profile.\n *\n * Only one owner.json can exist at the lock path, and it is removed only by\n * its holder's release or by recovery of a dead holder, so at most one\n * process holds the lock. Returns false when the lock is not acquired.\n */\nasync function publishProfileLock(profile: string, lockPath: string, token: string): Promise<boolean> {\n try {\n await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });\n } catch (error) {\n if (isErrno(error, \"EEXIST\")) return false;\n if (!isErrno(error, \"ENOENT\")) throw error;\n await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });\n return false;\n }\n await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);\n const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);\n const owner = { pid: process.pid, createdAt: new Date().toISOString(), token };\n try {\n await writeFile(staged, `${JSON.stringify(owner, null, 2)}\\n`, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE, flag: \"wx\" });\n await link(staged, join(lockPath, \"owner.json\"));\n return true;\n } catch (error) {\n if (isErrno(error, \"EEXIST\") || isErrno(error, \"ENOENT\")) return false;\n // Do not leave an ownerless lock behind for others to wait out. rmdir\n // removes only an empty directory; anyone who created it after ours went\n // away sees ENOENT on link and retries.\n await rmdir(lockPath).catch(() => undefined);\n throw error;\n } finally {\n await rm(staged, { force: true }).catch(() => undefined);\n }\n}\n\n/**\n * Releases the lock if `.lock/owner.json` is still this acquisition's. No\n * one else can put an owner record there while it exists (owners are only\n * linked into a directory their own `mkdir` created, and recovery removes\n * only a dead holder's record), so the record read is the one unlinked.\n */\nasync function releaseProfileLock(profile: string, lockPath: string, token: string): Promise<void> {\n const ownerPath = join(lockPath, \"owner.json\");\n const owner = await readJson<{ token?: unknown }>(ownerPath).catch(() => null);\n if (owner?.token !== token) return;\n await rm(ownerPath, { force: true });\n // `.lock` is empty from here until the rmdir; a contender that removes it\n // and creates its own meanwhile is not harmed: rmdir fails on its owner\n // file, or the contender's link sees ENOENT and retries. If a recoverer's\n // claim file is still inside, rmdir fails and that recoverer removes the\n // directory when it drops its claim.\n await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);\n await rmdir(lockPath).catch(() => undefined);\n}\n\n/**\n * Gives process-level contention tests an event-driven witness that a second\n * writer has reached an already-held lock. This deliberately has no effect\n * outside an explicit test environment, and only creates a new signal file\n * beneath TC_HOME; it never changes lock acquisition or release semantics.\n */\nasync function signalTestLockContention(profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n\n const configuredPath = process.env[TEST_LOCK_CONTENTION_SIGNAL_PATH];\n const configuredHome = process.env.TC_HOME;\n if (!configuredPath || !configuredHome) return;\n\n const home = resolve(configuredHome);\n const signalPath = resolve(configuredPath);\n const pathFromHome = relative(home, signalPath);\n if (\n pathFromHome === \"\" ||\n pathFromHome === \"..\" ||\n pathFromHome.startsWith(`..${process.platform === \"win32\" ? \"\\\\\" : \"/\"}`) ||\n pathFromHome.startsWith(\"../\") ||\n pathFromHome.startsWith(\"..\\\\\")\n ) {\n return;\n }\n\n await writeFile(signalPath, `${profile}\\n`, { encoding: \"utf8\", flag: \"wx\" })\n .catch(() => undefined);\n}\n\n/**\n * Reclaims a lock directory with no owner record that is older than the\n * stale threshold: one left by a crash, or by an older release. A process\n * killed while recovering (or releasing, in older releases) can leave its\n * `.stale-*` / `.release-*` claim file behind; such claim files are removed\n * first. owner.json is never touched, and the directory itself is only\n * `rmdir`ed, which succeeds only while it is empty, so a published lock is\n * never removed. If the directory was a contender's lock not yet published,\n * that contender's `link` fails with ENOENT and it retries without holding\n * anything.\n */\nasync function recoverOwnerlessLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n try {\n const { mtimeMs } = await stat(lockPath);\n if (Date.now() - mtimeMs < staleAfterMs) return false;\n } catch {\n return false;\n }\n const checkedAt = performance.now();\n await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);\n try {\n const entries = await readdir(lockPath);\n if (entries.includes(\"owner.json\")) return false;\n // The age check above is only good for a while; see recoveryFenced.\n if (recoveryFenced(checkedAt, staleAfterMs)) return false;\n for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {\n await rm(join(lockPath, name), { force: true });\n }\n await rmdir(lockPath);\n } catch {\n return false;\n }\n await removeAgedOwnerFiles(lockPath, staleAfterMs);\n return true;\n}\n\n/**\n * Whether a recovery step decided at `since` (a monotonic timestamp) is too\n * old to act on. Claim-only and ownerless cleanup act only on a `.lock`\n * unchanged for `staleAfterMs`, and a recoverer's claim (or the change that\n * made a directory look abandoned) dates the directory no earlier than\n * `since`. Acting within half that window means no such cleanup can have\n * removed the directory and let a new holder in meanwhile; past it, the\n * recoverer drops only its own claim and starts the acquisition over.\n */\nfunction recoveryFenced(since: number, staleAfterMs: number): boolean {\n return performance.now() - since >= staleAfterMs / 2;\n}\n\n/** Claim files of stale recovery (and of release in older releases). */\nconst ABANDONED_CLAIM = /^\\.(?:release|stale)-[0-9a-f-]+\\.json$/;\n/** Owner files staged by publishProfileLock beside `.lock`. */\nconst STAGED_OWNER = /^\\.lock-owner-[0-9a-f-]+\\.tmp$/;\n\n/**\n * Removes owner files a crashed acquirer staged beside `.lock`. A live\n * acquirer's staged file exists only for its write and link, so only files\n * older than the stale threshold are removed. Runs after a recovery, never\n * on the uncontended path.\n */\nasync function removeAgedOwnerFiles(lockPath: string, staleAfterMs: number): Promise<void> {\n const directory = dirname(lockPath);\n const names = await readdir(directory).catch(() => []);\n for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {\n const path = join(directory, name);\n const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);\n if (aged) await rm(path, { force: true }).catch(() => undefined);\n }\n}\n\n/**\n * Removes a dead holder's lock. The claim is a hard link to owner.json, so\n * the owner record is never moved away: a recoverer whose observation is\n * outdated (the dead holder's lock was already recovered and a live holder\n * now owns `.lock`) sees another token, drops its link and leaves the live\n * owner record in place. Only after the claimed record is confirmed to be\n * the observed dead holder's is owner.json unlinked. Nothing can link a new\n * owner.json meanwhile: the claim keeps `.lock` non-empty, so it cannot be\n * removed and recreated.\n */\nasync function recoverStaleLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n const ownerPath = join(lockPath, \"owner.json\");\n const owner = await readJson<{ pid?: unknown; createdAt?: unknown; token?: unknown }>(ownerPath)\n .catch(() => null);\n if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);\n if (!isStaleOwner(owner, staleAfterMs)) return false;\n\n const observedToken = typeof owner?.token === \"string\" && owner.token.length > 0\n ? owner.token\n : \"legacy\";\n await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);\n const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);\n try {\n await link(ownerPath, claimPath);\n } catch {\n return false;\n }\n const claimedAt = performance.now();\n await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);\n\n const claimed = await readJson<{ pid?: unknown; createdAt?: unknown; token?: unknown }>(claimPath)\n .catch(() => null);\n const sameInstance = claimed !== null && (\n observedToken === \"legacy\"\n ? claimed.token === undefined\n : claimed.token === observedToken\n );\n await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);\n if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {\n // Paused too long: another recoverer may have finished this recovery and\n // a cleanup may have removed this claim, so owner.json may now be a live\n // holder's. Touch nothing but this claim.\n await rm(claimPath, { force: true });\n await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);\n return false;\n }\n if (sameInstance) {\n await rm(ownerPath, { force: true });\n await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);\n }\n await rm(claimPath, { force: true });\n // Empty now if the claimed holder is gone or a live holder released while\n // this claim kept `.lock` from being removed: remove it rather than leave\n // an ownerless lock to age out. rmdir fails while an owner record exists.\n await rmdir(lockPath).catch(() => undefined);\n if (!sameInstance) return false;\n await removeAgedOwnerFiles(lockPath, staleAfterMs);\n return true;\n}\n\n/** Test-only rendezvous (NODE_ENV=test and the named barrier directory set). */\nasync function waitForTestBarrier(environmentName: string, profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n const barrierDirectory = process.env[environmentName];\n if (!barrierDirectory) return;\n await mkdir(barrierDirectory, { recursive: true });\n const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);\n await writeFile(readyPath, \"ready\\n\", { encoding: \"utf8\", flag: \"wx\" }).catch(() => undefined);\n const releasePath = join(barrierDirectory, \"release\");\n while (true) {\n try {\n await readFile(releasePath, \"utf8\");\n return;\n } catch {\n await sleep(1);\n }\n }\n}\n\nfunction isStaleOwner(\n owner: { pid?: unknown; createdAt?: unknown } | null,\n staleAfterMs: number,\n): boolean {\n const now = Date.now();\n const createdAt = typeof owner?.createdAt === \"string\"\n ? Date.parse(owner.createdAt)\n : Number.NaN;\n\n // Reclaim only a fully published lock whose owner PID is confirmed dead.\n // A lock without an owner record is handled by recoverOwnerlessLock: a\n // process holds the lock only once its owner.json is linked in.\n if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;\n if (typeof owner?.pid !== \"number\" || !Number.isInteger(owner.pid) || owner.pid <= 0) {\n return false;\n }\n return !isProcessAlive(owner.pid);\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return !isErrno(error, \"ESRCH\");\n }\n}\n\nfunction validateProfileName(profile: string): string {\n if (\n !profile ||\n profile === \".\" ||\n profile === \"..\" ||\n profile.includes(\"/\") ||\n profile.includes(\"\\\\\") ||\n profile.includes(\"\\0\")\n ) {\n throw new TypeError(\"Profile names must be non-empty path segments.\");\n }\n return profile;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n return typeof error === \"object\" && error !== null && \"code\" in error &&\n (error as { code?: unknown }).code === code;\n}\n\nfunction sleep(milliseconds: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, milliseconds));\n}\n\nexport { DEFAULT_PROFILE };\n"],"mappings":"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","names":["ownership","resolve"]}
1
+ {"version":3,"sources":["../src/state.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport {\n chmod,\n link,\n mkdir,\n readFile,\n readdir,\n rename,\n rmdir,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, relative, resolve } from \"node:path\";\n\nconst DEFAULT_PROFILE = \"default\";\nconst DEFAULT_LOCK_TIMEOUT_MS = 2_000;\nconst DEFAULT_LOCK_RETRY_MS = 25;\nconst DEFAULT_STALE_LOCK_MS = 30_000;\nconst TEST_LOCK_CONTENTION_SIGNAL_PATH = \"TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH\";\n// Profile state holds keys, sessions and delegations: owner-only access.\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIR_MODE = 0o700;\nconst TEST_LOCK_RECOVERY_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR\";\nconst TEST_LOCK_OWNERLESS_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR\";\nconst TEST_LOCK_PUBLISH_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR\";\nconst TEST_LOCK_RELEASE_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR\";\nconst TEST_LOCK_CLAIM_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR\";\nconst TEST_LOCK_CLAIMED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR\";\nconst TEST_LOCK_FENCED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR\";\nconst TEST_LOCK_VERIFIED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR\";\n/** One acquisition of a profile lock; `active` is cleared before release. */\ninterface HeldProfileLock {\n readonly lockPath: string;\n active: boolean;\n}\n/**\n * Async context shared by every copy of this module in the process. Each\n * bundled operations entry point (`state`, `delegation-binding`, the root)\n * carries its own copy, so module-level context would make lock reentrancy and\n * the invocation state root stop at an entry-point boundary.\n *\n * The `.v1` in each key is the format version shared by every copy that reads\n * it. Bump it whenever the `HeldProfileLock` entry shape or the on-disk lock\n * protocol changes, so copies with different formats (two operations versions\n * in one process) stop trusting each other's held-lock entries and simply\n * wait for the lock instead. TC-633 changes the held-lock shape and adds a\n * per-process `turnsInProgress` set: whichever of TC-602 and TC-633 lands\n * second bumps these keys to `.v2` and makes that set process-wide under the\n * same version.\n */\nfunction processWideContext<T>(key: string): AsyncLocalStorage<T> {\n const registry = globalThis as unknown as Record<symbol, AsyncLocalStorage<T> | undefined>;\n return registry[Symbol.for(key)] ??= new AsyncLocalStorage<T>();\n}\nconst invocationStateRoot = processWideContext<string>(\"tinycloud.operations.invocationStateRoot.v1\");\n/** Profile lock acquisitions held by the current async call chain (see withProfileLock). */\nconst heldProfileLocks = processWideContext<readonly HeldProfileLock[]>(\"tinycloud.operations.heldProfileLocks.v1\");\n\nexport type ProfileStoreName =\n | \"session\"\n | \"additional-delegations\"\n | \"auth-requests\";\n\nexport interface StoreMetadata {\n formatVersion: number;\n}\n\nexport interface ProfileStoreContents<T> {\n formatVersion: number;\n records: T[];\n}\n\nexport interface ProfileLockOptions {\n timeoutMs?: number;\n retryMs?: number;\n staleAfterMs?: number;\n}\n\nexport class ProfileLockTimeoutError extends Error {\n readonly code = \"PROFILE_LOCK_TIMEOUT\";\n\n constructor(profile: string, timeoutMs: number) {\n super(`Timed out waiting for the profile lock for \"${profile}\" after ${timeoutMs}ms.`);\n this.name = \"ProfileLockTimeoutError\";\n }\n}\n\n/**\n * The CLI's delegated-secret path treats TC_HOME as a home directory, rather\n * than a direct TinyCloud directory. Keep the shared store on that convention.\n */\nexport function tinycloudHomePath(): string {\n const home = invocationStateRoot.getStore() ??\n process.env.TC_HOME ??\n process.env.HOME ??\n process.env.USERPROFILE ??\n homedir();\n return join(home, \".tinycloud\");\n}\n\n/** Run one operation against an isolated TinyCloud home directory. */\nexport function withTinyCloudStateRoot<T>(\n stateRoot: string | undefined,\n action: () => Promise<T>,\n): Promise<T> {\n if (stateRoot === undefined) return action();\n if (!isAbsolute(stateRoot) || stateRoot.includes(\"\\0\")) {\n throw new TypeError(\"The TinyCloud state root must be an absolute path.\");\n }\n return invocationStateRoot.run(resolve(stateRoot), action);\n}\n\nexport function tinycloudConfigPath(): string {\n return join(tinycloudHomePath(), \"config.json\");\n}\n\nexport function profilesPath(): string {\n return join(tinycloudHomePath(), \"profiles\");\n}\n\nexport function profilePath(profile: string): string {\n return join(profilesPath(), validateProfileName(profile));\n}\n\nexport function profileConfigPath(profile: string): string {\n return join(profilePath(profile), \"profile.json\");\n}\n\nexport function sessionPath(profile: string): string {\n return profileStorePath(profile, \"session\");\n}\n\nexport function additionalDelegationsPath(profile: string): string {\n return profileStorePath(profile, \"additional-delegations\");\n}\n\nexport function authRequestsPath(profile: string): string {\n return profileStorePath(profile, \"auth-requests\");\n}\n\nexport function profileStorePath(profile: string, store: ProfileStoreName): string {\n return join(profilePath(profile), `${store}.json`);\n}\n\n/** The format record is deliberately separate from each legacy JSON payload. */\nexport function profileStoreMetadataPath(profile: string, store: ProfileStoreName): string {\n return `${profileStorePath(profile, store)}.metadata.json`;\n}\n\nexport function profileLockPath(profile: string): string {\n return join(profilePath(profile), \".lock\");\n}\n\nexport function profileLockMetadataPath(profile: string): string {\n return join(profileLockPath(profile), \"owner.json\");\n}\n\n/**\n * Reads JSON without hiding malformed or inaccessible files. A missing file is\n * the only condition represented as null.\n */\nexport async function readJson<T>(filePath: string): Promise<T | null> {\n try {\n return JSON.parse(await readFile(filePath, \"utf8\")) as T;\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return null;\n throw error;\n }\n}\n\n/**\n * Writes the same pretty-printed, trailing-newline JSON shape used by the CLI,\n * but publishes it with an atomic rename from the same directory.\n */\nexport async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n const directory = dirname(filePath);\n const temporaryPath = join(\n directory,\n `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`,\n );\n const contents = `${JSON.stringify(value, null, 2)}\\n`;\n\n await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });\n try {\n await writeFile(temporaryPath, contents, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE });\n await rename(temporaryPath, filePath);\n } catch (error) {\n await rm(temporaryPath, { force: true }).catch(() => undefined);\n throw error;\n }\n}\n\nexport async function readProfile<T extends object = Record<string, unknown>>(\n profile: string,\n): Promise<T | null> {\n return readJson<T>(profileConfigPath(profile));\n}\n\nexport async function readSession<T extends object = Record<string, unknown>>(\n profile: string,\n): Promise<T | null> {\n await readStoreMetadata(profile, \"session\");\n return readJson<T>(sessionPath(profile));\n}\n\nexport async function readAdditionalDelegations<T = Record<string, unknown>>(\n profile: string,\n): Promise<T[]> {\n return (await readProfileStore<T>(profile, \"additional-delegations\")).records;\n}\n\nexport async function readAuthRequests<T = Record<string, unknown>>(\n profile: string,\n): Promise<T[]> {\n return (await readProfileStore<T>(profile, \"auth-requests\")).records;\n}\n\nexport async function readStoreMetadata(\n profile: string,\n store: ProfileStoreName,\n): Promise<StoreMetadata> {\n const metadata = await readJson<StoreMetadata>(profileStoreMetadataPath(profile, store));\n if (metadata === null) return { formatVersion: 1 };\n if (\n typeof metadata !== \"object\" ||\n metadata.formatVersion !== 1\n ) {\n throw new TypeError(`Unsupported store format for \"${store}\".`);\n }\n return metadata;\n}\n\nexport async function readProfileStore<T>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n): Promise<ProfileStoreContents<T>> {\n const [metadata, raw] = await Promise.all([\n readStoreMetadata(profile, store),\n readJson<unknown>(profileStorePath(profile, store)),\n ]);\n return {\n formatVersion: metadata.formatVersion,\n records: Array.isArray(raw) ? raw as T[] : [],\n };\n}\n\n/**\n * Runs a small critical section under the one advisory lock shared by all\n * profile stores. Lock release verifies its ownership token before removing\n * the exact metadata instance it acquired.\n *\n * Reentrant within one async call chain: a critical section that already\n * holds a profile's lock (for example a login's compare-and-commit) can call\n * store writers that take the same lock without deadlocking. Ownership is\n * keyed by the resolved lock path (so switching state roots inside a lock\n * still takes the other root's lock) and lasts exactly as long as the\n * acquisition: it is revoked before release, so work deferred past the\n * critical section waits for the lock like any other caller.\n */\nexport async function withProfileLock<T>(\n profile: string,\n action: () => Promise<T>,\n options: ProfileLockOptions = {},\n): Promise<T> {\n const normalizedProfile = validateProfileName(profile);\n const lockPath = profileLockPath(normalizedProfile);\n const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);\n if (held.some((ownership) => ownership.lockPath === lockPath)) return action();\n const release = await acquireProfileLock(normalizedProfile, options);\n const ownership: HeldProfileLock = { lockPath, active: true };\n try {\n return await heldProfileLocks.run([...held, ownership], action);\n } finally {\n ownership.active = false;\n await release();\n }\n}\n\n/**\n * Appends a record or replaces the existing record with the supplied explicit\n * key. The extractor is supplied by the owning store so state.ts never needs\n * to know permission-request or delegation record shapes.\n */\nexport async function upsertProfileRecord<T>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n key: string,\n record: T,\n getKey: (candidate: T) => string | undefined,\n options: ProfileLockOptions = {},\n): Promise<T[]> {\n if (!key) throw new TypeError(\"A non-empty record key is required.\");\n\n return withProfileLock(profile, async () => {\n const current = (await readProfileStore<T>(profile, store)).records;\n const next = current.filter((candidate) => getKey(candidate) !== key);\n next.push(record);\n await writeJsonAtomic(profileStorePath(profile, store), next);\n await writeFormatOneMetadata(profile, store);\n return next;\n }, options);\n}\n\n/**\n * Performs a typed read-modify-write of a legacy array store under the one\n * per-profile lock. The payload stays an array and the sibling metadata stays\n * format 1, so existing CLI readers keep their byte/layout contract.\n */\nexport async function updateProfileStore<T, Result>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n update: (\n records: readonly T[],\n ) => Promise<{ readonly records: readonly T[]; readonly result: Result }> | {\n readonly records: readonly T[];\n readonly result: Result;\n },\n options: ProfileLockOptions = {},\n): Promise<Result> {\n return withProfileLock(\n profile,\n () => updateProfileStoreWhileLocked(profile, store, update),\n options,\n );\n}\n\n/** The caller must already own this profile's lock. */\nasync function updateProfileStoreWhileLocked<T, Result>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n update: (\n records: readonly T[],\n ) => Promise<{ readonly records: readonly T[]; readonly result: Result }> | {\n readonly records: readonly T[];\n readonly result: Result;\n },\n): Promise<Result> {\n const current = await readProfileStore<T>(profile, store);\n const next = await update(current.records);\n await writeJsonAtomic(profileStorePath(profile, store), next.records);\n await writeFormatOneMetadata(profile, store);\n return next.result;\n}\n\nexport async function writeSession<T extends object>(\n profile: string,\n session: T,\n options: ProfileLockOptions = {},\n): Promise<void> {\n await withProfileLock(profile, async () => {\n await readStoreMetadata(profile, \"session\");\n await writeJsonAtomic(sessionPath(profile), session);\n await writeFormatOneMetadata(profile, \"session\");\n }, options);\n}\n\nexport async function removeSession(\n profile: string,\n options: ProfileLockOptions = {},\n): Promise<void> {\n await withProfileLock(profile, async () => {\n await readStoreMetadata(profile, \"session\");\n await rm(sessionPath(profile), { force: true });\n }, options);\n}\n\nasync function writeFormatOneMetadata(profile: string, store: ProfileStoreName): Promise<void> {\n await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });\n}\n\nasync function acquireProfileLock(\n profile: string,\n options: ProfileLockOptions,\n): Promise<() => Promise<void>> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n const retryMs = options.retryMs ?? DEFAULT_LOCK_RETRY_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;\n const startedAt = Date.now();\n const lockPath = profileLockPath(profile);\n\n const directory = profilePath(profile);\n await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });\n // Every store write takes this lock: tighten the tree older releases\n // created 0775 before writing sessions or delegations into it.\n for (const path of [tinycloudHomePath(), profilesPath(), directory]) {\n await chmod(path, PRIVATE_DIR_MODE).catch((error: unknown) => {\n // A profile deletion can remove the profile directory right after the\n // mkdir above; the lock attempt below recreates it 0700.\n if (path !== directory || !isErrno(error, \"ENOENT\")) throw error;\n });\n }\n\n while (true) {\n const token = randomUUID();\n if (await publishProfileLock(profile, lockPath, token)) {\n return async () => {\n await releaseProfileLock(profile, lockPath, token);\n };\n }\n await signalTestLockContention(profile);\n\n if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;\n\n const elapsedMs = Date.now() - startedAt;\n if (elapsedMs >= timeoutMs) {\n throw new ProfileLockTimeoutError(profile, timeoutMs);\n }\n await sleep(Math.min(retryMs, timeoutMs - elapsedMs));\n }\n}\n\n/**\n * One attempt to take the lock. Nothing here replaces an existing path:\n *\n * 1. `mkdir(.lock)` (never recursive) is exclusive, and is the primitive\n * older releases use too, so they exclude each other.\n * 2. Ownership is published by `link`ing a fully written owner file (staged\n * beside `.lock`, on the same filesystem) to `.lock/owner.json`. `link`\n * fails with EEXIST if an owner exists, and with ENOENT if the directory\n * was removed meanwhile (an aged ownerless-lock reclaim). Only a\n * successful link holds the lock; on either failure nothing is released,\n * since nothing was acquired, and the caller retries.\n *\n * A profile deletion removes the profile directory once it has released its\n * lock, so `mkdir(.lock)` can see ENOENT; the directory is recreated (0700)\n * and the caller retries within its deadline, as for a first write to a new\n * profile.\n *\n * Only one owner.json can exist at the lock path, and it is removed only by\n * its holder's release or by recovery of a dead holder, so at most one\n * process holds the lock. Returns false when the lock is not acquired.\n */\nasync function publishProfileLock(profile: string, lockPath: string, token: string): Promise<boolean> {\n try {\n await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });\n } catch (error) {\n if (isErrno(error, \"EEXIST\")) return false;\n if (!isErrno(error, \"ENOENT\")) throw error;\n await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });\n return false;\n }\n await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);\n const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);\n const owner = { pid: process.pid, createdAt: new Date().toISOString(), token };\n try {\n await writeFile(staged, `${JSON.stringify(owner, null, 2)}\\n`, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE, flag: \"wx\" });\n await link(staged, join(lockPath, \"owner.json\"));\n return true;\n } catch (error) {\n if (isErrno(error, \"EEXIST\") || isErrno(error, \"ENOENT\")) return false;\n // Do not leave an ownerless lock behind for others to wait out. rmdir\n // removes only an empty directory; anyone who created it after ours went\n // away sees ENOENT on link and retries.\n await rmdir(lockPath).catch(() => undefined);\n throw error;\n } finally {\n await rm(staged, { force: true }).catch(() => undefined);\n }\n}\n\n/**\n * Releases the lock if `.lock/owner.json` is still this acquisition's. No\n * one else can put an owner record there while it exists (owners are only\n * linked into a directory their own `mkdir` created, and recovery removes\n * only a dead holder's record), so the record read is the one unlinked.\n */\nasync function releaseProfileLock(profile: string, lockPath: string, token: string): Promise<void> {\n const ownerPath = join(lockPath, \"owner.json\");\n const owner = await readJson<{ token?: unknown }>(ownerPath).catch(() => null);\n if (owner?.token !== token) return;\n await rm(ownerPath, { force: true });\n // `.lock` is empty from here until the rmdir; a contender that removes it\n // and creates its own meanwhile is not harmed: rmdir fails on its owner\n // file, or the contender's link sees ENOENT and retries. If a recoverer's\n // claim file is still inside, rmdir fails and that recoverer removes the\n // directory when it drops its claim.\n await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);\n await rmdir(lockPath).catch(() => undefined);\n}\n\n/**\n * Gives process-level contention tests an event-driven witness that a second\n * writer has reached an already-held lock. This deliberately has no effect\n * outside an explicit test environment, and only creates a new signal file\n * beneath TC_HOME; it never changes lock acquisition or release semantics.\n */\nasync function signalTestLockContention(profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n\n const configuredPath = process.env[TEST_LOCK_CONTENTION_SIGNAL_PATH];\n const configuredHome = process.env.TC_HOME;\n if (!configuredPath || !configuredHome) return;\n\n const home = resolve(configuredHome);\n const signalPath = resolve(configuredPath);\n const pathFromHome = relative(home, signalPath);\n if (\n pathFromHome === \"\" ||\n pathFromHome === \"..\" ||\n pathFromHome.startsWith(`..${process.platform === \"win32\" ? \"\\\\\" : \"/\"}`) ||\n pathFromHome.startsWith(\"../\") ||\n pathFromHome.startsWith(\"..\\\\\")\n ) {\n return;\n }\n\n await writeFile(signalPath, `${profile}\\n`, { encoding: \"utf8\", flag: \"wx\" })\n .catch(() => undefined);\n}\n\n/**\n * Reclaims a lock directory with no owner record that is older than the\n * stale threshold: one left by a crash, or by an older release. A process\n * killed while recovering (or releasing, in older releases) can leave its\n * `.stale-*` / `.release-*` claim file behind; such claim files are removed\n * first. owner.json is never touched, and the directory itself is only\n * `rmdir`ed, which succeeds only while it is empty, so a published lock is\n * never removed. If the directory was a contender's lock not yet published,\n * that contender's `link` fails with ENOENT and it retries without holding\n * anything.\n */\nasync function recoverOwnerlessLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n try {\n const { mtimeMs } = await stat(lockPath);\n if (Date.now() - mtimeMs < staleAfterMs) return false;\n } catch {\n return false;\n }\n const checkedAt = performance.now();\n await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);\n try {\n const entries = await readdir(lockPath);\n if (entries.includes(\"owner.json\")) return false;\n // The age check above is only good for a while; see recoveryFenced.\n if (recoveryFenced(checkedAt, staleAfterMs)) return false;\n for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {\n await rm(join(lockPath, name), { force: true });\n }\n await rmdir(lockPath);\n } catch {\n return false;\n }\n await removeAgedOwnerFiles(lockPath, staleAfterMs);\n return true;\n}\n\n/**\n * Whether a recovery step decided at `since` (a monotonic timestamp) is too\n * old to act on. Claim-only and ownerless cleanup act only on a `.lock`\n * unchanged for `staleAfterMs`, and a recoverer's claim (or the change that\n * made a directory look abandoned) dates the directory no earlier than\n * `since`. Acting within half that window means no such cleanup can have\n * removed the directory and let a new holder in meanwhile; past it, the\n * recoverer drops only its own claim and starts the acquisition over.\n */\nfunction recoveryFenced(since: number, staleAfterMs: number): boolean {\n return performance.now() - since >= staleAfterMs / 2;\n}\n\n/** Claim files of stale recovery (and of release in older releases). */\nconst ABANDONED_CLAIM = /^\\.(?:release|stale)-[0-9a-f-]+\\.json$/;\n/** Owner files staged by publishProfileLock beside `.lock`. */\nconst STAGED_OWNER = /^\\.lock-owner-[0-9a-f-]+\\.tmp$/;\n\n/**\n * Removes owner files a crashed acquirer staged beside `.lock`. A live\n * acquirer's staged file exists only for its write and link, so only files\n * older than the stale threshold are removed. Runs after a recovery, never\n * on the uncontended path.\n */\nasync function removeAgedOwnerFiles(lockPath: string, staleAfterMs: number): Promise<void> {\n const directory = dirname(lockPath);\n const names = await readdir(directory).catch(() => []);\n for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {\n const path = join(directory, name);\n const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);\n if (aged) await rm(path, { force: true }).catch(() => undefined);\n }\n}\n\n/**\n * Removes a dead holder's lock. The claim is a hard link to owner.json, so\n * the owner record is never moved away: a recoverer whose observation is\n * outdated (the dead holder's lock was already recovered and a live holder\n * now owns `.lock`) sees another token, drops its link and leaves the live\n * owner record in place. Only after the claimed record is confirmed to be\n * the observed dead holder's is owner.json unlinked. Nothing can link a new\n * owner.json meanwhile: the claim keeps `.lock` non-empty, so it cannot be\n * removed and recreated.\n */\nasync function recoverStaleLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n const ownerPath = join(lockPath, \"owner.json\");\n const owner = await readJson<{ pid?: unknown; createdAt?: unknown; token?: unknown }>(ownerPath)\n .catch(() => null);\n if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);\n if (!isStaleOwner(owner, staleAfterMs)) return false;\n\n const observedToken = typeof owner?.token === \"string\" && owner.token.length > 0\n ? owner.token\n : \"legacy\";\n await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);\n const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);\n try {\n await link(ownerPath, claimPath);\n } catch {\n return false;\n }\n const claimedAt = performance.now();\n await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);\n\n const claimed = await readJson<{ pid?: unknown; createdAt?: unknown; token?: unknown }>(claimPath)\n .catch(() => null);\n const sameInstance = claimed !== null && (\n observedToken === \"legacy\"\n ? claimed.token === undefined\n : claimed.token === observedToken\n );\n await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);\n if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {\n // Paused too long: another recoverer may have finished this recovery and\n // a cleanup may have removed this claim, so owner.json may now be a live\n // holder's. Touch nothing but this claim.\n await rm(claimPath, { force: true });\n await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);\n return false;\n }\n if (sameInstance) {\n await rm(ownerPath, { force: true });\n await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);\n }\n await rm(claimPath, { force: true });\n // Empty now if the claimed holder is gone or a live holder released while\n // this claim kept `.lock` from being removed: remove it rather than leave\n // an ownerless lock to age out. rmdir fails while an owner record exists.\n await rmdir(lockPath).catch(() => undefined);\n if (!sameInstance) return false;\n await removeAgedOwnerFiles(lockPath, staleAfterMs);\n return true;\n}\n\n/** Test-only rendezvous (NODE_ENV=test and the named barrier directory set). */\nasync function waitForTestBarrier(environmentName: string, profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n const barrierDirectory = process.env[environmentName];\n if (!barrierDirectory) return;\n await mkdir(barrierDirectory, { recursive: true });\n const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);\n await writeFile(readyPath, \"ready\\n\", { encoding: \"utf8\", flag: \"wx\" }).catch(() => undefined);\n const releasePath = join(barrierDirectory, \"release\");\n while (true) {\n try {\n await readFile(releasePath, \"utf8\");\n return;\n } catch {\n await sleep(1);\n }\n }\n}\n\nfunction isStaleOwner(\n owner: { pid?: unknown; createdAt?: unknown } | null,\n staleAfterMs: number,\n): boolean {\n const now = Date.now();\n const createdAt = typeof owner?.createdAt === \"string\"\n ? Date.parse(owner.createdAt)\n : Number.NaN;\n\n // Reclaim only a fully published lock whose owner PID is confirmed dead.\n // A lock without an owner record is handled by recoverOwnerlessLock: a\n // process holds the lock only once its owner.json is linked in.\n if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;\n if (typeof owner?.pid !== \"number\" || !Number.isInteger(owner.pid) || owner.pid <= 0) {\n return false;\n }\n return !isProcessAlive(owner.pid);\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return !isErrno(error, \"ESRCH\");\n }\n}\n\nfunction validateProfileName(profile: string): string {\n if (\n !profile ||\n profile === \".\" ||\n profile === \"..\" ||\n profile.includes(\"/\") ||\n profile.includes(\"\\\\\") ||\n profile.includes(\"\\0\")\n ) {\n throw new TypeError(\"Profile names must be non-empty path segments.\");\n }\n return profile;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n return typeof error === \"object\" && error !== null && \"code\" in error &&\n (error as { code?: unknown }).code === code;\n}\n\nfunction sleep(milliseconds: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, milliseconds));\n}\n\nexport { DEFAULT_PROFILE };\n"],"mappings":"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","names":["ownership","resolve"]}
package/dist/state.js CHANGED
@@ -30,8 +30,13 @@ var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
30
30
  var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
31
31
  var TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
32
32
  var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
33
- var invocationStateRoot = new AsyncLocalStorage();
34
- var heldProfileLocks = new AsyncLocalStorage();
33
+ function processWideContext(key) {
34
+ var _a;
35
+ const registry = globalThis;
36
+ return registry[_a = Symbol.for(key)] ?? (registry[_a] = new AsyncLocalStorage());
37
+ }
38
+ var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
39
+ var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v1");
35
40
  var ProfileLockTimeoutError = class extends Error {
36
41
  constructor(profile, timeoutMs) {
37
42
  super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
package/dist/state.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/state.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport {\n chmod,\n link,\n mkdir,\n readFile,\n readdir,\n rename,\n rmdir,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, relative, resolve } from \"node:path\";\n\nconst DEFAULT_PROFILE = \"default\";\nconst DEFAULT_LOCK_TIMEOUT_MS = 2_000;\nconst DEFAULT_LOCK_RETRY_MS = 25;\nconst DEFAULT_STALE_LOCK_MS = 30_000;\nconst TEST_LOCK_CONTENTION_SIGNAL_PATH = \"TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH\";\n// Profile state holds keys, sessions and delegations: owner-only access.\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIR_MODE = 0o700;\nconst TEST_LOCK_RECOVERY_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR\";\nconst TEST_LOCK_OWNERLESS_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR\";\nconst TEST_LOCK_PUBLISH_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR\";\nconst TEST_LOCK_RELEASE_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR\";\nconst TEST_LOCK_CLAIM_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR\";\nconst TEST_LOCK_CLAIMED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR\";\nconst TEST_LOCK_FENCED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR\";\nconst TEST_LOCK_VERIFIED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR\";\nconst invocationStateRoot = new AsyncLocalStorage<string>();\n/** One acquisition of a profile lock; `active` is cleared before release. */\ninterface HeldProfileLock {\n readonly lockPath: string;\n active: boolean;\n}\n/** Profile lock acquisitions held by the current async call chain (see withProfileLock). */\nconst heldProfileLocks = new AsyncLocalStorage<readonly HeldProfileLock[]>();\n\nexport type ProfileStoreName =\n | \"session\"\n | \"additional-delegations\"\n | \"auth-requests\";\n\nexport interface StoreMetadata {\n formatVersion: number;\n}\n\nexport interface ProfileStoreContents<T> {\n formatVersion: number;\n records: T[];\n}\n\nexport interface ProfileLockOptions {\n timeoutMs?: number;\n retryMs?: number;\n staleAfterMs?: number;\n}\n\nexport class ProfileLockTimeoutError extends Error {\n readonly code = \"PROFILE_LOCK_TIMEOUT\";\n\n constructor(profile: string, timeoutMs: number) {\n super(`Timed out waiting for the profile lock for \"${profile}\" after ${timeoutMs}ms.`);\n this.name = \"ProfileLockTimeoutError\";\n }\n}\n\n/**\n * The CLI's delegated-secret path treats TC_HOME as a home directory, rather\n * than a direct TinyCloud directory. Keep the shared store on that convention.\n */\nexport function tinycloudHomePath(): string {\n const home = invocationStateRoot.getStore() ??\n process.env.TC_HOME ??\n process.env.HOME ??\n process.env.USERPROFILE ??\n homedir();\n return join(home, \".tinycloud\");\n}\n\n/** Run one operation against an isolated TinyCloud home directory. */\nexport function withTinyCloudStateRoot<T>(\n stateRoot: string | undefined,\n action: () => Promise<T>,\n): Promise<T> {\n if (stateRoot === undefined) return action();\n if (!isAbsolute(stateRoot) || stateRoot.includes(\"\\0\")) {\n throw new TypeError(\"The TinyCloud state root must be an absolute path.\");\n }\n return invocationStateRoot.run(resolve(stateRoot), action);\n}\n\nexport function tinycloudConfigPath(): string {\n return join(tinycloudHomePath(), \"config.json\");\n}\n\nexport function profilesPath(): string {\n return join(tinycloudHomePath(), \"profiles\");\n}\n\nexport function profilePath(profile: string): string {\n return join(profilesPath(), validateProfileName(profile));\n}\n\nexport function profileConfigPath(profile: string): string {\n return join(profilePath(profile), \"profile.json\");\n}\n\nexport function sessionPath(profile: string): string {\n return profileStorePath(profile, \"session\");\n}\n\nexport function additionalDelegationsPath(profile: string): string {\n return profileStorePath(profile, \"additional-delegations\");\n}\n\nexport function authRequestsPath(profile: string): string {\n return profileStorePath(profile, \"auth-requests\");\n}\n\nexport function profileStorePath(profile: string, store: ProfileStoreName): string {\n return join(profilePath(profile), `${store}.json`);\n}\n\n/** The format record is deliberately separate from each legacy JSON payload. */\nexport function profileStoreMetadataPath(profile: string, store: ProfileStoreName): string {\n return `${profileStorePath(profile, store)}.metadata.json`;\n}\n\nexport function profileLockPath(profile: string): string {\n return join(profilePath(profile), \".lock\");\n}\n\nexport function profileLockMetadataPath(profile: string): string {\n return join(profileLockPath(profile), \"owner.json\");\n}\n\n/**\n * Reads JSON without hiding malformed or inaccessible files. A missing file is\n * the only condition represented as null.\n */\nexport async function readJson<T>(filePath: string): Promise<T | null> {\n try {\n return JSON.parse(await readFile(filePath, \"utf8\")) as T;\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return null;\n throw error;\n }\n}\n\n/**\n * Writes the same pretty-printed, trailing-newline JSON shape used by the CLI,\n * but publishes it with an atomic rename from the same directory.\n */\nexport async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n const directory = dirname(filePath);\n const temporaryPath = join(\n directory,\n `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`,\n );\n const contents = `${JSON.stringify(value, null, 2)}\\n`;\n\n await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });\n try {\n await writeFile(temporaryPath, contents, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE });\n await rename(temporaryPath, filePath);\n } catch (error) {\n await rm(temporaryPath, { force: true }).catch(() => undefined);\n throw error;\n }\n}\n\nexport async function readProfile<T extends object = Record<string, unknown>>(\n profile: string,\n): Promise<T | null> {\n return readJson<T>(profileConfigPath(profile));\n}\n\nexport async function readSession<T extends object = Record<string, unknown>>(\n profile: string,\n): Promise<T | null> {\n await readStoreMetadata(profile, \"session\");\n return readJson<T>(sessionPath(profile));\n}\n\nexport async function readAdditionalDelegations<T = Record<string, unknown>>(\n profile: string,\n): Promise<T[]> {\n return (await readProfileStore<T>(profile, \"additional-delegations\")).records;\n}\n\nexport async function readAuthRequests<T = Record<string, unknown>>(\n profile: string,\n): Promise<T[]> {\n return (await readProfileStore<T>(profile, \"auth-requests\")).records;\n}\n\nexport async function readStoreMetadata(\n profile: string,\n store: ProfileStoreName,\n): Promise<StoreMetadata> {\n const metadata = await readJson<StoreMetadata>(profileStoreMetadataPath(profile, store));\n if (metadata === null) return { formatVersion: 1 };\n if (\n typeof metadata !== \"object\" ||\n metadata.formatVersion !== 1\n ) {\n throw new TypeError(`Unsupported store format for \"${store}\".`);\n }\n return metadata;\n}\n\nexport async function readProfileStore<T>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n): Promise<ProfileStoreContents<T>> {\n const [metadata, raw] = await Promise.all([\n readStoreMetadata(profile, store),\n readJson<unknown>(profileStorePath(profile, store)),\n ]);\n return {\n formatVersion: metadata.formatVersion,\n records: Array.isArray(raw) ? raw as T[] : [],\n };\n}\n\n/**\n * Runs a small critical section under the one advisory lock shared by all\n * profile stores. Lock release verifies its ownership token before removing\n * the exact metadata instance it acquired.\n *\n * Reentrant within one async call chain: a critical section that already\n * holds a profile's lock (for example a login's compare-and-commit) can call\n * store writers that take the same lock without deadlocking. Ownership is\n * keyed by the resolved lock path (so switching state roots inside a lock\n * still takes the other root's lock) and lasts exactly as long as the\n * acquisition: it is revoked before release, so work deferred past the\n * critical section waits for the lock like any other caller.\n */\nexport async function withProfileLock<T>(\n profile: string,\n action: () => Promise<T>,\n options: ProfileLockOptions = {},\n): Promise<T> {\n const normalizedProfile = validateProfileName(profile);\n const lockPath = profileLockPath(normalizedProfile);\n const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);\n if (held.some((ownership) => ownership.lockPath === lockPath)) return action();\n const release = await acquireProfileLock(normalizedProfile, options);\n const ownership: HeldProfileLock = { lockPath, active: true };\n try {\n return await heldProfileLocks.run([...held, ownership], action);\n } finally {\n ownership.active = false;\n await release();\n }\n}\n\n/**\n * Appends a record or replaces the existing record with the supplied explicit\n * key. The extractor is supplied by the owning store so state.ts never needs\n * to know permission-request or delegation record shapes.\n */\nexport async function upsertProfileRecord<T>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n key: string,\n record: T,\n getKey: (candidate: T) => string | undefined,\n options: ProfileLockOptions = {},\n): Promise<T[]> {\n if (!key) throw new TypeError(\"A non-empty record key is required.\");\n\n return withProfileLock(profile, async () => {\n const current = (await readProfileStore<T>(profile, store)).records;\n const next = current.filter((candidate) => getKey(candidate) !== key);\n next.push(record);\n await writeJsonAtomic(profileStorePath(profile, store), next);\n await writeFormatOneMetadata(profile, store);\n return next;\n }, options);\n}\n\n/**\n * Performs a typed read-modify-write of a legacy array store under the one\n * per-profile lock. The payload stays an array and the sibling metadata stays\n * format 1, so existing CLI readers keep their byte/layout contract.\n */\nexport async function updateProfileStore<T, Result>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n update: (\n records: readonly T[],\n ) => Promise<{ readonly records: readonly T[]; readonly result: Result }> | {\n readonly records: readonly T[];\n readonly result: Result;\n },\n options: ProfileLockOptions = {},\n): Promise<Result> {\n return withProfileLock(\n profile,\n () => updateProfileStoreWhileLocked(profile, store, update),\n options,\n );\n}\n\n/** The caller must already own this profile's lock. */\nasync function updateProfileStoreWhileLocked<T, Result>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n update: (\n records: readonly T[],\n ) => Promise<{ readonly records: readonly T[]; readonly result: Result }> | {\n readonly records: readonly T[];\n readonly result: Result;\n },\n): Promise<Result> {\n const current = await readProfileStore<T>(profile, store);\n const next = await update(current.records);\n await writeJsonAtomic(profileStorePath(profile, store), next.records);\n await writeFormatOneMetadata(profile, store);\n return next.result;\n}\n\nexport async function writeSession<T extends object>(\n profile: string,\n session: T,\n options: ProfileLockOptions = {},\n): Promise<void> {\n await withProfileLock(profile, async () => {\n await readStoreMetadata(profile, \"session\");\n await writeJsonAtomic(sessionPath(profile), session);\n await writeFormatOneMetadata(profile, \"session\");\n }, options);\n}\n\nexport async function removeSession(\n profile: string,\n options: ProfileLockOptions = {},\n): Promise<void> {\n await withProfileLock(profile, async () => {\n await readStoreMetadata(profile, \"session\");\n await rm(sessionPath(profile), { force: true });\n }, options);\n}\n\nasync function writeFormatOneMetadata(profile: string, store: ProfileStoreName): Promise<void> {\n await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });\n}\n\nasync function acquireProfileLock(\n profile: string,\n options: ProfileLockOptions,\n): Promise<() => Promise<void>> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n const retryMs = options.retryMs ?? DEFAULT_LOCK_RETRY_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;\n const startedAt = Date.now();\n const lockPath = profileLockPath(profile);\n\n const directory = profilePath(profile);\n await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });\n // Every store write takes this lock: tighten the tree older releases\n // created 0775 before writing sessions or delegations into it.\n for (const path of [tinycloudHomePath(), profilesPath(), directory]) {\n await chmod(path, PRIVATE_DIR_MODE).catch((error: unknown) => {\n // A profile deletion can remove the profile directory right after the\n // mkdir above; the lock attempt below recreates it 0700.\n if (path !== directory || !isErrno(error, \"ENOENT\")) throw error;\n });\n }\n\n while (true) {\n const token = randomUUID();\n if (await publishProfileLock(profile, lockPath, token)) {\n return async () => {\n await releaseProfileLock(profile, lockPath, token);\n };\n }\n await signalTestLockContention(profile);\n\n if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;\n\n const elapsedMs = Date.now() - startedAt;\n if (elapsedMs >= timeoutMs) {\n throw new ProfileLockTimeoutError(profile, timeoutMs);\n }\n await sleep(Math.min(retryMs, timeoutMs - elapsedMs));\n }\n}\n\n/**\n * One attempt to take the lock. Nothing here replaces an existing path:\n *\n * 1. `mkdir(.lock)` (never recursive) is exclusive, and is the primitive\n * older releases use too, so they exclude each other.\n * 2. Ownership is published by `link`ing a fully written owner file (staged\n * beside `.lock`, on the same filesystem) to `.lock/owner.json`. `link`\n * fails with EEXIST if an owner exists, and with ENOENT if the directory\n * was removed meanwhile (an aged ownerless-lock reclaim). Only a\n * successful link holds the lock; on either failure nothing is released,\n * since nothing was acquired, and the caller retries.\n *\n * A profile deletion removes the profile directory once it has released its\n * lock, so `mkdir(.lock)` can see ENOENT; the directory is recreated (0700)\n * and the caller retries within its deadline, as for a first write to a new\n * profile.\n *\n * Only one owner.json can exist at the lock path, and it is removed only by\n * its holder's release or by recovery of a dead holder, so at most one\n * process holds the lock. Returns false when the lock is not acquired.\n */\nasync function publishProfileLock(profile: string, lockPath: string, token: string): Promise<boolean> {\n try {\n await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });\n } catch (error) {\n if (isErrno(error, \"EEXIST\")) return false;\n if (!isErrno(error, \"ENOENT\")) throw error;\n await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });\n return false;\n }\n await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);\n const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);\n const owner = { pid: process.pid, createdAt: new Date().toISOString(), token };\n try {\n await writeFile(staged, `${JSON.stringify(owner, null, 2)}\\n`, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE, flag: \"wx\" });\n await link(staged, join(lockPath, \"owner.json\"));\n return true;\n } catch (error) {\n if (isErrno(error, \"EEXIST\") || isErrno(error, \"ENOENT\")) return false;\n // Do not leave an ownerless lock behind for others to wait out. rmdir\n // removes only an empty directory; anyone who created it after ours went\n // away sees ENOENT on link and retries.\n await rmdir(lockPath).catch(() => undefined);\n throw error;\n } finally {\n await rm(staged, { force: true }).catch(() => undefined);\n }\n}\n\n/**\n * Releases the lock if `.lock/owner.json` is still this acquisition's. No\n * one else can put an owner record there while it exists (owners are only\n * linked into a directory their own `mkdir` created, and recovery removes\n * only a dead holder's record), so the record read is the one unlinked.\n */\nasync function releaseProfileLock(profile: string, lockPath: string, token: string): Promise<void> {\n const ownerPath = join(lockPath, \"owner.json\");\n const owner = await readJson<{ token?: unknown }>(ownerPath).catch(() => null);\n if (owner?.token !== token) return;\n await rm(ownerPath, { force: true });\n // `.lock` is empty from here until the rmdir; a contender that removes it\n // and creates its own meanwhile is not harmed: rmdir fails on its owner\n // file, or the contender's link sees ENOENT and retries. If a recoverer's\n // claim file is still inside, rmdir fails and that recoverer removes the\n // directory when it drops its claim.\n await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);\n await rmdir(lockPath).catch(() => undefined);\n}\n\n/**\n * Gives process-level contention tests an event-driven witness that a second\n * writer has reached an already-held lock. This deliberately has no effect\n * outside an explicit test environment, and only creates a new signal file\n * beneath TC_HOME; it never changes lock acquisition or release semantics.\n */\nasync function signalTestLockContention(profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n\n const configuredPath = process.env[TEST_LOCK_CONTENTION_SIGNAL_PATH];\n const configuredHome = process.env.TC_HOME;\n if (!configuredPath || !configuredHome) return;\n\n const home = resolve(configuredHome);\n const signalPath = resolve(configuredPath);\n const pathFromHome = relative(home, signalPath);\n if (\n pathFromHome === \"\" ||\n pathFromHome === \"..\" ||\n pathFromHome.startsWith(`..${process.platform === \"win32\" ? \"\\\\\" : \"/\"}`) ||\n pathFromHome.startsWith(\"../\") ||\n pathFromHome.startsWith(\"..\\\\\")\n ) {\n return;\n }\n\n await writeFile(signalPath, `${profile}\\n`, { encoding: \"utf8\", flag: \"wx\" })\n .catch(() => undefined);\n}\n\n/**\n * Reclaims a lock directory with no owner record that is older than the\n * stale threshold: one left by a crash, or by an older release. A process\n * killed while recovering (or releasing, in older releases) can leave its\n * `.stale-*` / `.release-*` claim file behind; such claim files are removed\n * first. owner.json is never touched, and the directory itself is only\n * `rmdir`ed, which succeeds only while it is empty, so a published lock is\n * never removed. If the directory was a contender's lock not yet published,\n * that contender's `link` fails with ENOENT and it retries without holding\n * anything.\n */\nasync function recoverOwnerlessLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n try {\n const { mtimeMs } = await stat(lockPath);\n if (Date.now() - mtimeMs < staleAfterMs) return false;\n } catch {\n return false;\n }\n const checkedAt = performance.now();\n await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);\n try {\n const entries = await readdir(lockPath);\n if (entries.includes(\"owner.json\")) return false;\n // The age check above is only good for a while; see recoveryFenced.\n if (recoveryFenced(checkedAt, staleAfterMs)) return false;\n for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {\n await rm(join(lockPath, name), { force: true });\n }\n await rmdir(lockPath);\n } catch {\n return false;\n }\n await removeAgedOwnerFiles(lockPath, staleAfterMs);\n return true;\n}\n\n/**\n * Whether a recovery step decided at `since` (a monotonic timestamp) is too\n * old to act on. Claim-only and ownerless cleanup act only on a `.lock`\n * unchanged for `staleAfterMs`, and a recoverer's claim (or the change that\n * made a directory look abandoned) dates the directory no earlier than\n * `since`. Acting within half that window means no such cleanup can have\n * removed the directory and let a new holder in meanwhile; past it, the\n * recoverer drops only its own claim and starts the acquisition over.\n */\nfunction recoveryFenced(since: number, staleAfterMs: number): boolean {\n return performance.now() - since >= staleAfterMs / 2;\n}\n\n/** Claim files of stale recovery (and of release in older releases). */\nconst ABANDONED_CLAIM = /^\\.(?:release|stale)-[0-9a-f-]+\\.json$/;\n/** Owner files staged by publishProfileLock beside `.lock`. */\nconst STAGED_OWNER = /^\\.lock-owner-[0-9a-f-]+\\.tmp$/;\n\n/**\n * Removes owner files a crashed acquirer staged beside `.lock`. A live\n * acquirer's staged file exists only for its write and link, so only files\n * older than the stale threshold are removed. Runs after a recovery, never\n * on the uncontended path.\n */\nasync function removeAgedOwnerFiles(lockPath: string, staleAfterMs: number): Promise<void> {\n const directory = dirname(lockPath);\n const names = await readdir(directory).catch(() => []);\n for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {\n const path = join(directory, name);\n const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);\n if (aged) await rm(path, { force: true }).catch(() => undefined);\n }\n}\n\n/**\n * Removes a dead holder's lock. The claim is a hard link to owner.json, so\n * the owner record is never moved away: a recoverer whose observation is\n * outdated (the dead holder's lock was already recovered and a live holder\n * now owns `.lock`) sees another token, drops its link and leaves the live\n * owner record in place. Only after the claimed record is confirmed to be\n * the observed dead holder's is owner.json unlinked. Nothing can link a new\n * owner.json meanwhile: the claim keeps `.lock` non-empty, so it cannot be\n * removed and recreated.\n */\nasync function recoverStaleLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n const ownerPath = join(lockPath, \"owner.json\");\n const owner = await readJson<{ pid?: unknown; createdAt?: unknown; token?: unknown }>(ownerPath)\n .catch(() => null);\n if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);\n if (!isStaleOwner(owner, staleAfterMs)) return false;\n\n const observedToken = typeof owner?.token === \"string\" && owner.token.length > 0\n ? owner.token\n : \"legacy\";\n await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);\n const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);\n try {\n await link(ownerPath, claimPath);\n } catch {\n return false;\n }\n const claimedAt = performance.now();\n await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);\n\n const claimed = await readJson<{ pid?: unknown; createdAt?: unknown; token?: unknown }>(claimPath)\n .catch(() => null);\n const sameInstance = claimed !== null && (\n observedToken === \"legacy\"\n ? claimed.token === undefined\n : claimed.token === observedToken\n );\n await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);\n if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {\n // Paused too long: another recoverer may have finished this recovery and\n // a cleanup may have removed this claim, so owner.json may now be a live\n // holder's. Touch nothing but this claim.\n await rm(claimPath, { force: true });\n await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);\n return false;\n }\n if (sameInstance) {\n await rm(ownerPath, { force: true });\n await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);\n }\n await rm(claimPath, { force: true });\n // Empty now if the claimed holder is gone or a live holder released while\n // this claim kept `.lock` from being removed: remove it rather than leave\n // an ownerless lock to age out. rmdir fails while an owner record exists.\n await rmdir(lockPath).catch(() => undefined);\n if (!sameInstance) return false;\n await removeAgedOwnerFiles(lockPath, staleAfterMs);\n return true;\n}\n\n/** Test-only rendezvous (NODE_ENV=test and the named barrier directory set). */\nasync function waitForTestBarrier(environmentName: string, profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n const barrierDirectory = process.env[environmentName];\n if (!barrierDirectory) return;\n await mkdir(barrierDirectory, { recursive: true });\n const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);\n await writeFile(readyPath, \"ready\\n\", { encoding: \"utf8\", flag: \"wx\" }).catch(() => undefined);\n const releasePath = join(barrierDirectory, \"release\");\n while (true) {\n try {\n await readFile(releasePath, \"utf8\");\n return;\n } catch {\n await sleep(1);\n }\n }\n}\n\nfunction isStaleOwner(\n owner: { pid?: unknown; createdAt?: unknown } | null,\n staleAfterMs: number,\n): boolean {\n const now = Date.now();\n const createdAt = typeof owner?.createdAt === \"string\"\n ? Date.parse(owner.createdAt)\n : Number.NaN;\n\n // Reclaim only a fully published lock whose owner PID is confirmed dead.\n // A lock without an owner record is handled by recoverOwnerlessLock: a\n // process holds the lock only once its owner.json is linked in.\n if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;\n if (typeof owner?.pid !== \"number\" || !Number.isInteger(owner.pid) || owner.pid <= 0) {\n return false;\n }\n return !isProcessAlive(owner.pid);\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return !isErrno(error, \"ESRCH\");\n }\n}\n\nfunction validateProfileName(profile: string): string {\n if (\n !profile ||\n profile === \".\" ||\n profile === \"..\" ||\n profile.includes(\"/\") ||\n profile.includes(\"\\\\\") ||\n profile.includes(\"\\0\")\n ) {\n throw new TypeError(\"Profile names must be non-empty path segments.\");\n }\n return profile;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n return typeof error === \"object\" && error !== null && \"code\" in error &&\n (error as { code?: unknown }).code === code;\n}\n\nfunction sleep(milliseconds: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, milliseconds));\n}\n\nexport { DEFAULT_PROFILE };\n"],"mappings":";AAAA,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe;AACxB,SAAS,UAAU,SAAS,YAAY,MAAM,UAAU,eAAe;AAEvE,IAAM,kBAAkB;AACxB,IAAM,0BAA0B;AAChC,IAAM,wBAAwB;AAC9B,IAAM,wBAAwB;AAC9B,IAAM,mCAAmC;AAEzC,IAAM,oBAAoB;AAC1B,IAAM,mBAAmB;AACzB,IAAM,iCAAiC;AACvC,IAAM,kCAAkC;AACxC,IAAM,gCAAgC;AACtC,IAAM,gCAAgC;AACtC,IAAM,8BAA8B;AACpC,IAAM,gCAAgC;AACtC,IAAM,+BAA+B;AACrC,IAAM,iCAAiC;AACvC,IAAM,sBAAsB,IAAI,kBAA0B;AAO1D,IAAM,mBAAmB,IAAI,kBAA8C;AAsBpE,IAAM,0BAAN,cAAsC,MAAM;AAAA,EAGjD,YAAY,SAAiB,WAAmB;AAC9C,UAAM,+CAA+C,OAAO,WAAW,SAAS,KAAK;AAHvF,SAAS,OAAO;AAId,SAAK,OAAO;AAAA,EACd;AACF;AAMO,SAAS,oBAA4B;AAC1C,QAAM,OAAO,oBAAoB,SAAS,KACxC,QAAQ,IAAI,WACZ,QAAQ,IAAI,QACZ,QAAQ,IAAI,eACZ,QAAQ;AACV,SAAO,KAAK,MAAM,YAAY;AAChC;AAGO,SAAS,uBACd,WACA,QACY;AACZ,MAAI,cAAc,OAAW,QAAO,OAAO;AAC3C,MAAI,CAAC,WAAW,SAAS,KAAK,UAAU,SAAS,IAAI,GAAG;AACtD,UAAM,IAAI,UAAU,oDAAoD;AAAA,EAC1E;AACA,SAAO,oBAAoB,IAAI,QAAQ,SAAS,GAAG,MAAM;AAC3D;AAEO,SAAS,sBAA8B;AAC5C,SAAO,KAAK,kBAAkB,GAAG,aAAa;AAChD;AAEO,SAAS,eAAuB;AACrC,SAAO,KAAK,kBAAkB,GAAG,UAAU;AAC7C;AAEO,SAAS,YAAY,SAAyB;AACnD,SAAO,KAAK,aAAa,GAAG,oBAAoB,OAAO,CAAC;AAC1D;AAEO,SAAS,kBAAkB,SAAyB;AACzD,SAAO,KAAK,YAAY,OAAO,GAAG,cAAc;AAClD;AAEO,SAAS,YAAY,SAAyB;AACnD,SAAO,iBAAiB,SAAS,SAAS;AAC5C;AAEO,SAAS,0BAA0B,SAAyB;AACjE,SAAO,iBAAiB,SAAS,wBAAwB;AAC3D;AAEO,SAAS,iBAAiB,SAAyB;AACxD,SAAO,iBAAiB,SAAS,eAAe;AAClD;AAEO,SAAS,iBAAiB,SAAiB,OAAiC;AACjF,SAAO,KAAK,YAAY,OAAO,GAAG,GAAG,KAAK,OAAO;AACnD;AAGO,SAAS,yBAAyB,SAAiB,OAAiC;AACzF,SAAO,GAAG,iBAAiB,SAAS,KAAK,CAAC;AAC5C;AAEO,SAAS,gBAAgB,SAAyB;AACvD,SAAO,KAAK,YAAY,OAAO,GAAG,OAAO;AAC3C;AAEO,SAAS,wBAAwB,SAAyB;AAC/D,SAAO,KAAK,gBAAgB,OAAO,GAAG,YAAY;AACpD;AAMA,eAAsB,SAAY,UAAqC;AACrE,MAAI;AACF,WAAO,KAAK,MAAM,MAAM,SAAS,UAAU,MAAM,CAAC;AAAA,EACpD,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,QAAQ,EAAG,QAAO;AACrC,UAAM;AAAA,EACR;AACF;AAMA,eAAsB,gBAAgB,UAAkB,OAA+B;AACrF,QAAM,YAAY,QAAQ,QAAQ;AAClC,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA,IAAI,SAAS,QAAQ,CAAC,IAAI,QAAQ,GAAG,IAAI,WAAW,CAAC;AAAA,EACvD;AACA,QAAM,WAAW,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA;AAElD,QAAM,MAAM,WAAW,EAAE,WAAW,MAAM,MAAM,iBAAiB,CAAC;AAClE,MAAI;AACF,UAAM,UAAU,eAAe,UAAU,EAAE,UAAU,QAAQ,MAAM,kBAAkB,CAAC;AACtF,UAAM,OAAO,eAAe,QAAQ;AAAA,EACtC,SAAS,OAAO;AACd,UAAM,GAAG,eAAe,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAC9D,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,YACpB,SACmB;AACnB,SAAO,SAAY,kBAAkB,OAAO,CAAC;AAC/C;AAEA,eAAsB,YACpB,SACmB;AACnB,QAAM,kBAAkB,SAAS,SAAS;AAC1C,SAAO,SAAY,YAAY,OAAO,CAAC;AACzC;AAEA,eAAsB,0BACpB,SACc;AACd,UAAQ,MAAM,iBAAoB,SAAS,wBAAwB,GAAG;AACxE;AAEA,eAAsB,iBACpB,SACc;AACd,UAAQ,MAAM,iBAAoB,SAAS,eAAe,GAAG;AAC/D;AAEA,eAAsB,kBACpB,SACA,OACwB;AACxB,QAAM,WAAW,MAAM,SAAwB,yBAAyB,SAAS,KAAK,CAAC;AACvF,MAAI,aAAa,KAAM,QAAO,EAAE,eAAe,EAAE;AACjD,MACE,OAAO,aAAa,YACpB,SAAS,kBAAkB,GAC3B;AACA,UAAM,IAAI,UAAU,iCAAiC,KAAK,IAAI;AAAA,EAChE;AACA,SAAO;AACT;AAEA,eAAsB,iBACpB,SACA,OACkC;AAClC,QAAM,CAAC,UAAU,GAAG,IAAI,MAAM,QAAQ,IAAI;AAAA,IACxC,kBAAkB,SAAS,KAAK;AAAA,IAChC,SAAkB,iBAAiB,SAAS,KAAK,CAAC;AAAA,EACpD,CAAC;AACD,SAAO;AAAA,IACL,eAAe,SAAS;AAAA,IACxB,SAAS,MAAM,QAAQ,GAAG,IAAI,MAAa,CAAC;AAAA,EAC9C;AACF;AAeA,eAAsB,gBACpB,SACA,QACA,UAA8B,CAAC,GACnB;AACZ,QAAM,oBAAoB,oBAAoB,OAAO;AACrD,QAAM,WAAW,gBAAgB,iBAAiB;AAClD,QAAM,QAAQ,iBAAiB,SAAS,KAAK,CAAC,GAAG,OAAO,CAACA,eAAcA,WAAU,MAAM;AACvF,MAAI,KAAK,KAAK,CAACA,eAAcA,WAAU,aAAa,QAAQ,EAAG,QAAO,OAAO;AAC7E,QAAM,UAAU,MAAM,mBAAmB,mBAAmB,OAAO;AACnE,QAAM,YAA6B,EAAE,UAAU,QAAQ,KAAK;AAC5D,MAAI;AACF,WAAO,MAAM,iBAAiB,IAAI,CAAC,GAAG,MAAM,SAAS,GAAG,MAAM;AAAA,EAChE,UAAE;AACA,cAAU,SAAS;AACnB,UAAM,QAAQ;AAAA,EAChB;AACF;AAOA,eAAsB,oBACpB,SACA,OACA,KACA,QACA,QACA,UAA8B,CAAC,GACjB;AACd,MAAI,CAAC,IAAK,OAAM,IAAI,UAAU,qCAAqC;AAEnE,SAAO,gBAAgB,SAAS,YAAY;AAC1C,UAAM,WAAW,MAAM,iBAAoB,SAAS,KAAK,GAAG;AAC5D,UAAM,OAAO,QAAQ,OAAO,CAAC,cAAc,OAAO,SAAS,MAAM,GAAG;AACpE,SAAK,KAAK,MAAM;AAChB,UAAM,gBAAgB,iBAAiB,SAAS,KAAK,GAAG,IAAI;AAC5D,UAAM,uBAAuB,SAAS,KAAK;AAC3C,WAAO;AAAA,EACT,GAAG,OAAO;AACZ;AAOA,eAAsB,mBACpB,SACA,OACA,QAMA,UAA8B,CAAC,GACd;AACjB,SAAO;AAAA,IACL;AAAA,IACA,MAAM,8BAA8B,SAAS,OAAO,MAAM;AAAA,IAC1D;AAAA,EACF;AACF;AAGA,eAAe,8BACb,SACA,OACA,QAMiB;AACjB,QAAM,UAAU,MAAM,iBAAoB,SAAS,KAAK;AACxD,QAAM,OAAO,MAAM,OAAO,QAAQ,OAAO;AACzC,QAAM,gBAAgB,iBAAiB,SAAS,KAAK,GAAG,KAAK,OAAO;AACpE,QAAM,uBAAuB,SAAS,KAAK;AAC3C,SAAO,KAAK;AACd;AAEA,eAAsB,aACpB,SACA,SACA,UAA8B,CAAC,GAChB;AACf,QAAM,gBAAgB,SAAS,YAAY;AACzC,UAAM,kBAAkB,SAAS,SAAS;AAC1C,UAAM,gBAAgB,YAAY,OAAO,GAAG,OAAO;AACnD,UAAM,uBAAuB,SAAS,SAAS;AAAA,EACjD,GAAG,OAAO;AACZ;AAEA,eAAsB,cACpB,SACA,UAA8B,CAAC,GAChB;AACf,QAAM,gBAAgB,SAAS,YAAY;AACzC,UAAM,kBAAkB,SAAS,SAAS;AAC1C,UAAM,GAAG,YAAY,OAAO,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,EAChD,GAAG,OAAO;AACZ;AAEA,eAAe,uBAAuB,SAAiB,OAAwC;AAC7F,QAAM,gBAAgB,yBAAyB,SAAS,KAAK,GAAG,EAAE,eAAe,EAAE,CAAC;AACtF;AAEA,eAAe,mBACb,SACA,SAC8B;AAC9B,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,YAAY,KAAK,IAAI;AAC3B,QAAM,WAAW,gBAAgB,OAAO;AAExC,QAAM,YAAY,YAAY,OAAO;AACrC,QAAM,MAAM,WAAW,EAAE,WAAW,MAAM,MAAM,iBAAiB,CAAC;AAGlE,aAAW,QAAQ,CAAC,kBAAkB,GAAG,aAAa,GAAG,SAAS,GAAG;AACnE,UAAM,MAAM,MAAM,gBAAgB,EAAE,MAAM,CAAC,UAAmB;AAG5D,UAAI,SAAS,aAAa,CAAC,QAAQ,OAAO,QAAQ,EAAG,OAAM;AAAA,IAC7D,CAAC;AAAA,EACH;AAEA,SAAO,MAAM;AACX,UAAM,QAAQ,WAAW;AACzB,QAAI,MAAM,mBAAmB,SAAS,UAAU,KAAK,GAAG;AACtD,aAAO,YAAY;AACjB,cAAM,mBAAmB,SAAS,UAAU,KAAK;AAAA,MACnD;AAAA,IACF;AACA,UAAM,yBAAyB,OAAO;AAEtC,QAAI,MAAM,iBAAiB,SAAS,UAAU,YAAY,EAAG;AAE7D,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,QAAI,aAAa,WAAW;AAC1B,YAAM,IAAI,wBAAwB,SAAS,SAAS;AAAA,IACtD;AACA,UAAM,MAAM,KAAK,IAAI,SAAS,YAAY,SAAS,CAAC;AAAA,EACtD;AACF;AAuBA,eAAe,mBAAmB,SAAiB,UAAkB,OAAiC;AACpG,MAAI;AACF,UAAM,MAAM,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAAA,EAClD,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,QAAQ,EAAG,QAAO;AACrC,QAAI,CAAC,QAAQ,OAAO,QAAQ,EAAG,OAAM;AACrC,UAAM,MAAM,YAAY,OAAO,GAAG,EAAE,WAAW,MAAM,MAAM,iBAAiB,CAAC;AAC7E,WAAO;AAAA,EACT;AACA,QAAM,mBAAmB,+BAA+B,OAAO;AAC/D,QAAM,SAAS,KAAK,QAAQ,QAAQ,GAAG,eAAe,KAAK,MAAM;AACjE,QAAM,QAAQ,EAAE,KAAK,QAAQ,KAAK,YAAW,oBAAI,KAAK,GAAE,YAAY,GAAG,MAAM;AAC7E,MAAI;AACF,UAAM,UAAU,QAAQ,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,GAAM,EAAE,UAAU,QAAQ,MAAM,mBAAmB,MAAM,KAAK,CAAC;AACxH,UAAM,KAAK,QAAQ,KAAK,UAAU,YAAY,CAAC;AAC/C,WAAO;AAAA,EACT,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,QAAQ,KAAK,QAAQ,OAAO,QAAQ,EAAG,QAAO;AAIjE,UAAM,MAAM,QAAQ,EAAE,MAAM,MAAM,MAAS;AAC3C,UAAM;AAAA,EACR,UAAE;AACA,UAAM,GAAG,QAAQ,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,EACzD;AACF;AAQA,eAAe,mBAAmB,SAAiB,UAAkB,OAA8B;AACjG,QAAM,YAAY,KAAK,UAAU,YAAY;AAC7C,QAAM,QAAQ,MAAM,SAA8B,SAAS,EAAE,MAAM,MAAM,IAAI;AAC7E,MAAI,OAAO,UAAU,MAAO;AAC5B,QAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AAMnC,QAAM,mBAAmB,+BAA+B,OAAO;AAC/D,QAAM,MAAM,QAAQ,EAAE,MAAM,MAAM,MAAS;AAC7C;AAQA,eAAe,yBAAyB,SAAgC;AACtE,MAAI,QAAQ,IAAI,aAAa,OAAQ;AAErC,QAAM,iBAAiB,QAAQ,IAAI,gCAAgC;AACnE,QAAM,iBAAiB,QAAQ,IAAI;AACnC,MAAI,CAAC,kBAAkB,CAAC,eAAgB;AAExC,QAAM,OAAO,QAAQ,cAAc;AACnC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,eAAe,SAAS,MAAM,UAAU;AAC9C,MACE,iBAAiB,MACjB,iBAAiB,QACjB,aAAa,WAAW,KAAK,QAAQ,aAAa,UAAU,OAAO,GAAG,EAAE,KACxE,aAAa,WAAW,KAAK,KAC7B,aAAa,WAAW,MAAM,GAC9B;AACA;AAAA,EACF;AAEA,QAAM,UAAU,YAAY,GAAG,OAAO;AAAA,GAAM,EAAE,UAAU,QAAQ,MAAM,KAAK,CAAC,EACzE,MAAM,MAAM,MAAS;AAC1B;AAaA,eAAe,qBAAqB,SAAiB,UAAkB,cAAwC;AAC7G,MAAI;AACF,UAAM,EAAE,QAAQ,IAAI,MAAM,KAAK,QAAQ;AACvC,QAAI,KAAK,IAAI,IAAI,UAAU,aAAc,QAAO;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,YAAY,YAAY,IAAI;AAClC,QAAM,mBAAmB,iCAAiC,OAAO;AACjE,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,QAAQ;AACtC,QAAI,QAAQ,SAAS,YAAY,EAAG,QAAO;AAE3C,QAAI,eAAe,WAAW,YAAY,EAAG,QAAO;AACpD,eAAW,QAAQ,QAAQ,OAAO,CAAC,UAAU,gBAAgB,KAAK,KAAK,CAAC,GAAG;AACzE,YAAM,GAAG,KAAK,UAAU,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,IAChD;AACA,UAAM,MAAM,QAAQ;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,qBAAqB,UAAU,YAAY;AACjD,SAAO;AACT;AAWA,SAAS,eAAe,OAAe,cAA+B;AACpE,SAAO,YAAY,IAAI,IAAI,SAAS,eAAe;AACrD;AAGA,IAAM,kBAAkB;AAExB,IAAM,eAAe;AAQrB,eAAe,qBAAqB,UAAkB,cAAqC;AACzF,QAAM,YAAY,QAAQ,QAAQ;AAClC,QAAM,QAAQ,MAAM,QAAQ,SAAS,EAAE,MAAM,MAAM,CAAC,CAAC;AACrD,aAAW,QAAQ,MAAM,OAAO,CAAC,UAAU,aAAa,KAAK,KAAK,CAAC,GAAG;AACpE,UAAM,OAAO,KAAK,WAAW,IAAI;AACjC,UAAM,OAAO,MAAM,KAAK,IAAI,EAAE,KAAK,CAAC,EAAE,QAAQ,MAAM,KAAK,IAAI,IAAI,WAAW,cAAc,MAAM,KAAK;AACrG,QAAI,KAAM,OAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,EACjE;AACF;AAYA,eAAe,iBAAiB,SAAiB,UAAkB,cAAwC;AACzG,QAAM,YAAY,KAAK,UAAU,YAAY;AAC7C,QAAM,QAAQ,MAAM,SAAkE,SAAS,EAC5F,MAAM,MAAM,IAAI;AACnB,MAAI,UAAU,KAAM,QAAO,qBAAqB,SAAS,UAAU,YAAY;AAC/E,MAAI,CAAC,aAAa,OAAO,YAAY,EAAG,QAAO;AAE/C,QAAM,gBAAgB,OAAO,OAAO,UAAU,YAAY,MAAM,MAAM,SAAS,IAC3E,MAAM,QACN;AACJ,QAAM,mBAAmB,gCAAgC,OAAO;AAChE,QAAM,YAAY,KAAK,UAAU,UAAU,WAAW,CAAC,OAAO;AAC9D,MAAI;AACF,UAAM,KAAK,WAAW,SAAS;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,YAAY,YAAY,IAAI;AAClC,QAAM,mBAAmB,6BAA6B,OAAO;AAE7D,QAAM,UAAU,MAAM,SAAkE,SAAS,EAC9F,MAAM,MAAM,IAAI;AACnB,QAAM,eAAe,YAAY,SAC/B,kBAAkB,WACd,QAAQ,UAAU,SAClB,QAAQ,UAAU;AAExB,QAAM,mBAAmB,gCAAgC,OAAO;AAChE,MAAI,gBAAgB,eAAe,WAAW,YAAY,GAAG;AAI3D,UAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AACnC,UAAM,mBAAmB,8BAA8B,OAAO;AAC9D,WAAO;AAAA,EACT;AACA,MAAI,cAAc;AAChB,UAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AACnC,UAAM,mBAAmB,+BAA+B,OAAO;AAAA,EACjE;AACA,QAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AAInC,QAAM,MAAM,QAAQ,EAAE,MAAM,MAAM,MAAS;AAC3C,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,qBAAqB,UAAU,YAAY;AACjD,SAAO;AACT;AAGA,eAAe,mBAAmB,iBAAyB,SAAgC;AACzF,MAAI,QAAQ,IAAI,aAAa,OAAQ;AACrC,QAAM,mBAAmB,QAAQ,IAAI,eAAe;AACpD,MAAI,CAAC,iBAAkB;AACvB,QAAM,MAAM,kBAAkB,EAAE,WAAW,KAAK,CAAC;AACjD,QAAM,YAAY,KAAK,kBAAkB,SAAS,QAAQ,GAAG,IAAI,OAAO,EAAE;AAC1E,QAAM,UAAU,WAAW,WAAW,EAAE,UAAU,QAAQ,MAAM,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAC7F,QAAM,cAAc,KAAK,kBAAkB,SAAS;AACpD,SAAO,MAAM;AACX,QAAI;AACF,YAAM,SAAS,aAAa,MAAM;AAClC;AAAA,IACF,QAAQ;AACN,YAAM,MAAM,CAAC;AAAA,IACf;AAAA,EACF;AACF;AAEA,SAAS,aACP,OACA,cACS;AACT,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,YAAY,OAAO,OAAO,cAAc,WAC1C,KAAK,MAAM,MAAM,SAAS,IAC1B,OAAO;AAKX,MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,MAAM,YAAY,aAAc,QAAO;AAC1E,MAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,UAAU,MAAM,GAAG,KAAK,MAAM,OAAO,GAAG;AACpF,WAAO;AAAA,EACT;AACA,SAAO,CAAC,eAAe,MAAM,GAAG;AAClC;AAEA,SAAS,eAAe,KAAsB;AAC5C,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,CAAC,QAAQ,OAAO,OAAO;AAAA,EAChC;AACF;AAEA,SAAS,oBAAoB,SAAyB;AACpD,MACE,CAAC,WACD,YAAY,OACZ,YAAY,QACZ,QAAQ,SAAS,GAAG,KACpB,QAAQ,SAAS,IAAI,KACrB,QAAQ,SAAS,IAAI,GACrB;AACA,UAAM,IAAI,UAAU,gDAAgD;AAAA,EACtE;AACA,SAAO;AACT;AAEA,SAAS,QAAQ,OAAgB,MAAuB;AACtD,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,SAC7D,MAA6B,SAAS;AAC3C;AAEA,SAAS,MAAM,cAAqC;AAClD,SAAO,IAAI,QAAQ,CAACC,aAAY,WAAWA,UAAS,YAAY,CAAC;AACnE;","names":["ownership","resolve"]}
1
+ {"version":3,"sources":["../src/state.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport {\n chmod,\n link,\n mkdir,\n readFile,\n readdir,\n rename,\n rmdir,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, relative, resolve } from \"node:path\";\n\nconst DEFAULT_PROFILE = \"default\";\nconst DEFAULT_LOCK_TIMEOUT_MS = 2_000;\nconst DEFAULT_LOCK_RETRY_MS = 25;\nconst DEFAULT_STALE_LOCK_MS = 30_000;\nconst TEST_LOCK_CONTENTION_SIGNAL_PATH = \"TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH\";\n// Profile state holds keys, sessions and delegations: owner-only access.\nconst PRIVATE_FILE_MODE = 0o600;\nconst PRIVATE_DIR_MODE = 0o700;\nconst TEST_LOCK_RECOVERY_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR\";\nconst TEST_LOCK_OWNERLESS_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR\";\nconst TEST_LOCK_PUBLISH_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR\";\nconst TEST_LOCK_RELEASE_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR\";\nconst TEST_LOCK_CLAIM_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR\";\nconst TEST_LOCK_CLAIMED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR\";\nconst TEST_LOCK_FENCED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR\";\nconst TEST_LOCK_VERIFIED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR\";\n/** One acquisition of a profile lock; `active` is cleared before release. */\ninterface HeldProfileLock {\n readonly lockPath: string;\n active: boolean;\n}\n/**\n * Async context shared by every copy of this module in the process. Each\n * bundled operations entry point (`state`, `delegation-binding`, the root)\n * carries its own copy, so module-level context would make lock reentrancy and\n * the invocation state root stop at an entry-point boundary.\n *\n * The `.v1` in each key is the format version shared by every copy that reads\n * it. Bump it whenever the `HeldProfileLock` entry shape or the on-disk lock\n * protocol changes, so copies with different formats (two operations versions\n * in one process) stop trusting each other's held-lock entries and simply\n * wait for the lock instead. TC-633 changes the held-lock shape and adds a\n * per-process `turnsInProgress` set: whichever of TC-602 and TC-633 lands\n * second bumps these keys to `.v2` and makes that set process-wide under the\n * same version.\n */\nfunction processWideContext<T>(key: string): AsyncLocalStorage<T> {\n const registry = globalThis as unknown as Record<symbol, AsyncLocalStorage<T> | undefined>;\n return registry[Symbol.for(key)] ??= new AsyncLocalStorage<T>();\n}\nconst invocationStateRoot = processWideContext<string>(\"tinycloud.operations.invocationStateRoot.v1\");\n/** Profile lock acquisitions held by the current async call chain (see withProfileLock). */\nconst heldProfileLocks = processWideContext<readonly HeldProfileLock[]>(\"tinycloud.operations.heldProfileLocks.v1\");\n\nexport type ProfileStoreName =\n | \"session\"\n | \"additional-delegations\"\n | \"auth-requests\";\n\nexport interface StoreMetadata {\n formatVersion: number;\n}\n\nexport interface ProfileStoreContents<T> {\n formatVersion: number;\n records: T[];\n}\n\nexport interface ProfileLockOptions {\n timeoutMs?: number;\n retryMs?: number;\n staleAfterMs?: number;\n}\n\nexport class ProfileLockTimeoutError extends Error {\n readonly code = \"PROFILE_LOCK_TIMEOUT\";\n\n constructor(profile: string, timeoutMs: number) {\n super(`Timed out waiting for the profile lock for \"${profile}\" after ${timeoutMs}ms.`);\n this.name = \"ProfileLockTimeoutError\";\n }\n}\n\n/**\n * The CLI's delegated-secret path treats TC_HOME as a home directory, rather\n * than a direct TinyCloud directory. Keep the shared store on that convention.\n */\nexport function tinycloudHomePath(): string {\n const home = invocationStateRoot.getStore() ??\n process.env.TC_HOME ??\n process.env.HOME ??\n process.env.USERPROFILE ??\n homedir();\n return join(home, \".tinycloud\");\n}\n\n/** Run one operation against an isolated TinyCloud home directory. */\nexport function withTinyCloudStateRoot<T>(\n stateRoot: string | undefined,\n action: () => Promise<T>,\n): Promise<T> {\n if (stateRoot === undefined) return action();\n if (!isAbsolute(stateRoot) || stateRoot.includes(\"\\0\")) {\n throw new TypeError(\"The TinyCloud state root must be an absolute path.\");\n }\n return invocationStateRoot.run(resolve(stateRoot), action);\n}\n\nexport function tinycloudConfigPath(): string {\n return join(tinycloudHomePath(), \"config.json\");\n}\n\nexport function profilesPath(): string {\n return join(tinycloudHomePath(), \"profiles\");\n}\n\nexport function profilePath(profile: string): string {\n return join(profilesPath(), validateProfileName(profile));\n}\n\nexport function profileConfigPath(profile: string): string {\n return join(profilePath(profile), \"profile.json\");\n}\n\nexport function sessionPath(profile: string): string {\n return profileStorePath(profile, \"session\");\n}\n\nexport function additionalDelegationsPath(profile: string): string {\n return profileStorePath(profile, \"additional-delegations\");\n}\n\nexport function authRequestsPath(profile: string): string {\n return profileStorePath(profile, \"auth-requests\");\n}\n\nexport function profileStorePath(profile: string, store: ProfileStoreName): string {\n return join(profilePath(profile), `${store}.json`);\n}\n\n/** The format record is deliberately separate from each legacy JSON payload. */\nexport function profileStoreMetadataPath(profile: string, store: ProfileStoreName): string {\n return `${profileStorePath(profile, store)}.metadata.json`;\n}\n\nexport function profileLockPath(profile: string): string {\n return join(profilePath(profile), \".lock\");\n}\n\nexport function profileLockMetadataPath(profile: string): string {\n return join(profileLockPath(profile), \"owner.json\");\n}\n\n/**\n * Reads JSON without hiding malformed or inaccessible files. A missing file is\n * the only condition represented as null.\n */\nexport async function readJson<T>(filePath: string): Promise<T | null> {\n try {\n return JSON.parse(await readFile(filePath, \"utf8\")) as T;\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return null;\n throw error;\n }\n}\n\n/**\n * Writes the same pretty-printed, trailing-newline JSON shape used by the CLI,\n * but publishes it with an atomic rename from the same directory.\n */\nexport async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n const directory = dirname(filePath);\n const temporaryPath = join(\n directory,\n `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`,\n );\n const contents = `${JSON.stringify(value, null, 2)}\\n`;\n\n await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });\n try {\n await writeFile(temporaryPath, contents, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE });\n await rename(temporaryPath, filePath);\n } catch (error) {\n await rm(temporaryPath, { force: true }).catch(() => undefined);\n throw error;\n }\n}\n\nexport async function readProfile<T extends object = Record<string, unknown>>(\n profile: string,\n): Promise<T | null> {\n return readJson<T>(profileConfigPath(profile));\n}\n\nexport async function readSession<T extends object = Record<string, unknown>>(\n profile: string,\n): Promise<T | null> {\n await readStoreMetadata(profile, \"session\");\n return readJson<T>(sessionPath(profile));\n}\n\nexport async function readAdditionalDelegations<T = Record<string, unknown>>(\n profile: string,\n): Promise<T[]> {\n return (await readProfileStore<T>(profile, \"additional-delegations\")).records;\n}\n\nexport async function readAuthRequests<T = Record<string, unknown>>(\n profile: string,\n): Promise<T[]> {\n return (await readProfileStore<T>(profile, \"auth-requests\")).records;\n}\n\nexport async function readStoreMetadata(\n profile: string,\n store: ProfileStoreName,\n): Promise<StoreMetadata> {\n const metadata = await readJson<StoreMetadata>(profileStoreMetadataPath(profile, store));\n if (metadata === null) return { formatVersion: 1 };\n if (\n typeof metadata !== \"object\" ||\n metadata.formatVersion !== 1\n ) {\n throw new TypeError(`Unsupported store format for \"${store}\".`);\n }\n return metadata;\n}\n\nexport async function readProfileStore<T>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n): Promise<ProfileStoreContents<T>> {\n const [metadata, raw] = await Promise.all([\n readStoreMetadata(profile, store),\n readJson<unknown>(profileStorePath(profile, store)),\n ]);\n return {\n formatVersion: metadata.formatVersion,\n records: Array.isArray(raw) ? raw as T[] : [],\n };\n}\n\n/**\n * Runs a small critical section under the one advisory lock shared by all\n * profile stores. Lock release verifies its ownership token before removing\n * the exact metadata instance it acquired.\n *\n * Reentrant within one async call chain: a critical section that already\n * holds a profile's lock (for example a login's compare-and-commit) can call\n * store writers that take the same lock without deadlocking. Ownership is\n * keyed by the resolved lock path (so switching state roots inside a lock\n * still takes the other root's lock) and lasts exactly as long as the\n * acquisition: it is revoked before release, so work deferred past the\n * critical section waits for the lock like any other caller.\n */\nexport async function withProfileLock<T>(\n profile: string,\n action: () => Promise<T>,\n options: ProfileLockOptions = {},\n): Promise<T> {\n const normalizedProfile = validateProfileName(profile);\n const lockPath = profileLockPath(normalizedProfile);\n const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);\n if (held.some((ownership) => ownership.lockPath === lockPath)) return action();\n const release = await acquireProfileLock(normalizedProfile, options);\n const ownership: HeldProfileLock = { lockPath, active: true };\n try {\n return await heldProfileLocks.run([...held, ownership], action);\n } finally {\n ownership.active = false;\n await release();\n }\n}\n\n/**\n * Appends a record or replaces the existing record with the supplied explicit\n * key. The extractor is supplied by the owning store so state.ts never needs\n * to know permission-request or delegation record shapes.\n */\nexport async function upsertProfileRecord<T>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n key: string,\n record: T,\n getKey: (candidate: T) => string | undefined,\n options: ProfileLockOptions = {},\n): Promise<T[]> {\n if (!key) throw new TypeError(\"A non-empty record key is required.\");\n\n return withProfileLock(profile, async () => {\n const current = (await readProfileStore<T>(profile, store)).records;\n const next = current.filter((candidate) => getKey(candidate) !== key);\n next.push(record);\n await writeJsonAtomic(profileStorePath(profile, store), next);\n await writeFormatOneMetadata(profile, store);\n return next;\n }, options);\n}\n\n/**\n * Performs a typed read-modify-write of a legacy array store under the one\n * per-profile lock. The payload stays an array and the sibling metadata stays\n * format 1, so existing CLI readers keep their byte/layout contract.\n */\nexport async function updateProfileStore<T, Result>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n update: (\n records: readonly T[],\n ) => Promise<{ readonly records: readonly T[]; readonly result: Result }> | {\n readonly records: readonly T[];\n readonly result: Result;\n },\n options: ProfileLockOptions = {},\n): Promise<Result> {\n return withProfileLock(\n profile,\n () => updateProfileStoreWhileLocked(profile, store, update),\n options,\n );\n}\n\n/** The caller must already own this profile's lock. */\nasync function updateProfileStoreWhileLocked<T, Result>(\n profile: string,\n store: Exclude<ProfileStoreName, \"session\">,\n update: (\n records: readonly T[],\n ) => Promise<{ readonly records: readonly T[]; readonly result: Result }> | {\n readonly records: readonly T[];\n readonly result: Result;\n },\n): Promise<Result> {\n const current = await readProfileStore<T>(profile, store);\n const next = await update(current.records);\n await writeJsonAtomic(profileStorePath(profile, store), next.records);\n await writeFormatOneMetadata(profile, store);\n return next.result;\n}\n\nexport async function writeSession<T extends object>(\n profile: string,\n session: T,\n options: ProfileLockOptions = {},\n): Promise<void> {\n await withProfileLock(profile, async () => {\n await readStoreMetadata(profile, \"session\");\n await writeJsonAtomic(sessionPath(profile), session);\n await writeFormatOneMetadata(profile, \"session\");\n }, options);\n}\n\nexport async function removeSession(\n profile: string,\n options: ProfileLockOptions = {},\n): Promise<void> {\n await withProfileLock(profile, async () => {\n await readStoreMetadata(profile, \"session\");\n await rm(sessionPath(profile), { force: true });\n }, options);\n}\n\nasync function writeFormatOneMetadata(profile: string, store: ProfileStoreName): Promise<void> {\n await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });\n}\n\nasync function acquireProfileLock(\n profile: string,\n options: ProfileLockOptions,\n): Promise<() => Promise<void>> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n const retryMs = options.retryMs ?? DEFAULT_LOCK_RETRY_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;\n const startedAt = Date.now();\n const lockPath = profileLockPath(profile);\n\n const directory = profilePath(profile);\n await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });\n // Every store write takes this lock: tighten the tree older releases\n // created 0775 before writing sessions or delegations into it.\n for (const path of [tinycloudHomePath(), profilesPath(), directory]) {\n await chmod(path, PRIVATE_DIR_MODE).catch((error: unknown) => {\n // A profile deletion can remove the profile directory right after the\n // mkdir above; the lock attempt below recreates it 0700.\n if (path !== directory || !isErrno(error, \"ENOENT\")) throw error;\n });\n }\n\n while (true) {\n const token = randomUUID();\n if (await publishProfileLock(profile, lockPath, token)) {\n return async () => {\n await releaseProfileLock(profile, lockPath, token);\n };\n }\n await signalTestLockContention(profile);\n\n if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;\n\n const elapsedMs = Date.now() - startedAt;\n if (elapsedMs >= timeoutMs) {\n throw new ProfileLockTimeoutError(profile, timeoutMs);\n }\n await sleep(Math.min(retryMs, timeoutMs - elapsedMs));\n }\n}\n\n/**\n * One attempt to take the lock. Nothing here replaces an existing path:\n *\n * 1. `mkdir(.lock)` (never recursive) is exclusive, and is the primitive\n * older releases use too, so they exclude each other.\n * 2. Ownership is published by `link`ing a fully written owner file (staged\n * beside `.lock`, on the same filesystem) to `.lock/owner.json`. `link`\n * fails with EEXIST if an owner exists, and with ENOENT if the directory\n * was removed meanwhile (an aged ownerless-lock reclaim). Only a\n * successful link holds the lock; on either failure nothing is released,\n * since nothing was acquired, and the caller retries.\n *\n * A profile deletion removes the profile directory once it has released its\n * lock, so `mkdir(.lock)` can see ENOENT; the directory is recreated (0700)\n * and the caller retries within its deadline, as for a first write to a new\n * profile.\n *\n * Only one owner.json can exist at the lock path, and it is removed only by\n * its holder's release or by recovery of a dead holder, so at most one\n * process holds the lock. Returns false when the lock is not acquired.\n */\nasync function publishProfileLock(profile: string, lockPath: string, token: string): Promise<boolean> {\n try {\n await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });\n } catch (error) {\n if (isErrno(error, \"EEXIST\")) return false;\n if (!isErrno(error, \"ENOENT\")) throw error;\n await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });\n return false;\n }\n await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);\n const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);\n const owner = { pid: process.pid, createdAt: new Date().toISOString(), token };\n try {\n await writeFile(staged, `${JSON.stringify(owner, null, 2)}\\n`, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE, flag: \"wx\" });\n await link(staged, join(lockPath, \"owner.json\"));\n return true;\n } catch (error) {\n if (isErrno(error, \"EEXIST\") || isErrno(error, \"ENOENT\")) return false;\n // Do not leave an ownerless lock behind for others to wait out. rmdir\n // removes only an empty directory; anyone who created it after ours went\n // away sees ENOENT on link and retries.\n await rmdir(lockPath).catch(() => undefined);\n throw error;\n } finally {\n await rm(staged, { force: true }).catch(() => undefined);\n }\n}\n\n/**\n * Releases the lock if `.lock/owner.json` is still this acquisition's. No\n * one else can put an owner record there while it exists (owners are only\n * linked into a directory their own `mkdir` created, and recovery removes\n * only a dead holder's record), so the record read is the one unlinked.\n */\nasync function releaseProfileLock(profile: string, lockPath: string, token: string): Promise<void> {\n const ownerPath = join(lockPath, \"owner.json\");\n const owner = await readJson<{ token?: unknown }>(ownerPath).catch(() => null);\n if (owner?.token !== token) return;\n await rm(ownerPath, { force: true });\n // `.lock` is empty from here until the rmdir; a contender that removes it\n // and creates its own meanwhile is not harmed: rmdir fails on its owner\n // file, or the contender's link sees ENOENT and retries. If a recoverer's\n // claim file is still inside, rmdir fails and that recoverer removes the\n // directory when it drops its claim.\n await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);\n await rmdir(lockPath).catch(() => undefined);\n}\n\n/**\n * Gives process-level contention tests an event-driven witness that a second\n * writer has reached an already-held lock. This deliberately has no effect\n * outside an explicit test environment, and only creates a new signal file\n * beneath TC_HOME; it never changes lock acquisition or release semantics.\n */\nasync function signalTestLockContention(profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n\n const configuredPath = process.env[TEST_LOCK_CONTENTION_SIGNAL_PATH];\n const configuredHome = process.env.TC_HOME;\n if (!configuredPath || !configuredHome) return;\n\n const home = resolve(configuredHome);\n const signalPath = resolve(configuredPath);\n const pathFromHome = relative(home, signalPath);\n if (\n pathFromHome === \"\" ||\n pathFromHome === \"..\" ||\n pathFromHome.startsWith(`..${process.platform === \"win32\" ? \"\\\\\" : \"/\"}`) ||\n pathFromHome.startsWith(\"../\") ||\n pathFromHome.startsWith(\"..\\\\\")\n ) {\n return;\n }\n\n await writeFile(signalPath, `${profile}\\n`, { encoding: \"utf8\", flag: \"wx\" })\n .catch(() => undefined);\n}\n\n/**\n * Reclaims a lock directory with no owner record that is older than the\n * stale threshold: one left by a crash, or by an older release. A process\n * killed while recovering (or releasing, in older releases) can leave its\n * `.stale-*` / `.release-*` claim file behind; such claim files are removed\n * first. owner.json is never touched, and the directory itself is only\n * `rmdir`ed, which succeeds only while it is empty, so a published lock is\n * never removed. If the directory was a contender's lock not yet published,\n * that contender's `link` fails with ENOENT and it retries without holding\n * anything.\n */\nasync function recoverOwnerlessLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n try {\n const { mtimeMs } = await stat(lockPath);\n if (Date.now() - mtimeMs < staleAfterMs) return false;\n } catch {\n return false;\n }\n const checkedAt = performance.now();\n await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);\n try {\n const entries = await readdir(lockPath);\n if (entries.includes(\"owner.json\")) return false;\n // The age check above is only good for a while; see recoveryFenced.\n if (recoveryFenced(checkedAt, staleAfterMs)) return false;\n for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {\n await rm(join(lockPath, name), { force: true });\n }\n await rmdir(lockPath);\n } catch {\n return false;\n }\n await removeAgedOwnerFiles(lockPath, staleAfterMs);\n return true;\n}\n\n/**\n * Whether a recovery step decided at `since` (a monotonic timestamp) is too\n * old to act on. Claim-only and ownerless cleanup act only on a `.lock`\n * unchanged for `staleAfterMs`, and a recoverer's claim (or the change that\n * made a directory look abandoned) dates the directory no earlier than\n * `since`. Acting within half that window means no such cleanup can have\n * removed the directory and let a new holder in meanwhile; past it, the\n * recoverer drops only its own claim and starts the acquisition over.\n */\nfunction recoveryFenced(since: number, staleAfterMs: number): boolean {\n return performance.now() - since >= staleAfterMs / 2;\n}\n\n/** Claim files of stale recovery (and of release in older releases). */\nconst ABANDONED_CLAIM = /^\\.(?:release|stale)-[0-9a-f-]+\\.json$/;\n/** Owner files staged by publishProfileLock beside `.lock`. */\nconst STAGED_OWNER = /^\\.lock-owner-[0-9a-f-]+\\.tmp$/;\n\n/**\n * Removes owner files a crashed acquirer staged beside `.lock`. A live\n * acquirer's staged file exists only for its write and link, so only files\n * older than the stale threshold are removed. Runs after a recovery, never\n * on the uncontended path.\n */\nasync function removeAgedOwnerFiles(lockPath: string, staleAfterMs: number): Promise<void> {\n const directory = dirname(lockPath);\n const names = await readdir(directory).catch(() => []);\n for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {\n const path = join(directory, name);\n const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);\n if (aged) await rm(path, { force: true }).catch(() => undefined);\n }\n}\n\n/**\n * Removes a dead holder's lock. The claim is a hard link to owner.json, so\n * the owner record is never moved away: a recoverer whose observation is\n * outdated (the dead holder's lock was already recovered and a live holder\n * now owns `.lock`) sees another token, drops its link and leaves the live\n * owner record in place. Only after the claimed record is confirmed to be\n * the observed dead holder's is owner.json unlinked. Nothing can link a new\n * owner.json meanwhile: the claim keeps `.lock` non-empty, so it cannot be\n * removed and recreated.\n */\nasync function recoverStaleLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n const ownerPath = join(lockPath, \"owner.json\");\n const owner = await readJson<{ pid?: unknown; createdAt?: unknown; token?: unknown }>(ownerPath)\n .catch(() => null);\n if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);\n if (!isStaleOwner(owner, staleAfterMs)) return false;\n\n const observedToken = typeof owner?.token === \"string\" && owner.token.length > 0\n ? owner.token\n : \"legacy\";\n await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);\n const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);\n try {\n await link(ownerPath, claimPath);\n } catch {\n return false;\n }\n const claimedAt = performance.now();\n await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);\n\n const claimed = await readJson<{ pid?: unknown; createdAt?: unknown; token?: unknown }>(claimPath)\n .catch(() => null);\n const sameInstance = claimed !== null && (\n observedToken === \"legacy\"\n ? claimed.token === undefined\n : claimed.token === observedToken\n );\n await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);\n if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {\n // Paused too long: another recoverer may have finished this recovery and\n // a cleanup may have removed this claim, so owner.json may now be a live\n // holder's. Touch nothing but this claim.\n await rm(claimPath, { force: true });\n await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);\n return false;\n }\n if (sameInstance) {\n await rm(ownerPath, { force: true });\n await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);\n }\n await rm(claimPath, { force: true });\n // Empty now if the claimed holder is gone or a live holder released while\n // this claim kept `.lock` from being removed: remove it rather than leave\n // an ownerless lock to age out. rmdir fails while an owner record exists.\n await rmdir(lockPath).catch(() => undefined);\n if (!sameInstance) return false;\n await removeAgedOwnerFiles(lockPath, staleAfterMs);\n return true;\n}\n\n/** Test-only rendezvous (NODE_ENV=test and the named barrier directory set). */\nasync function waitForTestBarrier(environmentName: string, profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n const barrierDirectory = process.env[environmentName];\n if (!barrierDirectory) return;\n await mkdir(barrierDirectory, { recursive: true });\n const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);\n await writeFile(readyPath, \"ready\\n\", { encoding: \"utf8\", flag: \"wx\" }).catch(() => undefined);\n const releasePath = join(barrierDirectory, \"release\");\n while (true) {\n try {\n await readFile(releasePath, \"utf8\");\n return;\n } catch {\n await sleep(1);\n }\n }\n}\n\nfunction isStaleOwner(\n owner: { pid?: unknown; createdAt?: unknown } | null,\n staleAfterMs: number,\n): boolean {\n const now = Date.now();\n const createdAt = typeof owner?.createdAt === \"string\"\n ? Date.parse(owner.createdAt)\n : Number.NaN;\n\n // Reclaim only a fully published lock whose owner PID is confirmed dead.\n // A lock without an owner record is handled by recoverOwnerlessLock: a\n // process holds the lock only once its owner.json is linked in.\n if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;\n if (typeof owner?.pid !== \"number\" || !Number.isInteger(owner.pid) || owner.pid <= 0) {\n return false;\n }\n return !isProcessAlive(owner.pid);\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return !isErrno(error, \"ESRCH\");\n }\n}\n\nfunction validateProfileName(profile: string): string {\n if (\n !profile ||\n profile === \".\" ||\n profile === \"..\" ||\n profile.includes(\"/\") ||\n profile.includes(\"\\\\\") ||\n profile.includes(\"\\0\")\n ) {\n throw new TypeError(\"Profile names must be non-empty path segments.\");\n }\n return profile;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n return typeof error === \"object\" && error !== null && \"code\" in error &&\n (error as { code?: unknown }).code === code;\n}\n\nfunction sleep(milliseconds: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, milliseconds));\n}\n\nexport { DEFAULT_PROFILE };\n"],"mappings":"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","names":["ownership","resolve"]}
@@ -5302,7 +5302,8 @@
5302
5302
  },
5303
5303
  "liveAdditionalDelegationCount": {
5304
5304
  "type": "integer",
5305
- "minimum": 0
5305
+ "minimum": 0,
5306
+ "description": "Stored, unexpired delegations the replay rule accepts: compact-UCAN or signed-login records with a request binding, and unbound compact-UCAN records before the profile's one-time binding migration. Each is still validated, and checked against its binding, when a runtime installs it."
5306
5307
  }
5307
5308
  },
5308
5309
  "required": [
@@ -6139,7 +6140,8 @@
6139
6140
  },
6140
6141
  "liveAdditionalDelegationCount": {
6141
6142
  "type": "integer",
6142
- "minimum": 0
6143
+ "minimum": 0,
6144
+ "description": "Stored, unexpired delegations the replay rule accepts: compact-UCAN or signed-login records with a request binding, and unbound compact-UCAN records before the profile's one-time binding migration. Each is still validated, and checked against its binding, when a runtime installs it."
6143
6145
  }
6144
6146
  },
6145
6147
  "required": [
@@ -15041,7 +15043,8 @@
15041
15043
  },
15042
15044
  "liveAdditionalDelegationCount": {
15043
15045
  "type": "integer",
15044
- "minimum": 0
15046
+ "minimum": 0,
15047
+ "description": "Stored, unexpired delegations the replay rule accepts: compact-UCAN or signed-login records with a request binding, and unbound compact-UCAN records before the profile's one-time binding migration. Each is still validated, and checked against its binding, when a runtime installs it."
15045
15048
  }
15046
15049
  },
15047
15050
  "required": [
@@ -15878,7 +15881,8 @@
15878
15881
  },
15879
15882
  "liveAdditionalDelegationCount": {
15880
15883
  "type": "integer",
15881
- "minimum": 0
15884
+ "minimum": 0,
15885
+ "description": "Stored, unexpired delegations the replay rule accepts: compact-UCAN or signed-login records with a request binding, and unbound compact-UCAN records before the profile's one-time binding migration. Each is still validated, and checked against its binding, when a runtime installs it."
15882
15886
  }
15883
15887
  },
15884
15888
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinycloud/operations",
3
- "version": "0.3.4-beta.5",
3
+ "version": "0.4.0-beta.7",
4
4
  "description": "TinyCloud shared operations contracts and runtime",
5
5
  "author": "TinyCloud, Inc.",
6
6
  "license": "EGPL",
@@ -34,6 +34,11 @@
34
34
  "import": "./dist/profile.js",
35
35
  "require": "./dist/profile.cjs"
36
36
  },
37
+ "./delegation-binding": {
38
+ "types": "./dist/delegation-binding.d.ts",
39
+ "import": "./dist/delegation-binding.js",
40
+ "require": "./dist/delegation-binding.cjs"
41
+ },
37
42
  "./secret-capabilities": {
38
43
  "types": "./dist/secret-capabilities.d.ts",
39
44
  "import": "./dist/secret-capabilities.js",
@@ -50,7 +55,7 @@
50
55
  "coverage.json"
51
56
  ],
52
57
  "scripts": {
53
- "build": "tsup --entry src/index.ts --entry src/state.ts --entry src/artifacts.ts --entry src/cli-runtime.ts --entry src/profile.ts --entry src/secret-capabilities.ts",
58
+ "build": "tsup --entry src/index.ts --entry src/state.ts --entry src/artifacts.ts --entry src/cli-runtime.ts --entry src/profile.ts --entry src/secret-capabilities.ts --entry src/delegation-binding.ts",
54
59
  "clean": "rm -rf dist",
55
60
  "test": "bun scripts/test.ts",
56
61
  "typecheck": "tsc -p tsconfig.typecheck.json",
@@ -59,8 +64,8 @@
59
64
  "check:generated": "bun scripts/generate.ts --check"
60
65
  },
61
66
  "dependencies": {
62
- "@tinycloud/node-sdk": "3.1.0-beta.3",
63
- "@tinycloud/sdk-core": "3.1.0-beta.3",
67
+ "@tinycloud/node-sdk": "3.1.0-beta.5",
68
+ "@tinycloud/sdk-core": "3.1.0-beta.4",
64
69
  "node-sql-parser": "5.4.0",
65
70
  "zod": "^3.22.0",
66
71
  "zod-to-json-schema": "^3.22.0"