19 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-oauth2-1-authorization-server | 05 | auth |
|
|
|
|
|
|
|
|
~55min | 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/DenyPendingport PHPOAuthConsentController::pendingFor/OAuthCodeManager::issueCodeas app-agnostic protocol operations: every missing, foreign-owner, used, expired, or already-issued pending row collapses to the identicalErrPendingNotFound(T-08-CROSS-USER/T-08-REQUEST-LEAK);IssueCodetrusts the caller's already-computed granted scopes/collection ids and returns the orderedredirect_toURL through the existing RFC 3986 encoderAuthCodeStore.MarkIssuedwas extended (scopes/collectionIDs/expiresAt) andClientStore.MarkConsentedwas added because the narrower 08-02 store interface could not express PHP's full single-UPDATEissueCodesemantics; both changes are covered by real-Postgres assertions inoauth_store.go's existing test packagefonoteka's JWT-groupConsentShow/ConsentStore/ConsentDenygrant exactlysubmitted ∩ pending-request ∩ MINTABLE_SCOPES, pin collection ids exclusively throughResolveActiveCollection, and never accept a client-supplied collection id; an empty granted intersection is an exact 422{"error":"No grantable scopes"}POST /oauth/mcp/tokenis now mounted on the raw group withthrottle:fonoteka-oauth-token, closing the gap 08-04 deliberately left open -- a fullauthorize -> consent(JWT) -> tokenPKCE round trip now produces a realinv_-prefixed access token through the assembled app and real Postgresscripts/check-phase8-ui.mjsencodes the complete08-UI-SPEC.mdconsent/connected-app state/accessibility/responsive/i18n matrix as a 32-scenario, 7-category catalog;--contract-self-testvalidates catalog completeness, guarded-Nuxt-file hash stability, and@playwright/testresolvability in ~50ms without booting anything;--final-gateis scaffolded but refuses to run without an explicit opt-in env var, reserved for 08-10- Both
scripts/check-phase8-red.shsentinel 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 bothfonoteka.go/plugins/golem15/fonotekaandsummercms.go
Task Commits
Each task was committed atomically (TDD RED then GREEN where applicable, split per repo):
- Task 1: consent RED anchor + wristband prerequisite --
a1fa9c6(feat, summercms.go):wristband.Server.PendingRequest/IssueCode/DenyPending, theMarkIssued/MarkConsentedinterface extensions,Options.CodeTTL;306b06e(test, fonoteka.go):TestPhase8RedConsentController/TestPhase8RedConsentAppfail against 501 stub handlers and unmounted routes (PHASE8_RED:consent-controller/PHASE8_RED:consent-app), plus the compile-forcedoauth_store.goadapter update. Both sentinels verified fail-closed viascripts/check-phase8-red.sh. - Task 2: implement owner-bound JWT consent and mount raw token route --
a52e8fa(feat, fonoteka.go): the realConsentShow/ConsentStore/ConsentDenyhandlers,routes.go's consent-route and/oauth/mcp/tokenmounts,plugin.go's*wristband.Serverpublish andcode_ttl_secondswiring, 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). - 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,ErrNoGrantableScopeswristband/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-usewristband/stores.go--AuthCodeStore.MarkIssuedsignature extension,ClientStore.MarkConsentedwristband/server.go--Options.CodeTTL(600s default)wristband/registration_test.go--memoryTx.MarkIssued/MarkConsentedupdated to satisfy the extended interface../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go--oauthTx.MarkIssued/MarkConsentedGORM 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) plusTestPhase8RedConsentController../fonoteka.go/plugins/golem15/fonoteka/plugin.go-- publishes*wristband.Server; wirescode_ttl_seconds../fonoteka.go/plugins/golem15/fonoteka/routes.go-- mounts the three JWT-group consent routes andPOST /oauth/mcp/token../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go--TestPhase8RedConsentAppplusTestOAuthConsentCrossUserIsNotFound,TestOAuthConsentScopeCeilingViaShow,TestOAuthConsentEmptySubmittedScopeIntersectionIs422,TestOAuthDenyRedirectsAccessDeniedAndConsumesRequest,TestOAuthConsentMissingHandleIsNotFoundWithNoRequest,TestOAuthConsentSurfaceIsolationscripts/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 PHPOAuthCodeManager::issueCode's full write (it also overwritesscopes,collection_ids, andexpires_at), and no store method existed to stampOAuthClient.consented_atonce (D-08's explicit "consented_at stamping" requirement, and a precondition for DCR'sSweepUnconsentedto correctly skip consented clients) - Fix: Extended
MarkIssued's signature to acceptscopes []string, collectionIDs []uint, expiresAt time.Time; addedMarkConsented(ctx, clientID) errorwith an idempotentWHERE consented_at IS NULLguard. Updated the GORM adapter (oauth_store.go) and the framework's in-memory test double (registration_test.go'smemoryTx) 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'sTestIssueCodeGrantsOnlySubmittedScopesAndReturnsOrderedRedirect;classes/authpackage'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_SECONDSis a distinct constant fromPENDING_TTL_SECONDS; wristband'sOptionshad no field for it, and 08-02'sconfig.yamlalready declaredcode_ttl_secondswith no Go consumer (flagged as an open seam in 08-02-SUMMARY.md) - Fix: Added
Options.CodeTTL(600s PHP-parity default) and wiredgolem15.fonoteka.oauth.code_ttl_secondsinto it inplugin.go - Files modified:
wristband/server.go,../fonoteka.go/plugins/golem15/fonoteka/plugin.go - Verification:
wristband/consent_test.goexercises 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 (
TestPhase8RedConsentAppfailed withsurf: 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 lastWhere()call; the three consent routes were registered before a single trailingWhere("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 afterg.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 infonoteka.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/tokenis 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) andfonoteka-mcp's/api/v1/fonoteka/meprerequisite 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