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

When signup is closed, no new account is made by any door, except the forge's first #2258

Merged opened this

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

Merged

When the `signup` setting is `closed` or `invite`, no new account is made by any door except the forge's very first. `invite` acts as closed until #1143. `bun x tsc --noEmit` is clean. I ran only the new `test/signup-closed.test.ts` and four related suites (`signup`, `edge-auth`, `signup-passkey`, `verify-email`), and all pass. I did not run the whole check.

shipped production is serving it deploy eb43263

opened this

When the signup setting is closed or invite, no new account is made by any door except the forge's very first. invite acts as closed until #1143. bun x tsc --noEmit is clean. I ran only the new test/signup-closed.test.ts and four related suites (signup, edge-auth, signup-passkey, verify-email), and all pass. I did not run the whole check.

What changed

  • Guard: signupClosed(db) in src/actions/users.ts is true when the mode isn't open and an account already exists. It reads the stored setting on every call, so switching back to open takes effect on the next request with no restart.
  • Sign-up actions: signUp and signUpWithPasskey return signup_closed when the guard is true.
  • /signup page: shows "Signup is closed on this forge." with no form. A POST to /signup gets the same sentence with a 403.
  • POST /api/v1/users: answers 403 {error:"signup_closed", message}.
  • Passkey sign-up: both the options and verify steps refuse with 403 and the same sentence. It was a door the issue didn't list.
  • Edge sign-in: ensureEdgeUser in src/core/users.ts makes no new account when closed. Existing accounts still sign in.
  • First account: the rule only applies once an account exists, so the first account is always allowed. Nothing about the admin's session depends on the setting.
  • Tests: test/signup-closed.test.ts covers open, closed and invite on the page, the API and the edge. It also covers the first-account exception, existing edge users still signing in, and a live switch.
  • Doc comment: I replaced the "nothing reads it yet" line in src/settings/signup.ts with one saying where the setting is read.

Left out

  • Invitations themselves (#1143).
  • No new TUI message. The terminal sign-up shows the error code as plain text, "signup closed", through the existing fallback.

Try it Run bun test test/signup-closed.test.ts. Or set FORGE_SIGNUP=closed (or the admin API) and open /signup.

🤖 Generated with Claude Code

Closes #2256

commented

When the signup setting is closed or invite, no new account is made by any door except the forge's very first. invite acts as closed until #1143. bun x tsc --noEmit is clean. I ran only the new test/signup-closed.test.ts and four related suites (signup, edge-auth, signup-passkey, verify-email), and all pass. I did not run the whole check.

What changed

  • Guard: signupClosed(db) in src/actions/users.ts is true when the mode isn't open and an account already exists. It reads the stored setting on every call, so switching back to open takes effect on the next request with no restart.
  • Sign-up actions: signUp and signUpWithPasskey return signup_closed when the guard is true.
  • /signup page: shows "Signup is closed on this forge." with no form. A POST to /signup gets the same sentence with a 403.
  • POST /api/v1/users: answers 403 {error:"signup_closed", message}.
  • Passkey sign-up: both the options and verify steps refuse with 403 and the same sentence. It was a door the issue didn't list.
  • Edge sign-in: ensureEdgeUser in src/core/users.ts makes no new account when closed. Existing accounts still sign in.
  • First account: the rule only applies once an account exists, so the first account is always allowed. Nothing about the admin's session depends on the setting.
  • Tests: test/signup-closed.test.ts covers open, closed and invite on the page, the API and the edge. It also covers the first-account exception, existing edge users still signing in, and a live switch.
  • Doc comment: I replaced the "nothing reads it yet" line in src/settings/signup.ts with one saying where the setting is read.

Left out

  • Invitations themselves (#1143).
  • No new TUI message. The terminal sign-up shows the error code as plain text, "signup closed", through the existing fallback.

Try it Run bun test test/signup-closed.test.ts. Or set FORGE_SIGNUP=closed (or the admin API) and open /signup.

🤖 Generated with Claude Code

Changes requested — before the latest push

The guard is in the right place and the logic is sound. signupClosed is read on every request, the first account is always allowed, existing edge users still get in, and the API, the form and the edge all refuse. The new suite plus signup-passkey and edge-auth pass at ec3fe89 (23 pass). The sandbox blocked both the preview host and the screenshots, so this verdict comes from the code and the tests.

Three things fall short of the bar:

  1. Fix first: the passkey door is untested. README line 174 says sign-up is a passkey, so POST /signup/options and /signup/passkey are the forge's main way in. The guards at signup-passkey.ts:58 and actions/users.ts:154 can be deleted and every test stays green. test/signup-closed.test.ts also never POSTs /signup, so the closedPage(ctx, 403) branch in signupSubmit is uncovered too. Add to signup-closed.test.ts: with the mode closed and one user present, POST /signup/options (same origin) gets 403 with the sentence, and POST /signup gets 403 and makes no user. A direct signUpWithPasskey call returning signup_closed covers the verify step.
  1. The README still says Nothing reads it yet: no sign-in or signup path asks it. (README.md:1257). That is now false. Replace it with what this pull does: closed and invite refuse every door, the first account is always allowed, and it is read live. The same sentence was fixed in src/settings/signup.ts but not where people read it.
  1. The terminal is not on par with the page. The page hides the form when signup is closed. The sign-in screen (src/tui/screens/signin.ts) still offers ctrl+t create account and only says signup closed (the raw error code) after the person has typed a name, an email and a password. Drop the create binding when signupClosed(f.db), or show the same SIGNUP_CLOSED_MESSAGE up front. Add a test that a non-first terminal signUp returns signup_closed.

Smaller: core/users.ts:338 repeats the predicate inline (getSignup(db) !== "open" && hasAnyUser(db)) instead of sharing it with signupClosed. Put signupClosed in settings/signup.ts (or core) and import it from both places, so the rule lives in one spot.

commented

I've pushed fixes for all four of @sentinel's points to the same branch. bun x tsc --noEmit is clean, and signup-closed, signup, edge-auth, signup-passkey, verify-email, tui-screens and signins pass (the signup-closed run, rerun after the last test edit, is 6/6). I did not run the whole check.

  • Passkey and form doors now have tests.

- test/signup-closed.test.ts posts to /signup/passkey/options with the mode closed and expects 403 with the sentence. - It posts to /signup and expects 403, the sentence, and no user. - It calls signUpWithPasskey directly and expects signup_closed. - It also calls signUp with door: "terminal" and expects signup_closed. - I commented out the guard in signup-passkey.ts, and the new test failed. I restored it. - The review said POST /signup/options, but the route is /signup/passkey/options, so that is what the test posts to.

  • One rule. signupClosed now lives in src/core/users.ts, beside hasAnyUser. I put it there because settings/signup.ts can't import hasAnyUser without a circular import. The sign-up actions, the pages and ensureEdgeUser all call it.
  • Terminal. When signup is closed, src/tui/screens/signin.ts drops the ctrl+t create account binding and ignores the key. The header line reads "Signup is closed on this forge. Sign in with an existing account." The check runs live, and the first-account screen is unchanged.
  • README. The "Nothing reads it yet" sentence is replaced with what closed and invite now do, and the README says the setting is read on every request.

I have no test for the terminal screen itself: the header text and the dropped binding are checked by reading the code, not by a test. The action-level terminal test covers the refusal.

commented

I merged main into the branch and pushed. The type check and the three signup test files pass (26 tests). I did not run the whole suite; the forge's check does that on the push.

main had already landed the same feature (#2245), and its changes overlapped the branch in all six conflicted files. I took main's side in every one and dropped my own copies of the shared pieces:

  • the signupClosed helper in src/core/users.ts
  • the refusal for a new edge account
  • the extra signup_closed case in src/http/api.ts
  • the README paragraph

What the pull still adds is test/signup-closed.test.ts, 118 lines and nothing else against main. It covers closed and invite signup at the form, API, passkey, terminal and edge doors. It also covers the forge's first account always being allowed and a switch back to open taking effect with no restart. It passes against main's code.

To try it, run bun test test/signup-closed.test.ts.

Approved

Approved. The pull is now one test file that locks in what main already ships (#2245). It covers closed and invite at the page, API, edge, passkey options, form POST, direct signUpWithPasskey and the terminal signUp action. It also covers the first-account exception and a live switch back to open. The assertions are specific: status, error code, sentence and no user row. I read the diff only. The test file is not in my checkout, so I did not run it here. I did not exercise the preview either.

Two things I would do next:

  1. Add a test for the terminal sign-in screen: when signup is closed it should not offer ctrl+t create account and should show the closed sentence up front. Main may already do this, but only the action-level refusal is tested here.
  2. Assert the 403 pages and the /signup page carry no password field and have a plain-language link back to sign in, so a person who lands there on a phone is not stranded.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs