import { createHash, randomUUID } from "node:crypto"; import { chmod, mkdir, readFile, realpath, rename, writeFile, } from "node:fs/promises"; import { join } from "node:path"; export const TRUST_SCHEMA_VERSION = 1; export type TrustIdentity = { project: string; fingerprint: string }; export type LocalTrustRecord = TrustIdentity; type TrustFile = { version: number; records: LocalTrustRecord[] }; export function getTrustPath(): string { return join( process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? "/tmp", ".config"), "kuber", "trust.json", ); } export async function resolveTrustIdentity( project: string, cwd = process.cwd(), ): Promise { const path = await realpath(cwd); return { project, fingerprint: createHash("sha256").update(path).digest("hex"), }; } function validRecord(value: unknown): value is LocalTrustRecord { return ( !!value && typeof value === "object" && typeof (value as Record).project === "string" && typeof (value as Record).fingerprint === "string" && /^[a-f0-9]{64}$/.test( (value as Record).fingerprint as string, ) ); } export async function readTrust(): Promise { try { const value: unknown = JSON.parse(await readFile(getTrustPath(), "utf8")); if ( !value || typeof value !== "object" || (value as TrustFile).version !== TRUST_SCHEMA_VERSION || !Array.isArray((value as TrustFile).records) ) return []; return (value as TrustFile).records.filter(validRecord); } catch { return []; } } export async function writeTrust(records: LocalTrustRecord[]): Promise { const path = getTrustPath(); const directory = path.slice(0, path.lastIndexOf("/")); await mkdir(directory, { recursive: true, mode: 0o700 }); await chmod(directory, 0o700); const temporary = `${path}.${randomUUID()}.tmp`; await writeFile( temporary, JSON.stringify({ version: TRUST_SCHEMA_VERSION, records }, null, 2) + "\n", { flag: "wx", mode: 0o600 }, ); await rename(temporary, path); await chmod(path, 0o600); } export async function updateTrust( update: (records: LocalTrustRecord[]) => LocalTrustRecord[], ): Promise { await writeTrust(update(await readTrust())); } export function trustHeaders(identity: TrustIdentity): Record { return { "x-kuber-trust-project": identity.project, "x-kuber-trust-fingerprint": identity.fingerprint, }; } export async function requireLocalTrust( identity: TrustIdentity, ): Promise { const record = (await readTrust()).find( (candidate) => candidate.project === identity.project && candidate.fingerprint === identity.fingerprint, ); if (record) return record; throw new Error( "TRUST_REQUIRED: this directory is not trusted for this namespace. Run kuber trust before kuber up.", ); }