/*
RFC-0968: Sternsystem handover protocol — authorization types, signing/verification,
and file IO helpers for cross-werkstatt site transfer. Uses canonical-JSON hashing (RFC-0849)
and Ed25519 signing (RFC-0931 helpers), same patterns as passport.ts.
- Does not implement command logic — that lives in sternsystem-handover-prepare/complete/cancel.ts.
- Does not enforce HANDOVER validation rules — that lives in sternsystem-validate.ts.
- Does not manage key lifecycle or rotation.
- RFC-0968: initial handover authorization types, signing/verification, and file IO helpers.
- RFC-0988: add Zod schema and parseAuthorizationData for --dry-run --authorization-data CLI input validation.
- 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.
*/
import path from "node:path";
import {
snapshotCanonicalJsonObjectV1,
canonicalJsonHashV1,
} from "@warpgogol/werkstatt-engine/fingerprint";
import { signBytes, verifyBytes, toHex, fromHex } from "@warpgogol/werkstatt-engine/signing";
import { resolveCacheClonePath } from "./registry-io.ts";
import { atomicWriteFile } from "../werkstatt/atomic.ts";
import { z } from "zod";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
export interface HandoverAuthorizationV1 {
schema: "handover-authorization/v1";
systemId: string;
sender: {
identity: string;
publicKey: string;
};
recipient: {
identity: string;
publicKey: string;
};
passportHash: string;
bordbuchHead: string;
authorizedAt: string;
expiresAt: string;
}
export interface SignedHandoverAuthorization {
payload: HandoverAuthorizationV1;
authorizationHash: string;
signature: string;
}
export interface HandoverCompleteResult {
systemId: string;
previousCreator: string;
newCreator: string;
newPassportHash: string;
bordbuchEventHash: string;
ownershipRegistryUpdated: boolean;
}
export interface HandoverEventMetadata {
from: string;
to: string;
authorizationHash: string;
newPassportHash: string;
}
export function computeAuthorizationHash(payload: HandoverAuthorizationV1): string {
const snapshot = snapshotCanonicalJsonObjectV1(payload);
if (!snapshot.ok) {
throw new Error(
`CERT-CANONICAL-SNAPSHOT-01: failed to canonicalize authorization payload (${snapshot.code})`,
);
}
return canonicalJsonHashV1(snapshot.value);
}
export async function signAuthorization(
payload: HandoverAuthorizationV1,
privateKeyBytes: Uint8Array,
): Promise {
const authorizationHash = computeAuthorizationHash(payload);
const hashBytes = new Uint8Array(Buffer.from(authorizationHash, "utf8"));
const signatureBytes = await signBytes(privateKeyBytes, hashBytes);
return {
payload,
authorizationHash,
signature: toHex(signatureBytes),
};
}
export async function verifyAuthorization(
doc: SignedHandoverAuthorization,
senderPublicKey: string,
): 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.authorizationHash !== expectedHash) {
errors.push(
`authorizationHash mismatch: expected ${expectedHash}, got ${doc.authorizationHash}`,
);
}
try {
const publicKeyBytes = fromHex(senderPublicKey);
const hashBytes = new Uint8Array(Buffer.from(doc.authorizationHash, "utf8"));
const signatureBytes = fromHex(doc.signature);
const sigValid = await verifyBytes(publicKeyBytes, hashBytes, signatureBytes);
if (!sigValid) {
errors.push(
"Ed25519 signature verification failed — authorization 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 function resolveAuthorizationPath(workspaceRoot: string, systemId: string): string {
const cachePath = resolveCacheClonePath(workspaceRoot, systemId);
return path.join(cachePath, "handover-authorization.json");
}
export async function readAuthorization(
workspaceRoot: string,
systemId: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const filePath = resolveAuthorizationPath(workspaceRoot, systemId);
if (!(await io.exists(filePath))) return null;
const raw = await io.readFile(filePath);
return JSON.parse(raw) as SignedHandoverAuthorization;
}
export async function writeAuthorization(
workspaceRoot: string,
systemId: string,
doc: SignedHandoverAuthorization,
): Promise {
const filePath = resolveAuthorizationPath(workspaceRoot, systemId);
const content = JSON.stringify(doc, null, 2) + "\n";
await atomicWriteFile(filePath, content);
return filePath;
}
export async function removeAuthorization(
workspaceRoot: string,
systemId: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const filePath = resolveAuthorizationPath(workspaceRoot, systemId);
if (!(await io.exists(filePath))) return false;
await io.rm(filePath);
return true;
}
export function isAuthorizationExpired(expiresAt: string, now: Date = new Date()): boolean {
const expiry = new Date(expiresAt);
return expiry.getTime() < now.getTime();
}
const handoverAuthorizationV1Schema = z.object({
schema: z.literal("handover-authorization/v1"),
systemId: z.string(),
sender: z.object({
identity: z.string(),
publicKey: z.string(),
}),
recipient: z.object({
identity: z.string(),
publicKey: z.string(),
}),
passportHash: z.string(),
bordbuchHead: z.string(),
authorizedAt: z.string(),
expiresAt: z.string(),
});
const signedHandoverAuthorizationSchema = z.object({
payload: handoverAuthorizationV1Schema,
authorizationHash: z.string(),
signature: z.string(),
});
export function parseAuthorizationData(json: string): SignedHandoverAuthorization {
let parsed: unknown;
try {
parsed = JSON.parse(json);
} catch (err) {
throw new Error(
`[sternsystem.handover.complete] invalid authorization-data JSON: ${err instanceof Error ? err.message : String(err)}`,
);
}
const result = signedHandoverAuthorizationSchema.safeParse(parsed);
if (!result.success) {
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
throw new Error(
`[sternsystem.handover.complete] authorization-data validation failed: ${issues}`,
);
}
return result.data as SignedHandoverAuthorization;
}