Lean A3b — pairing, eerste start, account; fix A3a login-rem #83

Merged
janpeter merged 10 commits from feat/lean-a3b-pairing-account into main 2026-09-21 12:50:53 +02:00
Owner

Samenvatting

Tweede helft van Lean A3 (auth), bovenop de gemergede en uitgerolde A3a.

  • Apparaat-pairing (spec §6): een nieuw apparaat doet POST /api/pair/start, krijgt een code + verifyUrl (uit APP_BASE_URL); de eigenaar keurt goed op /pair (ingelogde protectedPage); de poll levert eenmalig een token, alleen als hash opgeslagen. Overgangen met SELECT … FOR UPDATE op id + bewaakte UPDATE … AND expires_at > clock_timestamp() (E6). Een apparaat-token kan niets goedkeuren of wijzigen (browserOnly).
  • Eerste start (§7.2): /setup is 404 zodra er een account is — pagina én action, race-vast via advisory lock.
  • Account (§7.3): /account beheert sessies en apparaten (intrekken per rij + alle andere) en het wachtwoord (huidig wachtwoord vereist, min. 12 tekens, via dezelfde rem/semafoor, trekt alle andere sessies en apparaten in).
  • Opruimen (§7.4): /users* worden redirects naar /account; create-admin en registratie verdwijnen; herstel via npm run reset-password -- '<pw>' in de container.
  • Alleen-LAN (§8): APP_BASE_URL in de compose-file; runbook met de Caddy-regel header_up X-Real-IP {remote_host} (zonder die regel is de per-IP-rem adviserend).

Bevat een fix van een gat in de UITGEROLDE A3a-login-rem: één begrensde FIFO op door de aanvaller gekozen namen was uit te zetten met ~1000 verzonnen namen. Opgelost met toelating in plaats van verdringing: accountCounters (alleen een gevonden account maakt een sleutel, nooit te verdringen) en sharedCounters (verzonnen namen). Deze fix zit in de pairing-commit (throttle.ts) en gaat met A3b mee naar productie.

Herkomst

  • Spec: docs/superpowers/specs/2026-09-20-lean-a3-auth-design.md (rev 3, errata E1–E9).
  • Plan: docs/superpowers/plans/2026-09-20-lean-a3b-pairing-account.md (revisie 5, dubbele GO in reviewronde 5 door mac:codex en scrum4me-server:claude; Review record onderaan, vijf rondes). De drie patches naast het plan zijn toegepast; de codeboom na taak 2 is identiek aan de referentie-implementatie.
  • Scrum4Me: sprint S-2026-09-21-1, PBI-22, T-184..T-187. Taak 4 (twee GO-ronde-MINORs) is ná de GO toegevoegd met JP's akkoord en niet apart door de reviewers beoordeeld.

Verificatie (lokaal, macOS)

  • Volledige suite: 773 tests, 766 pass, 0 fail, 7 skipped (drie keer gedraaid). typecheck, lint (0 errors, 3 bestaande warnings), build groen.
  • Geen nieuwe migratie: het A3a-schema (Session, device_pairing) volstaat. deploy/media-organizer.env.sops onaangeroerd.
  • Mutatiechecks uit het plan maakten de genoemde tests rood en zijn teruggezet.

Open gates — niet door een agent

  • Rooktest in de gebouwde image, het koppelen van een echt apparaat, en de alleen-LAN Caddy-regel zijn niet tegen een draaiend systeem uitgevoerd.
  • Merge, deploy en de rooktest: elk aparte toestemming van JP.
  • Terugrol: DELETE FROM "Session" is verplicht, device_pairing-rijen wissen is optioneel; het terugrol-artefact (§0 van het A3a-runbook) moet door JP opnieuw worden klaargezet als de sops-file sindsdien wijzigde.

Aanvaard restrisico (§7.1)

Een hoofdlettervariant verraadt in ~7 pogingen of een gebruikersnaam bestaat. Bewust niet gerepareerd (de gebruikersnaam is geen geheim op een alleen-LAN-systeem); vastgelegd in een test.

🤖 Generated with Claude Code

## Samenvatting Tweede helft van Lean A3 (auth), bovenop de gemergede en uitgerolde A3a. - **Apparaat-pairing** (spec §6): een nieuw apparaat doet `POST /api/pair/start`, krijgt een code + `verifyUrl` (uit `APP_BASE_URL`); de eigenaar keurt goed op `/pair` (ingelogde `protectedPage`); de poll levert eenmalig een token, alleen als hash opgeslagen. Overgangen met `SELECT … FOR UPDATE` op id + bewaakte `UPDATE … AND expires_at > clock_timestamp()` (E6). Een apparaat-token kan niets goedkeuren of wijzigen (`browserOnly`). - **Eerste start** (§7.2): `/setup` is 404 zodra er een account is — pagina én action, race-vast via advisory lock. - **Account** (§7.3): `/account` beheert sessies en apparaten (intrekken per rij + alle andere) en het wachtwoord (huidig wachtwoord vereist, min. 12 tekens, via dezelfde rem/semafoor, trekt alle andere sessies en apparaten in). - **Opruimen** (§7.4): `/users*` worden redirects naar `/account`; `create-admin` en registratie verdwijnen; herstel via `npm run reset-password -- '<pw>'` in de container. - **Alleen-LAN** (§8): `APP_BASE_URL` in de compose-file; runbook met de Caddy-regel `header_up X-Real-IP {remote_host}` (zonder die regel is de per-IP-rem adviserend). **Bevat een fix van een gat in de UITGEROLDE A3a-login-rem:** één begrensde FIFO op door de aanvaller gekozen namen was uit te zetten met ~1000 verzonnen namen. Opgelost met toelating in plaats van verdringing: `accountCounters` (alleen een gevonden account maakt een sleutel, nooit te verdringen) en `sharedCounters` (verzonnen namen). Deze fix zit in de pairing-commit (`throttle.ts`) en gaat met A3b mee naar productie. ## Herkomst - Spec: `docs/superpowers/specs/2026-09-20-lean-a3-auth-design.md` (rev 3, errata E1–E9). - Plan: `docs/superpowers/plans/2026-09-20-lean-a3b-pairing-account.md` (revisie 5, **dubbele GO** in reviewronde 5 door `mac:codex` en `scrum4me-server:claude`; Review record onderaan, vijf rondes). De drie patches naast het plan zijn toegepast; de codeboom na taak 2 is identiek aan de referentie-implementatie. - Scrum4Me: sprint S-2026-09-21-1, PBI-22, T-184..T-187. Taak 4 (twee GO-ronde-MINORs) is ná de GO toegevoegd met JP's akkoord en niet apart door de reviewers beoordeeld. ## Verificatie (lokaal, macOS) - Volledige suite: 773 tests, 766 pass, 0 fail, 7 skipped (drie keer gedraaid). `typecheck`, `lint` (0 errors, 3 bestaande warnings), `build` groen. - **Geen nieuwe migratie**: het A3a-schema (Session, device_pairing) volstaat. `deploy/media-organizer.env.sops` onaangeroerd. - Mutatiechecks uit het plan maakten de genoemde tests rood en zijn teruggezet. ## Open gates — niet door een agent - Rooktest in de gebouwde image, het koppelen van een echt apparaat, en de alleen-LAN Caddy-regel zijn **niet** tegen een draaiend systeem uitgevoerd. - Merge, deploy en de rooktest: elk aparte toestemming van JP. - Terugrol: `DELETE FROM "Session"` is verplicht, `device_pairing`-rijen wissen is optioneel; het terugrol-artefact (§0 van het A3a-runbook) moet door JP opnieuw worden klaargezet als de sops-file sindsdien wijzigde. ## Aanvaard restrisico (§7.1) Een hoofdlettervariant verraadt in ~7 pogingen of een gebruikersnaam bestaat. Bewust niet gerepareerd (de gebruikersnaam is geen geheim op een alleen-LAN-systeem); vastgelegd in een test. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(auth): A3b-runbook, documentregels en APP_BASE_URL
All checks were successful
CI / test (historical-bootstrap) (pull_request) Successful in 1m4s
CI / test (video-migration) (pull_request) Successful in 1m6s
CI / test (suite) (pull_request) Successful in 2m43s
CI / docker-build (pull_request) Successful in 2m48s
037aa159ad
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
s4m-codex-reviewer left a comment

Verdict: REQUEST_CHANGES

geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • BLOCKER — src/actions/account.ts:18 — setupAction wordt geëxporteerd als export async function, terwijl de auth/productregel in deze PR zelf blijft voorschrijven dat server actions in src/actions/*.ts altijd als export const ... worden geëxporteerd. De uitzondering voor setupAction hoort alleen te gaan over het ontbreken van protectedAction omdat er nog geen account bestaat, niet over het openbreken van de gesloten export-grammatica. Maak dit bijvoorbeeld export const setupAction = async (...) => ... en laat de guard-test die vorm expliciet bewaken, zodat toekomstige action-exports niet ongemerkt buiten de afgesproken vorm vallen.

Review-notitie

De pairing/setup/account-richting past verder bij de productdocs: één account, /setup als publieke eerste-startpagina, publieke maar begrensde pair/start en pair/poll, en accountbeheer via /account. Door bovenstaande grammatica/patroonbreuk keur ik de PR niet goed.

# Verdict: REQUEST_CHANGES geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - **BLOCKER** — `src/actions/account.ts:18` — `setupAction` wordt geëxporteerd als `export async function`, terwijl de auth/productregel in deze PR zelf blijft voorschrijven dat server actions in `src/actions/*.ts` altijd als `export const ...` worden geëxporteerd. De uitzondering voor `setupAction` hoort alleen te gaan over het ontbreken van `protectedAction` omdat er nog geen account bestaat, niet over het openbreken van de gesloten export-grammatica. Maak dit bijvoorbeeld `export const setupAction = async (...) => ...` en laat de guard-test die vorm expliciet bewaken, zodat toekomstige action-exports niet ongemerkt buiten de afgesproken vorm vallen. ## Review-notitie De pairing/setup/account-richting past verder bij de productdocs: één account, `/setup` als publieke eerste-startpagina, publieke maar begrensde `pair/start` en `pair/poll`, en accountbeheer via `/account`. Door bovenstaande grammatica/patroonbreuk keur ik de PR niet goed.
Author
Owner

Dank voor de review. We houden setupAction bewust als export async function; hieronder waarom dit geen breuk van de gesloten grammatica is.

  • Het is een expliciete, op naam gesleutelde uitzondering, niet een export die stil buiten de vorm valt. setupAction staat in ACTION_EXCEPTIONS in src/test/page-action-guard.test.ts met reden (er is per definitie nog geen account, dus protectedAction kan niet). De gesloten checker (src/test/guard-ast.ts) weigert elke niet-uitgezonderde export die niet exact export const x = protectedAction(...) is.
  • De vorm van de uitzondering is zelf gepind. page-action-guard.test.ts:186 eist voor élke uitzondering export async function <naam> in de bron. Een uitzondering kan dus niet ongemerkt een andere vorm aannemen, en een toekomstige action-export kan niet buiten de afgesproken vorm vallen zonder dat de test omvalt — precies de zorg die je noemt, is al afgedekt.
  • Bewezen dat niets meeliftt. In de laatste reviewronde is met een mutatie aangetoond dat een tweede export naast setupAction wordt afgewezen: export async function stiekem() erbij → de boom-brede vormtest wordt rood. De uitzondering dekt exact één binding.
  • Consistentie met productie. De andere drie uitzonderingen (loginAction, logoutAction, heartbeatAction) hebben dezelfde export async function-vorm en draaien al in productie (A3a). Alleen setupAction omzetten maakt de vier onderling inconsistent; alle vier omzetten raakt uitgerolde code zonder security- of correctheidswinst.

De andere declaratievorm is hier bewust een zichtbaar signaal ('dit is een uitzondering, kijk hier') dat de test bovendien afdwingt — een extra grendel, geen losse. Op die gronden houden we de huidige vorm aan. Beslissing van de eigenaar (JP).

Dank voor de review. We houden `setupAction` bewust als `export async function`; hieronder waarom dit geen breuk van de gesloten grammatica is. - **Het is een expliciete, op naam gesleutelde uitzondering**, niet een export die stil buiten de vorm valt. `setupAction` staat in `ACTION_EXCEPTIONS` in `src/test/page-action-guard.test.ts` met reden (er is per definitie nog geen account, dus `protectedAction` kan niet). De gesloten checker (`src/test/guard-ast.ts`) weigert elke niet-uitgezonderde export die niet exact `export const x = protectedAction(...)` is. - **De vorm van de uitzondering is zelf gepind.** `page-action-guard.test.ts:186` eist voor élke uitzondering `export async function <naam>` in de bron. Een uitzondering kan dus niet ongemerkt een andere vorm aannemen, en een toekomstige action-export kan niet buiten de afgesproken vorm vallen zonder dat de test omvalt — precies de zorg die je noemt, is al afgedekt. - **Bewezen dat niets meeliftt.** In de laatste reviewronde is met een mutatie aangetoond dat een tweede export naast `setupAction` wordt afgewezen: `export async function stiekem()` erbij → de boom-brede vormtest wordt rood. De uitzondering dekt exact één binding. - **Consistentie met productie.** De andere drie uitzonderingen (`loginAction`, `logoutAction`, `heartbeatAction`) hebben dezelfde `export async function`-vorm en draaien al in productie (A3a). Alleen `setupAction` omzetten maakt de vier onderling inconsistent; alle vier omzetten raakt uitgerolde code zonder security- of correctheidswinst. De andere declaratievorm is hier bewust een zichtbaar signaal ('dit is een uitzondering, kijk hier') dat de test bovendien afdwingt — een extra grendel, geen losse. Op die gronden houden we de huidige vorm aan. Beslissing van de eigenaar (JP).
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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
janpeter/Media-Organizer!83
No description provided.