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

Something that newly needs you reaches your phone: a notify URL fed from the event stream #354

Merged opened this

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

Merged

A person can now set one notify URL on Settings → Profile. When something newly asks them, the forge POSTs one plain-text line and a link to the asks screen (`/`) to that URL. It does not yet cover "a health problem for an admin": nothing on the event stream signals one, so I left it out and noted it in `docs/APPS.md`.

shipped production is serving it deploy 7b21267

opened this

A person can now set one notify URL on Settings → Profile. When something newly asks them, the forge POSTs one plain-text line and a link to the asks screen (/) to that URL. It does not yet cover "a health problem for an admin": nothing on the event stream signals one, so I left it out and noted it in docs/APPS.md.

I ran test/notify.test.ts (8 tests), plus studio, event-audience and keys, all green. bun test for the whole repo did not finish within the tool timeout, and bun x tsc --noEmit can't run in this sandbox because the bun types aren't installed. The forge's own check, which includes the typecheck, therefore runs for the first time on this push.

  • What triggers it: the new src/crew/notify.ts listens to the event stream. After run.finished, pull.opened, pull.pushed, pull.reviewed, pull.merged, issue.closed, issue.commented or repo.settings_updated, it re-reads what asks each person who has a URL set.
  • What counts as an ask: a pull the crew stopped on, a green pull waiting for a look, or a repository paused at its ceiling. It reads them with the asks panel's own candidate logic, as that person, via a new askLinesFor in src/web/studio/asks.ts. Private-repository titles therefore only reach someone who can read that repository. askCandidatesFor now takes a database and a user instead of a request context, so the notifier can reuse it.
  • No repeats: an ask is told once. It becomes new again only after it has gone away and come back. A restart, or saving the URL, starts from what asks now, so nothing floods.
  • Limits: at most 10 a hour per person. A failing endpoint pauses that person's notifications for a minute, doubling up to an hour. A line that can't be sent is dropped, never retried or queued, and the work runs off the event stream.
  • The URL: it must be https:, with no credentials, at most 200 characters, and no loopback, private, link-local, .local, .internal, bare or IPv6-literal host. The name is resolved and checked again when sending, and redirects are not followed. It is stored in the existing user_setting table, so there is no migration. It is never logged (only the host is), and the account page shows only the host. Saving it with an empty field turns it off.
  • Wiring: POST /settings/notify is a same-origin, admin-scope form post. src/main.ts starts and stops the notifier. The README gets no section; the feature is documented in docs/APPS.md.
  • Tests: with a stub sender, a green pull sends one POST and the same pull again sends none, and a user who can't read the repository gets nothing. A failing endpoint is paused and not retried, the hourly cap holds, and http:, credentials, private hosts and over-long URLs are refused. The page shows only the host, and cross-origin posts are refused.

Left out on purpose: DNS-rebinding between the send-time check and the connect (the same limit the migrate path has) and retrying a failed send. The "told" state is held in memory only, which is why a restart re-baselines.

🤖 Generated with Claude Code

Closes #347

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs