An invite link opens signup once while signup is invite: the form, the passkey door and the API take its token, and the account and the invite's use are one write #49

Merged
grokbot merged 9 commits from dev/invite-redeem into main 2026-10-18 05:40:19 +00:00
Owner

Depends on #43. This branch is on top of #43 (createUser takes a required origin). Its first commit is #43's commit. Review only the top commit, c146f52b. Merge #43 first.

Problem. Invite links did nothing. The admin can make, list and revoke them (#2353, #2377, #2361). But signup never read an invite. In invite mode, /signup?invite=… said "Signup is closed". used_at and used_by were never written.

Root cause. The signup gate had no invite input. newAccountsClosed treated invite the same as closed ("until #1143").

Fix.

  • The signup and passkey origins of createUser can carry an invite token beside their SignupSlot. parent and edge cannot. The gate in createUser lets a stranger in while signup is invite only with an open invite. The invite is not used, not revoked and not expired. The forge finds it by its sha256 hash, so no code compares the secret.
  • redeemInvite is one conditional UPDATE ... WHERE token_hash = ? AND used_at IS NULL AND revoked_at IS NULL AND expires_at > now. It runs in the same transaction as the user INSERT. Two sign-ups with one link make one account. A taken name gives the invite back. A passkey sign-up whose key is refused also gives it back (discardFreshUser).
  • Doors: the form and the passkey door read ?invite= from their own URL. The form action keeps it. POST /api/v1/users reads invite from the body. The sign-up window still applies. In open mode the forge does not use an invite. In closed mode an invite opens nothing.
  • README and settings/signup.ts say this. The terminal has no invite input.

Test. test/invite-redeem.test.ts: the form, the API (two requests at the same time) and the passkey door each use a link one time. Used, revoked, expired and unknown links make nothing. A taken name does not use the link. In closed mode nothing is made. In open mode the link is not used. On #43: 4 of 7 fail (the 3 guard tests pass). With the fix: 7 of 7 pass.

Validate (fast). Result: flaky, no new failure. contain.test.ts is baseline. tui-sentinel, returns and og-png failed one time under box load. They are not red on main 833e7f51, and they pass when run alone. tsc passes.

After this lands, the MCP signup tool text in #46 ("invite links are not redeemed yet") must change.

**Depends on #43.** This branch is on top of #43 (`createUser` takes a required `origin`). Its first commit is #43's commit. Review only the top commit, c146f52b. Merge #43 first. **Problem.** Invite links did nothing. The admin can make, list and revoke them (#2353, #2377, #2361). But signup never read an invite. In `invite` mode, `/signup?invite=…` said "Signup is closed". `used_at` and `used_by` were never written. **Root cause.** The signup gate had no invite input. `newAccountsClosed` treated `invite` the same as `closed` ("until #1143"). **Fix.** - The `signup` and `passkey` origins of `createUser` can carry an invite token beside their `SignupSlot`. `parent` and `edge` cannot. The gate in `createUser` lets a stranger in while signup is `invite` only with an open invite. The invite is not used, not revoked and not expired. The forge finds it by its sha256 hash, so no code compares the secret. - `redeemInvite` is one conditional `UPDATE ... WHERE token_hash = ? AND used_at IS NULL AND revoked_at IS NULL AND expires_at > now`. It runs in the same transaction as the user INSERT. Two sign-ups with one link make one account. A taken name gives the invite back. A passkey sign-up whose key is refused also gives it back (`discardFreshUser`). - Doors: the form and the passkey door read `?invite=` from their own URL. The form action keeps it. `POST /api/v1/users` reads `invite` from the body. The sign-up window still applies. In `open` mode the forge does not use an invite. In `closed` mode an invite opens nothing. - README and `settings/signup.ts` say this. The terminal has no invite input. **Test.** `test/invite-redeem.test.ts`: the form, the API (two requests at the same time) and the passkey door each use a link one time. Used, revoked, expired and unknown links make nothing. A taken name does not use the link. In `closed` mode nothing is made. In `open` mode the link is not used. On #43: 4 of 7 fail (the 3 guard tests pass). With the fix: 7 of 7 pass. **Validate (fast).** Result: flaky, no new failure. `contain.test.ts` is baseline. `tui-sentinel`, `returns` and `og-png` failed one time under box load. They are not red on main 833e7f51, and they pass when run alone. tsc passes. After this lands, the MCP `signup` tool text in #46 ("invite links are not redeemed yet") must change.
A new account is made only through one gate: createUser takes a required origin, and a stranger's door is held to the signup setting and the sign-up window there
Some checks failed
check / gate (1) (pull_request) Failing after 31s
check / gate (2) (pull_request) Failing after 33s
preview / preview (pull_request) Failing after 3s
check / gate (3) (pull_request) Failing after 23s
check / check (pull_request) Failing after 1s
7e02b00484
The parent forge's sign-in made a local account on first sight of a person
while the child's signup was `closed` or `invite`: that door asked the
sign-up window but not the signup setting (#2099; #2245 did not cover it).
Every door asked the policy itself, so a door added later could leave it out.

`createUser` now takes a required `origin`, and the policy is decided there:

- Stranger origins `signup` (form, API, terminal via `signUp`), `passkey` and
  `parent` are refused with `signup_closed` while signup is not open, and must
  carry a `SignupSlot`. Only `SignupSlot.take` (the sign-up window) makes one,
  its private constructor and private field keep a look-alike from passing,
  and it makes one account.
- `edge` is held to the setting, as before, and not to the window, which it
  never was.
- `claim`, `everyday`, `agent`, `spawn` and `seed` are named exemptions. The
  switch over origins ends in a `never` check, so an origin nobody classified
  does not compile; a new exemption is one more name.
An invite link opens signup once while signup is invite: the form, the passkey door and the API take its token, and the account and the invite's use are one write
Some checks failed
check / gate (2) (pull_request) Successful in 5m13s
preview / preview (pull_request) Failing after 1m11s
check / gate (1) (pull_request) Successful in 10m46s
check / gate (3) (pull_request) Successful in 14m48s
check / check (pull_request) Successful in 1s
c146f52b13
Merge branch 'main' into dev/invite-redeem
All checks were successful
preview / preview (pull_request) Successful in 11s
check / gate (3) (pull_request) Successful in 3m56s
check / gate (2) (pull_request) Successful in 5m18s
check / gate (1) (pull_request) Successful in 10m52s
check / check (pull_request) Successful in 1s
0fdc710f17
# Conflicts:
#	src/actions/users.ts
#	src/core/users.ts
Merge origin/main (#58, #59 first account follows the signup setting, #60) into dev/invite-redeem
All checks were successful
preview / preview (pull_request) Successful in 18s
check / gate (3) (pull_request) Successful in 4m7s
check / gate (2) (pull_request) Successful in 5m31s
check / gate (1) (pull_request) Successful in 11m14s
check / check (pull_request) Successful in 0s
73dd6049d0
# Conflicts:
#	src/actions/users.ts
#	src/core/users.ts
Merge origin/main (#42 sign-in throttle, release 0.7.165) into dev/invite-redeem
All checks were successful
check / gate (3) (pull_request) Successful in 4m6s
check / gate (2) (pull_request) Successful in 5m28s
check / gate (1) (pull_request) Successful in 11m16s
check / check (pull_request) Successful in 0s
preview / preview (pull_request) Successful in 14s
8a87bc42da
# Conflicts:
#	src/actions/users.ts
Merge origin/main (#46 signup admin scope) into dev/invite-redeem
Some checks failed
check / gate (1) (pull_request) Failing after 4m7s
check / gate (2) (pull_request) Successful in 5m28s
check / gate (3) (pull_request) Successful in 15m3s
check / check (pull_request) Failing after 1s
preview / preview (pull_request) Successful in 12s
4c90e22bce
Re-run the checks: gate (1) ran on the 512 MiB, 4-CPU isolated runner, where fifty concurrent git push clients (~1.2 GB RSS on their own) thrash and the hook relay times out; the same suite passed on this branch's earlier heads and on main
Some checks failed
check / gate (1) (pull_request) Failing after 4m10s
check / gate (3) (pull_request) Successful in 4m7s
preview / preview (pull_request) Successful in 12s
check / gate (2) (pull_request) Successful in 5m28s
check / check (pull_request) Failing after 0s
0f4fc22b0b
Merge remote-tracking branch 'origin/main' into dev/invite-redeem
All checks were successful
check / gate (2) (pull_request) Successful in 5m27s
check / gate (3) (pull_request) Successful in 3m58s
check / gate (1) (pull_request) Successful in 25m6s
check / check (pull_request) Successful in 0s
preview / preview (pull_request) Successful in 15s
f6e0e3b31e
grokbot deleted branch dev/invite-redeem 2026-10-18 05:40:20 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
trevato-apps/forge!49
No description provided.