Last-Event-ID resumability, now built as a bounded replay; the origin and auth tests are merge-blocking for anyone who is not the repository owner — main is branch-protected on the ci check, with enforce_admins: false deliberate for a single-maintainer repository — see the as-built updates below)concept-analysis-v2.md §3, row CD-5
(consolidates AD5)Verdict: holds, built as decided. SSE is the transport; no WebSocket dependency exists anywhere in the tree.
/api/stream is a hijacked Fastify route writing text/event-stream with
cache-control: no-cache, no-transform and a periodic heartbeat comment, fanned
out by a RealtimeHub.Origin on
/api/stream is rejected with 403 whether or not a valid token was presented — the
browser attack surface closes before the token is even examined. There is no
wildcard CORS.request.routeOptions.url), not the
raw request URL, specifically so a percent-encoded path like /%61pi/health cannot
slip past a prefix check that the router would then decode back to /api/.Origin yields 403 with and without a valid token, and the four
wrong-token shapes return byte-identical 401 bodies, so neither check leaks an
oracle.One acceptance item is not built as written: WP-U1 calls the endpoint “resumable.”
The server emits a retry: directive so a dropped client reconnects, but there is no
Last-Event-ID replay — a reconnecting client resubscribes to the live feed rather
than being caught up on frames it missed. For a liveness channel whose durable facts
all live in the database this is a small gap, but it is a gap, not a completed
criterion.
Verdict: holds, with one word corrected. SSE is still the only realtime transport in
the tree, no WebSocket dependency has appeared, and the security gate would fail the build
if one did (ADR-0009). The negative catalogue
still asserts a 403 on a foreign Origin with and without a valid token, and
byte-identical 401 bodies across the four wrong-token shapes, so neither check leaks an
oracle.
Calling those assertions merge-blocking was wrong when written and is now only
partly right. They run in CI on every push and pull request and fail the run; since
2026-08-25 main is branch-protected on the ci check, so that failure withholds a
merge from a contributor, while the owner stays exempt by design
(enforce_admins: false) — see
the standing correction.
The Last-Event-ID gap is unchanged: the server still emits only a retry: directive, a
reconnecting client still resubscribes to the live feed instead of being caught up on the
frames it missed, and WP-U1’s “resumable” is still not met. It has been open long enough
now to be worth naming as a decision rather than an oversight — nobody has chosen to build
replay, and nobody has chosen to drop the word from the work package either.
The dashboard needs a live, server→browser feed so the status board and DAG views update
without polling. docs/ai/DESIGN.md §3’s original architecture diagram names the transport
loosely as “WebSocket/SSE,” leaving the choice open. A bidirectional channel (WebSocket) carries
a larger same-origin/attack-surface burden than a strictly one-directional one, and this project’s
security posture treats attack-surface minimization as a first-class constraint
(docs/ai/DESIGN.md §8), not an afterthought.
Transport is Server-Sent Events (SSE), with same-origin enforcement live from Phase 1.
The feed is server→browser-only; WebSocket is revisited only if a genuine bidirectional
control need ever arises — it does not today. This settles the transport choice docs/ai/DESIGN.md
§3 left open in favor of SSE specifically.
From concept-analysis-v2.md §6 (“Security, build-failing, from Phase 1”) — the CD-5/CD-7
shared acceptance surface:
Supporting evidence from development-plan.md WP-U1 (“RealtimeHub SSE endpoint”): “A
cross-origin Origin on /api/stream is rejected; no wildcard CORS.”
docs/ai/DESIGN.md §8).development-plan.md WP-U1 (RealtimeHub SSE endpoint, same-origin,
auth-gated, resumable). See the security model and
architecture overview.docs/ai/DESIGN.md §3’s diagram originally left open alongside
SSE. Rejected for now: no proven bidirectional-control need exists, and a bidirectional
channel carries a strictly larger same-origin/attack-surface burden to secure correctly.concept-analysis-v2.md §6, “Product / business”).As-built addendum 2026-09-26 — the open item closed. RealtimeHub keeps the last 256
frames and /api/stream replays every buffered frame after the request’s Last-Event-ID
(which EventSource sends on its own reconnect) before the client joins live fan-out, in one
synchronous step. Transport, origin check and auth gate are unchanged. Bounded, stated: a
client gone longer than the window, or reconnecting across a server restart (ids restart at
1), still misses frames, and the dashboard still reports the id gap it can see.