No sign-up makes the site admin, not even a forge's first account: only the operator's first-admin code, a claim or a seed makes one #57

Merged
grokbot merged 2 commits from grokbot/forge:fix/no-signup-admin into main 2026-10-18 00:59:02 +00:00
Owner

AdminOrigin is claim, first-admin and seed. createUser takes the
admin flags only from one of those: they are not in any other origin's
input type, and createUser drops them for any other origin at run time.
So the signup form, the API, the passkey door, the terminal, the parent
forge's sign-in and the platform edge each make a person who is not an
admin, even on a forge with no admin at all. A standalone forge's admin
comes from the operator's --first-admin code (#41); a spawned child's
from its claim link.

With no sign-up an admin, welcome no longer proves an admin's address by
being one, and the edge's first account is not proven by being first:
both start unproven and follow the link like anyone else.

Test suites whose admin was their first sign-up make it with
test/helpers/admin.ts (seedAdmin, or adminCookie for a signed-in
browser) instead. test/no-signup-admin.test.ts fails on main: the API,
passkey, terminal and edge doors each made the first account an admin.

Why

Trevor's rule (2026-10-17): a Forge site admin comes only from the operator's first-admin link (#41, merged), a spawned child's claim handover, or the seed origin tests use, never from any signup path, even when the forge has no admin. This PR depends on #41, which is already on main: without it a standalone forge would have no way to make its admin.

What changed

  • src/core/users.ts: AdminOrigin = claim | first-admin | seed. Only those origins' inputs carry isAdmin/adminIfFirst (a type error anywhere else), and createUser drops them at run time for every other origin.
  • The signup, passkey and edge doors no longer ask for adminIfFirst. welcome and ensureEdgeUser no longer mark an account proven for being the first/admin, so every sign-up follows the email link.
  • test/helpers/admin.ts (seedAdmin, adminCookie): the 82 suites whose admin was their first sign-up make that same account as seed instead. The tests that asserted the old rule now assert the new one.
  • test/no-signup-admin.test.ts: on main 18917b93 it fails 4 of 6 (API, passkey door, terminal and edge each made the first account an admin). On this branch it passes 6/6.
  • Docs: README, ARCHITECTURE, APPS.

Beyond validate, every test file was also run on its own on this head (525 files, CI-shaped env, bun 1.3.14). All are green except contain.test.ts (DNS), crew-interrupted.test.ts and snapshot-destination.test.ts, and those three fail the same way alone on main on this box.

Left for a decision

Not changed here: while signup is closed/invite, a fresh forge still lets in its first account (newAccountsClosed), and the terminal still skips Turnstile for that first account. Both were there so the owner could sign up as the admin. Now all they do is let a stranger make an unproven guest before the operator gets there. Closing them is a small follow-up if Trevor wants it.

Validation

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

step result time
bun install --frozen-lockfile ok (exit 0) 0s
forge.json check.fast BASELINE (exit 1) 137s
check.yml gate (1) pull_request BASELINE (exit 1) 15s
check.yml gate (2) pull_request ok (exit 0) 273s
check.yml gate (3) pull_request ok (exit 0) 184s
  • ⚠️ 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
test output (failures, totals)
▶ bun install --frozen-lockfile
  ok  exit 0  0s
▶ forge.json check.fast
(fail) reviews, threads and the merge gate > require-review refuses the merge until the head is approved [309.29ms] [shard 9]
(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.47ms] [shard 10]
Ran 3146 tests across 315 files in 133.4s; 2 failed.
classify: reran test/contain.test.ts test/pull-mergeable.test.ts test/tui-workflows.test.ts  alone on origin/main 098fafdf and on this branch
  BASELINE  exit 1  137s
▶ 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 [2.42ms] [shard 3]
Ran 110 tests across 3 files in 14.4s; 1 failed.
classify: reran test/contain.test.ts  alone on origin/main 098fafdf and on this branch
  BASELINE  exit 1  15s
▶ check.yml gate (2) pull_request
Ran 1455 tests across 144 files in 273.3s (144 shards).
  ok  exit 0  273s
▶ check.yml gate (3) pull_request
Ran 1541 tests across 145 files in 183.4s (145 shards).
  ok  exit 0  184s
`AdminOrigin` is `claim`, `first-admin` and `seed`. createUser takes the admin flags only from one of those: they are not in any other origin's input type, and createUser drops them for any other origin at run time. So the signup form, the API, the passkey door, the terminal, the parent forge's sign-in and the platform edge each make a person who is not an admin, even on a forge with no admin at all. A standalone forge's admin comes from the operator's `--first-admin` code (#41); a spawned child's from its claim link. With no sign-up an admin, `welcome` no longer proves an admin's address by being one, and the edge's first account is not proven by being first: both start unproven and follow the link like anyone else. Test suites whose admin was their first sign-up make it with `test/helpers/admin.ts` (`seedAdmin`, or `adminCookie` for a signed-in browser) instead. `test/no-signup-admin.test.ts` fails on main: the API, passkey, terminal and edge doors each made the first account an admin. ### Why Trevor's rule (2026-10-17): a Forge site admin comes only from the operator's first-admin link (#41, merged), a spawned child's claim handover, or the `seed` origin tests use, never from any signup path, even when the forge has no admin. This PR depends on #41, which is already on main: without it a standalone forge would have no way to make its admin. ### What changed - `src/core/users.ts`: `AdminOrigin = claim | first-admin | seed`. Only those origins' inputs carry `isAdmin`/`adminIfFirst` (a type error anywhere else), and `createUser` drops them at run time for every other origin. - The signup, passkey and edge doors no longer ask for `adminIfFirst`. `welcome` and `ensureEdgeUser` no longer mark an account proven for being the first/admin, so every sign-up follows the email link. - `test/helpers/admin.ts` (`seedAdmin`, `adminCookie`): the 82 suites whose admin was their first sign-up make that same account as `seed` instead. The tests that asserted the old rule now assert the new one. - `test/no-signup-admin.test.ts`: on main 18917b93 it fails 4 of 6 (API, passkey door, terminal and edge each made the first account an admin). On this branch it passes 6/6. - Docs: README, ARCHITECTURE, APPS. Beyond `validate`, every test file was also run on its own on this head (525 files, CI-shaped env, bun 1.3.14). All are green except contain.test.ts (DNS), crew-interrupted.test.ts and snapshot-destination.test.ts, and those three fail the same way alone on main on this box. ### Left for a decision Not changed here: while signup is `closed`/`invite`, a fresh forge still lets in its first account (`newAccountsClosed`), and the terminal still skips Turnstile for that first account. Both were there so the owner could sign up as the admin. Now all they do is let a stranger make an unproven guest before the operator gets there. Closing them is a small follow-up if Trevor wants it. <!-- pipeline:validation --> ### Validation `pipeline/validate` on `ea16896f`: **baseline** in 609s — bun 1.3.14, CI-shaped env (scrubbed vars, runner PATH), exact check.yml shard + forge.json commands. | step | result | time | |---|---|---| | `bun install --frozen-lockfile` | ok (exit 0) | 0s | | `forge.json check.fast` | BASELINE (exit 1) | 137s | | `check.yml gate (1) pull_request` | BASELINE (exit 1) | 15s | | `check.yml gate (2) pull_request` | ok (exit 0) | 273s | | `check.yml gate (3) pull_request` | ok (exit 0) | 184s | - ⚠️ 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 <details><summary>test output (failures, totals)</summary> ``` ▶ bun install --frozen-lockfile ok exit 0 0s ▶ forge.json check.fast (fail) reviews, threads and the merge gate > require-review refuses the merge until the head is approved [309.29ms] [shard 9] (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.47ms] [shard 10] Ran 3146 tests across 315 files in 133.4s; 2 failed. classify: reran test/contain.test.ts test/pull-mergeable.test.ts test/tui-workflows.test.ts alone on origin/main 098fafdf and on this branch BASELINE exit 1 137s ▶ 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 [2.42ms] [shard 3] Ran 110 tests across 3 files in 14.4s; 1 failed. classify: reran test/contain.test.ts alone on origin/main 098fafdf and on this branch BASELINE exit 1 15s ▶ check.yml gate (2) pull_request Ran 1455 tests across 144 files in 273.3s (144 shards). ok exit 0 273s ▶ check.yml gate (3) pull_request Ran 1541 tests across 145 files in 183.4s (145 shards). ok exit 0 184s ``` </details>
No sign-up makes the site admin, not even a forge's first account: only the operator's first-admin code, a claim or a seed makes one
Some checks failed
check / gate (2) (pull_request) Failing after 2m18s
preview / preview (pull_request) Has been skipped
check / gate (3) (pull_request) Successful in 4m14s
check / gate (1) (pull_request) Successful in 11m17s
check / check (pull_request) Failing after 1s
4368c93033
`AdminOrigin` is `claim`, `first-admin` and `seed`. createUser takes the
admin flags only from one of those: they are not in any other origin's
input type, and createUser drops them for any other origin at run time.
So the signup form, the API, the passkey door, the terminal, the parent
forge's sign-in and the platform edge each make a person who is not an
admin, even on a forge with no admin at all. A standalone forge's admin
comes from the operator's `--first-admin` code (#41); a spawned child's
from its claim link.

With no sign-up an admin, `welcome` no longer proves an admin's address by
being one, and the edge's first account is not proven by being first:
both start unproven and follow the link like anyone else.

Test suites whose admin was their first sign-up make it with
`test/helpers/admin.ts` (`seedAdmin`, or `adminCookie` for a signed-in
browser) instead. `test/no-signup-admin.test.ts` fails on main: the API,
passkey, terminal and edge doors each made the first account an admin.
The ssh suites seed their admin too: alice is made as seed, not by being the forge's first sign-up
All checks were successful
check / gate (3) (pull_request) Successful in 4m9s
check / gate (1) (pull_request) Successful in 11m14s
check / gate (2) (pull_request) Successful in 18m58s
check / check (pull_request) Successful in 1s
preview / preview (pull_request) Has been skipped
ea16896f5a
git-ssh and ssh-app drive the real OpenSSH client, so they skip on a machine
without it and the branch's validate run never saw them. On CI they failed:
alice, no longer admin by signing up first, is unverified, and createRepo
refuses her ("repo creation failed"). Same fix as the other suites:
seedAdmin, which keeps her the proven admin she was on main.
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!57
No description provided.