From 003dc9928de0b9bbef25a263cfb8357e8c985fa7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 00:27:53 +0000 Subject: [PATCH 1/2] docs(organizations): state the entitlement boundary as the reason the cross-tenant proofs stand in ADR-0132 brought the multi-organization runtime back to open core (Apache-2.0, published on npm), which falsified the reason thirteen files gave for standing in for it: "the enterprise package is cloud-private / not installable in this workspace". The EFFECT those files describe is unchanged and every behaviour, every dependency and the pin stay exactly as they were. Only the stated cause moves, to the true one: ADR-0132's entitlement boundary forbids any framework package DECLARING `@objectstack/organizations`, pinned by `no-framework-dependents.pin.test.ts` ("Apps declare it; packages do not"), so a framework package cannot depend on it and cannot resolve it; the proof that the real plugin walls tenants lives in cloud's `security-enterprise` multi-org integration test. Sites were enumerated by subject + cause with context (`git grep -nE -C2 "cloud-private|not installable|closed-source"`) and classified per site; only the class that still stated the stale reason as currently true was edited. Measured while classifying: a bare ESM `import()` of the package from a framework package answers ERR_MODULE_NOT_FOUND "Cannot find package" with its `dist` built and unbuilt alike, so the plugin-dev fail-fast signal is not a function of build state. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude --- content/docs/plugins/packages.mdx | 2 +- .../areas/attachments-storage.json | 6 ++-- ...-organizations-host-resolution.e2e.test.ts | 12 ++++---- .../src/dev-plugin-tenancy-failfast.test.ts | 19 ++++++++---- .../dev-plugin-tenancy-mount-refusal.test.ts | 11 ++++--- .../src/dev-plugin-tenancy-posture.test.ts | 6 ++-- ...mation-toggle-tenant-scope.dogfood.test.ts | 4 ++- ...wcase-external-autoconnect.dogfood.test.ts | 4 ++- packages/types/src/node.test.ts | 3 +- .../src/harness.host-resolution.test.ts | 12 ++++---- .../verify/src/harness.posture-only.test.ts | 11 ++++--- packages/verify/src/harness.posture.test.ts | 12 +++++--- packages/verify/src/harness.ts | 29 ++++++++++++++----- 13 files changed, 87 insertions(+), 44 deletions(-) diff --git a/content/docs/plugins/packages.mdx b/content/docs/plugins/packages.mdx index 137e982351..76fa96c547 100644 --- a/content/docs/plugins/packages.mdx +++ b/content/docs/plugins/packages.mdx @@ -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 diff --git a/docs/qa/platform-checklist/areas/attachments-storage.json b/docs/qa/platform-checklist/areas/attachments-storage.json index e8aff08fb8..d33d74bfd4 100644 --- a/docs/qa/platform-checklist/areas/attachments-storage.json +++ b/docs/qa/platform-checklist/areas/attachments-storage.json @@ -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." ] @@ -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." ] @@ -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.)" ] diff --git a/packages/cli/test/serve-organizations-host-resolution.e2e.test.ts b/packages/cli/test/serve-organizations-host-resolution.e2e.test.ts index 0739621f5c..04a365e036 100644 --- a/packages/cli/test/serve-organizations-host-resolution.e2e.test.ts +++ b/packages/cli/test/serve-organizations-host-resolution.e2e.test.ts @@ -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 @@ -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. */ diff --git a/packages/plugins/plugin-dev/src/dev-plugin-tenancy-failfast.test.ts b/packages/plugins/plugin-dev/src/dev-plugin-tenancy-failfast.test.ts index f9877a7fe1..2b739caf2b 100644 --- a/packages/plugins/plugin-dev/src/dev-plugin-tenancy-failfast.test.ts +++ b/packages/plugins/plugin-dev/src/dev-plugin-tenancy-failfast.test.ts @@ -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. @@ -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' }); }); diff --git a/packages/plugins/plugin-dev/src/dev-plugin-tenancy-mount-refusal.test.ts b/packages/plugins/plugin-dev/src/dev-plugin-tenancy-mount-refusal.test.ts index 22b1dcabcb..9c4bbb12c5 100644 --- a/packages/plugins/plugin-dev/src/dev-plugin-tenancy-mount-refusal.test.ts +++ b/packages/plugins/plugin-dev/src/dev-plugin-tenancy-mount-refusal.test.ts @@ -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'; diff --git a/packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts b/packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts index 2ad36ceb8a..12d71616b2 100644 --- a/packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts +++ b/packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts @@ -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 diff --git a/packages/qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts b/packages/qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts index 04025af464..6cb5590d80 100644 --- a/packages/qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts +++ b/packages/qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts @@ -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()` diff --git a/packages/qa/dogfood/test/showcase-external-autoconnect.dogfood.test.ts b/packages/qa/dogfood/test/showcase-external-autoconnect.dogfood.test.ts index 509e292179..cf4f0aadc3 100644 --- a/packages/qa/dogfood/test/showcase-external-autoconnect.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-external-autoconnect.dogfood.test.ts @@ -75,7 +75,9 @@ describe('showcase: external datasource auto-connects with no onEnable bridge (A // Why this is deliberately left as-is (maintainer ruling, 2026-08-12 on // #7834): the federated-read × org-walled intersection is accepted as // covered at the unit/seam tier ONLY. The single honest walled harness is - // `multiTenant: true` with the cloud-private `@objectstack/organizations` + // `multiTenant: true` with the real `@objectstack/organizations`, which + // ADR-0132's entitlement boundary keeps out of every framework package's + // manifest — the package is open core, but only a HOST may declare it // (`'posture-only'` stamps nothing and scopes no query — see // `BootOptions.multiTenant` — so it would assert nothing here); its // feasibility in this repo is untested, and one intersection does not diff --git a/packages/types/src/node.test.ts b/packages/types/src/node.test.ts index 419d673b42..3d663ffee4 100644 --- a/packages/types/src/node.test.ts +++ b/packages/types/src/node.test.ts @@ -7,7 +7,8 @@ * The first defect: `serve` loaded `@objectstack/organizations` with a BARE * `import()`. Node ESM resolves that against the importer's own realpath — the * framework package's, inside the framework workspace — while the package is - * cloud-private and only ever exists in the host app's `node_modules`. It could + * host-supplied and only ever exists in the host app's `node_modules` (ADR-0132's + * entitlement boundary forbids any framework package declaring it). It could * therefore never resolve, and every walled tenancy posture died on the ADR-0093 * D5 fail-fast. #4700 found the same bare import in two more framework packages * (`@objectstack/verify`'s `bootStack`, the dogfood multi-org probes), which is diff --git a/packages/verify/src/harness.host-resolution.test.ts b/packages/verify/src/harness.host-resolution.test.ts index c89a1c1b1d..e4ea9ed7eb 100644 --- a/packages/verify/src/harness.host-resolution.test.ts +++ b/packages/verify/src/harness.host-resolution.test.ts @@ -6,8 +6,9 @@ * * The defect: the organizations load used a bare `import()`, which Node ESM * resolves against the importer's own realpath — `packages/verify`'s, inside the - * framework workspace. `@objectstack/organizations` is cloud-private and only - * ever lives in the verified 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 verified app's `node_modules` and the import could never * succeed: `objectstack verify --multi-tenant` (and every programmatic * `bootStack(app, { multiTenant: true })`) fell into the catch and told the * operator to "Install/link it in this workspace" — about a package the app had @@ -21,9 +22,10 @@ * These cases use a real temp app directory with a real `node_modules` and a * real stand-in package on disk, and mock nothing. * - * 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/verify` 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. */ diff --git a/packages/verify/src/harness.posture-only.test.ts b/packages/verify/src/harness.posture-only.test.ts index b143ece5ab..c8f67c27ff 100644 --- a/packages/verify/src/harness.posture-only.test.ts +++ b/packages/verify/src/harness.posture-only.test.ts @@ -1,7 +1,8 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. // // [#5261] `bootStack({ multiTenant: 'posture-only' })` — the stand-in that -// activates the tenancy POSTURE without the cloud-private enterprise runtime. +// activates the tenancy POSTURE without the enterprise runtime this package may +// not declare (ADR-0132's entitlement boundary). // // Why it exists: since #5261 the `organization/create` gate reads the EFFECTIVE // tenancy posture (the `tenancy` service's answer), not the operator's env @@ -9,9 +10,11 @@ // into minting organizations by any env combination. That deliberately closed // the trick `org-create-default-team.dogfood.test.ts` (#3624's e2e half) used to // open the route with — flipping `OS_MULTI_ORG_ENABLED` after boot — and the -// enterprise `@objectstack/organizations` package is not installable in this -// workspace, so the fixture needed an honest way to be a walled deployment -// rather than a single-tenant stack lying to one gate. +// enterprise `@objectstack/organizations` package may not be declared by any +// framework package — ADR-0132's entitlement boundary, pinned by +// `no-framework-dependents.pin.test.ts`; the package itself is open core and on +// npm — so the fixture needed an honest way to be a walled deployment rather +// than a single-tenant stack lying to one gate. // // ⚠️ Deliberately NO `vi.mock('@objectstack/organizations')` here — unlike // `harness.posture.test.ts`, which fakes the module to exercise the REAL diff --git a/packages/verify/src/harness.posture.test.ts b/packages/verify/src/harness.posture.test.ts index be69c2f333..be0de40034 100644 --- a/packages/verify/src/harness.posture.test.ts +++ b/packages/verify/src/harness.posture.test.ts @@ -7,10 +7,14 @@ // mounts the plugin without requesting a posture silently boots `single` (no // wall, default-org write stamping) and every multi-org fixture asserts against // the wrong posture — the regression this file pins. The enterprise package is -// not installable in this workspace, so a fake stands in for it, registering -// the same `org-scoping` service + entitlement surface; the proof that the REAL -// plugin walls tenants lives in cloud's security-enterprise multi-org -// integration test. +// open core since ADR-0132 (Apache-2.0, on npm) — ⛔ "not installable here" is +// no longer the reason. The reason is ADR-0132's entitlement boundary: no +// framework package may declare `@objectstack/organizations` +// (`no-framework-dependents.pin.test.ts` — "Apps declare it; packages do not"), +// so `packages/verify` cannot depend on it and a fake stands in for it, +// registering the same `org-scoping` service + entitlement surface; the proof +// that the REAL plugin walls tenants lives in cloud's security-enterprise +// multi-org integration test. import { describe, it, expect, vi, afterEach } from 'vitest'; import { bootStack } from './harness.js'; diff --git a/packages/verify/src/harness.ts b/packages/verify/src/harness.ts index 249ff636c5..adc271bd85 100644 --- a/packages/verify/src/harness.ts +++ b/packages/verify/src/harness.ts @@ -42,8 +42,18 @@ interface InjectableApp { } /** - * [#5261] Stand-in for the cloud-private `@objectstack/organizations` runtime, - * mounted by `bootStack({ multiTenant: 'posture-only' })`. + * [#5261] Stand-in for the `@objectstack/organizations` runtime, mounted by + * `bootStack({ multiTenant: 'posture-only' })`. + * + * ⚠️ Why a stand-in and not the real package: since ADR-0132 that runtime is + * OPEN CORE — Apache-2.0, published on npm — so "it is closed-source" is no + * longer the reason and has not been since #16215. What keeps it out of here is + * ADR-0132's entitlement boundary: no framework package may DECLARE + * `@objectstack/organizations` (`no-framework-dependents.pin.test.ts`, its + * mechanical half — "Apps declare it; packages do not"), so `packages/verify` + * cannot depend on it and a bare import from here does not resolve it. The proof + * that the REAL plugin walls tenants lives in cloud's `security-enterprise` + * multi-org integration test. * * It registers the `org-scoping` service and nothing else. That single fact is * what the open core reads to decide whether a REQUESTED organization wall can @@ -156,8 +166,9 @@ export interface BootOptions { * ## `'posture-only'` — a stand-in, for proving org LIFECYCLE without isolation * * `multiTenant: 'posture-only'` boots the same shape but registers a built-in - * stand-in for the `org-scoping` service instead of requiring the cloud-private - * enterprise package. The `tenancy` service then resolves a real, NON-DEGRADED + * stand-in for the `org-scoping` service instead of requiring the enterprise + * package that no framework package may declare (ADR-0132's entitlement + * boundary). The `tenancy` service then resolves a real, NON-DEGRADED * `isolated` posture, which is what posture-gated seams key on — above all * `POST /auth/organization/create`, which since #5261 refuses whenever the * EFFECTIVE posture has no organization wall. @@ -510,8 +521,9 @@ export async function bootStack( } else if (opts.multiTenant) { // #4700: this used a bare `import()`, which Node ESM resolves against the // IMPORTER's realpath — `packages/verify`, inside the framework workspace. - // `@objectstack/organizations` is cloud-private and only ever lives in the - // host app's `node_modules`, so the import could never succeed and the + // `@objectstack/organizations` is host-supplied — ADR-0132's entitlement + // boundary forbids any framework package declaring it, so it only ever lives + // in the host app's `node_modules` — and the import could never succeed: the // message below fired at apps that had already installed the package, // telling them to install it again. Resolve from the host app (the project // `objectstack verify` runs in) and fall back to this package's own @@ -531,8 +543,9 @@ export async function bootStack( // any caller until each one handed in its base. `(s) => import(s)` here is // literally this module's resolver, so the sentence now holds for // `bootStack`. Measured: it changes nothing for THIS specifier — - // `@objectstack/organizations` is cloud-private and resolves from nowhere - // in the framework workspace — and it is what stops the next app-supplied + // `@objectstack/organizations` resolves from nowhere in the framework + // workspace, because ADR-0132's entitlement boundary means no framework + // package declares it — and it is what stops the next app-supplied // package added to this path from silently missing `packages/verify`'s own // dependencies. const organizationsPkg = '@objectstack/organizations'; From 7990da9233af387b15acd8c5e8e748855087d450 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 00:36:06 +0000 Subject: [PATCH 2/2] chore(changeset): patch @objectstack/verify for the comment-only cause correction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/verify/src/harness.ts` is the one touched file inside a published `files[]` path whose bytes move: its TSDoc is carried into `dist/index.d.ts` (measured after a real build, with a positive control and a stale-cache probe). The other touched published packages — cli, plugin-dev, types — changed only `*.test.ts` files, which the shared tsup entry (`src/index.ts`) never reaches. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude --- .../17369-organizations-entitlement-boundary-prose.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/17369-organizations-entitlement-boundary-prose.md diff --git a/.changeset/17369-organizations-entitlement-boundary-prose.md b/.changeset/17369-organizations-entitlement-boundary-prose.md new file mode 100644 index 0000000000..dda4928577 --- /dev/null +++ b/.changeset/17369-organizations-entitlement-boundary-prose.md @@ -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.