D-03: Configuration defaults pending requests and authorization codes to 600s, access tokens to 3600s, refresh tokens to 30 days, the DCR client cap to 200, and the unconsented-client sweep to 24h; derives issuer from app.url with its trailing slash trimmed, defaults the RFC 8707 resource to https://mcp.plytarium.com/mcp, and builds consent URLs as app.url + /connect?request=<opaque>.
D-07: Public clients and multiple pending authorization requests persist through one transaction-scoped GORM adapter.
D-17: Expiry sweeps delete only expired lifecycle rows and retain unexpired replay evidence.
D-02/D-21: A connector can dynamically register through the assembled JSON-only 64 KiB-bounded route and receive an exact persistent response.
WithinTx callback whose methods all use callback *gorm.DB
WithinTx
Deliver a connector-visible persistent RFC 7591 registration slice, including the schema and transaction semantics it requires.
Purpose: Let a real connector register in Wave 2 while proving nullability, indexes, bounds, constant-time secret handling, locking, cap serialization, and sweep semantics.
Output: Corrected models/migration, wristband DCR, GORM backend, configured raw route, and exact assembled tests.
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
@.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
@.planning/phases/08-oauth2-1-authorization-server/08-01-SUMMARY.md
Task 1: Correct the OAuth lifecycle schema with executable migration evidence
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go, ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go, ../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go, ../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go
.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go
../fonoteka.go/plugins/golem15/fonoteka/updates/11_secrets_slice.go
../fonoteka.go/plugins/golem15/fonoteka/updates/registry.go
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.1.7/create_oauth_tables.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.1.9/add_scope_ceiling_to_oauth_clients.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMigrationTest.php
- Public client secret and pending request id/code hash/user id are pointer-backed and nullable in real Postgres.
- PHP-equivalent named operational indexes exist and safe rollback refuses when null lifecycle data exists.
- `TestPhase8RedOAuthSchema` is the only selected failing test/action during RED.
D-07 and D-18: first add a compiling real-Postgres `TestPhase8RedOAuthSchema`, validate it with `check-phase8-red.sh go`, then change the four model fields to pointers and add a new gormigrate correction rather than editing applied history. Drop four NOT NULL constraints, create the exact named indexes idempotently, and make rollback refuse without coercing/deleting when null lifecycle rows exist. Use the existing migration/Postgres harness and PHP schema/tests as the contract.
(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/updates -run '^TestOAuthSchemaCorrection$' -count=1)
- Before implementation, `scripts/check-phase8-red.sh go PHASE8_RED:persistence-schema git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka/updates TestPhase8RedOAuthSchema -- bash -lc "cd ../fonoteka.go && go test -json ./plugins/golem15/fonoteka/updates -run '^TestPhase8RedOAuthSchema$' -count=1"` accepts only the exact behavior RED.
- After implementation, real-Postgres assertions prove all four nullable columns and every PHP-equivalent named index.
- Down succeeds when safe and refuses with rows unchanged when any required field is null; applied migration history remains byte-unchanged.
The corrected additive schema can represent public clients and every pending/code transition without destructive rollback.
Task 2: Implement bounded DCR and the transaction-scoped persistent backend
wristband/stores.go, wristband/crypto.go, wristband/register.go, wristband/registration_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
../fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/postgres_test.go
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthRegisterController.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/OAuthClient.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthRegisterTest.php
- JSON-only DCR enforces URI/grant/response/auth-method/name rules, 65,536-byte maximum, cap 200, and 24h stale-unconsented sweep.
- Raw client secrets are returned once, SHA-256 hashes alone persist, and verification uses `crypto/subtle.ConstantTimeCompare` over fixed transforms.
- Sweep/cap/create share one transaction; concurrent cap-1 registration yields one success and one native error.
D-01/D-02/D-04/D-05/D-06/D-07/D-17/D-21: add app-agnostic Backend/Tx records, deterministic clock/entropy seams, fixed-transform crypto, a local exact response writer, and the RFC 7591 handler. Apply `http.MaxBytesReader` before decode and return the native `invalid_client_metadata` body for overflow/malformed/non-JSON. Strip control characters, cap names at 120, return raw secrets once, and persist hashes only. Implement the app GORM adapter using only the callback `*gorm.DB`; serialize stale sweep/cap/create in one transaction and expose later row-lock lifecycle methods without importing GORM into wristband. Before implementation, run `scripts/check-phase8-red.sh go PHASE8_RED:registration git.golem15.com/golem15/summercms/wristband TestPhase8RedRegistration -- go test -json ./wristband -run '^TestPhase8RedRegistration$' -count=1` and `scripts/check-phase8-red.sh go PHASE8_RED:registration-store git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka/classes/auth TestPhase8RedRegistrationStore -- bash -lc "cd ../fonoteka.go && go test -json ./plugins/golem15/fonoteka/classes/auth -run '^TestPhase8RedRegistrationStore$' -count=1"`; each invocation must observe its exact sentinel once, the anchored selected test and named package only, and zero unexpected fail actions/package/test events, build/setup/syntax failures, panics, malformed JSON events, or zero-test selection. Then make focused unit/Postgres tests green.
go test ./wristband -run '^Test(Register|Registration)' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth -run '^TestOAuth(RegistrationStore|RegistrationCap)$' -count=1)
- RED uses `go test -json` with exact package/test/sentinel for `TestPhase8RedRegistration` and `TestPhase8RedRegistrationStore`; no unrelated failure can satisfy either invocation.
- Exact tests cover public/confidential responses, wrong content type, malformed/oversized 65,537-byte input, five-URI/count/length bounds, unsupported grant/response/auth method, control-character name cleaning, and no newline/envelope.
- A synchronized real-Postgres cap-1 test yields exactly one created row; stale unconsented rows are swept while consented/fresh rows remain, and all mutations use the callback transaction.
- Persisted/logged/output audits find no raw client secret; fixed-transform comparison contains `crypto/subtle.ConstantTimeCompare`.
Wristband and Postgres provide exact bounded, concurrency-safe, secret-safe registration behavior.
Task 3: Configure and mount persistent DCR on the assembled raw surface
../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
../fonoteka.go/plugins/golem15/fonoteka/routes.go
../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml
../fonoteka.go/config/app.yaml
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/config/fonoteka.php
- Assembled POST `/oauth/mcp/register` persists public/confidential clients and returns exact PHP bytes/headers.
- Only register carries `throttle:fonoteka-oauth-register`; metadata remains raw with no middleware.
- Config defaults are pending/code 600s, access 3600s, refresh 30 days, DCR cap 200, stale age 24h, resource URL, and register max 65,536.
D-03: add `plugins.golem15.fonoteka.oauth.*` defaults and construct the store-backed server in Plugin.Boot while preserving 08-01 metadata. Read the existing `../fonoteka.go/config/app.yaml` only as the `app.url` source; do not modify it. D-09: mount register in the raw group with only its named throttle. D-10/D-12: register no oauth guard and add no rich Bearer/resource-server surface. Add an assembled real-Postgres `TestPhase8RedRegistrationApp`, then before implementation run `scripts/check-phase8-red.sh go PHASE8_RED:registration-app git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka TestPhase8RedRegistrationApp -- bash -lc "cd ../fonoteka.go && go test -json ./plugins/golem15/fonoteka -run '^TestPhase8RedRegistrationApp$' -count=1"`; it must observe the exact sentinel once, the anchored selected test and named package only, and zero unexpected fail actions/package/test events, build/setup/syntax failures, panics, malformed JSON events, or zero-test selection. Then assert exact bytes/headers, durable reload, middleware isolation, config values, and unchanged metadata.
(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestOAuth(RegisterAssembled|MetadataAssembled|RawRegistrationSurface)$' -count=1)
- RED command names exact app package, `TestPhase8RedRegistrationApp`, and `PHASE8_RED:registration-app` under `go test -json`; compile/setup/no-test or another failing test is rejected.
- A public and confidential registration each return status 201 and exact fields/order/headers; reload through a fresh transaction finds hash-only rows with null/non-null secret hash as appropriate.
- Oversized and wrong-content-type requests retain endpoint-native errors through the assembled router; register has only its named limiter and metadata remains byte-identical to 08-01.
An unchanged connector can dynamically register against the assembled app and its client persists correctly in Postgres.
<threat_model>
Trust Boundaries
Boundary
Description
wristband records → GORM
App-agnostic state crosses into persistent rows and locks.
STRIDE Threat Register
Threat ID
Category
Component
Disposition
Mitigation Plan
T-08-DCR-FLOOD
Denial of Service
client store
mitigate
Transactionally serialized cap/sweep/create with contention test.
T-08-CODE-REPLAY
Spoofing
auth-code store
mitigate
App-tier row-lock methods and single transaction handle.
T-08-REFRESH-REPLAY
Spoofing/Elevation
refresh store
mitigate
Preserve replay evidence until expiry and expose locked traversal.
T-08-SC
Tampering
dependencies
mitigate
Existing GORM/Postgres only; no install.
</threat_model>
- Focused migration/store/DCR tests pass; no task command runs full repositories, race, parity, UI, or real MCP.
- `rg -n 'clause.Locking' ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go` finds app-tier locks only.
<success_criteria>
Schema and store can represent every client/pending/code/refresh lifecycle state.
DCR cap and single-use operations have real-Postgres concurrency evidence.
</success_criteria>
Create `.planning/phases/08-oauth2-1-authorization-server/08-02-SUMMARY.md` when done.