/*
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}`);
}
}