docs(08-01): complete metadata RED infrastructure plan

This commit is contained in:
Jakub Zych
2026-09-23 19:20:35 +02:00
parent c578bb58d7
commit deee2cc8d7
3 changed files with 141 additions and 14 deletions

View File

@@ -325,7 +325,7 @@ Plans:
**Wave 1**
- [ ] 08-01-PLAN.md — Mount exact connector-visible metadata and establish fail-closed RED verification
- [x] 08-01-PLAN.md — Mount exact connector-visible metadata and establish fail-closed RED verification
**Wave 2** *(blocked on 08-01)*
@@ -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 | 0/10 | Not started | - |
| 8. OAuth2.1 authorization server | 1/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 | - |

View File

@@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: executing
stopped_at: Phase 8 UI-SPEC approved
last_updated: "2026-09-23T16:42:33.535Z"
last_activity: 2026-09-23 -- Phase 08 planning complete
stopped_at: Completed 08-01-PLAN.md
last_updated: "2026-09-23T17:18:26.201Z"
last_activity: 2026-09-23
progress:
total_phases: 15
completed_phases: 7
total_plans: 55
completed_plans: 45
completed_plans: 46
percent: 47
---
@@ -21,16 +21,16 @@ progress:
See: .planning/PROJECT.md (updated 2026-09-16)
**Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test.
**Current focus:** Phase 08 — oauth2 1 authorization server
**Current focus:** Phase 08 — oauth2-1-authorization-server
## Current Position
Phase: 08
Plan: Not started
Phase: 08 (oauth2-1-authorization-server) — EXECUTING
Plan: 2 of 10
Status: Ready to execute
Last activity: 2026-09-23 -- Phase 08 planning complete
Last activity: 2026-09-23
Progress: [██████████] 100%
Progress: [████████░░] 84%
## Performance Metrics
@@ -91,6 +91,7 @@ Progress: [██████████] 100%
| Phase 07 P07 | 3h 15m | 3 tasks | 28 files |
| Phase 07-user-plugin-and-authentication P07 | 3h 15m | 3 tasks | 28 files |
| Phase 07-user-plugin-and-authentication P08 | 25min | 3 tasks | 6 files |
| Phase 08 P01 | 25min | 2 tasks | 6 files |
## Accumulated Context
@@ -207,6 +208,9 @@ Recent decisions affecting current work:
- [Phase 07]: Already-activated activate/activate-by-code is Winter 500 HTML — User::attemptActivation throws when the user is already active; Go keys that path on IsAlreadyActivated, not wrong-code.
- [Phase 07]: Both CLI serve and in-process Handler must publish *blob.Bucket; unit tests that call attach.Publish themselves cannot stand in for boot — Phase 5 shipped OpenBucket/Publish but never wired them. Phase 7 added HTTP avatar. UAT and corpus replay boot via Handler, so serve-only publish would leave the same 500.
- [Phase 07]: Assembled Handler avatar coverage lives in parity/ next to newConfiguredTarget, not in the user plugin package — The user plugin cannot import app.Handler without a reverse import. newConfiguredTarget is the same boot as replay.
- [Phase 08]: wristband.Options exposes only the four PHP-configurable metadata fields (service_documentation path, scopes_supported, token_endpoint_auth_methods_supported, authorization_response_iss_parameter_supported); response_types/grant_types/code_challenge_methods stay fixed protocol constants per D-06
- [Phase 08]: Plugin.Boot always constructs wristband.Server even with an empty app.url rather than failing loud, to avoid breaking the many existing fonoteka tests that boot without app.url configured; production must set app.url
- [Phase 08]: D-10 (oauth guard retirement) is recorded via TestOAuthMetadataRouteIsolation rather than editing Phase 6 historical docs, per 08-PATTERNS.md guidance to prefer a supersession note
### Pending Todos
@@ -228,6 +232,6 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity
Last session: 2026-09-23T10:52:49.787Z
Stopped at: Phase 8 UI-SPEC approved
Resume file: .planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
Last session: 2026-09-23T17:18:26.184Z
Stopped at: Completed 08-01-PLAN.md
Resume file: None

View File

@@ -0,0 +1,123 @@
---
phase: 08-oauth2-1-authorization-server
plan: 01
subsystem: auth
tags: [oauth2, rfc8414, wristband, surf, raw-routes, standard-library]
# Dependency graph
requires:
- phase: 06-http-routing-auth-groups-and-rate-limiting
provides: surf.GroupRaw raw-route surface, the reserved (now-retired) "oauth" guard slot, and fonoteka-oauth-token/fonoteka-oauth-register buckets
- phase: 07-user-plugin-and-authentication
provides: the JWT/inv_token guard split and bouncer.Registry that oauth-issued inv_ tokens will later authenticate against
provides:
- "wristband: a new app-agnostic framework package (Options, Server, DefaultOptions, exact RFC 8414 writer) that never imports fonoteka or GORM"
- "GET /.well-known/oauth-authorization-server mounted on the assembled fonoteka.go raw route group with zero middleware and no oauth guard"
- "scripts/check-phase8-red.sh: the shared fail-closed RED verifier (go and shell modes) every later Phase 8 plan uses before implementing its GREEN"
affects: [08-02-persistence-and-registration, 08-03-authorize, 08-04-token-exchange, 08-05-consent-and-connected-apps, 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:
- "wristband package: app-agnostic OAuth/RFC 8414 surface with Options for every deployment-specific value; only response_types/grant_types/code_challenge_methods stay fixed protocol constants (D-06)"
- "Local exact-JSON writer in wristband (not wire.WriteJSON): unwrapped body, no trailing newline, no HTML escaping, and on encode failure a bare 500 instead of the house opaque-500 envelope, so raw RFC responses never acquire a house-shaped body"
- "Fail-closed go-test-json RED verifier pattern: a stdlib-only Go program (invoked via `go run` from the bash wrapper) parses every JSON event, requires the named test to run+fail with its package, requires the exact sentinel exactly once, and rejects any other fail action, build/setup failure, panic substring, or malformed line"
key-files:
created:
- wristband/server.go
- wristband/server_test.go
- scripts/check-phase8-red.sh
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_metadata_test.go
modified:
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
key-decisions:
- "wristband.Options carries only the four PHP-configurable metadata fields (service_documentation path, scopes_supported, token_endpoint_auth_methods_supported, authorization_response_iss_parameter_supported); response_types/grant_types/code_challenge_methods are hardcoded protocol constants per D-06, not Options fields"
- "Plugin.Boot always constructs wristband.Server even when app.url is unset (issuer becomes empty string) rather than failing loud, to avoid a breaking change across the many existing fonoteka tests that boot the plugin without app.url configured; production deployments must set app.url"
- "D-10 (oauth guard retirement) is recorded here rather than by editing Phase 6 docs: TestOAuthMetadataRouteIsolation asserts no \"oauth\" guard is registered anywhere in the assembled bouncer.Registry"
patterns-established:
- "Two-repo TDD plan structure: RED and GREEN land as separate commits per repo (test commit in the repo whose test fails, feat commit in the repo(s) whose code makes it pass), matching the two-repositories execution contract"
requirements-completed: [AUTH-05, AUTH-06]
# Metrics
duration: 25min
completed: 2026-09-23
---
# Phase 08 Plan 01: Fail-Closed RED Infrastructure and Assembled RFC 8414 Metadata Summary
**New `wristband` framework package serves the exact 11-field unwrapped RFC 8414 document from the assembled Go app's raw route group, backed by a stdlib-only fail-closed `go test -json` RED verifier every later Phase 8 plan will reuse.**
## Performance
- **Duration:** ~25 min
- **Started:** 2026-09-23T17:05:00Z (approx.)
- **Completed:** 2026-09-23T17:14:26Z
- **Tasks:** 2 completed (4 commits: RED/GREEN pairs across both repos)
- **Files modified:** 6 (3 created in summercms.go, 1 created + 2 modified in fonoteka.go)
## Accomplishments
- `wristband` package created: app-agnostic `Options`/`Server`/`DefaultOptions` plus an exact-byte RFC 8414 metadata writer that never imports `fonoteka` or `gorm.io` (verified via `go list -deps`)
- `GET /.well-known/oauth-authorization-server` is now connector-visible on the real assembled Go app: exact PHP-parity bytes, `Content-Type: application/json`, `Cache-Control: no-cache, private`, no envelope, no trailing newline
- Route-table inspection proves the metadata route is `Raw`, carries zero middleware, and no `"oauth"` guard is registered anywhere — closing the Phase 6 D-09 reservation per D-10
- `scripts/check-phase8-red.sh` implements the shared `go`/`shell` fail-closed RED contract (D-04/D-18) that every remaining Phase 8 plan's verify step depends on; manually fuzz-tested against 8 rejection classes (unrelated failing test, compile failure, panic, malformed output, missing sentinel, duplicate sentinel, zero selection, plus both shell-mode wrong-exit and extra-stage-line cases) and the one acceptance case
## Task Commits
Each task was committed atomically (TDD RED then GREEN, split per repo where both repos changed):
1. **Task 1: fail-closed RED verification and metadata contracts** — `24d35d8` (test, summercms.go): stub `Server.Metadata` (501), failing `TestPhase8RedMetadata`, and `scripts/check-phase8-red.sh`
2. **Task 2a: implement exact metadata writer (GREEN for wristband)** — `c578bb5` (feat, summercms.go): real `Metadata` handler, `TestMetadataExactBytes`, `TestMetadataUsesConfiguredOptions`
3. **Task 2b: assembled metadata route test (RED for fonoteka.go)** — `58bb628` (test, fonoteka.go): `TestOAuthMetadataAssembled`/`RouteIsolation`/`IssuerTracksAppURL`, all failing 404 since the route was unmounted
4. **Task 2c: mount the metadata route (GREEN for fonoteka.go)** — `26b2478` (feat, fonoteka.go): `Plugin.Boot` constructs the server from `app.url`; `routes.go` mounts the raw GET route
**Plan metadata:** committed as part of this summary/state-update commit.
_Note: both tasks carry `tdd="true"`; RED/GREEN pairs land as separate commits, and Task 2 splits its RED/GREEN across the two repositories it touches._
## Files Created/Modified
- `wristband/server.go` — `Options`, `DefaultOptions`, `Server`, `NewServer`, `Metadata` handler, local exact-JSON writer
- `wristband/server_test.go` — `TestPhase8RedMetadata` (RED anchor), `TestMetadataExactBytes`, `TestMetadataUsesConfiguredOptions`
- `scripts/check-phase8-red.sh` — shared `go`/`shell` fail-closed RED verifier for the rest of Phase 8
- `../fonoteka.go/plugins/golem15/fonoteka/oauth_metadata_test.go` — assembled route tests (metadata bytes, route isolation, issuer tracking)
- `../fonoteka.go/plugins/golem15/fonoteka/plugin.go` — constructs `wristband.Server` in `Boot` from `app.url`
- `../fonoteka.go/plugins/golem15/fonoteka/routes.go` — mounts the metadata route on the existing raw group
## Decisions Made
- `wristband.Options` exposes only the four PHP-configurable metadata fields (D-06); `response_types_supported`, `grant_types_supported`, and `code_challenge_methods_supported` are hardcoded protocol constants inside `Metadata`, not Options, since this authorization server only ever supports one fixed set of grants/PKCE method (D-01)
- `Plugin.Boot` always constructs the OAuth server, even with an empty `app.url` (issuer becomes `""`), rather than failing Boot loudly. A strict fail-loud check was considered (matching the project's general "fail loud, no default" convention) but rejected here because it would break every existing test in the package that boots via `bootConfig`/`testConfig` without `app.url` set — dozens of unrelated tests across `plugins/golem15/fonoteka` and `parity`. This is scoped narrowly to this plan; a later plan may tighten validation once the full `plugins.golem15.fonoteka.oauth.*` config surface (D-03) is wired.
- D-10 (retiring the Phase 6 `"oauth"` guard reservation) is recorded here rather than by editing Phase 6 historical docs, per `08-PATTERNS.md`'s explicit guidance to prefer a supersession note in the Phase 8 plan/summary. `TestOAuthMetadataRouteIsolation` is the failing-when-broken proof: it asserts `reg.Middleware("oauth")` returns an error.
- `requirements.mark-complete AUTH-05 AUTH-06` was run per the state-update protocol, then manually reverted in `REQUIREMENTS.md` (checkbox and traceability table back to Pending). AUTH-05's own text spans authorize/PKCE/consent/token/DCR/refresh, none of which exist yet — only the metadata slice does. Unlike the HTTP-04/AUTH-01 precedent from Phases 6/7 (where the first touching plan already implemented the full requirement), marking AUTH-05/AUTH-06 Complete after Task 2's metadata-only slice would misrepresent phase progress; a later Phase 8 plan (the one that ships authorize/token/DCR/refresh, or 08-10's closing review) should be the one to flip these.
## Deviations from Plan
None — plan executed exactly as written. The RED verifier's fixture self-tests (compile failure, panic, malformed output, missing/duplicate sentinel, zero selection, unrelated failure, shell-mode wrong-exit/extra-stage-line) were exercised manually against throwaway fixtures in `/tmp` during Task 1 to prove the fail-closed contract described in the plan's acceptance criteria; per the plan's file list (`wristband/server.go`, `wristband/server_test.go`, `scripts/check-phase8-red.sh` only), these fixture self-tests are not committed as a permanent Go test suite here — `08-VALIDATION.md` row `08-W0-07` assigns the RED gate's own self-validation to plan 08-09.
## Issues Encountered
None.
## User Setup Required
None — no external service configuration required.
## Next Phase Readiness
- `wristband` exists as the framework home for the rest of Phase 8's OAuth surface (authorize, token, register, refresh rotation, sweeps) with an established Options/writer convention to extend.
- `scripts/check-phase8-red.sh` is ready for immediate reuse by plans 08-02 through 08-09, all of which reference it directly in their verify steps.
- The corrective OAuth schema/model nullability work flagged in `08-RESEARCH.md` ("Required Schema Correction") is still outstanding and is Wave 0 for plan 08-02, not touched here.
- No blockers.
## Self-Check: PASSED
- FOUND: wristband/server.go, wristband/server_test.go, scripts/check-phase8-red.sh, .planning/phases/08-oauth2-1-authorization-server/08-01-SUMMARY.md
- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/oauth_metadata_test.go, plugin.go, routes.go
- FOUND commits: 24d35d8, c578bb5 (summercms.go); 58bb628, 26b2478 (fonoteka.go)