Skip to content

Commit db7b6f8

Browse files
committed
docs: sync permissions / plugins / protocol docs with implementation (#1880 Wave 1, batch 5)
Commits the 23 permissions/plugins/protocol docs whose audit completed cleanly this window. Highlights: - plugins/*: removed the fabricated `definePlugin()` helper from examples — a plugin is a plain default-export object; no such helper exists. - permissions/explain: corrected the explain `via` value to "direct grant" (adversarial-verify repair); permission-metadata, administrator-guide, authentication, field-level-security, sharing-rules: scope/flag/API fixes. - protocol/diagram, protocol/kernel/config-resolution: path/reference fixes. Some docs' verify pass was cut short by a weekly usage limit, so a subset is audit-only (hand-scanned: no define*/scale/count regressions; MDX build-safe). Two docs whose audit itself was interrupted (protocol/kernel/error-handling and http-protocol) are reverted and deferred for a clean re-audit after the limit resets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012urEihGTAsQP2xizAqfCwt
1 parent b809ec0 commit db7b6f8

24 files changed

Lines changed: 173 additions & 155 deletions

content/docs/kernel/runtime-services/audit-service.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ services.audit.record(entry: {
3333

3434
## Typical Errors
3535

36-
Implementations should avoid throwing into user paths. Callers should treat audit writes as best-effort unless strict compliance mode is enabled by host code.
36+
Sink implementations should avoid throwing into user paths: the runtime caller (for example `service-settings`) awaits `record()` without a surrounding `try`/`catch`, so a throwing sink propagates its error and fails the caller's write.

content/docs/kernel/services.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ The core ecosystem defines several standard service contracts:
8585

8686
| Service Name | Interface | Provider Example |
8787
| :--- | :--- | :--- |
88-
| `http-server` | `IHttpServer` | `plugin-hono-server`, `adapter-nextjs` |
88+
| `http-server` | `IHttpServer` | `plugin-hono-server` |
8989
| `data` | `IDataEngine` | `@objectstack/objectql` (drivers implement `IDataDriver`) |
9090
| `auth` | `IAuthService` | `plugin-auth` |
9191
| `api-registry` | `ApiRegistry` | `@objectstack/core` |

content/docs/permissions/access-recipes.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Keeping capability, assignment and requirement decoupled means resources stay st
6262

6363
## Runnable example
6464

65-
- Built-in permission sets: [`default-permission-sets.ts`](https://github.com/objectstack-ai/framework/blob/main/packages/plugins/plugin-security/src/objects/default-permission-sets.ts) (`member_default` shows owner-scoped RLS + tenant isolation).
65+
- Built-in permission sets: [`default-permission-sets.ts`](https://github.com/objectstack-ai/framework/blob/main/packages/plugins/plugin-security/src/objects/default-permission-sets.ts) (`member_default` shows owner-scoped RLS; tenant isolation is now the Layer 0 tenant wall in `tenant-layer.ts`, ADR-0095 D1).
6666
- Security objects: [`packages/plugins/plugin-security/src/objects`](https://github.com/objectstack-ai/framework/tree/main/packages/plugins/plugin-security/src/objects).
6767

6868
## Anti-patterns

content/docs/permissions/administrator-guide.mdx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The one thing to internalize before anything else:
2222

2323
| Concept | What it is | Who authors it | You touch it |
2424
|:--|:--|:--|:--|
25-
| **Permission set** | The only capability container — object CRUD, field security, access depth, system capabilities | Platform & app developers (in Studio) | Rarely — [Step 5](#step-5--configure-permissions-only-when-the-shipped-positions-dont-fit) |
25+
| **Permission set** | The only capability container — object CRUD, field security, access depth, system capabilities | Platform & app developers; ships with apps | Rarely — [Step 5](#step-5--configure-permissions-only-when-the-shipped-positions-dont-fit) |
2626
| **Position** (岗位) | A flat, named job function that binds permission sets; users hold positions | Ships with apps; you assign it | **Daily** |
2727
| **Business unit** | The *one* hierarchy — org tree that decides visibility depth and delegation boundaries | You, at onboarding and reorgs | Occasionally |
2828
| **User** | The person signing in | You | Daily |
@@ -144,12 +144,12 @@ report names the exact permission set and layer to fix.
144144

145145
## Step 5 — Configure permissions (only when the shipped positions don't fit)
146146

147-
One rule of thumb: **permissions are *designed* in Studio, *assigned* in
148-
Setup.** If you are in this section, you've confirmed no shipped position
149-
covers the need.
147+
One rule of thumb: **capability ships with the apps you install — you rarely
148+
author a permission set by hand.** If you are in this section, you've confirmed
149+
no shipped position covers the need.
150150

151-
The permission-set **matrix editor** (Studio → Access) is a structured
152-
spreadsheet — you never hand-write JSON:
151+
The permission-set **matrix editor** (Setup → Access Control → Permission Sets)
152+
is a structured spreadsheet — you never hand-write JSON:
153153

154154
- **Object grid** — per object: Create / Read / Update / Delete, lifecycle
155155
bits (Transfer / Restore / Purge), and View All / Modify All, with per-row
@@ -180,9 +180,9 @@ Full model: [Permission Sets](/docs/permissions/permission-sets).
180180

181181
## FAQ
182182

183-
**Do I need Studio?** Rarely. Setup is the administrator's home; Studio is
184-
where permission sets are *designed* — you enter it for Step 5 and for the
185-
Explain panel.
183+
**Do I need Studio?** Rarely. Setup is the administrator's home — the
184+
permission-set matrix editor (Step 5) lives there too. You enter Studio mainly
185+
for the **Explain** panel.
186186

187187
**A new user sees almost nothing — broken?** No: that's the baseline working.
188188
Access arrives when you assign a position (Step 3).
@@ -207,7 +207,7 @@ sets can be overlaid (Step 5) or simply left unassigned.
207207
| Password policy, MFA, lockout, SSO | Setup → Configuration → Authentication |
208208
| Company info, localization, branding | Setup → Configuration |
209209
| Sessions, notification events, audit logs | Setup → Diagnostics |
210-
| Permission matrix editor, Explain access | Studio → Access |
210+
| Explain access | Studio → Access |
211211

212212
---
213213

content/docs/permissions/attachments-access.mdx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ Attachments panel renders only for such objects. Any other target is rejected:
3636
This is enforced by a `beforeInsert` hook on `sys_attachment` and is
3737
independent of the record-level checks below.
3838

39-
## Create — read visibility (+ edit-on-parent) & provenance
39+
## Create — parent-edit access & provenance
4040

41-
Creating a `sys_attachment` requires that the caller can **read** the parent
42-
record (verified with a caller-scoped `findOne`, so RLS / OWD / sharing of the
43-
parent object apply), and — for edit-on-parent parity — that they can edit it.
41+
Creating a `sys_attachment` requires that the caller can **edit** the parent
42+
record — Salesforce edit-on-parent parity — verified with the sharing service's
43+
`canEdit`, so RLS / OWD / sharing of the parent object apply (public-model
44+
parents are editable by any member by design). When no sharing service is
45+
wired, the gate degrades to caller-scoped read visibility (a `findOne`).
4446
`uploaded_by` is **server-stamped** from the session; a client-supplied value
4547
is ignored.
4648

@@ -117,7 +119,7 @@ can be shared across records). Reclamation is handled by the platform LifecycleS
117119
| Operation | Requirement | Deny code |
118120
| --- | --- | --- |
119121
| Attach (create) | parent object opts in (`enable.files`) | `FILES_DISABLED` (403) |
120-
| Attach (create) | can read + edit the parent record | `ATTACHMENT_PARENT_ACCESS` (403) |
122+
| Attach (create) | can edit the parent record | `ATTACHMENT_PARENT_ACCESS` (403) |
121123
| List / read | inherits parent read visibility | *(filtered out)* |
122124
| Delete | uploader or parent editor (+ RBAC delete grant) | `ATTACHMENT_DELETE_DENIED` / `PERMISSION_DENIED` (403) |
123125
| Download | session + owner-or-parent-read (attachments scope) | `AUTH_REQUIRED` (401) / `ATTACHMENT_DOWNLOAD_DENIED` (403) |

content/docs/permissions/authentication.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ new AuthPlugin({
570570
#### Send Magic Link
571571

572572
```typescript
573-
const response = await fetch('http://localhost:3000/api/v1/auth/magic-link/send', {
573+
const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/magic-link', {
574574
method: 'POST',
575575
headers: { 'Content-Type': 'application/json' },
576576
body: JSON.stringify({
@@ -780,7 +780,9 @@ const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/email',
780780
});
781781

782782
const data = await response.json();
783-
const token = data.data.token;
783+
// better-auth returns the bearer token at the top level of a direct
784+
// `/sign-in/email` response (the client SDK re-wraps it under `data`).
785+
const token = data.token;
784786

785787
// Use token in subsequent requests
786788
const protectedResponse = await fetch('http://localhost:3000/api/v1/data/task', {
@@ -830,7 +832,7 @@ All endpoints are available under `/api/v1/auth/*`:
830832

831833
#### Magic Links
832834

833-
- `POST /api/v1/auth/magic-link/send` - Send magic link email
835+
- `POST /api/v1/auth/sign-in/magic-link` - Send magic link email
834836
- `GET /api/v1/auth/magic-link/verify` - Verify magic link
835837

836838
#### Phone Number (requires `plugins.phoneNumber`)

content/docs/permissions/authorization.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,13 +377,13 @@ The complete, prioritized gap map lives in issue **#2561** (the production
377377
| ADR | Owns |
378378
|---|---|
379379
| [0049](/adr/0049-no-unenforced-security-properties) | No unenforced security properties (enforce / mark / remove) |
380-
| [0054](/adr/0054-prove-it-runs) | Prove-it-runs — high-risk classes need runtime proofs |
380+
| [0054](/adr/0054-runtime-proof-for-authorable-surface) | Prove-it-runs — high-risk classes need runtime proofs |
381381
| [0056](/adr/0056-permission-model-landing-verification) | Permission-model landing: OWD, anonymous deny default, D10 matrix |
382382
| [0057](/adr/0057-erp-authorization-core-business-units-and-scope-depth) | Business units, scope depth, declarative RBAC seeding, platform-owned assignment |
383383
| [0066](/adr/0066-unified-authorization-model) | Unified model: capability registry, posture, precedence, future refinements |
384-
| [0068](/adr/0068-identity-roles) | Built-in identity positions (formerly "identity roles"), `EvalUser` |
384+
| [0068](/adr/0068-unified-user-context-and-built-in-identity-roles) | Built-in identity positions (formerly "identity roles"), `EvalUser` |
385385
| [0069](/adr/0069-enterprise-authentication-hardening) | Enterprise authentication hardening (phased) |
386-
| [0078](/adr/0078-no-inert-declarable-metadata) | No inert declarable metadata |
386+
| [0078](/adr/0078-no-silently-inert-metadata) | No inert declarable metadata |
387387
| [0086](/adr/0086-authz-metadata-config-boundary-and-cross-package-composition) | Metadata↔config boundary, package provenance, cross-package composition |
388388
| 0090 | Permission Model v2: position rename + vocabulary freeze, profile removal, fail-closed OWD default + external dial, audience anchors, principal taxonomy, publish linter, delegated administration, explain engine + access matrix |
389389
| 0091 | Grant lifecycle: validity windows + resolution-time filtering (L1, landed), delegation, break-glass, recertification substrate |

content/docs/permissions/explain.mdx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ another user's private note reports:
4141
"layer": "vama_bypass",
4242
"verdict": "widens",
4343
"detail": "View/Modify All Data bypass held via [showcase_auditor] — ownership and sharing checks are skipped.",
44-
"contributors": [{ "kind": "permission_set", "name": "showcase_auditor", "via": "position:auditor" }]
44+
"contributors": [{ "kind": "permission_set", "name": "showcase_auditor", "via": "direct grant" }]
4545
}
4646
```
4747

@@ -95,9 +95,10 @@ A deployment without `@objectstack/plugin-security` answers 501.
9595
- **The [access-matrix snapshot gate](/docs/permissions/access-matrix)**
9696
the publish-time matrix is the same evaluation run over representative
9797
(permission set × object) pairs; explain is the per-decision zoom lens.
98-
- **Delegated-administration audits** — explain reports both *who granted*
99-
(the `granted_by` stamp) and, via contributor attribution, *who could
100-
have* ([delegated administration](/docs/permissions/delegated-administration)).
98+
- **Delegated-administration audits** — via contributor attribution, each
99+
layer's verdict names the permission set behind it and how the caller holds
100+
it — including a position held *via delegation*
101+
([delegated administration](/docs/permissions/delegated-administration)).
101102

102103
## See also
103104

content/docs/permissions/field-level-security.mdx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ names in `details.forbiddenFields`:
5454

5555
```json
5656
{
57-
"error": {
58-
"code": "PERMISSION_DENIED",
59-
"message": "[Security] Field write denied: not permitted to edit [salary, ssn] on 'employee'",
60-
"details": {
61-
"operation": "insert",
62-
"object": "employee",
63-
"forbiddenFields": ["salary", "ssn"]
64-
}
57+
"name": "PermissionDeniedError",
58+
"code": "PERMISSION_DENIED",
59+
"statusCode": 403,
60+
"message": "[Security] Field write denied: not permitted to edit [salary, ssn] on 'employee'",
61+
"details": {
62+
"operation": "insert",
63+
"object": "employee",
64+
"forbiddenFields": ["salary", "ssn"]
6565
}
6666
}
6767
```
@@ -100,4 +100,3 @@ writes.
100100

101101
- [Permissions & Identity overview](/docs/permissions)
102102
- [Permission Sets](/docs/permissions/permission-sets)
103-
- [Permission Sets](/docs/permissions/permission-sets)

content/docs/permissions/permission-metadata.mdx

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ const salesUserPermission = {
6060
| :--- | :--- | :--- | :--- |
6161
| `name` | `string` || Machine name (`snake_case`) |
6262
| `label` | `string` | optional | Display label |
63-
| `isDefault` | `boolean` | optional | [ADR-0090 D5] Install-time suggestion to bind this set to the `everyone` position (admin confirms; never auto-bound) |
63+
| `isDefault` | `boolean` | optional | [ADR-0090 D5] Baseline for the `everyone` position: an app-level default set is auto-bound at boot (guarded, idempotent); a package-shipped set instead becomes an install-time suggestion an admin confirms |
6464
| `adminScope` | `AdminScope` | optional | [ADR-0090 D12] Delegated-administration scope (BU subtree + assignable-set allowlist) |
6565
| `objects` | `Record<string, ObjectPermission>` || Object-level permissions |
6666
| `fields` | `Record<string, FieldPermission>` | optional | Field-level security, keyed by `'object.field'` |
6767
| `systemPermissions` | `string[]` | optional | System-level capabilities |
6868
| `tabPermissions` | `Record<string, TabVisibility>` | optional | Tab/app visibility |
6969
| `rowLevelSecurity` | `RowLevelSecurityPolicy[]` | optional | Row-level security policies |
70-
| `contextVariables` | `Record<string, any>` | optional | RLS context variables |
70+
| `contextVariables` | `Record<string, any>` | optional | **Not implemented** — declared but ignored; the RLS engine reads only the `current_user.*` built-ins (see note below) |
7171

7272
## Object Permissions
7373

@@ -129,19 +129,21 @@ Control which apps/tabs are visible to users:
129129

130130
```typescript
131131
tabPermissions: {
132-
crm: 'visible', // Always shown
133-
admin: 'hidden', // Never shown
134-
reports: 'default_on', // Shown by default, user can hide
135-
analytics: 'default_off', // Hidden by default, user can show
132+
crm: 'visible', // Shown
133+
admin: 'hidden', // Hidden — removed from nav
134+
reports: 'default_on', // Shown today (per-user toggle default not yet implemented)
135+
analytics: 'default_off', // Shown today (per-user toggle default not yet implemented)
136136
}
137137
```
138138

139139
| Visibility | Description |
140140
| :--- | :--- |
141-
| `visible` | Always visible, user cannot hide |
142-
| `hidden` | Always hidden, user cannot show |
143-
| `default_on` | Visible by default, user can toggle off |
144-
| `default_off` | Hidden by default, user can toggle on |
141+
| `visible` | Shown (also the effective behavior when a tab is left unset) |
142+
| `hidden` | Hidden — the app is removed from the user's navigation |
143+
| `default_on` | Shown today; reserved for a future per-user toggle default |
144+
| `default_off` | Shown today; reserved for a future per-user toggle default |
145+
146+
**Note:** Only `hidden` currently affects runtime behavior — the app-visibility gate hides an app when its tab is set to `hidden` and treats every other value (including `default_off`) as visible. The per-user toggle / default-off semantics implied by `default_on`/`default_off` are not yet implemented.
145147

146148
## System Permissions
147149

@@ -173,26 +175,21 @@ rowLevelSecurity: [
173175
using: "owner_id == current_user.id",
174176
},
175177
{
176-
name: 'same_department',
178+
name: 'same_organization',
177179
object: 'account',
178180
operation: 'select',
179-
using: "department == current_user.department",
181+
using: "organization_id == current_user.organization_id",
180182
},
181183
]
182184
```
183185

184186
### Context Variables
185187

186-
Provide custom context values that RLS conditions can reference (in addition to
187-
the built-in `current_user.*` variables):
188-
189-
```typescript
190-
contextVariables: {
191-
allowed_regions: ['US', 'EU'],
192-
access_level: 2,
193-
custom_attribute: 'value',
194-
}
195-
```
188+
**Note:** The `contextVariables` field is **not yet implemented** — the RLS
189+
compiler never reads it, so custom context values are **not** injected into
190+
row-level conditions. Conditions can only reference the built-in `current_user.*`
191+
variables described below; authoring `contextVariables` raises a build-time
192+
liveness warning.
196193

197194
The built-in context variables available in RLS `using`/`check` clauses include
198195
`current_user.id`, `current_user.organization_id`, and `current_user.positions`
@@ -274,10 +271,10 @@ const salesManagerPermission = {
274271

275272
rowLevelSecurity: [
276273
{
277-
name: 'team_accounts',
274+
name: 'org_isolation',
278275
object: 'account',
279276
operation: 'select',
280-
using: "team == current_user.team",
277+
using: "organization_id == current_user.organization_id",
281278
},
282279
],
283280
};

0 commit comments

Comments
 (0)