fix(housekeeping): harden audited command dispatch

This commit is contained in:
Simo committed 2026-08-27 18:58:14 +02:00
1 parent 6aed71a8b2
commit 796d009c07
7 files changed
+775 -124

No files matched your search

@@ -1,3 +1,4 @@
import { z } from "zod";
import type { AuditEntry, HousekeepingAuditWriter } from "@/lib/services/audit";
import { satisfiesCapability } from "../capability-context";
import {
@@ -7,7 +8,11 @@ import {
mapUnknownError,
} from "../contracts";
import { createCorrelationId } from "../correlation";
import { writeIntent, writeOutcome } from "./audit-envelope";
import {
AuditOutcomePersistenceError,
writeIntent,
writeOutcome,
} from "./audit-envelope";
import { confirmHousekeepingCommand } from "./confirmation";
import {
getHousekeepingCommand,
@@ -15,6 +20,20 @@ import {
type HousekeepingCommandContext,
} from "./registry";
const normalizedCommandId = z
.string()
.transform((value) => value.normalize("NFC").trim())
.pipe(z.string().min(1).max(160));
const normalizedReason = z
.string()
.transform((value) => value.normalize("NFC").trim())
.pipe(z.string().max(1000));
const commandRequestSchema = z.strictObject({
commandId: normalizedCommandId,
input: z.unknown(),
reason: normalizedReason.optional(),
});
export interface HousekeepingCommandRequest {
commandId: string;
input: unknown;
@@ -29,54 +48,86 @@ export interface HousekeepingCommandDependencies {
}
export async function dispatchHousekeepingCommand(
request: HousekeepingCommandRequest,
request: unknown,
dependencies: HousekeepingCommandDependencies,
): Promise<HousekeepingResult<unknown>> {
const correlationId = createCorrelationId();
const command = getHousekeepingCommand(request.commandId);
let parsedRequest: ReturnType<typeof commandRequestSchema.safeParse>;
try {
parsedRequest = commandRequestSchema.safeParse(request);
} catch (error) {
return mapUnknownError(error, correlationId);
}
if (!parsedRequest.success) {
return fail("VALIDATION", "errors.housekeeping.validation", correlationId);
}
const commandRequest = parsedRequest.data;
const command = getHousekeepingCommand(commandRequest.commandId);
if (!command) {
return fail("NOT_FOUND", "errors.housekeeping.notFound", correlationId);
const result = fail(
"NOT_FOUND",
"errors.housekeeping.notFound",
correlationId,
);
return persistReturnedOutcome(
dependencies.audit,
createUnknownCommandAuditEntry(
commandRequest.commandId,
dependencies,
correlationId,
),
"denied",
result,
);
}
const reason =
typeof request.reason === "string"
? request.reason.normalize("NFC").trim() || undefined
: undefined;
const auditEntry = createAuditEntry(
command,
dependencies.context,
dependencies.ipAddress,
correlationId,
reason,
commandRequest.reason || undefined,
);
if (command.risk === "sensitive") {
try {
await writeIntent(dependencies.audit, auditEntry);
} catch (error) {
return mapUnknownError(error, correlationId);
}
let authorized: boolean;
try {
authorized = satisfiesCapability(dependencies.context, command.capability);
} catch (error) {
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
"failure",
mapUnknownError(error, correlationId),
);
}
if (!satisfiesCapability(dependencies.context, command.capability)) {
return finishWithAudit(
if (!authorized) {
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
"denied",
fail("FORBIDDEN", "errors.housekeeping.forbidden", correlationId),
correlationId,
);
}
const parsedInput = command.input.safeParse(request.input);
let parsedInput: ReturnType<typeof command.input.safeParse>;
try {
parsedInput = command.input.safeParse(commandRequest.input);
} catch (error) {
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
"failure",
mapUnknownError(error, correlationId),
);
}
if (!parsedInput.success) {
const fieldErrors: Record<string, readonly string[]> = {};
for (const issue of parsedInput.error.issues) {
const field = String(issue.path[0] ?? "input");
fieldErrors[field] = ["errors.validation.invalid"];
}
return finishWithAudit(
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
"denied",
@@ -86,22 +137,30 @@ export async function dispatchHousekeepingCommand(
correlationId,
fieldErrors,
),
correlationId,
);
}
const confirmation = confirmHousekeepingCommand(
command,
reason,
correlationId,
);
let confirmation: HousekeepingResult<unknown>;
try {
confirmation = confirmHousekeepingCommand(
command,
commandRequest.reason || undefined,
correlationId,
);
} catch (error) {
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
"failure",
mapUnknownError(error, correlationId),
);
}
if (!confirmation.ok) {
return finishWithAudit(
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
"denied",
confirmation,
correlationId,
);
}
@@ -117,25 +176,30 @@ export async function dispatchHousekeepingCommand(
command.rateLimit.windowMs,
);
} catch (error) {
return finishWithAudit(
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
"failure",
mapUnknownError(error, correlationId),
correlationId,
);
}
if (!allowed) {
return finishWithAudit(
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
"denied",
fail("RATE_LIMITED", "errors.housekeeping.rateLimited", correlationId),
correlationId,
);
}
if (command.risk === "sensitive") {
try {
await writeIntent(dependencies.audit, auditEntry);
} catch (error) {
return mapUnknownError(error, correlationId);
}
}
const commandContext: HousekeepingCommandContext = {
capability: dependencies.context,
correlationId,
@@ -145,22 +209,27 @@ export async function dispatchHousekeepingCommand(
try {
result = await command.execute(commandContext, parsedInput.data);
} catch (error) {
return finishWithAudit(
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
"failure",
mapUnknownError(error, correlationId),
correlationId,
);
}
const correlatedResult = withCorrelation(result, correlationId);
return finishWithAudit(
if (!correlatedResult.ok) {
return persistReturnedOutcome(
dependencies.audit,
auditEntry,
outcomeForFailure(correlatedResult),
correlatedResult,
);
}
return persistSuccessfulOutcome(
dependencies.audit,
auditEntry,
outcomeFor(correlatedResult),
correlatedResult,
correlationId,
);
}
@@ -182,10 +251,24 @@ function createAuditEntry(
};
}
function createUnknownCommandAuditEntry(
commandId: string,
dependencies: HousekeepingCommandDependencies,
correlationId: string,
): AuditEntry {
return {
userId: dependencies.context.actor.id,
action: "housekeeping.command.dispatch",
target: commandId,
correlationId,
ipAddress: dependencies.ipAddress,
};
}
function createRateLimitKey(
commandId: string,
context: HousekeepingCapabilityContext,
ipAddress = "0.0.0.0",
ipAddress: string,
): string {
return `housekeeping-command:${context.actor.id}:${ipAddress}:${commandId}`;
}
@@ -197,10 +280,9 @@ function withCorrelation<T>(
return { ...result, correlationId };
}
function outcomeFor(
result: HousekeepingResult<unknown>,
): "success" | "failure" | "denied" {
if (result.ok) return "success";
function outcomeForFailure(
result: Extract<HousekeepingResult<unknown>, { ok: false }>,
): "failure" | "denied" {
return result.error.code === "FORBIDDEN" ||
result.error.code === "VALIDATION" ||
result.error.code === "RATE_LIMITED"
@@ -208,17 +290,34 @@ function outcomeFor(
: "failure";
}
async function finishWithAudit<T>(
async function persistReturnedOutcome<T>(
writer: HousekeepingAuditWriter,
entry: AuditEntry,
outcome: "success" | "failure" | "denied",
outcome: "failure" | "denied",
result: HousekeepingResult<T>,
correlationId: string,
): Promise<HousekeepingResult<T>> {
try {
await writeOutcome(writer, entry, outcome);
} catch {
// The command/preflight failure remains authoritative if evidence is down.
}
return result;
}
async function persistSuccessfulOutcome<T>(
writer: HousekeepingAuditWriter,
entry: AuditEntry,
result: HousekeepingResult<T>,
): Promise<HousekeepingResult<T>> {
try {
await writeOutcome(writer, entry, "success");
return result;
} catch (error) {
return mapUnknownError(error, correlationId);
} catch (auditOutcomeError) {
try {
await writeOutcome(writer, entry, "partial");
} catch {
// The completed-operation error below remains the primary evidence.
}
throw new AuditOutcomePersistenceError(result, auditOutcomeError);
}
}