docs(19.7-08): complete plugin gap-closure plan (lane C, E-1, E-2)
This commit is contained in:
343
.planning/phases/19.7-generic-mcp-module/19.7-08-SUMMARY.md
Normal file
343
.planning/phases/19.7-generic-mcp-module/19.7-08-SUMMARY.md
Normal file
@@ -0,0 +1,343 @@
|
||||
---
|
||||
phase: 19.7-generic-mcp-module
|
||||
plan: 08
|
||||
subsystem: plugins
|
||||
status: complete
|
||||
gap_closure: true
|
||||
tags: [sm-user-plugin, sm-oauthserver-plugin, sm-journal-plugin, sm-demo-app, vue-demo-app, auth_time, account-link, revocation, oidc, member-token-scopes, organisation-logo, nginx, gap-closure]
|
||||
|
||||
requires:
|
||||
- phase: 19.7-06
|
||||
provides: pact.MCPPrompt.Writes (Journal's draft_post)
|
||||
- phase: 19.7-07
|
||||
provides: bouncer.VerifyAuthTime, pact.HasFrontendTokenScopes / FrontendTokenScope / ValidTokenScopeName, MCPSubjectChanged.TokenID / ClientID, towel.ClientIP
|
||||
provides:
|
||||
- "sm-user-plugin: auth_time-based account link confirm, POST /backend-link/show, #code= link with backend_links.page_path, BackendLinksFor, BackendLinkPagePath"
|
||||
- "sm-user-plugin: AccessRevokedEvent + FireAccessRevoked (ban, password reset, admin-set password, self password change), banned users excluded from LinkedBackendUser, FireSubjectEvent / FireSubjectEventAfterCommit, last_used_ip on the MCP personal-token path"
|
||||
- "sm-user-plugin: member token scopes (APITokenScope, APITokenScopeCatalog, CompileAPITokenScopes, APITokenScopesFor, MintAPITokenFor, ResetDeclaredAPITokenScopes, GET /_user/api/v1/tokens/scopes)"
|
||||
- "sm-user-plugin: organisation logo (OrganisationLogoField, OrganisationLogoURL, OrganisationLogoFields, OrganisationLogoSize)"
|
||||
- "sm-oauthserver-plugin: verifier and MCP refresh refuse banned and pre-cutoff tokens (issuedBeforeCutoff); Cascade.OnAccessRevoked / OnBackendLinked / OnBackendUnlinked / OnBackendLinkChanged over one revokeUserGrants; client-specific revoke event"
|
||||
- "sm-journal-plugin: draft_post with Writes: true"
|
||||
- "sm-demo-app: nginx client_max_body_size 4m on /mcp enforced by check-deploy.sh; vue-demo-app /link-admin fragment flow; TestMCPEndToEnd updated for C-WR-02"
|
||||
affects: [19.7-09 (named unit tests, gate, security-review corrections), framework v0.5.0 and the plugin tags, sm-servers-app (E-1, E-2 hand-off)]
|
||||
|
||||
actuals:
|
||||
tokens: 45949
|
||||
tasks: 4
|
||||
commits: 0
|
||||
commits_by_repo:
|
||||
sm-user-plugin: 4
|
||||
sm-oauthserver-plugin: 1
|
||||
sm-journal-plugin: 1
|
||||
vue-demo-app: 1
|
||||
sm-demo-app: 3
|
||||
plan_head_before: 5aeea304d02080bcffb7b127153e0e92ba91d2ab
|
||||
plan_head_after: 5aeea304d02080bcffb7b127153e0e92ba91d2ab
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "A sign-in proof reads auth_time (bouncer.VerifyAuthTime), never iat; a token without the claim is treated as stale"
|
||||
- "One revocation event per user change (AccessRevokedEvent) fired in the write's transaction; listeners revoke elsewhere-held grants; the after-commit subject event ends sessions"
|
||||
- "Whole-second cutoffs compared as created_at < cutoff + 1s (issuedBeforeCutoff)"
|
||||
- "Per-application catalogs with a process-wide fallback only for a legacy signature (MintAPIToken vs MintAPITokenFor)"
|
||||
- "A one-shot secret travels in a URL fragment and a POST body, never a logged URL"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/access_revoked.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/api_token_scopes.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/organisation_logo.go
|
||||
- ../sm-demo-app/plugins/golem15/user/backend_link_auth_time_smoke_test.go
|
||||
- ../sm-demo-app/plugins/golem15/user/member_token_scopes_smoke_test.go
|
||||
- ../sm-oauthserver-plugin/mcp_revocation_smoke_test.go
|
||||
- ../sm-demo-app/vue-demo-app/app/lib/linkCode.ts
|
||||
modified:
|
||||
- ../sm-demo-app/plugins/golem15/user/controllers/backend_link_controller.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/backend_links.go
|
||||
- ../sm-demo-app/plugins/golem15/user/account_contributions.go
|
||||
- ../sm-demo-app/plugins/golem15/user/routes.go
|
||||
- ../sm-demo-app/plugins/golem15/user/config/config.yaml
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/events.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/subject_events.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/mcp_guard.go
|
||||
- ../sm-demo-app/plugins/golem15/user/classes/api_tokens.go
|
||||
- ../sm-demo-app/plugins/golem15/user/controllers/api_controller.go
|
||||
- ../sm-demo-app/plugins/golem15/user/controllers/users_admin_controller.go
|
||||
- ../sm-demo-app/plugins/golem15/user/controllers/api_tokens.go
|
||||
- ../sm-demo-app/plugins/golem15/user/console/api_tokens.go
|
||||
- ../sm-demo-app/plugins/golem15/user/plugin.go
|
||||
- ../sm-demo-app/plugins/golem15/user/models/organisation.go
|
||||
- ../sm-demo-app/plugins/golem15/user/models/organisation/fields.yaml
|
||||
- ../sm-demo-app/plugins/golem15/user/lang/en/lang.yaml
|
||||
- ../sm-demo-app/plugins/golem15/user/lang/pl/lang.yaml
|
||||
- ../sm-demo-app/plugins/golem15/user/README.md
|
||||
- ../sm-oauthserver-plugin/classes/mcp_verifier.go
|
||||
- ../sm-oauthserver-plugin/classes/mcp_store.go
|
||||
- ../sm-oauthserver-plugin/classes/cascade.go
|
||||
- ../sm-oauthserver-plugin/classes/subject_events.go
|
||||
- ../sm-oauthserver-plugin/controllers/authorizations.go
|
||||
- ../sm-oauthserver-plugin/plugin.go
|
||||
- ../sm-oauthserver-plugin/README.md
|
||||
- ../sm-journal-plugin/mcp.go
|
||||
- ../sm-journal-plugin/README.md
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/link-admin.vue
|
||||
- ../sm-demo-app/vue-demo-app/i18n/locales/en.json
|
||||
- ../sm-demo-app/vue-demo-app/i18n/locales/pl.json
|
||||
- ../sm-demo-app/deploy/nginx/demo.summercms.io.conf
|
||||
- ../sm-demo-app/scripts/check-deploy.sh
|
||||
- ../sm-demo-app/mcp_e2e_test.go
|
||||
|
||||
key-decisions:
|
||||
- "P-34: the account link confirm reads auth_time (VerifyAuthTime); a token without it answers 409 stale_session; the code travels as #code= and through POST /backend-link/show; GET show kept, deprecated"
|
||||
- "P-35: AccessRevokedEvent + FireAccessRevoked from ban (newly banned only), password reset, admin-set password and self-service change; Before is the untruncated instant (self change: iat second); verifier and MCP refresh refuse banned users and created_at < tokens_valid_after + 1s"
|
||||
- "P-35 / user decision 2026-10-23: OnAccessRevoked ends MCP and OIDC grants alike (tokens revoked, consents and unused codes deleted, subjects kept)"
|
||||
- "P-36: backend-scoped MCP grants are revoked on every link and unlink, with a client-specific revoked event per affected client"
|
||||
- "P-37: member token scopes are validated per application (MintAPITokenFor, POST /tokens); MintAPIToken keeps a process-wide fallback"
|
||||
- "P-38: organisation logo is a second public attachOne; the plugin ships OrganisationLogoURL / OrganisationLogoFields instead of a payload it does not serve"
|
||||
- "MCP refresh refusals sit in ByTokenHashForUpdate only (the rotation lookup), not in ByAPITokenIDForUpdate, so wristband's Revoke still finds and kills a banned user's lineage"
|
||||
|
||||
requirements-completed: [SC-4, D-03, D-05, D-07, D-08, D-09, D-19, D-20, D-21, D-26, D-27, D-28, E-1, E-2]
|
||||
|
||||
duration: 41min
|
||||
completed: 2026-10-23
|
||||
---
|
||||
|
||||
# Phase 19.7 Plan 08: Plugin gap closure (lane C, E-1, E-2) Summary
|
||||
|
||||
Lane C of the code review and both sm-servers-app escalations are closed in the plugins and the demo, additively. The account link is proven by the session's `auth_time`, so a refreshed old session no longer passes, and the link code travels only in a URL fragment and a POST body. A ban, a password reset or any password change now ends the user's MCP and OIDC grants and their MCP sessions. The MCP verifier and refresh grant refuse banned users and tokens from before the cutoff. A `backend` grant cannot outlive or be re-targeted by a link change. Plugins can declare member token scopes, and organisations have a logo.
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** about 41 min
|
||||
- **Started:** 2026-10-23T14:30Z (ledger recorded 14:38:55Z)
|
||||
- **Completed:** 2026-10-23T15:11Z
|
||||
- **Tasks:** 4 of 4
|
||||
- **Files modified:** 47 across five repositories (7 created)
|
||||
|
||||
## Commits and findings
|
||||
|
||||
The framework repository has no code commit in this plan; only this SUMMARY is committed there. The `commits: 0` in the frontmatter is the framework ledger count. `commits_by_repo` holds the counts for the other repositories.
|
||||
|
||||
**sm-user-plugin** (`../sm-demo-app/plugins/golem15/user`, master, base 72de822)
|
||||
|
||||
| Commit | Task | Findings |
|
||||
|--------|------|----------|
|
||||
| 32f9259 `fix: account link proof by auth_time, code in the URL fragment and POST show` | 1 (tracer) | C-CR-02 (plugin side), C-WR-01, C-IN-06 |
|
||||
| 970719c `fix: access-revoked event on ban and password changes, banned users lose their link, token-specific MCP revoke` | 2 | C-CR-01 (user side), C-WR-02 (events confirmed), C-IN-02 (token), C-IN-04, C-IN-05 |
|
||||
| ee5c096 `feat: plugin-declared member token scopes (pact.HasFrontendTokenScopes)` | 3 | E-2 |
|
||||
| f20a8d3 `feat: organisation logo (attachOne logo) and payload helpers` | 4 | E-1 |
|
||||
|
||||
**sm-oauthserver-plugin** (master, base 2929c91)
|
||||
|
||||
| Commit | Task | Findings |
|
||||
|--------|------|----------|
|
||||
| 671513d `fix: revoke OAuth grants on ban and password changes, refuse banned and pre-cutoff MCP tokens, backend grants follow the link` | 2 | C-CR-01, C-WR-02, C-IN-02 (client), C-IN-05 |
|
||||
|
||||
**sm-journal-plugin** (main, base a9a9b80)
|
||||
|
||||
| Commit | Task | Findings |
|
||||
|--------|------|----------|
|
||||
| b129902 `fix: draft_post prompt is offered only to grants that may write` | 4 | C-IN-01 |
|
||||
|
||||
**vue-demo-app** (master, base 21a22b6)
|
||||
|
||||
| Commit | Task | Findings |
|
||||
|--------|------|----------|
|
||||
| 0125e29 `fix: link page reads the code from the fragment, shows the request by POST and the signed-in account` | 1 | C-WR-01, C-CR-02 (signed-in account shown) |
|
||||
|
||||
**sm-demo-app** (master, base 6fa2d56)
|
||||
|
||||
| Commit | Task | Findings |
|
||||
|--------|------|----------|
|
||||
| 00f57bf `chore: bump vue-demo-app (link page: code in the fragment, POST show)` | 1 | vue-demo-app pointer only |
|
||||
| 3b569a1 `fix: nginx accepts MCP bodies up to pier's 4 MiB cap` | 4 | C-IN-03 |
|
||||
| 7731641 `test: TestMCPEndToEnd expects a link change to end the backend grant` | 4 | C-WR-02 (e2e, deviation 3) |
|
||||
|
||||
Nothing was tagged, pushed or deployed.
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- **Account link proof (Task 1, tracer).**
|
||||
- `BackendLinkConfirm` reads `bouncer.VerifyAuthTime(SessionToken, secret, AudienceUser)`. A subject mismatch or a verify error answers 401. A token without `auth_time` answers the existing 409 `stale_session`, and so does an `auth_time` before the request. `ConfirmLinkRequest` keeps its signature; the parameter is now `sessionAuthTime`.
|
||||
- `BackendLinkShowPost` serves `POST /_user/api/v1/backend-link/show` with `{"code"}` and answers exactly like the GET route. Both show routes send `Cache-Control: no-store, private` (`writeNoStoreJSON`), and the README marks the GET route deprecated.
|
||||
- My account answers `FrontendURL + BackendLinkPagePath + "#code=" + code`. The new key `golem15.user.backend_links.page_path` defaults to `/link-admin` and gets a leading `/` when it lacks one.
|
||||
- vue-demo-app `/link-admin` reads `#code=` with `readLinkCode`, stores the code under `LINK_CODE_KEY` in sessionStorage and removes the fragment with `history.replaceState`. It shows the request through POST, shows "Signed in as …" with a "Not you? Sign out" action, and signs in through `/login?next=/link-admin`, so the code never appears in a URL. The stored code is removed on success and when the request is gone. The `signInAgain` text is reworded in en and pl.
|
||||
- The tracer gate re-ran the verify end to end (3 PASS, vue tests green) before expansion.
|
||||
- **Revocation chain (Task 2).**
|
||||
- `AccessRevokedEvent` (`user.AccessRevoked`) and `FireAccessRevoked` fire the event in the transaction, then one `revoked` subject event per user after the commit. The reasons are `ban`, `password_reset`, `password_set_by_admin` and `password_changed`, exported as `AccessRevoked*` constants.
|
||||
- Callers:
|
||||
- the `ban` bulk action, through the `banUsers` wrapper (ThrottleStates before and after, newly banned ids only);
|
||||
- `ResetPassword`, now inside `lagoon.Transaction`, with the untruncated `now`;
|
||||
- `ChangePassword`, also inside a transaction, with `cutoff + 1s`;
|
||||
- the admin password store in `FormBeforeUpdate`, using the save's tx and the untruncated instant.
|
||||
- `LinkedBackendUser` excludes banned users (`user_throttle.is_banned`).
|
||||
- `FireSubjectEvent` and `FireSubjectEventAfterCommit` are new; the old helpers wrap them. `DELETE /tokens/{id}` fires with `TokenID`.
|
||||
- `MCPResolvePersonalToken` stamps `last_used_ip` from `towel.ClientIP`.
|
||||
- `requestUsable` uses `IsUsableAt`. `BackendLinksFor` replaces the two duplicate lookups.
|
||||
- oauthserver:
|
||||
- `issuedBeforeCutoff` is used by the verifier and by the MCP refresh lookup. Both also refuse banned users.
|
||||
- `revokeUserGrants` is the one revocation function.
|
||||
- `OnAccessRevoked` covers the MCP and OIDC kinds. `OnBackendLinked`, `OnBackendUnlinked` and `OnBackendLinkChanged` revoke only `backend`-scoped MCP tokens and fire a revoked event per client.
|
||||
- The authorized-apps revoke fires with `ClientID`.
|
||||
- The subject-event helpers delegate to the user plugin.
|
||||
- **Member token scopes (Task 3, E-2).**
|
||||
- The scope types are `APITokenScope` and `APITokenScopeCatalog` (`Scopes`, `Allowed`).
|
||||
- `CompileAPITokenScopes` refuses a bad name, a built-in, a duplicate (the error names both plugins) and an empty label or description.
|
||||
- Boot (`bootAPITokenScopes`) also checks `::` phrase keys against the translator, publishes the catalog and fails on any error.
|
||||
- `APITokenScopesFor` returns the published catalog, or the built-ins alone when none is published.
|
||||
- `MintAPITokenFor` and `MintAPIToken` share `mintAPIToken`. The legacy `MintAPIToken` falls back to the process-wide declared set, which `ResetDeclaredAPITokenScopes` clears for tests.
|
||||
- `POST /tokens` and `user:token:create` use `MintAPITokenFor`, and the 422 text is unchanged.
|
||||
- `GET /_user/api/v1/tokens/scopes` (behind `jwt.auth`) lists the scopes with translated labels.
|
||||
- The built-in label and description keys are added in en and pl, and the README has a "Member token scopes" section.
|
||||
- `AllowedAPITokenScopes` keeps its exported form.
|
||||
- **Logo, Journal, nginx, demo (Task 4).**
|
||||
- `OrganisationLogoField = "logo"` is the second public attachOne, after the avatar.
|
||||
- The Organisations form gains a `logo` image fileupload (240×120).
|
||||
- `OrganisationLogoURL`, `OrganisationLogoFields` and `OrganisationLogoSize` (256) are new. No migration was added (`72de822..HEAD -- updates/` is empty).
|
||||
- Journal's `draft_post` now has `Writes: true`.
|
||||
- nginx `location = /mcp` gets `client_max_body_size 4m;`, and check-deploy.sh enforces it. A deliberately removed directive made the check fail as intended; the file was then restored.
|
||||
|
||||
## Delivered differently: E-1 (for the sm-servers-app hand-off)
|
||||
|
||||
The escalation asked that "the frontend organisation payloads gain the logo". sm-user-plugin serves no frontend organisation payload: no route answers an organisation object. The logo is therefore delivered as two exported helpers that applications call when they build their own organisation payloads:
|
||||
|
||||
- `classes.OrganisationLogoFields(ctx, app, org)` answers `has_logo` (bool), `logo` (the attachment's disk name, or nil) and `logo_url` (a 256 px thumbnail URL, or nil). These are the user payload's avatar keys with "avatar" replaced by "logo". An application merges the map into its organisation JSON.
|
||||
- `classes.OrganisationLogoURL(ctx, app, org, size)` answers the URL at any size, and false when there is no logo or no file bucket.
|
||||
|
||||
The logo is uploaded on the backend Organisations form (field `logo`) and stored in `system_files` with `field = 'logo'`.
|
||||
|
||||
## OIDC decision (user, 2026-10-23) and the rows it touches
|
||||
|
||||
A ban, a password reset, an admin-set password or the user's own password change ends every OAuth grant of the user, MCP agents and OIDC sign-ins alike. `Cascade.OnAccessRevoked` calls `revokeUserGrants(ctx, ev.Tx, ev.UserIDs, ev.Before, [mcp, oidc], "")`, which touches these rows of the users at clients of kind `mcp` or `oidc`:
|
||||
|
||||
| Table | Rows | Change |
|
||||
|-------|------|--------|
|
||||
| `golem15_oauthserver_access_tokens` | unrevoked, `created_at < Before` | `revoked_at`, `updated_at` set |
|
||||
| `golem15_oauthserver_refresh_tokens` | unrevoked, `created_at < Before` | `revoked_at`, `updated_at` set |
|
||||
| `golem15_oauthserver_consents` | `created_at` and `updated_at` before `Before` (or NULL) | deleted, so an OIDC client asks for consent again |
|
||||
| `golem15_oauthserver_auth_codes` | `consumed_at IS NULL`, `created_at < Before` | deleted (a pending OIDC code cannot mint a token afterwards) |
|
||||
| `golem15_oauthserver_mcp_codes` | `used_at IS NULL`, `created_at < Before` | deleted |
|
||||
| `golem15_oauthserver_subjects` | — | kept (a restored or unbanned user keeps the same `sub`) |
|
||||
|
||||
Link changes (C-WR-02) call the same function with kind `mcp` and scope `backend`, and only revoke tokens. `TestMCPRevocationSmoke` checks the OIDC rows after a ban: no unrevoked access token, no consent, no unused code, and the subject kept.
|
||||
|
||||
## Local environment (not committed)
|
||||
|
||||
- **sm-oauthserver-plugin go.work.** The `sm-user-plugin` replace previously pointed at `../fonoteka.go/plugins/golem15/user` and now points at `../sm-demo-app/plugins/golem15/user`. See deviation 1: go.work is tracked in this repository, so it shows as modified and was deliberately left uncommitted.
|
||||
- **sm-demo-app submodule checkouts (detached, working tree only):**
|
||||
- `plugins/golem15/oauthserver` at 671513d1fccb2defce8fbce517bde17619253dbb (committed pointer 8bbec72)
|
||||
- `plugins/golem15/journal` at b1299021cd4031bf5430adcdbb1b139a8d1c3eb0 (committed pointer 4ab94da)
|
||||
- `plugins/golem15/user` at f20a8d3b98d3bafa5a58bcacc54ebbd427affa45 (committed pointer at v0.1.10). This is the plugin checkout itself, and the pointer was already shown as modified before this plan.
|
||||
- **Deferred pointer bumps:** commit the oauthserver, journal and user pointers in sm-demo-app after the plugin tags (release order: framework v0.5.0, then the plugin tags, then the application bumps). vue-demo-app 0125e29 must be pushed before sm-demo-app 00f57bf.
|
||||
|
||||
## Verification
|
||||
|
||||
- User plugin:
|
||||
- `GOWORK=off go vet ./...` clean.
|
||||
- `GOWORK=off go test -count=1 ./...` green with Docker (root 58 s, classes, models, updates), run after the last change.
|
||||
- `TestBackendLinkAuthTimeSmoke`, `TestBackendLink`, `TestBackendLinkSmoke`, `TestMemberTokenScopesSmoke` (both subtests), `TestUserMCPGuard` and `TestUserMCPGuardSmoke` PASS.
|
||||
- oauthserver:
|
||||
- `go vet ./...` clean.
|
||||
- `go test -count=1 ./...` green with Docker.
|
||||
- `TestMCPRevocationSmoke`, `TestMCPTokenVerifier`, `TestMCPConsent`, `TestMCPConsentSmoke`, `TestMCPTokenVerifierSmoke`, `TestMCPStore` and `TestMCPStoreSmoke` PASS.
|
||||
- Journal: `go vet ./...` and `go test -count=1 -short ./...` green (`TestJournalMCP` and `TestJournalMCPSmoke` included).
|
||||
- Demo:
|
||||
- `bash scripts/check-deploy.sh` prints ok.
|
||||
- `go build -tags summercms_mcp ./...` and `go test -count=1 -short ./...` are green.
|
||||
- `go test -count=1 -tags summercms_mcp -run '^TestMCPEndToEnd$' .` PASS (10 subtests) on the detached plugin checkouts.
|
||||
- vue-demo-app: `pnpm run test` (14 files, 178 tests) and `pnpm run build` are green.
|
||||
- `bash scripts/check-phase19.7.sh --plugins` PASS. It prints `additive:` lines:
|
||||
- user: nothing removed since 930b158 or v0.1.9;
|
||||
- oauthserver: nothing removed since d31f8e7;
|
||||
- journal: nothing removed since ffa2c8f.
|
||||
- fonoteka.go:
|
||||
- builds against this plan's user plugin through the scratch GOWORK of Task 3's verify;
|
||||
- `go build ./...` also passes against its own checkout;
|
||||
- its go.work is untouched (status count 0).
|
||||
- sm-bm-app: `go build ./...` passes.
|
||||
- go.mod and go.sum are unchanged in all three plugins (no `go get`), and pnpm-lock.yaml is unchanged.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] sm-oauthserver-plugin's go.work is tracked, not git-ignored**
|
||||
- **Found during:** Task 2, step 0
|
||||
- **Issue:** P-39 assumed a git-ignored go.work. It is tracked (`git ls-files go.work`, committed in 459e389), so retargeting it leaves ` M go.work`, and the acceptance line "status --short go.work prints 0" cannot hold while the plan's own verify (`go -C ../sm-oauthserver-plugin vet ./...`) needs the retarget.
|
||||
- **Fix:** I retargeted go.work in the working tree and did not commit it, as the plan says ("never commit go.work"). The previous target is recorded above. Before the release, either restore the line or decide whether go.work should leave version control.
|
||||
- **Files:** ../sm-oauthserver-plugin/go.work (uncommitted)
|
||||
|
||||
**2. [Rule 1 - Bug] Show routes could not set no-store through writeJSON**
|
||||
- **Found during:** Task 1
|
||||
- **Issue:** `writeJSON` always overwrites `Cache-Control` with `no-cache, private`.
|
||||
- **Fix:** the new `writeNoStoreJSON` serves both show routes, including their 404s.
|
||||
- **Commit:** 32f9259
|
||||
|
||||
**3. [Rule 1 - Test pinned old behaviour] Demo TestMCPEndToEnd expected a backend grant to survive an unlink**
|
||||
- **Found during:** Task 4 (end-to-end run on the detached checkouts)
|
||||
- **Issue:** two subtests ("journal tools as the linked admin" ending, and "unlink sends list_changed") expected the backend session to keep running with a shorter tool list after an unlink. C-WR-02 (P-36) revokes that grant by design.
|
||||
- **Fix:**
|
||||
- The first subtest now checks three things after the unlink: the backend session ends, the backend token answers 401, and a grant without `backend` still lists `user_me` and no Journal tool.
|
||||
- The second subtest is now "a link change ends the backend grant": the old token stays 401 after a relink to another admin, and a new grant reaches the new admin's tools.
|
||||
- This test file was not in the plan's file list. It passes only with the plugin commits (the detached checkouts) until the pointer bumps.
|
||||
- **Commit:** 7731641 (sm-demo-app)
|
||||
|
||||
**4. [Rule 1 - Correctness] MCP refresh refusals only in the rotation lookup**
|
||||
- **Found during:** Task 2
|
||||
- **Issue:** the plan put the refusals in `mcpTx.refresh`, which wristband's `Revoke` (connected-app revocation) also uses through `ByAPITokenIDForUpdate`. A "not found" there would skip killing a banned user's lineage.
|
||||
- **Fix:** `refresh` is split into `refreshRow` and `refreshRecord`. Only `ByTokenHashForUpdate` (the refresh grant) applies `refreshUserUsable`.
|
||||
- **Commit:** 671513d
|
||||
|
||||
**5. [Rule 2 - Missing critical] Full revocation also deletes unused authorization codes**
|
||||
- **Found during:** Task 2
|
||||
- **Issue:** an OIDC or MCP code issued before a ban or reset could still be exchanged for a new token after it.
|
||||
- **Fix:** `revokeUserGrants` (full mode) deletes unconsumed `auth_codes` and unused `mcp_codes` created before `Before`. Recorded in the OIDC table above.
|
||||
- **Commit:** 671513d
|
||||
|
||||
**6. [Plan detail] revokeUserGrants signature**
|
||||
- It takes a `scope` argument (empty for all scopes) and returns the touched `(user, client)` pairs (`[]grantRef`), so the link handlers can share the one function, as the plan's artifact list describes. It is unexported.
|
||||
|
||||
**7. [Rule 1] Built-in scope phrase keys spelled out**
|
||||
- `TestAdminRegistration` collects phrase-key string literals from the sources. A concatenated prefix (`…api_tokens.scope_`) failed it, so the six keys are literal (`builtInAPITokenScopeKeys`).
|
||||
- **Commit:** ee5c096
|
||||
|
||||
**8. [Scope] Additional exports**
|
||||
- `BackendLinkPagePath`, `DefaultBackendLinkPagePath`, the `AccessRevoked*` reason constants and `OrganisationLogoSize` were added (additive) and are documented in the README.
|
||||
- Also documented in the README:
|
||||
- `BackendLinkShowPost` was missing from the controllers line in 32f9259 and was added in 970719c;
|
||||
- `TestBackendLinkAuthTimeSmoke` in the Testing section.
|
||||
|
||||
### Tests changed for old behaviour
|
||||
|
||||
- `backend_link_test.go` `TestBackendLink` "fresh sign-in at the second" now crafts `auth_time` tokens and adds a no-`auth_time` case that expects `stale_session` (it pinned the iat confirm). Commit 32f9259.
|
||||
- `backend_link_smoke_test.go`: the link URL regexp is `…/link-admin#code=…`. Commit 32f9259.
|
||||
- `password_routes_test.go` `TestPasswordPublicRoutes`: route count 21 → 22 (POST show, 32f9259), then 23 (GET /tokens/scopes, ee5c096).
|
||||
- `mcp_guard_smoke_test.go` `TestUserMCPGuardSmoke`: the token revoke now expects `TokenID` (new `subjectSpy.hasEvent`). Commit 970719c.
|
||||
- `models/admin_models_test.go` `TestAdminModelContracts` expects 2 organisation attach relations, avatar then logo. `admin_organisations_test.go` `TestAdminOrganisations` expects the `logo` field. Commit f20a8d3.
|
||||
- oauthserver `mcp_consent_smoke_test.go` `TestMCPConsentSmoke`: the authorized-apps revoke event carries `ClientID`. Commit 671513d.
|
||||
- sm-demo-app `mcp_e2e_test.go` `TestMCPEndToEnd` (deviation 3). Commit 7731641.
|
||||
|
||||
## Notes for plan 09
|
||||
|
||||
- Named tests: `TestMCPVerifierRevocation`, `TestMCPGrantRevocation`, `TestAccessRevocation` (threat register T-19.7-57). Unit tests for `linkCode.ts` (`readLinkCode`, `isLinkCode`). `OrganisationLogoFields` with and without a logo.
|
||||
- `FireAccessRevoked` is not called by `UnbanUsers` or by the `user:token:revoke` CLI; neither needs it. The `admin:token:revoke` CLI still fires no MCP event (from plan 07).
|
||||
- `connectWatching` in the demo's mcp_e2e_test.go is now unused.
|
||||
- Pre-existing and out of scope: `gofmt -l` lists sm-journal-plugin `classes/blog_editor_test.go`.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None beyond the plan's register. The new surfaces are covered there:
|
||||
- `POST /backend-link/show` (T-19.7-58);
|
||||
- `GET /tokens/scopes`, which needs `jwt.auth` and lists only scope metadata (T-19.7-60);
|
||||
- the organisation logo upload (T-19.7-61).
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: classes/access_revoked.go, classes/api_token_scopes.go, classes/organisation_logo.go, backend_link_auth_time_smoke_test.go, member_token_scopes_smoke_test.go (user); mcp_revocation_smoke_test.go (oauthserver); app/lib/linkCode.ts (vue-demo-app)
|
||||
- FOUND: 32f9259, 970719c, ee5c096, f20a8d3 (user); 671513d (oauthserver); b129902 (journal); 0125e29 (vue-demo-app); 00f57bf, 3b569a1, 7731641 (sm-demo-app)
|
||||
- No commit deleted a tracked file.
|
||||
Reference in New Issue
Block a user