@tinycloud/operations 0.2.1 → 0.3.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.
- package/dist/artifacts.cjs +3 -1
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.d.cts +1 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +4 -2
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +27 -3
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.d.cts +1 -1
- package/dist/cli-runtime.d.ts +1 -1
- package/dist/cli-runtime.js +28 -4
- package/dist/cli-runtime.js.map +1 -1
- package/dist/{contract-OnWT3OXa.d.cts → contract-xMIIMtpF.d.cts} +6 -0
- package/dist/{contract-OnWT3OXa.d.ts → contract-xMIIMtpF.d.ts} +6 -0
- package/dist/index.cjs +34 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +35 -12
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +3 -1
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.d.cts +1 -1
- package/dist/profile.d.ts +1 -1
- package/dist/profile.js +4 -2
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +12 -1
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.cts +3 -1
- package/dist/state.d.ts +3 -1
- package/dist/state.js +12 -2
- package/dist/state.js.map +1 -1
- package/package.json +1 -1
package/dist/state.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/state.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\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, 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\";\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 = process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();\n return join(home, \".tinycloud\");\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":";AAAA,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe;AACxB,SAAS,UAAU,SAAS,MAAM,UAAU,eAAe;AAE3D,IAAM,kBAAkB;AACxB,IAAM,0BAA0B;AAChC,IAAM,wBAAwB;AAC9B,IAAM,wBAAwB;AAC9B,IAAM,mCAAmC;AACzC,IAAM,iCAAiC;AAsBhC,IAAM,0BAAN,cAAsC,MAAM;AAAA,EAGjD,YAAY,SAAiB,WAAmB;AAC9C,UAAM,+CAA+C,OAAO,WAAW,SAAS,KAAK;AAHvF,SAAS,OAAO;AAId,SAAK,OAAO;AAAA,EACd;AACF;AAMO,SAAS,oBAA4B;AAC1C,QAAM,OAAO,QAAQ,IAAI,WAAW,QAAQ,IAAI,QAAQ,QAAQ,IAAI,eAAe,QAAQ;AAC3F,SAAO,KAAK,MAAM,YAAY;AAChC;AAEO,SAAS,sBAA8B;AAC5C,SAAO,KAAK,kBAAkB,GAAG,aAAa;AAChD;AAEO,SAAS,eAAuB;AACrC,SAAO,KAAK,kBAAkB,GAAG,UAAU;AAC7C;AAEO,SAAS,YAAY,SAAyB;AACnD,SAAO,KAAK,aAAa,GAAG,oBAAoB,OAAO,CAAC;AAC1D;AAEO,SAAS,kBAAkB,SAAyB;AACzD,SAAO,KAAK,YAAY,OAAO,GAAG,cAAc;AAClD;AAEO,SAAS,YAAY,SAAyB;AACnD,SAAO,iBAAiB,SAAS,SAAS;AAC5C;AAEO,SAAS,0BAA0B,SAAyB;AACjE,SAAO,iBAAiB,SAAS,wBAAwB;AAC3D;AAEO,SAAS,iBAAiB,SAAyB;AACxD,SAAO,iBAAiB,SAAS,eAAe;AAClD;AAEO,SAAS,iBAAiB,SAAiB,OAAiC;AACjF,SAAO,KAAK,YAAY,OAAO,GAAG,GAAG,KAAK,OAAO;AACnD;AAGO,SAAS,yBAAyB,SAAiB,OAAiC;AACzF,SAAO,GAAG,iBAAiB,SAAS,KAAK,CAAC;AAC5C;AAEO,SAAS,gBAAgB,SAAyB;AACvD,SAAO,KAAK,YAAY,OAAO,GAAG,OAAO;AAC3C;AAEO,SAAS,wBAAwB,SAAyB;AAC/D,SAAO,KAAK,gBAAgB,OAAO,GAAG,YAAY;AACpD;AAMA,eAAsB,SAAY,UAAqC;AACrE,MAAI;AACF,WAAO,KAAK,MAAM,MAAM,SAAS,UAAU,MAAM,CAAC;AAAA,EACpD,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,QAAQ,EAAG,QAAO;AACrC,UAAM;AAAA,EACR;AACF;AAMA,eAAsB,gBAAgB,UAAkB,OAA+B;AACrF,QAAM,YAAY,QAAQ,QAAQ;AAClC,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA,IAAI,SAAS,QAAQ,CAAC,IAAI,QAAQ,GAAG,IAAI,WAAW,CAAC;AAAA,EACvD;AACA,QAAM,WAAW,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA;AAElD,QAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAC1C,MAAI;AACF,UAAM,UAAU,eAAe,UAAU,MAAM;AAC/C,UAAM,OAAO,eAAe,QAAQ;AAAA,EACtC,SAAS,OAAO;AACd,UAAM,GAAG,eAAe,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAC9D,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,YACpB,SACmB;AACnB,SAAO,SAAY,kBAAkB,OAAO,CAAC;AAC/C;AAEA,eAAsB,YACpB,SACmB;AACnB,QAAM,kBAAkB,SAAS,SAAS;AAC1C,SAAO,SAAY,YAAY,OAAO,CAAC;AACzC;AAEA,eAAsB,0BACpB,SACc;AACd,UAAQ,MAAM,iBAAoB,SAAS,wBAAwB,GAAG;AACxE;AAEA,eAAsB,iBACpB,SACc;AACd,UAAQ,MAAM,iBAAoB,SAAS,eAAe,GAAG;AAC/D;AAEA,eAAsB,kBACpB,SACA,OACwB;AACxB,QAAM,WAAW,MAAM,SAAwB,yBAAyB,SAAS,KAAK,CAAC;AACvF,MAAI,aAAa,KAAM,QAAO,EAAE,eAAe,EAAE;AACjD,MACE,OAAO,aAAa,YACpB,SAAS,kBAAkB,GAC3B;AACA,UAAM,IAAI,UAAU,iCAAiC,KAAK,IAAI;AAAA,EAChE;AACA,SAAO;AACT;AAEA,eAAsB,iBACpB,SACA,OACkC;AAClC,QAAM,CAAC,UAAU,GAAG,IAAI,MAAM,QAAQ,IAAI;AAAA,IACxC,kBAAkB,SAAS,KAAK;AAAA,IAChC,SAAkB,iBAAiB,SAAS,KAAK,CAAC;AAAA,EACpD,CAAC;AACD,SAAO;AAAA,IACL,eAAe,SAAS;AAAA,IACxB,SAAS,MAAM,QAAQ,GAAG,IAAI,MAAa,CAAC;AAAA,EAC9C;AACF;AAOA,eAAsB,gBACpB,SACA,QACA,UAA8B,CAAC,GACnB;AACZ,QAAM,oBAAoB,oBAAoB,OAAO;AACrD,QAAM,UAAU,MAAM,mBAAmB,mBAAmB,OAAO;AACnE,MAAI;AACF,WAAO,MAAM,OAAO;AAAA,EACtB,UAAE;AACA,UAAM,QAAQ;AAAA,EAChB;AACF;AAOA,eAAsB,oBACpB,SACA,OACA,KACA,QACA,QACA,UAA8B,CAAC,GACjB;AACd,MAAI,CAAC,IAAK,OAAM,IAAI,UAAU,qCAAqC;AAEnE,SAAO,gBAAgB,SAAS,YAAY;AAC1C,UAAM,WAAW,MAAM,iBAAoB,SAAS,KAAK,GAAG;AAC5D,UAAM,OAAO,QAAQ,OAAO,CAAC,cAAc,OAAO,SAAS,MAAM,GAAG;AACpE,SAAK,KAAK,MAAM;AAChB,UAAM,gBAAgB,iBAAiB,SAAS,KAAK,GAAG,IAAI;AAC5D,UAAM,uBAAuB,SAAS,KAAK;AAC3C,WAAO;AAAA,EACT,GAAG,OAAO;AACZ;AAOA,eAAsB,mBACpB,SACA,OACA,QAMA,UAA8B,CAAC,GACd;AACjB,SAAO;AAAA,IACL;AAAA,IACA,MAAM,8BAA8B,SAAS,OAAO,MAAM;AAAA,IAC1D;AAAA,EACF;AACF;AAGA,eAAe,8BACb,SACA,OACA,QAMiB;AACjB,QAAM,UAAU,MAAM,iBAAoB,SAAS,KAAK;AACxD,QAAM,OAAO,MAAM,OAAO,QAAQ,OAAO;AACzC,QAAM,gBAAgB,iBAAiB,SAAS,KAAK,GAAG,KAAK,OAAO;AACpE,QAAM,uBAAuB,SAAS,KAAK;AAC3C,SAAO,KAAK;AACd;AAEA,eAAsB,aACpB,SACA,SACA,UAA8B,CAAC,GAChB;AACf,QAAM,gBAAgB,SAAS,YAAY;AACzC,UAAM,kBAAkB,SAAS,SAAS;AAC1C,UAAM,gBAAgB,YAAY,OAAO,GAAG,OAAO;AACnD,UAAM,uBAAuB,SAAS,SAAS;AAAA,EACjD,GAAG,OAAO;AACZ;AAEA,eAAsB,cACpB,SACA,UAA8B,CAAC,GAChB;AACf,QAAM,gBAAgB,SAAS,YAAY;AACzC,UAAM,kBAAkB,SAAS,SAAS;AAC1C,UAAM,GAAG,YAAY,OAAO,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,EAChD,GAAG,OAAO;AACZ;AAEA,eAAe,uBAAuB,SAAiB,OAAwC;AAC7F,QAAM,gBAAgB,yBAAyB,SAAS,KAAK,GAAG,EAAE,eAAe,EAAE,CAAC;AACtF;AAEA,eAAe,mBACb,SACA,SAC8B;AAC9B,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,YAAY,KAAK,IAAI;AAC3B,QAAM,WAAW,gBAAgB,OAAO;AAExC,QAAM,MAAM,YAAY,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AAErD,SAAO,MAAM;AACX,QAAI;AACF,YAAM,MAAM,QAAQ;AACpB,YAAM,QAAQ,WAAW;AACzB,UAAI;AACF,cAAM,gBAAgB,wBAAwB,OAAO,GAAG;AAAA,UACtD,KAAK,QAAQ;AAAA,UACb,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,UAClC;AAAA,QACF,CAAC;AAAA,MACH,SAAS,OAAO;AACd,cAAM,MAAM,QAAQ,EAAE,MAAM,MAAM,MAAS;AAC3C,cAAM;AAAA,MACR;AAEA,aAAO,YAAY;AACjB,cAAM,mBAAmB,UAAU,KAAK;AAAA,MAC1C;AAAA,IACF,SAAS,OAAO;AACd,UAAI,CAAC,kBAAkB,KAAK,EAAG,OAAM;AACrC,YAAM,yBAAyB,OAAO;AAAA,IACxC;AAEA,QAAI,MAAM,iBAAiB,SAAS,UAAU,YAAY,EAAG;AAE7D,UAAM,YAAY,KAAK,IAAI,IAAI;AAC/B,QAAI,aAAa,WAAW;AAC1B,YAAM,IAAI,wBAAwB,SAAS,SAAS;AAAA,IACtD;AACA,UAAM,MAAM,KAAK,IAAI,SAAS,YAAY,SAAS,CAAC;AAAA,EACtD;AACF;AAEA,eAAe,mBAAmB,UAAkB,OAA8B;AAChF,QAAM,YAAY,KAAK,UAAU,YAAY;AAC7C,QAAM,YAAY,KAAK,UAAU,YAAY,KAAK,OAAO;AACzD,MAAI;AACF,UAAM,OAAO,WAAW,SAAS;AAAA,EACnC,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,QAAQ,EAAG;AAC9B,UAAM;AAAA,EACR;AAEA,QAAM,UAAU,MAAM,SAA8B,SAAS,EAAE,MAAM,MAAM,IAAI;AAC/E,MAAI,SAAS,UAAU,OAAO;AAC5B,UAAM,OAAO,WAAW,SAAS,EAAE,MAAM,MAAM,MAAS;AACxD;AAAA,EACF;AAEA,QAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AACnC,QAAM,MAAM,QAAQ,EAAE,MAAM,MAAM,MAAS;AAC7C;AAQA,eAAe,yBAAyB,SAAgC;AACtE,MAAI,QAAQ,IAAI,aAAa,OAAQ;AAErC,QAAM,iBAAiB,QAAQ,IAAI,gCAAgC;AACnE,QAAM,iBAAiB,QAAQ,IAAI;AACnC,MAAI,CAAC,kBAAkB,CAAC,eAAgB;AAExC,QAAM,OAAO,QAAQ,cAAc;AACnC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,eAAe,SAAS,MAAM,UAAU;AAC9C,MACE,iBAAiB,MACjB,iBAAiB,QACjB,aAAa,WAAW,KAAK,QAAQ,aAAa,UAAU,OAAO,GAAG,EAAE,KACxE,aAAa,WAAW,KAAK,KAC7B,aAAa,WAAW,MAAM,GAC9B;AACA;AAAA,EACF;AAEA,QAAM,UAAU,YAAY,GAAG,OAAO;AAAA,GAAM,EAAE,UAAU,QAAQ,MAAM,KAAK,CAAC,EACzE,MAAM,MAAM,MAAS;AAC1B;AAEA,eAAe,iBAAiB,SAAiB,UAAkB,cAAwC;AACzG,QAAM,YAAY,KAAK,UAAU,YAAY;AAC7C,QAAM,QAAQ,MAAM,SAAkE,SAAS,EAC5F,MAAM,MAAM,IAAI;AACnB,MAAI,CAAC,aAAa,OAAO,YAAY,EAAG,QAAO;AAK/C,QAAM,gBAAgB,OAAO,OAAO,UAAU,YAAY,MAAM,MAAM,SAAS,IAC3E,MAAM,QACN;AACJ,QAAM,gCAAgC,OAAO;AAC7C,QAAM,YAAY,KAAK,UAAU,UAAU,WAAW,CAAC,OAAO;AAC9D,MAAI;AACF,UAAM,OAAO,WAAW,SAAS;AAAA,EACnC,SAAS,OAAO;AACd,QAAI,QAAQ,OAAO,QAAQ,EAAG,QAAO;AACrC,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,MAAM,SAAkE,SAAS,EAC9F,MAAM,MAAM,IAAI;AACnB,QAAM,eAAe,YAAY,SAC/B,kBAAkB,WACd,QAAQ,UAAU,SAClB,QAAQ,UAAU;AAExB,MAAI,CAAC,cAAc;AACjB,UAAM,OAAO,WAAW,SAAS,EAAE,MAAM,MAAM,MAAS;AACxD,WAAO;AAAA,EACT;AAEA,QAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AACnC,QAAM,MAAM,QAAQ,EAAE,MAAM,MAAM,MAAS;AAC3C,SAAO;AACT;AAEA,eAAe,gCAAgC,SAAgC;AAC7E,MAAI,QAAQ,IAAI,aAAa,OAAQ;AACrC,QAAM,mBAAmB,QAAQ,IAAI,8BAA8B;AACnE,MAAI,CAAC,iBAAkB;AACvB,QAAM,MAAM,kBAAkB,EAAE,WAAW,KAAK,CAAC;AACjD,QAAM,YAAY,KAAK,kBAAkB,SAAS,QAAQ,GAAG,IAAI,OAAO,EAAE;AAC1E,QAAM,UAAU,WAAW,WAAW,EAAE,UAAU,QAAQ,MAAM,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAC7F,QAAM,cAAc,KAAK,kBAAkB,SAAS;AACpD,SAAO,MAAM;AACX,QAAI;AACF,YAAM,SAAS,aAAa,MAAM;AAClC;AAAA,IACF,QAAQ;AACN,YAAM,MAAM,CAAC;AAAA,IACf;AAAA,EACF;AACF;AAEA,SAAS,aACP,OACA,cACS;AACT,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,YAAY,OAAO,OAAO,cAAc,WAC1C,KAAK,MAAM,MAAM,SAAS,IAC1B,OAAO;AAOX,MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,MAAM,YAAY,aAAc,QAAO;AAC1E,MAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,UAAU,MAAM,GAAG,KAAK,MAAM,OAAO,GAAG;AACpF,WAAO;AAAA,EACT;AACA,SAAO,CAAC,eAAe,MAAM,GAAG;AAClC;AAEA,SAAS,kBAAkB,OAAyB;AAClD,SAAO,QAAQ,OAAO,QAAQ,KAAK,QAAQ,OAAO,WAAW;AAC/D;AAEA,SAAS,eAAe,KAAsB;AAC5C,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,CAAC,QAAQ,OAAO,OAAO;AAAA,EAChC;AACF;AAEA,SAAS,oBAAoB,SAAyB;AACpD,MACE,CAAC,WACD,YAAY,OACZ,YAAY,QACZ,QAAQ,SAAS,GAAG,KACpB,QAAQ,SAAS,IAAI,KACrB,QAAQ,SAAS,IAAI,GACrB;AACA,UAAM,IAAI,UAAU,gDAAgD;AAAA,EACtE;AACA,SAAO;AACT;AAEA,SAAS,QAAQ,OAAgB,MAAuB;AACtD,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,SAC7D,MAA6B,SAAS;AAC3C;AAEA,SAAS,MAAM,cAAqC;AAClD,SAAO,IAAI,QAAQ,CAACA,aAAY,WAAWA,UAAS,YAAY,CAAC;AACnE;","names":["resolve"]}
|
|
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"]}
|