fix(08): revise plans based on checker feedback

This commit is contained in:
Jakub Zych
2026-09-23 17:13:47 +02:00
parent 241af16ba7
commit 3c6a505c5f
14 changed files with 1100 additions and 911 deletions

View File

@@ -5,48 +5,54 @@ type: execute
wave: 6
depends_on: [08-05]
files_modified:
- wristband/phase08_coverage_test.go
- ../fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go
- ../fonoteka.go/parity/oauth_audit_test.go
- scripts/check-phase8.sh
- .planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md
- .planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md
- .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
autonomous: false
- wristband/token.go
- wristband/token_test.go
- wristband/stores.go
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go
autonomous: true
requirements: [AUTH-05, AUTH-06, AUTH-07]
must_haves:
truths:
- "Every PHP OAuth functional/security test method maps to a named passing Go test or subtest, and both repositories pass vet/test/race."
- "Every T-08 threat maps to a failing-when-broken test with zero open high-severity findings."
- "The phase gate refuses missing tests, route parity, secret scans, unchanged-client evidence, or an unverified security review."
- "D-04: A valid refresh token rotates to a new access/refresh pair while revoking the prior access token."
- "D-17: Replaying a spent refresh token commits revocation of the whole lineage and unexpired evidence remains, leaving no usable branch."
- "D-08: A user sees only their live connected OAuth apps and can revoke one access token plus its refresh lineage atomically."
artifacts:
- path: ".planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md"
provides: "Auditable one-to-one map of all 103 PHP methods to Go evidence"
- path: ".planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md"
provides: "ASVS L1 threat disposition and executed evidence"
- path: ".planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md"
provides: "Nyquist-complete task/status and gate sign-off"
- path: "wristband/token.go"
provides: "Refresh grant rotation, replay detection, and committed lineage kill"
- path: "../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go"
provides: "Row-locked refresh traversal, revoke, and expiry sweep storage"
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go"
provides: "Owner-scoped connected-app list and revoke handlers"
key_links:
- from: "08-SECURITY-REVIEW.md"
to: "named Go tests"
via: "file:TestName evidence for every mitigated threat"
pattern: "T-08-"
- from: "scripts/check-phase8.sh"
to: "08-SECURITY-REVIEW.md"
via: "fail-closed verified/zero-open audit check"
pattern: "08-SECURITY-REVIEW"
- from: "wristband/token.go"
to: "oauth_store.go"
via: "transaction outcome commits lineage kill before returning invalid_grant"
pattern: "WithinTx"
- from: "connected_app_controller.go"
to: "wristband.Server"
via: "cascade revoke operation rather than direct refresh-row deletion"
pattern: "Revoke"
- from: "connected_app_controller.go"
to: "token_api_controller.go"
via: "reuse of positive allow-list token serializer"
pattern: "serializeToken"
---
<objective>
Close Phase 8 with complete unit/security coverage, an independent security-review agent pass, and one fail-closed verification gate.
Deliver the durable OAuth lifecycle slice: safe refresh rotation/replay handling and user-visible connected-app listing/revocation.
Purpose: Demonstrate that the exact OAuth implementation is not merely functional but resistant to every identified high-severity replay, redirect, timing, scope, ownership, leakage, flooding, and surface threat.
Output: Coverage tests, 103-method audit map, verified security review, signed validation strategy, and final gate.
Purpose: Ensure stolen or replayed refresh tokens cannot create surviving branches and the unchanged Settings UI controls the same grant lineage.
Output: Refresh-grant state machine, row-locked store operations, connected-app controllers/routes, and concurrency-backed lifecycle tests.
</objective>
## Phase Goal
**As a** Płytarium operator, **I want to** rely on independently reviewed OAuth behavior and complete regression evidence, **so that** unchanged connectors can be enabled without accepting an unproven high-severity security risk.
**As a** connected-app user, **I want to** refresh access safely and revoke applications from Settings, **so that** replayed or revoked credentials immediately lose access.
<execution_context>
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
@@ -59,126 +65,114 @@ Output: Coverage tests, 103-method audit map, verified security review, signed v
@.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-VALIDATION.md
@.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
@.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
@.planning/phases/08-oauth2-1-authorization-server/08-05-SUMMARY.md
@.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md
<interfaces>
Security-review evidence contract:
- One register row per `T-08-*` threat with category, component, disposition, mitigation, and exact `file:TestName` evidence.
- Frontmatter reports total/closed/open and status; completion requires `status: verified`, `threats_open: 0`, and no unmitigated HIGH.
From Plan 08-05:
- Token handler already dispatches `authorization_code` and recognizes the configured refresh grant.
- `wristband.Tx` provides row-lock-capable refresh/code stores and the transaction-bound access-token issuer.
- OAuth access tokens are `models.ApiToken` rows distinguished by non-null `OAuthClientID`.
PHP test inventory contract:
- 103 methods: authorize 11, client-command 8, metadata 2, migration 7, register 10, token 10, consent/scope 7, refresh rotation 10, revocation 8, surface isolation 30.
Existing serializer:
- `serializeToken(gdb, *models.ApiToken) map[string]any` is the required positive allow-list for connected-app responses.
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Close the 103-method PHP audit and Phase 8 coverage gaps</name>
<files>wristband/phase08_coverage_test.go, ../fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go, ../fonoteka.go/parity/oauth_audit_test.go, .planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md</files>
<name>Task 1: Specify rotation, replay, list, and revoke as one lifecycle</name>
<files>wristband/token_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go</files>
<read_first>
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
wristband/registration_test.go
wristband/authorize_test.go
wristband/token_test.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go
../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go
../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go
../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthAuthorizeTest.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthClientCommandTest.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMetadataTest.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMigrationTest.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthRegisterTest.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthTokenTest.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthConsentScopeCeilingTest.php
wristband/token.go
wristband/stores.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/auth/OAuthCodeManager.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/ConnectedAppController.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthRefreshRotationTest.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthRevocationTest.php
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/TokenSurfaceIsolationTest.php
</read_first>
<behavior>
- Every PHP method is listed once with its source class, behavior, and a named Go test/subtest that actually runs.
- Coverage tests exercise error branches, encoding failures, nil/misconfigured dependencies, clock/entropy errors, parser edges, and route/config drift not already covered.
- Audit fails if a mapped Go test is renamed/missing or if any PHP method is unmapped/duplicated.
- Normal refresh revokes the old access token, marks predecessor `rotated_to_id`, and returns a new same-scope/same-collection pair.
- Sequential or concurrent spent-token replay returns `invalid_grant` only after the complete lineage and current access token are durably revoked.
- Expiry sweep removes only expired pending/code/refresh rows and retains unexpired rotated/revoked refresh rows as replay evidence.
- Connected-app list is newest-first, owner-only, live OAuth tokens only, with manual count separate and no secret/client-id fields.
- Revoke of an owned OAuth token kills its refresh lineage; foreign, missing, and manual token IDs share the exact 404.
</behavior>
<action>Per D-18 and the repository rule that unit coverage is the last plan, enumerate all 103 PHP methods into `08-PHP-TEST-MAP.md`, map each to existing Phase 8 tests, and add focused coverage tests only where no named evidence exists. Add an executable audit that parses the inventory/map and Go test list so counts alone cannot hide missing or duplicate mappings. Close framework handler/store branches, app boot/config/route/controller/command branches, parity projections, and every exact response/header path. Do not replace behavior assertions with coverage-only calls or map one broad test to methods whose distinct assertions are absent.</action>
<action>D-04: and D-18: extend the RED suite with deterministic in-memory tests and synchronized real-Postgres contention tests for T-08-REFRESH-REPLAY, T-08-CROSS-USER, T-08-SCOPE-CEILING, T-08-REQUEST-LEAK, and T-08-SURFACE. D-16: cover the lifecycle sequence later recorded by parity. D-17: prove exact sweep retention. Include an assembled lifecycle that starts with the grant from 08-05, refreshes, replays the spent predecessor, verifies the new branch and access token are dead, creates another grant, lists it, revokes it, and proves refresh afterward fails. Use compiling stubs and separate `PHASE8_RED:lifecycle-framework` and `PHASE8_RED:lifecycle-app` assertions; reject syntax/build/setup/missing-test failures through the shared RED verifier. Assert exact UI response allow-lists and 404 bytes.</action>
<verify>
<automated>go test ./wristband -count=1 &amp;&amp; cd ../fonoteka.go &amp;&amp; go test ./plugins/golem15/fonoteka/... ./parity -run 'Test(OAuth|Phase08|PHPTestMap|TokenSurface|MeToken)' -count=1</automated>
<automated>scripts/check-phase8-red.sh lifecycle-framework go test ./wristband -run 'Test(Refresh|Replay|Sweep)' -count=1 &amp;&amp; scripts/check-phase8-red.sh lifecycle-app bash -lc "cd ../fonoteka.go &amp;&amp; go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Refresh|Replay|Connected|Revoke|Sweep)' -count=1"</automated>
</verify>
<acceptance_criteria>
- The map contains exactly 103 unique PHP method rows distributed 11/8/2/7/10/10/7/10/8/30 by source suite.
- The executable audit confirms every mapped Go `TestName[/subtest]` exists and executes; missing or duplicate rows make it fail.
- Both repositories pass full `go vet ./...`, `go test ./...`, and `go test -race ./...`, including nested plugin modules.
- Coverage additions retain exact byte/header/concurrency assertions for security branches.
- Tests include sequential replay, a barrier-synchronized double refresh, committed lineage kill, expiry retention, owner isolation, manual-token exclusion, list ordering, and post-revoke refresh failure.
- The replay test explicitly reloads database rows after the `invalid_grant` response and asserts revoked lineage/access state, preventing rollback-hidden false positives.
- Tests fail on missing refresh/connected-app implementation while all 08-02 happy-path tests remain green.
</acceptance_criteria>
<done>All PHP OAuth behavior has one-to-one named Go evidence and the phase's code paths are covered by meaningful regression tests.</done>
<done>The RED lifecycle suite detects branch survival, rollback of replay revocation, ownership leaks, serialization leaks, and route misplacement.</done>
</task>
<task type="auto">
<name>Task 2: Run the mandated security-review agent and close every high-severity finding</name>
<files>.planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md, .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md, scripts/check-phase8.sh</files>
<task type="auto" tdd="true">
<name>Task 2: Implement refresh rotation, committed replay kill, and exact sweeps</name>
<files>wristband/token.go, wristband/stores.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go</files>
<read_first>
.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-PHP-TEST-MAP.md
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md
scripts/check-phase8.sh
wristband/server.go
wristband/authorize.go
wristband/token_test.go
wristband/token.go
wristband/register.go
wristband/crypto.go
wristband/stores.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go
../fonoteka.go/plugins/golem15/fonoteka/routes.go
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_refresh_token.go
../fonoteka.go/plugins/golem15/fonoteka/models/api_token.go
</read_first>
<action>Invoke the `gsd-security-auditor` security-review agent against all Phase 8 production/test changes and the locked threat map, explicitly requiring OWASP ASVS L1 review of T-08-PKCE, CODE-REPLAY, REFRESH-REPLAY, OPEN-REDIRECT, SECRET-TIMING, SCOPE-CEILING, CROSS-USER, REQUEST-LEAK, DCR-FLOOD, SURFACE, and supply-chain status. Write `08-SECURITY-REVIEW.md` in the Phase 6 format with trust boundaries, complete STRIDE register, severity, disposition, mitigation, and executed `file:TestName` evidence. If the agent finds any HIGH issue, stop sign-off, implement the narrow fix and failing regression in the owning Phase 8 file, rerun the focused and full gates, and re-run the auditor until no HIGH remains. Then update VALIDATION task IDs/statuses, set `nyquist_compliant: true` and `wave_0_complete: true`, and add a fail-closed verified/zero-open security-review check to `check-phase8.sh`.</action>
<behavior>
- Presented refresh lookup uses SHA-256 hash and a row lock inside the single transaction boundary.
- Replay revocation returns success from the transaction callback, then maps the recorded outcome to `invalid_grant` outside it.
- Rotation keeps predecessor/successor relationships and unexpired evidence rows.
</behavior>
<action>Implement D-04, D-05, D-07, and D-17's refresh branch in wristband and the GORM adapter. Authenticate the client using the same Basic-over-form rule, hash the presented refresh secret, lock its row, reject expired/revoked/wrong-client grants, and rotate atomically by revoking the old access token, minting/persisting its successor, creating the next refresh secret/hash, and linking `rotated_to_id`. If a spent token is presented, traverse and revoke the whole lineage and associated access tokens, return nil from the transaction so the kill commits, then return `invalid_grant` from the handler. Keep the old scopes, collection IDs, offline flag, and client binding. Sweep only rows whose `expires_at` is past; do not delete unexpired replay evidence.</action>
<verify>
<automated>scripts/check-phase8.sh</automated>
<automated>go test ./wristband -run 'Test(Refresh|Replay|Sweep)' -count=1 &amp;&amp; cd ../fonoteka.go &amp;&amp; go test ./plugins/golem15/fonoteka/classes/auth -run 'TestOAuth(Refresh|Replay|Sweep)' -count=1</automated>
</verify>
<acceptance_criteria>
- `08-SECURITY-REVIEW.md` has `status: verified`, `threats_open: 0`, and one evidence-backed disposition for every named T-08 threat plus T-08-SC.
- Every HIGH finding is mitigated by a named failing-when-broken test; no HIGH is accepted, deferred, or omitted.
- Static evidence finds `crypto/subtle.ConstantTimeCompare` for client secret and PKCE, row locks for code/refresh, committed replay kill, 64 KiB register cap, raw/JWT/personal route isolation, and no sensitive-value logging.
- `08-VALIDATION.md` maps final plan/task IDs, all required test/gate files exist, all statuses are green, and both Nyquist flags are true.
- `scripts/check-phase8.sh` exits nonzero if the review is missing, unverified, has a nonzero open count, or lacks any required T-08 row.
- Normal refresh and sequential/concurrent replay tests pass under real Postgres.
- A spent-token replay leaves every lineage refresh row and its live access token revoked after the response transaction commits.
- `go test -race ./wristband` passes and the app contention test produces a single usable branch.
- Sweep tests prove expired rows are removed and unexpired rotated/revoked rows remain.
</acceptance_criteria>
<done>An independent security agent has reviewed the implemented phase, all high-severity findings are closed with executable evidence, and the final gate enforces the review.</done>
<done>Refresh rotation is atomic, preserves replay evidence, and commits whole-lineage revocation before emitting the protocol error.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking-human">
<name>Task 3: Approve the OAuth security and unchanged-client evidence</name>
<files>.planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md, .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md</files>
<task type="auto" tdd="true">
<name>Task 3: Expose connected-app list and atomic revoke to the unchanged Settings UI</name>
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go</files>
<read_first>
.planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md
.planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
.planning/phases/08-oauth2-1-authorization-server/08-05-SUMMARY.md
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go
../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go
../fonoteka.go/plugins/golem15/fonoteka/routes.go
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/components/fonoteka/ConnectedAppsManager.vue
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/stores/fonoteka.ts
</read_first>
<action>Present the completed automated evidence after the security-review agent has produced zero open high-severity findings. Do not ask the user to rerun automation; show the exact gate result, threat totals, 103-method audit result, nine-route parity result, real-MCP lifecycle result, and unchanged Nuxt/MCP worktree checks. Block completion if any displayed result is missing or non-green.</action>
<behavior>
- List returns `data` and numeric `manual_tokens_count`, filters to owner/live/OAuth tokens, and orders created_at descending.
- Every row is `serializeToken` plus sanitized client name and contains no raw credential, hash, OAuth client id, redirect URI, or other-user data.
- Revoke completes access-token and refresh-lineage revocation before returning `{"data":{"revoked":true}}`.
</behavior>
<action>Per D-08 and the UI-SPEC, add GET and DELETE connected-app controllers in the JWT group. Reuse `serializeToken`; append only the sanitized/truncated client name, initialize collection/scope arrays as arrays, count live manual tokens separately, and order OAuth tokens newest first. Scope every query by `bouncer.User`. For DELETE, require an owned OAuth token, invoke the wristband lineage-revoke operation in the same committed transaction, and collapse missing/foreign/manual IDs to exact `{"error":"Token not found"}` 404. Mount only under `/_fonoteka/api/v1/oauth`; do not expose these routes on the personal-token or raw groups.</action>
<verify>
<automated>scripts/check-phase8.sh</automated>
<automated>cd ../fonoteka.go &amp;&amp; go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(ConnectedApps|Revoke|Lifecycle|Surface)' -count=1</automated>
</verify>
<what-built>Direct standard-library OAuth authorization server with DCR, S256 PKCE, JWT consent, authorization-code and rotating-refresh grants, connected-app revocation, operator client command, MCP bootstrap endpoint, PHP parity, and unchanged real-MCP proof.</what-built>
<how-to-verify>
1. Review `08-SECURITY-REVIEW.md`; expect `status: verified`, zero open threats, and named test evidence for every T-08 row.
2. Review the recorded gate transcript; expect both repositories' vet/test/race, 103/103 PHP method map, nine OAuth route replays, secret scan, and real MCP lifecycle to be green.
3. Confirm the Nuxt and fonoteka-mcp repositories have no Phase 8 source diff.
</how-to-verify>
<acceptance_criteria>
- Human approval occurs only after zero open HIGH findings and a passing `scripts/check-phase8.sh` result are shown.
- The evidence explicitly includes exact Basic `WWW-Authenticate` at backend invalid-client, unchanged no-challenge token 401, and MCP-owned rich Bearer/resource-metadata behavior.
- Rejection includes the failing threat/test/gate identifier so remediation is deterministic.
- Empty, populated, manual-count, newest-first, foreign/manual 404, and successful atomic revoke tests pass with exact bytes.
- The lifecycle test proves the revoked app disappears on the next list and its refresh token returns `invalid_grant`.
- JSON assertions reject `token`, `token_hash`, `oauth_client_id`, `client_secret`, `refresh_token`, `request_id`, and `redirect_uris` anywhere in list output.
- The Nuxt repository remains unchanged.
</acceptance_criteria>
<resume-signal>Type "approved" to close Phase 8, or provide the failed threat/test/gate identifier.</resume-signal>
<done>The user has accepted the complete automated OAuth compatibility and security evidence.</done>
<done>The existing Settings → Integrations UI can list and revoke only the current user's connected applications, and revoke kills the entire grant lineage.</done>
</task>
</tasks>
@@ -188,39 +182,32 @@ PHP test inventory contract:
| Boundary | Description |
|----------|-------------|
| Phase implementation → independent auditor | Claims must be supported by executable evidence, not implementation intent. |
| Test inventory → completion status | Missing/renamed tests or unmapped PHP methods must fail closed. |
| Security report → phase gate | Stale, missing, or open findings must prevent sign-off. |
| Refresh credential → token endpoint | A bearer-like long-lived credential requests a new grant branch. |
| JWT principal → connected-app API | User-controlled ids request listing/revocation of durable credentials. |
| Transaction outcome → OAuth error | Security revocation must commit even though the protocol response is an error. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-08-PKCE | Spoofing/Elevation | authorize/exchange | mitigate | Independent audit plus missing/plain/wrong/syntax/constant-time tests. |
| T-08-CODE-REPLAY | Spoofing | code transaction | mitigate | Sequential/concurrent single-winner tests and row-lock source evidence. |
| T-08-REFRESH-REPLAY | Spoofing/Elevation | refresh transaction | mitigate | Branch-concurrency and post-error persisted lineage-kill evidence. |
| T-08-OPEN-REDIRECT | Spoofing/Disclosure | redirect construction | mitigate | Exact allow-list/validation-order and no-Location tests. |
| T-08-SECRET-TIMING | Information Disclosure | crypto/client auth | mitigate | `subtle.ConstantTimeCompare` source gate and invalid-secret behavior tests. |
| T-08-SCOPE-CEILING | Elevation | authorize/consent/refresh | mitigate | End-to-end requested/submitted/ceiling/mintable and server-derived tenant proofs. |
| T-08-CROSS-USER | Elevation | consent/connected apps | mitigate | Foreign ownership tests with indistinguishable 404s. |
| T-08-REQUEST-LEAK | Information Disclosure | logs/fixtures/output | mitigate | Log capture, source scan, fixture secret scan, positive output allow-lists. |
| T-08-DCR-FLOOD | Denial of Service | register | mitigate | 64 KiB cap, limiter, atomic client cap, sweep, concurrency evidence. |
| T-08-SURFACE | Elevation | route/MCP boundary | mitigate | Assembled route table and real client header-ownership gate. |
| T-08-SC | Tampering | package supply chain | mitigate | No added package; module-diff and package-audit checks. |
| T-08-REFRESH-REPLAY | Spoofing/Elevation | refresh state machine | mitigate | Row lock, rotation chain, single-winner concurrency, commit lineage kill before `invalid_grant`. |
| T-08-CROSS-USER | Elevation | connected-app controllers | mitigate | Owner-scoped reads/deletes and indistinguishable missing/foreign/manual 404. |
| T-08-SCOPE-CEILING | Elevation | refresh rotation | mitigate | Copy only stored granted scopes/collection ids; refresh cannot add request-provided authority. |
| T-08-REQUEST-LEAK | Information Disclosure | list response/logs | mitigate | Positive allow-list serializer and explicit forbidden-field tests. |
| T-08-SURFACE | Elevation | route groups | mitigate | JWT-only management route inspection; token endpoint remains raw. |
| T-08-SC | Tampering | dependencies | mitigate | No install; standard library and existing GORM only. |
</threat_model>
<verification>
- `go vet ./... && go test ./... && go test -race ./...`
- `cd ../fonoteka.go && go vet ./... && go test ./... && go test -race ./...`
- `scripts/check-phase8.sh`
- Human approval after independent security review reports zero open HIGH findings.
- `go test ./wristband -run 'Test(Refresh|Replay|Sweep)' -count=1`
- `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Refresh|Replay|ConnectedApps|Revoke|Lifecycle|Surface)' -count=1`
- `cd ../fonoteka.go && go test -race ./plugins/golem15/fonoteka/classes/auth ./plugins/golem15/fonoteka`
</verification>
<success_criteria>
- All 103 PHP methods map uniquely to named passing Go tests/subtests.
- All T-08 threats have explicit dispositions and executable evidence; zero high-severity findings remain open.
- Nyquist validation, parity, secret scan, and unchanged real-MCP lifecycle are green in the final gate.
- The blocking human security checkpoint is approved.
- Refresh rotation has exactly one usable successor and replay durably kills the entire lineage.
- Connected-app output matches the UI contract and cannot disclose secrets or other users.
- Revocation removes the app from the list and invalidates both access and refresh credentials before success is returned.
</success_criteria>
<output>