Skip to content

Commit 5081090

Browse files
feat(rest): the /meta read gate enforces a list view's and a dashboard's requiredPermissions; the liveness rows flip to live (#22639) (#22731)
Fixes #22639 Clause-②: yes (widening) ## Summary The rest half of the ruling of record `6095014058` on #22611 (letter A). The `/meta` read gate now applies a list view's and a dashboard's `requiredPermissions`: a list of capabilities, all required. It asks one predicate, `holdsRequiredPermissions` in `packages/rest/src/meta-item-read-gate.ts`, which the app arm now asks too. There is no second evaluator and no any-of form. Both transports ask the same gate: `RestServer` and the runtime dispatcher's `/meta` domain. - **List read.** `GET /api/v1/meta/view` and `GET /api/v1/meta/dashboard` list only what the caller may open. So the console's view switcher (`?object=`) follows. A view container is listed minus the `list` / `listViews` entries the caller does not hold. - **By-name read.** A list view or a dashboard the caller does not hold is refused WHOLE with `403 PERMISSION_DENIED`, the app arm's whole refusal (same emitter, same envelope), on every door: the plain read, `/published`, `/layers`, `?layers=`, `?state=draft`, `/diff`, `/history`, `/audit`. The refusal reaches an author as anyone. - **The object door** (scope note `6096254862`, item 1). An object's own `listViews` entries the caller does not hold are pruned inside `/meta/object`, on the list read and the by-name read, for every caller ADR-0106 D4 does not exempt. - **The flip.** Both liveness rows (`view.json` `list.requiredPermissions`, `dashboard.json` `requiredPermissions`) are `live`, cite the gate symbol, and no longer carry `authorWarn` / `authorHint`. The describe in `packages/spec/src/ui/audience-required-permissions.ts` drops its `[PLANNED — not enforced yet]` clause. The two lint warn pins turned red at the flip, as the rows said they would. They are re-subjected to the silence with a lit control; none was deleted. - **The lint** (scope note item 2). `validateCapabilityReferences` resolves the capability names at every list-view and dashboard position. That is a dashboard, a view container's `list` and `listViews` entries, a view item's `config`, a flattened list view, and an object's own `listViews`. - **The docs.** `content/docs/permissions/capabilities.mdx` said "A list view's and a dashboard's `requiredPermissions` are not enforced yet". That callout now says what the server enforces (dispatch assumption 6). No other docs prose is touched. The `content/docs/references/**` rows are regenerated from the describe. ## The dispatch's mechanism assumptions, measured 1. **Confirmed.** At `c63028e5b`, `meta-item-read-gate.ts` read `requiredPermissions` at exactly two code sites: `filterAppForUserWithReason` `:744` (the app) and `:761` (a navigation entry). The dashboard arms gated widgets on `requiresService` only, and no `view` read had an audience check. Both sites now call `holdsRequiredPermissions`, with unchanged semantics: absent or empty means no gate, and every name must be held. 2. **Confirmed.** Both rows were `planned` + `authorWarn`, and their notes prescribed the flip "in that same edit". `check:liveness` now reports "author warnings: 2 ledger row(s) opt into `authorWarn` … (planned 2); 0 on a `live` row". The two left are `object.externalSharingModel` and `page.print`. 3. **`authorExempt`, measured and stated.** These are the app arm's two rules, carried over. There is no new exemption. - **A whole refusal is not lifted for an author.** This covers ONE list view (a view item, its key under `config`; or a flattened list view, its key at the top level) and a dashboard. Each is refused on every door to an author as to anyone, as an app with an app-level `requiredPermissions` is. Pinned in the census (`stillRefused` gains the four `view/clm_contract.legal_queue × author` stored-door cells). - **Entry pruning follows `MetaReadGatePolicy.app`.** A view CONTAINER's `list` / `listViews` entries are pruned for every caller on the rendered doors. On the four `author-exempt` doors (`/layers`, `?layers=true`, `/diff`, `?state=draft`), a caller whose save door admits them (`mayWriteItem`) reads the stored container whole, as an app's author does. Otherwise Studio's designer would save back a container minus its withheld entries. `AUTHOR_EXEMPTION.types` in the census is now `['app', 'view']`. The census also holds the predicate to `PUT /meta/view/clm_contract`'s admission. - **An object's own list views follow ADR-0106 D4 on every door.** That is `isObjectSchemaMaskExempt`, the same caller property that already serves such a caller the object's fields unmasked: whoever may write a schema reads all of it. A pruned load PUT back verbatim would delete the withheld list views. This is the rule the object plain read already holds for fields, so it is not `MetaReadGatePolicy.app`. - A caller with no signed-in user is not gated, the app arm's rule. An anonymous caller is refused before any `/meta` read of these types, on both transports (`isPublicAudienceRead` admits book/doc only). 4. **The refused by-name answer, read and reused.** The app arm's whole refusal is `{ reason: 'app-permission', status: 403, code: 'PERMISSION_DENIED' }`. `RestServer` writes it through `sendEnvelopeError` as the ADR-0112 nested envelope (`{ success: false, error: { code, message } }`). The dispatcher writes `deps.error(message, 403, { code })`. A view or dashboard refusal is `{ reason: 'audience-permission', status: 403, code: 'PERMISSION_DENIED' }`, which `RestServer.sendMetaReadRefusal` writes through the same `sendEnvelopeError` case. Only the message names the item (`You do not have permission to open the 'NAME' view.` / `dashboard.`). The pin asserts `code` + `status`, and that the body's and `error`'s key sets equal the app refusal's. 5. **The `object-schema-fls-references.ts` walk, measured.** It CAN drop a `listViews` entry (`recordOf(presentationEntry(…), true)` returns `REMOVE`). But its only verdict input is the caller's denied FIELD set, and it runs only under a `project` mask posture: it is `passthrough` for a D4-exempt caller, a deployment with no `security` service, and the ADR-0106 D8 opt-out. Gating there would need two things: the caller's capabilities threaded into the mask posture, and the requiredPermissions decision made inside `metadata-core`, which would be a second evaluator. It would also tie the audience gate to the FLS opt-out. So the object door is gated in the rest gate's `object` arm with the one predicate, and `metadata-core` is untouched. Measured, not a stop: the ruling's one-predicate constraint holds. 6. **Docs, measured.** The spec half wrote the "Or is one capability, granted twice" section in `capabilities.mdx` with a "not enforced yet" warning callout. That sentence is corrected here and named in this PR. No other hand-written docs page says the key is not enforced (`grep` over `content/docs` outside `references/`). ## Where each door is enforced - `packages/rest/src/meta-item-read-gate.ts`: `holdsRequiredPermissions` (the predicate), `filterViewForUser`, `filterObjectListViewsForUser`, the `dashboard` / `view` / `object` arms of `createMetaItemReadGate` and `createMetaListReadGate`, and the `audience-permission` refusal. - `packages/rest/src/rest-server.ts`. The plain read's CACHED arm serves `view` and `object` (the default path, `enableCache: true`). It now asks the same gate before the mask and folds what the gate withheld into its `ETag`, so a caller who no longer holds a capability is never answered a `304` for the body that carried it. The conditional request is judged against that validator, not by the protocol. When nothing is withheld the `ETag` is unchanged. `gatesPerCaller` adds `view` and `dashboard`, so `/history`, `/audit` and `/diff` judge them. - `packages/runtime/src/domains/meta.ts`. The one-segment `/meta/OBJECT_NAME` legacy object exit asks the gate before the mask. It is reached only when the protocol has no type catalogue and no list for the segment. Every other runtime door already went through the shared gate. ## Pins, per audience (holder served, non-holder not served, no key served to all) - `packages/rest/src/meta-alternate-door-read-gates.test.ts` (the census, every door × reader / non-reader / author / anonymous): five subjects. A view with no key (served to all), ONE gated view (refused whole), a view container (pruned; author whole on the stored doors), a gated dashboard (refused whole) and an object with a gated list view (pruned; D4 whole). - `packages/rest/src/meta-view-dashboard-audience-gate.test.ts` (new, 19 cases): the predicate (all required, no any-of, the app arm asks it); `GET /meta/view` (holder / non-holder / `?object=` switcher / no key served to all); `GET /meta/dashboard`; `GET /meta/object` list views (D4 author whole); the by-name refusal envelope (view item, flattened view, dashboard, author); the cached arm (a different `ETag` per what was withheld, no `304` for a stale validator, a refused view never answered `304`, and a control that a matching validator is still `304`). - `packages/runtime/src/domains/meta-item-read-gate-parity.test.ts` and `meta-list-read-gate-parity.test.ts`: the same rows through both transports, the list fallback exits, and the legacy one-segment object exit. - `packages/lint/src/validate-capability-references.test.ts`: every position warns at its own path, a granted capability resolves (lit control in the same stack), and a view or dashboard without the key resolves nothing. - Re-subjected, none deleted: - The two lint warn pins: silent on a `live` row, lit control `object.externalSharingModel`. - The spec describe pin: no `[planned` marker now. - Three pins whose `view` subject was "a type no per-caller gate judges": the census edge and the two runtime parity controls now use `flow` (a new `LEAD_FLOW` fixture); the `view` control without a key stays, as "no key served to all". - Two `/audit` scoping pins whose stub deliberately has no `getMetaItem`: they now use `reports`, because a `view` now costs that door a current-document read. ## Verification Measured on head `87008a7e8` (the merge of `origin/main` `762db996a`) unless a line says otherwise. - **Build.** Closures built through `os-verify-lock.sh`: `pnpm --workspace-concurrency=2 --filter '@objectstack/runtime...' --filter '@objectstack/lint...' build` and `--filter '@objectstack/client-react...' build`. Both exited 0. `pnpm --filter @objectstack/spec check:generated` printed "All 15 generated artifacts are up to date". Before the merge, `--fix` regenerated exactly the two it proved stale: `content/docs/references/**` and `liveness/state-counts/`. - **Tests.** - `@objectstack/rest` `vitest run --project local`: 272 files, 5181 passed, 326 skipped. - `@objectstack/lint` `vitest run`: 135 files, 6307 passed. - `@objectstack/spec` `vitest run --project local`: 642 files in three shards, 214 + 214 + 214, 19197 passed (1 todo). - `@objectstack/runtime`: the three `meta-*-parity` files on `87008a7e8`, 850 passed. The full `--project local` suite ran on `d21e206ac`, before the merge, which touched no `packages/runtime` file: 348 files, 4984 passed, 19 skipped. - `integration` layers are declared to CI. - **Typecheck.** `typecheck` exited 0 for `@objectstack/spec`, `rest`, `lint` and `runtime`. The `check:test-typecheck` ledgers held unchanged: spec 52/246/135, rest 0/0/0, lint 2/6/2, runtime 27/190/68. - **Reverse verification of the type change** (one-shot probe, deleted). `@objectstack/rest`'s exported `MetaItemReadRefusal` gains `audience-permission`. A probe in `packages/runtime/src` typed against it, through the rebuilt `.d.ts`, gave two results. `reason: 'audience-permission'` → `tsc` exit 0. `'audience-permision'` → exit 2, TS2820, which lists the union with `"audience-permission"`. `git status` was clean after. - **Ablation** (one-shot, committed state `275083c38`). Each mutation went through `scripts/ablation-replace.mjs` WRAP mode with a `trap` restore. Each anchor hit x1 → x0 with a blob change. No build or dist preflight applies: the rest and runtime suites resolve `@objectstack/rest` from `src`. Direction: red, all four. - A1, `filterViewForUser` stops asking a view item's `config`: rest 14 failed / 431 passed, runtime 4 failed / 164 passed. - A2, the cached arm stops asking the gate: rest 28 failed / 417 passed. - A3, the legacy object exit masks the ungated object: runtime 1 failed (the legacy-exit pin) / 49 passed. - A4, the lint stops resolving dashboards: lint 2 failed / 10 passed. - Restore: each blob equals its HEAD blob, and `git diff HEAD` is empty. - **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) derived 113 commands on `87008a7e8`. All 80 lines of the dispatch's list are among them. Each was run with its exit captured before any pipe, then reconciled with `--ran`: "113 derived, 111 run, 1 NOT-MEASURED, 1 UNRUN". - 110 exited 0. `check:skill-examples` first exited 3 (PREREQUISITE NOT MET: `client-react` unbuilt) and exited 0 after that closure was built. - `check:platform-checklist` exited 1 with "7 problem(s)": absent symbols in `docs/qa/platform-checklist/areas/access-security.json` and `attachments-storage.json`, which this diff does not touch. The control leg is the same command on a checkout at `762db996a`, without this branch: exit 1, "7 problem(s)". - NOT MEASURED: `check:dual-build-cjs-loads`, exit 3, PREREQUISITE NOT MET (it needs every package's `dist/`, a whole-workspace build). - Not run: `check:type-check-debt`. Its `--re-measure` builds every DEBT package's closure, and this diff touches no DEBT or TEST_DEBT ledger package. Declared to CI. - **`pnpm lint`, proven narrowing.** The population, read from `eslint.config.mjs`, is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus its ignores. This diff's 14 files in that population, linted with `--no-inline-config --format json`: 14 files, 0 errors, 0 warnings, exit 0. Invariance: the config enables no type-aware linting (no `parserOptions.project`, no typed rules; the config says so at `:327`), so this diff cannot move a verdict on any untouched file. The JSON, MD and MDX files are outside the population. ## Acceptance notes - **Declared file surface, extended (named here for the PM).** The claim named `meta-item-read-gate.ts` and the `metadata-core` walk. This PR also edits `packages/rest/src/rest-server.ts` (the cached arm, `gatesPerCaller`, the refusal writer, the `/diff` rule below) and `packages/runtime/src/domains/meta.ts` (the legacy object exit). Both are `domain:cli` packages, and no open PR touched either file when this was opened. `packages/metadata-core` is NOT edited (assumption 5). Outside `packages/spec`'s three named files, only regenerated artifacts changed. - **Type surface.** The claim said no export is added. `MetaItemReadRefusal`, exported from `@objectstack/rest`, gains one union member, `audience-permission`. No new export, no error code (`PERMISSION_DENIED` exists) and no authorable key. The new functions are not on the package's entry. The rest changeset is graded `minor` for it. - **`/diff` of a never-published view or dashboard.** With no published row, `/diff` has no current document to judge, so it serves the diff as before. That is the event doors' existing rule for every gated type. A builder diffing a draft-only view keeps that diff, key or no key. The alternative, the app arm's "absence" for a missing current document, would have turned a draft-only view's or dashboard's diff into `404` for every builder, including views that name no capability; the pin `meta-history-diff-authoring-door.test.ts` holds that diff. The draft itself is judged where it is served whole (`?state=draft`, `/layers`). Boundary: a never-published GATED item's diff is readable by a builder who lacks the capability. Every such builder can already read that draft body through `GET /meta/_drafts`, which applies no audience gate to apps either. - **For the `domain:spec` seat, outside this card's spec surface (not edited, per the dispatch).** - `packages/spec/liveness/README.md`: the `view` and `dashboard` state-table notes (#22611, 2026-10-10) still describe the key as `planned` with `authorWarn`, awaiting this card. A dated flip note belongs there. - `packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`: the two `omit` rows' `why` reads "declared, not enforced yet — liveness verdict `planned` with `authorWarn` … whether to offer it then is a ruling for the enforcement". The text is now stale. Whether the Studio forms should offer the key is that ruling, not this card's. The pin is green. - Carrier: the `domain:spec` seat. Neither is a defect class. - **`.changeset/22611-list-view-dashboard-required-permissions.md`** is still unreleased and says "Not enforced yet … Enforcement … is the separate enforcing change". If both land before the next release, the release notes carry both sentences, and this PR's four changesets say the key is enforced. The release compiler's call. - **Contract-tier review owed** before the queue (the `packages/spec/src` path and a narrowing gate). The PM arranges it. --- _Generated by [Claude Code](https://claude.ai/code/session_019SvPnd2bzECRNmAU9i6E4k)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7c7e750 commit 5081090

27 files changed

Lines changed: 1267 additions & 165 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): `validateCapabilityReferences` resolves the capabilities a list view or a dashboard requires (#22639)
6+
7+
Clause-②: no
8+
9+
- The `/meta` read gate now serves a list view or a dashboard only to a user who holds every capability its `requiredPermissions` names, so a misspelled name hides the item from everyone. `validateCapabilityReferences` resolves those names like every other carrier's: a dashboard's, a view container's `list` and each `listViews` entry, a view item's `config`, a flattened list view's, and an object's own `listViews` entries. An unresolved name warns `capability-reference-unknown` at its own path (for example `views[0].listViews.legal_review.requiredPermissions`).
10+
- It is a warning, as for objects, fields, apps and actions: a single package's lint cannot see a capability another installed package declares.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/rest": minor
3+
---
4+
5+
feat(rest): the `/meta` read gate enforces a list view's and a dashboard's `requiredPermissions` (#22639)
6+
7+
Clause-②: yes (widening)
8+
9+
- **What the server now does.** `requiredPermissions` on a list view and on a dashboard is a list of capabilities a user must ALL hold, the same key and meaning as on an app, a navigation item and an action. The `/meta` read gate applies it through the one predicate the app arm uses (`holdsRequiredPermissions`), on both transports (`RestServer` and the runtime dispatcher's `/meta` domain):
10+
- `GET /api/v1/meta/view` and `GET /api/v1/meta/dashboard` list only what the caller may open. A view the caller does not hold is left out, so the console's view switcher (`?object=`) lists only the views the caller may open. A view container is listed minus the `list` / `listViews` entries the caller does not hold.
11+
- `GET /api/v1/meta/view/:name` and `GET /api/v1/meta/dashboard/:name` refuse a view or a dashboard the caller does not hold with `403 PERMISSION_DENIED`, the same refusal and envelope an app the caller may not open gets. Every alternate door of the read refuses it too (`/published`, `/layers`, `?layers=`, `?state=draft`, `/diff`, `/history`, `/audit`), to an author as to anyone.
12+
- `GET /api/v1/meta/object` and `/meta/object/:name` serve an object without the `listViews` entries the caller does not hold. A caller who may edit the object (the ADR-0106 D4 exemption the field mask already honours) reads every list view, so a save of what they loaded keeps them.
13+
- A view container is served whole to a caller who may write it on the stored-version doors (`/layers`, `?layers=`, `/diff`, `?state=draft`), as an app is, so a designer's save keeps the entries withheld from its author elsewhere.
14+
- **Caching.** The plain read's cached arm runs the same gate for `view` and `object`. Its `ETag` folds in what the gate withheld, and the conditional request is judged against that validator, so a caller who no longer holds a capability is never answered a `304` for the body that carried the gated view. When nothing is withheld the `ETag` is the same as before.
15+
- **Type surface.** `MetaItemReadRefusal` gains the `audience-permission` reason (`403 PERMISSION_DENIED`). A consumer that switches on `reason` writes it the way it writes `app-permission`.
16+
- **Nothing to migrate.** Only a list view or a dashboard that sets `requiredPermissions` is affected. The key was not accepted on either before the release that declared it.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): the `/meta/<objectName>` legacy object exit asks the read gate before it serves the schema (#22639)
6+
7+
Clause-②: no
8+
9+
- The runtime dispatcher's one-segment `/meta/<objectName>` exit, reached when a host's protocol has no type catalogue and no list for the segment, served the object schema through the field mask alone. It now asks the shared `/meta` read gate first, as every other object exit does, so an object's own `listViews` entries whose `requiredPermissions` the caller does not hold are not served there either. A caller who may edit the object still reads them all.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): a list view's and a dashboard's `requiredPermissions` is enforced — the describe and the liveness ledger say so (#22639)
6+
7+
Clause-②: no
8+
9+
- The shared description of `ListViewSchema.requiredPermissions` and `DashboardSchema.requiredPermissions` drops its "not enforced yet" clause and says what the server does: a user who does not hold every capability is not listed the item and is refused it by name, a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object.
10+
- The liveness ledger rows (`liveness/view.json` `list.requiredPermissions`, `liveness/dashboard.json` `requiredPermissions`) are `live`, citing the `/meta` read gate, and no longer carry `authorWarn`, so `os lint` / `os validate` stop warning `liveness-planned-property` when a view or a dashboard sets the key.
11+
- The schemas themselves do not change: same shape, same accept set.

‎content/docs/permissions/capabilities.mdx‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,14 @@ Listing each audience's own capability instead —
183183
`requiredPermissions: ['clm_legal.access', 'clm_admin.access']` — requires both,
184184
so it shuts out everyone who holds only one of them.
185185

186-
<Callout type="warn">
187-
**A list view's and a dashboard's `requiredPermissions` are not enforced yet.**
188-
The key is declared, so it validates and is stored, but no server reads it
189-
today: the view or the dashboard is still served to every user who can read
190-
it. Do not rely on it to keep a view or a dashboard from an audience until the
191-
platform enforces it.
186+
<Callout type="info">
187+
**A list view's and a dashboard's `requiredPermissions` are enforced by the
188+
server.** A user who does not hold every capability listed is not served the
189+
view or the dashboard: it is left out of their lists — the view switcher
190+
included — and opening it by name or by its direct URL is refused. The named
191+
list views of a view container, and an object's own list views, are left out
192+
the same way; a user who may edit the object still reads all of its list views,
193+
so a save of what they loaded keeps them.
192194
</Callout>
193195

194196
## What the seeder does, and what it refuses

‎content/docs/references/api/protocol.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,7 +1689,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
16891689
| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '<page_name>' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
16901690
| **description** | `string \| Record<string, string>` | optional | View description for documentation/tooltips |
16911691
| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration |
1692-
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. [PLANNED — not enforced yet] No server reads this key today, so the item is still served to every user who can read it; do not rely on it to keep a view or a dashboard from an audience until the `/meta` read gate applies it. |
1692+
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. |
16931693
| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting |
16941694
| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) |
16951695
| **rowColor** | `{ field: string; colors?: Record<string, string> }` | optional | Color rows based on field value |
@@ -1775,7 +1775,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
17751775
| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '<page_name>' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
17761776
| **description** | `string \| Record<string, string>` | optional | View description for documentation/tooltips |
17771777
| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration |
1778-
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. [PLANNED — not enforced yet] No server reads this key today, so the item is still served to every user who can read it; do not rely on it to keep a view or a dashboard from an audience until the `/meta` read gate applies it. |
1778+
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. |
17791779
| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting |
17801780
| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) |
17811781
| **rowColor** | `{ field: string; colors?: Record<string, string> }` | optional | Color rows based on field value |

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ const result = ApiMethod.parse(data);
391391
| **pageName** | `never` | optional | [REMOVED] `view.pageName` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it named the page a `type: 'page'` view was to mount, and that mount was never built: no renderer read the key, so the named page was never reached and the view drew an empty grid. Delete the key; to put a published page in front of users, give the app a navigation item — `{ type: 'page', pageName: '<page_name>' }` under the app's `navigation` — which is a different key on a different surface and is the page mount that has always rendered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
392392
| **description** | `string \| Record<string, string>` | optional | View description for documentation/tooltips |
393393
| **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration |
394-
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. [PLANNED — not enforced yet] No server reads this key today, so the item is still served to every user who can read it; do not rely on it to keep a view or a dashboard from an audience until the `/meta` read gate applies it. |
394+
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. |
395395
| **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting |
396396
| **grouping** | `{ fields: object[] }` | optional | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a group are paged (see GroupingConfigSchema) |
397397
| **rowColor** | `{ field: string; colors?: Record<string, string> }` | optional | Color rows based on field value |

‎content/docs/references/ui/dashboard.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const result = DashboardSchema.parse(data);
3939
| **refreshInterval** | `never` | optional | [REMOVED] `dashboard.refreshInterval` was renamed to `refreshIntervalSeconds` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `refreshIntervalSeconds`; the value (seconds) is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
4040
| **dateRange** | `{ field?: string; defaultRange?: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| …>; allowCustomRange?: boolean }` | optional | Global dashboard date range filter configuration |
4141
| **globalFilters** | `{ name?: string; field: string; object?: string; label?: string \| Record<string, string>; … }[]` | optional | Global filters that apply to all widgets in the dashboard |
42-
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. [PLANNED — not enforced yet] No server reads this key today, so the item is still served to every user who can read it; do not rely on it to keep a view or a dashboard from an audience until the `/meta` read gate applies it. |
42+
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities a user must ALL hold for this list view or dashboard to be served to them — names that permission sets grant through `systemPermissions`, the same list as `requiredPermissions` on an app, a navigation item or an action. Absent or empty: no audience gate. There is no any-of form: to serve one item to several audiences (legal OR admin), declare one capability and grant it to each of their permission sets. The server enforces it: a user who does not hold them all is not listed the item and is refused it by name; a view container's list views are pruned the same way, and so are an object's own list views for a user who may not edit the object. |
4343
| **aria** | `never` | optional | [REMOVED] `dashboard.aria` was removed in @objectstack/spec 17.0.0 (audit close-out) — no dashboard renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
4444
| **performance** | `never` | optional | [REMOVED] `dashboard.performance` was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; dashboard performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
4545
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this dashboard. |

0 commit comments

Comments
 (0)