15 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 | 02 | auth |
|
|
|
|
|
|
|
|
~30min | 2026-09-23 |
Phase 08 Plan 02: Persistence and Registration Summary
Corrected OAuth lifecycle schema plus a transaction-scoped, advisory-lock-serialized RFC 7591 Dynamic Client Registration slice, live end to end from the assembled Go app through real Postgres.
Performance
- Duration: ~30 min
- Started: 2026-09-23T17:18:26Z (approx., continuing from 08-01)
- Completed: 2026-09-23T17:48:00Z (approx.)
- Tasks: 3 completed (6 commits: RED/GREEN pairs across both repos)
- Files modified: 14 (5 created + 2 modified in summercms.go's wristband package; 5 created + 4 modified in fonoteka.go)
Accomplishments
- The shipped OAuth schema (
202609180010_create_oauth_tables) could not represent a pre-consent pending authorization row or a public DCR client; a new additive migration relaxes the four wrongly-NOT NULLcolumns and adds all nine PHP-equivalent named indexes, with a rollback that refuses rather than destroys data when any row still depends on nullability wristbandgained its transaction-scopedBackend/Txstore bundle and an in-memory implementation for framework-only tests, plus fixed-transform crypto helpers (crypto/randbase64url, sha256 hex,crypto/subtle.ConstantTimeCompare, S256)wristband.Registeris an exact byte-for-byte port ofOAuthRegisterController::register's validation order, redirect-URI/grant/response-type/auth-method rules, 64 KiB body bound, and one-time-secret/hash-only persistence contractfonoteka'sOAuthStoreGORM adapter satisfieswristband.Backend:CreateWithCapserializes sweep+cap-check+create viapg_advisory_xact_lock, proven by a synchronized real-Postgres cap-1 test (TestOAuthRegistrationCap) that yields exactly one created row and oneErrClientCapReachedunder concurrencyPOST /oauth/mcp/registeris live on the assembled app's raw route group with only its named throttle; a public and a confidential client each register successfully against real Postgres and reload through a fresh transaction with hash-only rows
Task Commits
Each task's RED test was committed and verified fail-closed via scripts/check-phase8-red.sh before its GREEN implementation:
- Task 1: OAuth schema correction
0b18d27(test, fonoteka.go):TestPhase8RedOAuthSchemafails against the still-NOT NULLschema (PHASE8_RED:persistence-schema)4536b3e(feat, fonoteka.go): the additive correction migration (21_oauth_schema_correction.go) and pointer-ified model fields
- Task 2a: wristband registration (RED) —
c026b83(test, summercms.go):TestPhase8RedRegistrationfails against a 501 stub (PHASE8_RED:registration); addsstores.go/crypto.goand the Options/Server seams Task 2b: wristband registration (GREEN) —c0b1e3c(feat, summercms.go): the exact RFC 7591Registerhandler Task 2c: OAuth store (RED) —fec99f0(test, fonoteka.go):TestPhase8RedRegistrationStorefails against a stubbedCreateWithCap(PHASE8_RED:registration-store) Task 2d: OAuth store (GREEN) —381de57(feat, fonoteka.go): the transaction-scoped GORM adapter with the advisory lock and row-lock reads - Task 3a: assembled mount (RED) —
5891c7c(test, fonoteka.go):TestPhase8RedRegistrationAppfails 404 against the unmounted route (PHASE8_RED:registration-app); addsgolem15.fonoteka.oauth.*config and wires the store-backed server intoPlugin.BootTask 3b: assembled mount (GREEN) —ebdb249(feat, fonoteka.go): mountsPOST /oauth/mcp/registeron the raw group with only its named throttle
Plan metadata: committed as part of this summary/state-update commit.
Note: all three tasks carry tdd="true"; RED/GREEN pairs land as separate commits, split per repo where both repos changed (Tasks 2 and 3).
Files Created/Modified
wristband/stores.go—ClientRecord/AuthCodeRecord/RefreshTokenRecord/IssuedToken,ClientStore/AuthCodeStore/RefreshTokenStore/AccessTokenIssuer,Tx,Backend,ErrClientCapReachedwristband/crypto.go—randomBase64URL,sha256Hex,constantEqual,s256Challengewristband/register.go— the RFC 7591Server.Registerhandler and its PHP-ported validation helperswristband/registration_test.go—TestPhase8RedRegistrationplus the in-memorymemoryBackend/memoryTxand the full public/confidential/bounds/sweep/cap test matrixwristband/server.go—OptionsgainsDCRClientCap/DCRUnconsentedSweepAge/RegisterMaxBodyBytes;Servergainsbackend/now/randomBytesandSetBackend../fonoteka.go/plugins/golem15/fonoteka/updates/21_oauth_schema_correction.go— the additive, refusing-rollback correction migration../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go— the package's own real-Postgres harness plus the RED/GREEN schema tests../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go,oauth_auth_code.go— pointer-ified nullable fields../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go—OAuthStore/oauthTx(thewristband.Backend/wristband.TxGORM adapter) and record↔model conversions../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go—TestPhase8RedRegistrationStore,TestOAuthRegistrationStore,TestOAuthRegistrationCap../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml—golem15.fonoteka.oauth.*defaults (new file; the plugin had no config before this plan)../fonoteka.go/plugins/golem15/fonoteka/plugin.go—pact.HasConfig, config-drivenwristband.Options,SetBackend(auth.NewOAuthStore(gdb))../fonoteka.go/plugins/golem15/fonoteka/routes.go— mountsPOST /oauth/mcp/registerwiththrottle:fonoteka-oauth-register../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go—TestPhase8RedRegistrationApp,TestOAuthRegisterAssembled,TestOAuthRegisterOversizedAndWrongContentType,TestOAuthRawRegistrationSurface
Decisions Made
See frontmatter key-decisions. The most load-bearing one: the corrective migration file had to be renamed from the plan's suggested 12_ prefix to 21_ because Go's init() file-order registration — not the migration's own ID — determines gormigrate slice position, and RollbackLast() rolls back the slice's last element. 12_ before 20_remaining.go meant RollbackLast() targeted the wrong migration; this was caught by TestOAuthSchemaCorrection's rollback-refusal assertion failing with "rollback succeeded while null-dependent rows exist" during GREEN verification, fixed, and re-verified (Rule 1 — bug, self-caught before commit, not a deviation requiring separate documentation since it's the initial implementation, not a later fix).
Deviations from Plan
1. [Rule 1 - Bug] Migration filename changed from 12_ to 21_ for correct init-order/RollbackLast semantics
- Found during: Task 1, before the GREEN commit (caught by
TestOAuthSchemaCorrection's own rollback assertion) - Issue: Naming the file
12_oauth_schema_correction.goplaced itsinit()-timeRegister()call before20_remaining.go's, making it not the last element ofupdates.All()despite having the numerically latest migration ID;gormigrate.RollbackLast()rolled back20_remaining.go's last migration instead - Fix: Renamed the file to
21_oauth_schema_correction.go; 08-PATTERNS.md explicitly marks the filename as discretionary - Files modified:
../fonoteka.go/plugins/golem15/fonoteka/updates/21_oauth_schema_correction.go(created directly under this name; never committed as12_) - Verification:
TestOAuthSchemaCorrection's rollback-refusal and rollback-success assertions both pass - Committed in:
4536b3e(Task 1 GREEN commit)
Total deviations: 1 auto-fixed (1 bug) Impact on plan: No scope change; purely a file-naming/ordering correction caught by the plan's own test before commit.
Issues Encountered
None beyond the migration-ordering issue documented above.
User Setup Required
None — no external service configuration required.
Next Phase Readiness
wristband.Tx's full store bundle (including row-lock reads andRevokeLineage) is ready for 08-03 (authorize) and 08-04 (token exchange/refresh rotation) to consume without another interface change.golem15.fonoteka.oauth.*config already declarespending_request_ttl_seconds/code_ttl_seconds/access_token_ttl_seconds/refresh_token_ttl_seconds/resource; 08-03/08-04 need only read them, not add new keys.- The
oauthClientRegistrationLockKeyadvisory-lock pattern is available for any future atomic count-then-mutate need (e.g. a future per-user rate concern). - D-17's expiry sweep (deleting expired pending/code/refresh rows) is explicitly deferred to 08-06 —
AuthCodeStore/RefreshTokenStoredo not yet have aDeleteExpired-shaped method; 08-06 should add it rather than assume it exists. - No blockers.
Self-Check: PASSED
- FOUND: wristband/stores.go, wristband/crypto.go, wristband/register.go, wristband/registration_test.go
- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/updates/21_oauth_schema_correction.go, oauth_schema_correction_test.go
- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, oauth_store_test.go
- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml, oauth_registration_test.go
- FOUND commits (fonoteka.go): 0b18d27, 4536b3e, fec99f0, 381de57, 5891c7c, ebdb249
- FOUND commits (summercms.go):
c026b83,c0b1e3c