From 8598a8e63db7abcd113991a1f0f9035237b02dcc Mon Sep 17 00:00:00 2001 From: alfredorubin96 Date: Tue, 28 Jul 2026 20:49:31 +0200 Subject: [PATCH] =?UTF-8?q?docs(claude):=20the=20tenant=20guard=20shipped?= =?UTF-8?q?=20=E2=80=94=20stop=20saying=20nothing=20catches=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md:131 still read "a cross-tenant leak that nothing catches. Adding a guard is tracked in #1226." The guard shipped in #1351. That is worse than an ordinary stale doc because CLAUDE.md is loaded as ground truth by every agent session, and the claim is false in a direction that changes behaviour: an agent reading it either duplicates the guard or reasons more defensively than the code requires. What I deliberately did NOT do is soften the mandate. The first two clauses are load-bearing and stay verbatim: there is genuinely no ORM- or middleware- level enforcement, and the filter must still be written per query, in the route. The ratchet is a test-time safety net; an agent that believes the ORM scopes queries will write an unscoped one, which is a worse outcome than the stale sentence. The guard pins the reference by PATH, not by phrasing, so a future rewrite is free to reword but not to drop the pointer — and asserts the path exists, so renaming the test without updating the doc fails too. Proven in both directions: removing the path fails, and softening "not runtime enforcement" to "handled for you" fails. Closes #1355 Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 2 +- app/src/lib/__tests__/docs-accuracy.test.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index dc9ccfabe..e3638cb4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,7 +128,7 @@ Playwright E2E with **server-side coverage collection** (`collectServer: true` i ## Multi-Tenancy -- `tenant_id` column on ALL tables. Every DB query MUST include an explicit tenant filter — `eq(table.tenantId, session.tenantId)` — written **per query, in the route**. There is no ORM-level or middleware-level enforcement today (`app/src/lib/db/index.ts` is a plain Drizzle client), so a forgotten filter is a cross-tenant leak that nothing catches. Adding a guard is tracked in #1226. +- `tenant_id` column on ALL tables. Every DB query MUST include an explicit tenant filter — `eq(table.tenantId, session.tenantId)` — written **per query, in the route**. There is no ORM-level or middleware-level enforcement today (`app/src/lib/db/index.ts` is a plain Drizzle client), so a forgotten filter is a cross-tenant leak that the ORM will not catch. A test-time ratchet (`app/src/lib/db/__tests__/tenant-scope.test.ts`, #1226) fails the build on any unscoped query against a tenant table — it is a safety net, not runtime enforcement, so the per-query filter is still mandatory. - Take `tenantId` from `requireSession()`, NEVER from the request body. - JWT tokens include `tenantId` claim. Validate before ANY DB or API access. - SaaS vs on-prem: env vars only, never code branches. diff --git a/app/src/lib/__tests__/docs-accuracy.test.ts b/app/src/lib/__tests__/docs-accuracy.test.ts index 7b2558a0f..41a107785 100644 --- a/app/src/lib/__tests__/docs-accuracy.test.ts +++ b/app/src/lib/__tests__/docs-accuracy.test.ts @@ -102,6 +102,27 @@ describe("documentation accuracy", () => { expect(doc).toContain("there is no `--skip-migrations` CLI flag"); }); + it("CLAUDE.md points at the tenant guard by path, and that path exists", () => { + // The section used to say a forgotten tenant filter is "a leak that + // nothing catches. Adding a guard is tracked in #1226." The guard shipped + // (#1351), so that was false in a direction that changes behaviour: an + // agent reading it would either duplicate the guard or reason more + // defensively than the code requires (#1355). + // + // Pinned by PATH rather than by phrasing, so a rewrite that drops the + // pointer fails while a rewrite that keeps it is free to reword. + const doc = readDoc("CLAUDE.md"); + const guardPath = "app/src/lib/db/__tests__/tenant-scope.test.ts"; + expect(doc).toContain(guardPath); + expect(existsSync(resolve(REPO_ROOT, guardPath))).toBe(true); + + // The mandate itself is load-bearing and must survive any rewording: the + // ratchet is a test-time safety net, NOT runtime enforcement. An agent + // that believes the ORM scopes queries will write an unscoped one. + expect(doc).toMatch(/per query, in the route/); + expect(doc).toMatch(/not runtime enforcement/); + }); + it("the deploy skill does not send auditors looking for a flag that does not exist", () => { // CLAUDE.md was corrected but the deploy skill still listed // "`--skip-migrations` flag missing or undocumented" as a gap to capture