diff --git a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VALIDATION.md b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VALIDATION.md index 282d1ec1..dfce0d3c 100644 --- a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VALIDATION.md +++ b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-VALIDATION.md @@ -4,8 +4,8 @@ slug: "oidc-provider-on-wristband" # status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6) # audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117) status: draft -nyquist_compliant: false -wave_0_complete: false +nyquist_compliant: true +wave_0_complete: true created: "2026-10-19" --- @@ -20,49 +20,49 @@ created: "2026-10-19" | Property | Value | |----------|-------| | **Framework** | Go `testing` + `net/http/httptest`; testcontainers-go v0.44.0 (Postgres) | -| **Config file** | none in framework; plugin repo gets `testmain_test.go` (`flag.Parse()` + `testing.Short()`, pattern of `sm-pgstripe-plugin`) | +| **Config file** | none in framework; plugin repo has `testmain_test.go` (`flag.Parse()` + `testing.Short()`, pattern of `sm-pgstripe-plugin`) | | **Quick run command** | `go vet ./... && go test -short ./modules/wristband/...` (plugin repo: `go test -short ./...`) | -| **Full suite command** | `go test ./...` + `cd ../sm-oauthserver-plugin && go test ./...` + `cd ../fonoteka.go && go test ./...` (D-09 gate) | -| **Estimated runtime** | ~30 s quick, ~180 s full (Docker) | +| **Full suite command** | `bash scripts/check-phase19.4.sh --all` (summercms.go, sm-oauthserver-plugin, sm-user-plugin, the application's OAuth suites; Docker host) | +| **Estimated runtime** | ~30 s quick, ~6 min `--all` with Docker | --- ## Sampling Rate - **After every task commit:** Run the quick command for the repo the task touched -- **After every plan wave:** Run the full suite command, including the fonoteka regression -- **Before `/msd-verify-work`:** `scripts/check-phase19.4.sh --all` and the full suite must be green +- **After every plan wave:** Run the full suite command, including the application regression (D-09) +- **Before `/msd-verify-work`:** `scripts/check-phase19.4.sh --all` must be green (docker ran or NOT RUN; parity NOT RUN is the deferred D-14 recording) - **Max feedback latency:** 60 seconds --- ## Per-Task Verification Map -Filled in by the planner per task; seed rows from RESEARCH.md "Phase Requirements → Test Map": +Commands run from summercms.go; `go -C ../sm-oauthserver-plugin` and `go -C ../fonoteka.go` reach the sibling repositories. Gate stages: `go`, `plugin`, `userplugin`, `fonoteka` (Docker-free), `docker` (testcontainers), `parity`, `docs`. | Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | |---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| -| 19.4-01-01 | 01 | 1 | SC-1, SC-2, D-06, D-09, D-16 | T-19.4-07, T-19.4-08, T-19.4-SC | decision note before code; id_token verifies against served JWKS; aud string, nonce, at_hash | tracer (memory fakes) | `go test -count=1 -short -run '^TestOIDCTracerSmoke$' -v ./modules/wristband` | ❌ W0 | ⬜ pending | -| 19.4-01-02 | 01 | 1 | SC-3, D-04, D-07, D-08, D-14 | T-19.4-01, T-19.4-04, T-19.4-05 | rendered local errors, one-shot consent token, kind isolation, PHP encoding | smoke | `go test -count=1 -short -run '^(TestOIDCAuthorizeStartSmoke\|TestOIDCResumeDecideSwitchSmoke\|TestKindIsolationSmoke\|TestPHPRedirectEncodingSmoke)$' ./modules/wristband` | ❌ | ⬜ pending | -| 19.4-01-03 | 01 | 1 | SC-2, D-12, D-15, D-17 | T-19.4-02, T-19.4-03, T-19.4-06, T-19.4-10, T-19.4-12 | burn-then-check replay revocation, uniform 401, mixed rings, pairwise, opt-in refresh | smoke | `go test -count=1 -short -run '^(TestOIDCTokenErrorsSmoke\|TestOIDCReplayRevokesSmoke\|TestOIDCUserInfoSmoke\|TestMixedKeyRingSmoke\|TestPairwiseSubjectSmoke\|TestOIDCRefreshOptInSmoke)$' ./modules/wristband` | ❌ | ⬜ pending | -| 19.4-01-04 | 01 | 1 | SC-5, D-09, folded todo | T-19.4-14 | neutral Resource default; fonoteka unchanged | regression + docs | `go -C ../fonoteka.go test -count=1 -short ./plugins/golem15/fonoteka/... && go test -count=1 -run '^TestDocsTree$' ./cmd/summer` | ✅ | ⬜ pending | -| 19.4-02-01 | 02 | 2 | SC-2, D-01, D-02, D-10, D-19 | T-19.4-15, T-19.4-16 | encrypted private PEM, one active key under a race | tracer (Postgres) | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestOIDCFlowSmoke\|TestKeyRingFirstKeyRaceSmoke)$' ./...` | ❌ W0 | ⬜ pending | -| 19.4-02-02 | 02 | 2 | D-03, D-04, D-05, D-18 | T-19.4-17, T-19.4-18, T-19.4-20, T-19.4-21, T-19.4-22, T-19.4-25 | frame headers, sanitised branding, login handoff, switch blacklists jti, bucket keys, route guard | Postgres smoke | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestConsentPageBrandingSmoke\|TestLoginHandoffAndSwitchSmoke\|TestBucketKeysSmoke\|TestRouteGuardSmoke)$' ./...` | ❌ | ⬜ pending | -| 19.4-02-03 | 02 | 2 | SC-3, D-02, D-05, D-06 | T-19.4-19, T-19.4-23, T-19.4-24, T-19.4-26 | cross-user revoke 404, cascade on delete and deactivate, MCP off by default | Postgres smoke | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestAuthorizationsSmoke\|TestUserCascadeSmoke\|TestMCPStoreSmoke)$' ./...` | ❌ | ⬜ pending | -| 19.4-02-04 | 02 | 2 | D-16 | T-19.4-SC | blocking human package check before go-oidc and x/oauth2 | checkpoint | — (human reply recorded in SUMMARY) | — | ⬜ pending | -| 19.4-02-05 | 02 | 2 | SC-2, D-16 | T-19.4-07 | stock RP verifies id_token and at_hash with RS256 and ES256 | interop | `go -C ../sm-oauthserver-plugin test -count=1 -run '^TestRelyingPartyInterop$' -v ./...` | ❌ | ⬜ pending | -| 19.4-03-01 | 03 | 3 | SC-4, D-11, D-19 | T-19.4-28, T-19.4-29 | original kids, old tokens and secrets work after import | tracer (Postgres) | `go -C ../sm-oauthserver-plugin test -count=1 -run '^TestImportWinterCutoverSmoke$' ./...` | ❌ | ⬜ pending | -| 19.4-03-02 | 03 | 3 | D-03, D-08 | T-19.4-27, T-19.4-32 | secret revealed once, hash never exposed, permissions | Postgres smoke | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestClientsAdminSmoke\|TestBrandingAdminSmoke)$' ./...` | ❌ | ⬜ pending | -| 19.4-03-03 | 03 | 3 | SC-3, D-13 | T-19.4-30, T-19.4-33 | grace window, daily prune, opt-in rotation | Postgres smoke | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestRotateAndPruneSmoke\|TestScheduleSmoke)$' ./...` | ❌ | ⬜ pending | -| 19.4-03-04 | 03 | 3 | D-14 | T-19.4-31 | parity suite prepared, replay skips without recordings | setup | `go -C ../sm-oauthserver-plugin test -count=1 ./...` | ❌ | ⬜ pending | -| 19.4-03-05 | 03 | 3 | D-14 | T-19.4-31 | human starts WavePath; vars file mode 0600 outside the repos | checkpoint | — (human action) | — | ⬜ pending | -| 19.4-03-06 | 03 | 3 | D-14 | T-19.4-31 | Go replays PHP recordings; no secrets under testdata | parity | `PHASE194_REQUIRE_PARITY=1 go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestPHPParityReplay\|TestIDTokenShapeParity)$' ./...` | ❌ | ⏭ deferred to the WavePath cutover (D-14 amended 2026-10-19; todo 2026-10-19-record-php-oidc-parity-against-wavepath) | -| 19.4-03-07 | 03 | 3 | SC-4 | all T-19.4 | ASVS L1 review, every threat verified, fixed or accepted | review | `test -s .planning/phases/19.4-oidc-provider-on-wristband/19.4-SECURITY-REVIEW.md` | ❌ | ⬜ pending | -| 19.4-04-01 | 04 | 4 | SC-5 | T-19.4-35 | gate fails closed on skip, fail, missing name | gate | `bash scripts/check-phase19.4.sh --self-test` | ❌ W0 | ⬜ pending | -| 19.4-04-02 | 04 | 4 | SC-5, D-07, D-08, D-12, D-15, D-17 | T-19.4-01..14 | wristband PHP protocol cases and Go-only cases | unit | `bash scripts/check-phase19.4.sh go` | ❌ | ⬜ pending | -| 19.4-04-03 | 04 | 4 | SC-5, D-03, D-04, D-05, D-14 | T-19.4-17..25 | plugin feature and security cases on Postgres | unit | `bash scripts/check-phase19.4.sh docker` | ❌ | ⬜ pending | -| 19.4-04-04 | 04 | 4 | SC-5, D-10..D-13, D-18, D-19 | T-19.4-15, T-19.4-16, T-19.4-27..30 | internals and the complete PHP method map | unit | `go -C ../sm-oauthserver-plugin test -count=1 -run '^TestPHPMethodMap$' ./...` | ❌ | ⬜ pending | -| 19.4-04-05 | 04 | 4 | SC-5, D-05, D-09 | T-19.4-26 | sm-user-plugin additions tested; full gate green | unit + gate | `bash scripts/check-phase19.4.sh --all` | ❌ | ⬜ pending | +| 19.4-01-01 | 01 | 1 | SC-1, SC-2, D-06, D-09, D-16 | T-19.4-07, T-19.4-08, T-19.4-SC | decision note before code; id_token verifies against served JWKS; aud string, nonce, at_hash | tracer (memory fakes) | `go test -count=1 -short -run '^TestOIDCTracerSmoke$' -v ./modules/wristband` (gate `go`; note checked by gate `docs`) | ✅ | ✅ green | +| 19.4-01-02 | 01 | 1 | SC-3, D-04, D-07, D-08, D-14 | T-19.4-01, T-19.4-04, T-19.4-05 | rendered local errors, one-shot consent token, kind isolation, PHP encoding | smoke + unit | `bash scripts/check-phase19.4.sh go` (TestOIDCAuthorizeStartSmoke, TestOIDCResumeDecideSwitchSmoke, TestKindIsolationSmoke, TestPHPRedirectEncodingSmoke; TestAuthorizeValidationOrder, TestConsentCSRF, TestKindIsolation, TestPHPQueryEncoding) | ✅ | ✅ green | +| 19.4-01-03 | 01 | 1 | SC-2, D-12, D-15, D-17 | T-19.4-02, T-19.4-03, T-19.4-06, T-19.4-10, T-19.4-12 | burn-then-check replay revocation, uniform 401, mixed rings, pairwise, opt-in refresh | smoke + unit | `bash scripts/check-phase19.4.sh go` (token smokes; TestPKCEAndReplay, TestTokenIssuer, TestUserInfoUniform401, TestMixedRingJWKS, TestPairwiseSubjects, TestRefreshOptIn) | ✅ | ✅ green | +| 19.4-01-04 | 01 | 1 | SC-5, D-09, folded todo | T-19.4-14 | neutral Resource default; the application's MCP flavour unchanged | regression + docs | `bash scripts/check-phase19.4.sh fonoteka && bash scripts/check-phase19.4.sh docs` (TestOAuthResourceExplicit in gate `docker`) | ✅ | ✅ green | +| 19.4-02-01 | 02 | 2 | SC-2, D-01, D-02, D-10, D-19 | T-19.4-15, T-19.4-16 | encrypted private PEM, one active key under a race | tracer (Postgres) | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestOIDCFlowSmoke\|TestKeyRingFirstKeyRaceSmoke\|TestKeyRingMultiInstanceRace)$' ./...` (gate `docker`) | ✅ | ✅ green | +| 19.4-02-02 | 02 | 2 | D-03, D-04, D-05, D-18 | T-19.4-17, T-19.4-18, T-19.4-20, T-19.4-21, T-19.4-22, T-19.4-25 | frame headers, sanitised branding, login handoff, switch blacklists jti, bucket keys, route guard | Postgres smoke + unit | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestConsentPageBrandingSmoke\|TestLoginHandoffAndSwitchSmoke\|TestBucketKeysSmoke\|TestRouteGuardSmoke\|TestConsentPageSecurity\|TestRouteGuard\|TestBuckets\|TestBrandingSanitisers)$' ./...` | ✅ | ✅ green | +| 19.4-02-03 | 02 | 2 | SC-3, D-02, D-05, D-06 | T-19.4-19, T-19.4-23, T-19.4-24, T-19.4-26 | cross-user revoke 404, cascade on delete and deactivate, MCP off by default | Postgres smoke + unit | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestAuthorizationsSmoke\|TestUserCascadeSmoke\|TestMCPStoreSmoke\|TestRevokeAuthorization\|TestUserDeleteCascade\|TestMCPStore)$' ./...` | ✅ | ✅ green | +| 19.4-02-04 | 02 | 2 | D-16 | T-19.4-SC | blocking human package check before go-oidc and x/oauth2 | checkpoint | — (human reply "approved" recorded in 19.4-02-SUMMARY.md) | — | ✅ answered | +| 19.4-02-05 | 02 | 2 | SC-2, D-16 | T-19.4-07 | stock RP verifies id_token and at_hash with RS256 and ES256 | interop | `go -C ../sm-oauthserver-plugin test -count=1 -run '^TestRelyingPartyInterop$' -v ./...` (gate `docker`) | ✅ | ✅ green | +| 19.4-03-01 | 03 | 3 | SC-4, D-11, D-19 | T-19.4-28, T-19.4-29 | original kids, old tokens and secrets work after import | tracer (Postgres) + unit | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestImportWinterCutoverSmoke\|TestImportWinter\|TestImportKeyDirErrors)$' ./...` | ✅ | ✅ green | +| 19.4-03-02 | 03 | 3 | D-03, D-08 | T-19.4-27, T-19.4-32 | secret revealed once, hash never exposed, permissions | Postgres smoke + unit | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestClientsAdminSmoke\|TestBrandingAdminSmoke\|TestClientRegistry\|TestAdminSurfaceHidesCredentials)$' ./...` | ✅ | ✅ green | +| 19.4-03-03 | 03 | 3 | SC-3, D-13 | T-19.4-30, T-19.4-33 | grace window, daily prune, opt-in rotation | Postgres smoke + unit | `go -C ../sm-oauthserver-plugin test -count=1 -run '^(TestRotateAndPruneSmoke\|TestScheduleSmoke\|TestKeyRing\|TestPruneExpired\|TestRotateKeysCommand\|TestScheduleConfig)$' ./...` | ✅ | ✅ green | +| 19.4-03-04 | 03 | 3 | D-14 | T-19.4-31 | parity suite prepared, replay skips without recordings | setup | `go -C ../sm-oauthserver-plugin test -count=1 -short -run '^TestParityFixturesHoldNoSecrets$' ./...` (gate `plugin`) | ✅ | ✅ green | +| 19.4-03-05 | 03 | 3 | D-14 | T-19.4-31 | human starts the PHP site; vars file mode 0600 outside the repos | checkpoint | — (human reply "skip-parity", D-14 amended 2026-10-19) | — | ✅ answered | +| 19.4-03-06 | 03 | 3 | D-14 | T-19.4-31 | Go replays PHP recordings; no secrets under testdata | parity | `bash scripts/check-phase19.4.sh parity` (NOT RUN without recordings; `PHASE194_REQUIRE_PARITY=1` fails) | ✅ | ⏭ deferred to the WavePath cutover (D-14 amended 2026-10-19; todo 2026-10-19-record-php-oidc-parity-against-wavepath) | +| 19.4-03-07 | 03 | 3 | SC-4 | all T-19.4 | ASVS L1 review, every threat verified, fixed or accepted | review | `test -s .planning/phases/19.4-oidc-provider-on-wristband/19.4-SECURITY-REVIEW.md` | ✅ | ✅ green | +| 19.4-04-01 | 04 | 4 | SC-5 | T-19.4-35 | gate fails closed on skip, fail, missing name; NOT RUN only for docker and parity and only without the REQUIRE variables | gate | `bash scripts/check-phase19.4.sh --self-test` | ✅ | ✅ green | +| 19.4-04-02 | 04 | 4 | SC-5, D-07, D-08, D-12, D-15, D-17 | T-19.4-01..14 | wristband PHP protocol cases and Go-only cases | unit | `bash scripts/check-phase19.4.sh go` | ✅ | ✅ green | +| 19.4-04-03 | 04 | 4 | SC-5, D-03, D-04, D-05, D-14 | T-19.4-17..25 | plugin feature and security cases on Postgres | unit | `bash scripts/check-phase19.4.sh docker` | ✅ | ✅ green | +| 19.4-04-04 | 04 | 4 | SC-5, D-10..D-13, D-18, D-19 | T-19.4-15, T-19.4-16, T-19.4-27..30 | internals and the complete PHP method map (171 methods, 0 not-applicable) | unit | `go -C ../sm-oauthserver-plugin test -count=1 -short -run '^TestPHPMethodMap$' ./...` (gate `plugin`) | ✅ | ✅ green | +| 19.4-04-05 | 04 | 4 | SC-5, D-05, D-09 | T-19.4-26 | sm-user-plugin additions tested; full gate green | unit + gate | `bash scripts/check-phase19.4.sh --all` | ✅ | ✅ green | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* @@ -70,10 +70,10 @@ Filled in by the planner per task; seed rows from RESEARCH.md "Phase Requirement ## Wave 0 Requirements -- [ ] New repo `sm-oauthserver-plugin` with `go.mod`, `go.work`/replace to `../summercms.go`, `testmain_test.go` -- [ ] Decision note `.planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md` -- [ ] `scripts/check-phase19.4.sh` (named-test detector, fail-closed like 19.3) -- [ ] Human verification before adding `coreos/go-oidc/v3` and `golang.org/x/oauth2` (test-only) +- [x] New repo `sm-oauthserver-plugin` with `go.mod`, `go.work`/replace to `../summercms.go`, `testmain_test.go` +- [x] Decision note `.planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md` +- [x] `scripts/check-phase19.4.sh` (named-test detector, fail-closed like 19.3) +- [x] Human verification before adding `coreos/go-oidc/v3` and `golang.org/x/oauth2` (test-only) --- @@ -81,18 +81,19 @@ Filled in by the planner per task; seed rows from RESEARCH.md "Phase Requirement | Behavior | Requirement | Why Manual | Test Instructions | |----------|-------------|------------|-------------------| -| tide recording of the PHP OIDC contract | SC-4 / D-14 | needs the local WavePath app running | start WavePath locally, run the recording, commit fixtures under the plugin `testdata/parity/` | +| tide recording of the PHP OIDC contract | SC-4 / D-14 | needs the local PHP site running; deferred to the WavePath cutover (D-14 amended 2026-10-19) | the todo `2026-10-19-record-php-oidc-parity-against-wavepath`: record the ten spec flows, then `PHASE194_REQUIRE_PARITY=1 bash scripts/check-phase19.4.sh parity` | | Consent/error HTML honours branding | D-03 | visual | open the consent page with branding settings set and compare against PHP | +| Clients and Branding admin screens render usably | D-03, D-08 | the SPA rendering of the partials and widgets | open both screens in the embedded admin SPA: secret reveal callout, scopes taglist, authorized-users table, regenerate button, Settings link | --- ## Validation Sign-Off -- [ ] All tasks have `` verify or Wave 0 dependencies -- [ ] Sampling continuity: no 3 consecutive tasks without automated verify -- [ ] Wave 0 covers all MISSING references -- [ ] No watch-mode flags -- [ ] Feedback latency < 60s -- [ ] `nyquist_compliant: true` set in frontmatter +- [x] All tasks have `` verify or Wave 0 dependencies +- [x] Sampling continuity: no 3 consecutive tasks without automated verify +- [x] Wave 0 covers all MISSING references +- [x] No watch-mode flags +- [x] Feedback latency < 60s (quick command) +- [x] `nyquist_compliant: true` set in frontmatter -**Approval:** pending +**Approval:** pending (`/msd-validate-phase` sets the status)