Skip to content

Commit c396216

Browse files
DavidBabinecclaude
andcommitted
docs(auth): update capabilities source-of-truth references after @core/capabilities refactor
- docs/reference/capabilities.md: fix TL;DR (TypeBox union → const array, server/auth → src/core), fix SITE_WRITE_CAPABILITIES location (now defined locally at point-of-use, not in capabilities.ts), update "Adding a capability" steps to reflect single source of truth - docs/features/auth-and-access.md: fix TL;DR and Capabilities section source path; update "Where the code lives" tree to show src/core/capabilities.ts and correct server/auth/capabilities.ts description; fix Roles table (Admin is force-resynced, not "Editable"); fix "Add a new capability" cookbook (remove defunct CoreCapabilitySchema, point to correct file, add picker-coverage step); update Related source-of-truth entries - docs/server.md: fix one-liner pointing to old server/auth/capabilities.ts location and wrong doc link (should be docs/reference/capabilities.md) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0f5a75f commit c396216

3 files changed

Lines changed: 31 additions & 27 deletions

File tree

docs/features/auth-and-access.md

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Every state-changing CMS request goes through one auth funnel: parse the session
99
## TL;DR
1010

1111
- **Sessions** are token-cookie based. Cookie name: `SESSION_COOKIE_NAME` (`instatic_admin_session`). Tokens are hashed before storage; the cookie carries the raw token.
12-
- **Capabilities** are the access model. 36 `CoreCapability` strings defined in `server/auth/capabilities.ts`. Roles are sets of capabilities. Handlers gate on capability, not role.
12+
- **Capabilities** are the access model. 36 `CoreCapability` strings defined in `src/core/capabilities.ts` (`@core/capabilities`). Roles are sets of capabilities. Handlers gate on capability, not role.
1313
- **`requireCapability(req, db, 'site.read')`** is the canonical handler entrypoint. Returns the `AuthUser` or a 401/403 `Response`.
1414
- **MFA (TOTP)** is per-user opt-in. Sessions for MFA-enrolled users are `pending_mfa` until verified, then become `active`. Failed MFA codes go through `mfaRateLimit` AND increment the per-account lockout counter — the same counter the password step uses. A locked account is rejected at the MFA step before any code is checked.
1515
- **Step-up auth** gates sensitive actions (delete user, revoke another device, sign out all) unless the user disables it on Account -> Security. The default window is 15 minutes; users can configure 5, 15, 30, or 60 minutes.
@@ -22,9 +22,11 @@ Every state-changing CMS request goes through one auth funnel: parse the session
2222
## Where the code lives
2323

2424
```text
25+
src/core/capabilities.ts — CORE_CAPABILITIES (the canonical list), CoreCapability type (@core/capabilities)
26+
2527
server/auth/
2628
├── authz.ts — requireAuthenticatedUser, requireCapability, requireAnyCapability, requireStepUp
27-
├── capabilities.ts — CoreCapability enum, SYSTEM_ROLES, SITE_WRITE_CAPABILITIES
29+
├── capabilities.ts — imports/re-exports CORE_CAPABILITIES; owns SYSTEM_ROLES, FORCE_SYNC_ROLE_IDS, runtime guards
2830
├── sessions.ts — createSession, findUserBySessionHash, rotateSessionToken, MFA gates, step-up timer
2931
├── stepUpPolicy.ts — step-up modes and allowed window lengths
3032
├── tokens.ts — SESSION_COOKIE_NAME, hashSessionToken
@@ -114,24 +116,25 @@ Users can list active sessions and revoke them individually. `revokeOtherSession
114116

115117
## Capabilities
116118

117-
36 core capabilities, defined as a closed TypeBox literal union in `server/auth/capabilities.ts`:
119+
36 core capabilities. The canonical list is in `src/core/capabilities.ts` (`@core/capabilities`) as an `as const` array; `CoreCapability` is derived from it via `typeof CORE_CAPABILITIES[number]`:
118120

119121
```ts
120-
type CoreCapability =
121-
| 'dashboard.read'
122-
| 'site.read'
123-
| 'site.structure.edit' | 'site.content.edit' | 'site.style.edit'
124-
| 'pages.edit' | 'pages.publish'
125-
| 'content.create' | 'content.edit.own' | 'content.edit.any'
126-
| 'content.publish.own' | 'content.publish.any'
127-
| 'content.manage'
128-
| 'media.read' | 'media.write' | 'media.replace' | 'media.delete'
129-
| 'runtime.dependencies' | 'storage.elect' | 'storage.migrate'
130-
| 'plugins.read' | 'plugins.configure' | 'plugins.install' | 'plugins.lifecycle'
131-
| 'users.manage' | 'roles.manage'
132-
| 'audit.read'
133-
| 'data.tables.read' | 'data.tables.manage' | 'data.rows.move' | 'data.export' | 'data.import'
134-
| 'ai.chat' | 'ai.tools.write' | 'ai.providers.manage' | 'ai.audit.read'
122+
// src/core/capabilities.ts — source of truth
123+
export const CORE_CAPABILITIES = [
124+
'dashboard.read', 'site.read',
125+
'site.structure.edit', 'site.content.edit', 'site.style.edit',
126+
'pages.edit', 'pages.publish',
127+
'content.create', 'content.edit.own', 'content.edit.any',
128+
'content.publish.own', 'content.publish.any', 'content.manage',
129+
'media.read', 'media.write', 'media.replace', 'media.delete',
130+
'runtime.dependencies', 'storage.elect', 'storage.migrate',
131+
'plugins.read', 'plugins.configure', 'plugins.install', 'plugins.lifecycle',
132+
'users.manage', 'roles.manage', 'audit.read',
133+
'data.tables.read', 'data.tables.manage', 'data.rows.move', 'data.export', 'data.import',
134+
'ai.chat', 'ai.tools.write', 'ai.providers.manage', 'ai.audit.read',
135+
] as const
136+
137+
export type CoreCapability = typeof CORE_CAPABILITIES[number]
135138
```
136139
137140
### Site-editing split
@@ -170,7 +173,7 @@ Four system roles, defined in `SYSTEM_ROLES`:
170173
| Role | id | Capabilities | Special |
171174
|---------|-----------|------------------------------------------------------------------------------|-------------|
172175
| Owner | `owner` | All `CORE_CAPABILITIES` | Owner-only `roles.manage`. Resyncs on every boot via `syncSystemRoles(db)`. |
173-
| Admin | `admin` | All except `roles.manage` | Editable |
176+
| Admin | `admin` | All except `roles.manage` | Force-resynced on every boot. Hand-edits restored at next boot. |
174177
| Client | `client` | `dashboard.read`, `site.read`, `site.content.edit`, `media.read`, `data.tables.read` | Editable |
175178
| Member | `member` | (none) | Editable |
176179
@@ -399,10 +402,10 @@ export async function handleSubscribersRoutes(req: Request, db: DbClient): Promi
399402

400403
### Add a new capability
401404

402-
1. Add the literal to `CoreCapabilitySchema` and `CORE_CAPABILITIES` in `server/auth/capabilities.ts`.
403-
2. If it belongs to an existing role surface (admin / client), add it to the matching `*Capabilities` array.
405+
1. Append the literal to `CORE_CAPABILITIES` in `src/core/capabilities.ts` (`@core/capabilities`). The `CoreCapability` type updates automatically; the server picks it up via import.
406+
2. If it belongs to the Owner / Admin / Client default sets, add it to the matching entry in `SYSTEM_ROLES` inside `server/auth/capabilities.ts`. Owner + Admin force-sync on next boot.
404407
3. Use `requireCapability(req, db, 'your.new.capability')` in the handler that needs it.
405-
4. Owner and Admin roles auto-sync on next boot — no migration needed for existing Owner or Admin accounts.
408+
4. Add a `CAPABILITY_META` entry + `CAPABILITY_GROUPS` section in `src/admin/pages/users/utils/capabilities.ts` so the role-edit dialog renders a checkbox. The `capability-picker-coverage.test.ts` gate fails until you do.
406409
5. Existing custom roles will NOT have the new capability until users grant it through the Roles admin page.
407410

408411
### Gate a sensitive action
@@ -462,7 +465,8 @@ if (userHasAnyCapability(user, SITE_WRITE_CAPABILITIES)) { /* … */ }
462465
- [docs/reference/capabilities.md](../reference/capabilities.md) — full capability matrix + when to add a new one
463466
- Source-of-truth files:
464467
- `server/auth/authz.ts``requireCapability`, `requireAnyCapability`, `requireStepUp`
465-
- `server/auth/capabilities.ts``CoreCapability`, `SYSTEM_ROLES`, `SITE_WRITE_CAPABILITIES`
468+
- `src/core/capabilities.ts` (`@core/capabilities`) — `CORE_CAPABILITIES`, `CoreCapability` (single source of truth)
469+
- `server/auth/capabilities.ts` — imports/re-exports `CORE_CAPABILITIES`; owns `SYSTEM_ROLES`, `FORCE_SYNC_ROLE_IDS`, runtime guards
466470
- `server/auth/sessions.ts` — session lifecycle, MFA gates, step-up timer
467471
- `server/auth/tokens.ts``SESSION_COOKIE_NAME`, `hashSessionToken`
468472
- `server/auth/mfa.ts` — TOTP + recovery codes

docs/reference/capabilities.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ For the broader auth flow (sessions, MFA, step-up), see [docs/features/auth-and-
88

99
## TL;DR
1010

11-
- Defined as a closed TypeBox literal union in `server/auth/capabilities.ts`. **36 capabilities.**
11+
- Defined as a `const` array in `src/core/capabilities.ts` (`@core/capabilities`); `CoreCapability` is derived via `typeof CORE_CAPABILITIES[number]`. **36 capabilities.**
1212
- Handlers gate on capability, not on role: `requireCapability(req, db, 'site.read')`.
1313
- The **Owner AND Admin** roles get their capability lists force-resynced from `SYSTEM_ROLES` on every server boot. Hand-edits to either built-in role through the admin UI are restored at next boot — they are code-level decisions, not runtime ones.
14-
- Adding a capability: append the literal in three places (server schema, server array, client array), then add it to the SYSTEM_ROLES entries it should belong to, wire `requireCapability(...)` at the gate point, and add picker meta + groups for the role-edit dialog. The two architecture tests (`capability-picker-coverage.test.ts`, `cms-handlers-capability-gated.test.ts`) catch missing pieces.
14+
- Adding a capability: append the literal to `CORE_CAPABILITIES` in `src/core/capabilities.ts` (one place — server imports it), add it to the relevant `SYSTEM_ROLES` entries, wire `requireCapability(...)` at the gate point, and add picker meta + groups for the role-edit dialog. The two architecture tests (`capability-picker-coverage.test.ts`, `cms-handlers-capability-gated.test.ts`) catch missing pieces.
1515
- Custom roles editable in the Roles admin page (Owner-only `roles.manage`).
1616

1717
---
@@ -33,7 +33,7 @@ For the broader auth flow (sessions, MFA, step-up), see [docs/features/auth-and-
3333
| `site.content.edit` | Modify content props (text, image src/alt, link href) on existing nodes — no structure or style edits | Owner, Admin, Client |
3434
| `site.style.edit` | Modify CSS classes, style overrides, breakpoints, framework tokens | Owner, Admin |
3535

36-
`SITE_WRITE_CAPABILITIES` (in `capabilities.ts`) is the convenience set `['site.structure.edit', 'site.content.edit', 'site.style.edit']`used by the site shell save handler. The `/pages` and `/components` reconcile endpoints additionally require `site.structure.edit` because their wholesale reconcile can soft-delete entries.
36+
`SITE_WRITE_CAPABILITIES` is the convenience set `['site.structure.edit', 'site.content.edit', 'site.style.edit']`defined locally in `server/handlers/cms/site.ts` and `src/admin/access.ts` at each point of use, not in a shared capabilities module. Used by the site shell save handler. The `/pages` and `/components` reconcile endpoints additionally require `site.structure.edit` because their wholesale reconcile can soft-delete entries.
3737

3838
### Page publishing
3939

docs/server.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ if (user instanceof Response) return user // 401 or 403 already encoded
260260
// ... user is now AuthUser
261261
```
262262

263-
`requireCapability` and `requireAnyCapability` are the only auth surfaces a handler should call. Capabilities are strings like `site.read`, `site.structure.edit`, `media.write`, `plugins.install`, `users.manage`, etc. Owner accounts get all `CORE_CAPABILITIES` automatically. The full list is in `server/auth/capabilities.ts`; `docs/features/auth-and-access.md` catalogs every one.
263+
`requireCapability` and `requireAnyCapability` are the only auth surfaces a handler should call. Capabilities are strings like `site.read`, `site.structure.edit`, `media.write`, `plugins.install`, `users.manage`, etc. Owner accounts get all `CORE_CAPABILITIES` automatically. The full list is in `src/core/capabilities.ts` (`@core/capabilities`); `docs/reference/capabilities.md` catalogs every one.
264264

265265
### Step-up auth
266266

0 commit comments

Comments
 (0)