Building Relay

Phần 3 · Chương 3.25

Cột mốc: cửa ải cô lập tenant

Bạn sẽ tạo ra: Ba lần cố ý tái tạo lỗi — một lần vẫn xanh và dạy ta giới hạn của bộ kiểm thử · khoảng 13 phút, bao gồm bài tập

Tài liệu gốc: SRS — Đặc tả yêu cầu phần mềm · SAD — Tài liệu kiến trúc phần mềm (tiếng Anh)

Bản dịch đang được chuẩn bị. Phần diễn giải của chương này chưa được dịch sang tiếng Việt. Các khối mã bên dưới là bản gốc tiếng Anh và giống hệt bản tiếng Anh của chương — bạn có thể gõ theo chúng ngay bây giờ. Bản dịch đầy đủ sẽ thay thế trang này.

Has it ever caught anything?

× GET /v1/channels/:channelId/messages — a foreign channel reads as an absent one
× GET .../messages/:messageId/edits — a foreign message's history reads as an absent one
× PATCH .../messages/:messageId — a foreign message is not edited, and says so like an absent one
× DELETE .../messages/:messageId — a foreign message is not tombstoned
× POST /auth/dev-token — a token minted for one environment is refused by another

  "status 200 (foreign) vs 404 (absent)"
  "body {"messages":[{"id":"c80ae99d-…","user":"victim-mtv023g0-user",
   "text":"victim-mtv023g0 says something"…}]} (foreign) vs
   {"code":"not_found","message":"channel not found"…} (absent)"
× POST /v1/webhooks/:id/disable — refused on a foreign endpoint, and nothing moves
  AssertionError: the victim's endpoints moved: expected true to be false
// services/api/src/webhooks/webhooks.service.ts — DELIBERATELY UNTITLED.
// The fence chain replays every titled fence onto the repository, and this code
// was reverted the moment it was measured. A title here would put a
// reintroduction into the canonical tree, which is the one thing FR-015 forbids.
if (!row) {
  // T066's REINTRODUCTION — reverted immediately after measuring.
  if (await this.repo.anyEndpointExists(id)) {
    throw new ForbiddenException("that endpoint belongs to another environment");
  }
  throw new NotFoundException("no such webhook endpoint");
}
× POST /v1/webhooks/:id/enable  — refused on a foreign endpoint, and nothing moves
× POST /v1/webhooks/:id/disable — refused on a foreign endpoint, and nothing moves
  "status 403 (foreign) vs 404 (absent)"

The instruments had never produced output

services/api/src/isolation/attack.test.ts
import { afterEach, describe, expect, it, vi } from "vitest";
 
import { comparePair, credentialAttack, rowsOf } from "./attack";
 
// The oracle's REPORTING arm, which a passing gauntlet cannot reach.
//
// Every assertion in `gauntlet.itest.ts` asserts that `differences` is empty, so
// the code that builds a difference string only ever runs when the platform is
// broken. That is the same problem Phase 7's reintroductions solve for the suite
// as a whole, one layer down: an instrument that has never produced output has
// never had its output checked.
 
const answer = (status: number, body: unknown) => ({ status, body });
 
describe("comparing a foreign answer against an absent one", () => {
  it("reports nothing when both agree", () => {
    expect(comparePair(answer(404, { code: "not_found" }), answer(404, { code: "not_found" }))).toEqual(
      [],
    );
  });
 
  it("ignores request_id, which differs on every request by design", () => {
    expect(
      comparePair(
        answer(404, { code: "not_found", request_id: "a" }),
        answer(404, { code: "not_found", request_id: "b" }),
      ),
    ).toEqual([]);
  });
 
  it("names the statuses when they differ", () => {
    const [first] = comparePair(answer(403, {}), answer(404, {}));
    expect(first).toBe("status 403 (foreign) vs 404 (absent)");
  });
 
  it("names both bodies when they differ", () => {
    const differences = comparePair(answer(404, { code: "forbidden" }), answer(404, { code: "not_found" }));
    expect(differences).toHaveLength(1);
    expect(differences[0]).toContain("forbidden");
    expect(differences[0]).toContain("not_found");
  });
 
  it("reports both when both differ", () => {
    expect(comparePair(answer(200, { messages: [1] }), answer(404, { code: "not_found" }))).toHaveLength(
      2,
    );
  });
 
  it("compares a non-object body without throwing", () => {
    // `withoutRequestId` returns a non-object unchanged, and an html error page or
    // an empty string is a real answer a misconfigured route can give.
    expect(comparePair(answer(502, "bad gateway"), answer(404, ""))).toHaveLength(2);
  });
});
 
describe("counting the rows in a list answer", () => {
  it("reads a bare array", () => {
    expect(rowsOf([1, 2, 3])).toEqual([1, 2, 3]);
  });
 
  it("reads a paginated envelope", () => {
    expect(rowsOf({ data: [1, 2], next_cursor: "x" })).toEqual([1, 2]);
  });
 
  it("returns nothing for a shape it does not recognise", () => {
    // The arm that matters. Zero rows from an unknown shape looks exactly like
    // zero rows from a correctly-scoped list, and only one of those is a pass.
    expect(rowsOf({ code: "not_found" })).toEqual([]);
    expect(rowsOf(null)).toEqual([]);
    expect(rowsOf("an html error page")).toEqual([]);
    expect(rowsOf({ data: "not an array" })).toEqual([]);
  });
});
 
describe("a credential attack that could not even mint", () => {
  // THE ARM THAT REPORTS A FAILED MINT, which is the third instrument in this file
  // that a healthy lane never reaches. `credentialAttack` asks the api for a token
  // with the ATTACKER's own credential — a legitimate request that succeeds every
  // time the platform is up — and only then presents it against the victim.
  //
  // WHY IT MATTERS THAT THIS ARM IS RIGHT. If a broken mint returned the shape of a
  // success, every credential attack in the gauntlet would read `minted: true` and
  // compare a token that does not exist against a route that refused it for the wrong
  // reason. The suite would be green and would be asserting nothing — which is the
  // failure this whole chapter is about, arriving through the instrument rather than
  // through the platform.
  //
  // Driven with a stubbed `fetch`, because the only way to fail a mint against a live
  // api is to break the api.
  afterEach(() => {
    vi.unstubAllGlobals();
  });
 
  it("reports the mint's own status and body, and does not claim a token", async () => {
    vi.stubGlobal(
      "fetch",
      vi.fn(async () =>
        Promise.resolve({
          ok: false,
          status: 401,
          json: async () => Promise.resolve({ code: "unauthorized" }),
        }),
      ),
    );
 
    const verdict = await credentialAttack("http://unused.invalid", "rk_dev_bad", "mai", {
      method: "GET",
      path: "/v1/channels/whatever",
    });
 
    expect(verdict.minted, "a failed mint reported as a successful one").toBe(false);
    expect(verdict.crossStatus).toBe(401);
    expect(verdict.crossBody).toEqual({ code: "unauthorized" });
  });
});
services/api/src/isolation/targets.test.ts
import { describe, expect, it } from "vitest";
 
import { deriveTargets } from "./targets";
 
// THE DERIVATION'S SHAPE-HANDLING, driven with fakes.
//
// `targets.itest.ts` runs `deriveTargets` against a real Nest application, which
// is the assertion that matters — and a real application has exactly one router
// shape, so the fallbacks for the others are unreachable there. Express 4 exposed
// `_router`, Express 5 exposes `router`, and a future adapter may expose neither;
// the branch that reports `none` is the one a reader of a failure most needs to be
// working, because it is what turns "no routes found" into "no router found".
 
const route = (path: string, methods: Record<string, boolean>) => ({ route: { path, methods } });
 
describe("deriving targets from whatever the adapter exposes", () => {
  it("reads Express 5's `router`", () => {
    const result = deriveTargets({
      router: { stack: [route("/v1/x", { get: true })] },
    });
    expect(result.property).toBe("router");
    expect(result.targets).toEqual([{ method: "GET", path: "/v1/x" }]);
  });
 
  it("falls back to Express 4's `_router`", () => {
    const result = deriveTargets({
      _router: { stack: [route("/v1/y", { post: true })] },
    });
    expect(result.property).toBe("_router");
    expect(result.targets).toEqual([{ method: "POST", path: "/v1/y" }]);
  });
 
  it("says `none` rather than pretending the surface is empty", () => {
    // The distinction the suite depends on: an empty target list from a found
    // router is a clean surface, and an empty list from no router at all is a
    // broken derivation. Only this field tells them apart.
    const result = deriveTargets({});
    expect(result.property).toBe("none");
    expect(result.targets).toEqual([]);
  });
 
  it("counts middleware layers, which have no route", () => {
    const result = deriveTargets({
      router: { stack: [{}, {}, route("/v1/z", { delete: true })] },
    });
    expect(result.middlewareLayers).toBe(2);
    expect(result.targets).toEqual([{ method: "DELETE", path: "/v1/z" }]);
  });
 
  it("takes only the verbs a layer actually answers", () => {
    // Express marks every verb on the layer and flags the live ones. Treating the
    // keys as the answer would invent a target per HTTP verb per route.
    const result = deriveTargets({
      router: { stack: [route("/v1/w", { get: true, post: false, put: false })] },
    });
    expect(result.targets).toEqual([{ method: "GET", path: "/v1/w" }]);
  });
 
  it("handles a layer with a route but no methods, and one with no path", () => {
    const result = deriveTargets({
      router: { stack: [{ route: { path: "/v1/none" } }, { route: { methods: { get: true } } }] },
    });
    expect(result.targets).toEqual([{ method: "GET", path: "" }]);
  });
});
services/api/src/db/catalogue.test.ts
import { describe, expect, it } from "vitest";
 
import { classifyRow, SPINE_TABLES } from "./catalogue";
 
// The classification's four arms, driven with rows made up here rather than with
// rows a database happens to hold (FR-040).
//
// The one that matters is `null`. It executes only when somebody adds a table with
// no tenant path — which is the state `tenant-scope.itest.ts` exists to prevent —
// so against a healthy database it is the one arm nothing can reach. T042 proved it
// fires by creating a scratch table by hand; this proves it without a database and
// without a hand.
 
const row = (over: Partial<Parameters<typeof classifyRow>[0]>) => ({
  table_name: "made_up",
  has_environment_id: false,
  fk_targets: null,
  ...over,
});
 
describe("a table's tenant path", () => {
  it("is direct when it carries environment_id", () => {
    expect(classifyRow(row({ has_environment_id: true }))).toEqual({
      table: "made_up",
      path: "direct",
      via: [],
    });
  });
 
  it("is a hop when a foreign key reaches a direct table", () => {
    expect(classifyRow(row({ fk_targets: ["channels", "users"] }))).toEqual({
      table: "made_up",
      path: "hop",
      via: ["channels", "users"],
    });
  });
 
  it("prefers direct over hop when a table has both", () => {
    // The ordering the function's own comment argues for: a spine table that
    // gains `environment_id` should report `direct` so its SPINE entry becomes
    // visibly wrong rather than quietly ignored.
    const both = classifyRow(row({ has_environment_id: true, fk_targets: ["channels"] }));
    expect(both.path).toBe("direct");
  });
 
  it("is spine for a listed table, and carries its reason", () => {
    const result = classifyRow(row({ table_name: "organisations" }));
    expect(result.path).toBe("spine");
    expect(result.reason).toBeTruthy();
  });
 
  it("IS NULL for a table with no path at all", () => {
    expect(classifyRow(row({ table_name: "t042_scratch" }))).toEqual({
      table: "t042_scratch",
      path: null,
      via: [],
    });
  });
 
  it("gives every spine table a reason", () => {
    for (const name of SPINE_TABLES) {
      expect(classifyRow(row({ table_name: name })).reason, name).toBeTruthy();
    }
  });
});
services/api/src/isolation/compare.test.ts
import { describe, expect, it } from "vitest";
 
import { withoutRequestId } from "./compare";
 
// The oracle's own tests. Pure — no database, no HTTP — because the thing being
// checked is one comparison rule, and a rule that needs a stack to test is a rule
// nobody re-reads.
 
describe("withoutRequestId", () => {
  it("makes two bodies differing only in request_id equal", () => {
    const foreign = {
      code: "not_found",
      message: "channel not found",
      docs_url: "https://relay.example/docs/errors/not_found",
      request_id: "req_aaa",
    };
    const absent = { ...foreign, request_id: "req_bbb" };
    expect(withoutRequestId(foreign)).toEqual(withoutRequestId(absent));
  });
 
  it("leaves two bodies differing in message unequal", () => {
    // The case that matters: `messages.service.ts` keeps a CONSTANT message for
    // exactly this reason — echoing the id back would make the foreign answer
    // differ from the absent one, and "different" is itself a disclosure.
    const foreign = { code: "not_found", message: "channel abc not found", request_id: "req_a" };
    const absent = { code: "not_found", message: "channel not found", request_id: "req_b" };
    expect(withoutRequestId(foreign)).not.toEqual(withoutRequestId(absent));
  });
 
  it("leaves two bodies differing in code unequal", () => {
    const forbidden = { code: "forbidden", message: "no", request_id: "req_a" };
    const missing = { code: "not_found", message: "no", request_id: "req_b" };
    expect(withoutRequestId(forbidden)).not.toEqual(withoutRequestId(missing));
  });
 
  it("passes a non-object body through untouched", () => {
    // A 204 has no body and a proxy may hand back a string. Neither is an error
    // envelope, and neither should throw on the way to a comparison.
    expect(withoutRequestId(null)).toBeNull();
    expect(withoutRequestId("gateway timeout")).toBe("gateway timeout");
    expect(withoutRequestId(undefined)).toBeUndefined();
  });
 
  it("does not mutate its argument", () => {
    const body = { code: "not_found", request_id: "req_a" };
    withoutRequestId(body);
    expect(body.request_id).toBe("req_a");
  });
});

One rule, and a later block that replaced it

services/api/src/channels/channels.itest.ts, with `import { sql } from "drizzle-orm"`

  one block      error  'drizzle-orm' import is restricted  no-restricted-imports
  two blocks     error  'sql' is defined but never used     @typescript-eslint/no-unused-vars
eslint.config.mjs
@@ -1,11 +1,277 @@
 import eslint from "@eslint/js";
 import globals from "globals";
 import tseslint from "typescript-eslint";
 
 // One lint config for the whole workspace (ADR-01's consequence made literal).
+// ── THE RESTRICTION SETS, HOISTED SO THEY CAN BE COMPOSED ───────────────────────
+//
+// `no-restricted-imports` is ONE rule, and in flat config a later block REPLACES an
+// earlier block's setting for it rather than merging. Everything below exists because
+// of that sentence: a second block matching `**/*.itest.ts` — every one of which the
+// `**/*.ts` block already matched — switches the first block's rule OFF for every
+// integration test in the workspace, silently.
+//
+// MEASURED BEFORE IT WAS WRITTEN. `services/api/src/channels/channels.itest.ts` is on
+// no exemption list. Given `import { sql } from "drizzle-orm"` it fails lint under a
+// single block, and under a naively-added second block the only error left is
+// `'sql' is defined but never used`.
+const DRIVER_AND_ENGINE = {
+  paths: [
+    {
+      name: "pg",
+      message:
+        "Raw database access is forbidden outside services/api/src/db (constitution I).",
+    },
+    {
+      name: "drizzle-orm",
+      message:
+        "The query engine lives inside the repository layer only (constitution I, ADR-16).",
+    },
+    {
+      name: "ioredis",
+      message:
+        "The counter store lives in services/api/src/limits and services/gateway/src/limits.ts only (constitution I). Its keys are per environment; an unrestricted client is a cross-tenant read.",
+    },
+  ],
+  patterns: [
+    {
+      group: ["drizzle-orm/*"],
+      message:
+        "The query engine lives inside the repository layer only (constitution I, ADR-16).",
+    },
+  ],
+};
+
+// The paths excused from the driver and the engine. Two data-access LAYERS as
+// directories and everything else by path, each with the argument it needs.
+const DRIVER_EXEMPT = [
+    "services/api/src/db/**",
+    "services/api/src/limits/**",
+    // DRIVER_EXEMPT — every path below is exempt from all three restricted modules,
+    // the driver's name on the marker notwithstanding: `driver-exempt.test.ts` reads
+    // the module names out of the rule, so this list governs whatever the rule names.
+    //
+    // First the lane's own infrastructure. Reasons, one per path:
+    //   global-setup.ts  installs the guard against a database vitest names
+    //   setup.ts         rewrites the connection string to carry the exemption
+    //   guard.itest.ts   holds one exempt client and one plain one, and the
+    //                    difference between them is the whole test
+    "packages/test-harness/src/global-setup.ts",
+    "packages/test-harness/src/setup.ts",
+    "packages/test-harness/src/guard.itest.ts",
+  // AND THE LANE RESET'S OWN TEST, which arrives with the table it clears. It asserts
+  // what the SCRIPT DID — a planted stale delivery gone, an organisation count unmoved
+  // — and both are facts about rows the script reached through its own connection.
+  // Going through the repository layer would mean asserting the script's effect against
+  // the code the script does not use.
+  "packages/test-harness/src/reset-lane.itest.ts",
+    // AND TWO SUITES THAT WRITE A ROW THE TYPE SYSTEM FORBIDS.
+    //
+    //   backfill.itest.ts  asserts what `toFrame` does with a SENDERLESS message.
+    //                      Those rows exist — every one written through the socket
+    //                      before the sender was threaded looks like this — and the
+    //                      repository can no longer produce one, because `userId` is
+    //                      required. The fixture has to be raw SQL or the behaviour
+    //                      has no test at all.
+    //   history.itest.ts   reads the same row from the other end: a page whose
+    //                      `user` comes back null. FR-MSG-15 made `sendMessage`
+    //                      require a sender, so this suite lost the ability to build
+    //                      its own fixture in the same change that gave it the case.
+    //
+    // This is the exemption's honest case: not "the repository is inconvenient" but
+    // "the state under test is one the repository is now unable to reach". Both are
+    // listed by path rather than reached through a shared helper, because a helper in
+    // another file names none of these specifiers and this rule sees only imports —
+    // an invisible exemption is worse than a listed one.
+    "services/api/src/internal/backfill.itest.ts",
+    "services/api/src/messages/history.itest.ts",
+    // THE QUOTA CHAPTER'S PERIOD SUITE, and its case is the two above's in a third
+    // shape: the state under test is one the repository cannot reach. `periodOf`
+    // returns the month a timestamp falls in, and the property is that a row INSERTED
+    // under that value is FOUND by it — which needs a `usage_periods` row written
+    // directly, because every repository path that writes one derives the period from
+    // the clock and so cannot disagree with the function under test.
+    //
+    // A suite that used the repository here would be asserting that `periodOf` equals
+    // itself.
+    "services/api/src/quotas/period.itest.ts",
+    // AND THE QUOTA SUITE ITSELF, for a different reason from its sibling above.
+    // `period.itest.ts` writes a row the repository cannot; this one READS the two
+    // roll-up tables directly to check what a send left behind. Going through
+    // `usageFor` would mean asserting the roll-up against the function that reads
+    // it — the same circularity, one table over.
+    "services/api/src/quotas/quotas.itest.ts",
+    // AND THE CONNECTION-METERING CHAPTER'S, WHICH MAKES THE SAME CLAIM ONE
+    // DIMENSION OVER: a credited minute survives a `FLUSHALL` of the counter store,
+    // because a quota is about THIS MONTH and the rate limiter's store is allowed to
+    // lose things. Proving that needs the flush, and the flush needs a raw client.
+    //
+    // LISTED RATHER THAN DODGED. Published's version reached for
+    // `await import("ioredis")` inside the test, which this rule cannot see — an
+    // exemption that is invisible, which the note at the top of this block calls
+    // worse than a listed one. The static import puts it back under the rule and
+    // this entry is the answer.
+    "services/api/src/quotas/connections.itest.ts",
+    // ── AND EVERY OTHER REDIS CLIENT, BY PATH, WITH THE ARGUMENT IT NEEDS ──
+    //
+    // The rule arrives here and TWELVE files older than it already import `ioredis`.
+    // A missing exemption is not a silent one — this rule goes red on a chapter
+    // nobody is editing — so all of them land in the commit that adds the rule.
+    // FIVE DIFFERENT ARGUMENTS, and a blanket "the gateway's Redis files" would
+    // erase all five distinctions the rule exists to make.
+    //
+    // (1) NO KEY IS TOUCHED AT ALL — these name a pub/sub SUBJECT and never a key,
+    // which is the property, not whether they publish or subscribe. The subjects are
+    // `chan:{channel_id}`, `member:{channel_id}` and `typing:{channel_id}`: a channel
+    // UUID, and a subject is not readable at all, only listened to by whoever already
+    // subscribed. There is no key here for a cross-tenant read to reach. (The api's
+    // two publishers publish; the gateway's `membership.ts` only ever subscribes,
+    // because the api publishes that fabric — and the argument is the same either
+    // way.)
+    "services/api/src/fanout/publisher.ts",
+    "services/api/src/membership/publisher.ts",
+    //
+    // (2) THE COUNTER STORE'S OTHER HALF. `rl:{environment_id}:…` is the key shape
+    // the whole restriction is about, and this file composes it — so it is exempt as
+    // the rule's own subject, not against its reason. `limits.itest.ts` is listed
+    // beside it for something the rule cannot express at all: its subject is that
+    // the api and the gateway increment the SAME key, and the only way to check that
+    // is to read the key with NEITHER of their code.
+    "services/gateway/src/limits.ts",
+    "services/gateway/src/limits.itest.ts",
+    "services/gateway/src/fanout.ts",
+    // `member:{env}:{user}` — the principal-addressed half of that fabric — DOES
+    // carry an environment id, and that still does not make it the limiter's case:
+    // a subject is not readable, and the id is composed from the repository's own
+    // scope on the way out and from the authenticated connection's identity on the
+    // way in, never read from a payload.
+    "services/gateway/src/membership.ts",
+    // `typing.ts` both publishes and subscribes and composes no key at all — the
+    // environment travels INSIDE the payload, where the receiving gateway checks it
+    // against the connection it is about to act on.
+    "services/gateway/src/typing.ts",
+    //
+    // (2) KEYS ARE COMPOSED AND THEY ARE ENVIRONMENT-SCOPED — the limiter's own
+    // argument rather than the publishers'. `presence:{env}:{user}` is exactly the
+    // shape the restriction guards. Every key is composed from the environment id on
+    // the authenticated connection's own identity; no path takes one from a client,
+    // and there is no scan, `KEYS` or pattern read that could reach another tenant's.
+    "services/gateway/src/presence.ts",
+    //
+    // (3) THE ENVIRONMENT COMES FIRST IN THE KEY, which is the strongest case on
+    // this list rather than the weakest. `conn:{env}:{user}:{slot}` makes
+    // constitution I structural in the key itself: reaching across a tenant needs a
+    // caller to hand this module another environment's id, and the session layer
+    // takes that from the api's verified identity. The other entries argue about
+    // what they touch; this one cannot be wrong without being lied to.
+    "services/gateway/src/connections.ts",
+    //
+    // (4) THE SUBJECT IS WHAT REACHES THE FABRIC, so the oracle cannot be either
+    // service's own client. A spy on `createFanout` or on `createPresence` proves
+    // that an object was asked to publish, not that a frame arrived — and these
+    // suites' receive halves have rejection paths (a body that is not JSON, a body
+    // that is JSON and not a transition) that no module-level API can produce,
+    // because each only ever publishes payloads its own schema built.
+    "services/api/src/fanout/fanout.itest.ts",
+    "services/gateway/src/presence.itest.ts",
+    "services/gateway/src/membership.itest.ts",
+    "services/gateway/src/typing.itest.ts",
+    //
+    // (5) THE RAW CLIENT IS THE STIMULUS, NOT THE ORACLE — a fifth reason, and the
+    // rule cannot express it. This suite's subject is delivery and it asserts on
+    // sockets. It needs a client to CAUSE a membership change: `Membership` exposes
+    // `onChange`, `subscribeChannel` and `watch` and no `publish`, because the api
+    // publishes and the gateway only ever subscribes.
+    "services/gateway/src/connections.itest.ts",
+    //
+    // `services/gateway/src/connections.test.ts` IS DELIBERATELY ABSENT, and the
+    // ledger that owed these entries said to add it. It reads the module's own
+    // source off disk and imports nothing restricted, so the exemption would be one
+    // over nothing — and `driver-exempt.test.ts`'s stale-entry check is the half of
+    // this list that goes red when a listed file stops needing it.
+];
+
+// The suites that drive a global drain on purpose — derived by asking, not by
+// remembering: these are exactly the `*.itest.ts` files in this tree that import one of
+// the functions `GLOBAL_DRAINS` names, and `drain-exempt.test.ts` asserts that in both
+// directions against the tree rather than against a second list.
+const DRAIN_EXEMPT_TESTS = [
+  // `outboxDepth` — the relay's whole subject IS a global drain.
+  "services/api/src/outbox/outbox.itest.ts",
+  // `drainDueDeliveries`, `sweepDisabledEndpoints`, `pendingDeliveryDepth`.
+  "services/api/src/webhooks/deliveries.itest.ts",
+  // `drainDueDeliveries`.
+  "services/api/src/webhooks/attempts.itest.ts",
+  //
+  // THREE, AND PUBLISHED'S LIST IS SIX. `test-event.itest.ts`,
+  // `notifications.itest.ts` and `dispatcher.itest.ts` are on it there and import
+  // nothing restricted HERE: two name a drain only in prose explaining why they do not
+  // call one, and the dispatcher's suite declares `drainDueDeliveries` as a property on
+  // a stub it builds. Listing them would be three standing exemptions over nothing on
+  // the list's first day — the failure mode this file's own note calls out, arriving
+  // by inheritance rather than by drift.
+  //
+  // `drain-exempt.test.ts` found all three, which is the only reason the list is three
+  // long. It reads the names out of `DRAIN_NAMES` below and asserts both directions
+  // against the TREE.
+];
+
+/** The six functions, and the two counts that cannot be bounded. Named once so the
+ * two specifier spellings below cannot drift apart. */
+const DRAIN_NAMES = [
+  "drainOutbox",
+  "drainDueDeliveries",
+  "drainDisableNotifications",
+  "drainQuotaNotifications",
+  "sweepDisabledEndpoints",
+  "outboxDepth",
+  "pendingDeliveryDepth",
+];
+
+const DRAIN_MESSAGE =
+  "This claims or counts rows across EVERY environment. In an integration test that " +
+  "is a local assertion about a global operation, or a global operation over a " +
+  "neighbour's fixture. Scope the assertion to rows this test created, or add the " +
+  "suite to DRAIN_EXEMPT_TESTS with its reason.";
+
+// THE GLOBAL ADMIN FUNCTIONS, RESTRICTED IN INTEGRATION TESTS.
+//
+// Six recorded instances of one fault: a test asserts a local fact about a global
+// operation, or performs one and damages a neighbour's fixture. Each imported one of
+// these and called it as though the database held only its own rows.
+//
+// The two `*Depth` functions are here for a different reason from the other five. They
+// take no batch size and cannot — a count has nothing to bound — and a global count
+// compared against itself is the instance that appeared twice in one file, four
+// chapters apart.
+//
+// WHAT THIS DOES NOT CATCH, and must not be trusted to: an indirect call — a helper in
+// another file that calls the function, imported here under an innocent name — and raw
+// SQL, which names no import at all. Both are the sentinel trigger's job; it watches
+// STATEMENTS rather than imports. A rule trusted further than it goes is worse than no
+// rule.
+//
+// BOTH SPELLINGS, because `no-restricted-imports` matches the specifier as WRITTEN.
+// `../db/repository` and `./repository` are two rules, and the second is the one a
+// suite inside `services/api/src/db/` would use.
+const GLOBAL_DRAINS = {
+  paths: [
+    {
+      name: "../db/repository",
+      importNames: DRAIN_NAMES,
+      message: DRAIN_MESSAGE,
+    },
+    {
+      name: "./repository",
+      importNames: DRAIN_NAMES,
+      message: DRAIN_MESSAGE,
+    },
+  ],
+};
+
 export default tseslint.config(
   { ignores: ["**/node_modules/**", "**/dist/**", "**/coverage/**"] },
   eslint.configs.recommended,
   ...tseslint.configs.recommended,
   {
     // Dev scripts run on Node directly, outside any package's tsconfig —
@@ -39,185 +305,48 @@ export default tseslint.config(
     // file that imports the driver; nothing here can catch a LISTED file that stopped
     // importing it, so the list can only grow and a stale entry holds a standing
     // exemption forever. `driver-exempt.test.ts` reads this array and asserts each
     // path exists and still imports a module the rule below restricts — with those
     // module names read out of the rule rather than restated.
     files: ["**/*.ts"],
-    ignores: [
-      "services/api/src/db/**",
-      "services/api/src/limits/**",
-      // DRIVER_EXEMPT — every path below is exempt from all three restricted modules,
-      // the driver's name on the marker notwithstanding: `driver-exempt.test.ts` reads
-      // the module names out of the rule, so this list governs whatever the rule names.
-      //
-      // First the lane's own infrastructure. Reasons, one per path:
-      //   global-setup.ts  installs the guard against a database vitest names
-      //   setup.ts         rewrites the connection string to carry the exemption
-      //   guard.itest.ts   holds one exempt client and one plain one, and the
-      //                    difference between them is the whole test
-      "packages/test-harness/src/global-setup.ts",
-      "packages/test-harness/src/setup.ts",
-      "packages/test-harness/src/guard.itest.ts",
-      // AND THE LANE RESET'S OWN TEST, which arrives with the table it clears. It
-      // asserts what the SCRIPT DID — a stale pending backlog gone, an organisation
-      // count unmoved — and both are facts about rows the script reached through its
-      // own connection. Going through the repository layer would mean asserting the
-      // script's effect against the code the script does not use.
-      "packages/test-harness/src/reset-lane.itest.ts",
-      // AND TWO SUITES THAT WRITE A ROW THE TYPE SYSTEM FORBIDS.
-      //
-      //   backfill.itest.ts  asserts what `toFrame` does with a SENDERLESS message.
-      //                      Those rows exist — every one written through the socket
-      //                      before the sender was threaded looks like this — and the
-      //                      repository can no longer produce one, because `userId` is
-      //                      required. The fixture has to be raw SQL or the behaviour
-      //                      has no test at all.
-      //   history.itest.ts   reads the same row from the other end: a page whose
-      //                      `user` comes back null. FR-MSG-15 made `sendMessage`
-      //                      require a sender, so this suite lost the ability to build
-      //                      its own fixture in the same change that gave it the case.
-      //
-      // This is the exemption's honest case: not "the repository is inconvenient" but
-      // "the state under test is one the repository is now unable to reach". Both are
-      // listed by path rather than reached through a shared helper, because a helper in
-      // another file names none of these specifiers and this rule sees only imports —
-      // an invisible exemption is worse than a listed one.
-      "services/api/src/internal/backfill.itest.ts",
-      "services/api/src/messages/history.itest.ts",
-      // THE QUOTA CHAPTER'S PERIOD SUITE, and its case is the two above's in a third
-      // shape: the state under test is one the repository cannot reach. `periodOf`
-      // returns the month a timestamp falls in, and the property is that a row INSERTED
-      // under that value is FOUND by it — which needs a `usage_periods` row written
-      // directly, because every repository path that writes one derives the period from
-      // the clock and so cannot disagree with the function under test.
-      //
-      // A suite that used the repository here would be asserting that `periodOf` equals
-      // itself.
-      "services/api/src/quotas/period.itest.ts",
-      // AND THE QUOTA SUITE ITSELF, for a different reason from its sibling above.
-      // `period.itest.ts` writes a row the repository cannot; this one READS the two
-      // roll-up tables directly to check what a send left behind. Going through
-      // `usageFor` would mean asserting the roll-up against the function that reads
-      // it — the same circularity, one table over.
-      "services/api/src/quotas/quotas.itest.ts",
-      // AND THE CONNECTION-METERING CHAPTER'S, WHICH MAKES THE SAME CLAIM ONE
-      // DIMENSION OVER: a credited minute survives a `FLUSHALL` of the counter store,
-      // because a quota is about THIS MONTH and the rate limiter's store is allowed to
-      // lose things. Proving that needs the flush, and the flush needs a raw client.
-      //
-      // LISTED RATHER THAN DODGED. Published's version reached for
-      // `await import("ioredis")` inside the test, which this rule cannot see — an
-      // exemption that is invisible, which the note at the top of this block calls
-      // worse than a listed one. The static import puts it back under the rule and
-      // this entry is the answer.
-      "services/api/src/quotas/connections.itest.ts",
-      // ── AND EVERY OTHER REDIS CLIENT, BY PATH, WITH THE ARGUMENT IT NEEDS ──
-      //
-      // The rule arrives here and TWELVE files older than it already import `ioredis`.
-      // A missing exemption is not a silent one — this rule goes red on a chapter
-      // nobody is editing — so all of them land in the commit that adds the rule.
-      // FIVE DIFFERENT ARGUMENTS, and a blanket "the gateway's Redis files" would
-      // erase all five distinctions the rule exists to make.
-      //
-      // (1) NO KEY IS TOUCHED AT ALL — these name a pub/sub SUBJECT and never a key,
-      // which is the property, not whether they publish or subscribe. The subjects are
-      // `chan:{channel_id}`, `member:{channel_id}` and `typing:{channel_id}`: a channel
-      // UUID, and a subject is not readable at all, only listened to by whoever already
-      // subscribed. There is no key here for a cross-tenant read to reach. (The api's
-      // two publishers publish; the gateway's `membership.ts` only ever subscribes,
-      // because the api publishes that fabric — and the argument is the same either
-      // way.)
-      "services/api/src/fanout/publisher.ts",
-      "services/api/src/membership/publisher.ts",
-      //
-      // (2) THE COUNTER STORE'S OTHER HALF. `rl:{environment_id}:…` is the key shape
-      // the whole restriction is about, and this file composes it — so it is exempt as
-      // the rule's own subject, not against its reason. `limits.itest.ts` is listed
-      // beside it for something the rule cannot express at all: its subject is that
-      // the api and the gateway increment the SAME key, and the only way to check that
-      // is to read the key with NEITHER of their code.
-      "services/gateway/src/limits.ts",
-      "services/gateway/src/limits.itest.ts",
-      "services/gateway/src/fanout.ts",
-      // `member:{env}:{user}` — the principal-addressed half of that fabric — DOES
-      // carry an environment id, and that still does not make it the limiter's case:
-      // a subject is not readable, and the id is composed from the repository's own
-      // scope on the way out and from the authenticated connection's identity on the
-      // way in, never read from a payload.
-      "services/gateway/src/membership.ts",
-      // `typing.ts` both publishes and subscribes and composes no key at all — the
-      // environment travels INSIDE the payload, where the receiving gateway checks it
-      // against the connection it is about to act on.
-      "services/gateway/src/typing.ts",
-      //
-      // (2) KEYS ARE COMPOSED AND THEY ARE ENVIRONMENT-SCOPED — the limiter's own
-      // argument rather than the publishers'. `presence:{env}:{user}` is exactly the
-      // shape the restriction guards. Every key is composed from the environment id on
-      // the authenticated connection's own identity; no path takes one from a client,
-      // and there is no scan, `KEYS` or pattern read that could reach another tenant's.
-      "services/gateway/src/presence.ts",
-      //
-      // (3) THE ENVIRONMENT COMES FIRST IN THE KEY, which is the strongest case on
-      // this list rather than the weakest. `conn:{env}:{user}:{slot}` makes
-      // constitution I structural in the key itself: reaching across a tenant needs a
-      // caller to hand this module another environment's id, and the session layer
-      // takes that from the api's verified identity. The other entries argue about
-      // what they touch; this one cannot be wrong without being lied to.
-      "services/gateway/src/connections.ts",
-      //
-      // (4) THE SUBJECT IS WHAT REACHES THE FABRIC, so the oracle cannot be either
-      // service's own client. A spy on `createFanout` or on `createPresence` proves
-      // that an object was asked to publish, not that a frame arrived — and these
-      // suites' receive halves have rejection paths (a body that is not JSON, a body
-      // that is JSON and not a transition) that no module-level API can produce,
-      // because each only ever publishes payloads its own schema built.
-      "services/api/src/fanout/fanout.itest.ts",
-      "services/gateway/src/presence.itest.ts",
-      "services/gateway/src/membership.itest.ts",
-      "services/gateway/src/typing.itest.ts",
-      //
-      // (5) THE RAW CLIENT IS THE STIMULUS, NOT THE ORACLE — a fifth reason, and the
-      // rule cannot express it. This suite's subject is delivery and it asserts on
-      // sockets. It needs a client to CAUSE a membership change: `Membership` exposes
-      // `onChange`, `subscribeChannel` and `watch` and no `publish`, because the api
-      // publishes and the gateway only ever subscribes.
-      "services/gateway/src/connections.itest.ts",
-      //
-      // `services/gateway/src/connections.test.ts` IS DELIBERATELY ABSENT, and the
-      // ledger that owed these entries said to add it. It reads the module's own
-      // source off disk and imports nothing restricted, so the exemption would be one
-      // over nothing — and `driver-exempt.test.ts`'s stale-entry check is the half of
-      // this list that goes red when a listed file stops needing it.
-    ],
+    ignores: DRIVER_EXEMPT,
+
+    rules: {
+      "no-restricted-imports": ["error", DRIVER_AND_ENGINE],
+    },
+  },
+  {
+    // ── AND THE UNION, WHICH IS THE WHOLE REASON THE SETS ARE NAMED ─────────────
+    //
+    // Every `*.itest.ts` the block above matched as `**/*.ts` is matched again here, so
+    // this rule must be the UNION or the driver ban is switched off for all of them.
+    // The two exemption lists are ignored here and given their own single rule below,
+    // because `ignores` on the FIRST block cannot reach a rule the SECOND one sets.
+    files: ["**/*.itest.ts"],
+    ignores: [...DRAIN_EXEMPT_TESTS, ...DRIVER_EXEMPT],
     rules: {
       "no-restricted-imports": [
         "error",
         {
-          paths: [
-            {
-              name: "pg",
-              message:
-                "Raw database access is forbidden outside services/api/src/db (constitution I).",
-            },
-            {
-              name: "drizzle-orm",
-              message:
-                "The query engine lives inside the repository layer only (constitution I, ADR-16).",
-            },
-            {
-              name: "ioredis",
-              message:
-                "The counter store lives in services/api/src/limits and services/gateway/src/limits.ts only (constitution I). Its keys are per environment; an unrestricted client is a cross-tenant read.",
-            },
-          ],
-          patterns: [
-            {
-              group: ["drizzle-orm/*"],
-              message:
-                "The query engine lives inside the repository layer only (constitution I, ADR-16).",
-            },
-          ],
+          paths: [...DRIVER_AND_ENGINE.paths, ...GLOBAL_DRAINS.paths],
+          patterns: DRIVER_AND_ENGINE.patterns,
         },
       ],
     },
   },
+  {
+    // The driver-exempt paths keep their exemption and gain the drain rule. Without
+    // this block the union above would restore the ban they were excused from.
+    files: DRIVER_EXEMPT,
+    rules: {
+      "no-restricted-imports": ["error", GLOBAL_DRAINS],
+    },
+  },
+  {
+    // And the drain-exempt suites get the driver rule alone. They drive a global drain
+    // on purpose; they are excused from nothing else.
+    files: DRAIN_EXEMPT_TESTS,
+    rules: {
+      "no-restricted-imports": ["error", DRIVER_AND_ENGINE],
+    },
+  },
 );

What the suite does not cover

services/api/src/isolation/gauntlet.itest.ts (excerpt)
// TIMING. A foreign id answering in 3 ms and an absent id in 30 ms is a disclosure
// this suite cannot see. Measuring that stably in CI is a different discipline and
// is not attempted here; the chapter names it as unaddressed rather than implying
// it is covered.
//
// A LEAKED PLATFORM CREDENTIAL. FR-044 narrowed which routes each internal service
// may call, and that is all it did. There is no rotation, and `service` is
// self-reported by which variable matched — so the change shrinks the blast radius
// of a leak and does not make one survivable.
//
// MESSAGE CONTENT BEYOND EQUALITY. The pair proves the two answers match. It does
// not prove that what they say is wise: a constant message leaking a schema detail
// would pass every assertion below.