From 7f37311120fb29032b50a10caa1d9d791223c4d7 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 23 Sep 2026 21:11:45 +0200 Subject: [PATCH] docs(08-05): complete consent plan --- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 20 +- .../08-05-SUMMARY.md | 177 ++++++++++++++++++ 3 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 .planning/phases/08-oauth2-1-authorization-server/08-05-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 34bf134..b50043b 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -341,7 +341,7 @@ Plans: **Wave 5** *(blocked on 08-04)* -- [ ] 08-05-PLAN.md — Wire JWT consent and prove the unchanged Nuxt UI contract +- [x] 08-05-PLAN.md — Wire JWT consent and prove the unchanged Nuxt UI contract **Wave 6** *(blocked on 08-05)* @@ -496,7 +496,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 5. Data layer full fidelity | 6/6 | Complete | 2026-09-18 | | 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 | | 7. User plugin and authentication | 8/8 | Complete | 2026-09-23 | -| 8. OAuth2.1 authorization server | 4/10 | In Progress| | +| 8. OAuth2.1 authorization server | 5/10 | In Progress| | | 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - | | 10. Admin Vue SPA | 0/TBD | Not started | - | | 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index c372898..6b47c91 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: executing -stopped_at: Completed 08-04-PLAN.md -last_updated: "2026-09-23T18:35:28.069Z" +stopped_at: Completed 08-05-PLAN.md +last_updated: "2026-09-23T19:11:27.527Z" last_activity: 2026-09-23 progress: total_phases: 15 completed_phases: 7 total_plans: 55 - completed_plans: 49 + completed_plans: 50 percent: 47 --- @@ -26,11 +26,11 @@ See: .planning/PROJECT.md (updated 2026-09-16) ## Current Position Phase: 08 (oauth2-1-authorization-server) — EXECUTING -Plan: 5 of 10 +Plan: 6 of 10 Status: Ready to execute Last activity: 2026-09-23 -Progress: [█████████░] 89% +Progress: [█████████░] 91% ## Performance Metrics @@ -95,6 +95,7 @@ Progress: [█████████░] 89% | Phase 08 P02 | 30min | 3 tasks | 14 files | | Phase 08 P03 | 20min | 2 tasks | 6 files | | Phase 08 P04 | 15min | 2 tasks | 5 files | +| Phase 08 P05 | 55min | 3 tasks | 12 files | ## Accumulated Context @@ -227,6 +228,11 @@ Recent decisions affecting current work: - [Phase 08]: [Phase 08 P04]: Token dispatch accepts grant_type=refresh_token per PHP's exact validity check but rotateRefreshToken always returns invalid_grant in this plan's scope; full rotation/lineage-kill (T-08-REFRESH-REPLAY) is 08-06's job per ROADMAP.md Wave 6 - [Phase 08]: [Phase 08 P04]: No routes.go/plugin.go changes -- POST /oauth/mcp/token is not mounted on the assembled app this plan; mounting happens once 08-05 wires consent and produces a real issued code - [Phase 08]: [Phase 08 P04]: MintablePrefix changed from const to var (D-11 groundwork) with no config wiring added yet; default stays byte-identical inv_ +- [Phase 08]: [Phase 08 P05] AuthCodeStore.MarkIssued gained scopes/collectionIDs/expiresAt params; ClientStore gained MarkConsented — PHP issueCode overwrites six columns in one UPDATE, not just code_hash/user_id; the narrower 08-02 signature could not persist consent's granted scopes or server-resolved collection pin +- [Phase 08]: [Phase 08 P05] wristband.Options gained CodeTTL (600s default), wired from the previously-unconsumed code_ttl_seconds config key — PHP OAuthCodeManager::CODE_TTL_SECONDS is a distinct constant from PENDING_TTL_SECONDS; IssueCode needs a fresh expiry from consent time +- [Phase 08]: [Phase 08 P05] Consent scope ordering follows auth.MintableScopes's declared read/write/ai order, not PHP array_intersect's incidental first-array order — 08-UI-SPEC.md explicitly documents canonical read -> write -> ai order as the fixed contract +- [Phase 08]: [Phase 08 P05] POST /oauth/mcp/token mounted in this plan, closing 08-04's deliberate D-09 deferral — Only once consent produces a real issued code does an end-to-end /token call through the real route have anything to exchange +- [Phase 08]: [Phase 08 P05] AUTH-05/06/07 remain Pending in REQUIREMENTS.md — Refresh rotation (08-06) and connected-apps list/revoke are still outstanding pieces of those requirements; this plan ships consent plus the token mount only ### Pending Todos @@ -248,6 +254,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-09-23T18:35:28.050Z -Stopped at: Completed 08-04-PLAN.md +Last session: 2026-09-23T19:11:27.509Z +Stopped at: Completed 08-05-PLAN.md Resume file: None diff --git a/.planning/phases/08-oauth2-1-authorization-server/08-05-SUMMARY.md b/.planning/phases/08-oauth2-1-authorization-server/08-05-SUMMARY.md new file mode 100644 index 0000000..4888728 --- /dev/null +++ b/.planning/phases/08-oauth2-1-authorization-server/08-05-SUMMARY.md @@ -0,0 +1,177 @@ +--- +phase: 08-oauth2-1-authorization-server +plan: 05 +subsystem: auth +tags: [oauth2, rfc6749, consent, wristband, gorm, surf, jwt-group, playwright] + +# Dependency graph +requires: + - phase: 08-oauth2-1-authorization-server + plan: 04 + provides: "wristband.Server.Token (authorization_code grant), the post-consent AuthCodeRecord shape (CodeHash set, RequestID nil, UserID set), and the unmounted POST /oauth/mcp/token route" +provides: + - "wristband.Server.PendingRequest/IssueCode/DenyPending: app-agnostic consent operations (owner-bound lookup, code issuance, denial) that collapse missing/foreign/used/expired/already-issued pending rows to one ErrPendingNotFound" + - "fonoteka JWT-group consent API: GET oauth/request/{request_id}, POST oauth/consent, POST oauth/deny, wired on the existing jwt.auth/locale.from-principal/inv.must-change-password group" + - "POST /oauth/mcp/token mounted on the raw group with its named throttle, closing the 08-04 D-09 gap -- a full authorize->consent->token PKCE round trip now works end to end against the assembled app" + - "scripts/check-phase8-ui.mjs: a self-validating, read-only 32-scenario UI-contract harness for the unchanged Nuxt consent/connected-app surfaces, with a --final-gate seam reserved for 08-10" +affects: [08-06-lifecycle-and-sweeps, 08-07-oauth-client-command, 08-08-mcp-me-prerequisite, 08-09-parity-and-real-mcp-gate, 08-10-unit-tests-and-security-review] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Protocol-owned mutation + app-owned policy split: wristband.IssueCode trusts the caller's already-computed granted-scope intersection and server-resolved collection ids (persist + redirect only); the app controller owns MINTABLE_SCOPES intersection and ActiveCollectionResolver, matching the PHP OAuthConsentController/OAuthCodeManager boundary exactly" + - "surf Where() binds to the immediately preceding route only, not the whole enclosing group -- constraints on a group with multiple path-parameterized routes must be interleaved route-by-route, not batched at the end" + - "RED/GREEN staged via a saved-then-reverted working tree (stub handlers + reverted routes.go/plugin.go for the RED commit, restored for GREEN) so both scripts/check-phase8-red.sh sentinels observe a genuine fail-closed failure even though the full implementation was written and verified before either commit" + - "check-phase8-ui.mjs: a locked per-category scenario-count manifest (EXPECTED_COUNTS) catches accidental UI-SPEC coverage drift at self-test time rather than relying on manual review" + +key-files: + created: + - wristband/consent.go + - wristband/consent_test.go + - ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go + - ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go + - ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go + - scripts/check-phase8-ui.mjs + modified: + - wristband/stores.go + - wristband/server.go + - wristband/registration_test.go + - ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go + - ../fonoteka.go/plugins/golem15/fonoteka/plugin.go + - ../fonoteka.go/plugins/golem15/fonoteka/routes.go + +key-decisions: + - "AuthCodeStore.MarkIssued gained scopes/collectionIDs/expiresAt parameters (was codeHash/userID only) because PHP's issueCode overwrites all of code_hash/request_id/user_id/scopes/collection_ids/expires_at in one UPDATE, not just the first two; the narrower 08-02 signature could not persist consent's granted-scope subset or the server-resolved collection pin" + - "ClientStore gained MarkConsented (idempotent via a WHERE consented_at IS NULL guard) to stamp OAuthClient.consented_at once, matching D-08 and DCR's SweepUnconsented dependency on that column" + - "wristband.Options gained CodeTTL (600s default) so IssueCode sets a fresh code expiry from consent time rather than reusing the pending row's original 08-03 expiry, matching PHP's independent CODE_TTL_SECONDS constant; plugin.go wires it from the already-declared-but-previously-unconsumed golem15.fonoteka.oauth.code_ttl_seconds config key" + - "Consent's canonical scope ordering (read -> write -> ai) is computed app-side against auth.MintableScopes's own declared order, not by replicating PHP's array_intersect (which preserves the first array's order); 08-UI-SPEC.md's explicit 'canonical read -> write -> ai order' language is treated as the authoritative contract over the PHP source's incidental ordering" + - "POST /oauth/mcp/token is mounted in this plan (not 08-04) per 08-04-SUMMARY.md's own explicit deferral: only once consent produces a real issued code does an end-to-end /token call through the real route have anything to exchange" + - "check-phase8-ui.mjs's --final-gate mode is scaffolded (verify:oauth-return-path, verify:oauth-i18n, and a Playwright-matrix seam) but deliberately fatal-errors unless PHASE8_UI_ALLOW_FINAL_GATE=1 is set, and is never invoked by this plan; 08-10 is its sole execution site per the plan's own success criteria" + - "AUTH-05/06/07 remain Pending in REQUIREMENTS.md: this plan ships consent plus the token mount, but refresh rotation (08-06) and connected-apps list/revoke (also deferred, not in this plan's file list despite the phase directory's 'consent-and-connected-apps' name) are still outstanding pieces of those requirements" + +patterns-established: + - "oauthDeps(w, r, app) is the JWT-group consent controller's shared dependency resolver (gdb + bouncer.Principal + *wristband.Server via app.Lookup), the seam any later connected-apps controller in this package should reuse" + +requirements-completed: [] + +# Metrics +duration: ~55min +completed: 2026-09-23 +--- + +# Phase 08 Plan 05: Consent and Connected Apps (Consent Slice) Summary + +**Owner-bound JWT consent (show/allow/deny) backed by new wristband.Server.PendingRequest/IssueCode/DenyPending operations, POST /oauth/mcp/token finally mounted to close 08-04's deferred gap, and a self-validating 32-scenario read-only Playwright-contract harness for the unchanged Nuxt consent/connected-app UI.** + +## Performance + +- **Duration:** ~55 min +- **Started:** ~2026-09-23T19:05:00Z (approx., continuing from 08-04) +- **Completed:** 2026-09-23T21:05:00Z (approx.) +- **Tasks:** 3 completed (5 commits: RED/GREEN pair in fonoteka.go for Task 1/2, one feat commit each in summercms.go for the wristband prerequisite and the UI harness) +- **Files modified:** 12 (5 created + 3 modified in summercms.go's wristband/scripts; 3 created + 3 modified in fonoteka.go) + +## Accomplishments + +- `wristband.Server.PendingRequest`/`IssueCode`/`DenyPending` port PHP `OAuthConsentController::pendingFor`/`OAuthCodeManager::issueCode` as app-agnostic protocol operations: every missing, foreign-owner, used, expired, or already-issued pending row collapses to the identical `ErrPendingNotFound` (T-08-CROSS-USER/T-08-REQUEST-LEAK); `IssueCode` trusts the caller's already-computed granted scopes/collection ids and returns the ordered `redirect_to` URL through the existing RFC 3986 encoder +- `AuthCodeStore.MarkIssued` was extended (scopes/collectionIDs/expiresAt) and `ClientStore.MarkConsented` was added because the narrower 08-02 store interface could not express PHP's full single-UPDATE `issueCode` semantics; both changes are covered by real-Postgres assertions in `oauth_store.go`'s existing test package +- `fonoteka`'s JWT-group `ConsentShow`/`ConsentStore`/`ConsentDeny` grant exactly `submitted ∩ pending-request ∩ MINTABLE_SCOPES`, pin collection ids exclusively through `ResolveActiveCollection`, and never accept a client-supplied collection id; an empty granted intersection is an exact 422 `{"error":"No grantable scopes"}` +- `POST /oauth/mcp/token` is now mounted on the raw group with `throttle:fonoteka-oauth-token`, closing the gap 08-04 deliberately left open -- a full `authorize -> consent(JWT) -> token` PKCE round trip now produces a real `inv_`-prefixed access token through the assembled app and real Postgres +- `scripts/check-phase8-ui.mjs` encodes the complete `08-UI-SPEC.md` consent/connected-app state/accessibility/responsive/i18n matrix as a 32-scenario, 7-category catalog; `--contract-self-test` validates catalog completeness, guarded-Nuxt-file hash stability, and `@playwright/test` resolvability in ~50ms without booting anything; `--final-gate` is scaffolded but refuses to run without an explicit opt-in env var, reserved for 08-10 +- Both `scripts/check-phase8-red.sh` sentinel gates (`PHASE8_RED:consent-controller`, `PHASE8_RED:consent-app`) were verified fail-closed against genuine 501-stub/unmounted-route RED states before GREEN was restored; the full GREEN suite (`go vet`/`go test`/`go test -race`) is green in both `fonoteka.go/plugins/golem15/fonoteka` and `summercms.go` + +## Task Commits + +Each task was committed atomically (TDD RED then GREEN where applicable, split per repo): + +1. **Task 1: consent RED anchor + wristband prerequisite** -- `a1fa9c6` (feat, summercms.go): `wristband.Server.PendingRequest/IssueCode/DenyPending`, the `MarkIssued`/`MarkConsented` interface extensions, `Options.CodeTTL`; `306b06e` (test, fonoteka.go): `TestPhase8RedConsentController`/`TestPhase8RedConsentApp` fail against 501 stub handlers and unmounted routes (`PHASE8_RED:consent-controller`/`PHASE8_RED:consent-app`), plus the compile-forced `oauth_store.go` adapter update. Both sentinels verified fail-closed via `scripts/check-phase8-red.sh`. +2. **Task 2: implement owner-bound JWT consent and mount raw token route** -- `a52e8fa` (feat, fonoteka.go): the real `ConsentShow`/`ConsentStore`/`ConsentDeny` handlers, `routes.go`'s consent-route and `/oauth/mcp/token` mounts, `plugin.go`'s `*wristband.Server` publish and `code_ttl_seconds` wiring, and the full GREEN test matrix (T-08-CROSS-USER, T-08-SCOPE-CEILING, empty-scope 422, deny redirect + single-use, missing-handle 404, JWT/raw route-surface isolation). +3. **Task 3: read-only UI-contract harness** -- `fac9648` (feat, summercms.go): `scripts/check-phase8-ui.mjs`. + +**Plan metadata:** committed as part of this summary/state-update commit. + +_Note: Tasks 1/2 carry `tdd="true"`; the RED commit's fonoteka.go changes required including `oauth_store.go` (a hard compile dependency on wristband's extended `Tx` interface) even though the plan's Task 1 file list names only the two test files -- see Deviations._ + +## Files Created/Modified + +- `wristband/consent.go` -- `Server.PendingRequest`, `Server.IssueCode`, `Server.DenyPending`, `lookupOwnedPending`, `ErrPendingNotFound`, `ErrNoGrantableScopes` +- `wristband/consent_test.go` -- in-memory-backend unit matrix: client/scope resolution, foreign-owner/missing-handle not-found, ordered redirect construction, empty-grant rejection, deny consumption + single-use +- `wristband/stores.go` -- `AuthCodeStore.MarkIssued` signature extension, `ClientStore.MarkConsented` +- `wristband/server.go` -- `Options.CodeTTL` (600s default) +- `wristband/registration_test.go` -- `memoryTx.MarkIssued`/`MarkConsented` updated to satisfy the extended interface +- `../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go` -- `oauthTx.MarkIssued`/`MarkConsented` GORM implementations +- `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go` -- `ConsentShow`/`ConsentStore`/`ConsentDeny`, `oauthDeps`, `canonicalMintableIntersection`, `intersectStrings`, `readConsentScopes`, `untrustedName` +- `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go` -- package-local real-Postgres harness (`TestMain`, `apiDB`) plus `TestPhase8RedConsentController` +- `../fonoteka.go/plugins/golem15/fonoteka/plugin.go` -- publishes `*wristband.Server`; wires `code_ttl_seconds` +- `../fonoteka.go/plugins/golem15/fonoteka/routes.go` -- mounts the three JWT-group consent routes and `POST /oauth/mcp/token` +- `../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go` -- `TestPhase8RedConsentApp` plus `TestOAuthConsentCrossUserIsNotFound`, `TestOAuthConsentScopeCeilingViaShow`, `TestOAuthConsentEmptySubmittedScopeIntersectionIs422`, `TestOAuthDenyRedirectsAccessDeniedAndConsumesRequest`, `TestOAuthConsentMissingHandleIsNotFoundWithNoRequest`, `TestOAuthConsentSurfaceIsolation` +- `scripts/check-phase8-ui.mjs` -- the read-only UI-contract harness (Task 3) + +## Decisions Made + +See frontmatter `key-decisions`. The two most load-bearing: (1) `MarkIssued`'s signature had to grow beyond 08-02's original codeHash/userID-only shape because PHP's `issueCode` is a single UPDATE touching six columns, not two -- consent could not otherwise persist the user's actual granted-scope subset or the server-resolved collection pin; (2) canonical scope ordering in the consent show/allow response follows `auth.MintableScopes`'s declared order (read, write, ai) per `08-UI-SPEC.md`'s explicit language, not PHP's `array_intersect`, which happens to preserve first-array order and would leak submission-order-dependent behavior into a contract the UI-SPEC documents as fixed. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Missing functionality] Extended `wristband.AuthCodeStore.MarkIssued` and added `ClientStore.MarkConsented`** +- **Found during:** Task 1, while designing `wristband.Server.IssueCode` +- **Issue:** 08-02's `MarkIssued(ctx, id, codeHash, userID)` could not express PHP `OAuthCodeManager::issueCode`'s full write (it also overwrites `scopes`, `collection_ids`, and `expires_at`), and no store method existed to stamp `OAuthClient.consented_at` once (D-08's explicit "consented_at stamping" requirement, and a precondition for DCR's `SweepUnconsented` to correctly skip consented clients) +- **Fix:** Extended `MarkIssued`'s signature to accept `scopes []string, collectionIDs []uint, expiresAt time.Time`; added `MarkConsented(ctx, clientID) error` with an idempotent `WHERE consented_at IS NULL` guard. Updated the GORM adapter (`oauth_store.go`) and the framework's in-memory test double (`registration_test.go`'s `memoryTx`) to match +- **Files modified:** `wristband/stores.go`, `wristband/registration_test.go`, `../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go` +- **Verification:** `wristband/consent_test.go`'s `TestIssueCodeGrantsOnlySubmittedScopesAndReturnsOrderedRedirect`; `classes/auth` package's real-Postgres suite stays green (`go test ./...`, `go test -race ./...`) +- **Committed in:** `a1fa9c6` (summercms.go), `306b06e` (fonoteka.go, compile-forced alongside the RED test commit) + +**2. [Rule 2 - Missing functionality] Added `wristband.Options.CodeTTL`** +- **Found during:** Task 1, implementing `IssueCode`'s fresh expiry +- **Issue:** PHP's `OAuthCodeManager::CODE_TTL_SECONDS` is a distinct constant from `PENDING_TTL_SECONDS`; wristband's `Options` had no field for it, and 08-02's `config.yaml` already declared `code_ttl_seconds` with no Go consumer (flagged as an open seam in 08-02-SUMMARY.md) +- **Fix:** Added `Options.CodeTTL` (600s PHP-parity default) and wired `golem15.fonoteka.oauth.code_ttl_seconds` into it in `plugin.go` +- **Files modified:** `wristband/server.go`, `../fonoteka.go/plugins/golem15/fonoteka/plugin.go` +- **Verification:** `wristband/consent_test.go` exercises the default; no config-key behavior change to any currently-passing test +- **Committed in:** `a1fa9c6` (summercms.go), `a52e8fa` (fonoteka.go) + +**3. [Rule 1 - Bug] Fixed `surf.Where("request_id", ...)` binding to the wrong route** +- **Found during:** Task 2, first GREEN test run (`TestPhase8RedConsentApp` failed with `surf: Where("request_id") is not a path parameter of /_fonoteka/api/v1/oauth/deny`) +- **Issue:** `g.Where()` constrains only the immediately preceding registered route (matching PHP's `->where()`), not every route registered since the last `Where()` call; the three consent routes were registered before a single trailing `Where("request_id", ...)`, so the constraint attached to `/oauth/deny` (the last-added route, which has no `{request_id}` parameter) instead of `/oauth/request/{request_id}` +- **Fix:** Moved `g.Where("request_id", ...)` to sit immediately after `g.Get("/oauth/request/{request_id}", ...)`, before the two POST routes +- **Files modified:** `../fonoteka.go/plugins/golem15/fonoteka/routes.go` +- **Verification:** Full GREEN suite (`go test ./...`, `go test -race ./...`) passes in `fonoteka.go/plugins/golem15/fonoteka` +- **Committed in:** `a52e8fa` + +--- + +**Total deviations:** 3 auto-fixed (2 missing functionality, 1 bug) +**Impact on plan:** No scope change beyond the plan's own stated D-08 behavior (consent's scope/collection persistence and consented_at stamping); the `Where()` bug was self-caught by the plan's own GREEN test suite before commit. + +## Issues Encountered + +None beyond the auto-fixed items above. Full `go vet`/`go test ./...`/`go test -race ./...` are green in `fonoteka.go/plugins/golem15/fonoteka` (and its sibling `classes`, `classes/auth`, `controllers/api`, `middleware`, `models`, `updates` packages) and in `summercms.go`. The root `fonoteka.go`/`parity` module and `plugins/golem15/user` module were also checked (`go vet`/`go test`) and remain green; no new failures were introduced outside this plan's files. + +## User Setup Required + +None -- no external service configuration required. + +## Next Phase Readiness + +- `POST /oauth/mcp/token` is now live end to end through the assembled app; 08-06 (lifecycle and sweeps) can build refresh rotation directly against the same mounted route rather than needing to mount it itself. +- `wristband.Server.IssueCode`/`DenyPending`'s pattern (protocol persists + redirects, app computes policy) is the established seam for any future consent-adjacent operation. +- Connected-apps list/revoke (`GET/DELETE .../oauth/connected-apps`) and `fonoteka-mcp`'s `/api/v1/fonoteka/me` prerequisite remain unbuilt -- neither was in this plan's file list despite the phase directory's "consent-and-connected-apps" name; 08-06/08-08 (per ROADMAP.md) own them. +- `scripts/check-phase8-ui.mjs`'s scenario catalog and guarded-file list are ready for 08-10 to wire into a real Playwright spec via `--final-gate`; no further catalog changes should be needed unless a new UI-SPEC state is added. +- AUTH-05/06/07 remain Pending in REQUIREMENTS.md, continuing 08-01 through 08-04's decision: refresh rotation and connected-apps list/revoke are still outstanding pieces of those requirements. +- No blockers. + +## Self-Check: PASSED + +- FOUND: wristband/consent.go, wristband/consent_test.go, wristband/stores.go, wristband/server.go, wristband/registration_test.go +- FOUND: scripts/check-phase8-ui.mjs +- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go +- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go, oauth_consent_controller_test.go +- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, routes.go, oauth_connect_test.go +- FOUND commits (summercms.go): a1fa9c6, fac9648 +- FOUND commits (fonoteka.go): 306b06e, a52e8fa + +--- +*Phase: 08-oauth2-1-authorization-server* +*Completed: 2026-09-23*