106 lines
2.9 KiB
TypeScript
106 lines
2.9 KiB
TypeScript
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.",
|
|
);
|
|
}
|