diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index e2e0c5b..4347547 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -20,7 +20,7 @@ Decimal phases appear between their surrounding integers in numeric order.
- [x] **Phase 5: Data layer full fidelity** - All 25 models and their squashed migrations with fillable/hidden/cast/soft-delete discipline (completed 2026-09-18)
- [x] **Phase 6: HTTP routing, auth groups and rate limiting** - Three auth groups, named rate buckets, OAuth-safe middleware structure (completed 2026-09-21)
- [x] **Phase 7: User plugin and authentication** - Registration, login, JWT, organizations, personal tokens, must-change-password (completed 2026-09-22)
-- [ ] **Phase 8: OAuth2.1 authorization server** - zitadel/oidc server for fonoteka-mcp and the ChatGPT connector
+- [ ] **Phase 8: OAuth2.1 authorization server** - direct standard-library `wristband` server for fonoteka-mcp and the ChatGPT connector
- [ ] **Phase 9: Backend admin authentication and schema pipeline** - Admin roles, fields.yaml/columns.yaml, relation manager
- [ ] **Phase 10: Admin Vue SPA** - Login, navigation, lists, forms and relation manager for five controllers
- [ ] **Phase 11: Jobs, realtime and search infrastructure** - River, Centrifugo and Typesense sync brought up before the API phases that need them
@@ -318,26 +318,26 @@ Plans:
3. The token endpoint returns exactly `WWW-Authenticate: Basic realm="OAuth"` on `invalid_client`, the backend personal-token 401 remains unchanged with no added challenge, and fonoteka-mcp's own rich Bearer challenge plus protected-resource metadata are verified through its actual discovery flow, not just a Go unit test.
4. Connected apps can be listed and revoked; `OAuthClient`/`OAuthAuthCode`/`OAuthRefreshToken` models persist correctly; fonoteka-mcp completes its install and auth flow unchanged; client-secret comparison uses `crypto/subtle.ConstantTimeCompare`.
-**Plans**: 6 plans
+**Plans**: 10 plans
**Research flag:** yes
Plans:
**Wave 1**
-- [ ] 08-01-PLAN.md — Define and prove the app-agnostic metadata and DCR engine
+- [ ] 08-01-PLAN.md — Mount exact connector-visible metadata and establish fail-closed RED verification
**Wave 2** *(blocked on 08-01)*
-- [ ] 08-02-PLAN.md — Correct OAuth schema and implement transaction-scoped Postgres stores
+- [ ] 08-02-PLAN.md — Deliver persistent connector-visible DCR with corrected schema and transaction-scoped stores
**Wave 3** *(blocked on 08-02)*
-- [ ] 08-03-PLAN.md — Mount persistent metadata and DCR on the assembled raw route surface
+- [ ] 08-03-PLAN.md — Create durable PKCE-bound authorize requests on the assembled raw route surface
**Wave 4** *(blocked on 08-03)*
-- [ ] 08-04-PLAN.md — Implement ordered authorize validation and atomic PKCE code exchange
+- [ ] 08-04-PLAN.md — Implement atomic PKCE-bound authorization-code exchange
**Wave 5** *(blocked on 08-04)*
@@ -354,7 +354,7 @@ Plans:
**Wave 8** *(blocked on 08-07 and 08-08)*
-- [ ] 08-09-PLAN.md — Replay PHP OAuth flows and run the unchanged real MCP lifecycle through the pre-security gate
+- [ ] 08-09-PLAN.md — Replay PHP OAuth flows and assemble the self-validating final unchanged-MCP gate
**Wave 9** *(blocked on 08-09; blocking security checkpoint)*
diff --git a/.planning/phases/08-oauth2-1-authorization-server/08-01-PLAN.md b/.planning/phases/08-oauth2-1-authorization-server/08-01-PLAN.md
index 6132a75..34cfe40 100644
--- a/.planning/phases/08-oauth2-1-authorization-server/08-01-PLAN.md
+++ b/.planning/phases/08-oauth2-1-authorization-server/08-01-PLAN.md
@@ -6,43 +6,42 @@ wave: 1
depends_on: []
files_modified:
- wristband/server.go
- - wristband/stores.go
- - wristband/crypto.go
- - wristband/register.go
- - wristband/registration_test.go
+ - wristband/server_test.go
- scripts/check-phase8-red.sh
+ - ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
+ - ../fonoteka.go/plugins/golem15/fonoteka/routes.go
+ - ../fonoteka.go/plugins/golem15/fonoteka/oauth_metadata_test.go
autonomous: true
requirements: [AUTH-05, AUTH-06]
must_haves:
truths:
- - "D-01: The app-agnostic standard-library wristband package serves exact RFC 8414 metadata and validates RFC 7591 registration without zitadel/oidc."
+ - "D-01: A connector can fetch exact RFC 8414 metadata from the assembled Go app without zitadel/oidc."
- "D-06: PHP-minimal response shapes and configurable metadata fields are wristband defaults with no response hooks."
- - "D-02: Registration is JSON-only, and D-21: its body is bounded at 64 KiB before decoding with endpoint-native errors."
- - "D-04: Client-secret checks use constant-time fixed transforms and DCR cap/sweep behavior is deterministic under concurrency."
+ - "D-09: Metadata is mounted on the raw route surface without house, JWT, personal-token, or oauth-guard middleware."
artifacts:
- path: "wristband/server.go"
provides: "Options, exact metadata writer, and app-agnostic server contract"
- - path: "wristband/register.go"
- provides: "RFC 7591 validation, issuance, cap, sweep, and bounded handler"
+ - path: "../fonoteka.go/plugins/golem15/fonoteka/routes.go"
+ provides: "Connector-visible raw metadata route"
- path: "scripts/check-phase8-red.sh"
provides: "Fail-closed RED verifier rejecting syntax/setup/missing-test failures"
key_links:
- - from: "wristband/register.go"
- to: "wristband.Backend.WithinTx"
- via: "serialized sweep, cap check, and create"
- pattern: "WithinTx"
+ - from: "../fonoteka.go/plugins/golem15/fonoteka/routes.go"
+ to: "wristband.Server.Metadata"
+ via: "assembled raw GET route"
+ pattern: "oauth-authorization-server"
---
-Define and implement the framework-only discovery and dynamic-registration contract before app persistence or routing.
+Deliver an assembled connector-visible RFC 8414 metadata endpoint and the fail-closed RED infrastructure used by every later slice.
-Purpose: Keep D-01's protocol engine small and app-agnostic while making the RED phase executable and diagnostic.
-Output: `wristband` metadata/DCR contracts, deterministic tests, crypto helpers, and the shared RED verifier.
+Purpose: Obtain end-to-end feedback in the first wave while keeping the protocol writer app-agnostic and making every later RED phase diagnostic.
+Output: `wristband` metadata contract, mounted raw route, assembled exact-byte tests, and the shared RED verifier.
## Phase Goal
-**As a** connector implementer, **I want to** exercise discovery and registration against a deterministic OAuth engine, **so that** the app adapter can persist and mount an already proven wire contract.
+**As a** connector implementer, **I want to** discover the assembled Go authorization server, **so that** I can obtain its exact OAuth endpoints and capabilities before registering.
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
@@ -62,33 +61,60 @@ Output: `wristband` metadata/DCR contracts, deterministic tests, crypto helpers,
- Task 1: Create compiling RED discovery and registration contracts
- wristband/server.go, wristband/stores.go, wristband/registration_test.go, scripts/check-phase8-red.sh
+ Task 1: Create fail-closed RED verification and metadata contracts
+ wristband/server.go, wristband/server_test.go, scripts/check-phase8-red.sh
+
+ .planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
+ .planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
+ /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthMetadataController.php
+ /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMetadataTest.php
+ wire/response.go
+ scripts/check-phase3.sh
+
- - Metadata is the exact unwrapped 11-field document with recorded order, content type, and cache header.
- - Public and confidential DCR validate PHP-compatible URI, grant, response, auth-method, cap, sweep, and 65,536-byte rules.
- - Test failures use `PHASE8_RED:registration` only for missing behavior; syntax, build, setup, missing-test, panic, and unrelated failures are rejected.
+ - Metadata is the exact unwrapped 11-field document with recorded order, `application/json`, no trailing newline, and the PHP cache header.
+ - The selected RED test fails with `PHASE8_RED:metadata` only because metadata behavior is absent.
+ - Any other failed test/package/action, compilation/setup failure, panic, malformed JSON event stream, or zero selected tests makes the verifier fail.
- D-06: define the exported options, typed records, transaction-scoped Backend/Tx contracts, handler signatures, and deterministic clock/random seams with compiling stubs. D-18: add behavior tests that compile and intentionally fail through `PHASE8_RED:registration` assertions. Create `scripts/check-phase8-red.sh` to run the supplied command, require a nonzero result and the requested marker, and fail if output contains `build failed`, `setup failed`, `syntax error`, `no tests to run`, `no test files`, or a panic. Include named T-08-DCR-FLOOD, T-08-SECRET-TIMING, and T-08-REQUEST-LEAK cases. Commit RED separately.
+ D-06 and D-18: define only the exported metadata options/server handler needed by this slice, with compiling stubs and an exact `TestPhase8RedMetadata` assertion. Implement `scripts/check-phase8-red.sh` with two explicit modes. In `go` mode accept `sentinel`, exact package import path, exact test name, `--`, and a required `go test -json` command; parse every JSON event, require the selected test to emit the exact sentinel and fail, require its package to fail, require at least one selected test run, and reject every other `Action:"fail"` test/package, non-JSON output, compile/build/setup/syntax failure, panic, timeout, and no-test/zero-selection result. Package-level fail is allowed only for the named package after the named test failure. In `shell` mode accept `sentinel`, exact stage, and a command; require exit 86 and exactly one line `PHASE8_STAGE:<stage>:FAIL:<sentinel>`, reject every other FAIL/ERROR/PANIC stage and missing/extra sentinel. D-01: use only the standard library. Commit RED separately.
- scripts/check-phase8-red.sh registration go test ./wristband -run 'Test(Metadata|Register|Registration)' -count=1
+ scripts/check-phase8-red.sh go PHASE8_RED:metadata git.golem15.com/golem15/summercms/wristband TestPhase8RedMetadata -- go test -json ./wristband -run '^TestPhase8RedMetadata$' -count=1
- The tests compile, the named tests execute, and the verifier accepts only the expected missing-behavior RED marker.
+
+ - The verifier accepts one JSON stream containing only `TestPhase8RedMetadata` plus its package failure and exact sentinel.
+ - Fixture self-tests reject an unrelated failing test, another failing package/action, compile/setup failure, panic, malformed/non-JSON output, missing sentinel, duplicate sentinel, and zero selected tests.
+ - Metadata RED asserts the exact 11-field byte order, `Content-Type: application/json`, cache header, status 200, and no trailing newline.
+
+ The metadata RED test compiles and the shared verifier is fail-closed against every unrelated or non-behavior failure class.
- Task 2: Implement exact metadata, DCR, bounds, and cryptography
- wristband/server.go, wristband/stores.go, wristband/crypto.go, wristband/register.go, wristband/registration_test.go
+ Task 2: Implement and mount exact metadata on the assembled app
+ wristband/server.go, wristband/server_test.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_metadata_test.go
+
+ wristband/server_test.go
+ surf/router.go
+ ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
+ ../fonoteka.go/plugins/golem15/fonoteka/routes.go
+ /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php
+ /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthMetadataController.php
+ ../fonoteka.go/parity/fixtures/mcp/mcp-oauth.yaml
+
- - Fixed SHA-256 transforms use `crypto/subtle.ConstantTimeCompare`; raw client secrets are returned once and never persisted/logged.
- - Sweep, cap check, and create occur within one backend transaction; concurrent registrations cannot cross the cap.
- - Oversized and malformed registration input returns exact `invalid_client_metadata` bytes without a house envelope.
+ - Direct handler and assembled route return the same exact metadata bytes and headers.
+ - The route has no house/JWT/personal-token middleware and no `oauth` guard registration.
+ - Issuer is `app.url` with one trailing slash trim; endpoint/resource/service-documentation/scope/auth-method values match PHP defaults.
- D-01: use only `crypto/rand`, `crypto/sha256`, `crypto/subtle`, `encoding/base64`, `encoding/json`, `net/http`, and `net/url`; add no dependency. D-03: model configurable TTLs, cap 200, stale age 24h, issuer/resource/endpoints, and `RegisterMaxBytes: 65536`. D-05: keep all protocol rules in wristband and import no fonoteka/GORM code. D-06: use a local no-newline exact JSON writer with no response hooks. D-07: use only the transaction-scoped interfaces. D-17: expose expired-row and unconsented-client sweep operations without timers/goroutines. D-21: apply `http.MaxBytesReader` before JSON decode. Strip control characters and cap names at 120 characters.
+ D-01, D-03, D-05, and D-06: implement the local no-newline exact JSON metadata writer and configurable values without response hooks or app imports. Construct the metadata-capable server during app boot from `app.url` and locked PHP defaults, retain it on Plugin, and mount only `GET /.well-known/oauth-authorization-server` in the existing raw group. D-09/D-10/D-12: attach no middleware, register no oauth guard, and do not add protected-resource metadata or Bearer challenges. Add an assembled test that boots the real plugin/router and compares status, headers, and exact bytes to the PHP contract.
- go test ./wristband -run 'Test(Metadata|Register|Registration)' -count=1
+ go test ./wristband -run '^TestMetadata' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestOAuthMetadataAssembled$' -count=1)
- Framework discovery and DCR tests pass with exact bytes, atomic cap behavior, bounded decoding, hash-only persistence, and no app-tier imports.
+
+ - Direct and assembled GET return status 200, exact PHP metadata bytes in field order, `Content-Type: application/json`, the expected cache header, and no envelope/newline.
+ - Route inspection shows only the raw GET path and rejects `jwt.auth`, `inv_token`, `inv.scope`, body-limit, house tags, and an `oauth` guard.
+ - Changing `app.url` changes issuer/endpoints after exactly one trailing-slash trim; framework import checks find no fonoteka or GORM reference.
+
+ A connector can fetch the exact metadata contract from the assembled production router in Wave 1.
@@ -98,28 +124,26 @@ Output: `wristband` metadata/DCR contracts, deterministic tests, crypto helpers,
| Boundary | Description |
|----------|-------------|
-| Connector → wristband | Untrusted metadata/DCR requests cross into protocol parsing. |
-| wristband → Backend | Protocol state crosses into an app-provided transaction. |
+| Connector → assembled raw route | Untrusted discovery traffic reaches the exact wristband writer. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
-| T-08-DCR-FLOOD | Denial of Service | register handler/store | mitigate | 64 KiB cap, serialized client cap, stale sweep, concurrency tests. |
-| T-08-SECRET-TIMING | Information Disclosure | crypto/client secret | mitigate | Fixed SHA-256 transforms and `subtle.ConstantTimeCompare`. |
-| T-08-REQUEST-LEAK | Information Disclosure | handler/tests | mitigate | Hash-only records and no sensitive-value logging. |
+| T-08-SURFACE | Elevation | raw metadata route | mitigate | Assembled route inspection proves no guard/house middleware. |
+| T-08-REQUEST-LEAK | Information Disclosure | metadata/config | mitigate | Only public configured metadata fields are serialized. |
| T-08-SC | Tampering | dependencies | mitigate | No package install; stdlib-only import audit. |
-- `go test ./wristband -count=1`
+- `go test ./wristband -run '^TestMetadata' -count=1`
+- `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestOAuthMetadataAssembled$' -count=1`
- `go list -deps ./wristband | rg 'fonoteka|gorm.io'` returns no matches.
-- Exact metadata and DCR behavior is green in a self-contained framework package.
-- RED verification cannot pass on mere file presence, compile errors, missing tests, or unrelated failures.
-- DCR is bounded, concurrency-safe, and secret-safe before app integration.
+- Exact metadata is connector-visible through the assembled app.
+- RED verification cannot pass on compile/setup/panic/no-test errors or any unrelated test/package/stage failure.