/* RFC-0968: sternsystem.handover.prepare — the sending instance generates a signed handover authorization that transfers the site to a specified recipient identity. Reads the passport for sender identity + public key, loads SIGNING_PRIVATE_KEY, builds HandoverAuthorizationV1, signs it, and writes handover-authorization.json to the cache clone. Does not verify the authorization — that is the receiving instance's job in sternsystem.handover.complete. Does not propagate the authorization to mirrors — use sternsystem.sync for that. RFC-0968: initial handover prepare command handler. RFC-0988: add --dry-run flag — sign authorization in memory, return SignedHandoverAuthorization in result, skip writeAuthorization. RFC-1237: resolve dry-run via context.dryRun fallback — consumeCommonFlags strips --dry-run into context.dryRun before dispatch, so a flags-only read was permanently false under werkstatt run (enforced by KERNEL-FLAG-10). RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt */ import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { loadPrivateKey } from "@warpgogol/werkstatt-engine/signing"; import { resolveActor } from "../mission/actor-identity.ts"; import { readPassport } from "./registry-io.ts"; import { readBordbuch } from "../bordbuch/bordbuch-io.ts"; import { signAuthorization, writeAuthorization, type HandoverAuthorizationV1, type SignedHandoverAuthorization, } from "./handover.ts"; import { acquireLock, releaseLock, generateOperationId } from "../werkstatt/index.ts"; export interface SternsystemHandoverPrepareData { command: "sternsystem.handover.prepare"; systemId: string; senderIdentity: string; recipientIdentity: string; authorizationHash: string; authorizationPath: string; expiresAt: string; dryRun: boolean; signedAuthorization?: SignedHandoverAuthorization; } function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } function flagBool(input: KernelCommandInput, key: string): boolean { return input.flags[key] === true; } function filterEnv(env: Record): Record { const result: Record = {}; for (const [key, value] of Object.entries(env)) { if (value !== undefined) { result[key] = value; } } return result; } const DEFAULT_EXPIRY_DAYS = 30; export async function runSternsystemHandoverPrepare( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const systemId = flagString(input, "id"); const recipientIdentity = flagString(input, "recipient-identity"); const recipientPublicKey = flagString(input, "recipient-public-key"); const dryRun = context.dryRun === true || flagBool(input, "dry-run"); if (!systemId) { throw new Error("[sternsystem.handover.prepare] requires --id "); } if (!recipientIdentity) { throw new Error( "[sternsystem.handover.prepare] requires --recipient-identity ", ); } if (!recipientPublicKey) { throw new Error( "[sternsystem.handover.prepare] requires --recipient-public-key ", ); } const senderIdentity = resolveActor(input); const env = { ...filterEnv(process.env) }; const privateKeyEnv = env["SIGNING_PRIVATE_KEY"]; const privateKeyPath = env["SIGNING_PRIVATE_KEY_PATH"]; if (!privateKeyEnv && !privateKeyPath) { throw new Error( "[sternsystem.handover.prepare] No signing key configured. Set SIGNING_PRIVATE_KEY or SIGNING_PRIVATE_KEY_PATH env var.", ); } let privateKeyBytes: Uint8Array; try { if (privateKeyEnv) { privateKeyBytes = await loadPrivateKey({ pem: privateKeyEnv }); } else { privateKeyBytes = await loadPrivateKey({ filePath: privateKeyPath!, encoding: "pem" }); } } catch (err) { throw new Error( `[sternsystem.handover.prepare] Failed to load private key: ${err instanceof Error ? err.message : String(err)}`, ); } const passport = await readPassport(workspaceRoot, systemId); if (!passport) { throw new Error( `[sternsystem.handover.prepare] No passport found for ${systemId}. Run sternsystem.passport.generate first.`, ); } const senderPublicKey = passport.payload.creator.publicKey; const passportHash = passport.passportHash; let bordbuchHead = ""; try { const entries = await readBordbuch(workspaceRoot, systemId); if (entries.length > 0) { bordbuchHead = entries[entries.length - 1].hash; } } catch { // Bordbuch not readable — empty head } const now = new Date(); const expiresAt = new Date(now.getTime() + DEFAULT_EXPIRY_DAYS * 24 * 60 * 60 * 1000); const payload: HandoverAuthorizationV1 = { schema: "handover-authorization/v1", systemId, sender: { identity: senderIdentity, publicKey: senderPublicKey, }, recipient: { identity: recipientIdentity, publicKey: recipientPublicKey, }, passportHash, bordbuchHead, authorizedAt: now.toISOString(), expiresAt: expiresAt.toISOString(), }; const operationId = generateOperationId(); await acquireLock( workspaceRoot, `system:${systemId}`, operationId, "sternsystem.handover.prepare", senderIdentity, ); let doc: SignedHandoverAuthorization; try { doc = await signAuthorization(payload, privateKeyBytes); if (dryRun) { logger.success( `[sternsystem.handover.prepare] ${systemId} dry-run: authorization signed for ${recipientIdentity} (hash: ${doc.authorizationHash}) — no files written`, ); return { data: { command: "sternsystem.handover.prepare", systemId, senderIdentity, recipientIdentity, authorizationHash: doc.authorizationHash, authorizationPath: "", expiresAt: expiresAt.toISOString(), dryRun: true, signedAuthorization: doc, }, exitCode: 0, summary: `[sternsystem.handover.prepare] ${systemId} dry-run: authorization generated for ${recipientIdentity} (no files written)`, }; } const authorizationPath = await writeAuthorization(workspaceRoot, systemId, doc); logger.success( `[sternsystem.handover.prepare] ${systemId} authorization signed for ${recipientIdentity} (hash: ${doc.authorizationHash})`, ); return { data: { command: "sternsystem.handover.prepare", systemId, senderIdentity, recipientIdentity, authorizationHash: doc.authorizationHash, authorizationPath, expiresAt: expiresAt.toISOString(), dryRun: false, }, exitCode: 0, summary: `[sternsystem.handover.prepare] ${systemId} handover authorized from ${senderIdentity} to ${recipientIdentity}, expires ${expiresAt.toISOString()}`, }; } finally { await releaseLock(workspaceRoot, `system:${systemId}`); } }