fix(housekeeping): harden command dispatch boundaries

This commit is contained in:
Simo committed 2026-08-27 19:32:19 +02:00
1 parent 796d009c07
commit 00618fb2a3
8 files changed
+608 -30

No files matched your search

+29 -1
View File
@@ -1,6 +1,21 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { z } from "zod";
import { registerHousekeepingCommand } from "@/features/housekeeping/foundation/commands/registry";
vi.mock(
"@/features/housekeeping/foundation/commands/registry",
async (importOriginal) => ({
...(await importOriginal<
typeof import("@/features/housekeeping/foundation/commands/registry")
>()),
sealHousekeepingCommandRegistry: sealRegistryMock,
}),
);
vi.mock("@/features/housekeeping/foundation/commands/bootstrap", () => ({
housekeepingCommandRegistryReady: true,
}));
import {
anyCapability,
ok,
@@ -15,6 +30,7 @@ const {
getContextMock,
getIpMock,
rateLimitCalls,
sealRegistryMock,
} = vi.hoisted(() => ({
auditEntries: [] as AuditEntry[],
auditWriteMock: vi.fn(),
@@ -30,6 +46,7 @@ const {
getContextMock: vi.fn(),
getIpMock: vi.fn(),
rateLimitCalls: [] as Array<[string, number, number]>,
sealRegistryMock: vi.fn(),
}));
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
@@ -77,6 +94,7 @@ beforeEach(() => {
rateLimitCalls.length = 0;
getContextMock.mockReset().mockResolvedValue(context);
getIpMock.mockReset().mockResolvedValue("203.0.113.7");
sealRegistryMock.mockReset();
auditWriteMock.mockReset().mockImplementation(async (entry: AuditEntry) => {
auditEntries.push({ ...entry });
});
@@ -115,6 +133,7 @@ describe("executeHousekeepingCommand", () => {
},
]);
expect(auditEntries[0]?.correlationId).toBe(result.correlationId);
expect(sealRegistryMock).not.toHaveBeenCalled();
});
it("strictly rejects spoofed server-owned metadata before execution", async () => {
@@ -136,7 +155,16 @@ describe("executeHousekeepingCommand", () => {
});
expect(commandExecutions).toEqual([]);
expect(rateLimitCalls).toEqual([]);
expect(auditEntries).toEqual([]);
expect(auditEntries).toMatchObject([
{
userId: 71,
action: "housekeeping.command.dispatch",
target: "request-envelope",
ipAddress: "203.0.113.7",
outcome: "denied",
},
]);
expect(JSON.stringify(auditEntries)).not.toContain("forged");
});
it("sanitizes server context acquisition failures into typed results", async () => {
+1 -2
View File
@@ -1,8 +1,8 @@
"use server";
import "@/features/housekeeping/foundation/commands/bootstrap";
import { AuditOutcomePersistenceError } from "@/features/housekeeping/foundation/commands/audit-envelope";
import { dispatchHousekeepingCommand } from "@/features/housekeeping/foundation/commands/dispatcher";
import { sealHousekeepingCommandRegistry } from "@/features/housekeeping/foundation/commands/registry";
import {
fail,
type HousekeepingResult,
@@ -15,7 +15,6 @@ import { housekeepingAuditWriter } from "@/lib/services/audit";
export async function executeHousekeepingCommand(
request: unknown,
): Promise<HousekeepingResult<unknown>> {
sealHousekeepingCommandRegistry();
try {
const [context, ipAddress] = await Promise.all([
getHousekeepingCapabilityContext(),
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { anyCapability, ok } from "../contracts";
import { housekeepingCommandRegistryReady } from "./bootstrap";
import { registerHousekeepingCommand } from "./registry";
describe("housekeeping command bootstrap", () => {
it("registers the complete current list and seals during module initialization", () => {
expect(housekeepingCommandRegistryReady).toBe(true);
expect(() =>
registerHousekeepingCommand({
id: "system.bootstrap.too-late",
owner: "system",
risk: "safe",
capability: anyCapability("admin.settings.view"),
input: z.object({}),
requiresReason: false,
rateLimit: { attempts: 1, windowMs: 1_000 },
execute: async (context) => ok(null, context.correlationId),
}),
).toThrow("command registry is sealed");
});
});
@@ -0,0 +1,17 @@
import "server-only";
import type { HousekeepingCommand } from "./registry";
import {
registerHousekeepingCommand,
sealHousekeepingCommandRegistry,
} from "./registry";
const currentHousekeepingCommands =
[] as const satisfies readonly HousekeepingCommand<unknown, unknown>[];
for (const command of currentHousekeepingCommands) {
registerHousekeepingCommand(command);
}
sealHousekeepingCommandRegistry();
export const housekeepingCommandRegistryReady = true;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -123,6 +123,37 @@ describe("housekeeping command registry", () => {
expect(() => registerHousekeepingCommand(invalid)).toThrow();
});
it("keeps registered validation unchanged after original shape and child mutation", () => {
const child = z.string().min(3);
const input = z.object({ value: child, guard: child });
registerHousekeepingCommand({
...command("people.registry.deep-validation"),
input,
execute: async (context, value) =>
ok({ id: value.value.length }, context.correlationId),
} as HousekeepingCommand<{ value: string; guard: string }, { id: number }>);
const registered = getHousekeepingCommand(
"people.registry.deep-validation",
);
if (!registered) throw new Error("registered command missing");
(input.def as { shape: { value: z.ZodType } }).shape.value = z.number();
const minCheck = (child.def as { checks: unknown[] }).checks[0] as {
_zod: { def: { minimum: number } };
};
minCheck._zod.def.minimum = 0;
expect(input.safeParse({ value: 4, guard: "a" }).success).toBe(true);
expect(
registered.input.safeParse({ value: "abcd", guard: "ab" }).success,
).toBe(false);
expect(
registered.input.safeParse({ value: "abcd", guard: "abcd" }).success,
).toBe(true);
expect(
registered.input.safeParse({ value: 4, guard: "abcd" }).success,
).toBe(false);
});
it("seals the global registry after deterministic bootstrap", () => {
sealHousekeepingCommandRegistry();
File diff suppressed because it is too large. Load diff