You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* 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>
Copy file name to clipboardExpand all lines: content/docs/releases/implementation-status.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -291,7 +291,7 @@ Every route carries the `/api/v1` prefix. When project scoping is enabled each r
291
291
- Client SDK supports bearer token header — but token validation requires the auth plugin
292
292
- Auth route (`/auth/*`) only appears in Discovery when the auth plugin is registered
293
293
- 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.
295
295
- **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.
296
296
297
297
---
@@ -439,7 +439,7 @@ There is no MSW package in this repo — browser API mocking is a devDependency
439
439
-[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)
440
440
-[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
441
441
-[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)
0 commit comments