# Census: console email sign-in

Status: APPROVED by Paul (census owner), 2026-10-24, recorded on #280 with
main at 65ca4aca. The rows keep `rev3=PROPOSED-email-signin`: under the #369
process nothing is flipped, and later overlays re-froze units at these
dispositions, so a flip would move their fingerprints.

Branch `email-signin`, cut from main at 4d6f0e6c. Design:
`docs/superpowers/specs/2026-10-21-console-email-signin-design.md`.

## What changed

The front page has one address box. An address with a Keymail mailbox goes
to Keymail's sign-in as before. Any other address gets an emailed link and a
six-digit code. A session that signed in by email re-confirms risky actions
by email instead of a passkey. The old Apps-inbox confirmation link is
retired: its route stays, but only says the link has expired.

None of this runs as root. Every unit below runs in the console
(cmd/console serve under console.service, user carlos-console). The root
`carlos` binary links neither `internal/console` nor `internal/mail`, and
`TestRootBinaryLinksNoConsoleOrMail`
(`internal/provision/root_binary_deps_test.go`) pins that with
`go list -deps`. No root read is added.

## Root-read census (TestPlan14RootReadCensus)

Overlay `internal/provision/testdata/plan14-root-reads-email-signin.tsv`,
loaded last, after `plan14-root-reads-claim-merge-main-4.tsv`. Without it the
test reports 47 discrepancies; with it, none.

### Changed units, re-frozen at main's disposition

Each keeps main's basis, with this branch's identity and change appended
(the run-lifecycle-logs-325 form).

| unit | change | sites |
|---|---|---|
| `cmd/console/main.go` `serve` | passes `CARLOS_CONSOLE_NAME` to NewServer as `Config.ConsoleName` | same three `/data` literals, same offsets |
| `server.go` `NewServer` | sets `consoleNameCfg` and the `challengeSem` send bound; registers the four `/auth/email` routes | the logs/data route pattern, moved |
| `authcli.go` `Server.handleCLIApprovePage` | signed out, renders the front page with `next` back to `/auth/cli` instead of redirecting | adds `r.URL.Query()` (a URL, not a file); old site moves |
| `server.go` `Server.handleCallback` | session minting moves into `finishSignin`; reads the `next` cookie | `r.URL.Query()`, unchanged |
| `store.go` `Store.PurgeExpired` | also deletes expired `email_challenges` rows | one more SQLite `Exec` on CONSOLE_DB |
| `store.go` `Store.migrate` | adds `sessions.method` (default `keymail`) and calls `migrateEmailSignin` | SQLite DDL; old site moves |
| templates `check`, `home`, `layout`, `team` | approved copy, the address and code forms, CSS for email inputs | embedded UI input |

### New units, accepted legacy at their siblings' class

All J4 root-input, the class of the console's other store methods,
URL-query handlers and templates.

| unit | what it reads |
|---|---|
| `emailsignin_store.go`: `Store.HoldEmailChallenge`, `LiveEmailChallenge`, `PeekEmailLink`, `TakeEmailCode`, `TakeEmailLink`, `insertEmailChallenge`, `migrateEmailSignin`, `deleteBound` | SQLite on the console's own `email_challenges` table |
| `store.go` `Store.SessionInfo` | one `QueryRow` on `sessions`, as `SessionAddress` |
| `store.go` `Store.PendingInviteAddress` | one `QueryRow` on `invites`, without consuming it, as `PendingInviteFor` |
| `store.go` `Store.CreateSessionMethod` | `CreateSession`'s `INSERT`, moved, plus the method column |
| `stepup.go` `Store.takePendingActionByHash` | `TakePendingAction`'s body and both sites, moved unchanged, keyed by hash |
| `store.go` `newStore` | two `crypto/rand.Read` calls: an in-memory 32-byte HMAC key for codes and an 8-byte epoch label, never written down; as `Keymail.sealEnvelope` |
| `server.go` `Server.handleHome`, `Server.handleSignin`; `emailsignin.go` `Server.handleEmailLinkPage` | `r.URL.Query()` of the request URL (`next`, `t`), not a file; as `handleCallback` |
| templates `email_link.html`, `stepup_email.html` | embedded UI input, as their sibling templates |

### Retired units

Each is still declared and reads nothing now, so it is retired by an H3
removal. The loader accepts an H3 removal only with a `census-ruling-`
basis, so the rows read `census-ruling-email-signin (PROPOSED, awaiting the
census owner on #280): ...`. That is the form console-invite-delivery used
for `Keymail.SendApp` before its ruling. A removal is not in the seal.

- `server.go` `Server.handleConfirm`: renders the expired message only. Its
  `TakeConfirmation`, `EnsureUser`, `GrantToken` and `CreateSession` calls
  are gone.
- `stepup.go` `Store.TakePendingAction`: calls `takePendingActionByHash`.
- `store.go` `Store.CreateSession`: calls `CreateSessionMethod` with
  `MethodKeymail`.

### Envelopes and seal

`cmd/console`, `internal/console` and `internal/console/templates` are
re-pinned. The scan finds no other read-site change in them. Twenty-eight
frozen declarations are added, so the seal moves from securelisten-337's
`33a8ff46…c430` to `b25dfda5a68d2183af759b42398f79bb471749d46e68d267545647d1a2e6046b`.

## Record-write census (TestPlan9B8RecordWriteCensus)

`internal/console/server.go:NewServer` is re-digested to `9a49c1fc…8169`. The
effective entry is the replacement in `scanDemandWriteCensusInventory`
(`scan_demand_census_test.go`), which overrides the `plan9B8CensusInventory`
row, so that is the entry changed. Its kinds are still `lit:instances`. The
diff adds two struct fields and four route registrations. No record write is
added or moved.

## Questions for the census owner

1. Accept the ten changed and eighteen new units as accepted legacy under the
   console's identity, with seal `1221062f…1d00`?
2. Retire `handleConfirm`, `TakePendingAction` and `CreateSession` by H3
   removal? If ruled, the marker becomes `census-ruling-email-signin` in
   every row and the seal is recomputed.
