feat(housekeeping): dispatch audited commands
This commit is contained in:
1 parent
ca666d9f90
commit
6aed71a8b2
8 files changed
+996
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,27 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
dispatchHousekeepingCommand,
|
||||
type HousekeepingCommandRequest,
|
||||
} from "@/features/housekeeping/foundation/commands/dispatcher";
|
||||
import type { HousekeepingResult } from "@/features/housekeeping/foundation/contracts";
|
||||
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
import { housekeepingAuditWriter } from "@/lib/services/audit";
|
||||
|
||||
export async function executeHousekeepingCommand(
|
||||
request: HousekeepingCommandRequest,
|
||||
): Promise<HousekeepingResult<unknown>> {
|
||||
const [context, ipAddress] = await Promise.all([
|
||||
getHousekeepingCapabilityContext(),
|
||||
clientIp(),
|
||||
]);
|
||||
|
||||
return dispatchHousekeepingCommand(request, {
|
||||
context,
|
||||
ipAddress,
|
||||
audit: housekeepingAuditWriter,
|
||||
rateLimit: async (key, attempts, windowMs) =>
|
||||
(await rateLimit(key, attempts, windowMs)).ok,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { anyCapability, ok } from "../contracts";
|
||||
import { confirmHousekeepingCommand } from "./confirmation";
|
||||
import type { HousekeepingCommand } from "./registry";
|
||||
|
||||
function command(
|
||||
requiresReason: boolean,
|
||||
risk: "safe" | "sensitive",
|
||||
): HousekeepingCommand<Record<string, never>, null> {
|
||||
return {
|
||||
id: "system.confirmation.test",
|
||||
owner: "system",
|
||||
risk,
|
||||
capability: anyCapability("admin.settings.edit"),
|
||||
input: z.object({}),
|
||||
requiresReason,
|
||||
rateLimit: { attempts: 2, windowMs: 60_000 },
|
||||
execute: async (context) => ok(null, context.correlationId),
|
||||
};
|
||||
}
|
||||
|
||||
describe("housekeeping command confirmation", () => {
|
||||
it("rejects blank reasons when the registered command requires one", () => {
|
||||
expect(
|
||||
confirmHousekeepingCommand(
|
||||
command(true, "sensitive"),
|
||||
" \t ",
|
||||
"corr-reason-missing",
|
||||
),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "VALIDATION",
|
||||
messageKey: "errors.housekeeping.validation",
|
||||
fieldErrors: { reason: ["errors.validation.required"] },
|
||||
},
|
||||
correlationId: "corr-reason-missing",
|
||||
});
|
||||
});
|
||||
|
||||
it("derives confirmation risk from the registered command and normalizes its reason", () => {
|
||||
expect(
|
||||
confirmHousekeepingCommand(
|
||||
command(true, "sensitive"),
|
||||
" Scheduled maintenance ",
|
||||
"corr-confirm",
|
||||
),
|
||||
).toEqual({
|
||||
ok: true,
|
||||
data: {
|
||||
commandId: "system.confirmation.test",
|
||||
risk: "sensitive",
|
||||
requiresReason: true,
|
||||
reason: "Scheduled maintenance",
|
||||
},
|
||||
correlationId: "corr-confirm",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits an empty optional reason without inventing confirmation metadata", () => {
|
||||
const result = confirmHousekeepingCommand(
|
||||
command(false, "safe"),
|
||||
undefined,
|
||||
"corr-safe",
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
data: {
|
||||
commandId: "system.confirmation.test",
|
||||
risk: "safe",
|
||||
requiresReason: false,
|
||||
},
|
||||
correlationId: "corr-safe",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { fail, type HousekeepingResult, ok } from "../contracts";
|
||||
import type { HousekeepingCommand } from "./registry";
|
||||
|
||||
export interface HousekeepingCommandConfirmation {
|
||||
commandId: string;
|
||||
risk: "safe" | "sensitive";
|
||||
requiresReason: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export function confirmHousekeepingCommand(
|
||||
command: HousekeepingCommand<unknown, unknown>,
|
||||
reason: string | undefined,
|
||||
correlationId: string,
|
||||
): HousekeepingResult<HousekeepingCommandConfirmation> {
|
||||
const normalizedReason = reason?.normalize("NFC").trim() || undefined;
|
||||
if (command.requiresReason && !normalizedReason) {
|
||||
return fail("VALIDATION", "errors.housekeeping.validation", correlationId, {
|
||||
reason: ["errors.validation.required"],
|
||||
});
|
||||
}
|
||||
|
||||
return ok(
|
||||
{
|
||||
commandId: command.id,
|
||||
risk: command.risk,
|
||||
requiresReason: command.requiresReason,
|
||||
...(normalizedReason === undefined ? {} : { reason: normalizedReason }),
|
||||
},
|
||||
correlationId,
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { anyCapability, ok } from "../contracts";
|
||||
import {
|
||||
getHousekeepingCommand,
|
||||
type HousekeepingCommand,
|
||||
registerHousekeepingCommand,
|
||||
} from "./registry";
|
||||
|
||||
function command(
|
||||
id: string,
|
||||
owner: "people" | "system" = "people",
|
||||
): HousekeepingCommand<{ id: number }, { id: number }> {
|
||||
return {
|
||||
id,
|
||||
owner,
|
||||
risk: "safe",
|
||||
capability: anyCapability("admin.users.view"),
|
||||
input: z.object({ id: z.number().int().positive() }),
|
||||
requiresReason: false,
|
||||
rateLimit: { attempts: 4, windowMs: 30_000 },
|
||||
execute: async (context, input) => ok(input, context.correlationId),
|
||||
};
|
||||
}
|
||||
|
||||
describe("housekeeping command registry", () => {
|
||||
it("returns the exact command registered under its global ID", () => {
|
||||
const registered = command("people.registry.lookup");
|
||||
|
||||
registerHousekeepingCommand(registered);
|
||||
|
||||
expect(getHousekeepingCommand(registered.id)).toBe(registered);
|
||||
});
|
||||
|
||||
it("rejects a duplicate global command ID before it can shadow its owner", () => {
|
||||
registerHousekeepingCommand(command("people.registry.duplicate"));
|
||||
|
||||
expect(() =>
|
||||
registerHousekeepingCommand(command("people.registry.duplicate")),
|
||||
).toThrow("duplicate command id: people.registry.duplicate");
|
||||
});
|
||||
|
||||
it("rejects a command whose namespace disagrees with its declared owner", () => {
|
||||
expect(() =>
|
||||
registerHousekeepingCommand(
|
||||
command("people.registry.owner-mismatch", "system"),
|
||||
),
|
||||
).toThrow("command owner mismatch: people.registry.owner-mismatch");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
HOUSEKEEPING_DOMAIN_IDS,
|
||||
type HousekeepingDomainId,
|
||||
} from "../../migration/types";
|
||||
import type {
|
||||
CapabilityRequirement,
|
||||
HousekeepingCapabilityContext,
|
||||
HousekeepingResult,
|
||||
} from "../contracts";
|
||||
|
||||
export interface HousekeepingCommandContext {
|
||||
capability: HousekeepingCapabilityContext;
|
||||
correlationId: string;
|
||||
ipAddress: string;
|
||||
}
|
||||
|
||||
export interface HousekeepingCommand<I, O> {
|
||||
id: string;
|
||||
owner: HousekeepingDomainId;
|
||||
risk: "safe" | "sensitive";
|
||||
capability: CapabilityRequirement;
|
||||
input: z.ZodType<I>;
|
||||
requiresReason: boolean;
|
||||
rateLimit: { attempts: number; windowMs: number };
|
||||
execute(
|
||||
context: HousekeepingCommandContext,
|
||||
input: I,
|
||||
): Promise<HousekeepingResult<O>>;
|
||||
}
|
||||
|
||||
type RegisteredHousekeepingCommand = HousekeepingCommand<unknown, unknown>;
|
||||
|
||||
const approvedOwners = new Set<string>(HOUSEKEEPING_DOMAIN_IDS);
|
||||
const commands = new Map<string, RegisteredHousekeepingCommand>();
|
||||
|
||||
export function registerHousekeepingCommand<I, O>(
|
||||
command: HousekeepingCommand<I, O>,
|
||||
): void {
|
||||
validateCommand(command);
|
||||
if (commands.has(command.id)) {
|
||||
throw new Error(`duplicate command id: ${command.id}`);
|
||||
}
|
||||
|
||||
commands.set(command.id, command as RegisteredHousekeepingCommand);
|
||||
}
|
||||
|
||||
export function getHousekeepingCommand(
|
||||
id: string,
|
||||
): RegisteredHousekeepingCommand | undefined {
|
||||
return commands.get(id);
|
||||
}
|
||||
|
||||
function validateCommand<I, O>(command: HousekeepingCommand<I, O>): void {
|
||||
if (typeof command.id !== "string" || !command.id.trim()) {
|
||||
throw new Error("empty command id");
|
||||
}
|
||||
if (!approvedOwners.has(command.owner)) {
|
||||
throw new Error(`unknown command owner: ${command.owner}`);
|
||||
}
|
||||
if (!command.id.startsWith(`${command.owner}.`)) {
|
||||
throw new Error(`command owner mismatch: ${command.id}`);
|
||||
}
|
||||
if (
|
||||
!command.input ||
|
||||
typeof (command.input as { safeParse?: unknown }).safeParse !== "function"
|
||||
) {
|
||||
throw new Error(`command input schema missing: ${command.id}`);
|
||||
}
|
||||
if (
|
||||
!Number.isInteger(command.rateLimit.attempts) ||
|
||||
command.rateLimit.attempts <= 0 ||
|
||||
!Number.isInteger(command.rateLimit.windowMs) ||
|
||||
command.rateLimit.windowMs <= 0
|
||||
) {
|
||||
throw new Error(`invalid command rate limit: ${command.id}`);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user