Skip to content

Commit 2b64e0e

Browse files
os-zhuangclaude
andauthored
fix(docs,test): 文档失真修正与 ReDoS 断言去负载敏感 (#4485, #4476, #4486, #4452) (#4497)
* test(metadata-core): make the ReDoS guard load-insensitive (#4485) The ReDoS assertion in protocol-handshake.test.ts bounded the pathological scan with an absolute 50ms wall clock. Under the full-repo run (~130 parallel turbo tasks) that ceiling measures machine load rather than the parser: it exceeded 50ms on a healthy tree and reddened PRs that never touched this package, leaving the diagnosis cost to whoever happened to be running. The underlying guard is real and stays (CodeQL 837/838). What changes is how it is measured. The three `toBeNull()` behavioural assertions -- the actual contract, that adversarial input is unrecognized rather than falsely rejected -- are kept and now stand on their own. The wall-clock proxy is replaced by a scaling check: the same adversarial shapes at 1x and 8x length, asserting the parse stays linear in the input. Load largely cancels out of a ratio, which is what makes the criterion load-insensitive. Measured: healthy parsing tracks the input at 8.3-8.5x, stable across runs. The 40x ceiling keeps ~5x headroom while still catching a merely quadratic regression (~64x), let alone an exponential one, which would not finish. Two measurement details are load-hardening, both established empirically: timings are taken back-to-back within one iteration and reduced by minimum *ratio* rather than minimising each timing independently (a scheduler steal landing in only one window skewed the latter, observed reddening at 3x CPU oversubscription); and the JIT is warmed so the baseline is not inflated. Note the pathological-to-benign ratio suggested on the issue does not work here: a benign 16-char range parses ~300x faster than a 100k-char one purely because it is 100k characters shorter, so it would fail on a healthy machine. Verification: 5/5 consecutive clean runs, plus 8/8 under 3x CPU oversubscription (the shape that reproduced the original failure). Fixes #4485 * docs: date the v17 query-surface removals to 17, not 18 (#4476) Seventeen passages dated v17 removals to `@objectstack/spec` 18. They ship in 17 -- this train. The number is the actionable half of a removal notice: a reader on 16 asking whether upgrading to 17 breaks their cursor-paginated loop was told the removal is a major away, so they plan for it later and the upgrade breaks. Evidence that 17 is correct: `spec-changes.json` carries `toMajor: 17` for data.query.{cursor,distinct,joins,windowFunctions} and stack.api.requireAuth; this tree is 17.0.0-rc.1 with `PROTOCOL_VERSION = '17.0.0'`; and the keys are already `[RETIRED]` in `authorable-surface.json`. A removal cannot be retired in a 17 build and also ship in 18. #4476 fingerprinted nine locations. Grepping the bare pattern -- which the issue itself recommended over working the list file-by-file -- found eight more: - The nine listed: query-syntax.mdx (4), queries.mdx (4), troubleshooting.mdx. - query-syntax.mdx:98, the #4286 sweep summary paragraph, same error. - skills/objectstack-query/ (5): SKILL.md and the aggregation/pagination rules. These are agent-facing and the highest-leverage of the set -- an agent authoring a query reads them as ground truth. Body prose only, so the frontmatter-derived listings that build-skill-docs.ts generates are unchanged. - implementation-status.mdx (2): the same error shape on a different change, `api.requireAuth` (#3963), which spec-changes.json also puts at toMajor 17. Also records the fingerprint in the sweep run log, as #4476 asks. Runs 1-2 matched on surface names and so read past passages that named the right surface and the wrong release; the new row tells the next run to check the number, with spec-changes.json `toMajor` as the arbiter. Fixes #4476 * docs: restore the trailing options arg on IDataEngine reads (#4486) The `IDataEngine` block in data-engine.mdx wrote all four read methods with two parameters, dropping the trailing `options?: BaseEngineOptions` that the real contract gives each of them (packages/spec/src/contracts/data-engine.ts :70/85/89/90). The write methods in the same block each carried their own `options`, so the block taught exactly the wrong model -- "writes take options, reads do not" -- and that is the misconception #4251 existed to fix. The parameter is not incidental: the same `{ context }` object is correct as insert's 3rd argument but was SILENTLY DROPPED as find's, so an intended `isSystem` bypass vanished and control-plane reads came back empty once org-scoping hooks landed. Anyone -- human or agent -- writing code from this block was being led back to the pre-#4251 shape, against a failure mode that raises no error. Adds `BaseEngineOptions` to the block's import list (the contract imports it from the same module), and a callout recording the precedence the contract states: `query.context` remains supported, and when both are given `options.context` wins. `content/docs/kernel/contracts/` is hand-written -- only `content/docs/ references/` is generated -- so no generator run is involved. Fixes #4486 * docs(service-automation): rewrite the README against the real flow DSL (#4452) The README's flow sections described a DSL that has never existed. Every node type name was wrong (`record_create` vs `create_record`, `query` vs `get_record`), the interpolation dialect was Salesforce's `{!…}` which the platform does not parse, and branching/looping/error handling were written as nested `steps` arrays -- a shape the schema has no key for. Nothing in it ran. Since #4414 + #4439 this stopped being merely useless: `conditions[].expression` is on the expression ledger, so the README's `'{!trigger.record.amount} > 10000'` is now REFUSED at `registerFlow()` / `objectstack validate`. An author copying it got a CEL error whose advice ("drop the braces") did not fix their node, because the node's entire shape was wrong too. Rewritten from the schemas and executors, not from the old README: - Flows are a DAG of flat `nodes` + `edges`. Branching is an edge, and a decision routes by matching its branch `label` to an out-edge `label` -- the #4414 trap, called out inline. - The record-change binding lives on the `start` node's config (`{ objectName, triggerType, condition }`), not at the flow top level. - CRUD config table: `objectName` (not `object`), `filter` as an OBJECT (not a `filters` array of triples), `outputVariable` (not `output`), no `recordId`. Notes that `update_record` has no `outputVariable` -- the executor reads none. - Both expression dialects stated with the rule that disambiguates them: every condition is bare CEL, braces are for values. Says plainly that `{!…}` is not a dialect here. - `loop` / `parallel` / `try_catch` given their real ADR-0031 region shape (`config.body`, `config.branches`, `config.try`/`catch`), and `wait` its node-level `waitEventConfig` block rather than the invented `duration` + `nextSteps`. - Flow `type` values corrected (`schedule`, not `scheduled`). Per the issue's preference, the per-node reference is NOT duplicated here: the README now points at content/docs/automation/flows.mdx, the maintained one. Keeping a second hand-written node catalog in a package README is the #4027/#3569 shape that produced this drift. `pnpm --filter @objectstack/spec check:generated`: all 8 artifacts up to date. Fixes #4452 * chore: add release-nothing changeset for the v17 verification docs/test fixes The Check Changeset gate requires every PR to add at least one `.changeset/*.md` relative to base. This branch touches only `.md`/`.mdx` prose and one `.test.ts` file -- verified against the diff, no package source, no public export, no protocol change -- so the empty-frontmatter form is the accurate declaration: it publishes nothing. * docs(service-automation): note that parallel/try_catch are built-in too (#4452) The Node Types section presented `FLOW_BUILTIN_NODE_TYPES` (i.e. the `FlowNodeAction` enum) as the built-in set, but the ADR-0031 structured constructs `parallel` and `try_catch` ship registered builtin executors without appearing in that enum -- which is exactly why `FlowNodeSchema.type` validates against the live action registry rather than a closed enum. Left as written, the list contradicted the Advanced Features section just below it. * docs(service-automation): fix run-on sentence in the expressions section (#4452) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ba5ff2f commit 2b64e0e

12 files changed

Lines changed: 331 additions & 259 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
---
3+
4+
docs+test: v17 verification defects — ReDoS assertion load-insensitivity, doc drift (#4485, #4476, #4486, #4452)
5+
6+
Release-nothing: touches only `.md`/`.mdx` prose and one `.test.ts` file. No
7+
package source, no public export, no protocol change — so no package needs a
8+
version bump.
9+
10+
- **#4485** `protocol-handshake.test.ts` — the ReDoS guard bounded the
11+
pathological scan with an absolute 50ms wall clock, which measures machine
12+
load rather than the parser: under the full-repo run (~130 parallel turbo
13+
tasks) it exceeded 50ms on a healthy tree and reddened PRs that never touched
14+
`@objectstack/metadata-core`. The behavioural assertions are kept; the
15+
wall-clock proxy is replaced by a scaling check (same adversarial shapes at 1x
16+
and 8x length), so load largely cancels out of the ratio.
17+
- **#4476** Seventeen passages dated the v17 query-surface removals to
18+
`@objectstack/spec` 18. They ship in 17; `spec-changes.json` gives
19+
`toMajor: 17`.
20+
- **#4486** The `IDataEngine` doc block dropped the trailing
21+
`options?: BaseEngineOptions` from all four read methods — the very parameter
22+
#4251 added, against a failure mode that raises no error.
23+
- **#4452** `service-automation`'s README taught a flow DSL that never existed
24+
(node type names, interpolation dialect, and nested `steps` all wrong);
25+
rewritten from the schemas and executors. README only — no package code.

content/docs/data-modeling/queries.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ backend chooses, exactly as before.
256256

257257
### Keyset Pagination — a `where` predicate on the sort key
258258

259-
`query.cursor` was **removed in `@objectstack/spec` 18** (#4286): nothing on the server
259+
`query.cursor` was **removed in `@objectstack/spec` 17** (#4286): nothing on the server
260260
ever read it, so a cursor query silently returned the same first page every time. The
261261
key is tombstoned and `QueryBuilder.cursor()` was removed with it. Express the keyset
262262
directly — seek past the last row instead of offsetting:
@@ -422,7 +422,7 @@ come later behind a driver capability flag without changing these semantics.
422422
## Joins — removed
423423

424424
<Callout type="warn">
425-
`query.joins` was **removed in `@objectstack/spec` 18** (#4286, ADR-0049
425+
`query.joins` was **removed in `@objectstack/spec` 17** (#4286, ADR-0049
426426
enforce-or-remove): no driver's `find()` ever executed a join — the SQL, in-memory, and
427427
MongoDB drivers all ignored the array, so it only ever declared a capability that did
428428
not run. The key is tombstoned: authoring it is a `tsc` error, and a query carrying it
@@ -532,7 +532,7 @@ by `@objectstack/plugin-pinyin-search`) recomputes the column on demand.
532532
## Window Functions — removed from the request surface
533533

534534
<Callout type="warn">
535-
`query.windowFunctions` was **removed in `@objectstack/spec` 18** (#4286):
535+
`query.windowFunctions` was **removed in `@objectstack/spec` 17** (#4286):
536536
`ObjectQL.find()` / `.aggregate()` and the `POST /api/v1/data/:object/query` route never
537537
routed it anywhere, so sending it had no effect. The key is tombstoned — a query carrying
538538
it fails to parse with the upgrade prescription — and the
@@ -565,7 +565,7 @@ report/dashboard metadata.
565565

566566
### Distinct Records — removed flag, three live spellings
567567

568-
The top-level `query.distinct` flag was **removed in `@objectstack/spec` 18** (#4286):
568+
The top-level `query.distinct` flag was **removed in `@objectstack/spec` 17** (#4286):
569569
no driver's `find()` ever applied it, and its only observable effect was mis-wired —
570570
it silently suppressed the REST list count while still returning duplicate rows (the
571571
count is truthful again). The key is tombstoned and `QueryBuilder.distinct()` was

content/docs/deployment/troubleshooting.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ console.log(field.maxLength?.toString() ?? 'no limit');
280280
4. **Avoid deep nesting** — Limit nested `$and`/`$or` depth
281281
5. **Use keyset pagination** — For large datasets, seeking past the last row is
282282
faster than a deep `offset`. Express the keyset as a `where` predicate on the
283-
sort key (the `cursor` query property was removed in `@objectstack/spec` 18,
283+
sort key (the `cursor` query property was removed in `@objectstack/spec` 17,
284284
#4286 — nothing ever read it)
285285

286286
```typescript

content/docs/kernel/contracts/data-engine.mdx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The canonical `IDataEngine` interface uses **QueryAST-aligned parameter names**
2121

2222
```typescript
2323
import type {
24+
BaseEngineOptions,
2425
EngineQueryOptions,
2526
DataEngineInsertOptions,
2627
EngineUpdateOptions,
@@ -31,11 +32,12 @@ import type {
3132
} from '@objectstack/spec/data';
3233

3334
export interface IDataEngine {
34-
// Query
35-
find(objectName: string, query?: EngineQueryOptions): Promise<any[]>;
36-
findOne(objectName: string, query?: EngineQueryOptions): Promise<any>;
37-
count(objectName: string, query?: EngineCountOptions): Promise<number>;
38-
aggregate(objectName: string, query: EngineAggregateOptions): Promise<any[]>;
35+
// Query (reads take the execution context in a TRAILING options argument —
36+
// the same position the write methods take theirs)
37+
find(objectName: string, query?: EngineQueryOptions, options?: BaseEngineOptions): Promise<any[]>;
38+
findOne(objectName: string, query?: EngineQueryOptions, options?: BaseEngineOptions): Promise<any>;
39+
count(objectName: string, query?: EngineCountOptions, options?: BaseEngineOptions): Promise<number>;
40+
aggregate(objectName: string, query: EngineAggregateOptions, options?: BaseEngineOptions): Promise<any[]>;
3941

4042
// Mutation (write ops also accept in-process WriteObservabilityOptions — see `update`)
4143
insert(objectName: string, data: any | any[], options?: DataEngineInsertOptions & WriteObservabilityOptions): Promise<any>;
@@ -65,6 +67,20 @@ export interface IDataEngine {
6567

6668
All query methods use canonical **QueryAST parameter names**: `where`, `fields`, `orderBy`, `limit`, `offset`, `expand`.
6769

70+
<Callout type="warn">
71+
**Reads take the execution context in the trailing `options` argument**, the same
72+
position the write methods take theirs — `find`, `findOne`, `count` and `aggregate`
73+
all accept `options?: BaseEngineOptions`.
74+
75+
This matters because the mistake it prevents is silent. The same `{ context }` object
76+
is correct as the third argument to `insert`, and passing it as the third argument to
77+
`find` used to be **dropped without error** — so an intended `isSystem` bypass simply
78+
vanished, and control-plane reads started coming back empty once org-scoping hooks
79+
landed (#4251).
80+
81+
`query.context` remains supported. When **both** are given, `options.context` wins.
82+
</Callout>
83+
6884
### find
6985

7086
Executes a structured query with filtering, sorting, pagination, and field selection. Returns an array of records.

content/docs/protocol/objectql/query-syntax.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ on the `find()` path:
9595
`top` is the exception that *is* honored — the engine normalises it to `limit`.
9696

9797
The #4286 sweep (ADR-0049 enforce-or-remove) settled every other declared-but-inert
98-
member. **Removed** — tombstoned in `@objectstack/spec` 18, so a query carrying one
98+
member. **Removed** — tombstoned in `@objectstack/spec` 17, so a query carrying one
9999
fails to parse with the upgrade prescription and authoring it is a `tsc` error:
100100
`joins` (related records are read through `expand`), `windowFunctions` (a SQL-driver
101101
door remains: `SqlDriver.findWithWindowFunctions()`), `cursor` (express the keyset as
@@ -723,7 +723,7 @@ the driver's raw rows.
723723

724724
### Distinct
725725

726-
`query.distinct` was **removed in `@objectstack/spec` 18** (#4286): no driver ever
726+
`query.distinct` was **removed in `@objectstack/spec` 17** (#4286): no driver ever
727727
rendered `SELECT DISTINCT`, and the flag's only observable effect was mis-wired — it
728728
silently suppressed the REST list count (`total`/`hasMore` degraded to a page-local
729729
estimate) while still returning duplicate rows. The key is tombstoned and
@@ -793,7 +793,7 @@ expansion ignores them.
793793

794794
### Joins — removed (#4286)
795795

796-
`query.joins` was **removed in `@objectstack/spec` 18** (#4286, ADR-0049
796+
`query.joins` was **removed in `@objectstack/spec` 17** (#4286, ADR-0049
797797
enforce-or-remove): no driver ever read it, so a query carrying `joins` silently ran
798798
as a single-table query. The key is tombstoned — authoring it is a `tsc` error, and a
799799
query that still carries it (even as an empty array) fails to parse with the upgrade
@@ -805,7 +805,7 @@ joined in application code.
805805

806806
### Window Functions — removed from the request surface (#4286)
807807

808-
`query.windowFunctions` was **removed in `@objectstack/spec` 18** (#4286): `find()`
808+
`query.windowFunctions` was **removed in `@objectstack/spec` 17** (#4286): `find()`
809809
never applied it, so every OVER clause it declared was silently dropped. The key is
810810
tombstoned, and the `WindowFunction` / `WindowSpec` / `WindowFunctionNode` exports
811811
left with it — they declared `field` / `over` / `frame` members that no executor ever
@@ -871,7 +871,7 @@ every page full and every row real (objectui#3106, #4363). A query with no
871871
### Keyset Pagination
872872

873873
<Callout type="warn">
874-
`query.cursor` was **removed in `@objectstack/spec` 18** (#4286): no driver ever
874+
`query.cursor` was **removed in `@objectstack/spec` 17** (#4286): no driver ever
875875
implemented keyset pagination, so a cursor was accepted and ignored and every page came
876876
back identical — a caller looping "until `hasMore` is false" never terminated. The key
877877
is tombstoned (on `EngineQueryOptions` too) and `QueryBuilder.cursor()` was removed

content/docs/releases/implementation-status.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ Every route carries the `/api/v1` prefix. When project scoping is enabled each r
291291
- Client SDK supports bearer token header — but token validation requires the auth plugin
292292
- Auth route (`/auth/*`) only appears in Discovery when the auth plugin is registered
293293
- Fine-grained authorization (RLS, sharing) lives in `plugin-security` / `plugin-sharing`, not in the auth plugin. Territory-style access is expressed as an RLS dynamic-membership set (`ExecutionContext.rlsMembership`, e.g. `id in current_user.territory_account_ids`) rather than as a dedicated territory module
294-
- **Phase-1 RBAC enforcement is live end-to-end**: REST → ObjectQL → SecurityPlugin middleware now receives a populated `ExecutionContext` (userId, tenantId, positions, permissions). Tenant isolation is enforced as a Layer 0 tenant wall (`plugin-security/tenant-layer.ts`, ADR-0095 D1) that AND-composes `organization_id == current_user.organization_id` ahead of and independently of business RLS — the earlier wildcard `tenant_isolation` RLS policy on `member_default` was retired (an OR-merged business policy could widen it). The default `member_default` set still ships per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the global tables that lack an `organization_id` column. The earlier `tenantField` indirection (RLS expressions written against an abstract `tenant_id` column then rewritten to the configured physical column at compile time) was removed — the placeholder, the column name, and `RLSUserContext.organization_id` are now the same name end-to-end. The legacy `objectql.registerTenantMiddleware` (hardcoded `where.tenant_id` injection that pre-dated SecurityPlugin) has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics now uses the same reusable read scope via `security.getReadFilter`, so dataset-bound dashboards/reports do not bypass RLS. Verified cross-organization isolation on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_position_permission_set`. **Anonymous traffic is always denied** (ADR-0056 D2). The deployment-wide opt-out is gone: `api.requireAuth` was retired in `@objectstack/spec` 18 (#3963) and is now a tombstoned key that fails validation rather than reopening the data plane. The single decision lives in `@objectstack/core` (`security/anonymous-deny.ts`, 401 `UNAUTHENTICATED`), and every surface that legitimately serves a session-less caller derives its own narrow authorization from a declaration instead: control-plane paths via the auth-gate allowlist, public forms via `publicFormGrant` (ADR-0056 Option A), share links via a capability token validated then read as SYSTEM, `book.audience: 'public'` reads via the audience gate, and MCP via an OAuth token or API key.
294+
- **Phase-1 RBAC enforcement is live end-to-end**: REST → ObjectQL → SecurityPlugin middleware now receives a populated `ExecutionContext` (userId, tenantId, positions, permissions). Tenant isolation is enforced as a Layer 0 tenant wall (`plugin-security/tenant-layer.ts`, ADR-0095 D1) that AND-composes `organization_id == current_user.organization_id` ahead of and independently of business RLS — the earlier wildcard `tenant_isolation` RLS policy on `member_default` was retired (an OR-merged business policy could widen it). The default `member_default` set still ships per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the global tables that lack an `organization_id` column. The earlier `tenantField` indirection (RLS expressions written against an abstract `tenant_id` column then rewritten to the configured physical column at compile time) was removed — the placeholder, the column name, and `RLSUserContext.organization_id` are now the same name end-to-end. The legacy `objectql.registerTenantMiddleware` (hardcoded `where.tenant_id` injection that pre-dated SecurityPlugin) has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics now uses the same reusable read scope via `security.getReadFilter`, so dataset-bound dashboards/reports do not bypass RLS. Verified cross-organization isolation on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_position_permission_set`. **Anonymous traffic is always denied** (ADR-0056 D2). The deployment-wide opt-out is gone: `api.requireAuth` was retired in `@objectstack/spec` 17 (#3963) and is now a tombstoned key that fails validation rather than reopening the data plane. The single decision lives in `@objectstack/core` (`security/anonymous-deny.ts`, 401 `UNAUTHENTICATED`), and every surface that legitimately serves a session-less caller derives its own narrow authorization from a declaration instead: control-plane paths via the auth-gate allowlist, public forms via `publicFormGrant` (ADR-0056 Option A), share links via a capability token validated then read as SYSTEM, `book.audience: 'public'` reads via the audience gate, and MCP via an OAuth token or API key.
295295
- **OWD / sharing-model enforcement is live and proven end-to-end (ADR-0056)**: `private`, `public_read`, `public_read_write`, and `controlled_by_parent` are enforced through `plugin-sharing` + `plugin-security` and verified by dogfood proofs over the real HTTP stack. `object.sharingModel` accepts the canonical OWD vocabulary only (`private` / `public_read` / `public_read_write` / `controlled_by_parent`) — the legacy `read` / `read_write` / `full` aliases were removed from the enum (ADR-0090 D4), and an unset `sharingModel` on a custom object resolves to `private` (ADR-0090 D1). RLS owner policies resolve `current_user.email` in addition to `id` / `organization_id` / `positions` (#2054). Permission sets may declare `isDefault: true` as the install-time suggestion to bind the set to the built-in `everyone` position (ADR-0090 D5, superseding the ADR-0056 D7 fallback-profile mechanism). **A sharing rule must state its criteria** (#3896): all three write paths reject a match-all shape, a stored criteria-less rule matches nothing, and its materialised grants are revoked on the next reconcile.
296296

297297
---
@@ -439,7 +439,7 @@ There is no MSW package in this repo — browser API mocking is a devDependency
439439
- [x] Organization-Wide Defaults / sharing model — `private`, `public_read`, `public_read_write`, and `controlled_by_parent` enforced via `plugin-sharing` + `plugin-security`, proven by dogfood over the real HTTP stack (ADR-0056). Canonical vocabulary only — legacy aliases removed from the enum (ADR-0090 D4); unset custom-object OWD resolves to `private` (ADR-0090 D1)
440440
- [x] Sharing Rule evaluator — criteria rules re-evaluated on `afterInsert` / `afterUpdate` (`plugin-sharing/rule-hooks.ts`); every authorable recipient maps 1:1 onto an enforced `expandRecipient` branch (`plugin-sharing/sharing-rule-service.ts`) — `user`, `team`, `position`, `business_unit`, and `unit_and_subordinates` (business-unit-subtree widening, ADR-0057 D5 / ADR-0090 D3). Under ADR-0078 enforce-or-remove, `criteria` is now the only rule *type* (owner-type rules were removed from the authoring surface because the static materialiser cannot track live membership), the `group` recipient was renamed to `team`, `guest` was removed, and `queue` stays reserved in the runtime contract but deliberately non-authorable
441441
- [x] Everyone-baseline suggestion — a permission set may set `isDefault: true` as the install-time suggestion to bind it to the built-in `everyone` position; resolved per-request as an additive baseline, no fallback cliff (ADR-0090 D5)
442-
- [x] Default-deny for anonymous traffic — the global default-deny landed (ADR-0056 D2) and the `api.requireAuth` opt-out was then **removed** in `@objectstack/spec` 18 (#3963): the key is tombstoned and rejected at parse time, the deny decision is centralised in `@objectstack/core` `security/anonymous-deny.ts`, and public forms self-authorize via `publicFormGrant` (Option A)
442+
- [x] Default-deny for anonymous traffic — the global default-deny landed (ADR-0056 D2) and the `api.requireAuth` opt-out was then **removed** in `@objectstack/spec` 17 (#3963): the key is tombstoned and rejected at parse time, the deny decision is centralised in `@objectstack/core` `security/anonymous-deny.ts`, and public forms self-authorize via `publicFormGrant` (Option A)
443443
- [ ] Studio RLS visual editor
444444
- [ ] Per-user×org permission cache
445445
- [ ] Audit UI / denied-access logging

0 commit comments

Comments
 (0)