/*
RFC-0566 deploy.artifact.verify — verify artifact content hash and signature against manifest.
- Do not swap symlinks — that is deploy.atomic.swap's job.
- RFC-0566: initial deploy.artifact.verify handler.
- 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-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
*/
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import { verifyJsonSignatureAsync, requireEnv } from "@warpgogol/werkstatt-engine/integrity";
import type { ArtifactVerifyResult } from "./types.ts";
import { artifactDir, distPath, hashArtifactDir, readManifest } from "./deploy-utils.ts";
function flagString(input: KernelCommandInput, key: string): string | undefined {
const v = input.flags[key];
return typeof v === "string" ? v : undefined;
}
export async function runDeployArtifactVerify(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot, logger } = context;
const hash = flagString(input, "hash");
if (!hash) {
throw new Error("[deploy.artifact.verify] --hash is required");
}
const dir = artifactDir(workspaceRoot, hash);
if (!(await context.io.exists(dir))) {
logger.error(`[deploy.artifact.verify] artifact ${hash} not found`);
return {
exitCode: 1,
summary: `[deploy.artifact.verify] artifact ${hash} not found`,
nextSteps: [
{
action: `[deploy.artifact.verify] artifact ${hash} not found`,
kind: "required" as const,
},
],
data: { verified: false, hash, expectedHash: hash, signatureVerified: false },
};
}
const manifest = await readManifest(workspaceRoot, hash);
const distDir = distPath(workspaceRoot, hash);
if (!(await context.io.exists(distDir))) {
logger.error(`[deploy.artifact.verify] dist directory missing for artifact ${hash}`);
return {
exitCode: 1,
summary: `[deploy.artifact.verify] dist directory missing for ${hash}`,
nextSteps: [
{
action: `[deploy.artifact.verify] dist directory missing for ${hash}`,
kind: "required" as const,
},
],
data: { verified: false, hash, expectedHash: manifest.hash, signatureVerified: false },
};
}
const { treeHash } = await hashArtifactDir(distDir);
const hashMatches = treeHash === manifest.hash;
let signatureVerified = false;
if (manifest.signature) {
try {
const publicKeyPem = await requireEnv("SIGNING_PUBLIC_KEY", workspaceRoot);
const payload: Record = {
hash: manifest.hash,
files: manifest.files,
totalSize: manifest.totalSize,
builtAt: manifest.builtAt,
gitSha: manifest.gitSha,
buildHost: manifest.buildHost,
};
signatureVerified = await verifyJsonSignatureAsync({
payload,
signatureHex: manifest.signature,
publicKeyPem,
});
} catch {
signatureVerified = false;
}
}
const verified = hashMatches && (signatureVerified || !manifest.signature);
if (!verified) {
logger.error(
`[deploy.artifact.verify] verification failed for ${hash} (hash: ${hashMatches}, sig: ${signatureVerified})`,
);
return {
exitCode: 1,
summary: `[deploy.artifact.verify] ${hash} verification failed (hash-mismatch)`,
nextSteps: [
{
action: `[deploy.artifact.verify] ${hash} verification failed (hash-mismatch)`,
kind: "required" as const,
},
],
data: { verified: false, hash: treeHash, expectedHash: manifest.hash, signatureVerified },
};
}
logger.success(`[deploy.artifact.verify] artifact ${hash} verified`);
return {
data: { verified: true, hash: treeHash, expectedHash: manifest.hash, signatureVerified },
exitCode: 0,
summary: `[deploy.artifact.verify] ${hash} verified (hash: ok, sig: ${signatureVerified})`,
nextSteps: [
{
action: `Deploy the artifact: pnpm exec werkstatt run leitstand.dev-deploy --release `,
kind: "optional",
},
],
};
}