Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/17369-organizations-entitlement-boundary-prose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@objectstack/verify": patch
---

Comment-only correction: the reason `bootStack`'s cross-tenant proofs stand in for `@objectstack/organizations` is now stated as the true one.

Those doc comments said the enterprise multi-organization runtime was **cloud-private / not installable in this workspace**. ADR-0132 falsified that: the runtime is open core, Apache-2.0, and published on npm. The effect they describe has not changed, so the text now gives the reason that is actually load-bearing — **ADR-0132's entitlement boundary forbids any framework package DECLARING `@objectstack/organizations`** (`packages/plugins/organizations/src/no-framework-dependents.pin.test.ts`, its mechanical half: "Apps declare it; packages do not"), because the commercial repository ships a licence-gated subclass under the same package name. So `packages/verify` cannot depend on the runtime and cannot resolve it, the `'posture-only'` stand-in stays exactly what it was, and the proof that the real plugin walls tenants still lives in cloud's `security-enterprise` multi-organization integration test.

⛔ **No behaviour, no dependency and no public surface moves.** `BootOptions.multiTenant` accepts and does the same things it did; the only shipped bytes that change are the doc comments carried into `dist/index.d.ts`. Apps that mount the runtime keep declaring it in their own `package.json`, which is and remains the supported wiring.
2 changes: 1 addition & 1 deletion content/docs/plugins/packages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern

### @objectstack/organizations (enterprise)

**Organization Scoping** — Multi-org (a.k.a. "soft" multi-tenant) row-level scoping. Ships as a **separate, closed-source enterprise package** it is not part of the open framework repo. It composes with the Layer 0 tenant wall in `plugin-security` (`tenant-layer.ts`).
**Organization Scoping** — Multi-org (a.k.a. "soft" multi-tenant) row-level scoping. Ships from this repo as the **open-core, Apache-2.0 package** `packages/plugins/organizations`, published on npm — ADR-0132 brought it back to open core, so it is no longer closed-source or absent from this repository. ⛔ Only a **host app** may declare it: ADR-0132's entitlement boundary forbids any framework package taking `@objectstack/organizations` as a dependency, because the commercial repository ships a licence-gated subclass under the same package name and a framework-level dependency would put the ungated class inside the tree a commercial app links against (`packages/plugins/organizations/src/no-framework-dependents.pin.test.ts` is the mechanical half). It composes with the Layer 0 tenant wall in `plugin-security` (`tenant-layer.ts`).

- **Features**: `organization_id` auto-stamp on insert; every query is AND-composed against the tenant wall, so no row outside the caller's organization scope is ever returned
- **When to use**: Multi-organization SaaS where every row is scoped to an organization. Enable by setting `OS_TENANCY_POSTURE` to a walled posture — `group` (union read across every organization the caller belongs to) or `isolated` (the hard per-organization wall) — and installing `@objectstack/organizations`; if a walled posture is requested but the package is missing, the platform refuses to boot (override with `OS_ALLOW_DEGRADED_TENANCY=1`). The legacy `OS_MULTI_ORG_ENABLED` boolean is still honoured, but only as a fallback **input** when `OS_TENANCY_POSTURE` is unset, and it can only ever select `isolated` — never gate application code on it (ADR-0105 D1). See [Tenancy Postures & Membership](/docs/deployment/tenancy-modes) for how the posture resolves
Expand Down
6 changes: 3 additions & 3 deletions docs/qa/platform-checklist/areas/attachments-storage.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
"why": "qa_vault (private + files) is the invisible parent both 403 clauses need, qa_shared is the entitled contrast, and the two personas are the non-admin principals the deny side must be checked as. Without the recipe both deny clauses are blocked(fixture) on stock seeds."
},
"knownGaps": [
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is cloud-private, so the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is open core since ADR-0132 (Apache-2.0, published on npm) — ⛔ it being closed-source is NOT the reason and has not been since #16215. The reason is ADR-0132's entitlement boundary: no framework package may declare `@objectstack/organizations` (pinned by `packages/plugins/organizations/src/no-framework-dependents.pin.test.ts` — apps declare it, packages do not), so `packages/qa/dogfood` cannot depend on it and the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
"CLOSED by the qa-scratch-authz recipe (#7670): stock showcase's only files-enabled object (showcase_project) is public_read_write, so no stock parent is invisible to any member. The 403 deny side is otherwise reachable only through the pinned dogfood fixture (att_secret, private owner-scoped: fixtures/attachments-fixture.ts). Run the recipe; fall back to the dogfood pin only if it cannot be provisioned, and record which one the verdict rests on.",
"[API half CLOSED by #9483] no stock seed mints an acl='public_read' attachments file (the anonymous-embed opt-out) — author one via a system write on a qa_vault-attached file (the recipe gives you the parent; the acl flip is still a system write) or accept the unit-test coverage in storage-routes.test.ts for that variant. What changed: attachments-public-read-acl.dogfood.test.ts performs that system flip at runtime on its own private-parent fixture and asserts both sides plus the flip back, so clause 3 no longer waits on a stock seed. Landing one in the showcase seeds would additionally unblock the BROWSER half (clause 4), which a runtime-only file cannot reach."
]
Expand Down Expand Up @@ -274,7 +274,7 @@
"why": "the whole item turns on a parent one persona cannot read; qa_vault (private + files) is that parent, and persona B is the restricted member. On stock seeds there is no such pair."
},
"knownGaps": [
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is cloud-private, so the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is open core since ADR-0132 (Apache-2.0, published on npm) — ⛔ it being closed-source is NOT the reason and has not been since #16215. The reason is ADR-0132's entitlement boundary: no framework package may declare `@objectstack/organizations` (pinned by `packages/plugins/organizations/src/no-framework-dependents.pin.test.ts` — apps declare it, packages do not), so `packages/qa/dogfood` cannot depend on it and the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
"CLOSED by the qa-scratch-authz recipe (#7670): stock showcase's files-enabled parent (showcase_project) is public_read_write — every member sees every project, so the invisible-parent case is not demonstrable on stock seeds, and the pinned dogfood matrix proves it only on its own private fixture (att_secret). The recipe provisions the private files-enabled parent at runtime; landing one in the showcase seeds proper would retire this gap entirely, and remains the better long-term fix.",
"the READ_SCAN_LIMIT fail-closed branch (clause 4) needs a broad read past the 2000-candidate cap — run #7635 reached it by bulking to 2100 join rows on the scratch parent. Budget for that write volume, or the clause degrades to 'the warning never appeared', which is indistinguishable from a silent leak."
]
Expand Down Expand Up @@ -355,7 +355,7 @@
"why": "persona A is provisioned read-but-NOT-edit precisely for the attach gate (the whole point of clause 0: read inherits parent READ, attach requires parent EDIT — two different gates), and qa_nofiles is the purpose-built FILES_DISABLED probe target for the last clause."
},
"knownGaps": [
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is cloud-private, so the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is open core since ADR-0132 (Apache-2.0, published on npm) — ⛔ it being closed-source is NOT the reason and has not been since #16215. The reason is ADR-0132's entitlement boundary: no framework package may declare `@objectstack/organizations` (pinned by `packages/plugins/organizations/src/no-framework-dependents.pin.test.ts` — apps declare it, packages do not), so `packages/qa/dogfood` cannot depend on it and the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
"clause 3 ('unscoped multi-delete refused outright', #4757) IS PINNED as of 2026-08-23. The product gap this entry used to record (#9719) was fixed by PR #9797 and extended to the update verb by #9974: the engine now offers an opt-in whole-operation dispatch (`dispatchUnscopedMultiWrite`) that fires ONCE, before any row is resolved and zero-match included, and attachment-access-hooks.ts declares it on BOTH sys_attachment write registrations — so the refusal answers with ATTACHMENT_DELETE_DENIED / 403 / the 'Refusing an unscoped multi-delete of attachments' message. ⭐ The durable lesson, which outlives the fix: a fixture whose rows SPLIT entitled/not cannot tell 'refused outright' from 'the per-row gate refused one of the rows' — both answer 403 ATTACHMENT_DELETE_DENIED, so the clause reads green either way. Measured on this suite while re-verifying: with the declaration removed from both registrations and service-storage rebuilt, the original split-fixture block stayed 5/5 GREEN. Score clause 3 ONLY from a caller entitled to EVERY matched row (or from the message text) — never from a split fixture. The REST lane still cannot express the shape at all (DeleteManyDataRequestSchema strips options.where and requires ids, #3897), so the reachable callers are server-side engine holders — flows, actions, scripts, MCP tools — which is why this clause's oracle is `test` rather than `api`.",
"the FILES_DISABLED create-side clause (acceptance clause 4) still probes showcase_account, which is files-disabled only incidentally — if the showcase ever enables files on it, that clause goes green for the wrong reason. The recipe's qa_nofiles exists to be probed instead; prefer it there too, and treat showcase_account as the fallback. (The new UPDATE-side clause 5 below already uses qa_nofiles, since it needed a real parent_id to re-point onto.)"
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
*
* The defect: the organizations load used a bare `import()`, which Node ESM
* resolves against the importer's own realpath — the CLI's, inside the
* framework workspace. `@objectstack/organizations` is cloud-private and only
* ever lives in the served app's `node_modules`, so the import could never
* framework workspace. `@objectstack/organizations` is host-supplied — ADR-0132's
* entitlement boundary forbids any framework package declaring it — so it only
* ever lives in the served app's `node_modules` and the import could never
* succeed: EVERY self-hosted deployment with `OS_TENANCY_POSTURE=group` or
* `isolated` hit the ADR-0093 D5 fail-fast and exited 1, and the only way past
* it was `OS_ALLOW_DEGRADED_TENANCY=1` — i.e. the unwalled state D5 exists to
Expand All @@ -21,9 +22,10 @@
* against a real app directory, with a real package in a real `node_modules`
* and nothing mocked, exercises it.
*
* The fixture stands in for the enterprise package (it is not installable in
* this workspace — that is the whole point), registering the same `org-scoping`
* service and posture entitlement the real one does. What is under test here is
* The fixture stands in for the enterprise package (`packages/cli` may not
* declare it — ADR-0132's entitlement boundary — so it does not resolve here;
* that is the whole point), registering the same `org-scoping` service and
* posture entitlement the real one does. What is under test here is
* RESOLUTION, not the enterprise semantics: proof that the real plugin walls
* tenants lives in cloud's security-enterprise multi-org integration test.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@
// fail-open path — which is what this file now forbids.
//
// ── What is observed, and why it is honest ──────────────────────────────────
// `@objectstack/organizations` is a cloud-private enterprise package genuinely
// absent from this workspace, so the dynamic import genuinely fails and the
// real stage-1 catch runs — no stubbing of the thing under test. That makes
// `@objectstack/organizations` is open core since ADR-0132 and IS a member of
// this workspace — ⛔ "absent from this workspace" is no longer the reason. The
// reason is ADR-0132's entitlement boundary: no framework package may declare it
// (`no-framework-dependents.pin.test.ts`), so it is genuinely unresolvable from
// `plugin-dev` and the dynamic import genuinely fails and the real stage-1 catch
// runs — no stubbing of the thing under test. ⚠️ Measured on this file's own
// runner: a bare ESM `import()` from here answers ERR_MODULE_NOT_FOUND "Cannot
// find package" with `packages/plugins/organizations/dist` BUILT and unbuilt
// alike, because Node's ESM resolver does not consult `NODE_PATH` and so never
// reaches pnpm's hoisted store — i.e. this signal is NOT a function of build
// state, unlike the `require`-shaped probe #16539 had to move off a workspace
// name. That makes
// this file the faithful witness for the ABSENT-package half of #4818's split.
// The PRESENT-but-refusing half needs the package to resolve, so it lives in
// `dev-plugin-tenancy-mount-refusal.test.ts`, which mocks it.
Expand All @@ -33,8 +42,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// timeout under a parallel `pnpm test`. Each factory throws the shape an absent
// package produces, so the graceful-degradation branches run for real with zero
// module resolution on the hot path. `@objectstack/organizations` is
// deliberately NOT listed: it is really absent, and its real failure is the
// signal this file reads.
// deliberately NOT listed: it really does fail to resolve from here, and that
// real failure is the signal this file reads.
vi.mock('@objectstack/objectql', () => { throw Object.assign(new Error("Cannot find package '@objectstack/objectql'"), { code: 'ERR_MODULE_NOT_FOUND' }); });
vi.mock('@objectstack/runtime', () => { throw Object.assign(new Error("Cannot find package '@objectstack/runtime'"), { code: 'ERR_MODULE_NOT_FOUND' }); });
vi.mock('@objectstack/driver-memory', () => { throw Object.assign(new Error("Cannot find package '@objectstack/driver-memory'"), { code: 'ERR_MODULE_NOT_FOUND' }); });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
// nothing about the plugin's internals.
//
// ── Why this file mocks what its sibling refuses to mock ────────────────────
// `dev-plugin-tenancy-failfast.test.ts` reads the ABSENT-package path off the
// genuinely-absent cloud-private package, stubbing nothing. That is impossible
// `dev-plugin-tenancy-failfast.test.ts` reads the ABSENT-package path off a
// package that genuinely does not resolve from a framework package — ADR-0132's
// entitlement boundary forbids any of them declaring `@objectstack/organizations`
// (`no-framework-dependents.pin.test.ts`), and the package being open core since
// ADR-0132 changes nothing about that — stubbing nothing. That is impossible
// here by construction: "the package is present and refused" cannot be observed
// without a present package. The mock therefore supplies only the thing the
// open-source workspace cannot have — a resolvable `@objectstack/organizations`
// without a present package. The mock therefore supplies only the thing no
// framework package may declare — a resolvable `@objectstack/organizations`
// — and the refusal semantics under test stay entirely in dev-plugin.ts.

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
// service "is the wall up?" would be circular.
//
// ── What is observed, and why it is honest ──────────────────────────────────
// `@objectstack/organizations` is a cloud-private enterprise package that is
// genuinely absent from this workspace, so the dynamic import genuinely fails
// `@objectstack/organizations` is open core since ADR-0132 and IS a member of
// this workspace, but ADR-0132's entitlement boundary forbids any framework
// package declaring it (`no-framework-dependents.pin.test.ts`), so it is
// genuinely unresolvable from `plugin-dev`: the dynamic import genuinely fails
// and the real catch branch runs. That makes the emitted warning a faithful
// witness for "the multi-org branch was ENTERED": under the bug there is no
// warning at all, because the `if` was never taken. The assertions therefore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
*
* `multiTenant: 'posture-only'` activates the tenancy POSTURE and no row wall
* (see `BootOptions.multiTenant`) — the enterprise `@objectstack/organizations`
* runtime is cloud-private and genuinely absent from this workspace. An
* runtime is open core since ADR-0132, but ADR-0132's entitlement boundary
* forbids any framework package declaring it, so it is genuinely absent from
* this package's dependency graph and unresolvable from here. An
* organization wall scopes ROWS, and the enabled bit is not a row:
* `toggleFlow(name, enabled)` writes the automation engine's in-process
* `flowEnabled` map, keyed by flow name and nothing else, `getFlowRuntimeStates()`
Expand Down
Loading
Loading