/* RFC-0715/RFC-0921: nachweis.key.ensure command handler — generates an Ed25519 keypair for Nachweis operator signatures via shared signing core. Does not implement key rotation — that is a future RFC. Does not use multibase encoding — intentionally hex for simplicity (differs from passport). RFC-0715 review fix: import flagString/flagBool from nachweis-n3-types.ts. RFC-0921: delegate key generation to shared signing core (generateKeyPair, toHex). Remove @noble/ed25519 import. 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 RFC-0715 */ import path from "node:path"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; import { byteHash } from "@warpgogol/werkstatt-engine/fingerprint"; import { writeFileIfChanged } from "@warpgogol/werkstatt-engine/kernel"; import { generateKeyPair, toHex } from "@warpgogol/werkstatt-engine/signing"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { flagString, flagBool } from "./nachweis-n3-types.ts"; export interface NachweisKeyEnsureResult { keyFilePath: string; publicKeyFilePath: string; publicKeyHex: string; keyId: string; } export interface NachweisPublicKeyEntry { keyId: string; publicKeyHex: string; status: "current" | "retired"; createdAt: string; } export async function ensureNachweisKey( keyFilePath: string, force: boolean, io: WorkspaceIO = createDefaultIO().io, ): Promise { if ((await io.exists(keyFilePath)) && !force) { throw new Error( `[nachweis.key.ensure] KEY_FILE_EXISTS: '${keyFilePath}' already exists. Use --force to overwrite.`, ); } const keyPair = await generateKeyPair(); const privateKeyBytes = keyPair.privateKey; const publicKeyBytes = keyPair.publicKey; const privateKeyHex = toHex(privateKeyBytes); const publicKeyHex = toHex(publicKeyBytes); const keyId = byteHash(Buffer.from(publicKeyHex, "hex")).replace("sha256:", ""); await io.mkdir(path.dirname(keyFilePath)); await io.writeFile(keyFilePath, privateKeyHex); const publicKeyFilePath = `${keyFilePath}.pub`; await io.writeFile(publicKeyFilePath, publicKeyHex); return { keyFilePath, publicKeyFilePath, publicKeyHex, keyId, }; } export async function writePublicKeyJson( workpiecePublicDir: string, keyId: string, publicKeyHex: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const wellKnownDir = path.join(workpiecePublicDir, ".well-known"); const pubKeyPath = path.join(wellKnownDir, "nachweis-pubkey.json"); const existing: NachweisPublicKeyEntry[] = (await io.exists(pubKeyPath)) ? await safeReadJson(pubKeyPath, [], io) : []; const updated: NachweisPublicKeyEntry[] = existing.map((e) => ({ ...e, status: "retired" as const, })); updated.push({ keyId, publicKeyHex, status: "current" as const, createdAt: new Date().toISOString(), }); await io.mkdir(wellKnownDir); await writeFileIfChanged(pubKeyPath, JSON.stringify(updated, null, 2) + "\n"); return pubKeyPath; } async function safeReadJson(filePath: string, fallback: T, io: WorkspaceIO): Promise { try { const raw = await io.readFile(filePath); return JSON.parse(raw) as T; } catch { return fallback; } } export async function runNachweisKeyEnsure( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const keyFilePath = flagString(input, "key-file"); const force = flagBool(input, "force"); if (!keyFilePath) throw new Error("[nachweis.key.ensure] --key-file is required"); const result = await ensureNachweisKey(keyFilePath, force, context.io); const workpieceDir = context.site?.directory ? path.join(context.workspaceRoot, context.site.directory) : null; let pubKeyJsonPath: string | null = null; if (workpieceDir && (await context.io.exists(workpieceDir))) { const workpiecePublicDir = path.join(workpieceDir, "public"); if (await context.io.exists(workpiecePublicDir)) { pubKeyJsonPath = await writePublicKeyJson( workpiecePublicDir, result.keyId, result.publicKeyHex, context.io, ); } } return { data: result, exitCode: 0, summary: `[nachweis.key.ensure] generated Ed25519 keypair (keyId: ${result.keyId.slice(0, 12)}…) → ${keyFilePath}${pubKeyJsonPath ? `, published to ${pubKeyJsonPath}` : ""}`, }; }