docs(07): create phase plan
Six plans for the user plugin and authentication phase: - 07-01: bouncer JWT lifecycle, password hashing, I18N-02 locale override, lagoon.Validate extensions (summercms.go) - 07-02: User/Throttle schema, core session loop (login/logout/ fetch/refresh/register) (fonoteka.go) - 07-03: account management (forgot/reset, activation, update, change-password, avatar, mail) (fonoteka.go) - 07-04: personal API tokens, me/locale, 423-exempt route-table proof (fonoteka.go) - 07-05: parity evidence recording against the isolated PHP instance (fonoteka.go) - 07-06: full unit coverage and validation sign-off (both repos) Plan count and scope confirmed at the plan-count checkpoint.
This commit is contained in:
205
.planning/phases/07-user-plugin-and-authentication/07-05-PLAN.md
Normal file
205
.planning/phases/07-user-plugin-and-authentication/07-05-PLAN.md
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
phase: 07-user-plugin-and-authentication
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["07-03", "07-04"]
|
||||
files_modified:
|
||||
- ../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
|
||||
autonomous: false
|
||||
requirements: [AUTH-01, AUTH-02, AUTH-03, AUTH-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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)"
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/parity/fixtures/nuxt/nuxt-auth.yaml"
|
||||
provides: "the D-14 recorded client flow"
|
||||
- path: "../fonoteka.go/parity/manifest.yaml"
|
||||
provides: "15 new /_user/api/v1 entries plus every D-13 distinct-body case, status: ported once green"
|
||||
key_links:
|
||||
- from: "../fonoteka.go/parity/db_capture.go"
|
||||
to: "../fonoteka.go/parity/capture-rules.yaml two-step flows"
|
||||
via: "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}}"
|
||||
pattern: "reset_password_code|activation_code"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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`.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
|
||||
<interfaces>
|
||||
<!-- tide's exported vars-store API (summercms.go/tide/variables.go), the mechanism the DB-capture hook writes through. -->
|
||||
```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 <args>` runs any artisan command (e.g. `tinker`) against the SAME isolated DB — useful for a one-off read without a Go SQLite driver.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:human-action" gate="blocking">
|
||||
<name>Task 1: Start the isolated PHP instance</name>
|
||||
<files>../fonoteka.go/parity/php_parity.sh</files>
|
||||
<read_first>../fonoteka.go/parity/php_parity.sh (reset/serve/artisan subcommands and the refuse_db guard, already read this session)</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>curl -sf http://127.0.0.1:8423/_user/api/v1/oauth-providers</automated>
|
||||
</verify>
|
||||
<human-check>
|
||||
Confirm the isolated PHP instance is running against the parity-only SQLite database, not the developer's own database, before any recording begins.
|
||||
</human-check>
|
||||
<resume-signal>Type "ready" once the isolated PHP instance is reachable at 127.0.0.1:8423.</resume-signal>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>The isolated PHP instance is reachable at 127.0.0.1:8423 and backed by the parity-only SQLite database.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Record the 15 /_user/api/v1 routes and the nuxt-auth flow</name>
|
||||
<files>
|
||||
../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
|
||||
</files>
|
||||
<read_first>
|
||||
../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)
|
||||
</read_first>
|
||||
<action>
|
||||
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). 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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>go test ./parity/... -run 'TestParityCorpus|TestDBCapture' -short</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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`
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Verify no secrets leaked and the corpus replays green</name>
|
||||
<files>../fonoteka.go/parity/fixtures/routes, ../fonoteka.go/parity/fixtures/nuxt/nuxt-auth.yaml, ../fonoteka.go/parity/manifest.yaml</files>
|
||||
<read_first>the fixture files Task 2 recorded, ../fonoteka.go/parity/manifest.yaml</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>! 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</automated>
|
||||
</verify>
|
||||
<human-check>
|
||||
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.
|
||||
</human-check>
|
||||
<resume-signal>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.</resume-signal>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<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>
|
||||
|
||||
<verification>
|
||||
`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).
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/07-user-plugin-and-authentication/07-05-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user