Skip to content

Commit e889386

Browse files
os-zhuangclaude
andauthored
fix(verify): multiTenant boot requests the isolated tenancy posture — ADR-0105 D1 (#3596)
Since #3559 a walled posture is an explicit operator request resolved from env when AuthPlugin registers the `tenancy` service; mounting the enterprise organizations plugin only ENTITLES it. bootStack's multiTenant opt-in predates that split and only mounted the plugin, so multi-org fixtures silently booted `single` — no Layer 0 wall, default-org write stamping — and every cross-tenant proof asserted against the wrong posture. First surfaced by cloud's security-enterprise multi-org integration test (the licensed path open-core CI cannot run), which fails 3/4 against any framework checkout at or past #3559. bootStack({multiTenant: true}) now sets OS_TENANCY_POSTURE=isolated for the boot — before AuthPlugin snapshots the requested posture — unless the caller already provided one; the request is restored on stop() and on a failed multi-tenant boot, so later single-tenant boots in the same worker are unaffected. The verify package also gains a `test` script: `turbo run test` never ran its suite before, so the new posture regression pin (and the existing derive tests) now actually gate. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c6c59f1 commit e889386

5 files changed

Lines changed: 167 additions & 2 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/verify': patch
3+
---
4+
5+
`bootStack({ multiTenant: true })` now REQUESTS the `isolated` tenancy posture
6+
for the boot (ADR-0105 D1), restoring the request on `stop()` and respecting an
7+
explicit caller-provided `OS_TENANCY_POSTURE`.
8+
9+
Since #3559 a walled posture is an explicit operator request resolved from env
10+
when AuthPlugin registers the `tenancy` service — mounting the enterprise
11+
organizations plugin only ENTITLES it. The harness's multi-tenant opt-in
12+
predates that split and only mounted the plugin, so multi-org fixtures silently
13+
booted `single`: no Layer 0 wall, D3 default-org write stamping, and every
14+
cross-tenant proof asserting against the wrong posture (first surfaced by
15+
cloud's security-enterprise multi-org integration test, which runs the licensed
16+
path open-core CI cannot).
17+
18+
The verify package also gains a `test` script so its suite actually runs under
19+
`turbo run test`, including the new regression pin for this contract.

packages/verify/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"scripts": {
1717
"build": "tsup --config ../../tsup.config.ts",
1818
"dev": "tsc -w",
19-
"lint": "eslint src"
19+
"lint": "eslint src",
20+
"test": "vitest run"
2021
},
2122
"dependencies": {
2223
"@objectstack/core": "workspace:*",
@@ -36,7 +37,8 @@
3637
},
3738
"devDependencies": {
3839
"@types/node": "^26.1.1",
39-
"typescript": "^6.0.3"
40+
"typescript": "^6.0.3",
41+
"vitest": "^4.1.10"
4042
},
4143
"keywords": [
4244
"objectstack",
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// bootStack({ multiTenant: true }) must REQUEST the `isolated` tenancy posture
4+
// (ADR-0105 D1). Since #3559, mounting the enterprise organizations plugin only
5+
// ENTITLES a walled posture — activation is an explicit operator request, read
6+
// from env when AuthPlugin registers the `tenancy` service. A harness that
7+
// mounts the plugin without requesting a posture silently boots `single` (no
8+
// wall, default-org write stamping) and every multi-org fixture asserts against
9+
// the wrong posture — the regression this file pins. The enterprise package is
10+
// not installable in this workspace, so a fake stands in for it, registering
11+
// the same `org-scoping` service + entitlement surface; the proof that the REAL
12+
// plugin walls tenants lives in cloud's security-enterprise multi-org
13+
// integration test.
14+
15+
import { describe, it, expect, vi, afterEach } from 'vitest';
16+
import { bootStack } from './harness';
17+
18+
class FakeOrganizationsPlugin {
19+
readonly name = 'fake-organizations';
20+
readonly version = '0.0.0';
21+
readonly supportedPostures = ['group', 'isolated'] as const;
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23+
async init(ctx: any): Promise<void> {
24+
ctx.registerService('org-scoping', this);
25+
}
26+
}
27+
28+
// The factory runs lazily, on the harness's dynamic import — after this module
29+
// body has executed, so referencing the class above is safe.
30+
vi.mock('@objectstack/organizations', () => ({
31+
OrganizationsPlugin: FakeOrganizationsPlugin,
32+
}));
33+
34+
const app = {
35+
manifest: {
36+
id: 'com.example.posture',
37+
namespace: 'posture',
38+
version: '0.0.1',
39+
type: 'app',
40+
name: 'Posture Fixture',
41+
},
42+
objects: [],
43+
};
44+
45+
interface TenancyShape {
46+
posture: string;
47+
requestedPosture: string;
48+
isolationActive: boolean;
49+
}
50+
51+
afterEach(() => {
52+
delete process.env.OS_TENANCY_POSTURE;
53+
});
54+
55+
// Each case boots the full in-process stack — well beyond the 5s default, and a
56+
// timed-out boot keeps running past its case, so its deferred stop() would race
57+
// the NEXT case's env expectations. Generous per-case timeouts keep the boots
58+
// inside their own cases.
59+
const BOOT_TIMEOUT = 120_000;
60+
61+
describe('bootStack multiTenant — tenancy posture request (ADR-0105 D1)', () => {
62+
it(
63+
'requests `isolated` for the boot and restores the env on stop()',
64+
async () => {
65+
expect(process.env.OS_TENANCY_POSTURE).toBeUndefined();
66+
const stack = await bootStack(app, { multiTenant: true });
67+
try {
68+
expect(process.env.OS_TENANCY_POSTURE).toBe('isolated');
69+
const tenancy = await stack.kernel.getServiceAsync<TenancyShape>('tenancy');
70+
expect(tenancy.requestedPosture).toBe('isolated');
71+
expect(tenancy.posture).toBe('isolated');
72+
expect(tenancy.isolationActive).toBe(true);
73+
} finally {
74+
await stack.stop();
75+
}
76+
expect(process.env.OS_TENANCY_POSTURE).toBeUndefined();
77+
},
78+
BOOT_TIMEOUT,
79+
);
80+
81+
it(
82+
'never overrides an explicit caller-provided OS_TENANCY_POSTURE',
83+
async () => {
84+
process.env.OS_TENANCY_POSTURE = 'group';
85+
const stack = await bootStack(app, { multiTenant: true });
86+
try {
87+
const tenancy = await stack.kernel.getServiceAsync<TenancyShape>('tenancy');
88+
expect(tenancy.requestedPosture).toBe('group');
89+
} finally {
90+
await stack.stop();
91+
}
92+
// stop() must not clear a posture the harness did not set.
93+
expect(process.env.OS_TENANCY_POSTURE).toBe('group');
94+
},
95+
BOOT_TIMEOUT,
96+
);
97+
98+
it(
99+
'restores the env when the enterprise package is missing and the boot throws',
100+
async () => {
101+
// `vi.resetModules()` alone cannot evict a `vi.mock` factory result, so
102+
// re-mock with `vi.doMock` (re-evaluated on the next import) and clear the
103+
// module cache: the harness's dynamic import now rejects like a genuinely
104+
// missing package.
105+
vi.resetModules();
106+
vi.doMock('@objectstack/organizations', () => {
107+
throw new Error("Cannot find module '@objectstack/organizations'");
108+
});
109+
await expect(bootStack(app, { multiTenant: true })).rejects.toThrow(
110+
/requires the enterprise @objectstack\/organizations/,
111+
);
112+
expect(process.env.OS_TENANCY_POSTURE).toBeUndefined();
113+
},
114+
BOOT_TIMEOUT,
115+
);
116+
});

packages/verify/src/harness.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export interface BootOptions {
8181
* `collectRLSPolicies`). This exercises the org-scoped isolation real apps
8282
* rely on, rather than the single-tenant default where every tenant policy is
8383
* stripped and a member sees every row. Default `false`.
84+
*
85+
* Also REQUESTS the `isolated` tenancy posture (ADR-0105 D1) for the boot,
86+
* unless the caller already set `OS_TENANCY_POSTURE` — mounting the plugin
87+
* entitles a walled posture but no longer activates one by itself.
8488
*/
8589
multiTenant?: boolean;
8690
/**
@@ -117,6 +121,25 @@ export async function bootStack(
117121
): Promise<VerifyStack> {
118122
process.env.NODE_ENV = 'development';
119123

124+
// [ADR-0105 D1] `multiTenant: true` REQUESTS the hard organization wall —
125+
// posture `isolated`, what `OS_MULTI_ORG_ENABLED=true` historically meant.
126+
// Since #3559 a walled posture is an explicit operator request resolved from
127+
// env when AuthPlugin registers the `tenancy` service; mounting the
128+
// enterprise plugin only ENTITLES it. Without the request the fixture
129+
// silently boots `single` — no wall, default-org write stamping — and every
130+
// multi-org proof asserts against the wrong posture. Must be set BEFORE
131+
// AuthPlugin snapshots the requested posture; an explicit caller-provided
132+
// OS_TENANCY_POSTURE wins; restored on stop() (and on a failed multi-tenant
133+
// boot) so later single-tenant boots in the same worker are unaffected.
134+
const prevTenancyPosture = process.env.OS_TENANCY_POSTURE;
135+
const requestIsolatedPosture = !!opts.multiTenant && !prevTenancyPosture;
136+
if (requestIsolatedPosture) process.env.OS_TENANCY_POSTURE = 'isolated';
137+
const restoreTenancyPosture = () => {
138+
if (!requestIsolatedPosture) return;
139+
if (prevTenancyPosture === undefined) delete process.env.OS_TENANCY_POSTURE;
140+
else process.env.OS_TENANCY_POSTURE = prevTenancyPosture;
141+
};
142+
120143
const kernel = new ObjectKernel();
121144

122145
// Data engine + in-memory SQLite (pure-JS WASM driver — no native build, CI-safe).
@@ -181,6 +204,7 @@ export async function bootStack(
181204
try {
182205
mod = await import(/* webpackIgnore: true */ organizationsPkg);
183206
} catch (e) {
207+
restoreTenancyPosture();
184208
throw new Error(
185209
'verify: multiTenant=true requires the enterprise @objectstack/organizations package (migrated from plugin-org-scoping, ADR-0081 D2). ' +
186210
`Install/link it in this workspace to run multi-org fixtures. (${(e as Error).message})`,
@@ -309,6 +333,7 @@ export async function bootStack(
309333
} catch {
310334
/* best-effort */
311335
}
336+
restoreTenancyPosture();
312337
};
313338

314339
return { kernel, api, raw, signIn, signUp, apiAs, stop };

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)