@tinycloud/operations 0.3.4-beta.6 → 0.4.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifacts.cjs +416 -50
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.d.cts +115 -58
- package/dist/artifacts.d.ts +115 -58
- package/dist/artifacts.js +417 -50
- package/dist/artifacts.js.map +1 -1
- package/dist/authority-ofwbEqmj.d.cts +30 -0
- package/dist/authority-ofwbEqmj.d.ts +30 -0
- package/dist/cli-runtime.cjs +652 -120
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +645 -116
- package/dist/cli-runtime.js.map +1 -1
- package/dist/delegation-binding.cjs +7956 -0
- package/dist/delegation-binding.cjs.map +1 -0
- package/dist/delegation-binding.d.cts +130 -0
- package/dist/delegation-binding.d.ts +130 -0
- package/dist/delegation-binding.js +7948 -0
- package/dist/delegation-binding.js.map +1 -0
- package/dist/index.cjs +649 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +645 -116
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +9 -2
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.js +11 -2
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +426 -50
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.cts +38 -5
- package/dist/state.d.ts +38 -5
- package/dist/state.js +424 -50
- package/dist/state.js.map +1 -1
- package/generated/operations.json +8 -4
- package/package.json +8 -3
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":"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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 open,\n readFile,\n readdir,\n rename,\n rmdir,\n rm,\n stat,\n unlink,\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_VERIFIED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR\";\nconst TEST_LOCK_MOVED_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR\";\nconst TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR\";\nconst TEST_LOCK_TURN_SETTLE_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR\";\nconst TEST_LOCK_TURN_COLLECT_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR\";\n/** One acquisition of a profile lock; `active` is cleared before release. */\ninterface HeldProfileLock {\n readonly lockPath: string;\n /** The number of the turn this acquisition holds (see acquireTurn). */\n readonly turnSlot: number;\n /** A deletion of the profile ran while this acquisition waited for the lock. */\n readonly deletedWhileWaiting: boolean;\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 * Each key is independently versioned for the shape and meaning of the\n * value it stores. Held-lock entries gained `turnSlot` and\n * `deletedWhileWaiting` in TC-633, so `.v2` must not read TC-602's `.v1`\n * entries. The invocation root remains a string path with unchanged\n * semantics and stays `.v1`. Relinquished turn tokens use a separate\n * versioned process-wide key: only an explicitly given-up turn of this PID\n * may be settled across separately bundled entry points.\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.v2\");\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 /** `detail` names the cause when it is not simply another process holding the lock. */\n constructor(profile: string, timeoutMs: number, detail?: string) {\n super(`Timed out waiting for the profile lock for \"${profile}\" after ${timeoutMs}ms.${detail ? ` ${detail}` : \"\"}`);\n this.name = \"ProfileLockTimeoutError\";\n }\n}\n\n/**\n * A store write that waited for the profile lock while a deletion of that\n * profile held it, and found no profile settings once it got the lock. The\n * write is refused rather than recreating a profile with only a session or\n * store in it.\n */\nexport class ProfileDeletedError extends Error {\n readonly code = \"PROFILE_NOT_FOUND\";\n\n constructor(profile: string) {\n super(`Profile \"${profile}\" was deleted while this change waited for its lock, so the change was not written.`);\n this.name = \"ProfileDeletedError\";\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 * The turn lock of a profile (see acquireTurn). It lives outside the profile\n * directory, so neither a profile deletion nor an older release's cleanup\n * removes it.\n */\nexport function profileTurnLockPath(profile: string): string {\n return join(tinycloudHomePath(), \"profile-locks\", validateProfileName(profile));\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 await replaceJson(filePath, value, false);\n}\n\n/**\n * writeJsonAtomic; `durable` also flushes the data to disk before the rename,\n * so a crash or power loss leaves the old record or the new one, never a\n * truncated one (for lock records, whose damage would stop every writer).\n */\nasync function replaceJson(filePath: string, value: unknown, durable: boolean): 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 if (durable) await writeFileDurably(temporaryPath, contents);\n else 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\n/** Creates a file (exclusively) and, when it has contents, flushes them to disk. */\nasync function writeFileDurably(path: string, contents: string): Promise<void> {\n const handle = await open(path, \"wx\", PRIVATE_FILE_MODE);\n try {\n if (contents) {\n await handle.writeFile(contents, \"utf8\");\n await handle.datasync();\n }\n } finally {\n await handle.close();\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 lock shared by all profile\n * stores (see acquireProfileLock). Lock release verifies its ownership token\n * before removing 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 acquired = await acquireProfileLock(normalizedProfile, options);\n const ownership: HeldProfileLock = {\n lockPath,\n turnSlot: acquired.turnSlot,\n deletedWhileWaiting: acquired.deletedWhileWaiting,\n active: true,\n };\n try {\n return await heldProfileLocks.run([...held, ownership], action);\n } finally {\n ownership.active = false;\n await acquired.release();\n // Taking the lock recreated the deleted profile's directory; remove it\n // again unless something was written into it (rmdir needs it empty).\n if (acquired.deletedWhileWaiting) await rmdir(profilePath(normalizedProfile)).catch(() => undefined);\n }\n}\n\n/**\n * Records, inside a profile deletion's critical section and after its files\n * are gone, that the profile was deleted, with the number of the deleting\n * turn. A store write whose wait overlapped that turn (it arrived before the\n * deletion released the lock, whether before or after this record) then\n * refuses (ProfileDeletedError) instead of recreating a profile with only a\n * session or store in it. The caller must hold the profile's lock.\n */\nexport async function recordProfileDeletion(profile: string): Promise<void> {\n const lockPath = profileLockPath(profile);\n const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);\n if (!ownership) throw new Error(`Recording the deletion of profile \"${profile}\" requires holding its lock.`);\n await replaceJson(join(profileTurnLockPath(profile), \"deleted.json\"), { slot: ownership.turnSlot }, true);\n}\n\n/**\n * Refuses a write that would recreate a profile deleted while this critical\n * section waited for the lock (ProfileDeletedError). Every writer of profile\n * state other than the profile settings themselves calls this inside its\n * critical section, before writing. The caller must hold the profile's lock.\n */\nexport async function refuseWriteToDeletedProfile(profile: string): Promise<void> {\n const lockPath = profileLockPath(profile);\n const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);\n if (!ownership?.deletedWhileWaiting) return;\n // Recreated meanwhile (or by this critical section): writing is fine.\n if (await exists(profileConfigPath(profile))) return;\n throw new ProfileDeletedError(profile);\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 await refuseWriteToDeletedProfile(profile);\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 await refuseWriteToDeletedProfile(profile);\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 refuseWriteToDeletedProfile(profile);\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\ninterface AcquiredProfileLock {\n readonly deletedWhileWaiting: boolean;\n readonly turnSlot: number;\n release(): Promise<void>;\n}\n\ninterface LockDeadline {\n readonly startedAt: number;\n readonly timeoutMs: number;\n readonly retryMs: number;\n}\n\n/**\n * Takes a profile's lock in two layers:\n *\n * 1. The turn lock (acquireTurn), used by this release and later ones. It\n * decides nothing from elapsed time, so a process paused anywhere, for\n * any length of time, cannot let a second holder in.\n * 2. Holding a turn, the `.lock` directory every older release uses\n * (publishProfileLock), so they keep excluding this release and it them.\n * Only one process of this release at a time acquires, recovers or\n * releases `.lock`.\n *\n * Also reports whether a profile deletion (recordProfileDeletion) ran while\n * this acquisition waited.\n */\nasync function acquireProfileLock(\n profile: string,\n options: ProfileLockOptions,\n): Promise<AcquiredProfileLock> {\n const deadline: LockDeadline = {\n startedAt: Date.now(),\n timeoutMs: options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS,\n retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS,\n };\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;\n const lockPath = profileLockPath(profile);\n const turnDirectory = profileTurnLockPath(profile);\n\n const directory = profilePath(profile);\n await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });\n await mkdir(dirname(turnDirectory), { 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 const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);\n try {\n // An unreadable deletion record is ignored rather than failing every\n // acquisition; it is written durably, so only damage makes it so.\n const deletion = await readJson<{ slot?: unknown }>(join(turnDirectory, \"deleted.json\")).catch(() => null);\n const deletedWhileWaiting = typeof deletion?.slot === \"number\" && deletion.slot >= firstWaitedTurn;\n while (true) {\n const token = randomUUID();\n if (await publishProfileLock(profile, lockPath, token)) {\n return {\n deletedWhileWaiting,\n turnSlot: turn.slot,\n release: async () => {\n try {\n await releaseProfileLock(profile, lockPath, token);\n } finally {\n await releaseTurn(profile, turn);\n }\n },\n };\n }\n await signalTestLockContention(profile);\n if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;\n await waitOrTimeOut(profile, deadline);\n }\n } catch (error) {\n await releaseTurn(profile, turn);\n throw error;\n }\n}\n\n/** Throws once the acquisition's time is up; `detail` explains a damaged turn lock. */\nfunction checkDeadline(profile: string, deadline: LockDeadline, detail?: string): void {\n if (Date.now() - deadline.startedAt >= deadline.timeoutMs) {\n throw new ProfileLockTimeoutError(profile, deadline.timeoutMs, detail);\n }\n}\n\n/** Sleeps one retry interval, or throws once the acquisition's time is up. */\nasync function waitOrTimeOut(profile: string, deadline: LockDeadline, detail?: string): Promise<void> {\n checkDeadline(profile, deadline, detail);\n await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));\n}\n\n/** One granted turn of the turn lock. */\ninterface Turn {\n readonly directory: string;\n readonly slot: number;\n readonly token: string;\n}\n\n/** A turn as recorded on disk. */\ninterface TurnState {\n readonly slot: number;\n readonly token: string;\n readonly pid: number;\n /** The token of the turn before it that its creator observed; null for turn 0. */\n readonly after: string | null;\n readonly held: boolean;\n readonly done: boolean;\n}\n\n/** Turn directory names: decimal turn numbers. */\nconst TURN = /^(?:0|[1-9][0-9]*)$/;\n\n/**\n * Tokens of turns this process published and then gave up (a marker write or\n * a read failed after publication): the positive evidence that lets this\n * process settle a turn of its own PID. A live process's turn that is not in\n * here is never settled by it, so a module instance that cannot see another\n * instance's set (another bundled copy of this package) waits rather than\n * settling a turn that copy still holds.\n *\n * One set per process, not per module instance: the package's entry points\n * (state, index, artifacts, cli-runtime; ESM and CJS) each bundle their own\n * copy of this module, and one process may load several. The key is\n * versioned so that a release changing what the set holds uses a new one.\n */\nconst relinquishedTurns: Set<string> =\n (globalThis as unknown as Record<symbol, Set<string> | undefined>)[Symbol.for(\"tinycloud.operations.relinquishedProfileTurns.v1\")] ??=\n new Set<string>();\n\n/** Marker writes are retried this many times, after these delays (ms), before giving up. */\nconst MARKER_RETRY_DELAYS_MS = [10, 50, 250];\n/** A given-up turn is settled in the background every second for up to five minutes. */\nconst SETTLE_RETRY_MS = 1_000;\nconst SETTLE_RETRIES = 300;\n\n/**\n * The turn lock: mutual exclusion among processes of this release that does\n * not depend on how long any of them is paused. It uses only `mkdir`,\n * `rename`, exclusive file creation, and a check that a holder's PID is gone\n * (as `.lock` recovery always has), so it needs no hard links.\n *\n * Turns are numbered directories in profileTurnLockPath(profile). Turn n has\n * `owner.json` ({ pid, createdAt, token, after }), where `after` is the token\n * of the turn n-1 its creator observed, and gains `<token>.held` when granted\n * and `<token>.done` when over. Markers name the turn's own token, so one\n * written late into a recreated directory of the same number means nothing.\n *\n * - A turn is published by renaming a fully written staging directory to\n * `n`, which fails while `n` exists (rename never replaces a non-empty\n * directory, and turn directories are never empty). The turn directory\n * itself is published the same way, with a granted, finished turn 0.\n * - A process takes turn n only after reading turn n-1 granted and done.\n * Once `n` is published it checks that turn n-1 is still the one it read\n * (same token, granted, done) and only then writes `held`; otherwise the\n * turn is void: it writes `done` alone and starts over.\n * - Turns are removed by renaming them aside: a void turn at any time, a\n * granted one only once no older turn is left and turn n+1 is granted and\n * done. The latest turn is never removed.\n *\n * Why at most one granted turn is ever not done: a removed number can be\n * published again, but turn n-1 is always removed before turn n, so a\n * process whose stale read led it to republish n finds n-1 gone or holding\n * another token, and its turn is void. A process that finds n-1 as it read\n * it published `n` for the first time, so it is the only process granted n,\n * and n-1 was done before. A turn is marked done only by its holder, or by\n * another process once the holder's PID is gone (or by its own process,\n * once it has explicitly given that turn up); a paused holder is alive.\n * A turn is voided only on definite evidence (the turn it follows is gone or\n * holds another token), never because a read failed. Nothing above is\n * decided from elapsed time.\n *\n * Returns the turn and the number of the first turn this acquisition waited\n * on: the latest turn when it first looked if that was not done yet, else\n * the next one.\n */\nasync function acquireTurn(\n profile: string,\n directory: string,\n deadline: LockDeadline,\n): Promise<{ turn: Turn; firstWaitedTurn: number }> {\n let firstWaitedTurn: number | undefined;\n // Why the turn lock looks damaged, as of the last look, for the timeout message.\n let problem: string | undefined;\n const damaged = (what: string) =>\n `The turn lock ${directory} ${what}, so no process can take it. ` +\n `If no tc or MCP process is using profile \"${profile}\", remove that directory.`;\n for (let first = true; ; first = false) {\n if (!first) checkDeadline(profile, deadline, problem);\n problem = undefined;\n const latest = await readLatestTurn(directory);\n if (latest === \"missing\") {\n await createTurnDirectory(directory);\n continue;\n }\n if (latest === \"empty\") {\n // Usually a listing that straddled a turn's publication and an older\n // turn's removal (readdir is not a snapshot); damaged if it persists.\n problem = damaged(\"has no turns\");\n await waitOrTimeOut(profile, deadline, problem);\n continue;\n }\n if (latest === null) continue;\n if (typeof latest === \"number\") {\n problem = damaged(`has an unreadable turn ${latest}`);\n await waitOrTimeOut(profile, deadline, problem);\n continue;\n }\n firstWaitedTurn ??= latest.done ? latest.slot + 1 : latest.slot;\n if (latest.held && latest.done) {\n const turn = await takeTurn(profile, directory, latest);\n if (turn !== null) return { turn, firstWaitedTurn };\n await waitOrTimeOut(profile, deadline);\n } else if (latest.done) {\n problem = damaged(`ends with turn ${latest.slot}, which was never granted`);\n await waitOrTimeOut(profile, deadline, problem);\n } else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {\n await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);\n await settleAbandonedTurn(directory, latest);\n relinquishedTurns.delete(latest.token);\n } else {\n await signalTestLockContention(profile);\n await waitOrTimeOut(profile, deadline);\n }\n }\n}\n\n/**\n * The highest-numbered turn: its state, its number if it is damaged, null if\n * it went away while being read.\n */\nasync function readLatestTurn(directory: string): Promise<TurnState | number | \"missing\" | \"empty\" | null> {\n let names: string[];\n try {\n names = await readdir(directory);\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return \"missing\";\n throw error;\n }\n const slots = names.filter((name) => TURN.test(name)).map(Number);\n if (slots.length === 0) return \"empty\";\n const slot = Math.max(...slots);\n const turn = await readTurn(directory, slot);\n return turn === \"damaged\" ? slot : turn;\n}\n\n/**\n * Turn `slot` as recorded now; null if it is gone; \"damaged\" if its owner\n * record is missing or unreadable (turns are renamed into place and aside\n * whole, with their owner record written durably). Any other read error is\n * thrown: it says nothing about the turn. `done` is read before `held`,\n * which every writer creates first, so a turn read as done is never misread\n * as void.\n */\nasync function readTurn(directory: string, slot: number): Promise<TurnState | \"damaged\" | null> {\n const path = join(directory, String(slot));\n let text: string;\n try {\n text = await readFile(join(path, \"owner.json\"), \"utf8\");\n } catch (error) {\n if (!isErrno(error, \"ENOENT\") && !isErrno(error, \"ENOTDIR\")) throw error;\n return await exists(path) ? \"damaged\" : null;\n }\n let owner: { pid?: unknown; token?: unknown; after?: unknown } | null;\n try {\n owner = JSON.parse(text) as typeof owner;\n } catch {\n return \"damaged\";\n }\n if (owner === null || typeof owner !== \"object\" || typeof owner.token !== \"string\" || typeof owner.pid !== \"number\") {\n return \"damaged\";\n }\n const done = await exists(join(path, `${owner.token}.done`));\n const held = await exists(join(path, `${owner.token}.held`));\n return {\n slot,\n token: owner.token,\n pid: owner.pid,\n after: typeof owner.after === \"string\" ? owner.after : null,\n held,\n done,\n };\n}\n\n/**\n * Whether turn `slot` is still the granted, finished turn with `token`. False\n * only on definite evidence: it is gone, or holds another token. Throws if it\n * cannot be read, or is damaged, so the caller leaves its own turn undecided.\n */\nasync function isFinishedTurn(directory: string, slot: number, token: string): Promise<boolean> {\n const turn = await readTurn(directory, slot);\n if (turn === \"damaged\") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);\n return turn !== null && turn.token === token && turn.held && turn.done;\n}\n\n/**\n * Publishes the turn after `latest` and claims it. Null if another process\n * published that number first, or the turn is void. If a step after\n * publication fails, the turn is handed to settleOwnTurn and the error thrown.\n */\nasync function takeTurn(profile: string, directory: string, latest: TurnState): Promise<Turn | null> {\n const token = randomUUID();\n const slot = latest.slot + 1;\n const path = join(directory, String(slot));\n const owner = { pid: process.pid, createdAt: new Date().toISOString(), token, after: latest.token };\n let published = false;\n try {\n published = await publishDirectory(\n join(directory, `.stage-${process.pid}-${token}`),\n path,\n { \"owner.json\": `${JSON.stringify(owner, null, 2)}\\n` },\n () => waitForTestBarrier(TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, profile),\n );\n if (published && await isFinishedTurn(directory, latest.slot, latest.token) && await markTurn(path, token, \"held\")) {\n return { directory, slot, token };\n }\n if (published) await markTurn(path, token, \"done\");\n return null;\n } catch (error) {\n if (published) settleOwnTurn(directory, slot, token);\n throw error;\n }\n}\n\n/**\n * Finishes the turn of a process that is gone (or one this process gave up).\n * A turn its creator never got to grant is granted first if the turn before\n * it is still the one its creator read (the check the creator would have\n * made; that turn cannot have been removed, since that needs this turn\n * done), and left void only if it definitely is not.\n */\nasync function settleAbandonedTurn(directory: string, abandoned: TurnState): Promise<void> {\n const path = join(directory, String(abandoned.slot));\n if (!abandoned.held && abandoned.after !== null &&\n await isFinishedTurn(directory, abandoned.slot - 1, abandoned.after)) {\n await markTurn(path, abandoned.token, \"held\");\n }\n await markTurn(path, abandoned.token, \"done\");\n}\n\n/**\n * Settles a turn this process published but could not grant or finish (a\n * marker write or a read failed). Other processes wait on this live PID, so\n * it is retried in the background until it succeeds or the turn is settled\n * otherwise (by this process's next acquisition, or by another process once\n * this one has exited). The timer does not keep the process alive.\n */\nfunction settleOwnTurn(directory: string, slot: number, token: string, retries = SETTLE_RETRIES): void {\n relinquishedTurns.add(token);\n void (async () => {\n try {\n const turn = await readTurn(directory, slot);\n if (turn !== null && turn !== \"damaged\" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);\n relinquishedTurns.delete(token);\n } catch {\n // Still relinquished: this process's next acquisition settles it too.\n if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();\n }\n })();\n}\n\n/**\n * Creates `<token>.<kind>` in a turn directory; false if the directory is\n * gone. Other failures (a full disk, a permission error) are retried a few\n * times, then thrown.\n */\nasync function markTurn(path: string, token: string, kind: \"held\" | \"done\"): Promise<boolean> {\n for (let attempt = 0; ; attempt++) {\n try {\n await writeFile(join(path, `${token}.${kind}`), \"\", { mode: PRIVATE_FILE_MODE, flag: \"wx\" });\n return true;\n } catch (error) {\n if (isErrno(error, \"EEXIST\")) return true;\n if (isErrno(error, \"ENOENT\")) return false;\n if (attempt >= MARKER_RETRY_DELAYS_MS.length) throw error;\n await sleep(MARKER_RETRY_DELAYS_MS[attempt]!);\n }\n }\n}\n\n/** Creates the turn directory with a granted, finished turn 0, unless another process did. */\nasync function createTurnDirectory(directory: string): Promise<void> {\n const token = randomUUID();\n const owner = { pid: process.pid, createdAt: new Date().toISOString(), token, after: null };\n await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {\n \"0/owner.json\": `${JSON.stringify(owner, null, 2)}\\n`,\n [`0/${token}.held`]: \"\",\n [`0/${token}.done`]: \"\",\n });\n}\n\n/**\n * Writes `files` (durably, when not empty) into a new staging directory and\n * renames it to `target`. False if `target` already exists (or the turn\n * directory is gone); the caller starts over. Nothing else removes a live\n * process's staging directory (see collectTurns), so `target` never appears\n * incomplete.\n */\nasync function publishDirectory(\n stage: string,\n target: string,\n files: Readonly<Record<string, string>>,\n beforeRename?: () => Promise<void>,\n): Promise<boolean> {\n try {\n for (const [name, contents] of Object.entries(files)) {\n await mkdir(dirname(join(stage, name)), { recursive: true, mode: PRIVATE_DIR_MODE });\n await writeFileDurably(join(stage, name), contents);\n }\n await beforeRename?.();\n await rename(stage, target);\n return true;\n } catch (error) {\n // ENOTEMPTY/EEXIST: `target` exists, though it may be gone again by now.\n // Windows reports an existing target directory as EPERM.\n if ([\"ENOTEMPTY\", \"EEXIST\", \"ENOENT\"].some((code) => isErrno(error, code)) || await exists(target)) return false;\n throw error;\n } finally {\n await rm(stage, { recursive: true, force: true }).catch(() => undefined);\n }\n}\n\n/**\n * Finishes this process's turn. If `done` cannot be written even after\n * retries, the critical section is still over and its writes stand: the turn\n * is handed to settleOwnTurn rather than failing the caller.\n */\nasync function releaseTurn(profile: string, turn: Turn): Promise<void> {\n try {\n await markTurn(join(turn.directory, String(turn.slot)), turn.token, \"done\");\n } catch {\n settleOwnTurn(turn.directory, turn.slot, turn.token);\n return;\n }\n await collectTurns(profile, turn).catch(() => undefined);\n}\n\n/**\n * Leftovers in a turn directory whose writer is gone: trash (a removed turn\n * renamed aside, garbage from then on), and staging directories and\n * deletion-record temporaries named after their writer's PID.\n */\nfunction isAbandonedTurnEntry(name: string): boolean {\n if (name.startsWith(\".trash-\")) return true;\n const writer = /^\\.(?:stage|deleted\\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];\n return writer !== undefined && !isProcessAlive(Number(writer));\n}\n\n/** A turn directory's staging directory left beside it by a process that is gone. */\nfunction isAbandonedTurnDirectoryStage(name: string): boolean {\n const writer = /^\\.stage-.+-([0-9]+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.exec(name)?.[1];\n return writer !== undefined && !isProcessAlive(Number(writer));\n}\n\n/**\n * Removes the turns before `turn`, which just finished: void turns, and\n * granted ones oldest first, each once no older turn is left and the next is\n * granted and done (see acquireTurn). A turn is renamed aside before it is\n * deleted, so its number is never an empty directory. A turn that is damaged\n * or cannot be read is left alone, and a read error stops the collection.\n * Leftovers of crashed processes go too, judged by PID, never by age:\n * removing a paused process's staging directory could publish an incomplete\n * turn.\n */\nasync function collectTurns(profile: string, turn: Turn): Promise<void> {\n for (const [directory, abandoned] of [\n [turn.directory, isAbandonedTurnEntry],\n [dirname(turn.directory), isAbandonedTurnDirectoryStage],\n ] as const) {\n for (const name of (await readdir(directory)).filter(abandoned)) {\n await rm(join(directory, name), { recursive: true, force: true }).catch(() => undefined);\n }\n }\n const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number)\n .filter((slot) => slot < turn.slot)\n .sort((left, right) => left - right);\n for (const slot of older) {\n const candidate = await readTurn(turn.directory, slot);\n if (candidate === null || candidate === \"damaged\" || !candidate.done) continue;\n if (candidate.held) {\n if (await exists(join(turn.directory, String(slot - 1)))) continue;\n const next = await readTurn(turn.directory, slot + 1);\n if (next === null || next === \"damaged\" || !next.held || !next.done) continue;\n }\n await waitForTestBarrier(TEST_LOCK_TURN_COLLECT_BARRIER_DIR, profile);\n const trash = join(turn.directory, `.trash-${randomUUID()}`);\n if (await rename(join(turn.directory, String(slot)), trash).then(() => true, () => false)) {\n await rm(trash, { recursive: true, force: true }).catch(() => undefined);\n }\n }\n}\n\n/**\n * One attempt to take `.lock`, made while holding a turn. Nothing here\n * 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 by an older\n * release). Only a successful link holds the lock; on either failure\n * nothing is released, since nothing was acquired, and the caller retries.\n * On a filesystem without hard links (FAT/exFAT, some SMB mounts) the\n * owner file is created in place with an exclusive create instead, with\n * the same EEXIST/ENOENT outcomes. A reader may then briefly see it\n * incomplete; every release treats an owner file it cannot parse as a\n * held lock, and this release removes one only once it has aged (see\n * isAbandonedOwner).\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 an abandoned 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 ownerPath = join(lockPath, \"owner.json\");\n const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);\n const owner = `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), token }, null, 2)}\\n`;\n try {\n await writeFile(staged, owner, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE, flag: \"wx\" });\n try {\n await link(staged, ownerPath);\n } catch (error) {\n if (!lacksHardLinks(error)) throw error;\n await writeFile(ownerPath, owner, { encoding: \"utf8\", mode: PRIVATE_FILE_MODE, flag: \"wx\" });\n }\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 * Errors `link` reports on a filesystem without hard links: EPERM (Linux\n * FAT/exFAT), ENOTSUP/EOPNOTSUPP (macOS FAT/exFAT, SMB), ENOSYS, EISDIR\n * (libuv's code for Windows' ERROR_INVALID_FUNCTION on FAT32), and EMLINK\n * (too many links), for which the same fallback is just as correct.\n */\nconst NO_HARD_LINK_ERRORS = new Set([\"EPERM\", \"ENOTSUP\", \"EOPNOTSUPP\", \"ENOSYS\", \"EISDIR\", \"EMLINK\"]);\n\nfunction lacksHardLinks(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && \"code\" in error &&\n NO_HARD_LINK_ERRORS.has(String((error as { code?: unknown }).code));\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 * published into a directory their own `mkdir` created, and recovery\n * removes only an abandoned holder's record), so the record read is the one\n * 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` with no owner record, while holding a turn. A\n * `.recover-*` claim may contain a live legacy owner's record moved by a\n * no-hard-links recoverer just before it crashed. Restore that record first;\n * only orphaned claims holding dead or malformed records can be removed.\n * Otherwise the directory must be older than the stale threshold: one left\n * by a crash, or by an older release, whose `.stale-*` / `.release-*` claim\n * files are removed too. owner.json is never unlinked, and the directory\n * itself is only `rmdir`ed, which succeeds only while it is empty.\n *\n * Holding a turn, no other process of this release reclaims concurrently.\n * That closes the race in which two reclaimers' age checks let the later one\n * remove a directory a 1.0.0-beta.16-or-older release had just created,\n * which holds the lock from its `mkdir`. That race remains only if a\n * 1.0.0-beta.17 … 1.0.1-beta.4 release reclaims at the same moment, or the\n * older process stays paused between its `mkdir` and its owner write for\n * longer than the stale threshold; recoveryFenced bounds how stale this\n * process's age check can be.\n */\nasync function recoverOwnerlessLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n let aged: boolean;\n try {\n const { mtimeMs } = await stat(lockPath);\n aged = Date.now() - mtimeMs >= staleAfterMs;\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 const onlyOrphanedClaims = entries.length > 0 && entries.every((entry) => RECOVERY_CLAIM.test(entry));\n if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;\n for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {\n const claimPath = join(lockPath, name);\n if (await hasLiveRecoveryOwner(claimPath)) {\n // The claim keeps the directory in place. Only the original mkdir\n // owner could publish owner.json here; never overwrite that record.\n if (!await exists(join(lockPath, \"owner.json\"))) await rename(claimPath, join(lockPath, \"owner.json\"));\n return false;\n }\n await rm(claimPath, { force: true });\n }\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/** A no-hard-links claim may contain a live owner's record moved before a crash. */\nasync function hasLiveRecoveryOwner(claimPath: string): Promise<boolean> {\n let contents: string;\n try {\n contents = await readFile(claimPath, \"utf8\");\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return false;\n throw error; // An I/O failure is not evidence that the holder is gone.\n }\n let owner: unknown;\n try {\n owner = JSON.parse(contents);\n } catch {\n return false;\n }\n const pid = typeof owner === \"object\" && owner !== null && \"pid\" in owner ? owner.pid : null;\n return typeof pid === \"number\" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);\n}\n\n/**\n * Whether an ownerless-reclaim decision made at `since` (a monotonic\n * timestamp) is too old to act on. The age check is only good for a while:\n * past half the stale threshold the reclaim starts over.\n */\nfunction recoveryFenced(since: number, staleAfterMs: number): boolean {\n return performance.now() - since >= staleAfterMs / 2;\n}\n\n/** Claim files of older releases' stale recovery and (up to 1.0.0-beta.16) release. */\nconst ABANDONED_CLAIM = /^\\.(?:release|stale)-[0-9a-f-]+\\.json$/;\n/**\n * Claim files of this release's stale recovery. Releases up to 1.0.1-beta.4\n * never remove them, which is what makes this recovery safe against them; a\n * recoverer that crashes leaves one that only a release with the turn lock\n * clears (see REFERENCE.md, \"Profile lock\").\n */\nconst RECOVERY_CLAIM = /^\\.recover-[0-9a-f-]+\\.json$/;\n/** Owner files staged by publishProfileLock beside `.lock`. */\nconst STAGED_OWNER = /^\\.lock-owner-[0-9a-f-]+\\.tmp$/;\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/**\n * Removes an abandoned owner record (see isAbandonedOwner) and its `.lock`,\n * while holding a turn, so no other process of this release recovers,\n * reclaims or acquires `.lock` meanwhile.\n *\n * The record is claimed first: hard-linked to `.recover-<uuid>.json`, and the\n * claim compared with the exact bytes read. A record that replaced the\n * observed one is left in place. While the claim is inside `.lock`, the\n * directory cannot be removed and recreated: older releases never remove a\n * `.recover-*` claim (they clean up only their own claim names), and other\n * processes of this release are kept out by the turn lock. So no other owner\n * record can appear in it, and the unlink of owner.json can only remove the\n * claimed, dead record. A live record is never unlinked or moved. `.lock`\n * itself is removed only after this process's claim is unlinked from it,\n * which proves it is still the dead holder's directory; if the claim cannot\n * be made, `.lock` is left alone (it may be a directory a 1.0.0-beta.16-or-\n * older writer just created, which it holds from that `mkdir`). No step\n * depends on how long this process takes.\n *\n * Without hard links (where 1.0.0-beta.17 … 1.0.1-beta.4 cannot run at all),\n * the record is renamed into the claim instead, and put back if it is not\n * the observed one: one a 1.0.0-beta.16-or-older process published after\n * recovering the same lock at the same moment. The claim keeps `.lock` held\n * meanwhile, so no second writer gets in, but if that holder releases\n * between the two renames, the put-back restores a released record, and\n * `.lock` stays held until that PID is gone and the record is 30 s old.\n */\nasync function recoverStaleLock(profile: string, lockPath: string, staleAfterMs: number): Promise<boolean> {\n const ownerPath = join(lockPath, \"owner.json\");\n const observed = await readFile(ownerPath, \"utf8\").catch(() => null);\n if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);\n if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;\n\n await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);\n const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);\n let moved = false;\n try {\n await link(ownerPath, claimPath);\n } catch (error) {\n // Gone or replaced already (another release recovered it): leave `.lock`.\n if (!lacksHardLinks(error)) return false;\n try {\n await rename(ownerPath, claimPath);\n moved = true;\n } catch {\n return false;\n }\n }\n if (moved) await waitForTestBarrier(TEST_LOCK_MOVED_BARRIER_DIR, profile);\n const sameInstance = await readFile(claimPath, \"utf8\").then((claimed) => claimed === observed, () => false);\n await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);\n if (!sameInstance) {\n if (moved) {\n // No other owner.json can have appeared meanwhile: only a process\n // whose mkdir created this `.lock` publishes one, and it already has.\n await rename(claimPath, ownerPath).catch(() => undefined);\n } else {\n await unlink(claimPath).catch(() => undefined);\n }\n return false;\n }\n if (!moved) await rm(ownerPath, { force: true });\n await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);\n await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);\n // Drop any claim a crashed recoverer of this release left, then this one.\n for (const name of (await readdir(lockPath).catch(() => [])).filter((entry) => RECOVERY_CLAIM.test(entry))) {\n if (join(lockPath, name) !== claimPath) await rm(join(lockPath, name), { force: true });\n }\n // Empty now, and still the dead holder's directory if this claim was in\n // it: remove it rather than leave an ownerless lock to age out.\n if (await unlink(claimPath).then(() => true, () => false)) await rmdir(lockPath).catch(() => undefined);\n await removeAgedOwnerFiles(lockPath, staleAfterMs);\n return true;\n}\n\n/**\n * Whether an owner record (its exact bytes, read from `ownerPath`) is\n * abandoned: a holder's whose PID is gone, older than the stale threshold;\n * or, if it is not a complete record, a file older than that threshold.\n * Every release publishes complete records (by link or rename) except this\n * one's no-hard-link fallback, which writes in place while holding a turn;\n * so an incomplete record seen while holding a turn was left by a crash.\n */\nasync function isAbandonedOwner(observed: string, ownerPath: string, staleAfterMs: number): Promise<boolean> {\n let owner: unknown;\n try {\n owner = JSON.parse(observed);\n } catch {\n owner = null;\n }\n if (typeof owner === \"object\" && owner !== null && \"pid\" in owner) {\n return isStaleOwner(owner as { pid?: unknown; createdAt?: unknown }, staleAfterMs);\n }\n const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);\n return Date.now() - modifiedAt >= staleAfterMs;\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\n/** Whether `path` exists. Only ENOENT/ENOTDIR mean it does not; other errors are thrown. */\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch (error) {\n if (isErrno(error, \"ENOENT\") || isErrno(error, \"ENOTDIR\")) return false;\n throw error;\n }\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":["_a","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
|
+
"version": "0.4.0-beta.8",
|
|
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,7 +64,7 @@
|
|
|
59
64
|
"check:generated": "bun scripts/generate.ts --check"
|
|
60
65
|
},
|
|
61
66
|
"dependencies": {
|
|
62
|
-
"@tinycloud/node-sdk": "3.1.0-beta.
|
|
67
|
+
"@tinycloud/node-sdk": "3.1.0-beta.5",
|
|
63
68
|
"@tinycloud/sdk-core": "3.1.0-beta.4",
|
|
64
69
|
"node-sql-parser": "5.4.0",
|
|
65
70
|
"zod": "^3.22.0",
|