79 lines
2.1 KiB
TypeScript
79 lines
2.1 KiB
TypeScript
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}`);
|
|
}
|
|
}
|