Files
summercms/.planning/phases/08-oauth2-1-authorization-server/08-07-SUMMARY.md
2026-09-23 22:05:44 +02:00

16 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 07 auth
oauth2
rfc7591
dcr
bonfire
cobra
cli
wristband
gorm
phase plan provides
08-oauth2-1-authorization-server 06 wristband.Server.Revoke, D-17 expiry sweep, refresh rotation/lineage-kill, and the fonoteka classes/auth.OAuthStore GORM adapter this plan's command reuses for client persistence
bonfire.Flag.Repeatable / Input.Flags(name): an ordered, multi-occurrence string flag (Cobra StringSlice) alongside the existing scalar/bare Flag contract, with no change to existing callers
wristband.IssueClientCredentials / wristband.RejectRedirectURI: the exact random-id/secret/hash and redirect-URI validation RFC 7591 registration uses, exported so an app-owned command can share the identical path instead of re-deriving it
fonoteka:oauth-client (OAuthClientCommand): create a confidential, ceiling-bounded OAuth client with a one-time secret; --client-id adds redirect URIs without rotating the secret; --list prints id/name/revocation/redirects/ceiling and never a secret or hash
Fixed clientRecordToModel to persist ScopeCeiling, closing a gap silent since 08-02 (DCR never sets a ceiling, so nothing had exercised the missing mapping until this plan's command needed it)
08-08-mcp-me-prerequisite
08-09-parity-and-real-mcp-gate
08-10-unit-tests-and-security-review
added patterns
Repeatable bonfire flags: Command.Flags declares Repeatable:true, wrap() registers a Cobra StringSlice instead of a scalar String flag, and Input.Flags(name) reads it back in insertion order; scalar/bare flags on the same command are unaffected. Any future PHP-parity `=*` console option should follow this same shape.
Shared client-issuing path: wristband.IssueClientCredentials/RejectRedirectURI are the one place client_id/client_secret generation, hashing and redirect-URI validation happen; both RFC 7591 registration and the operator command call into them rather than each re-deriving the rule (T-08-SECRET-TIMING).
CreateWithCap reused with math.MaxInt32 for operator-issued clients: the atomic locked count+insert wristband.Tx.CreateWithCap already provides is reused for its insert path, with the cap effectively disabled, rather than adding a second uncapped Create method to the ClientStore interface.
created modified
../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go
../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client_test.go
wristband/client_issue.go
bonfire/command.go
bonfire/root.go
bonfire/output_test.go
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
../fonoteka.go/plugins/golem15/user/console_test.go
List/update operate directly on models.OAuthClient via *gorm.DB rather than extending wristband.ClientStore with List/Update methods: only Create needs the shared wristband hash/validation path (T-08-SECRET-TIMING); list/redirect-URI-merge are ordinary app-layer queries, matching how other app controllers (token_api_controller.go) already query models directly instead of going through wristband.
Create reuses wristband.Tx.CreateWithCap(ctx, rec, math.MaxInt32) instead of adding an uncapped Create method to ClientStore: an operator-issued client is never subject to DCR's 200-client cap (D-03/D-21 caps DCR flooding only), and reusing CreateWithCap keeps exactly one atomic insert path instead of two.
wristband/client_issue.go is a new exported file (not in the plan's literal files_modified list) because D-19's threat mitigation explicitly requires a shared hash/validation path between DCR and the operator command; wristband.IssueClientCredentials/RejectRedirectURI wrap the existing unexported randomBase64URL/sha256Hex/rejectRedirectURI so there is still exactly one implementation of each rule.
Fixed clientRecordToModel (classes/auth/oauth_store.go) to map ClientRecord.ScopeCeiling onto the persisted model: this field has existed on wristband.ClientRecord since 08-02 but nothing ever set it on a ClientRecord before this plan, so the missing mapping was silent until the operator command's ceiling needed to survive CreateWithCap.
AUTH-05/AUTH-07 remain Pending in REQUIREMENTS.md, continuing 08-02..08-06's decision: both requirements' full text also depends on an unchanged fonoteka-mcp install/auth flow proof that only 08-08/08-09 can establish; this plan ships the D-19 operator command only.
bonfire.Flag.Repeatable / Input.Flags(name) is the established shape for any future PHP `=*` console option; use it instead of inventing a second flag-collection mechanism.
~35min (approx.) 2026-09-23

Phase 08 Plan 07: OAuth Client Operator Command Summary

fonoteka:oauth-client bonfire command (create/--client-id/--list) sharing wristband's exact client-issuing hash/validation path, on top of a new repeatable-flag contract in bonfire (Flag.Repeatable / Input.Flags).

Performance

  • Duration: ~35 min (approx.)
  • Started: ~2026-09-23T19:25:00Z (approx.)
  • Completed: ~2026-09-23T20:00:08Z (approx.)
  • Tasks: 2 completed (4 commits: RED/GREEN pair in summercms.go for bonfire's repeatable-flag seam, RED test + one GREEN commit in fonoteka.go for the command)
  • Files modified: 9 (3 created + 3 modified in summercms.go's bonfire/wristband packages; 3 modified in fonoteka.go, one of them a pre-existing sibling test file fixed for the new Input method)

Accomplishments

  • bonfire.Flag gained Repeatable and Input gained Flags(name) []string: a Repeatable flag registers as a Cobra StringSlice instead of a scalar String flag, so --redirect-uri=a --redirect-uri=b is readable back in insertion order without disturbing any existing scalar/bare flag on the same command (TestFlagAndArgumentParsing and every other pre-existing bonfire test stayed green unchanged)
  • wristband.IssueClientCredentials/RejectRedirectURI export the exact random-id/secret/sha256-hash and redirect-URI validation RFC 7591 registration already used internally, so the operator command shares one implementation of "how a client secret is generated and hashed" with DCR (T-08-SECRET-TIMING) instead of re-deriving it
  • fonoteka:oauth-client (OAuthClientCommand) ports IssueOAuthClient.php byte-for-byte in shape: create prints client_id=/client_secret= once plus the non-recoverable warning, --client-id <id> merges new --redirect-uri values into an existing client without touching its secret hash, and --list prints client_id=/client_name=/revoked=/redirect_uri=/scope_ceiling= for every client and never a secret or hash
  • Fixed a silent gap in clientRecordToModel (fonoteka's GORM adapter): wristband.ClientRecord.ScopeCeiling has existed since 08-02 but was never mapped onto the persisted models.OAuthClient row, because DCR-created clients never set a ceiling; this plan's command is the first caller that needs the ceiling to actually survive CreateWithCap, and the gap would have silently dropped every --scope value without the fix
  • Both Phase 8 RED sentinels (PHASE8_RED:bonfire-flags in bonfire, PHASE8_RED:oauth-command in the fonoteka console package) were verified fail-closed via scripts/check-phase8-red.sh against the genuine pre-implementation state (unwired Repeatable; a "not implemented" command stub) before their GREEN commits; go vet/go test/go test -race are green across summercms.go and every touched fonoteka.go workspace module (root fonoteka/parity, plugins/golem15/fonoteka and its subpackages, plugins/golem15/user)

Task Commits

Each task's RED test was committed and verified fail-closed via scripts/check-phase8-red.sh before its GREEN implementation:

  1. Task 1: repeatable-flag and command RED anchors
    • 7096a90 (test, summercms.go): TestPhase8RedBonfireFlags fails against unwired Repeatable (PHASE8_RED:bonfire-flags); adds the compiling seam (Flag.Repeatable, Input.Flags, cobraInput.Flags)
    • 4efce33 (test, fonoteka.go): TestPhase8RedOAuthClientCommand fails against the "not implemented" command stub (PHASE8_RED:oauth-command); adds console/oauth_client.go's exact flag/argument contract and fixes user/console_test.go's emailArg to satisfy the extended bonfire.Input interface
  2. Task 2: implement repeatable flags and the exact OAuth client command
    • 398353b (feat, summercms.go): wires Repeatable into Cobra StringSlice registration in wrap(); exports wristband.IssueClientCredentials/RejectRedirectURI; adds TestRepeatableFlagUnsetReturnsEmpty/TestRepeatableFlagCoexistsWithBareAndScalar
    • 9e82cac (feat, fonoteka.go): the real OAuthClientCommand create/update/list implementation, Plugin.Commands() registration, the clientRecordToModel ScopeCeiling fix, and the full OAuthClientCommandTest parity test matrix

Plan metadata: committed as part of this summary/state-update commit.

Note: both tasks carry tdd="true"; RED/GREEN pairs land as separate commits, split per repo.

Files Created/Modified

  • bonfire/command.go — Flag.Repeatable, Input.Flags(name) []string, cobraInput.Flags
  • bonfire/root.go — wrap() registers a Repeatable flag as Cobra StringSliceP/StringSlice
  • bonfire/output_test.go — TestPhase8RedBonfireFlags, TestRepeatableFlagUnsetReturnsEmpty, TestRepeatableFlagCoexistsWithBareAndScalar
  • wristband/client_issue.go — IssueClientCredentials, RejectRedirectURI (exported wrappers over existing unexported primitives)
  • ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go — OAuthClientCommand: create/--client-id/--list, plus oauthClientCollectRedirectURIs/oauthClientCollectScopeCeiling/oauthClientMergeUniqueURIs helpers
  • ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client_test.go — TestPhase8RedOAuthClientCommand plus the OAuthClientCommandTest parity matrix (list-never-leaks-secret, add-redirect-uri-keeps-secret, unknown client_id, scope-ceiling persistence/listing, invalid-scope rejects without creating a client, 1..5-redirect-uri bound) and the package's own real-Postgres TestMain harness
  • ../fonoteka.go/plugins/golem15/fonoteka/plugin.go — Commands() registers console.OAuthClientCommand(p.app); pact.HasCommands assertion
  • ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go — clientRecordToModel now maps ScopeCeiling
  • ../fonoteka.go/plugins/golem15/user/console_test.go — emailArg.Flags(string) []string to satisfy the extended bonfire.Input interface

Decisions Made

See frontmatter key-decisions. The most load-bearing: list/update deliberately stay outside the wristband.ClientStore abstraction (ordinary *gorm.DB queries against models.OAuthClient), while create deliberately goes through wristband.Tx.CreateWithCap with math.MaxInt32 as the cap — only client issuance needs the shared hash/validation path the threat model calls for; list/redirect-URI-merge are plain app-layer reads/writes with no security-sensitive generation logic to share.

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] user/console_test.go's emailArg did not satisfy the extended bonfire.Input interface

  • Found during: Task 1, immediately after adding Input.Flags to bonfire/command.go
  • Issue: bonfire.Input gaining a new method broke compilation of every existing structural implementer; user/console_test.go's emailArg (used by TestRequirePasswordChange) is the only other one in either repo
  • Fix: Added func (e emailArg) Flags(string) []string { return nil }
  • Files modified: ../fonoteka.go/plugins/golem15/user/console_test.go
  • Verification: go build ./... and go test ./... green in the plugins/golem15/user module
  • Committed in: 4efce33 (Task 1 RED commit, fonoteka.go)

2. [Rule 2 - Missing Critical] clientRecordToModel never persisted ScopeCeiling

  • Found during: Task 2, while wiring the create path's scope-ceiling persistence
  • Issue: wristband.ClientRecord.ScopeCeiling has existed since 08-02, and clientModelToRecord already read it back, but the reverse mapping in clientRecordToModel was missing — every ceiling passed to CreateWithCap would have been silently dropped, defeating the ceiling this plan's command exists to set (T-08-SCOPE-CEILING)
  • Fix: clientRecordToModel now maps ScopeCeiling onto the persisted models.OAuthClient row
  • Files modified: ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
  • Verification: TestOAuthClientCommandScopeCeilingPersistsAndListsNeverTheSecret
  • Committed in: 9e82cac (Task 2 GREEN commit, fonoteka.go)

3. [Rule 2 - Missing Critical] wristband/client_issue.go added beyond the plan's literal files_modified list

  • Found during: Task 2, implementing the create path
  • Issue: The plan's threat model (T-08-SECRET-TIMING) requires the command to share DCR's exact hash/validation path, but no exported wristband function existed for random client-id/secret generation, hashing, or redirect-URI validation
  • Fix: Added wristband/client_issue.go exporting IssueClientCredentials/RejectRedirectURI as thin wrappers over the existing unexported randomBase64URL/sha256Hex/rejectRedirectURI, adding no new logic
  • Files modified: wristband/client_issue.go (new)
  • Verification: go test ./wristband/... -race; TestOAuthClientCommandListPrintsClientIDAndURIsNeverTheSecret et al. exercise the shared path end to end
  • Committed in: 398353b (Task 2 GREEN commit, summercms.go)

Total deviations: 3 auto-fixed (1 blocking compile fix, 2 missing-critical-functionality additions required by the plan's own threat model and objective) Impact on plan: No scope change; all three were necessary for the plan's stated D-19/T-08-SCOPE-CEILING/T-08-SECRET-TIMING behavior to actually work, not separate feature additions.

Issues Encountered

None beyond the auto-fixed items above. Full go vet/go test ./... (and go test -race on the touched packages) are green in summercms.go and in every fonoteka.go workspace module: the root fonoteka/parity module, plugins/golem15/fonoteka and its classes, classes/auth, console, controllers/api, middleware, models, updates subpackages, and plugins/golem15/user and its classes/updates subpackages.

User Setup Required

None — no external service configuration required.

Next Phase Readiness

  • fonoteka:oauth-client is available for operators to issue confidential clients for chat-app connectors ahead of 08-09's real-MCP gate, and for the D-16 lifecycle corpus's confidential-client-with-ceiling fixture.
  • bonfire.Flag.Repeatable/Input.Flags is the established shape for any future PHP =* console option; no further bonfire interface changes are anticipated for the remaining Phase 8 plans' known scope.
  • AUTH-05/AUTH-07 remain Pending in REQUIREMENTS.md: this plan ships the D-19 operator command, but both requirements' full text also needs 08-08/08-09's unchanged-fonoteka-mcp-install/auth-flow proof.
  • 08-08 (mcp-me prerequisite) and 08-09 (parity-and-real-mcp-gate) can proceed without any further change to this plan's surface.
  • No blockers.

Self-Check: PASSED

  • FOUND: bonfire/command.go, bonfire/root.go, bonfire/output_test.go
  • FOUND: wristband/client_issue.go
  • FOUND: ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go, oauth_client_test.go
  • FOUND: ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, classes/auth/oauth_store.go
  • FOUND: ../fonoteka.go/plugins/golem15/user/console_test.go
  • FOUND commits (summercms.go): 7096a90, 398353b
  • FOUND commits (fonoteka.go): 4efce33, 9e82cac

Phase: 08-oauth2-1-authorization-server Completed: 2026-09-23