PR D: typed + signed event bus + events table + SSE (arch step 5) (#8)
Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
This commit was merged in pull request #8.
This commit is contained in:
@@ -5,6 +5,11 @@ import { validatePlan, checkStepDependencies } from "../services/planValidation"
|
||||
import { storePlan, getPlanById, updatePlanSignature, listPlans } from "../db/plans";
|
||||
import { asyncHandler, AppError, ErrorType } from "../services/errorHandler";
|
||||
import { getTransactionState, getTransitionHistory } from "../services/stateMachine";
|
||||
import {
|
||||
getEventsForPlan,
|
||||
subscribe as subscribeToEvents,
|
||||
verifyChain,
|
||||
} from "../services/eventBus";
|
||||
import type { Plan, PlanStep } from "../types/plan";
|
||||
|
||||
/**
|
||||
@@ -220,3 +225,82 @@ export const getPlanState = asyncHandler(async (req: Request, res: Response) =>
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/plans/:planId/events
|
||||
* Return the full signed + hash-chained event trail for a plan
|
||||
* (arch §4.5 State Registry + §7 Event Model + §14 Audit).
|
||||
*
|
||||
* Query `?verify=1` re-verifies the chain server-side and adds
|
||||
* { chain_valid: true|false, broken_at?: n } to the response.
|
||||
*/
|
||||
export const getPlanEvents = asyncHandler(async (req: Request, res: Response) => {
|
||||
const { planId } = req.params;
|
||||
const plan = await getPlanById(planId);
|
||||
if (!plan) {
|
||||
throw new AppError(ErrorType.NOT_FOUND_ERROR, 404, "Plan not found");
|
||||
}
|
||||
|
||||
const events = await getEventsForPlan(planId);
|
||||
|
||||
const body: {
|
||||
plan_id: string;
|
||||
count: number;
|
||||
events: typeof events;
|
||||
chain_valid?: boolean;
|
||||
broken_at?: number;
|
||||
broken_reason?: string;
|
||||
} = { plan_id: planId, count: events.length, events };
|
||||
|
||||
if (req.query.verify === "1") {
|
||||
const v = await verifyChain(planId);
|
||||
body.chain_valid = v.ok;
|
||||
if (!v.ok) {
|
||||
body.broken_at = v.brokenAt;
|
||||
body.broken_reason = v.reason;
|
||||
}
|
||||
}
|
||||
|
||||
res.json(body);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/plans/:planId/events/stream
|
||||
* Server-sent-events stream of live events for a single plan.
|
||||
*/
|
||||
export const streamPlanEvents = asyncHandler(async (req: Request, res: Response) => {
|
||||
const { planId } = req.params;
|
||||
const plan = await getPlanById(planId);
|
||||
if (!plan) {
|
||||
throw new AppError(ErrorType.NOT_FOUND_ERROR, 404, "Plan not found");
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
res.flushHeaders?.();
|
||||
|
||||
// Replay the history on connect so clients can reconstruct state
|
||||
// without a separate REST call.
|
||||
const history = await getEventsForPlan(planId);
|
||||
for (const e of history) {
|
||||
res.write(`id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(e)}\n\n`);
|
||||
}
|
||||
|
||||
const unsubscribe = subscribeToEvents(planId, (record) => {
|
||||
res.write(
|
||||
`id: ${record.id}\nevent: ${record.type}\ndata: ${JSON.stringify(record)}\n\n`,
|
||||
);
|
||||
});
|
||||
|
||||
const keepAlive = setInterval(() => {
|
||||
res.write(": keep-alive\n\n");
|
||||
}, 15_000);
|
||||
|
||||
req.on("close", () => {
|
||||
clearInterval(keepAlive);
|
||||
unsubscribe();
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user