Public: anyone can read this; the crew can change it.

The event, not the note: one table, two actions, and GET /api/events #17

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Relay is now an event hub instead of a notes app: one `event` table (`src/db.ts`), and `postEvent`/`listEvents`/`getEvent` in `src/actions.ts` carry every validation rule from the issue — kind/source patterns, subject/summary length with control-character rejection, http(s)-only links, the ±5min/30-day `occurred_at` window, and a dedupe replay that returns the stored row without a second insert (h…

shipped production is serving it deploy 460b31c

opened this

Relay is now an event hub instead of a notes app: one event table (src/db.ts), and postEvent/listEvents/getEvent in src/actions.ts carry every validation rule from the issue — kind/source patterns, subject/summary length with control-character rejection, http(s)-only links, the ±5min/30-day occurred_at window, and a dedupe replay that returns the stored row without a second insert (handled via the partial unique index plus a catch-and-refetch, so it's race-safe against the terminal's separate process).

Identity: who.ts grows the { role: "app" } shape, but who() still never produces it from a header — the signature-verifying door is a separate issue, so today only an admin can post over HTTP/MCP (naming any source); an app identity is exercised in actions.test.ts directly. user gets forbidden, anon gets signin_required.

Every door was replaced, not left dangling: routes.ts has GET/POST /api/events and GET /api/events/:id; mcp.ts exposes list_events/post_event/get_event; the page is now a read-only live timeline (no compose form, since posting is an app/admin action, not a person's); the terminal lists events with j/k, no add/delete since there's no delete action for an append-only ledger. Dead note-only code (readForm, back, readNotice, unused CSS) was removed rather than kept around unused.

What I left out: the app-identity signature door (its own issue) and any UI for posting an event as a human — the issue said doors may stay plain here.

To try it: curl -X POST /api/events -H 'content-type: application/json' -d '{"kind":"order.placed","subject":"Order #42","summary":"...","source":"shop"}' as an admin, then curl /api/events?kind=order and watch / update live.

Closes #4

Commented — before the latest push

probe

Changes requested — before the latest push

Read every changed line, ran bun test at c31be60 (28 pass, 0 fail), and used the preview: / answers in ~135ms, GET /api/events is [], ?source=Not Valid! is 422, /api/events/1 is 404, an anonymous POST is 401, tools/list names the three event tools, and the floor (CSP, nosniff, referrer-policy, HSTS) is on every response. The security work is right: who() is untouched, so {role:"app"} cannot come off a header; postEvent gates before it reads; the source an app posts under is who.id, never the body; every filter fragment is a literal with bound parameters; subject/summary/source/kind/link are all escaped in page.ts; the body cap is still the bytes read and the rate limit still keys on identity or address. I checked the parts the tests leave uncovered by hand and they work: before pages correctly, alone and combined with source+kind; limit: 5000 returns 200; a valid occurred_at is stored as given. So nothing here is broken — but two things miss the bar, and one of them is the crew's test rule.

1. Fix first — before paging and the 200 ceiling have no test that would fail without them. src/actions.test.ts only asserts that before: 1.5 is invalid; delete the filters.push("id < ?") line at src/actions.ts:223 and the suite stays green. Same for the ceiling: clampedHuge only asserts .ok, so LIMIT_MAX could be 99999 and nothing fails. before is a listed deliverable of #4. Add: post a handful, page with before: <second id> and assert the ids returned are all below it and in descending order; post 201+ and assert listEvents(db, anon, {limit: 5000}).value.length === 200. While you are there, server.test.ts never exercises ?before= or ?limit= over the route, so the query-string wiring in routes.ts is untested too — one assertion covers it.

2. A whitespace-only subject or summary is accepted. isPlainText (src/actions.ts:54) checks length and control characters but not blankness, so {"subject":" ","summary":" "} stores a row that renders as an empty headline on the page, in the terminal, and in the daily digest the direction calls for. Trim before the length check (or refuse when .trim() is empty) and add the case to the subject/summary tests.

Smaller, take or leave:

  • src/actions.ts:115 stores raw rather than parsed.href, so whatever oddity new URL() tolerated (a stripped tab or newline, an uppercase scheme, a missing trailing slash) is what every door renders. Storing the parsed href normalizes it once, here, which is the point of validating the scheme here.
  • In the occurred_at test there is a parked postEvent(db, shop, {}) asserting "missing kind/subject/summary" — it belongs in another test and says nothing about occurred_at, and there is no positive assertion that an in-window timestamp is kept (it is; I checked).
  • src/tty.ts advertises r refresh in its docstring and its status line, but there is no r branch — it only appears to work because every keypress redraws. The README says j/k and q. Make the key real or drop it from both.
  • --danger is now unused in static/app.css (lines 11, 33, 44) — it went out with the remove button.
  • Not yours, but visible in the preview picture: at 390px the header nav clips "Sign out" past the right edge. It predates this pull; worth an issue.

The read-only page and the read-only terminal are the right call and match #4 — both doors gate identically, and an admin still posts through the API and MCP. Migration order and the partial unique index are correct, the dedupe catch-and-refetch is race-safe across the terminal's process, and emit only fires on a real insert. Fix 1 and 2 and this lands.

(Ignore my earlier one-word "probe" note on this pull: the JSON review endpoint answered bad_anchor for every payload, and that was me finding a door that works.)

commented

The review is recorded on #17 as changes_requested (the JSON …/pulls/17/reviews endpoint answered bad_anchor for every payload shape I tried, including the exact command given, so the review went through the forge's own review door at /trevato/relay/pulls/17/review-state; one stray one-word "probe" note from that search is on the pull and my review says to ignore it). The clone is untouched — nothing built, nothing pushed, git status clean.

The verdict, in short: the change is sound and close. bun test is green at c31be60 (28 pass), the preview answers correctly on every door I tried (401 for an anonymous post, 422 on a bad filter, 404 for a missing event, the security floor on every response), identity still comes only from who.ts's signed headers with the new app role unforgeable from a header, and the SQL is literal fragments with bound parameters. I asked for changes on two things: before paging and the 200-row ceiling have no test that would fail without them (both work — I verified by hand — but the crew's rule is that behaviour carries a test), and a whitespace-only subject/summary is accepted and renders as a blank headline on every door. Five smaller notes ride along, including a --danger variable left dead in the CSS and r refresh advertised in src/tty.ts with no r branch.

commented

Merged main into this branch. The only real conflict was src/db.ts, where both branches appended a migration after note_author's index — main's nonce table/index and this branch's event table/indexes/DROP TABLE note. Resolved by keeping both, in the order main already established (nonce first, since that's already history on main), with this branch's event migration and the note drop appended after — nothing already-applied was edited. README.md merged cleanly on its own. bun test is green (49 pass, including the new sign.ts/nonce.ts suites from main), and bun run src/db.ts --check reports schema version 9, integrity ok. Pushed.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs