src/services/local-integration-lifecycle-lock.js
Rama main · réplica de lectura del repositorio original.
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;
}