feat: harden self-managed reconciliation
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
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<TrustIdentity> {
|
||||
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<string, unknown>).project === "string" &&
|
||||
typeof (value as Record<string, unknown>).fingerprint === "string" &&
|
||||
/^[a-f0-9]{64}$/.test(
|
||||
(value as Record<string, unknown>).fingerprint as string,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function readTrust(): Promise<LocalTrustRecord[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await writeTrust(update(await readTrust()));
|
||||
}
|
||||
|
||||
export function trustHeaders(identity: TrustIdentity): Record<string, string> {
|
||||
return {
|
||||
"x-kuber-trust-project": identity.project,
|
||||
"x-kuber-trust-fingerprint": identity.fingerprint,
|
||||
};
|
||||
}
|
||||
|
||||
export async function requireLocalTrust(
|
||||
identity: TrustIdentity,
|
||||
): Promise<LocalTrustRecord> {
|
||||
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.",
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user