/*
RFC-0966: Signed site passport — payload builder, signer, verifier.
Provides SitePassportV1 and SignedSitePassport types, buildPassportPayload,
signPassport, and verifyPassport functions. Uses canonical-JSON hashing
(RFC-0849) and Ed25519 signing (RFC-0931/RFC-0921 helpers).
- Does not perform file IO for writing passport.json — use readPassport/writePassport in registry-io.ts.
- Does not manage key lifecycle or rotation — see RFC-0966 Key rotation section.
- Does not enforce single-creator invariant — that is sternsystem.validate PASSPORT-02.
- RFC-0966: initial passport types, builder, signer, verifier.
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
*/
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import path from "node:path";
import {
snapshotCanonicalJsonObjectV1,
canonicalJsonHashV1,
} from "@warpgogol/werkstatt-engine/fingerprint";
import {
signBytes,
verifyBytes,
toHex,
fromHex,
getPublicKey,
} from "@warpgogol/werkstatt-engine/signing";
import { readBordbuch } from "../bordbuch/bordbuch-io.ts";
import { resolveCacheClonePath, readSystemConfig } from "./registry-io.ts";
import { z } from "zod";
import type { SystemConfig } from "@warpgogol/werkstatt-engine/schemas";
export interface SitePassportV1 {
schema: "site-passport/v1";
systemId: string;
creator: {
identity: string;
publicKey: string;
};
platform: {
version: string;
semanticHash: string;
};
provenance: {
createdAt: string;
generatedAt: string;
bordbuchHead: string | null;
missionCounter: number;
};
resources: {
channels: Record<"dev" | "alt" | "main", { workerName: string; url: string }>;
customDomains: string[];
vectorizeIndexes: string[];
r2Prefixes: string[];
secretNames: string[];
};
mirrors: Array<{ role: "cache" | "bare" | "external"; locator: string }>;
}
export interface SignedSitePassport {
payload: SitePassportV1;
passportHash: string;
signature: string;
}
const partialPinSchema = z.object({
platform: z.object({
version: z.string().min(1),
platformSemanticHash: z.string().min(1),
}),
});
const CREDENTIAL_RE = /^https?:\/\/[^:]+:[^@]+@/;
function sanitizeLocator(locator: string): string {
if (CREDENTIAL_RE.test(locator)) {
return locator.replace(/^https?:\/\/[^@]+@/, (match) => {
const scheme = match.startsWith("https") ? "https://" : "http://";
return scheme;
});
}
return locator;
}
function mirrorRole(index: number): "cache" | "bare" | "external" {
if (index === 0) return "cache";
if (index === 1) return "bare";
return "external";
}
function parseSecretNames(envExampleContent: string): string[] {
const names: string[] = [];
for (const line of envExampleContent.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIdx = trimmed.indexOf("=");
if (eqIdx <= 0) continue;
const key = trimmed.slice(0, eqIdx).trim();
if (key) names.push(key);
}
return [...new Set(names)].sort();
}
async function countMissions(cacheClonePath: string, io: WorkspaceIO): Promise {
const missionsDir = path.join(cacheClonePath, "missions");
if (!(await io.exists(missionsDir))) return 0;
try {
const entries = await io.readdir(missionsDir);
return entries.filter((e) => e.isDirectory).length;
} catch {
return 0;
}
}
function extractChannels(
config: SystemConfig,
): Record<"dev" | "alt" | "main", { workerName: string; url: string }> {
const channels = config.deployment?.channels;
return {
dev: { workerName: channels?.dev?.workerName ?? "", url: channels?.dev?.url ?? "" },
alt: { workerName: channels?.alt?.workerName ?? "", url: channels?.alt?.url ?? "" },
main: { workerName: channels?.main?.workerName ?? "", url: channels?.main?.url ?? "" },
};
}
export async function buildPassportPayload(input: {
systemId: string;
werkstattRoot: string;
creatorIdentity: string;
creatorPublicKey: string;
io?: WorkspaceIO;
}): Promise {
const { systemId, werkstattRoot, creatorIdentity, creatorPublicKey } = input;
const io = input.io ?? createDefaultIO().io;
const cacheClonePath = resolveCacheClonePath(werkstattRoot, systemId);
const config = await readSystemConfig(werkstattRoot, systemId);
const pinPath = path.join(cacheClonePath, "system.pin.json");
let platformVersion = "0.0.0";
let platformSemanticHash =
"sha256:0000000000000000000000000000000000000000000000000000000000000000";
if (await io.exists(pinPath)) {
const pinRaw = await io.readFile(pinPath);
const pinParsed = partialPinSchema.safeParse(JSON.parse(pinRaw));
if (pinParsed.success) {
platformVersion = pinParsed.data.platform.version;
platformSemanticHash = pinParsed.data.platform.platformSemanticHash;
}
}
const envExamplePath = path.join(cacheClonePath, ".env.example");
let secretNames: string[] = [];
if (await io.exists(envExamplePath)) {
const envContent = await io.readFile(envExamplePath);
secretNames = parseSecretNames(envContent);
}
let bordbuchHead: string | null = null;
try {
const entries = await readBordbuch(werkstattRoot, systemId);
if (entries.length > 0) {
bordbuchHead = entries[entries.length - 1].hash;
}
} catch {
// Bordbuch not readable — treat as null
}
const missionCounter = await countMissions(cacheClonePath, io);
const mirrors: Array<{ role: "cache" | "bare" | "external"; locator: string }> =
config.mirrors.map((m, i) => ({
role: mirrorRole(i),
locator: sanitizeLocator(m.path),
}));
const now = new Date().toISOString();
return {
schema: "site-passport/v1",
systemId,
creator: {
identity: creatorIdentity,
publicKey: creatorPublicKey,
},
platform: {
version: platformVersion,
semanticHash: platformSemanticHash,
},
provenance: {
createdAt: now,
generatedAt: now,
bordbuchHead,
missionCounter,
},
resources: {
channels: extractChannels(config),
customDomains: [],
vectorizeIndexes: [],
r2Prefixes: [],
secretNames,
},
mirrors,
};
}
export async function signPassport(
payload: SitePassportV1,
privateKeyBytes: Uint8Array,
): Promise<{ passportHash: string; signature: string }> {
const snapshot = snapshotCanonicalJsonObjectV1(payload);
if (!snapshot.ok) {
throw new Error(
`CERT-CANONICAL-SNAPSHOT-01: failed to canonicalize passport payload (${snapshot.code})`,
);
}
const passportHash = canonicalJsonHashV1(snapshot.value);
const hashBytes = new Uint8Array(Buffer.from(passportHash, "utf8"));
const signatureBytes = await signBytes(privateKeyBytes, hashBytes);
return {
passportHash,
signature: toHex(signatureBytes),
};
}
export async function verifyPassport(
doc: SignedSitePassport,
): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
if (!doc.payload || typeof doc.payload !== "object") {
return { valid: false, errors: ["payload is missing or not an object"] };
}
const snapshot = snapshotCanonicalJsonObjectV1(doc.payload);
if (!snapshot.ok) {
return {
valid: false,
errors: [`CERT-CANONICAL-SNAPSHOT-01: failed to canonicalize payload (${snapshot.code})`],
};
}
const expectedHash = canonicalJsonHashV1(snapshot.value);
if (doc.passportHash !== expectedHash) {
errors.push(`passportHash mismatch: expected ${expectedHash}, got ${doc.passportHash}`);
}
try {
const publicKeyBytes = fromHex(doc.payload.creator.publicKey);
const hashBytes = new Uint8Array(Buffer.from(doc.passportHash, "utf8"));
const signatureBytes = fromHex(doc.signature);
const sigValid = await verifyBytes(publicKeyBytes, hashBytes, signatureBytes);
if (!sigValid) {
errors.push(
"Ed25519 signature verification failed — passport may be tampered or signed by a different key",
);
}
} catch (err) {
errors.push(
`Signature verification error: ${err instanceof Error ? err.message : String(err)}`,
);
}
return { valid: errors.length === 0, errors };
}
export async function derivePublicKey(privateKeyBytes: Uint8Array): Promise {
const pubKey = await getPublicKey(privateKeyBytes);
return toHex(pubKey);
}
export function computePassportHash(payload: SitePassportV1): string {
const snapshot = snapshotCanonicalJsonObjectV1(payload);
if (!snapshot.ok) {
throw new Error(
`CERT-CANONICAL-SNAPSHOT-01: failed to canonicalize passport payload (${snapshot.code})`,
);
}
return canonicalJsonHashV1(snapshot.value);
}