Uh oh!
There was an error while loading. Please reload this page.
feat(driver-memory)!: refuse to boot into a multi-tenant deployment (#6915) - #7924
Conversation
…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 (`distinct()` does not even accept a `DriverOptions`). Everything above the driver assumes tenant isolation is a platform guarantee, so a multi-tenant deployment backed by this driver did not fail — it served cross-tenant reads, updates and deletes SILENTLY. Route B of #6915, mirroring the guard #3724 landed on driver-mongodb: * `assertSingleTenantPosture()` reads `resolveTenancyPosture()` (ADR-0105 D1) and refuses both walled postures. 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`, 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. * `assertObjectsNotTenantScoped()` refuses object schemas declaring `tenancy.enabled: true`, naming every offender in one message. Called from `syncSchema()` before the table is allocated. Both throw `MemoryMultiTenantUnsupportedError` (`code === 'MEMORY_MULTI_TENANT_UNSUPPORTED'`) with a message naming the detected signal, the knobs that produced it, and the multi-tenant alternative. No override env var: an escape hatch would restore exactly the silent non-isolation this removes. Route A (real row-level isolation) stays behind the #5499 investment freeze per the maintainer ruling of 2026-08-12 — a startup refusal is not an investment in the driver's capabilities, it is the removal of a silent failure mode. `driver-memory` stays outside `scripts/check-tenant-chokepoint.mjs`'s scan set: a driver that refuses multi-tenant has no read-side chokepoint to re-derive. `@objectstack/types` becomes a dependency of the package — the posture resolver is the only correct way to read the mode, and it was not previously reachable from here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7aqcZXdqX6yPMtbQWfASy
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
…memory tenancy guard (#6915) `no-migration-prescription`: the guard retires no authorable surface, and `tenancy.enabled: true` stays valid, honoured metadata everywhere the SQL family enforces it — so `objectstack migrate meta` has nothing to rewrite, and rewriting would silently disarm a real isolation declaration on the deployments that do enforce it. What the guard refuses is a DEPLOYMENT pairing, whose repair (switch drivers, or set the posture to `single`) depends on a fact only the operator holds and no ledger entry can state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7aqcZXdqX6yPMtbQWfASy
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6915
Route B per the maintainer ruling (
5261729371): a startup hard-fail when the deployment is multi-tenant, mirroringdriver-mongodb'smongodb-tenancy-guard.ts. ⛔ No row-level isolation (route A) — that stays behind the #5499 investment freeze.driver-memorystays out ofscripts/check-tenant-chokepoint.mjs's scan set.What landed
New
packages/drivers/driver-memory/src/memory-tenancy-guard.ts, same shape and layering as the sibling:driver-mongodb)driver-memory)MongoDBMultiTenantUnsupportedError/MONGODB_MULTI_TENANT_UNSUPPORTEDMemoryMultiTenantUnsupportedError/MEMORY_MULTI_TENANT_UNSUPPORTEDassertSingleTenantPosture()←resolveTenancyPosture()(ADR-0105 D1)connect()connect()assertObjectsNotTenantScoped()syncSchema+syncSchemasBatchsyncSchemaonly (no batch path here — see below)Both walled postures (
group,isolated) are refused; onlysinglepasses. There is deliberately no override env var — an escape hatch would restore exactly the silent non-isolation this removes.Two things the precedent reading needed correcting on
@objectstack/typeswas not already reachable.driver-mongodbdeclares it;driver-memorydid not. Added as aworkspace:*dependency (+ a vitest alias, + the mechanicalpnpm-lock.yamlline).resolveTenancyPosture()is still the only knob read — no env var is touched directly.Both call sites of the posture layer are load-bearing, for a reason the sibling's docstring only half-states.
connect()alone is not enough:ObjectQLEngine.init()downgrades a driver connect rejection to a warning underOS_ALLOW_DRIVER_CONNECT_FAILURE=1(engine.ts:5303-5306), which would boot the deployment unisolated again. The constructor is behind no such hatch.connect()is kept because it is the seam that aborts kernel bootstrap with this message (framework#3741) and it catches a host that flips the posture between construction and connect.No
syncSchemasBatch. This driver does not advertisesupports.batchSchemaSync, so the engine syncs one object per call.assertObjectsNotTenantScoped()keeps the array-taking signature anyway — it is the precedent's shape and it keeps the all-offenders-in-one-message property directly testable — but adding a batch path would be capability investment in a frozen driver.Gates
pnpm lintturbo run typecheck(whole repo)@objectstack/driver-memory@objectstack/verify@objectstack/dogfoodobjectql·runtime·spec·cli·metadata-protocol·plugin-auth·plugin-dev·service-datasource·service-analyticscheck:driver-conformancecheck:query-options-erasurecheck:tenant-chokepointdriver-memorycorrectly absent)check-driver-memory-censuscheck:error-code-casing,check:empty-changeset,check-changeset-no-majorNot breaking the single-tenant path
This is where the card could do damage, so it was measured rather than assumed. Intersecting "names
InMemoryDriver" with "setsOS_TENANCY_POSTURE/OS_MULTI_ORG_ENABLED" acrosspackages/apps/examples/scriptsyields six files: threeplugin-devtests thatvi.mockthe driver intoERR_MODULE_NOT_FOUND(never constructed), two that only mention it in prose, anddev-plugin.tsitself. Nothing in the repo runs the real driver under a walled posture. Three tests pin the clean path explicitly: construct → connect → sync → round-trip with no posture set, the same under an explicitsingle, and objects that omit or disable thetenancyblock.Changeset
minor, notpatch— a deployment that boots today can stop booting. Same grade the sibling guard took.Generated by Claude Code