Public: anyone can read this; nobody can change it.

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 #43

Merged opened this view on origin

Check · Review · Land done · Live — Landed; nothing deploys from this branch, so this is the end of its path.

Merged

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.

shipped merged

opened this view on origin

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.

Every caller of createUser

calleroriginheld to
actions/users.ts signUp (signup form, POST /api/v1/users, terminal)signupsetting + window slot
actions/users.ts signUpWithPasskeypasskeysetting + window slot
core/federation.ts makeAccount (parent sign-in, first sight)parentsetting + window slot
core/users.ts ensureEdgeUseredgesetting
web/pages/claim.ts (one-time claim link)claimexempt
web/pages/everyday.ts (an admin's second account)everydayexempt
actions/crew.ts createAgent (admin only)agentexempt
actions/spawn.ts founder and carried crew of a spawned childspawnexempt
chaos/world.ts, test fixturesseedexempt

test/signup-gate-parent.test.ts pins this list, so a new caller is added on purpose. An operator first-admin bootstrap would be one more ExemptOrigin name and one more case.

The doors take their slot where they asked the window before, so the 429s, their wording and their place in each request are unchanged; the door-level signupClosed early outs stay for their pages and messages.

Order with #41 and a follow-up

#41 (operator first-admin bootstrap) calls createUser without an origin, so whichever lands second adds one line: "first-admin" in ExemptOrigin, a case in admission, its call site in the list in test/signup-gate-parent.test.ts. A follow-up, stacked on this branch, makes sure no sign-up ever becomes the site admin (today a fresh forge's first sign-up does): only claim, seed and first-admin may carry admin flags. It needs #41 first, since without it a standalone forge would have no way to get an admin, and it needs its own test fixture work. It is not part of this PR.

Mechanical test churn

A required field reaches every fixture: origin: "seed" is added to the existing createUser(...) calls in test/, and direct signUp(...) fixtures pass { slot: fixtureSlot(app) } (test/helpers/slot.ts, one source per fixture so set-up does not spend a window a test is reading). No assertion changed.

Regression test, before and after

bun test test/signup-gate-parent.test.ts, the same file, on origin/main (1051d451) and on this branch:

before (origin/main): 1 pass, 3 fail
bun test v1.3.14 (0d9b296a)
test/signup-gate-parent.test.ts:
74 | const shut: [SignupMode, string][] = [["closed", "alice"], ["invite", "bea"]];
75 | for (const [mode, name] of shut) {
76 |   test(`signup ${mode}: a parent's person new to the child gets no account and no session`, async () => {
77 |     setSignup(child.db, mode, owner);
78 |     const done = await through(name);
79 |     expect(done.status).toBe(403);
                             ^
error: expect(received).toBe(expected)
Expected: 403
Received: 303
      at <anonymous> (test/signup-gate-parent.test.ts:79:25)
(fail) signup closed: a parent's person new to the child gets no account and no session [23.92ms]
74 | const shut: [SignupMode, string][] = [["closed", "alice"], ["invite", "bea"]];
75 | for (const [mode, name] of shut) {
76 |   test(`signup ${mode}: a parent's person new to the child gets no account and no session`, async () => {
77 |     setSignup(child.db, mode, owner);
78 |     const done = await through(name);
79 |     expect(done.status).toBe(403);
                             ^
error: expect(received).toBe(expected)
Expected: 403
Received: 303
      at <anonymous> (test/signup-gate-parent.test.ts:79:25)
(fail) signup invite: a parent's person new to the child gets no account and no session [4.12ms]
(pass) signup open: the same door makes the account [5.82ms]
112 |   return found;
113 | }
114 | 
115 | test("every caller of createUser says where its account comes from", async () => {
116 |   // A new way in is added here on purpose: a stranger's door, or a named exemption.
117 |   expect(await callers()).toEqual({
                                ^
error: expect(received).toEqual(expected)
  {
    "src/actions/crew.ts": [
-     "agent",
+     "(none)",
    ],
    "src/actions/spawn.ts": [
-     "spawn",
-     "spawn",
+     "(none)",
+     "(none)",
    ],
    "src/actions/users.ts": [
-     "signup",
-     "passkey",
+     "(none)",
+     "(none)",
    ],
    "src/chaos/world.ts": [
-     "seed",
+     "(none)",
    ],
    "src/core/federation.ts": [
-     "parent",
+     "(none)",
    ],
    "src/core/users.ts": [
-     "edge",
+     "(none)",
    ],
    "src/web/pages/claim.ts": [
-     "claim",
+     "(none)",
    ],
    "src/web/pages/everyday.ts": [
-     "everyday",
+     "(none)",
    ],
  }
- Expected  - 10
+ Received  + 10
      at <anonymous> (test/signup-gate-parent.test.ts:117:27)
(fail) every caller of createUser says where its account comes from [356.04ms]
 1 pass
 3 fail
 6 expect() calls
Ran 4 tests across 1 file. [731.00ms]
after (this branch): 4 pass, 0 fail
bun test v1.3.14 (0d9b296a)
test/signup-gate-parent.test.ts:
(pass) signup closed: a parent's person new to the child gets no account and no session [22.92ms]
(pass) signup invite: a parent's person new to the child gets no account and no session [3.59ms]
(pass) signup open: the same door makes the account [3.76ms]
(pass) every caller of createUser says where its account comes from [292.66ms]
 4 pass
 0 fail
 10 expect() calls
Ran 4 tests across 1 file. [644.00ms]

<!-- pipeline:validation -->

Validation

pipeline/validate on 7e02b004: flaky in 469s — bun 1.3.14, CI-shaped env (scrubbed vars, runner PATH), exact check.yml shard + forge.json commands.

stepresulttime
bun install --frozen-lockfileok (exit 0)0s
forge.json check.fastBASELINE (exit 1)128s
check.yml gate (1) pull_requestBASELINE (exit 1)19s
check.yml gate (2) pull_requestFLAKY (exit 1)17s
check.yml gate (3) pull_requestok (exit 0)305s
  • ⚠️ check.yml cannot check out fork commits: Actions on a fork PR fail at 'clone source at this ref' whatever the code (README: fork PRs)
  • ⚠️ node v20.19.2 here, v22 on CI (tsc runs under node)
  • ⚠️ check.yml gate (1) pull_request stopped early at a BASELINE failure (fail-fast): its later files were not run in this step
  • ⚠️ check.yml gate (2) pull_request stopped early at a FLAKY failure (fail-fast): its later files were not run in this step
test output (failures, totals)
▶ bun install --frozen-lockfile
  ok  exit 0  0s
▶ forge.json check.fast
(fail) egress, reported: an undeclared public host goes through and is counted; a private one never does > an undeclared public host is let through and counted [1.12ms] [shard 10]
Ran 3530 tests across 350 files in 96.3s; 1 failed.
classify: reran test/contain.test.ts test/crew-network.test.ts test/variants-surfaces.test.ts  alone on origin/main 1051d451 and on this branch
  BASELINE  exit 1  128s
▶ check.yml gate (1) pull_request
(fail) egress, reported: an undeclared public host goes through and is counted; a private one never does > an undeclared public host is let through and counted [3.70ms] [shard 3]
Ran 110 tests across 3 files in 18.0s; 1 failed.
classify: reran test/contain.test.ts  alone on origin/main 1051d451 and on this branch
  BASELINE  exit 1  19s
▶ check.yml gate (2) pull_request
(fail) a fork that follows its parent > a proposal names only what is actually ahead: work the parent already has is not listed again [396.12ms] [shard 3]
Ran 61 tests across 3 files in 12.2s; 1 failed.
classify: reran test/following.test.ts  alone on origin/main 1051d451 and on this branch
classify: not red on main:
  FLAKY  exit 1  17s
▶ check.yml gate (3) pull_request
Ran 1525 tests across 143 files in 304.8s (143 shards).
  ok  exit 0  305s
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs