rastrillo / idear Public

The invitation page should start the invitee's sign-in Question open

On branch invitation-signs-in

The problem

An invitee follows the link in their invitation email and lands on a page that says "Sign in with the email address this invitation was sent to and you'll be let straight in", with a Sign in button. That leads to the app's general sign-in page — passwords, passkeys, "email me a link" — for a person who has none of those, and nothing on it says whether they should sign in or sign up. Watched live on jam it read as broken.

What must not change

idear separates invitation from identity on purpose, and rightly: an invitation link is a week-long secret that sits in an inbox, gets forwarded to colleagues and lands in link-scanner logs, so following it must prove nothing by itself. Admission happens through Roster.Authorize when a verified address signs in, and that stays exactly as it is. (A first cut of this branch let the page sign the visitor in directly; that made the invitation link a credential, and was wrong.)

The change (branch invitation-signs-in)

  • HandlerConfig.Begin func(w, r, address string) error, optional. When set, POST /invitations/{token} from a visitor with no session looks up the pending invitation and hands the invited address to the hook, which starts the app's own sign-in for it — sends its sign-in link, or the keymail ceremony — and owns the response. Whoever holds the inbox follows that link and is admitted, through Authorize, as always. Whoever merely holds the invitation link sees "an email was sent" to an inbox they cannot read. The token is not touched by the press.
  • InvitationPage.Join bool: the hook is set and the viewer holds no session, so the page shows one button. The address stays off the page; TestInvitationPageDoesNotDiscloseTheAddress still holds.
  • Apps without the hook keep the old contract exactly (the "sign in first" 403 path is untouched). TestInvitationBeginsTheInviteesSignIn covers the new path: the hook gets the invited address, nobody is admitted by the press, the token stays pending and is redeemed by the sign-in.

The GET stays a page with a button rather than acting on the GET, so a mail scanner following links does nothing.

jam's side (lazyatom/jam PR #43): the hook feeds the invited address into the same link flow every sign-in uses, so the join link is fifteen minutes and single use; the invitee gets a "check your email" page that names no address and offers to resend; an expired join link offers another; and the first sign-in lands on jam's set-up page, where a passkey or an authenticator app is required before the inbox opens.

Also on the branch: a removed member is restored, not re-invited

Found while testing the flow above: inviting an address that belongs to a deactivated member created an invitation that could never be redeemed, because admission refuses a deactivated member whatever they hold (readmission is Restore, by an admin). The invitee found out at the end of the link, as a bare "This address is verified but not admitted here." Roster.Invite now refuses up front with ErrRemovedInvitee (an ErrInvalid), and the members page says to restore them instead. The two tests that invited a removed member to prove the later refusal now invite first and remove after, which is the case that refusal exists for.

Note on the branch

It sits on 19a26b8 "Follow rastrillo to amadan.net/rastrillo/rastrillo", which is not yet on main here, so the push carries both commits.

---

Where this came from, and what we are asking

This was written while building deeper authentication into jam, a rastrillo app. The branch is pushed as invitation-signs-in, and it has been run against a live instance rather than only asserted about in tests — "watched live on jam it read as broken", above, is literal.

We are not asking for it to be merged as it stands. We are asking whether the shape is right, and whether idear wants to own this at all. The hook is deliberately small and optional: an app that sets nothing keeps today's behaviour exactly. If you would rather the app solved this on its own side, that is a fine answer and we will do it in jam instead.

The two parts most worth disagreeing with are the security argument — that following an invitation link must still prove nothing by itself — and whether Begin is the right seam, or whether something narrower would do.