diff --git a/.changeset/memory-driver-tenancy-boot-guard.md b/.changeset/memory-driver-tenancy-boot-guard.md new file mode 100644 index 0000000000..d7dc3b98f1 --- /dev/null +++ b/.changeset/memory-driver-tenancy-boot-guard.md @@ -0,0 +1,64 @@ +--- +"@objectstack/driver-memory": minor +--- + +feat(driver-memory)!: declare the driver single-tenant and refuse to boot multi-tenant (#6915) + +`InMemoryDriver` implements **no row-level tenant isolation** — it never reads +`DriverOptions.tenantId`, so reads carry no tenant predicate and writes are not +stamped with a tenant column. The layer the SQL family has (`resolveTenantField` ++ `applyTenantScope`) does not exist here at all, which is why +`scripts/check-tenant-chokepoint.mjs` scans `driver-sql` / `driver-sqlite-wasm` / +`driver-turso` and not this package — and `distinct(object, field, query?)` does +not even accept a `DriverOptions`, so a caller has nowhere to pass a tenant even +deliberately. + +Everything above the driver assumes tenant isolation is a *platform* guarantee +(object metadata's `tenancy` block, `applySystemFields` injecting +`organization_id`, the engine threading `tenantId` into every driver call). So a +multi-tenant deployment backed by this driver did not fail — it served +cross-tenant reads, updates and deletes **silently**, the "declared ≠ enforced" +shape Prime Directive #10 forbids. + +It now refuses to run there, at startup, on two signals: + +- **Deployment posture** — `assertSingleTenantPosture()` reads the shared + `resolveTenancyPosture()` resolver (ADR-0105 D1), the canonical knob which also + subsumes the legacy `OS_MULTI_ORG_ENABLED` boolean, so the driver, auth, the + registry and the CLI can never disagree about the mode. Both walled postures + (`group` and `isolated`) need an organization wall this driver cannot draw, so + both are refused; only `single` passes. Called from the **constructor** and + re-checked in `connect()`. Both seams are load-bearing: `connect()` is what + `ObjectQLEngine.init()` turns into a boot-aborting `DriverConnectError` + (framework#3741), while the constructor is the seam no escape hatch reaches — + `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` downgrades a connect rejection to a warning + and would boot the deployment unisolated again. +- **Object metadata** — `assertObjectsNotTenantScoped()` refuses to sync an + object declaring `tenancy.enabled: true`, naming every offender in one message + so an operator fixes the whole set in one pass. Called from `syncSchema()`, + before the table is allocated. + +Both throw `MemoryMultiTenantUnsupportedError` with +`code === 'MEMORY_MULTI_TENANT_UNSUPPORTED'`, a message that names the detected +signal, the knobs that produced it, and `@objectstack/driver-sql` (including +`connection: { filename: ':memory:' }` as the closest in-process drop-in) as the +multi-tenant option. + +There is deliberately **no override env var**: an escape hatch would restore +exactly the silent non-isolation this guard removes. Single-tenant deployments — +the dev stack, the example apps, `@objectstack/verify`, and every in-process +embedding, none of which set a tenancy posture — are unaffected. + +This is option B of #6915, mirroring the guard #3724 landed on +`@objectstack/driver-mongodb`. Implementing real row-level isolation (option A) +stays behind the #5499 investment freeze; a startup refusal is not an investment +in this driver's capabilities, it is the removal of a silent failure mode +(maintainer ruling, 2026-08-12). + +Graded `minor` rather than `patch` for the same reason the sibling guard was: a +deployment that boots today can stop booting. It is a refusal that was always +owed, but it is still a behavior change, and the release notes must be able to +say so. + + + diff --git a/packages/drivers/driver-memory/package.json b/packages/drivers/driver-memory/package.json index 7273f15314..77e8ffe3eb 100644 --- a/packages/drivers/driver-memory/package.json +++ b/packages/drivers/driver-memory/package.json @@ -21,6 +21,7 @@ "dependencies": { "@objectstack/core": "workspace:*", "@objectstack/spec": "workspace:*", + "@objectstack/types": "workspace:*", "mingo": "^7.2.2" }, "devDependencies": { diff --git a/packages/drivers/driver-memory/src/index.ts b/packages/drivers/driver-memory/src/index.ts index 2cb5ec9766..d81d6a75e9 100644 --- a/packages/drivers/driver-memory/src/index.ts +++ b/packages/drivers/driver-memory/src/index.ts @@ -13,6 +13,15 @@ export type { MemoryAnalyticsConfig } from './memory-analytics.js'; export { InMemoryStrategy } from './in-memory-strategy.js'; +export { + MemoryMultiTenantUnsupportedError, + MULTI_TENANT_UNSUPPORTED_CODE, + assertSingleTenantPosture, + assertObjectsNotTenantScoped, + declaresTenantScope, +} from './memory-tenancy-guard.js'; +export type { TenancyAwareSchema } from './memory-tenancy-guard.js'; + export default { id: 'com.objectstack.driver.memory', version: '1.0.0', diff --git a/packages/drivers/driver-memory/src/memory-driver.ts b/packages/drivers/driver-memory/src/memory-driver.ts index 7957f9014d..28fc3dae19 100644 --- a/packages/drivers/driver-memory/src/memory-driver.ts +++ b/packages/drivers/driver-memory/src/memory-driver.ts @@ -13,6 +13,7 @@ import { hasDanglingLikeEscape, likePatternToRegexSource } from '@objectstack/sp import type { DriverQuery, IDataDriver } from '@objectstack/spec/contracts'; import { Logger, createLogger, nextUtcCalendarDay } from '@objectstack/core'; import { Query, Aggregator } from 'mingo'; +import { assertSingleTenantPosture, assertObjectsNotTenantScoped } from './memory-tenancy-guard.js'; import { getValueByPath } from './memory-matcher.js'; import { assertFilterConditionShape, @@ -156,6 +157,14 @@ export class InMemoryDriver implements IDataDriver { private persistenceAdapter: PersistenceAdapterInterface | null = null; constructor(config?: InMemoryDriverConfig) { + // #6915 — this driver has NO row-level tenant isolation, so it refuses a + // multi-tenant deployment outright rather than serving it unisolated. + // Construction is the earliest seam and the one behind no escape hatch: + // `connect()` re-checks (and is what aborts kernel bootstrap with this + // message), but `ObjectQLEngine.init()` downgrades a connect rejection to a + // warning under `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`, which would boot + // unisolated again. See `memory-tenancy-guard.ts`. + assertSingleTenantPosture(); this.config = config || {}; this.logger = config?.logger || createLogger({ level: 'info', format: 'pretty' }); this.logger.debug('InMemory driver instance created'); @@ -198,6 +207,12 @@ export class InMemoryDriver implements IDataDriver { // =================================== async connect() { + // #6915 — re-checked here (not just in the constructor) because a host may + // flip the posture between construction and connect, and because a rejection + // from here is what `ObjectQLEngine.init()` turns into a `DriverConnectError` + // that aborts kernel bootstrap (framework#3741). + assertSingleTenantPosture(); + // Initialize persistence adapter if configured await this.initPersistence(); @@ -1277,6 +1292,9 @@ export class InMemoryDriver implements IDataDriver { // =================================== async syncSchema(object: string, schema: any, options?: DriverOptions) { + // #6915 — metadata-level half of the tenancy guard: an object asking for + // row-level isolation cannot get it here, so the table is never allocated. + assertObjectsNotTenantScoped([{ object, schema }]); if (!this.db[object]) { this.db[object] = []; this.tablesCreatedHere.add(object); diff --git a/packages/drivers/driver-memory/src/memory-tenancy-guard.test.ts b/packages/drivers/driver-memory/src/memory-tenancy-guard.test.ts new file mode 100644 index 0000000000..921314e5e3 --- /dev/null +++ b/packages/drivers/driver-memory/src/memory-tenancy-guard.test.ts @@ -0,0 +1,225 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Multi-tenancy boot guard (#6915, mirroring driver-mongodb's #3724 guard). + * + * The guard is pure (env posture + object metadata), and this driver holds its + * store in a plain object, so nothing here needs a server. The driver-level + * cases assert two things at once: that the refusal fires at construction and + * at `connect()`, and — the risk this card carries — that the ordinary + * single-tenant in-process path the dogfood suites, `@objectstack/verify` and + * the example apps depend on still boots and serves clean. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { + assertSingleTenantPosture, + assertObjectsNotTenantScoped, + declaresTenantScope, + MemoryMultiTenantUnsupportedError, + MULTI_TENANT_UNSUPPORTED_CODE, +} from './memory-tenancy-guard.js'; +import { InMemoryDriver } from './memory-driver.js'; + +const ORIGINAL_MULTI_ORG = process.env.OS_MULTI_ORG_ENABLED; +const ORIGINAL_POSTURE = process.env.OS_TENANCY_POSTURE; + +function makeDriver() { + return new InMemoryDriver({ persistence: false }); +} + +describe('multi-tenancy boot guard (#6915)', () => { + beforeEach(() => { + delete process.env.OS_MULTI_ORG_ENABLED; + delete process.env.OS_TENANCY_POSTURE; + }); + + afterEach(() => { + if (ORIGINAL_MULTI_ORG === undefined) delete process.env.OS_MULTI_ORG_ENABLED; + else process.env.OS_MULTI_ORG_ENABLED = ORIGINAL_MULTI_ORG; + if (ORIGINAL_POSTURE === undefined) delete process.env.OS_TENANCY_POSTURE; + else process.env.OS_TENANCY_POSTURE = ORIGINAL_POSTURE; + }); + + describe('assertSingleTenantPosture', () => { + it('passes when nothing is configured (posture derives to `single`)', () => { + expect(() => assertSingleTenantPosture()).not.toThrow(); + }); + + it('passes when OS_MULTI_ORG_ENABLED is explicitly false', () => { + process.env.OS_MULTI_ORG_ENABLED = 'false'; + expect(() => assertSingleTenantPosture()).not.toThrow(); + }); + + it('passes for an explicit single posture', () => { + process.env.OS_TENANCY_POSTURE = 'single'; + expect(() => assertSingleTenantPosture()).not.toThrow(); + }); + + it('throws a coded error when multi-org mode is on', () => { + process.env.OS_MULTI_ORG_ENABLED = 'true'; + try { + assertSingleTenantPosture(); + expect.unreachable('expected the guard to throw'); + } catch (err) { + expect(err).toBeInstanceOf(MemoryMultiTenantUnsupportedError); + expect((err as any).code).toBe(MULTI_TENANT_UNSUPPORTED_CODE); + // The message must name the knobs and the escape route, not just fail. + expect((err as Error).message).toContain('OS_MULTI_ORG_ENABLED'); + expect((err as Error).message).toContain('OS_TENANCY_POSTURE'); + expect((err as Error).message).toContain('@objectstack/driver-sql'); + expect((err as Error).message).toContain('6915'); + } + }); + + it('treats any non-`false` value as enabled (matches resolveMultiOrgEnabled)', () => { + process.env.OS_MULTI_ORG_ENABLED = '1'; + expect(() => assertSingleTenantPosture()).toThrow(MemoryMultiTenantUnsupportedError); + }); + + // OS_TENANCY_POSTURE (ADR-0105 D1) supersedes the boolean — BOTH walled + // postures need an organization wall this driver cannot draw. + it.each(['isolated', 'group', 'multi'])( + 'throws for the `%s` posture even with OS_MULTI_ORG_ENABLED unset', + (posture) => { + process.env.OS_TENANCY_POSTURE = posture; + const err = (() => { + try { + assertSingleTenantPosture(); + return null; + } catch (e) { + return e; + } + })(); + expect(err).toBeInstanceOf(MemoryMultiTenantUnsupportedError); + // `multi` is the legacy alias, normalized to `isolated` by the resolver. + expect((err as Error).message).toContain(posture === 'multi' ? 'isolated' : posture); + }, + ); + }); + + describe('declaresTenantScope', () => { + it('is true only for an explicit tenancy.enabled === true', () => { + expect(declaresTenantScope({ name: 'task', tenancy: { enabled: true } })).toBe(true); + expect(declaresTenantScope({ name: 'task', tenancy: { enabled: false } })).toBe(false); + expect(declaresTenantScope({ name: 'task', tenancy: {} })).toBe(false); + expect(declaresTenantScope({ name: 'task' })).toBe(false); + expect(declaresTenantScope(null)).toBe(false); + expect(declaresTenantScope(undefined)).toBe(false); + }); + }); + + describe('assertObjectsNotTenantScoped', () => { + it('passes for objects that do not declare tenancy', () => { + expect(() => + assertObjectsNotTenantScoped([ + { object: 'task', schema: { name: 'task' } }, + { object: 'sys_license', schema: { name: 'sys_license', tenancy: { enabled: false } } }, + ]), + ).not.toThrow(); + }); + + it('names every offending object in a single message', () => { + try { + assertObjectsNotTenantScoped([ + { object: 'task', schema: { name: 'task' } }, + { object: 'account', schema: { name: 'account', tenancy: { enabled: true } } }, + { object: 'contact', schema: { name: 'contact', tenancy: { enabled: true } } }, + ]); + expect.unreachable('expected the guard to throw'); + } catch (err) { + expect((err as any).code).toBe(MULTI_TENANT_UNSUPPORTED_CODE); + const message = (err as Error).message; + expect(message).toContain('`account`'); + expect(message).toContain('`contact`'); + expect(message).not.toContain('`task`'); + // Plural remedy when there is more than one offender. + expect(message).toContain('these objects'); + } + }); + + it('stays singular for a lone offender — the shape `syncSchema` actually calls', () => { + try { + assertObjectsNotTenantScoped([ + { object: 'account', schema: { name: 'account', tenancy: { enabled: true } } }, + ]); + expect.unreachable('expected the guard to throw'); + } catch (err) { + const message = (err as Error).message; + expect(message).toContain('object declaring'); + expect(message).toContain('this object'); + } + }); + }); + + describe('InMemoryDriver wiring', () => { + it('the constructor refuses in multi-tenant mode', () => { + process.env.OS_MULTI_ORG_ENABLED = 'true'; + // Construction is the earliest seam, and the only one no escape hatch + // reaches: `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` downgrades a `connect()` + // rejection to a warning, which would boot the deployment unisolated. + expect(() => makeDriver()).toThrow(MemoryMultiTenantUnsupportedError); + }); + + it.each(['isolated', 'group'])('the constructor refuses the `%s` posture', (posture) => { + process.env.OS_TENANCY_POSTURE = posture; + expect(() => makeDriver()).toThrow(MemoryMultiTenantUnsupportedError); + }); + + it('connect() refuses when the posture flips after construction', async () => { + const driver = makeDriver(); // built single-tenant + process.env.OS_TENANCY_POSTURE = 'isolated'; + await expect(driver.connect()).rejects.toThrow(MemoryMultiTenantUnsupportedError); + }); + + it('syncSchema() refuses a tenant-scoped object, and allocates no table for it', async () => { + const driver = makeDriver(); + await driver.connect(); + await expect( + driver.syncSchema('account', { name: 'account', tenancy: { enabled: true } }), + ).rejects.toThrow(MemoryMultiTenantUnsupportedError); + // The refusal happens before the store is touched: reading the object back + // finds nothing was created for it. + const stats = driver.getSchemaSyncStats?.(); + expect(stats?.created ?? []).not.toContain('account'); + }); + }); + + // The risk this card carries is a guard that is too EAGER: `driver-memory` is + // the in-process store behind the dev stack, the example apps and every + // single-tenant embedding. None of those set a posture, so none of them may + // notice this guard exists. + describe('the ordinary single-tenant path still boots clean', () => { + it('constructs, connects, syncs and round-trips a record with no posture set', async () => { + const driver = makeDriver(); + await driver.connect(); + await driver.syncSchema('task', { + name: 'task', + fields: { title: { type: 'text' } }, + }); + await driver.create('task', { title: 'hello' }); + const rows = await driver.find('task', {}); + expect(rows).toHaveLength(1); + expect(rows[0].title).toBe('hello'); + await driver.disconnect?.(); + }); + + it('is unaffected by an explicit `single` posture', async () => { + process.env.OS_TENANCY_POSTURE = 'single'; + const driver = makeDriver(); + await expect(driver.connect()).resolves.not.toThrow(); + await expect( + driver.syncSchema('task', { name: 'task', fields: {} }), + ).resolves.not.toThrow(); + }); + + it('syncs an object that omits the tenancy block, and one that disables it', async () => { + const driver = makeDriver(); + await driver.connect(); + await expect(driver.syncSchema('task', { name: 'task' })).resolves.not.toThrow(); + await expect( + driver.syncSchema('sys_license', { name: 'sys_license', tenancy: { enabled: false } }), + ).resolves.not.toThrow(); + }); + }); +}); diff --git a/packages/drivers/driver-memory/src/memory-tenancy-guard.ts b/packages/drivers/driver-memory/src/memory-tenancy-guard.ts new file mode 100644 index 0000000000..9b0070c865 --- /dev/null +++ b/packages/drivers/driver-memory/src/memory-tenancy-guard.ts @@ -0,0 +1,164 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * In-Memory Driver — multi-tenancy boot guard (#6915, mirroring #3724). + * + * This driver implements **no row-level tenant isolation**: it never reads + * `DriverOptions.tenantId`, so reads carry no tenant predicate and writes are + * never stamped with a tenant column. The SQL family's `resolveTenantField()` + + * `applyTenantScope()` layer does not exist here at all — which is why + * `scripts/check-tenant-chokepoint.mjs` scans `driver-sql` / + * `driver-sqlite-wasm` / `driver-turso` and not this package: a driver that + * REFUSES multi-tenant has no read-side chokepoint for that gate to re-derive. + * `distinct(object, field, query?)` does not even accept a `DriverOptions`, so a + * caller has nowhere to pass a tenant even deliberately. + * + * The platform above the driver assumes tenant isolation is a *platform* + * guarantee (object metadata's `tenancy` block, `applySystemFields` injecting + * `organization_id`, the engine threading `tenantId` into every driver call). + * Booting this driver into a multi-tenant deployment therefore produces + * **silent** cross-tenant reads, updates and deletes — the exact + * "declared ≠ enforced" shape Prime Directive #10 forbids. + * + * So the driver refuses to run there. It is positioned as a **dev / demo / + * in-process** driver (#5704 moved the project's own test backends to sqlite + * `:memory:`) and fails fast — loudly, at startup — the moment it detects + * multi-tenant mode: + * + * 1. The deployment's tenancy posture is not `single` (deployment-level signal) + * → {@link assertSingleTenantPosture}, called from the `InMemoryDriver` + * **constructor** and re-checked in `connect()`. + * 2. An object declares `tenancy.enabled: true` (metadata-level signal) → + * {@link assertObjectsNotTenantScoped}, called from `syncSchema`. + * + * ## Why both seams, and not just one + * + * `connect()` alone is not enough: `ObjectQLEngine.init()` downgrades a driver's + * connect rejection to a warning when the operator sets + * `OS_ALLOW_DRIVER_CONNECT_FAILURE=1`, which would boot the deployment + * unisolated again — the precise failure this guard removes. Construction is + * behind no such hatch. `connect()` is kept because it is the seam that aborts + * kernel bootstrap with this message (framework#3741) and because it catches a + * host that flips the posture between construction and connect. + * + * There is deliberately **no escape-hatch env var of its own**: an override + * would restore exactly the silent non-isolation this guard exists to remove. + * Multi-tenant deployments use `@objectstack/driver-sql`, which implements + * driver-level tenant scoping. When real demand for in-memory multi-tenancy + * appears, the fix is to implement the isolation (option A in #6915) — not to + * weaken this gate. Route A stays behind the #5499 investment freeze; a startup + * refusal is not an investment in this driver's capabilities, it is the removal + * of a silent failure mode (maintainer ruling, 2026-08-12). + */ + +import { resolveTenancyPosture } from '@objectstack/types'; + +/** Stable, matchable error code for the boot refusal. */ +export const MULTI_TENANT_UNSUPPORTED_CODE = 'MEMORY_MULTI_TENANT_UNSUPPORTED'; + +const ISSUE_URL = 'https://github.com/objectstack-ai/objectstack/issues/6915'; + +/** + * Thrown when the in-memory driver is asked to run in a multi-tenant deployment. + * + * Carries {@link MULTI_TENANT_UNSUPPORTED_CODE} as `code` so hosts (CLI boot, + * runtime plugin loader, tests) can recognise it without string-matching the + * message or relying on cross-realm `instanceof`. + */ +export class MemoryMultiTenantUnsupportedError extends Error { + public readonly code = MULTI_TENANT_UNSUPPORTED_CODE; + + constructor(detected: string, remedy: string) { + super( + `[driver-memory] Refusing to start: this driver has NO row-level tenant isolation.\n` + + `\n` + + ` Detected: ${detected}\n` + + `\n` + + ` InMemoryDriver never reads \`DriverOptions.tenantId\` — reads carry no tenant\n` + + ` predicate and writes are not stamped with a tenant column, so queries would\n` + + ` read, update and delete OTHER tenants' records. Rather than run unisolated,\n` + + ` the driver fails at startup.\n` + + `\n` + + ` Fix one of:\n` + + ` • Use @objectstack/driver-sql (PostgreSQL / MySQL / SQLite) for multi-tenant\n` + + ` deployments — it enforces tenant scoping at the driver level. For an\n` + + ` in-process store, \`SqlDriver\` with \`connection: { filename: ':memory:' }\`\n` + + ` is the closest drop-in replacement.\n` + + ` ${remedy}\n` + + `\n` + + ` Tracking: ${ISSUE_URL}`, + ); + this.name = 'MemoryMultiTenantUnsupportedError'; + } +} + +/** Minimal shape of an object definition this guard inspects. */ +export interface TenancyAwareSchema { + tenancy?: { enabled?: boolean } | null; +} + +/** + * Whether an object definition asks for row-level tenant isolation. + * + * Only an **explicit** `tenancy.enabled === true` counts. An absent `tenancy` + * block is not treated as a multi-tenant signal here: platform-wide tenant + * scoping is driven by the deployment posture (checked separately by + * {@link assertSingleTenantPosture}), and every object in a single-tenant + * deployment omits the block. + */ +export function declaresTenantScope(schema: unknown): boolean { + return (schema as TenancyAwareSchema | null | undefined)?.tenancy?.enabled === true; +} + +/** + * Refuse to run unless the deployment's tenancy posture is `single`. + * + * Reads the posture through the shared `resolveTenancyPosture()` resolver + * (ADR-0105 D1) — the canonical knob, which also subsumes the legacy + * `OS_MULTI_ORG_ENABLED` boolean — so the driver, auth, the registry and the + * CLI can never disagree about the mode. Both walled postures (`group` and + * `isolated`) need an organization wall this driver cannot draw, so both are + * refused; only `single` passes. + */ +export function assertSingleTenantPosture(): void { + const posture = resolveTenancyPosture(); + if (posture === 'single') return; + throw new MemoryMultiTenantUnsupportedError( + `tenancy posture \`${posture}\` — a multi-tenant deployment ` + + '(from `OS_TENANCY_POSTURE`, or derived from `OS_MULTI_ORG_ENABLED`)', + '• Run this deployment single-tenant: `OS_TENANCY_POSTURE=single` (and unset\n' + + ' `OS_MULTI_ORG_ENABLED`, or set it to `false`).', + ); +} + +/** + * Refuse to sync object schemas that declare row-level tenant isolation. + * + * Reports **every** offending object in one message so an operator fixes the + * whole set in one pass instead of rediscovering them one boot at a time. + * + * This driver has no `syncSchemasBatch()` (it does not advertise + * `supports.batchSchemaSync`, so the engine syncs one object per call), which + * means the batch shape is reached one object at a time in practice. The + * array-taking signature is kept anyway: it is the precedent's shape, it is + * what makes the all-offenders-in-one-message property directly testable, and + * adding a batch path here would be capability investment in a driver whose + * capabilities are frozen (#5499). + */ +export function assertObjectsNotTenantScoped( + schemas: Array<{ object: string; schema: unknown }>, +): void { + const offenders = schemas + .filter(({ schema }) => declaresTenantScope(schema)) + .map(({ object }) => object); + + if (offenders.length === 0) return; + + const list = offenders.map((name) => `\`${name}\``).join(', '); + throw new MemoryMultiTenantUnsupportedError( + `object${offenders.length > 1 ? 's' : ''} declaring \`tenancy.enabled: true\`: ${list}`, + '• Drop the `tenancy` block from ' + + (offenders.length > 1 ? 'these objects' : 'this object') + + ' if the data is genuinely single-tenant.', + ); +} diff --git a/packages/drivers/driver-memory/vitest.config.ts b/packages/drivers/driver-memory/vitest.config.ts index 21bd9ebbb5..c5bcab95dd 100644 --- a/packages/drivers/driver-memory/vitest.config.ts +++ b/packages/drivers/driver-memory/vitest.config.ts @@ -11,6 +11,8 @@ export default defineConfig({ resolve: { alias: { '@objectstack/core': path.resolve(__dirname, '../../core/src/index.ts'), + // [ADR-0105 D1] `resolveTenancyPosture()`, read by the #6915 tenancy guard. + '@objectstack/types': path.resolve(__dirname, '../../types/src/index.ts'), '@objectstack/spec/api': path.resolve(__dirname, '../../spec/src/api/index.ts'), '@objectstack/spec/contracts': path.resolve(__dirname, '../../spec/src/contracts/index.ts'), '@objectstack/spec/data': path.resolve(__dirname, '../../spec/src/data/index.ts'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e94514aa56..9c7f027e4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -857,6 +857,9 @@ importers: '@objectstack/spec': specifier: workspace:* version: link:../../spec + '@objectstack/types': + specifier: workspace:* + version: link:../../types mingo: specifier: ^7.2.2 version: 7.2.2