@tinycloud/operations 0.3.3-beta.9 → 0.3.4-beta.0

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 mkdir,\n readFile,\n rename,\n rmdir,\n rm,\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\";\nconst TEST_LOCK_RECOVERY_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR\";\nconst invocationStateRoot = new AsyncLocalStorage<string>();\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 });\n try {\n await writeFile(temporaryPath, contents, \"utf8\");\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 */\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 release = await acquireProfileLock(normalizedProfile, options);\n try {\n return await action();\n } finally {\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 await mkdir(profilePath(profile), { recursive: true });\n\n while (true) {\n try {\n await mkdir(lockPath);\n const token = randomUUID();\n try {\n await writeJsonAtomic(profileLockMetadataPath(profile), {\n pid: process.pid,\n createdAt: new Date().toISOString(),\n token,\n });\n } catch (error) {\n await rmdir(lockPath).catch(() => undefined);\n throw error;\n }\n\n return async () => {\n await releaseProfileLock(lockPath, token);\n };\n } catch (error) {\n if (!isLockAlreadyHeld(error)) throw error;\n await signalTestLockContention(profile);\n }\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\nasync function releaseProfileLock(lockPath: string, token: string): Promise<void> {\n const ownerPath = join(lockPath, \"owner.json\");\n const claimPath = join(lockPath, `.release-${token}.json`);\n try {\n await rename(ownerPath, claimPath);\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return;\n throw error;\n }\n\n const claimed = await readJson<{ token?: unknown }>(claimPath).catch(() => null);\n if (claimed?.token !== token) {\n await rename(claimPath, ownerPath).catch(() => undefined);\n return;\n }\n\n await rm(claimPath, { force: true });\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\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 (!isStaleOwner(owner, staleAfterMs)) return false;\n\n // Claim the observed metadata file, rather than renaming/removing the lock\n // directory. A contender can only acquire the directory after it is empty;\n // rmdir below therefore cannot remove a replacement lock instance.\n const observedToken = typeof owner?.token === \"string\" && owner.token.length > 0\n ? owner.token\n : \"legacy\";\n await waitForTestStaleRecoveryBarrier(profile);\n const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);\n try {\n await rename(ownerPath, claimPath);\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return false;\n return false;\n }\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 if (!sameInstance) {\n await rename(claimPath, ownerPath).catch(() => undefined);\n return false;\n }\n\n await rm(claimPath, { force: true });\n await rmdir(lockPath).catch(() => undefined);\n return true;\n}\n\nasync function waitForTestStaleRecoveryBarrier(profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];\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 // An ownerless or malformed directory may still be between mkdir and\n // metadata publication, so reclaiming it could let two writers enter the\n // critical section. Current writers claim .lock with exclusive mkdir before\n // publishing owner metadata and therefore take the same conservative path.\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 isLockAlreadyHeld(error: unknown): boolean {\n return isErrno(error, \"EEXIST\") || isErrno(error, \"ENOTEMPTY\");\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":["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\";\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 await mkdir(profilePath(profile), { 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 directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {\n await chmod(directory, PRIVATE_DIR_MODE);\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 * 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 throw error;\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.d.cts CHANGED
@@ -55,6 +55,14 @@ declare function readProfileStore<T>(profile: string, store: Exclude<ProfileStor
55
55
  * Runs a small critical section under the one advisory lock shared by all
56
56
  * profile stores. Lock release verifies its ownership token before removing
57
57
  * the exact metadata instance it acquired.
58
+ *
59
+ * Reentrant within one async call chain: a critical section that already
60
+ * holds a profile's lock (for example a login's compare-and-commit) can call
61
+ * store writers that take the same lock without deadlocking. Ownership is
62
+ * keyed by the resolved lock path (so switching state roots inside a lock
63
+ * still takes the other root's lock) and lasts exactly as long as the
64
+ * acquisition: it is revoked before release, so work deferred past the
65
+ * critical section waits for the lock like any other caller.
58
66
  */
59
67
  declare function withProfileLock<T>(profile: string, action: () => Promise<T>, options?: ProfileLockOptions): Promise<T>;
60
68
  /**
package/dist/state.d.ts CHANGED
@@ -55,6 +55,14 @@ declare function readProfileStore<T>(profile: string, store: Exclude<ProfileStor
55
55
  * Runs a small critical section under the one advisory lock shared by all
56
56
  * profile stores. Lock release verifies its ownership token before removing
57
57
  * the exact metadata instance it acquired.
58
+ *
59
+ * Reentrant within one async call chain: a critical section that already
60
+ * holds a profile's lock (for example a login's compare-and-commit) can call
61
+ * store writers that take the same lock without deadlocking. Ownership is
62
+ * keyed by the resolved lock path (so switching state roots inside a lock
63
+ * still takes the other root's lock) and lasts exactly as long as the
64
+ * acquisition: it is revoked before release, so work deferred past the
65
+ * critical section waits for the lock like any other caller.
58
66
  */
59
67
  declare function withProfileLock<T>(profile: string, action: () => Promise<T>, options?: ProfileLockOptions): Promise<T>;
60
68
  /**
package/dist/state.js CHANGED
@@ -2,11 +2,15 @@
2
2
  import { randomUUID } from "crypto";
3
3
  import { AsyncLocalStorage } from "async_hooks";
4
4
  import {
5
+ chmod,
6
+ link,
5
7
  mkdir,
6
8
  readFile,
9
+ readdir,
7
10
  rename,
8
11
  rmdir,
9
12
  rm,
13
+ stat,
10
14
  writeFile
11
15
  } from "fs/promises";
12
16
  import { homedir } from "os";
@@ -16,8 +20,18 @@ var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
16
20
  var DEFAULT_LOCK_RETRY_MS = 25;
17
21
  var DEFAULT_STALE_LOCK_MS = 3e4;
18
22
  var TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
23
+ var PRIVATE_FILE_MODE = 384;
24
+ var PRIVATE_DIR_MODE = 448;
19
25
  var TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
26
+ var TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
27
+ var TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
28
+ var TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
29
+ var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
30
+ var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
31
+ var TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
32
+ var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
20
33
  var invocationStateRoot = new AsyncLocalStorage();
34
+ var heldProfileLocks = new AsyncLocalStorage();
21
35
  var ProfileLockTimeoutError = class extends Error {
22
36
  constructor(profile, timeoutMs) {
23
37
  super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
@@ -85,9 +99,9 @@ async function writeJsonAtomic(filePath, value) {
85
99
  );
86
100
  const contents = `${JSON.stringify(value, null, 2)}
87
101
  `;
88
- await mkdir(directory, { recursive: true });
102
+ await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
89
103
  try {
90
- await writeFile(temporaryPath, contents, "utf8");
104
+ await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
91
105
  await rename(temporaryPath, filePath);
92
106
  } catch (error) {
93
107
  await rm(temporaryPath, { force: true }).catch(() => void 0);
@@ -127,10 +141,15 @@ async function readProfileStore(profile, store) {
127
141
  }
128
142
  async function withProfileLock(profile, action, options = {}) {
129
143
  const normalizedProfile = validateProfileName(profile);
144
+ const lockPath = profileLockPath(normalizedProfile);
145
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
146
+ if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
130
147
  const release = await acquireProfileLock(normalizedProfile, options);
148
+ const ownership = { lockPath, active: true };
131
149
  try {
132
- return await action();
150
+ return await heldProfileLocks.run([...held, ownership], action);
133
151
  } finally {
152
+ ownership.active = false;
134
153
  await release();
135
154
  }
136
155
  }
@@ -181,28 +200,18 @@ async function acquireProfileLock(profile, options) {
181
200
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
182
201
  const startedAt = Date.now();
183
202
  const lockPath = profileLockPath(profile);
184
- await mkdir(profilePath(profile), { recursive: true });
203
+ await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
204
+ for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
205
+ await chmod(directory, PRIVATE_DIR_MODE);
206
+ }
185
207
  while (true) {
186
- try {
187
- await mkdir(lockPath);
188
- const token = randomUUID();
189
- try {
190
- await writeJsonAtomic(profileLockMetadataPath(profile), {
191
- pid: process.pid,
192
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
193
- token
194
- });
195
- } catch (error) {
196
- await rmdir(lockPath).catch(() => void 0);
197
- throw error;
198
- }
208
+ const token = randomUUID();
209
+ if (await publishProfileLock(profile, lockPath, token)) {
199
210
  return async () => {
200
- await releaseProfileLock(lockPath, token);
211
+ await releaseProfileLock(profile, lockPath, token);
201
212
  };
202
- } catch (error) {
203
- if (!isLockAlreadyHeld(error)) throw error;
204
- await signalTestLockContention(profile);
205
213
  }
214
+ await signalTestLockContention(profile);
206
215
  if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
207
216
  const elapsedMs = Date.now() - startedAt;
208
217
  if (elapsedMs >= timeoutMs) {
@@ -211,21 +220,35 @@ async function acquireProfileLock(profile, options) {
211
220
  await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
212
221
  }
213
222
  }
214
- async function releaseProfileLock(lockPath, token) {
215
- const ownerPath = join(lockPath, "owner.json");
216
- const claimPath = join(lockPath, `.release-${token}.json`);
223
+ async function publishProfileLock(profile, lockPath, token) {
217
224
  try {
218
- await rename(ownerPath, claimPath);
225
+ await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
219
226
  } catch (error) {
220
- if (isErrno(error, "ENOENT")) return;
227
+ if (isErrno(error, "EEXIST")) return false;
221
228
  throw error;
222
229
  }
223
- const claimed = await readJson(claimPath).catch(() => null);
224
- if (claimed?.token !== token) {
225
- await rename(claimPath, ownerPath).catch(() => void 0);
226
- return;
230
+ await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
231
+ const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
232
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
233
+ try {
234
+ await writeFile(staged, `${JSON.stringify(owner, null, 2)}
235
+ `, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
236
+ await link(staged, join(lockPath, "owner.json"));
237
+ return true;
238
+ } catch (error) {
239
+ if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
240
+ await rmdir(lockPath).catch(() => void 0);
241
+ throw error;
242
+ } finally {
243
+ await rm(staged, { force: true }).catch(() => void 0);
227
244
  }
228
- await rm(claimPath, { force: true });
245
+ }
246
+ async function releaseProfileLock(profile, lockPath, token) {
247
+ const ownerPath = join(lockPath, "owner.json");
248
+ const owner = await readJson(ownerPath).catch(() => null);
249
+ if (owner?.token !== token) return;
250
+ await rm(ownerPath, { force: true });
251
+ await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
229
252
  await rmdir(lockPath).catch(() => void 0);
230
253
  }
231
254
  async function signalTestLockContention(profile) {
@@ -242,32 +265,79 @@ async function signalTestLockContention(profile) {
242
265
  await writeFile(signalPath, `${profile}
243
266
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
244
267
  }
268
+ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
269
+ try {
270
+ const { mtimeMs } = await stat(lockPath);
271
+ if (Date.now() - mtimeMs < staleAfterMs) return false;
272
+ } catch {
273
+ return false;
274
+ }
275
+ const checkedAt = performance.now();
276
+ await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
277
+ try {
278
+ const entries = await readdir(lockPath);
279
+ if (entries.includes("owner.json")) return false;
280
+ if (recoveryFenced(checkedAt, staleAfterMs)) return false;
281
+ for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
282
+ await rm(join(lockPath, name), { force: true });
283
+ }
284
+ await rmdir(lockPath);
285
+ } catch {
286
+ return false;
287
+ }
288
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
289
+ return true;
290
+ }
291
+ function recoveryFenced(since, staleAfterMs) {
292
+ return performance.now() - since >= staleAfterMs / 2;
293
+ }
294
+ var ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
295
+ var STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
296
+ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
297
+ const directory = dirname(lockPath);
298
+ const names = await readdir(directory).catch(() => []);
299
+ for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
300
+ const path = join(directory, name);
301
+ const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
302
+ if (aged) await rm(path, { force: true }).catch(() => void 0);
303
+ }
304
+ }
245
305
  async function recoverStaleLock(profile, lockPath, staleAfterMs) {
246
306
  const ownerPath = join(lockPath, "owner.json");
247
307
  const owner = await readJson(ownerPath).catch(() => null);
308
+ if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
248
309
  if (!isStaleOwner(owner, staleAfterMs)) return false;
249
310
  const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
250
- await waitForTestStaleRecoveryBarrier(profile);
311
+ await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
251
312
  const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
252
313
  try {
253
- await rename(ownerPath, claimPath);
254
- } catch (error) {
255
- if (isErrno(error, "ENOENT")) return false;
314
+ await link(ownerPath, claimPath);
315
+ } catch {
256
316
  return false;
257
317
  }
318
+ const claimedAt = performance.now();
319
+ await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
258
320
  const claimed = await readJson(claimPath).catch(() => null);
259
321
  const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
260
- if (!sameInstance) {
261
- await rename(claimPath, ownerPath).catch(() => void 0);
322
+ await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
323
+ if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
324
+ await rm(claimPath, { force: true });
325
+ await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
262
326
  return false;
263
327
  }
328
+ if (sameInstance) {
329
+ await rm(ownerPath, { force: true });
330
+ await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
331
+ }
264
332
  await rm(claimPath, { force: true });
265
333
  await rmdir(lockPath).catch(() => void 0);
334
+ if (!sameInstance) return false;
335
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
266
336
  return true;
267
337
  }
268
- async function waitForTestStaleRecoveryBarrier(profile) {
338
+ async function waitForTestBarrier(environmentName, profile) {
269
339
  if (process.env.NODE_ENV !== "test") return;
270
- const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];
340
+ const barrierDirectory = process.env[environmentName];
271
341
  if (!barrierDirectory) return;
272
342
  await mkdir(barrierDirectory, { recursive: true });
273
343
  const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);
@@ -291,9 +361,6 @@ function isStaleOwner(owner, staleAfterMs) {
291
361
  }
292
362
  return !isProcessAlive(owner.pid);
293
363
  }
294
- function isLockAlreadyHeld(error) {
295
- return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
296
- }
297
364
  function isProcessAlive(pid) {
298
365
  try {
299
366
  process.kill(pid, 0);
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 mkdir,\n readFile,\n rename,\n rmdir,\n rm,\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\";\nconst TEST_LOCK_RECOVERY_BARRIER_DIR = \"TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR\";\nconst invocationStateRoot = new AsyncLocalStorage<string>();\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 });\n try {\n await writeFile(temporaryPath, contents, \"utf8\");\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 */\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 release = await acquireProfileLock(normalizedProfile, options);\n try {\n return await action();\n } finally {\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 await mkdir(profilePath(profile), { recursive: true });\n\n while (true) {\n try {\n await mkdir(lockPath);\n const token = randomUUID();\n try {\n await writeJsonAtomic(profileLockMetadataPath(profile), {\n pid: process.pid,\n createdAt: new Date().toISOString(),\n token,\n });\n } catch (error) {\n await rmdir(lockPath).catch(() => undefined);\n throw error;\n }\n\n return async () => {\n await releaseProfileLock(lockPath, token);\n };\n } catch (error) {\n if (!isLockAlreadyHeld(error)) throw error;\n await signalTestLockContention(profile);\n }\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\nasync function releaseProfileLock(lockPath: string, token: string): Promise<void> {\n const ownerPath = join(lockPath, \"owner.json\");\n const claimPath = join(lockPath, `.release-${token}.json`);\n try {\n await rename(ownerPath, claimPath);\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return;\n throw error;\n }\n\n const claimed = await readJson<{ token?: unknown }>(claimPath).catch(() => null);\n if (claimed?.token !== token) {\n await rename(claimPath, ownerPath).catch(() => undefined);\n return;\n }\n\n await rm(claimPath, { force: true });\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\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 (!isStaleOwner(owner, staleAfterMs)) return false;\n\n // Claim the observed metadata file, rather than renaming/removing the lock\n // directory. A contender can only acquire the directory after it is empty;\n // rmdir below therefore cannot remove a replacement lock instance.\n const observedToken = typeof owner?.token === \"string\" && owner.token.length > 0\n ? owner.token\n : \"legacy\";\n await waitForTestStaleRecoveryBarrier(profile);\n const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);\n try {\n await rename(ownerPath, claimPath);\n } catch (error) {\n if (isErrno(error, \"ENOENT\")) return false;\n return false;\n }\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 if (!sameInstance) {\n await rename(claimPath, ownerPath).catch(() => undefined);\n return false;\n }\n\n await rm(claimPath, { force: true });\n await rmdir(lockPath).catch(() => undefined);\n return true;\n}\n\nasync function waitForTestStaleRecoveryBarrier(profile: string): Promise<void> {\n if (process.env.NODE_ENV !== \"test\") return;\n const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];\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 // An ownerless or malformed directory may still be between mkdir and\n // metadata publication, so reclaiming it could let two writers enter the\n // critical section. Current writers claim .lock with exclusive mkdir before\n // publishing owner metadata and therefore take the same conservative path.\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 isLockAlreadyHeld(error: unknown): boolean {\n return isErrno(error, \"EEXIST\") || isErrno(error, \"ENOTEMPTY\");\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":["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\";\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 await mkdir(profilePath(profile), { 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 directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {\n await chmod(directory, PRIVATE_DIR_MODE);\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 * 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 throw error;\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinycloud/operations",
3
- "version": "0.3.3-beta.9",
3
+ "version": "0.3.4-beta.0",
4
4
  "description": "TinyCloud shared operations contracts and runtime",
5
5
  "author": "TinyCloud, Inc.",
6
6
  "license": "EGPL",
@@ -59,8 +59,8 @@
59
59
  "check:generated": "bun scripts/generate.ts --check"
60
60
  },
61
61
  "dependencies": {
62
- "@tinycloud/node-sdk": "3.0.0-beta.11",
63
- "@tinycloud/sdk-core": "3.0.0-beta.11",
62
+ "@tinycloud/node-sdk": "3.0.1-beta.0",
63
+ "@tinycloud/sdk-core": "3.0.1-beta.0",
64
64
  "node-sql-parser": "5.4.0",
65
65
  "zod": "^3.22.0",
66
66
  "zod-to-json-schema": "^3.22.0"