Files
summercms/.planning/phases/07-user-plugin-and-authentication/07-05-PLAN.md
2026-09-22 12:37:08 +02:00

22 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
07-user-plugin-and-authentication 05 execute 4
07-03
07-04
../fonoteka.go/parity/manifest.yaml
../fonoteka.go/parity/capture-rules.yaml
../fonoteka.go/parity/parity_test.go
../fonoteka.go/parity/check_corpus.go
../fonoteka.go/parity/db_capture.go
../fonoteka.go/parity/db_capture_test.go
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_login_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_logout_user-api.yaml
../fonoteka.go/parity/fixtures/routes/GET___user_api_v1_fetch_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_refresh_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_register_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_forgot-password_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_reset-password_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_activate_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_activate-by-code_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_update_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_change-password_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_avatar_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_avatar_remove_user-api.yaml
../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_marketing-consent_user-api.yaml
../fonoteka.go/parity/fixtures/routes/GET___user_api_v1_oauth-providers_user-api.yaml
../fonoteka.go/parity/fixtures/nuxt/nuxt-auth.yaml
false
AUTH-01
AUTH-02
AUTH-03
AUTH-04
truths artifacts key_links
Every one of the 15 /_user/api/v1 routes named in D-01 is recorded against the isolated PHP instance and replays green against the Go backend
The recorded corpus includes the A2 fixture (6 rapid failed logins) settling whether Winter's throttle actually gates the JWT login path, and the D-12 code-carrying two-step flows (forgot->reset, register->activate-by-code) using a real code read from the target's own database, not a hardcoded value
No live JWT, inv_ token or database credential is committed to git; the private vars store stays mode 0600 and untracked
A new nuxt-auth client flow fixture exercises register -> fetch -> update -> change-password -> refresh -> logout -> refused-reuse, plus a must_change_password user reaching 423 then clearing it via change-password after a successful me/locale call (D-14)
The 15 new /_user/api/v1 routes are added to fonoteka.go/parity/manifest.yaml (absent from the original 154-route fonoteka manifest), the fixed-154-total wording in parity_test.go/check_corpus.go is corrected to the new total, and no fixture is recorded for a dropped extraction source (query-string/body jwt_token, per D-09), per D-11
path provides
../fonoteka.go/parity/fixtures/nuxt/nuxt-auth.yaml the D-14 recorded client flow
path provides
../fonoteka.go/parity/manifest.yaml 15 new /_user/api/v1 entries plus every D-13 distinct-body case, status: ported once green
from to via pattern
../fonoteka.go/parity/db_capture.go ../fonoteka.go/parity/capture-rules.yaml two-step flows reads reset_password_code/activation_code from the target's own users row after step one, writes it into the shared tide.Store as a {{var}} reset_password_code|activation_code
Record real parity evidence for every route 07-02/07-03/07-04 shipped: the 15 `/_user/api/v1` routes (new manifest entries, D-11) plus the `nuxt-auth` client flow (D-14), against the isolated PHP instance via the Phase 2 `tide` tooling, then replay them green against the Go backend. This plan is the acceptance test for AUTH-01..04 — until it is green, "the PHP contract is the acceptance test" is only a design intent for this phase, not a proven fact.

Purpose: prove byte-for-byte parity on the newly-ported surface, including the two hardest-to-fake cases: a real Winter throttle confirmation (Assumption A2) and code-carrying two-step mail flows (D-12) that need a real database read, not a stubbed value. Output: 15 new manifest.yaml entries plus their fixtures, an extended capture-rules.yaml, a DB-reading capture helper, and nuxt-auth.yaml.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-user-plugin-and-authentication/07-CONTEXT.md @.planning/phases/07-user-plugin-and-authentication/07-RESEARCH.md @.planning/phases/07-user-plugin-and-authentication/07-VALIDATION.md @.planning/phases/07-user-plugin-and-authentication/07-02-SUMMARY.md @.planning/phases/07-user-plugin-and-authentication/07-03-SUMMARY.md @.planning/phases/07-user-plugin-and-authentication/07-04-SUMMARY.md ```go func tide.OpenStore(path string) (*Store, error) // loads/creates a private 0600 YAML map; empty path = memory-only func (s *Store) Get(name string) (string, bool) func (s *Store) Set(name, value string) func (s *Store) Save() error // writes YAML, mode 0600 func (s *Store) Expand(text string) (string, error) // {{name}} substitution used by request specs ``` `summer parity:record`/`parity:replay` CLI flags (summercms.go/cmd/summer/parity.go, already read this session): `--spec/--target/--output/--rules/--vars/--manifest/--fixtures/--update/--next-batch(max 15)/--resume/--allow-incomplete/--require-recorded`. `../fonoteka.go/parity/php_parity.sh {reset|serve|artisan}` (already read this session): `reset` wipes and recreates the isolated SQLite DB and runs `winter:up`; `serve` runs the isolated PHP app at `127.0.0.1:8423` with `APP_DEBUG=false`; `artisan ` runs any artisan command (e.g. `tinker`) against the SAME isolated DB — useful for a one-off read without a Go SQLite driver. Task 1: Start the isolated PHP instance ../fonoteka.go/parity/php_parity.sh ../fonoteka.go/parity/php_parity.sh (reset/serve/artisan subcommands and the refuse_db guard, already read this session) Run `../fonoteka.go/parity/php_parity.sh reset` (wipes the parity-only SQLite DB, runs `winter:up`). Run `../fonoteka.go/parity/php_parity.sh serve &` (or in a separate terminal) so it stays up for the rest of this plan. Confirm this is the ISOLATED parity database (`realpath $PARITY_ROOT`), never the developer's own `storage/database.sqlite` -- `php_parity.sh`'s own `refuse_db` guard already enforces this, but visually confirm the printed DB path before proceeding. curl -sf http://127.0.0.1:8423/_user/api/v1/oauth-providers Confirm the isolated PHP instance is running against the parity-only SQLite database, not the developer's own database, before any recording begins. Type "ready" once the isolated PHP instance is reachable at 127.0.0.1:8423. - `curl -sf http://127.0.0.1:8423/_user/api/v1/oauth-providers` returns HTTP 200 with a JSON body - The printed `DB_DATABASE` path from `php_parity.sh` lives under `$PARITY_ROOT` and is named `*parity*`, never the developer's own database The isolated PHP instance is reachable at 127.0.0.1:8423 and backed by the parity-only SQLite database. Task 2: Record the 15 /_user/api/v1 routes and the nuxt-auth flow ../fonoteka.go/parity/manifest.yaml, ../fonoteka.go/parity/capture-rules.yaml, ../fonoteka.go/parity/db_capture.go, ../fonoteka.go/parity/db_capture_test.go, ../fonoteka.go/parity/fixtures/routes/POST___user_api_v1_*.yaml, ../fonoteka.go/parity/fixtures/routes/GET___user_api_v1_*.yaml, ../fonoteka.go/parity/fixtures/nuxt/nuxt-auth.yaml ../fonoteka.go/parity/capture-rules.yaml (existing `login` capture rule to extend), ../fonoteka.go/parity/manifest.yaml (existing entry block shape — `id, method, path, auth_group, status, identities, cases[].fixture/request/response`), ../fonoteka.go/parity/fixtures/routes/GET__api_v1_fonoteka_genres_personal_token.yaml (single-route fixture shape), ../fonoteka.go/parity/fixtures/nuxt/nuxt-browse.yaml (first ~50 lines only — the client-flow fixture step shape and `{{jwt:alice}}`/`{{id:*}}` placeholder syntax), summercms.go/tide/variables.go (Store — full file, the exported API this task's DB-capture helper writes through), .planning/phases/07-user-plugin-and-authentication/07-CONTEXT.md (D-01, D-02, D-06 through D-21 — every response body this task must record matches what 07-02/07-03 already implemented), .planning/phases/07-user-plugin-and-authentication/07-VALIDATION.md (Manual-Only Verifications table — A1/A2 confirmation instructions) Extend `capture-rules.yaml` with capture blocks for `register`, `refresh`, and `activate-by-code` (each mints a token, Pitfall 3): `{method: POST, path: /_user/api/v1/register, capture: [{from: response.json, path: $.token, as: jwt:newuser, category: jwt}]}` and equivalents for the other two — mirror the existing `login` rule's shape exactly.
Create `db_capture.go` (package `main`, alongside the existing `parity_test.go` — or a small standalone `go run`-able file, whichever fits the existing package layout better once read): a helper that, given a target's connection info (the isolated PHP's SQLite path from `$PARITY_ROOT`, or the Go replay target's Postgres DSN) and an email, reads `reset_password_code` or `activation_code` from the `users` row and calls `tide.OpenStore(varsPath)` → `Set("code:reset", value)` or `Set("code:activate", value)` → `Save()`. For the PHP/SQLite side, shell out to `../fonoteka.go/parity/php_parity.sh artisan tinker --execute="echo \Golem15\User\Models\User::where('email','<email>')->value('reset_password_code');"` (avoids adding a new Go SQLite driver dependency for parity-only tooling) and capture stdout. For the Go/Postgres replay side, use the existing `*sql.DB`/DSN the replay harness already opens — a direct `SELECT reset_password_code FROM users WHERE email = $1`. This is the D-12 "app-owned tide seed/capture hook" — it lives in `fonoteka.go/parity`, not in the framework-owned `tide` package, exactly because it knows the Płytarium `users` table shape.

Record, for EACH of the 15 routes (`login, logout, fetch, refresh, register, forgot-password, reset-password, activate, activate-by-code, update, change-password, avatar, avatar/remove, marketing-consent, oauth-providers`), every distinct status+body PHP actually returns (D-13 — do not assume the bodies drafted during planning are exact; RECORD the real ones and treat any drift as authoritative over the plan text): success case, validation-failure (422) case where applicable, and the specific failure modes named in 07-CONTEXT.md (bad credentials, suspended/banned via the throttle, registration disabled/throttled, bad/expired refresh, wrong current password, expired/invalid reset or activation code). Every recorded `user` payload (login/fetch/register/update success bodies) MUST include the literal `feedback_widget_hidden: false` key -- if a recorded fixture is missing it, that is a signal the base payload construction in 07-02 needs a follow-up, not that the fixture should be trimmed to match.

Give two DELIBERATE PHP-quirk reproductions their own named recording pass, since they are easy to silently "fix" during recording if the operator assumes the plan text is wrong instead of PHP: (1) `register()` with `allow_registration=false` or the register throttle tripped, under the pinned `APP_DEBUG=false` -- confirm the recorded body is `{"error":"Internal server error"}`,500, NOT the literal "Registrations are currently disabled."/"Registration is throttled..." text (07-02 Task 3's `SafeExceptionResponse` finding); (2) `POST activate` (authenticated) with a WRONG code -- confirm the recorded body is still 200 `{"user":{...}}` with `is_activated` unchanged, NOT a 422/401 (07-03 Task 1's missing-`if`-guard finding). For both, the RECORDED PHP body is authoritative: if either one comes back looking different from what 07-02/07-03 assumed, that is a real finding to write into this plan's SUMMARY and file as a gap-closure item against the plan that implemented it -- do not silently adjust the fixture to match the plan text's expectation.

Use `summer parity:record --spec=<one-off YAML per case> --target=http://127.0.0.1:8423 --rules=capture-rules.yaml --vars=<private path> --manifest=manifest.yaml --fixtures=fixtures/routes --next-batch=15 --resume=true` for the bulk of the batch (the whole 15-route surface fits in one `--next-batch=15` call, matching the established 15-route resume workflow), then hand-author additional YAML specs for the extra per-route failure cases and record those individually with `--spec`.

Record the A2 confirmation case explicitly: 6 rapid `POST /_user/api/v1/login` calls with a wrong password for the SAME seeded account, asserting whether the 6th attempt's body differs from attempts 1-5 (settling Assumption A2 — if PHP's `JWTAuth::attempt()` does NOT actually reach Winter's throttle for this route, the 6th attempt's body will be identical to the first; if it does, confirm whatever the actual PHP body is and make sure the Go implementation from 07-02 matches it, filing a gap-closure note in this plan's SUMMARY if a code change is needed).

Record the two D-12 two-step flows using `db_capture.go`'s helper between steps: `forgot-password` (step 1) → read `reset_password_code` via the helper → `reset-password` (step 2) with `{{code:reset}}` in its request body. `register` in `user`-activation mode (step 1) → read `activation_code` → `activate-by-code` (step 2) with `{{code:activate}}`.

Author `fixtures/nuxt/nuxt-auth.yaml` per D-14: `register → fetch → update → change-password → refresh → logout → (reuse of the logged-out token refused)`, PLUS a second scenario in the same flow (or a second flow file if that's cleaner given the existing `nuxt-browse.yaml` convention — check how multi-scenario flows are structured there first): a `must_change_password` user hits 423 on an authenticated `/_fonoteka/api/v1` route, then successfully calls `me/locale`, then clears the lock via `change-password`. Use `{{jwt:newuser}}`/`{{id:*}}` placeholders exactly per `nuxt-browse.yaml`'s established syntax.

Add all 15 new manifest entries (they do not exist yet, unlike `tokens`/`me/locale` which 07-04 already flipped) with `auth_group: user-api` (a new auth-group label distinct from `jwt`/`personal_token`/`public` — the `/_user/api/v1` group has no `jwt.auth`, matching D-01), one `cases[]` entry per distinct status+body recorded. Set `status: ported` only for routes whose EVERY recorded case replays green against the Go backend this session — anything that doesn't reach full green stays `pending` (never claim ported-but-failing, matching the established "pending never equals passing" rule).

Update `parity_test.go`'s `expectedPHPRoutes` from `154` to `169` (154 + 15) and `check_corpus.go`'s `expectedRouteCount` the same way; update `parity_contract_test.go`'s hardcoded inventory numbers to the real recorded/ported/pending split this session produces (read the actual numbers off the corpus run, do not guess). Leave `ROADMAP.md`/`REQUIREMENTS.md`'s "All 154 routes" phrasing (API-09, QA-05, the Phase 15 goal) UNCHANGED — that figure is specifically the Płytarium `fonoteka` plugin's route surface for the cutover criterion, a stable definition unaffected by `golem15.user`'s own, separately-tracked routes sharing the same manifest FILE.
go test ./parity/... -run 'TestParityCorpus|TestDBCapture' -short - `parity/manifest.yaml` contains exactly 15 new entries with `auth_group: user-api`, one per D-01 route - Every one of the 15 entries has at least one recorded `cases[]` fixture; routes with a documented failure mode (bad credentials, throttled, disabled registration, expired code) have that case recorded too - The A2 fixture (6 rapid failed logins) exists and its 6th-attempt body is explicitly compared against attempt 1 in this plan's SUMMARY - `fixtures/nuxt/nuxt-auth.yaml` exists and its steps cover register, fetch, update, change-password, refresh, logout, and a refused token-reuse step - `parity_test.go`'s `expectedPHPRoutes` reads `169` - The recorded `register` disabled/throttled fixture body is `{"error":"Internal server error"}`,500 under `APP_DEBUG=false`, and the recorded authenticated `activate`-with-wrong-code fixture body is 200 `{"user":{...}}` with `is_activated` unchanged -- both named explicitly in this plan's SUMMARY as confirmed, not assumed - Every recorded login/fetch/register/update success fixture's `user` payload contains the literal key `feedback_widget_hidden: false` 15 new manifest entries exist with D-13-complete case coverage; the A2 and D-12 cases are recorded and settled; nuxt-auth.yaml exists and records the full D-14 sequence; both named PHP-quirk reproductions (register disabled/throttled degrading to 500, authenticated activate's no-op-on-wrong-code) are confirmed against the real recorded PHP body. Task 3: Verify no secrets leaked and the corpus replays green ../fonoteka.go/parity/fixtures/routes, ../fonoteka.go/parity/fixtures/nuxt/nuxt-auth.yaml, ../fonoteka.go/parity/manifest.yaml the fixture files Task 2 recorded, ../fonoteka.go/parity/manifest.yaml Confirm via `git status` over `fonoteka.go/parity/` that the private vars store path is NOT staged (it should sit outside the repo or be gitignored, mode 0600, per the established Phase 2 convention). Grep the new fixtures for live JWT/`inv_` token shapes (a capture leak `tide`'s masking should already prevent, verified directly here since these are brand-new files). Run the corpus and replay commands below. Spot-check 2-3 fixture bodies against this plan's D-13 expectations (e.g. the login-failure body, the change-password wrong-current-password body) to confirm the recorded PHP behavior matches what 07-02/07-03 implemented -- flag any drift for a gap-closure note rather than silently accepting a mismatch. Specifically re-confirm Task 2's two named PHP-quirk recordings here as part of the sign-off: the register disabled/throttled fixture reads `{"error":"Internal server error"}`,500 (not the literal disabled/throttled text), and the authenticated activate-with-wrong-code fixture reads 200 `{"user":{...}}` (not a 4xx) -- both are deliberate reproductions of PHP quirks read directly from source in 07-02/07-03, not planning guesses, so the recorded body is authoritative if either differs from what is written here. ! grep -rE "eyJ[A-Za-z0-9_-]+[.][A-Za-z0-9_-]+[.][A-Za-z0-9_-]+|inv_[A-Za-z0-9]{8,}" ../fonoteka.go/parity/fixtures/routes/*user_api_v1* ../fonoteka.go/parity/fixtures/nuxt/nuxt-auth.yaml && go test ./parity/... -run TestParityCorpus -v Confirm no live secret is committed and the corpus replays green before approving this plan, or list the specific fixtures/bodies that need a gap-closure follow-up. Type "approved" once no live secret is committed and the corpus replays green, or list the specific fixtures/bodies that need a gap-closure follow-up. - The JWT/`inv_`-shape grep over the new fixtures returns zero matches - `git status` does not list the private vars store path - `go test ./parity/... -run TestParityCorpus -v` reports zero failing cases among the newly ported routes - The two named PHP-quirk fixtures (register disabled/throttled -> 500 opaque body; authenticated activate-with-wrong-code -> 200 unchanged) are explicitly re-confirmed in this task's sign-off, with any drift noted as a gap-closure item rather than silently accepted No live JWT/inv_ token shape is present in any committed fixture; the private vars store is untracked and 0600; TestParityCorpus and summer parity:replay are green for every newly ported route; both named PHP-quirk reproductions are confirmed against the real recorded PHP body.

<threat_model>

Trust Boundaries

Boundary Description
Parity recorder → isolated PHP The recorder captures real HTTP traffic, including credential and token material, that must never reach git
Parity recorder → committed fixtures Recorded bodies cross from a private, secret-bearing capture session into a public, committed artifact

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-07-10 Information Disclosure Recorded fixtures mitigate Capture-rule masking (existing Phase 2 mechanism) plus this plan's explicit human-verify grep pass for JWT/inv_ shapes before commit; the private vars store stays 0600 and untracked
T-07-14 Information Disclosure DB-capture helper (db_capture.go) mitigate Reads only the single reset_password_code/activation_code column needed, via a parameterized query (Postgres side) or a fixed tinker one-liner with no user-controlled SQL (PHP side) — never a general-purpose DB shell

</threat_model>

`go test ./parity/... -run TestParityCorpus -v` is green with the new routes reflected in the coverage summary; `summer parity:replay` reports zero failing cases across the whole manifest (existing 154-route fonoteka surface unaffected, new 15-route user surface green or honestly pending).

<success_criteria> Every /_user/api/v1 route and the nuxt-auth flow are recorded against real PHP and replay green against the Go backend, with A2 and D-12 settled empirically rather than assumed. </success_criteria>

Create `.planning/phases/07-user-plugin-and-authentication/07-05-SUMMARY.md` when done