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
Check · Review · Land done · Live — Landed; nothing deploys from this branch, so this is the end of its path.
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
Sign in to join this conversation.
new above
offline
The parent forge's sign-in made a local account on first sight of a person while the child's signup was
closedorinvite: 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.createUsernow takes a requiredorigin, and the policy is decided there:signup(form, API, terminal viasignUp),passkeyandparentare refused withsignup_closedwhile signup is not open, and must carry aSignupSlot. OnlySignupSlot.take(the sign-up window) makes one, its private constructor and private field keep a look-alike from passing, and it makes one account.edgeis held to the setting, as before, and not to the window, which itnever was.
claim,everyday,agent,spawnandseedare named exemptions. Theswitch over origins ends in a
nevercheck, so an origin nobody classified does not compile; a new exemption is one more name.Every caller of
createUseractions/users.tssignUp(signup form,POST /api/v1/users, terminal)signupactions/users.tssignUpWithPasskeypasskeycore/federation.tsmakeAccount(parent sign-in, first sight)parentcore/users.tsensureEdgeUseredgeweb/pages/claim.ts(one-time claim link)claimweb/pages/everyday.ts(an admin's second account)everydayactions/crew.tscreateAgent(admin only)agentactions/spawn.tsfounder and carried crew of a spawned childspawnchaos/world.ts, test fixturesseedtest/signup-gate-parent.test.tspins this list, so a new caller is added on purpose. An operator first-admin bootstrap would be one moreExemptOriginname and one morecase.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
signupClosedearly outs stay for their pages and messages.Order with #41 and a follow-up
#41 (operator first-admin bootstrap) calls
createUserwithout anorigin, so whichever lands second adds one line:"first-admin"inExemptOrigin, acaseinadmission, its call site in the list intest/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): onlyclaim,seedandfirst-adminmay 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 existingcreateUser(...)calls intest/, and directsignUp(...)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, onorigin/main(1051d451) and on this branch:before (origin/main): 1 pass, 3 fail
after (this branch): 4 pass, 0 fail
<!-- pipeline:validation -->
Validation
pipeline/validateon7e02b004: flaky in 469s — bun 1.3.14, CI-shaped env (scrubbed vars, runner PATH), exact check.yml shard + forge.json commands.bun install --frozen-lockfileforge.json check.fastcheck.yml gate (1) pull_requestcheck.yml gate (2) pull_requestcheck.yml gate (3) pull_requesttest output (failures, totals)