0.3.0: the board sends its own sign-in email
The board sends its own sign-in email now.
Until this release it never could. SMTP_URL was declared in the config, read from the environment and threaded through both login routes, but nothing in the codebase could send mail. So config.smtpUrl === null was true on every instance that had ever run, every board took the "no mail server configured" path, and delivered in the API response was not a fact about a send.
If you are running 0.2.0 or earlier, upgrade. Because that condition was always true, POST /login rendered a working sign-in link into the page for whoever typed the address, and consumeMagicLink calls ensureUser, so the account did not need to exist first. Anyone could sign in as any address they could spell. The link now only ever reaches the server log, never the browser, on both the unconfigured path and the send-failed path.
Sending goes through Resend, via @profullstack/emailer (HTTP, no runtime dependencies). delivered now reports a send that happened: a provider refusing an unverified domain is not a delivery, and neither is having no provider, and the page says the board could not send instead of claiming an email is on its way.
A sign-in link asked for by a terminal now carries that terminal's device code, and the subject says "Approve your terminal". A device flow whose email cannot be matched against the request in front of you is one where a phished link approves someone else's session.
Config change
SMTP_URL is gone. Set RESEND_API_KEY instead, and MAIL_FROM to an address on a domain verified in that Resend account. MAIL_FROM now defaults to jobs@<PUBLIC_URL host> rather than jobs@localhost, because an unverified sender is refused whatever the key is. Leave RESEND_API_KEY unset and links go to the server log, which is what you want on a laptop.
This is why it is a minor and not a patch. Nothing that worked before stops working, since a board with SMTP_URL set was not sending anything, but the variable to set is a different one.
A board is not listed in its own directory
An instance that is also a directory names itself in DIRECTORY_URL, and announcing put it in its own listing. The guard sits in three places, because each has a different attacker: the heartbeat, agenticjobs announce, and POST /api/v1/directory/announce, which refuses its own origin from anyone. Guarding only the announcing end would let a third party list a board on its behalf.
Refusing new self-announcements does not remove a row an earlier one left, so the sweep drops it. A board that turns the guard on later heals itself rather than needing the row deleted by hand.
Tests
79 against a real Postgres, up from 65.