docs(phase-08): complete phase execution

This commit is contained in:
Jakub Zych
2026-09-24 01:13:55 +02:00
parent 0fcd06fde8
commit 23f295824c
2 changed files with 131 additions and 8 deletions

View File

@@ -2,9 +2,9 @@
gsd_state_version: 1.0 gsd_state_version: 1.0
milestone: v1.0 milestone: v1.0
milestone_name: milestone milestone_name: milestone
status: verifying status: ready_to_plan
stopped_at: Completed 08-10-PLAN.md (Phase 8 closed; Playwright UI matrix gap carried forward) stopped_at: Phase 08 complete (10/10) — ready to discuss Phase 09
last_updated: "2026-09-23T23:03:36.043Z" last_updated: 2026-09-23T23:13:48.826Z
last_activity: 2026-09-23 last_activity: 2026-09-23
progress: progress:
total_phases: 15 total_phases: 15
@@ -21,13 +21,13 @@ progress:
See: .planning/PROJECT.md (updated 2026-09-16) See: .planning/PROJECT.md (updated 2026-09-16)
**Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test. **Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test.
**Current focus:** Phase 08 — oauth2-1-authorization-server **Current focus:** Phase 09 — backend admin authentication and schema pipeline
## Current Position ## Current Position
Phase: 08 (oauth2-1-authorization-server) — EXECUTING Phase: 09
Plan: 10 of 10 Plan: Not started
Status: Phase complete — ready for verification Status: Ready to plan
Last activity: 2026-09-23 Last activity: 2026-09-23
Progress: [██████████] 100% Progress: [██████████] 100%
@@ -36,7 +36,7 @@ Progress: [██████████] 100%
**Velocity:** **Velocity:**
- Total plans completed: 56 - Total plans completed: 66
- Average duration: 21 min - Average duration: 21 min
- Total execution time: 104 min - Total execution time: 104 min
@@ -51,6 +51,7 @@ Progress: [██████████] 100%
| 05 | 6 | - | - | | 05 | 6 | - | - |
| 06 | 14 | - | - | | 06 | 14 | - | - |
| 7 | 8 | - | - | | 7 | 8 | - | - |
| 08 | 10 | - | - |
**Recent Trend:** **Recent Trend:**

View File

@@ -0,0 +1,122 @@
---
phase: 08-oauth2-1-authorization-server
verified: 2026-09-23T23:12:12Z
status: passed
score: 8/8 must-haves verified (4 roadmap success criteria + 4 representative plan-level truth clusters; all 20 plan-level truths across 10 plans independently spot-checked against source)
overrides_applied: 1
overrides:
- must_have: "scripts/check-phase8-ui.mjs --final-gate's Playwright browser matrix (32 UI-SPEC scenarios) validates the unchanged Nuxt consent/connected-apps UI"
reason: "Deliberate, self-documenting fatal() left by 08-05 as 08-10's seam (never a stub or skip). Every other check-phase8.sh stage ran green in the sole full 2026-09-24 gate execution (real unchanged fonoteka-mcp lifecycle: discovery, DCR, PKCE authorize, JWT consent, token, tool call, refresh, replay, revoke, post-revoke failure; both repos' vet/test/race; 169/169 parity corpus; secret scan; return-path 6/6; i18n 74 keys; security review 11/11 closed). AUTH-05/06/07's requirement text does not mandate a Playwright-verified browser regression suite. Verifier independently confirmed --contract-self-test and --security-review-only both pass, and that the JS source genuinely runs verify:oauth-return-path/verify:oauth-i18n for real before the deliberate fatal(), not a masked stub."
accepted_by: "user (checkpoint decision, 08-10 Task 3: 'Approve, carry gap forward')"
accepted_at: "2026-09-24T00:52:00Z"
---
# Phase 8: OAuth2.1 authorization server Verification Report
**Phase Goal:** A direct standard-library OAuth2.1-style authorization server (`wristband`) implements the RFC 8414/6749/7591/8707 contract needed by fonoteka-mcp and the ChatGPT connector unchanged. The backend preserves its exact Basic invalid-client challenge and existing personal-token 401, while fonoteka-mcp retains ownership of RFC 9728 protected-resource metadata and its rich Bearer challenge. Security-load-bearing: bearer tokens, PKCE, replay-family revocation, and constant-time secret comparison all live here.
**Verified:** 2026-09-23T23:12:12Z (re-verification run against a phase that carries a prior 2026-09-24 checkpoint-approved gap)
**Status:** passed
**Re-verification:** No — this is the first `/gsd:verify-work` pass; the referenced "checkpoint" in `known_state` was an in-phase human-verify gate (08-10 Task 3), not a prior VERIFICATION.md.
**Note on ROADMAP `mode: mvp`:** ROADMAP.md marks Phase 8 `Mode: mvp`, but the phase goal is not formatted as a User Story (`gsd-sdk query user-story.validate` returns `valid: false` — no "As a ... I want to ... so that ..." shape). This is a metadata/goal-format mismatch in the roadmap, not something this phase's implementation controls. Standard (non-MVP-narrowed) goal-backward verification was applied instead, using the four ROADMAP Success Criteria plus the merged `must_haves` from all 10 PLAN.md files, which is the correct fallback per this workflow's own MVP-mode guard ("do not attempt to verify against a non-User Story goal under MVP mode"). This should be flagged for a future `/gsd mvp-phase 8` correction or a roadmap edit removing `mode: mvp`, but it does not affect the substance of this verification.
## Goal Achievement
### Observable Truths (ROADMAP Success Criteria)
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | RFC 8414 metadata served unenveloped at well-known path; authorize with PKCE + consent screen; token issues/refreshes authorization_code and refresh_token as unwrapped RFC 6749 bodies with PHP cache headers | VERIFIED | `wristband/server.go` metadata struct field-for-field matches PHP contract (`response_types_supported`, `grant_types_supported`, `code_challenge_methods_supported`, `token_endpoint_auth_methods_supported`, `scopes_supported`, `service_documentation`, `authorization_response_iss_parameter_supported`, no `jwks_uri`/envelope); `wristband/token.go:145-146` sets `Cache-Control: no-store, private` + `Pragma: no-cache` on success; `go test ./wristband -count=1` green (independently re-run); `go test ./plugins/golem15/fonoteka -run '^TestOAuth' -v` — 30/30 PASS including `TestOAuthAuthorizeAssembled` |
| 2 | RFC 7591 DCR and RFC 8707 resource tolerance work against a real client registration call | VERIFIED | `wristband/register.go` + `registration_test.go` (cap, sweep, oversized-body tests independently confirmed passing); `Options.Resource` default `https://mcp.plytarium.com/mcp` confirmed in `wristband/server.go:100`; manifest entry `POST /oauth/mcp/register oauth` = `ported` in `../fonoteka.go/parity/manifest.yaml`; the 2026-09-24 `scripts/check-phase8.sh` full run (documented in 08-10-SUMMARY.md, deferred-items.md, STATE.md, cross-checked, not independently re-executed per explicit `known_state` instruction not to re-run the multi-minute Docker/MCP gate) drove real DCR against the real fonoteka-mcp process |
| 3 | Token endpoint returns exactly `WWW-Authenticate: Basic realm="OAuth"` on `invalid_client`; backend personal-token 401 unchanged with no added challenge; fonoteka-mcp's own rich Bearer challenge + protected-resource metadata verified through its actual discovery flow, not just a unit test | VERIFIED | `wristband/token.go:110-111` sets exactly `Basic realm="OAuth"` before `writeTokenError(..., "invalid_client")`; `plugins/golem15/fonoteka/middleware/token_scope.go:17` still writes the unchanged bare `{"error":"Invalid token"}` 401 with no new header (grepped directly, no `WWW-Authenticate` call in `token_guard.go`/`token_scope.go`); the "real discovery flow" clause is satisfied by the 2026-09-24 real unchanged-`fonoteka-mcp` lifecycle run documented across 08-10-SUMMARY.md/deferred-items.md/STATE.md (not re-executed by this verifier per explicit instruction — see Note below) |
| 4 | Connected apps listed/revoked; `OAuthClient`/`OAuthAuthCode`/`OAuthRefreshToken` models persist; fonoteka-mcp install/auth flow unchanged; client-secret comparison uses `crypto/subtle.ConstantTimeCompare` | VERIFIED | `wristband/crypto.go:33-34` `constantEqual` wraps `subtle.ConstantTimeCompare`, used at `token.go:197` (client secret) and `:210` (PKCE); `ConnectedAppsIndex`/`ConnectedAppsDestroy` mounted in `routes.go:22-23`; `oauth_store.go` uses real GORM transactions + `clause.Locking{Strength:"UPDATE"}` row locks (lines 45, 76, 132, 201, 218, 248, 262); `go test ./plugins/golem15/fonoteka/classes/auth -run '^TestOAuth' -count=1` green (14.5s, real Postgres via testcontainers) |
**Score:** 4/4 ROADMAP success criteria verified.
### Plan-Level Must-Haves (merged from 10 PLAN.md frontmatter, 20 truths total)
All 20 plan-level truths (D-01 through D-21, several plans sharing a decision ID) were cross-checked against source, not SUMMARY text. Representative spot-checks, all independently confirmed:
| Plan | Truth (paraphrased) | Status | Evidence |
|------|----------------------|--------|----------|
| 08-01 | RFC 8414 metadata without zitadel/oidc; PHP-minimal shapes, no response hooks | VERIFIED | `wristband/server.go`; no zitadel import in `go.mod`; `go vet`/`go test ./wristband` green |
| 08-02 | Config defaults (600s/600s/3600s/30d/200/24h); transaction-scoped GORM adapter | VERIFIED | `wristband/server.go:92-105` `DefaultOptions()` matches exactly; `oauth_store.go:45` `db.Transaction(...)` |
| 08-03 | Authorize reads query only; ordered RFC3986 errors, S256/scope/resource policy and pending-request creation live in wristband | VERIFIED | `wristband/authorize.go:38` `r.URL.Query()`; `authorize_test.go` ordered-redirect tests pass |
| 08-04 | Token rejects JSON before ParseForm; constant-time comparisons; code replay has one winner | VERIFIED | `wristband/token.go`; `TestTokenCodeConcurrentReplayHasExactlyOneWinner` and Postgres equivalent both pass |
| 08-05 | JWT consent returns exact unchanged-Nuxt payloads, server-derives scopes/collection ids; authorize/token stay raw, consent stays JWT-grouped | VERIFIED | `routes.go` — `oauth/consent` etc. inside `jwt.auth` group (line 11-42), `oauth/mcp/*` inside `GroupRaw` (line 71-76) |
| 08-06 | Refresh rotation revokes prior access token; replay commits whole-lineage revocation | VERIFIED | `oauth_store.go:243` lineage walk; `TestOAuthRefreshReplayRevokesLineageAndBothAccessTokens` passes (confirmed via `classes/auth` package run) |
| 08-07 | Operators can create/update/list OAuth clients with one-time secret output; command stores only a hash | VERIFIED | `console/oauth_client.go` exists; `go test ./plugins/golem15/fonoteka/console -run TestOAuth` green |
| 08-08 | `/me` returns exact scopes/collection_ids/user_id/name; invalid tokens keep exact `Invalid token` bytes, no new challenge | VERIFIED | `controllers/api/me_token_controller.go` matches contract; `middleware/token_scope.go:17` unchanged body confirmed by grep |
| 08-09 | 9 manifest routes + `mcp-lifecycle` replay and count as ported only after passing | VERIFIED | `parity/manifest.yaml` — all 9 relevant OAuth entries `status: ported`; `go test ./parity -run 'TestOAuthFlows|TestPHPTestMap'` green with real Postgres |
| 08-10 | 103/103 PHP method map; 11/11 T-08 threats closed | VERIFIED | `08-PHP-TEST-MAP.md` distribution matches `TestPHPTestMap` (independently re-run, PASS); `08-SECURITY-REVIEW.md` frontmatter `threats_closed: 11`, `threats_open: 0`; `scripts/check-phase8.sh --security-review-only` independently re-run, passed |
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `wristband/server.go`, `authorize.go`, `token.go`, `register.go`, `consent.go`, `crypto.go`, `stores.go`, `client_issue.go`, `redirect_html.go` | App-agnostic OAuth2.1 server package | VERIFIED | All present, `go vet`/`go test` green, no `fonoteka` import (checked via file inspection) |
| `../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go` | Transactional GORM store with row locks | VERIFIED | Real `Transaction`/`clause.Locking{Strength:"UPDATE"}` calls present |
| `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go`, `me_token_controller.go` | Consent + MCP bootstrap endpoints | VERIFIED | Both exist, mounted in `routes.go`, tests pass |
| `../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go` | `fonoteka:oauth-client` command | VERIFIED | Exists, tests pass |
| `../fonoteka.go/plugins/golem15/fonoteka/updates/21_oauth_schema_correction.go` | Additive nullability/index correction migration | VERIFIED | Exists with paired test file |
| `../fonoteka.go/parity/oauth_flow_test.go`, `oauth_audit_test.go`, `parity/fixtures/mcp/mcp-lifecycle.yaml` | Replay + 103-method audit + recorded lifecycle fixture | VERIFIED | All present; audit test and flow test independently re-run, PASS |
| `.planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md`, `08-SECURITY-REVIEW.md` | Coverage map + security review | VERIFIED | 103-row distribution matches test map; 11/11 threats closed |
| `scripts/check-phase8.sh`, `check-phase8-ui.mjs`, `check-phase8-mcp-client.mjs`, `check-phase8-red.sh` | Final unchanged-MCP + UI gate family | PARTIAL (documented) | `--contract-self-test` and `--security-review-only` independently re-run, both pass; `--final-gate`'s Playwright stage is a deliberate fatal() — see Override above |
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `routes.go` (raw group) | `wristband.Server.Metadata/Authorize/Register/Token` | `r.GroupRaw(...)` | WIRED | Confirmed by direct grep of `routes.go:71-76`; `GroupRaw` refuses house middleware at build time (Phase 6 invariant) |
| `routes.go` (jwt group) | `oauth_consent_controller.go`, connected-apps handlers | `r.Group("/_fonoteka/api/v1", surf.Use("jwt.auth",...))` | WIRED | Confirmed lines 11-42 |
| `routes.go` (`inv_token` group) | `me_token_controller.go` | `r.Group("/api/v1/fonoteka", surf.Use("inv_token",...))` | WIRED | Confirmed line 47-50 |
| `wristband/token.go` | `oauth_store.go` (Backend interface) | `Server.SetBackend` / `Tx` interface | WIRED | `plugin.go` constructs `wristband.NewServer` then wires the GORM-backed adapter; store methods use real transactions/locks |
| `oauth_client.go` (console) | wristband client issuance | direct call | WIRED | Console tests exercise real issuance path, pass |
| `08-SECURITY-REVIEW.md` | named Go tests | citation-to-test mapping | WIRED | Every cited `file:TestName` spot-checked exists and passes (independently re-run for a representative subset: PKCE, code-replay, refresh-replay, DCR-flood, cross-user tests) |
### Requirements Coverage
| Requirement | Source Plans | Description | Status | Evidence |
|-------------|--------------|--------------|--------|----------|
| AUTH-05 | 08-01, 08-02, 08-03, 08-04, 08-05, 08-06, 08-07, 08-09, 08-10 | Direct stdlib OAuth2.1 server: metadata, PKCE, grants, DCR, RFC 8707, exact Basic challenge, unchanged 401, fonoteka-mcp-owned RFC 9728 | SATISFIED | See truths 1-4 above; marked Complete in REQUIREMENTS.md and independently corroborated by source |
| AUTH-06 | 08-01, 08-02, 08-03, 08-04, 08-05, 08-06, 08-09, 08-10 | Form-urlencoded, CSRF-free, rate-limited, unwrapped RFC 6749 bodies with PHP cache headers | SATISFIED | `throttle:fonoteka-oauth-register`/`throttle:fonoteka-oauth-token` mounted (`routes.go:74-75`); `token.go` cache headers confirmed |
| AUTH-07 | 08-02, 08-05, 08-06, 08-07, 08-08, 08-09, 08-10 | Connected apps list/revoke; models ported; fonoteka-mcp install/auth unchanged | SATISFIED | Connected-apps handlers wired; models present (Phase 5, referenced); real fonoteka-mcp lifecycle documented green in 08-10's sole gate run |
No orphaned requirements — REQUIREMENTS.md maps only AUTH-05/06/07 to Phase 8, and all three appear across the plans' `requirements` frontmatter.
### Anti-Patterns Found
None. Grepped all OAuth-touching files in both repos for `TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER` and "not yet implemented"/"coming soon" phrasing — zero matches outside test files.
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| wristband unit/route tests | `go test ./wristband -count=1` | `ok` | PASS |
| fonoteka OAuth store (real Postgres, testcontainers) | `go test ./plugins/golem15/fonoteka/classes/auth -run '^TestOAuth' -count=1` | `ok` 14.5s | PASS |
| fonoteka OAuth controllers/routes (real Postgres) | `go test ./plugins/golem15/fonoteka -run '^TestOAuth' -v` | 30/30 PASS, 0 FAIL | PASS |
| fonoteka console command tests | `go test ./plugins/golem15/fonoteka/console -run TestOAuth` | `ok` | PASS |
| parity replay + 103-method audit (real Postgres) | `go test ./parity -run 'TestOAuthFlows|TestPHPTestMap'` | `ok`, all PASS | PASS |
| manifest route status | grep of 9 relevant OAuth entries in `parity/manifest.yaml` | all `status: ported` | PASS |
| `go vet` both repos (OAuth packages) | `go vet ./wristband/... ` / `go vet ./plugins/golem15/fonoteka/...` | clean | PASS |
### Probe Execution
| Probe | Command | Result | Status |
|-------|---------|--------|--------|
| `scripts/check-phase8.sh --contract-self-test` | `bash scripts/check-phase8.sh --contract-self-test` | "phase8 contract-self-test passed" | PASS |
| `scripts/check-phase8.sh --security-review-only` | `bash scripts/check-phase8.sh --security-review-only` | "phase8 security-review-only check passed" | PASS |
| `scripts/check-phase8.sh` (no flags, full gate) | not re-run | N/A | SKIPPED — explicit `known_state` instruction: boots Docker/app/MCP, takes many minutes, already documented green once (2026-09-24) in 08-10-SUMMARY.md/deferred-items.md/STATE.md with one named carried-forward gap (Playwright UI matrix) |
### Human Verification Required
None. The one item that would otherwise require human/browser verification — the Playwright UI matrix for `scripts/check-phase8-ui.mjs --final-gate` — already went through an in-phase human checkpoint (08-10 Task 3, 2026-09-24, "Approve, carry gap forward") and is recorded as an accepted override above, not a pending human-verification request.
### Gaps Summary
No blocking gaps. One pre-existing, user-approved, carried-forward gap (Playwright UI matrix for the Nuxt consent/connected-apps browser contract) is tracked via the override mechanism above — it does not affect any ROADMAP success criterion or plan-level must-have, was found not to hide a broken underlying contract (the two real sub-checks it does run, `verify:oauth-return-path` and `verify:oauth-i18n`, are genuinely wired and green), and `stage_ui_harness` correctly fails closed rather than silently passing. This item remains open in STATE.md's Deferred Items table and should be picked up by a future plan before Phase 15 (Cutover) if a Nuxt UI regression risk is a concern — it is not blocking Phase 8 goal achievement.
---
_Verified: 2026-09-23T23:12:12Z_
_Verifier: Claude (gsd-verifier)_