Fixes review finding CR-01 (quick 260927-q23): POST {prefix}/api/v1/auth/refresh
minted a new token without loading the admin, so a session kept alive by the
SPA's refresh-on-401 survived admin:reset-password, deactivation and deletion.
This broke Phase 9 truth T-09-04.
- bouncer: extract the JWT guard's subject lookup into subjectPrincipal and
issuedBeforeCutoff (same order and messages), add ErrSubjectRejected
- bouncer: add RefreshAudienceFor, which runs the guard's subject checks
after the token-only checks and before minting; Refresh and
RefreshAudience are unchanged (nil hook)
- cabana: share one lazyBackendUsers provider between the backend guard and
refresh; a cookie refresh refused for its subject expires summer_admin
- test: TestAdminRefreshRevocation (Postgres, real admin:reset-password)
- pact.FilterOptions on the model serves a scope filter's choices; a scope
filter whose model lacks it fails activation (D-27)
- GET /{vendor}/{plugin}/{controller}/filters/{scope}/options answers a
declared scope filter behind the controller permission with localized
{value, label} choices, 404 otherwise
- Every admin route documents a typed success schema, and protected routes
document 401, 403 and 404 (422 on writes); SuccessEnvelope is gone and
logout writes a typed AdminLogoutData
- jsonScalar and fieldContext decode their served shapes
- TestPhase10OpenAPIConformance calls every inventoried route through the
assembled router on PostgreSQL and decodes each body into its documented
type with unknown fields disallowed, checking admin.json's schema ref
- The SPA aliases every new schema type; Tailwind no longer scans the
generated API files, so API changes do not churn boardwalk/dist
- phrasebook ships the backend::lang admin strings (pl, en) with CLDR
plural maps, loads them as namespace backend, applies
pact.HasLangOverrides trees (lang/<locale>/<namespace>/<group>.yaml)
after every namespace, and fails activation when a backend key cannot
convert to plural forms
- Translator.Forms, Bundle, Resolved and Has serve keys as CLDR form maps
- Public GET /lang returns every backend::lang key for the request
locale over the fallback locale, Cache-Control no-cache
- config_list, config_form and config_relation accept a strict messages
block; omitted keys take framework defaults, schemas serve every message
as CLDR forms, and activation fails on a missing phrase key
- toolbar.buttons is an ordered [create, delete] list; the Winter string
form, duplicates, unknown actions and delete without showCheckboxes fail
at boot, and create is dropped when the controller has no form
- Form schema serves the raw Winter redirects; scaffold emits the list
syntax; form and relation schema routes are typed in the admin OpenAPI
- FieldRelationContract/FieldRelationProvider bind every type: relation
field to a belongsTo foreign key or a belongsToMany pivot; activation
fails naming plugin, controller and field on a missing or broken contract
- GET /{vendor}/{plugin}/{controller}/fields/{field}/options serves
{value, label} pages scoped by pact.RelationExtendOptionsQuery, behind
the controller permission; read-only and non-relation fields are 404
- Saves apply present relation keys after the Before hook: ids are
revalidated through the same scoped query (422 and full rollback
otherwise), belongsTo sets the foreign key, belongsToMany replaces pivot
rows in submitted order with the order column set to the index
- Show, create and update return relation values in data and meta.labels
- A belongsTo on a protected fill key is read-only (D-26)
- One six-segment GET pattern dispatches relation lists and field options,
which ServeMux cannot register side by side
- Admin OpenAPI documents the options route and RecordEnvelope
- refresh and logout read the Bearer header first, then the summer_admin
cookie; a cookie refresh rotates the cookie without a token in the body and
logout always expires the cookie
- backend.cookie_secure (default true) may drop Secure outside production only
- activation rejects controller vendor segments api, assets, login, settings
- BuildRouter rejects non-cabana routes at or under the admin prefix
- SPA single-flights refresh on 401, replays once, and refreshes proactively
at 80 percent of expires_in; dist rebuilt
- scripts/check-admin-dist.sh rebuilds the SPA and fails on dist drift
- tests: TestPhase10CookieAuth, TestPhase10CSRF, TestPhase10Prefix,
TestPhase10AdminPrefixCollision, boardwalk serving and header tests
- backend.uri prefix (default /backend) mounts the admin API at {prefix}/api/v1
and the embedded SPA shell at {prefix} with an api/ JSON 404 fallback
- cookie transport: an X-Requested-With login sets the HttpOnly summer_admin
cookie and returns no token; the backend guard reads the cookie after Bearer
- CSRF wrapper refuses cookie-only POST/PUT/DELETE without X-Requested-With
- boardwalk package embeds boardwalk/dist, rewrites index.html once per prefix
and sets cache and security headers
- framework admin OpenAPI pipeline (swag, swagger2openapi, openapi-typescript)
with prefix-relative paths and typed envelopes for the tracer routes
- admin/ Vite SPA: login, plugin rail, section panel and read-only list
through the openapi-fetch client typed by the generated schema
- GET schema/form localizes the compiled form after the permission check
- relation: genre resolves to the exported Go field without changing the YAML key
- Reject an empty selection and dedupe ids before locking rows in pk order
- Return deleted 0 when every requested row is already gone, without hooks
- Roll back mixed, hook, and cancelled batches so no partial delete commits
- Empty selections are 422 and duplicates run once in primary-key order
- A completed retry and an all-absent selection delete nothing and skip hooks
- Mixed, hook, cancel, and concurrent requests keep the batch atomic
- Mount show, create, update, and delete behind the backend permission check
- Run controller and model hooks once per operation and roll back on failure
- Treat missing and out-of-scope records the same, including idempotent delete
- Record routes must enforce permission before ids or bodies and return D-10 envelopes
- Create, update, and delete run Before and After hooks once inside the transaction
- Out-of-scope and missing records are indistinguishable, and hook failure rolls back
- Search, sort, filters, and pagination use compiled selectors and bound values
- Equal sort keys break ties on the primary key so adjacent pages do not overlap
- Unknown identifiers return validation_failed before SQL
- Typed columns, actions, default sort, search term, and page sizes
- Omitted sortable defaults to true and empty collections marshal as arrays
- Unknown keys, bad defaults, and path escape fail before routes are served
- Refresh, logout, and me use a separate PostgreSQL jti blacklist and safe profile
- Login stamps last_login only after a successful check and throttles repeated attempts
- Audience-aware mint, verify, refresh, and backend guard keep frontend tokens compatible
- Cabana mounts raw admin login, list schema, and record list behind admin.jwt.secret
- Framework migration seeds Winter backend users and developer/publisher roles