import { createHash, randomUUID } from "node:crypto"; import { basename, dirname, join, resolve } from "node:path"; import { lockDownLocalPath, recallWindowsLockdown, refreshWindowsLockdown, rememberWindowsLockdown, retryWindowsFileSharing, validateLocalDockerHost, } from "../infrastructure/local-process.js"; import { getLocalProcessIdentity, getLocalPidNamespaceIdentity } from "../infrastructure/process-identity.js"; const OWNER_FILE = ".owner.json"; const RECLAIM_DIRECTORY = ".reclaim"; const SCHEMA_VERSION = 3; const PUBLICATION_GRACE_MS = 30_000; const MAX_RECLAIM_ATTEMPTS = 4; const MAX_OWNER_BYTES = 4 * 1024; const MAX_PROCESS_ID = 2_147_483_647; const MAX_IDENTITY_BYTES = 512; const failedAtomicReleases = new Map(); const WINDOWS_ACL_RETRY_DELAYS_MS = Object.freeze([20, 80]); // A process's own start identity never changes, so each identity adapter is asked once per platform. const verifiedSelfIdentities = new WeakMap(); function failure(label, message, code) { const error = new Error(`Relmio ${message} ${label}.`); if (code) error.code = code; return error; } async function lstatIfExists(fileSystem, path, label) { try { return await fileSystem.lstat(path); } catch (error) { if (error?.code === "ENOENT") return null; throw failure(label, "could not inspect the"); } } function assertPrivateDirectory(metadata, { label, platform }) { if ( !metadata?.isDirectory?.() || metadata.isSymbolicLink() || (platform !== "win32" && ( !Number.isInteger(metadata.mode) || (metadata.mode & 0o077) !== 0 )) ) { throw failure(label, "refuses an unsafe"); } } function assertPrivateOwner(metadata, { label, platform }) { if ( !metadata?.isFile?.() || metadata.isSymbolicLink() || !Number.isInteger(metadata.size) || metadata.size < 0 || metadata.size > MAX_OWNER_BYTES || (platform !== "win32" && ( !Number.isInteger(metadata.mode) || (metadata.mode & 0o077) !== 0 )) ) { throw failure(label, "refuses an unsafe owner for the"); } } function validNamespaceIdentity(value) { return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= MAX_IDENTITY_BYTES && !/[\0\r\n]/u.test(value); } function defaultLeaseNow() { return Number(process.hrtime.bigint() / 1_000_000n); } function validLeaseTime(value) { return Number.isSafeInteger(value) && value >= 0; } function leaseTimestamp(leaseNow, label) { const value = leaseNow(); if (!validLeaseTime(value)) throw failure(label, "could not inspect the lease clock for the"); return value; } function namespaceBootIdentity(namespace) { return /^linux:([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}):pid:[1-9][0-9]{0,19}$/iu .exec(namespace)?.[1].toLowerCase() ?? null; } function validPublication(value) { return ( value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === (Object.hasOwn(value, "acquiredAt") ? 7 : 6) && value.schemaVersion === SCHEMA_VERSION && Number.isSafeInteger(value.pid) && value.pid > 0 && value.pid <= MAX_PROCESS_ID && typeof value.processStartIdentity === "string" && value.processStartIdentity.length > 0 && Buffer.byteLength(value.processStartIdentity) <= MAX_IDENTITY_BYTES && !/[\0\r\n]/u.test(value.processStartIdentity) && validNamespaceIdentity(value.processNamespaceIdentity) && (!Object.hasOwn(value, "acquiredAt") || validLeaseTime(value.acquiredAt)) && typeof value.token === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu.test(value.token) && Number.isSafeInteger(value.publishedAtMs) && value.publishedAtMs > 0 ); } function samePublication(left, right) { return validPublication(left) && validPublication(right) && left.schemaVersion === right.schemaVersion && left.pid === right.pid && left.processStartIdentity === right.processStartIdentity && left.processNamespaceIdentity === right.processNamespaceIdentity && left.acquiredAt === right.acquiredAt && left.token === right.token && left.publishedAtMs === right.publishedAtMs; } function directoryFingerprint(metadata, label) { if ( !Number.isInteger(metadata?.dev) || !Number.isInteger(metadata?.ino) || !Number.isFinite(metadata?.birthtimeMs) || metadata.birthtimeMs < 0 ) { throw failure(label, "could not inspect the"); } return Object.freeze({ dev: metadata.dev, ino: metadata.ino, birthtimeMs: metadata.birthtimeMs, }); } function sameDirectoryFingerprint(left, right) { return left?.dev === right?.dev && left?.ino === right?.ino && left?.birthtimeMs === right?.birthtimeMs; } function ownerFingerprint(metadata, raw) { return Object.freeze({ raw, size: metadata.size, dev: metadata.dev, ino: metadata.ino, mtimeMs: metadata.mtimeMs, ctimeMs: metadata.ctimeMs, }); } function sameOwnerFingerprint(left, right) { return left?.raw === right?.raw && left?.size === right?.size && left?.dev === right?.dev && left?.ino === right?.ino && left?.mtimeMs === right?.mtimeMs && left?.ctimeMs === right?.ctimeMs; } function timestamp(now, label) { const value = now(); if (!Number.isSafeInteger(value) || value <= 0) { throw failure(label, "could not inspect the"); } return value; } function pastGrace(metadata, { now, graceMs, label }) { const publishedAt = metadata?.mtimeMs; const current = timestamp(now, label); if (!Number.isFinite(publishedAt) || publishedAt <= 0 || publishedAt > current) return false; return current - Math.floor(publishedAt) >= graceMs; } function validProcessIdentity(identity) { return ( identity && typeof identity === "object" && !Array.isArray(identity) && (identity.state === "dead" || identity.state === "ambiguous" || ( identity.state === "active" && typeof identity.startIdentity === "string" && identity.startIdentity.length > 0 && Buffer.byteLength(identity.startIdentity) <= MAX_IDENTITY_BYTES && !/[\0\r\n]/u.test(identity.startIdentity) )) ); } // Only a verified active identity is kept; a failed or ambiguous query is asked again next time. async function currentProcessIdentity(getProcessIdentity, platform) { const cached = verifiedSelfIdentities.get(getProcessIdentity)?.get(platform); if (cached) return cached; let identity; try { identity = await getProcessIdentity(process.pid, { platform }); } catch { return null; } if (!validProcessIdentity(identity) || identity.state !== "active") return null; const verified = Object.freeze({ state: "active", startIdentity: identity.startIdentity }); let byPlatform = verifiedSelfIdentities.get(getProcessIdentity); if (!byPlatform) { byPlatform = new Map(); verifiedSelfIdentities.set(getProcessIdentity, byPlatform); } byPlatform.set(platform, verified); return verified; } // Another process's antivirus scan or ACL check can briefly refuse a claim file on // Windows; only those refusals are retried, for a bounded time. function withWindowsSharingRetry(fileSystem, platform) { if (platform !== "win32") return fileSystem; const retried = method => (...args) => retryWindowsFileSharing(() => fileSystem[method](...args), { platform }); return { ...fileSystem, lstat: retried("lstat"), readFile: retried("readFile"), link: retried("link"), rename: retried("rename"), unlink: retried("unlink") }; } async function inspectClaim({ fileSystem, lockPath, lockDownPath, platform, label }) { let lockMetadata = await lstatIfExists(fileSystem, lockPath, label); if (!lockMetadata) throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); assertPrivateDirectory(lockMetadata, { label, platform }); if (platform === "win32") { await lockDownPath(lockPath, { platform, verifyOnly: true }); lockMetadata = await lstatIfExists(fileSystem, lockPath, label); if (!lockMetadata) throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); assertPrivateDirectory(lockMetadata, { label, platform }); } const claimDirectoryFingerprint = directoryFingerprint(lockMetadata, label); const ownerPath = join(lockPath, OWNER_FILE); let ownerMetadata = await lstatIfExists(fileSystem, ownerPath, label); if (!ownerMetadata) { return Object.freeze({ kind: "incomplete", source: "missing", ownerPath, ageMetadata: lockMetadata, directoryFingerprint: claimDirectoryFingerprint, }); } assertPrivateOwner(ownerMetadata, { label, platform }); if (platform === "win32") { await lockDownPath(ownerPath, { platform, kind: "file", verifyOnly: true }); ownerMetadata = await lstatIfExists(fileSystem, ownerPath, label); if (!ownerMetadata) throw failure(label, "detected a changed owner for the", "RELMIO_LOCK_CHANGED"); assertPrivateOwner(ownerMetadata, { label, platform }); } let raw; try { raw = await fileSystem.readFile(ownerPath, "utf8"); } catch (error) { if (error?.code === "ENOENT") throw failure(label, "detected a changed owner for the", "RELMIO_LOCK_CHANGED"); throw failure(label, "could not inspect the"); } const ownerAfterRead = await lstatIfExists(fileSystem, ownerPath, label); if (!ownerAfterRead) throw failure(label, "detected a changed owner for the", "RELMIO_LOCK_CHANGED"); assertPrivateOwner(ownerAfterRead, { label, platform }); const fingerprint = ownerFingerprint(ownerMetadata, raw); if (!sameOwnerFingerprint(fingerprint, ownerFingerprint(ownerAfterRead, raw))) { throw failure(label, "detected a changed owner for the", "RELMIO_LOCK_CHANGED"); } try { const publication = JSON.parse(raw); if (validPublication(publication)) { return Object.freeze({ kind: "published", publication: Object.freeze(publication), ownerPath, fingerprint, directoryFingerprint: claimDirectoryFingerprint, }); } } catch { /* Preserve a claim without attested namespace ownership. */ } return Object.freeze({ kind: "ambiguous", source: "malformed", ownerPath, fingerprint, directoryFingerprint: claimDirectoryFingerprint, }); } // Only deadline-fenced callers may opt into reclaiming an ambiguous lease. function leaseClaimState(claim, { processNamespaceIdentity, leaseMs, leaseNow }) { if (leaseMs === null) return "ambiguous"; const ownerBoot = namespaceBootIdentity(claim.publication.processNamespaceIdentity); const currentBoot = namespaceBootIdentity(processNamespaceIdentity); if (!ownerBoot || !currentBoot) return "ambiguous"; if (ownerBoot !== currentBoot) return "stale"; if (!validLeaseTime(claim.publication.acquiredAt)) return "ambiguous"; let current; try { current = leaseNow(); } catch { return "ambiguous"; } if (!validLeaseTime(current) || current < claim.publication.acquiredAt) return "ambiguous"; return current - claim.publication.acquiredAt > leaseMs ? "stale" : "active"; } async function claimState(claim, { getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, now, graceMs, label }) { if (claim.kind === "ambiguous") return "ambiguous"; if (claim.kind === "incomplete") { return pastGrace(claim.ageMetadata, { now, graceMs, label }) ? "stale" : "starting"; } if (!validNamespaceIdentity(processNamespaceIdentity)) return "ambiguous"; if (claim.publication.processNamespaceIdentity !== processNamespaceIdentity) { return leaseClaimState(claim, { processNamespaceIdentity, leaseMs, leaseNow }); } let identity; try { identity = await getProcessIdentity(claim.publication.pid); } catch { return leaseClaimState(claim, { processNamespaceIdentity, leaseMs, leaseNow }); } if (!validProcessIdentity(identity) || identity.state === "ambiguous") { return leaseClaimState(claim, { processNamespaceIdentity, leaseMs, leaseNow }); } if (identity.state === "dead") return "stale"; return identity.startIdentity === claim.publication.processStartIdentity ? "active" : "stale"; } async function detach({ fileSystem, lockPath, label }) { for (let attempt = 0; attempt < MAX_RECLAIM_ATTEMPTS; attempt += 1) { const quarantinePath = `${lockPath}.quarantine-${randomUUID()}`; if (await lstatIfExists(fileSystem, quarantinePath, label)) continue; try { await fileSystem.rename(lockPath, quarantinePath); return quarantinePath; } catch (error) { if (error?.code === "ENOENT") return null; if (error?.code === "EEXIST" || error?.code === "ENOTEMPTY") continue; throw failure(label, "could not safely detach the"); } } throw failure(label, "could not safely detach the"); } function claimsMatch(before, after) { if (!sameDirectoryFingerprint(before.directoryFingerprint, after.directoryFingerprint)) return false; if (before.kind !== after.kind || before.source !== after.source) return false; if (before.kind === "published") return samePublication(before.publication, after.publication); if (before.source === "missing") return true; return sameOwnerFingerprint(before.fingerprint, after.fingerprint); } function staleClaimStillMatches(before, after, state) { if (!claimsMatch(before, after)) return false; // Adding the arbitration child changes the parent mtime. Once its missing // owner aged past the grace period, unchanged identity is sufficient proof. if (before.kind === "incomplete" && before.source === "missing") return true; return state === "stale"; } async function removeDetached({ fileSystem, claim, quarantinePath, label }) { let entries; try { entries = (await fileSystem.readdir(quarantinePath)).sort(); } catch { throw failure(label, "could not inspect the detached"); } const expectedEntries = claim.source === "missing" ? [] : [OWNER_FILE]; if ( entries.length !== expectedEntries.length || entries.some((entry, index) => entry !== expectedEntries[index]) ) { throw failure(label, "refuses to remove a"); } if (claim.source !== "missing") await fileSystem.unlink(join(quarantinePath, OWNER_FILE)); await fileSystem.rmdir(quarantinePath); } async function restoreDetached({ fileSystem, lockPath, quarantinePath, label }) { try { if (await lstatIfExists(fileSystem, lockPath, label)) return false; await fileSystem.rename(quarantinePath, lockPath); return true; } catch { return false; } } async function createdClaimStillMatches({ expectedDirectoryFingerprint, fileSystem, lockPath, ownerPublication, platform, requireReclaim = false, label, }) { try { const metadata = await lstatIfExists(fileSystem, lockPath, label); assertPrivateDirectory(metadata, { label, platform }); if (!sameDirectoryFingerprint(expectedDirectoryFingerprint, directoryFingerprint(metadata, label))) return false; const entries = (await fileSystem.readdir(lockPath)).sort(); const hasReclaim = entries.includes(RECLAIM_DIRECTORY); if (hasReclaim !== requireReclaim) return false; const remaining = entries.filter((entry) => entry !== RECLAIM_DIRECTORY); if (remaining.length === 0) return true; if (remaining.length !== 1 || remaining[0] !== OWNER_FILE) return false; const ownerPath = join(lockPath, OWNER_FILE); const ownerMetadata = await lstatIfExists(fileSystem, ownerPath, label); if ( !ownerMetadata?.isFile?.() || ownerMetadata.isSymbolicLink() || !Number.isInteger(ownerMetadata.size) || ownerMetadata.size < 0 || ownerMetadata.size > MAX_OWNER_BYTES ) return false; const raw = await fileSystem.readFile(ownerPath, "utf8"); const ownerAfterRead = await lstatIfExists(fileSystem, ownerPath, label); if (!sameOwnerFingerprint(ownerFingerprint(ownerMetadata, raw), ownerFingerprint(ownerAfterRead, raw))) return false; return samePublication(JSON.parse(raw), ownerPublication); } catch { return false; } } async function safelyAbandonCreatedClaim({ expectedDirectoryFingerprint, fileSystem, lockPath, ownerPublication, platform, label, }) { if (!await createdClaimStillMatches({ expectedDirectoryFingerprint, fileSystem, lockPath, ownerPublication, platform, label })) return; const reclaimPath = join(lockPath, RECLAIM_DIRECTORY); try { await fileSystem.mkdir(reclaimPath, { mode: 0o700 }); } catch { return; } if (!await createdClaimStillMatches({ expectedDirectoryFingerprint, fileSystem, lockPath, ownerPublication, platform, requireReclaim: true, label, })) { try { await fileSystem.rmdir(reclaimPath); } catch { /* Preserve changed contents. */ } return; } let quarantinePath; try { quarantinePath = await detach({ fileSystem, lockPath, label }); } catch { return; } if (!quarantinePath) return; try { if (!await createdClaimStillMatches({ expectedDirectoryFingerprint, fileSystem, lockPath: quarantinePath, ownerPublication, platform, requireReclaim: true, label, })) return; await fileSystem.rmdir(join(quarantinePath, RECLAIM_DIRECTORY)); if (await lstatIfExists(fileSystem, join(quarantinePath, OWNER_FILE), label)) { await fileSystem.unlink(join(quarantinePath, OWNER_FILE)); } await fileSystem.rmdir(quarantinePath); } catch { /* Keep unexpected or replaced data safely quarantined. */ } } async function createDirectory({ fileSystem, lockPath, ownerPublication, platform, lockDownPath, label }) { await fileSystem.mkdir(lockPath, { mode: 0o700 }); let expectedDirectoryFingerprint; try { expectedDirectoryFingerprint = directoryFingerprint(await fileSystem.lstat(lockPath), label); await fileSystem.chmod(lockPath, 0o700); await lockDownPath(lockPath, { platform }); const ownerPath = join(lockPath, OWNER_FILE); await fileSystem.writeFile(ownerPath, `${JSON.stringify(ownerPublication)}\n`, { flag: "wx", mode: 0o600 }); await fileSystem.chmod(ownerPath, 0o600); await lockDownPath(ownerPath, { platform, kind: "file" }); const published = await inspectClaim({ fileSystem, lockPath, lockDownPath, platform, label }); if ( published.kind !== "published" || !samePublication(published.publication, ownerPublication) || !sameDirectoryFingerprint(published.directoryFingerprint, expectedDirectoryFingerprint) ) throw failure(label, "detected a changed publication for the"); return published; } catch (error) { if (expectedDirectoryFingerprint) { await safelyAbandonCreatedClaim({ expectedDirectoryFingerprint, fileSystem, lockPath, ownerPublication, platform, label, }); } throw error; } } async function reclaimArbitration({ fileSystem, getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, graceMs, lockPath, now, platform, lockDownPath, label, }) { const initial = await inspectClaim({ fileSystem, lockPath, lockDownPath, platform, label }); const initialState = await claimState(initial, { getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, now, graceMs, label }); if (initialState !== "stale") return initialState; const quarantinePath = await detach({ fileSystem, lockPath, label }); if (!quarantinePath) return "changed"; try { const detached = await inspectClaim({ fileSystem, lockPath: quarantinePath, lockDownPath, platform, label }); const state = await claimState(detached, { getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, now, graceMs, label }); if (!claimsMatch(initial, detached) || state !== "stale") { await restoreDetached({ fileSystem, lockPath, quarantinePath, label }); return "changed"; } await removeDetached({ fileSystem, claim: detached, quarantinePath, label }); return "reclaimed"; } catch (error) { await restoreDetached({ fileSystem, lockPath, quarantinePath, label }); if (error?.code === "RELMIO_LOCK_CHANGED") throw error; throw failure(label, "could not safely reclaim the arbitration"); } } async function acquireArbitration({ fileSystem, getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, graceMs, lockPath, now, platform, lockDownPath, selfIdentity, label, }) { const reclaimPath = join(lockPath, RECLAIM_DIRECTORY); const ownerPublication = Object.freeze({ schemaVersion: SCHEMA_VERSION, pid: process.pid, processStartIdentity: selfIdentity.startIdentity, processNamespaceIdentity, acquiredAt: leaseTimestamp(leaseNow, label), token: randomUUID(), publishedAtMs: timestamp(now, label), }); for (let attempt = 0; attempt < MAX_RECLAIM_ATTEMPTS; attempt += 1) { try { await createDirectory({ fileSystem, lockPath: reclaimPath, ownerPublication, platform, lockDownPath, label }); return Object.freeze({ state: "owned", ownerPublication, reclaimPath }); } catch (error) { if (error?.code === "RELMIO_LOCK_CHANGED") throw error; if (error?.code !== "EEXIST") throw failure(label, "could not publish an arbitration claim for the"); } const state = await reclaimArbitration({ fileSystem, getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, graceMs, lockPath: reclaimPath, now, platform, lockDownPath, label, }); if (state === "reclaimed" || state === "changed") continue; return Object.freeze({ state }); } return Object.freeze({ state: "active" }); } async function release({ fileSystem, lockPath, ownerPublication, platform, lockDownPath, label }) { const initial = await inspectClaim({ fileSystem, lockPath, lockDownPath, platform, label }); if (initial.kind !== "published" || !samePublication(initial.publication, ownerPublication)) { throw failure(label, "refuses to release a replaced"); } if (await lstatIfExists(fileSystem, join(lockPath, RECLAIM_DIRECTORY), label)) { throw failure(label, "refuses to release a contended"); } const quarantinePath = await detach({ fileSystem, lockPath, label }); if (!quarantinePath) throw failure(label, "refuses to release a replaced"); try { const detached = await inspectClaim({ fileSystem, lockPath: quarantinePath, lockDownPath, platform, label }); if ( detached.kind !== "published" || !samePublication(detached.publication, ownerPublication) || !claimsMatch(initial, detached) || await lstatIfExists(fileSystem, join(quarantinePath, RECLAIM_DIRECTORY), label) ) throw failure(label, "refuses to release a replaced"); await removeDetached({ fileSystem, claim: detached, quarantinePath, label }); } catch (error) { await restoreDetached({ fileSystem, lockPath, quarantinePath, label }); if (error?.message?.startsWith("Relmio refuses")) throw error; throw failure(label, "could not release the safely"); } } async function reclaimStale({ fileSystem, getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, graceMs, lockPath, now, platform, lockDownPath, selfIdentity, label, }) { const initial = await inspectClaim({ fileSystem, lockPath, lockDownPath, platform, label }); const initialState = await claimState(initial, { getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, now, graceMs, label }); if (initialState !== "stale") return initialState; const arbitration = await acquireArbitration({ fileSystem, getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, graceMs, lockPath, now, platform, lockDownPath, selfIdentity, label, }); if (arbitration.state !== "owned") return arbitration.state; let quarantinePath = null; try { const current = await inspectClaim({ fileSystem, lockPath, lockDownPath, platform, label }); const currentState = await claimState(current, { getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, now, graceMs, label }); if (!staleClaimStillMatches(initial, current, currentState)) { throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); } const arbitrationClaim = await inspectClaim({ fileSystem, lockPath: arbitration.reclaimPath, lockDownPath, platform, label, }); if ( arbitrationClaim.kind !== "published" || !samePublication(arbitrationClaim.publication, arbitration.ownerPublication) ) throw failure(label, "refuses to reclaim without its exact arbitration claim for the"); quarantinePath = await detach({ fileSystem, lockPath, label }); if (!quarantinePath) return "changed"; const detached = await inspectClaim({ fileSystem, lockPath: quarantinePath, lockDownPath, platform, label }); const state = await claimState(detached, { getProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, now, graceMs, label }); if (!staleClaimStillMatches(initial, detached, state)) { throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); } await release({ fileSystem, lockPath: join(quarantinePath, RECLAIM_DIRECTORY), ownerPublication: arbitration.ownerPublication, platform, lockDownPath, label, }); await removeDetached({ fileSystem, claim: detached, quarantinePath, label }); return "reclaimed"; } catch (error) { if (quarantinePath) { await restoreDetached({ fileSystem, lockPath, quarantinePath, label }); } else { try { await release({ fileSystem, lockPath: arbitration.reclaimPath, ownerPublication: arbitration.ownerPublication, platform, lockDownPath, label, }); } catch { /* A changed arbitration claim must remain preserved. */ } } if (error?.message?.startsWith("Relmio refuses")) throw error; if (error?.code === "RELMIO_LOCK_CHANGED") throw error; throw failure(label, "could not safely reclaim the"); } } // `identityOnly` is only for deciding whether to unlink this process's own name for // an inode that must still equal a claim verified moments earlier; deleting it needs no ACL trust. async function inspectAtomicClaim({ fileSystem, lockPath, lockDownPath, platform, label, identityOnly = false }) { const metadata = await lstatIfExists(fileSystem, lockPath, label); if (!metadata) return null; assertPrivateOwner(metadata, { label, platform }); if (![1, 2].includes(metadata.nlink)) throw failure(label, "refuses an unsafe owner for the"); if (platform !== "win32" && metadata.uid !== process.getuid()) { throw failure(label, "refuses an unsafe owner for the"); } let raw; try { raw = await fileSystem.readFile(lockPath, "utf8"); } catch (error) { if (error?.code === "ENOENT") throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); throw failure(label, "could not inspect the"); } const after = await lstatIfExists(fileSystem, lockPath, label); if (!after) throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); assertPrivateOwner(after, { label, platform }); const fingerprint = ownerFingerprint(metadata, raw); if (!sameOwnerFingerprint(fingerprint, ownerFingerprint(after, raw))) { throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); } // Only an unchanged inode this process locked down itself skips the Windows ACL // check; foreign or changed inodes are always verified. if (platform === "win32" && !identityOnly && !recallWindowsLockdown(lockDownPath, metadata, raw)) { for (let attempt = 0; ; attempt += 1) { let verifyError = null; try { await lockDownPath(lockPath, { platform, kind: "file", verifyOnly: true, verifyEffectiveOwnerOnly: true }); } catch (error) { verifyError = error; } // A claim renamed, removed or rewritten during the check is a changed claim to inspect again. const verified = await lstatIfExists(fileSystem, lockPath, label); if (!verified) throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); assertPrivateOwner(verified, { label, platform }); if (!sameOwnerFingerprint(fingerprint, ownerFingerprint(verified, raw))) { throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); } if (!verifyError) break; // Another process can hold an unchanged claim exclusively for a moment; it fails closed after that. if (attempt >= WINDOWS_ACL_RETRY_DELAYS_MS.length) throw verifyError; await new Promise(resolve => setTimeout(resolve, WINDOWS_ACL_RETRY_DELAYS_MS[attempt])); } refreshWindowsLockdown(lockDownPath, metadata, raw); } let publication; try { publication = JSON.parse(raw); } catch { /* Preserve malformed claims. */ } return { kind: validPublication(publication) ? "published" : "ambiguous", publication, fingerprint }; } function sameAtomicClaim(left, right) { return left?.kind === "published" && right?.kind === "published" && left.fingerprint.dev === right.fingerprint.dev && left.fingerprint.ino === right.fingerprint.ino && left.fingerprint.raw === right.fingerprint.raw && samePublication(left.publication, right.publication); } // Windows cannot fsync a directory handle (Node reports EPERM), so publication // there relies on NTFS metadata journaling; exclusive link publication still applies. async function syncAtomicParent({ fileSystem, lockPath, platform, label }) { if (platform === "win32") return; let handle; try { handle = await fileSystem.open(dirname(lockPath), "r"); await handle.sync(); } catch { throw failure(label, "could not durably publish the", "RELMIO_LOCK_UNAVAILABLE"); } finally { if (handle) await handle.close(); } } async function publishAtomicClaim({ fileSystem, lockPath, ownerPublication, lockDownPath, platform, label }) { const temporaryPath = `${lockPath}.publication-${randomUUID()}`; const raw = `${JSON.stringify(ownerPublication)}\n`; let handle; let metadata; let linked = false; try { handle = await fileSystem.open(temporaryPath, "wx", 0o600); await handle.writeFile(raw, "utf8"); await handle.chmod(0o600); await lockDownPath(temporaryPath, { platform, kind: "file" }); await handle.sync(); metadata = await handle.stat(); assertPrivateOwner(metadata, { label, platform }); if (platform === "win32") rememberWindowsLockdown(lockDownPath, metadata, raw); await handle.close(); handle = null; // link is exclusive for files, directories and symlinks, unlike POSIX rename. await fileSystem.link(temporaryPath, lockPath); linked = true; const temporary = await inspectAtomicClaim({ fileSystem, lockPath: temporaryPath, lockDownPath, platform, label, identityOnly: true }); if (temporary?.fingerprint.dev !== metadata.dev || temporary?.fingerprint.ino !== metadata.ino || temporary?.fingerprint.raw !== raw) { throw failure(label, "detected a changed publication for the", "RELMIO_LOCK_CHANGED"); } await fileSystem.unlink(temporaryPath); await syncAtomicParent({ fileSystem, lockPath, platform, label }); const published = await inspectAtomicClaim({ fileSystem, lockPath, lockDownPath, platform, label }); if (published?.fingerprint.dev !== metadata.dev || published?.fingerprint.ino !== metadata.ino || !samePublication(published?.publication, ownerPublication)) { throw failure(label, "detected a changed publication for the", "RELMIO_LOCK_CHANGED"); } return published; } catch (error) { if (error?.code === "EEXIST") throw error; if (error?.code === "RELMIO_LOCK_CHANGED") throw error; if (linked && metadata) { const published = await inspectAtomicClaim({ fileSystem, lockPath, lockDownPath, platform, label }).catch(() => null); if (published?.fingerprint.dev === metadata.dev && published?.fingerprint.ino === metadata.ino && samePublication(published?.publication, ownerPublication)) { await removeAtomicClaim({ fileSystem, lockPath, expected: published, lockDownPath, platform, label }).catch(() => {}); } } // Unpublished crash leftovers are ignored; no other invocation deletes them. throw failure(label, "could not atomically publish the", "RELMIO_LOCK_UNAVAILABLE"); } finally { if (handle) await handle.close(); } } async function removeAtomicClaim({ fileSystem, lockPath, expected, lockDownPath, platform, label }) { const current = await inspectAtomicClaim({ fileSystem, lockPath, lockDownPath, platform, label }); if (!sameAtomicClaim(current, expected)) throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); const detachedPath = `${lockPath}.released-${randomUUID()}`; await fileSystem.rename(lockPath, detachedPath); const detached = await inspectAtomicClaim({ fileSystem, lockPath: detachedPath, lockDownPath, platform, label, identityOnly: true }); if (!sameAtomicClaim(detached, expected)) { try { await fileSystem.link(detachedPath, lockPath); await fileSystem.unlink(detachedPath); await syncAtomicParent({ fileSystem, lockPath, platform, label }); } catch { /* Never replace a successor record when restoring a changed claim. */ } throw failure(label, "detected a changed", "RELMIO_LOCK_CHANGED"); } await fileSystem.unlink(detachedPath); await syncAtomicParent({ fileSystem, lockPath, platform, label }); } function ownFailedAtomicRelease(context, claim, path = context.lockPath) { const failed = failedAtomicReleases.get(path); if (!failed) return null; if (!claim) { failed.onRecovered?.(); failedAtomicReleases.delete(path); return null; } if (!sameAtomicClaim(failed.claim, claim)) { failedAtomicReleases.delete(path); return null; } return claim.publication.pid === process.pid && claim.publication.processStartIdentity === context.ownerPublication.processStartIdentity && claim.publication.processNamespaceIdentity === context.processNamespaceIdentity ? failed : null; } function completeFailedAtomicRelease(path, failed) { if (!failed || failedAtomicReleases.get(path) !== failed) return; failed.onRecovered?.(); failedAtomicReleases.delete(path); } async function releaseAtomicGuard(context, guard) { try { await removeAtomicClaim({ ...context, lockPath: guard.guardPath, expected: guard.claim }); } catch (error) { failedAtomicReleases.set(guard.guardPath, { claim: guard.claim }); throw error; } } async function acquireAtomicGuard(context) { const { fileSystem, lockPath, ownerPublication, lockDownPath, platform, label, leaseNow, now } = context; const guardPath = `${lockPath}.publication-lock`; for (let attempt = 0; attempt < MAX_RECLAIM_ATTEMPTS; attempt += 1) { const existing = await inspectAtomicClaim({ fileSystem, lockPath: guardPath, lockDownPath, platform, label }); if (existing) { const failed = ownFailedAtomicRelease(context, existing, guardPath); const state = failed ? "stale" : await claimState(existing, context); if (state === "active") throw failure(label, "found contention for the", "RELMIO_LOCK_BUSY"); if (state !== "stale") throw failure(label, "could not verify the publication guard for the"); await removeAtomicClaim({ fileSystem, lockPath: guardPath, expected: existing, lockDownPath, platform, label }); completeFailedAtomicRelease(guardPath, failed); } try { const publication = { ...ownerPublication, token: randomUUID(), acquiredAt: leaseTimestamp(leaseNow, label), publishedAtMs: timestamp(now, label) }; const claim = await publishAtomicClaim({ fileSystem, lockPath: guardPath, ownerPublication: publication, lockDownPath, platform, label }); return { claim, guardPath }; } catch (error) { if (error?.code !== "EEXIST") throw error; } } throw failure(label, "found contention for the", "RELMIO_LOCK_BUSY"); } async function assertAtomicGuard(context, guard) { const { fileSystem, lockDownPath, platform, label, leaseMs, leaseNow } = context; const current = await inspectAtomicClaim({ fileSystem, lockPath: guard.guardPath, lockDownPath, platform, label }); if (!sameAtomicClaim(current, guard.claim)) throw failure(label, "detected a changed publication guard for the", "RELMIO_LOCK_CHANGED"); if (leaseMs !== null) { const age = leaseTimestamp(leaseNow, label) - guard.claim.publication.acquiredAt; if (age < 0 || age > leaseMs) throw failure(label, "could not use an expired publication guard for the", "RELMIO_LOCK_UNAVAILABLE"); } } async function acquireAtomicLifecycleLock(context) { const { fileSystem, lockPath, ownerPublication, lockDownPath, platform, label, leaseMs, leaseNow, releaseWaitMs } = context; const existing = await inspectAtomicClaim(context); const failed = ownFailedAtomicRelease(context, existing); if (existing) { const state = failed ? "stale" : await claimState(existing, context); if (state === "active") throw failure(label, "found contention for the", "RELMIO_LOCK_BUSY"); if (state !== "stale") throw failure(label, "could not verify the existing"); } const guard = await acquireAtomicGuard(context); let published; try { const current = await inspectAtomicClaim(context); if (current) { const failedCurrent = ownFailedAtomicRelease(context, current); const state = failedCurrent ? "stale" : await claimState(current, context); if (state === "active") throw failure(label, "found contention for the", "RELMIO_LOCK_BUSY"); if (state !== "stale") throw failure(label, "could not verify the existing"); await assertAtomicGuard(context, guard); await removeAtomicClaim({ fileSystem, lockPath, expected: current, lockDownPath, platform, label }); completeFailedAtomicRelease(lockPath, failedCurrent); } await assertAtomicGuard(context, guard); if (leaseMs !== null && leaseTimestamp(leaseNow, label) - ownerPublication.acquiredAt > leaseMs) { throw failure(label, "could not publish an expired lease for the", "RELMIO_LOCK_UNAVAILABLE"); } try { published = await publishAtomicClaim(context); } catch (error) { if (error?.code === "EEXIST") throw failure(label, "found contention for the", "RELMIO_LOCK_BUSY"); throw error; } } finally { try { await releaseAtomicGuard(context, guard); } catch (error) { if (published) failedAtomicReleases.set(lockPath, { claim: published }); throw error; } } let released = false; const releaseClaim = async () => { if (released) return; const deadline = defaultLeaseNow() + releaseWaitMs; let delay = 5; for (;;) { try { if (released) return; const current = await inspectAtomicClaim(context); if (!sameAtomicClaim(current, published)) throw failure(label, "refuses to release a replaced"); const releaseGuard = await acquireAtomicGuard(context); try { if (!released) { await assertAtomicGuard(context, releaseGuard); const afterWait = await inspectAtomicClaim(context); if (!sameAtomicClaim(afterWait, published)) throw failure(label, "refuses to release a replaced"); await removeAtomicClaim({ fileSystem, lockPath, expected: published, lockDownPath, platform, label }); released = true; completeFailedAtomicRelease(lockPath, ownFailedAtomicRelease(context, published)); } } finally { await releaseAtomicGuard(context, releaseGuard); } return; } catch (error) { const remaining = deadline - defaultLeaseNow(); if (["RELMIO_LOCK_BUSY", "RELMIO_LOCK_CHANGED"].includes(error?.code) && remaining > 0) { await new Promise(resolve => setTimeout(resolve, Math.min(delay, remaining))); delay = Math.min(delay * 2, 50); continue; } if (!released) failedAtomicReleases.set(lockPath, { claim: published, onRecovered: () => { released = true; }, }); throw error; } } }; Object.defineProperty(releaseClaim, "acquiredAt", { value: ownerPublication.acquiredAt }); return releaseClaim; } /** * Acquire a crash-recoverable, filesystem-only lifecycle lock for one local * integration. Call the returned release function exactly once and surface a * release failure to the caller; hiding it can otherwise leave a safe lock * that looks like a broken installer on the next launch. */ export async function acquireLocalIntegrationLifecycleLock({ fileSystem, getProcessIdentity = getLocalProcessIdentity, getPidNamespaceIdentity = getLocalPidNamespaceIdentity, lockDownPath = lockDownLocalPath, lockPath, now = Date.now, platform = process.platform, label = "local integration operation lock", reclaimIncomplete = true, leaseMs = null, leaseNow = defaultLeaseNow, atomicPublication = false, releaseWaitMs = leaseMs ?? PUBLICATION_GRACE_MS, }) { if ( !fileSystem || typeof fileSystem.mkdir !== "function" || typeof fileSystem.lstat !== "function" || typeof fileSystem.readFile !== "function" || typeof fileSystem.writeFile !== "function" || typeof fileSystem.rename !== "function" || typeof fileSystem.readdir !== "function" || typeof fileSystem.rmdir !== "function" || typeof fileSystem.unlink !== "function" || typeof fileSystem.chmod !== "function" || typeof getProcessIdentity !== "function" || typeof getPidNamespaceIdentity !== "function" || typeof lockDownPath !== "function" || typeof now !== "function" || typeof lockPath !== "string" || lockPath.length === 0 || typeof platform !== "string" || platform.length === 0 || typeof label !== "string" || label.length === 0 || typeof reclaimIncomplete !== "boolean" || typeof leaseNow !== "function" || (leaseMs !== null && (!Number.isSafeInteger(leaseMs) || leaseMs <= 0)) || typeof atomicPublication !== "boolean" || !Number.isSafeInteger(releaseWaitMs) || releaseWaitMs <= 0 || (atomicPublication && (typeof fileSystem.open !== "function" || typeof fileSystem.link !== "function")) ) { throw new TypeError("The local integration lifecycle lock adapter is invalid."); } const inspectProcessIdentity = (pid) => getProcessIdentity(pid, { platform }); const selfIdentity = await currentProcessIdentity(getProcessIdentity, platform); if (!selfIdentity) { throw failure(label, "could not verify this process identity for the", "RELMIO_LOCK_IDENTITY_UNAVAILABLE"); } let processNamespaceIdentity; try { processNamespaceIdentity = await getPidNamespaceIdentity({ platform }); } catch { processNamespaceIdentity = null; } if (!validNamespaceIdentity(processNamespaceIdentity)) { throw failure(label, "could not verify this process namespace for the"); } const ownerPublication = Object.freeze({ schemaVersion: SCHEMA_VERSION, pid: process.pid, processStartIdentity: selfIdentity.startIdentity, processNamespaceIdentity, acquiredAt: leaseTimestamp(leaseNow, label), token: randomUUID(), publishedAtMs: timestamp(now, label), }); if (atomicPublication) { return acquireAtomicLifecycleLock({ fileSystem: withWindowsSharingRetry(fileSystem, platform), lockPath, ownerPublication, lockDownPath, platform, label, now, leaseMs, leaseNow, releaseWaitMs, getProcessIdentity: inspectProcessIdentity, processNamespaceIdentity, }); } for (let attempt = 0; attempt < MAX_RECLAIM_ATTEMPTS; attempt += 1) { try { await createDirectory({ fileSystem, lockPath, ownerPublication, platform, lockDownPath, label }); return async () => release({ fileSystem, lockPath, ownerPublication, platform, lockDownPath, label }); } catch (error) { if (error?.code === "RELMIO_LOCK_CHANGED") throw error; if (error?.code !== "EEXIST") throw failure(label, "could not acquire the"); } let state; try { state = await reclaimStale({ fileSystem, getProcessIdentity: inspectProcessIdentity, processNamespaceIdentity, leaseMs, leaseNow, graceMs: reclaimIncomplete ? PUBLICATION_GRACE_MS : Infinity, lockPath, now, platform, lockDownPath, selfIdentity, label, }); } catch (error) { if (error?.message?.startsWith("Relmio refuses")) throw error; if (error?.code === "RELMIO_LOCK_CHANGED") throw error; throw failure(label, "could not safely inspect the"); } if (state === "reclaimed" || state === "changed") continue; if (state === "active" || state === "starting") { throw Object.assign(new Error(`Another Relmio process is changing ${label}.`), { code: "RELMIO_LOCK_BUSY" }); } throw failure(label, "could not verify the existing"); } throw Object.assign(new Error(`Another Relmio process is changing ${label}.`), { code: "RELMIO_LOCK_BUSY" }); } export async function assertNoLocalSiwcOneOffContainers({ runProcess, installRoot, dockerHost, projectName }) { validateLocalDockerHost(dockerHost); if (!/^relmio-(?:codex-chat|codex-chatgpt|n8n-openai-oauth)-[a-f0-9]{32}$/u.test(projectName)) { throw new TypeError("The frozen SIWC project identity is invalid."); } try { const result = await runProcess({ file: "docker", args: ["ps", "--all", "--filter", `label=com.docker.compose.project=${projectName}`, "--filter", "label=com.docker.compose.oneoff=True", "--format", "{{json .}}"], cwd: installRoot, dockerHost }); if (result.code !== 0 || typeof result.stdout !== "string" || result.stdout.trim() !== "") { throw new Error("An installed SIWC operation may still be running."); } } catch { throw Object.assign(new Error("The SIWC handoff may still be running. The sender remains frozen."), { remoteOutcomeUnknown: true }); } } export async function readLocalSiwcResumeAuthBinding({ checkpoint, registration, readReceipt, readRegistration, readPendingAuthHandoff, }) { if (!registration) throw new Error("Select the SIWC account before reviewing resume."); const record = await readRegistration(registration); if (registration.registrationId === checkpoint.registrationId) { if (record?.handoff?.state === "transferred" || record?.handoff?.state === "handoff-pending") { return checkpoint.plan.authBinding; } const binding = checkpoint.plan.authBinding; if (record?.handoff?.state !== "owned" || record.registrationId !== binding.registrationId || record.clientId !== binding.clientId || record.owner.hostId !== binding.ownerHostId || record.owner.runtimeId !== binding.ownerRuntimeId) { throw new Error("The staged SIWC account identity changed."); } return { registrationId: record.registrationId, clientId: record.clientId, generation: record.generation, ownerHostId: record.owner.hostId, ownerRuntimeId: record.owner.runtimeId }; } if (checkpoint.notAccepted !== true) throw new Error("Reconcile the original handoff before selecting a fresh sign-in."); const pending = await readPendingAuthHandoff({ storageRoot: registration.storageRoot, registrationId: checkpoint.registrationId, }); if (!pending || pending.target.runtimeId !== checkpoint.installId || pending.target.hostId !== checkpoint.ownerHostId || record?.handoff?.state !== "owned" || record.owner.hostId !== checkpoint.plan.authBinding.ownerHostId || !record.planEnabled || !record.session?.refreshToken || !record.session.scopes?.includes("chatgpt.tokens.use.direct")) { throw new Error("The fresh SIWC registration or original handoff changed."); } let result; try { result = await readReceipt(pending); } catch { throw Object.assign(new Error("The original destination receipt remains unresolved."), { remoteOutcomeUnknown: true }); } if (result?.receipt !== null) throw new Error("The original handoff was accepted; reconcile it before resuming."); return { registrationId: record.registrationId, clientId: record.clientId, generation: record.generation, ownerHostId: record.owner.hostId, ownerRuntimeId: record.owner.runtimeId }; } export async function fingerprintLocalSiwcResources({ runProcess, installRoot, checkpoint }) { const { projectName, dockerHost, target, installId } = checkpoint; if (!/^[a-f0-9]{32}$/u.test(installId) || projectName !== `relmio-${target}-${installId}`) { throw new Error("The staged SIWC project identity is invalid."); } const resources = []; for (const resource of ["container", "volume", ...(target === "n8n-openai-oauth" ? [] : ["network"])]) { const result = await runProcess({ file: "docker", args: [resource === "container" ? "ps" : resource, ...(resource === "container" ? ["--all"] : ["ls"]), "--filter", `label=com.docker.compose.project=${projectName}`, "--format", "{{json .}}"], cwd: dirname(resolve(installRoot, "..", "..")), dockerHost, }); if (result.code !== 0 || typeof result.stdout !== "string" || result.stdout.length > 64 * 1024) { throw new Error("The staged SIWC resources could not be inspected."); } const rows = result.stdout.trim() ? result.stdout.trim().split("\n").map(row => JSON.parse(row)) : []; if (rows.length > 10) throw new Error("The staged SIWC resources are ambiguous."); for (const row of rows) { const identity = resource === "container" ? row.ID : row.Name; if (typeof identity !== "string" || (resource === "container" ? !/^[a-f0-9]{12,64}$/u.test(identity) : !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,255}$/u.test(identity))) { throw new Error("The staged SIWC resource identity is invalid."); } const inspected = await runProcess({ file: "docker", args: [resource, "inspect", "--format", "{{json .}}", identity], cwd: dirname(resolve(installRoot, "..", "..")), dockerHost }); if (inspected.code !== 0) throw new Error("The staged SIWC resource changed."); const value = JSON.parse(inspected.stdout); if (resource === "container" ? !/^[a-f0-9]{64}$/u.test(value.Id) || !/^sha256:[a-f0-9]{64}$/u.test(value.Image) || !Array.isArray(value.Mounts) : resource === "network" ? value.Name !== identity || !/^[a-f0-9]{64}$/u.test(value.Id) : value.Name !== identity || typeof value.CreatedAt !== "string" || !Number.isFinite(Date.parse(value.CreatedAt)) || typeof value.Mountpoint !== "string") { throw new Error("The staged SIWC resource identity could not be attested."); } resources.push(resource === "container" ? [resource, value.Id, value.Image, value.Mounts, value.State?.Running] : [resource, value.Name, value.Id, value.CreatedAt, value.Driver, value.Mountpoint, value.Labels]); } } return createHash("sha256").update(JSON.stringify(resources.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))))).digest("hex"); } export function localSiwcStagingPath(installRoot) { const target = basename(installRoot); if (!["codex-chat", "codex-chatgpt", "n8n-openai-oauth"].includes(target) || basename(dirname(installRoot)) !== "local" || basename(resolve(installRoot, "..", "..")) !== ".relmio") { throw new TypeError("The SIWC staging target is invalid."); } return join(dirname(resolve(installRoot, "..", "..")), `.relmio-local-${target}.siwc-staging.json`); } export async function readLocalSiwcStaging({ fileSystem, installRoot, platform = process.platform, lockDownPath = lockDownLocalPath }) { const path = localSiwcStagingPath(installRoot); let metadata; try { metadata = await fileSystem.lstat(path); } catch (error) { if (error.code === "ENOENT") return null; throw error; } if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size > 32 * 1024 || (platform !== "win32" && (metadata.uid !== process.getuid() || (metadata.mode & 0o077) !== 0))) { throw new Error("The SIWC staging checkpoint is unsafe."); } if (platform === "win32") { await lockDownPath(path, { platform, kind: "file", verifyOnly: true, verifyEffectiveOwnerOnly: true }); } const raw = await fileSystem.readFile(path, "utf8"); const checkpoint = JSON.parse(raw); if (checkpoint.schemaVersion !== 1 || checkpoint.target !== basename(installRoot) || !/^[a-f0-9]{32}$/u.test(checkpoint.installId) || checkpoint.registrationId !== checkpoint.plan?.authBinding?.registrationId || !["staged", "retiring", "prepared", "handoff-pending", "transferred", "completed"].includes(checkpoint.stage)) { throw new Error("The SIWC staging checkpoint is invalid."); } return { checkpoint, checkpointSha256: createHash("sha256").update(raw).digest("hex") }; } export async function fingerprintLocalSiwcFiles({ fileSystem, installRoot, platform = process.platform, lockDownPath = lockDownLocalPath }) { const hash = createHash("sha256"); let count = 0; const visit = async (path, relative = "") => { let metadata; try { metadata = await fileSystem.lstat(path); } catch (error) { if (error.code === "ENOENT" && relative === "") return; throw error; } if (++count > 200 || metadata.isSymbolicLink() || (!metadata.isDirectory() && !metadata.isFile()) || (metadata.isFile() && (metadata.nlink !== 1 || metadata.size > 2 * 1024 * 1024)) || (platform !== "win32" && (metadata.uid !== process.getuid() || (metadata.mode & 0o077) !== 0))) { throw new Error("The staged SIWC files are unsafe."); } if (platform === "win32") { await lockDownPath(path, { platform, kind: metadata.isFile() ? "file" : "directory", verifyOnly: true, verifyEffectiveOwnerOnly: true }); } hash.update(JSON.stringify([relative, metadata.dev, metadata.ino, metadata.mode, metadata.uid])); if (metadata.isFile()) hash.update(await fileSystem.readFile(path)); else for (const name of (await fileSystem.readdir(path)).sort()) { await visit(join(path, name), `${relative}/${name}`); } }; for (const parent of [resolve(installRoot, "..", ".."), dirname(installRoot)]) { let metadata; try { metadata = await fileSystem.lstat(parent); } catch (error) { if (error.code === "ENOENT") continue; throw error; } if (!metadata.isDirectory() || metadata.isSymbolicLink() || (platform !== "win32" && (metadata.uid !== process.getuid() || (metadata.mode & 0o077) !== 0))) { throw new Error("The staged SIWC parent directory is unsafe."); } } await visit(installRoot); return hash.digest("hex"); } export function localSiwcFinalizationFailure(result, recovery = "review-again") { return { ...result, deploymentMode: "partial", readiness: "unverified", finalizationFailure: { error: "Ownership transferred, but installation finalization failed. Save the one-time key and review recovery before use.", recovery, }, }; } export async function settleLocalIntegrationLifecycleOperation({ completionLabel, operation, releaseLock, }) { let result; let operationError; try { result = await operation(); } catch (error) { operationError = error; } let releaseError; try { await releaseLock(); } catch (error) { releaseError = error; } if (operationError) { if (releaseError && operationError.cause === undefined) { try { Object.defineProperty(operationError, "cause", { configurable: true, value: releaseError }); } catch { /* Preserve the action error. */ } } throw operationError; } if (releaseError) { if (result?.credentialShownOnce === true && typeof result.clientCredential === "string" && result.account?.ownership === "owned") { return localSiwcFinalizationFailure(result); } throw Object.assign(new Error( `${completionLabel} completed, but Relmio could not release its operation lock. Restart Relmio before another local integration action.`, { cause: releaseError }, ), { code: "LOCAL_INTEGRATION_LIFECYCLE_LOCK_RELEASE" }); } return result; }