@nodii/db-rls 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # @nodii/db-rls
2
2
 
3
- Placeholder package at v0.0.1. Implementation lands at v0.1.0 per the locked feature_doc.
3
+ Three-pool RLS dispatch + GUC binding + policy DDL generation for the Nodii
4
+ microservice stack.
4
5
 
5
6
  **Spec**: `https://planning.dev.nucleus-cloud.in/api/v1/feature-docs?serviceId=nodii-libs&docKey=db-rls`
7
+
8
+ ## Unforgeable tenant binding
9
+
10
+ On the **authenticated** path the tenant is not a parameter. It is read from a
11
+ request-scoped verified-claims context that auth middleware binds once:
12
+
13
+ ```ts
14
+ // ONCE, in auth middleware, immediately after JWT verification.
15
+ app.use(async (c, next) => {
16
+ const claims = await verifyJwt(c.req.header("authorization"));
17
+ return runWithVerifiedTenantClaims(
18
+ { userId: claims.sub, tenantId: claims.tenant_id },
19
+ () => next(),
20
+ );
21
+ });
22
+
23
+ // Everywhere else — no tenant, and nowhere to put one.
24
+ const rows = await withUserContextDbPostgresJs(async (db) =>
25
+ db.select().from(invoices),
26
+ );
27
+ ```
28
+
29
+ `withUserContext` / `withUserContextDb` / `withUserContextDbPostgresJs` take the
30
+ callback as their **only** argument. `autoDispatch(fn)` is the same path;
31
+ `autoDispatch({ systemTenantId }, fn)` is the system leg.
32
+
33
+ Why: the previous shape, `withUserContext({ userId, tenantId }, fn)`, let a
34
+ handler pass a request-derived tenant instead of the verified claim, and RLS
35
+ accepted `app.tenant_id` without ever proving the principal held it. Membership
36
+ was checked once at login in `nodii-auth`, and nowhere near the database.
37
+
38
+ **Workers, cron, S2S and webhooks are unaffected** — they have a legitimately
39
+ resolved tenant and no user claim, and keep their explicit parameter:
40
+ `withSystemContext({ tenantId }, fn)`. Migrations use `withOwnerContext`
41
+ (`NODII_ENV=migrate`-gated); Nodii staff cross-tenant access uses
42
+ `withPlatformOpsContext` (RBAC-gated, a different predicate).
43
+
44
+ There is **no escape hatch** — no option to override the tenant. Binding is
45
+ refused if claims are already bound (a handler cannot rebind to a different
46
+ tenant), bound claims are copied and frozen (they cannot be mutated after the
47
+ fact), and a blank / missing / non-string `userId` or `tenantId` is refused at
48
+ bind time rather than binding an empty GUC that reads as "no rows". With no
49
+ claims bound the authenticated helpers throw `NoVerifiedTenantClaims` before
50
+ acquiring a pool — never a default, never a fallback to a passed value.
51
+
52
+ ## The system pool is fenced against authenticated fall-through
53
+
54
+ The services-pool predicate is the bare `tenant_id = auth.get_tenant_id()` —
55
+ no membership proof. So an authenticated request that falls through to
56
+ `withSystemContext({ tenantId: req.body.tenantId }, fn)` re-opens exactly the
57
+ forgery the claims binding closes: the handler supplies the tenant, and
58
+ nothing checks the principal holds it. That fall-through is the seam where the
59
+ ₹250,000 cross-tenant refund reproduced.
60
+
61
+ Therefore, **inside a claims-bound request** (`hasVerifiedTenantClaims()` is
62
+ true), `withSystemContext` / `withSystemContextDb` /
63
+ `withSystemContextDbPostgresJs` (and `autoDispatch`'s system leg) **throw
64
+ `SystemContextUnderClaimsRefused`** — before any pool is acquired — unless the
65
+ caller passes an explicit, non-blank `{ reason }`:
66
+
67
+ ```ts
68
+ // Deliberate system work inside a request — named, counted, greppable.
69
+ await withSystemContext(
70
+ { tenantId, reason: "outbox-emit: invoice.settled fan-out" },
71
+ async (tx) => { ... },
72
+ );
73
+ ```
74
+
75
+ A sanctioned escape is emitted to telemetry as
76
+ `db_rls.cross_pool_nesting_total{outer="authenticated", inner="services",
77
+ reason}`, so every crossing is observable. **Outside any claims-bound context
78
+ (boot, cron, workers, S2S, webhooks) nothing changes** — no reason is needed
79
+ and the helpers behave exactly as before. That compat property is what keeps
80
+ this fence from breaking every worker in the fleet.
81
+
82
+ Full migration steps, including the `createSecureClient` before/after, are in
83
+ `.changeset/db-rls-unforgeable-tenant-binding.md`.
@@ -0,0 +1,100 @@
1
+ import { type SystemContextOpts, type VerifiedTenantClaims } from "./types.js";
2
+ /**
3
+ * Establish the request-scoped verified-claims context. Call this ONCE per
4
+ * request, from auth middleware, IMMEDIATELY after the JWT has been
5
+ * verified — and pass the values off the VERIFIED claim set, never off the
6
+ * request body / path / headers.
7
+ *
8
+ * Everything downstream (`withUserContext`, `withUserContextDb`,
9
+ * `withUserContextDbPostgresJs`, `autoDispatch`) reads the tenant from
10
+ * here. Nothing downstream can supply one.
11
+ *
12
+ * ```ts
13
+ * app.use(async (c, next) => {
14
+ * const claims = await verifyJwt(c.req.header("authorization"));
15
+ * return runWithVerifiedTenantClaims(
16
+ * { userId: claims.sub, tenantId: claims.tenant_id },
17
+ * () => next(),
18
+ * );
19
+ * });
20
+ * ```
21
+ *
22
+ * Returns whatever `fn` returns (including a promise), so it composes with
23
+ * Hono / Express / gRPC interceptor shapes unchanged.
24
+ *
25
+ * Throws {@link VerifiedTenantClaimsAlreadyBound} if claims are already
26
+ * bound in the current async context. Re-binding is refused
27
+ * UNCONDITIONALLY — including to identical claims. A nested bind is the
28
+ * one shape that could re-open the hole (bind in middleware, re-bind to a
29
+ * request-derived tenant in the handler), so it is not a thing that
30
+ * happens quietly. If you have hit this legitimately, the answer is to
31
+ * move the single bind to the outermost auth boundary, not to relax it.
32
+ *
33
+ * Throws {@link InvalidVerifiedTenantClaims} if `userId` / `tenantId` is
34
+ * missing, not a string, or blank.
35
+ */
36
+ export declare function runWithVerifiedTenantClaims<T>(claims: VerifiedTenantClaims, fn: () => T): T;
37
+ /**
38
+ * The verified claims bound to the current request, or `null` outside any
39
+ * bound context.
40
+ *
41
+ * This is the DIAGNOSTIC read (health probes, "am I on the authenticated
42
+ * path?" branches). Anything that is about to touch the database must use
43
+ * {@link requireVerifiedTenantClaims} so an unbound context fails closed
44
+ * instead of degrading into a nullish branch.
45
+ */
46
+ export declare function getVerifiedTenantClaimsOrNull(): VerifiedTenantClaims | null;
47
+ /** Whether verified claims are bound in the current async context. */
48
+ export declare function hasVerifiedTenantClaims(): boolean;
49
+ /**
50
+ * The verified claims bound to the current request, or THROW.
51
+ *
52
+ * Fail closed and loud: there is no default tenant, no fallback to a
53
+ * passed value, and no empty result. A silently-empty result is how the
54
+ * crm 85-policy blackout stayed invisible for weeks.
55
+ */
56
+ export declare function requireVerifiedTenantClaims(helper?: string): VerifiedTenantClaims;
57
+ /**
58
+ * Refuse the retired `helper(opts, fn)` call shape.
59
+ *
60
+ * TypeScript already rejects it — the helpers take the callback as their
61
+ * only argument, so all 271 call sites are compile errors until each one is
62
+ * examined. This is the runtime backstop for consumers that do not
63
+ * typecheck against this package (plain JS, a stale `dist/`, a `any`-typed
64
+ * wrapper): without it, the opts object would be invoked as the callback
65
+ * and the caller would get `opts is not a function`, which reads like a bug
66
+ * in db-rls rather than a migration they have not done.
67
+ */
68
+ export declare function assertNoTenantArgument(helper: string, fn: unknown): void;
69
+ /**
70
+ * THE STEP-1 FENCE on the system leg. Called by `withSystemContext`,
71
+ * `withSystemContextDb` and `withSystemContextDbPostgresJs` before they
72
+ * open a services-pool transaction.
73
+ *
74
+ * Three outcomes:
75
+ *
76
+ * - **No claims bound** (boot, cron, worker, S2S, webhook) → returns
77
+ * `null`. The system helpers behave EXACTLY as before — this is the
78
+ * compat property that keeps the fence from breaking every worker.
79
+ * - **Claims bound + `opts.reason` is a non-blank string** → returns
80
+ * `{ boundTenantId, reason }` so the caller can annotate telemetry
81
+ * (`db_rls.cross_pool_nesting_total{outer=authenticated,inner=services,
82
+ * reason}`). The escape is sanctioned, named, and observable.
83
+ * - **Claims bound + no reason** → THROWS
84
+ * {@link SystemContextUnderClaimsRefused}, BEFORE any pool is touched.
85
+ *
86
+ * WHY. The services-pool predicate is the bare
87
+ * `tenant_id = auth.get_tenant_id()` — it trusts the GUC completely and
88
+ * proves no membership. An authenticated request that falls through to
89
+ * `withSystemContext` therefore re-opens the exact hole the verified-claims
90
+ * binding closes on the authenticated pool: the handler supplies the
91
+ * tenant, and nothing checks the principal holds it. That fall-through is
92
+ * where the ₹250,000 cross-tenant refund reproduces. Requiring an explicit
93
+ * `{ reason }` turns a silent fall-through into a deliberate, reviewable,
94
+ * telemetry-visible act — and everything else into a throw.
95
+ */
96
+ export declare function assertSystemContextEscapeAllowed(helper: string, opts: SystemContextOpts): {
97
+ boundTenantId: string;
98
+ reason: string;
99
+ } | null;
100
+ //# sourceMappingURL=claims-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claims-context.d.ts","sourceRoot":"","sources":["../src/claims-context.ts"],"names":[],"mappings":"AAiDA,OAAO,EAIL,KAAK,iBAAiB,EAEtB,KAAK,oBAAoB,EAE1B,MAAM,YAAY,CAAC;AAuGpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAC3C,MAAM,EAAE,oBAAoB,EAC5B,EAAE,EAAE,MAAM,CAAC,GACV,CAAC,CAcH;AAED;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,IAAI,oBAAoB,GAAG,IAAI,CAE3E;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,CAAC,EAAE,MAAM,GACd,oBAAoB,CAMtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,GAAG,IAAI,CAIxE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,iBAAiB,GACtB;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAclD"}
@@ -0,0 +1,262 @@
1
+ // Request-scoped VERIFIED-claims context — the tenant binding for the
2
+ // authenticated pool.
3
+ //
4
+ // WHY THIS EXISTS
5
+ // ---------------
6
+ // Until this module, `withUserContext({ userId, tenantId }, fn)` took the
7
+ // tenant as a PARAMETER. RLS then trusted `app.tenant_id` completely: the
8
+ // authenticated-pool predicate resolves the session tenant and never proves
9
+ // the principal actually holds it (membership was proven exactly once, in
10
+ // nodii-auth at login, and nowhere near the database). So any handler that
11
+ // passed a REQUEST-DERIVED tenant — `req.body.tenantId`, a path param, a
12
+ // header — instead of the verified JWT claim silently defeated row-level
13
+ // security, and nothing in the stack caught it. 271 call sites across 11
14
+ // services were one line away from serving another tenant's rows.
15
+ //
16
+ // The fix is structural, not advisory: the tenant is no longer expressible
17
+ // as an argument. It is read from a request-scoped store that carries the
18
+ // VERIFIED claim set, established ONCE per request by auth middleware. A
19
+ // handler cannot pass a tenant, cannot rebind the store to a different
20
+ // tenant, and cannot mutate a bound claim set. There is no override option
21
+ // and no default — an unbound context throws.
22
+ //
23
+ // SCOPE — the AUTHENTICATED path, plus ONE fence on the system path.
24
+ // `withSystemContext({ tenantId }, fn)` (workers, cron, S2S, webhooks: a
25
+ // legitimately resolved tenant and no user claim) keeps its explicit
26
+ // parameter — with claims UNBOUND it is completely unchanged. But when it
27
+ // is called INSIDE a claims-bound request it now refuses unless the caller
28
+ // passes an explicit `{ reason }` (see
29
+ // {@link assertSystemContextEscapeAllowed}): the services-pool predicate is
30
+ // the bare `tenant_id = auth.get_tenant_id()` with no membership proof, so
31
+ // an authenticated request that falls through to it can aim the session at
32
+ // ANY tenant — the seam where the ₹250,000 cross-tenant refund reproduced.
33
+ // `withPlatformOpsContext` (Nodii staff, RBAC-gated by a different
34
+ // predicate against `platform_ops_grants_replica`) is not touched.
35
+ //
36
+ // SUBPATH-SPLIT SAFETY (@nodii/grpc-auth `user-context-als.ts` class).
37
+ // @nodii/db-rls ships five entry points (`.`, `/drizzle`,
38
+ // `/drizzle/platform-ops-grants`, `/postgres-js`, `/node-postgres`). Under
39
+ // plain `tsc` they share one `dist/claims-context.js`, but a bundler that
40
+ // splits per entry point (esbuild for Lambda) would DUPLICATE a
41
+ // module-level `new AsyncLocalStorage()`: middleware would bind on one
42
+ // copy while the secure-client factory read another. The single instance is
43
+ // therefore held on a non-configurable `globalThis` slot so every bundle
44
+ // copy in a realm shares it. Note the failure mode here is loud rather than
45
+ // silent — an unbound read throws `NoVerifiedTenantClaims` — but "loud in
46
+ // production" is not the bar; we want it to be impossible.
47
+ import { AsyncLocalStorage } from "node:async_hooks";
48
+ import { InvalidVerifiedTenantClaims, NoVerifiedTenantClaims, SystemContextUnderClaimsRefused, TenantArgumentRefused, VerifiedTenantClaimsAlreadyBound, } from "./types.js";
49
+ const SLOT = "__nodii_db_rls_verified_tenant_claims_als__";
50
+ function resolveStore() {
51
+ const realm = globalThis;
52
+ const existing = realm[SLOT];
53
+ if (existing) {
54
+ return existing;
55
+ }
56
+ const als = new AsyncLocalStorage();
57
+ // Non-writable + non-configurable: the store cannot be swapped out from
58
+ // under the fence at runtime (a swapped store is a tenant override with
59
+ // extra steps).
60
+ Object.defineProperty(realm, SLOT, {
61
+ value: als,
62
+ enumerable: false,
63
+ writable: false,
64
+ configurable: false,
65
+ });
66
+ return als;
67
+ }
68
+ const store = resolveStore();
69
+ function describe(value) {
70
+ if (value === null)
71
+ return "null";
72
+ if (typeof value === "string")
73
+ return `a string (${JSON.stringify(value)})`;
74
+ if (typeof value === "object") {
75
+ const keys = Object.keys(value);
76
+ return `an object with keys [${keys.join(", ")}]`;
77
+ }
78
+ return `a ${typeof value}`;
79
+ }
80
+ /**
81
+ * Every claim field is validated EXPLICITLY against `typeof` + blankness.
82
+ * There is deliberately no `value || fallback` / `value ?? fallback` here:
83
+ * a configured-but-falsy value quietly becoming a default is a failure mode
84
+ * this fleet has shipped more than once, and on this path the "default"
85
+ * would be a tenant.
86
+ */
87
+ function requireNonBlankString(field, value) {
88
+ if (typeof value !== "string") {
89
+ throw new InvalidVerifiedTenantClaims(field, `expected a non-blank string, received ${describe(value)}`);
90
+ }
91
+ if (value.trim() === "") {
92
+ throw new InvalidVerifiedTenantClaims(field, "received a blank string — an empty tenant binds an empty GUC and the RLS predicate then resolves nothing, which reads as 'no rows' rather than 'refused'");
93
+ }
94
+ return value;
95
+ }
96
+ /**
97
+ * Copy the caller's claim fields into a fresh FROZEN object.
98
+ *
99
+ * The copy is load-bearing, not hygiene. Without it a handler could hold a
100
+ * reference to the object the middleware bound and mutate `tenantId` after
101
+ * the fact — the store would hand the mutated tenant to the next
102
+ * `withUserContext`, which is precisely the forgery this module exists to
103
+ * make unrepresentable. Extra properties are dropped for the same reason.
104
+ */
105
+ function sealClaims(claims) {
106
+ if (claims === null || typeof claims !== "object") {
107
+ throw new InvalidVerifiedTenantClaims("claims", `expected an object with { userId, tenantId }, received ${describe(claims)}`);
108
+ }
109
+ const userId = requireNonBlankString("userId", claims.userId);
110
+ const tenantId = requireNonBlankString("tenantId", claims.tenantId);
111
+ const sealed = {
112
+ userId,
113
+ tenantId,
114
+ };
115
+ // `sourceIp` is forensic-only, so it stays optional — but if it is
116
+ // supplied it must be usable. Binding an empty `app.source_ip` GUC would
117
+ // put a blank string in the audit trail and read as "we recorded it".
118
+ if (claims.sourceIp !== undefined) {
119
+ sealed.sourceIp = requireNonBlankString("sourceIp", claims.sourceIp);
120
+ }
121
+ return Object.freeze(sealed);
122
+ }
123
+ /** Read a tenant off an UNVALIDATED object, for an error message only. */
124
+ function peekTenant(claims) {
125
+ if (claims !== null && typeof claims === "object") {
126
+ const candidate = claims.tenantId;
127
+ if (typeof candidate === "string" && candidate.trim() !== "") {
128
+ return candidate;
129
+ }
130
+ }
131
+ return "<unreadable>";
132
+ }
133
+ /**
134
+ * Establish the request-scoped verified-claims context. Call this ONCE per
135
+ * request, from auth middleware, IMMEDIATELY after the JWT has been
136
+ * verified — and pass the values off the VERIFIED claim set, never off the
137
+ * request body / path / headers.
138
+ *
139
+ * Everything downstream (`withUserContext`, `withUserContextDb`,
140
+ * `withUserContextDbPostgresJs`, `autoDispatch`) reads the tenant from
141
+ * here. Nothing downstream can supply one.
142
+ *
143
+ * ```ts
144
+ * app.use(async (c, next) => {
145
+ * const claims = await verifyJwt(c.req.header("authorization"));
146
+ * return runWithVerifiedTenantClaims(
147
+ * { userId: claims.sub, tenantId: claims.tenant_id },
148
+ * () => next(),
149
+ * );
150
+ * });
151
+ * ```
152
+ *
153
+ * Returns whatever `fn` returns (including a promise), so it composes with
154
+ * Hono / Express / gRPC interceptor shapes unchanged.
155
+ *
156
+ * Throws {@link VerifiedTenantClaimsAlreadyBound} if claims are already
157
+ * bound in the current async context. Re-binding is refused
158
+ * UNCONDITIONALLY — including to identical claims. A nested bind is the
159
+ * one shape that could re-open the hole (bind in middleware, re-bind to a
160
+ * request-derived tenant in the handler), so it is not a thing that
161
+ * happens quietly. If you have hit this legitimately, the answer is to
162
+ * move the single bind to the outermost auth boundary, not to relax it.
163
+ *
164
+ * Throws {@link InvalidVerifiedTenantClaims} if `userId` / `tenantId` is
165
+ * missing, not a string, or blank.
166
+ */
167
+ export function runWithVerifiedTenantClaims(claims, fn) {
168
+ const alreadyBound = store.getStore();
169
+ if (alreadyBound) {
170
+ throw new VerifiedTenantClaimsAlreadyBound(alreadyBound.tenantId, peekTenant(claims));
171
+ }
172
+ if (typeof fn !== "function") {
173
+ throw new TypeError("runWithVerifiedTenantClaims: the second argument must be the callback to run inside the bound context.");
174
+ }
175
+ return store.run(sealClaims(claims), fn);
176
+ }
177
+ /**
178
+ * The verified claims bound to the current request, or `null` outside any
179
+ * bound context.
180
+ *
181
+ * This is the DIAGNOSTIC read (health probes, "am I on the authenticated
182
+ * path?" branches). Anything that is about to touch the database must use
183
+ * {@link requireVerifiedTenantClaims} so an unbound context fails closed
184
+ * instead of degrading into a nullish branch.
185
+ */
186
+ export function getVerifiedTenantClaimsOrNull() {
187
+ return store.getStore() ?? null;
188
+ }
189
+ /** Whether verified claims are bound in the current async context. */
190
+ export function hasVerifiedTenantClaims() {
191
+ return store.getStore() !== undefined;
192
+ }
193
+ /**
194
+ * The verified claims bound to the current request, or THROW.
195
+ *
196
+ * Fail closed and loud: there is no default tenant, no fallback to a
197
+ * passed value, and no empty result. A silently-empty result is how the
198
+ * crm 85-policy blackout stayed invisible for weeks.
199
+ */
200
+ export function requireVerifiedTenantClaims(helper) {
201
+ const claims = store.getStore();
202
+ if (!claims) {
203
+ throw new NoVerifiedTenantClaims(helper);
204
+ }
205
+ return claims;
206
+ }
207
+ /**
208
+ * Refuse the retired `helper(opts, fn)` call shape.
209
+ *
210
+ * TypeScript already rejects it — the helpers take the callback as their
211
+ * only argument, so all 271 call sites are compile errors until each one is
212
+ * examined. This is the runtime backstop for consumers that do not
213
+ * typecheck against this package (plain JS, a stale `dist/`, a `any`-typed
214
+ * wrapper): without it, the opts object would be invoked as the callback
215
+ * and the caller would get `opts is not a function`, which reads like a bug
216
+ * in db-rls rather than a migration they have not done.
217
+ */
218
+ export function assertNoTenantArgument(helper, fn) {
219
+ if (typeof fn !== "function") {
220
+ throw new TenantArgumentRefused(helper, describe(fn));
221
+ }
222
+ }
223
+ /**
224
+ * THE STEP-1 FENCE on the system leg. Called by `withSystemContext`,
225
+ * `withSystemContextDb` and `withSystemContextDbPostgresJs` before they
226
+ * open a services-pool transaction.
227
+ *
228
+ * Three outcomes:
229
+ *
230
+ * - **No claims bound** (boot, cron, worker, S2S, webhook) → returns
231
+ * `null`. The system helpers behave EXACTLY as before — this is the
232
+ * compat property that keeps the fence from breaking every worker.
233
+ * - **Claims bound + `opts.reason` is a non-blank string** → returns
234
+ * `{ boundTenantId, reason }` so the caller can annotate telemetry
235
+ * (`db_rls.cross_pool_nesting_total{outer=authenticated,inner=services,
236
+ * reason}`). The escape is sanctioned, named, and observable.
237
+ * - **Claims bound + no reason** → THROWS
238
+ * {@link SystemContextUnderClaimsRefused}, BEFORE any pool is touched.
239
+ *
240
+ * WHY. The services-pool predicate is the bare
241
+ * `tenant_id = auth.get_tenant_id()` — it trusts the GUC completely and
242
+ * proves no membership. An authenticated request that falls through to
243
+ * `withSystemContext` therefore re-opens the exact hole the verified-claims
244
+ * binding closes on the authenticated pool: the handler supplies the
245
+ * tenant, and nothing checks the principal holds it. That fall-through is
246
+ * where the ₹250,000 cross-tenant refund reproduces. Requiring an explicit
247
+ * `{ reason }` turns a silent fall-through into a deliberate, reviewable,
248
+ * telemetry-visible act — and everything else into a throw.
249
+ */
250
+ export function assertSystemContextEscapeAllowed(helper, opts) {
251
+ const claims = store.getStore();
252
+ if (!claims)
253
+ return null;
254
+ const reason = opts?.reason;
255
+ // Explicit typeof + blankness — a blank reason is not a reason, and there
256
+ // is deliberately no `reason || default` on this path.
257
+ if (typeof reason !== "string" || reason.trim() === "") {
258
+ throw new SystemContextUnderClaimsRefused(helper, claims.tenantId, typeof opts?.tenantId === "string" ? opts.tenantId : "<unreadable>");
259
+ }
260
+ return { boundTenantId: claims.tenantId, reason };
261
+ }
262
+ //# sourceMappingURL=claims-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claims-context.js","sourceRoot":"","sources":["../src/claims-context.ts"],"names":[],"mappings":"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"}
package/dist/context.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type OwnerContextOpts } from "./migrate-guard.js";
2
- import { type AutoDispatchOpts, type PgTx, type PlatformOpsContextOpts, type SystemContextOpts, type UserContextOpts, type UserMembershipContextOpts } from "./types.js";
2
+ import { type AutoDispatchOpts, type PgTx, type PlatformOpsContextOpts, type SystemContextOpts, type UserMembershipContextOpts } from "./types.js";
3
3
  /** Test-only. Not exported through the public barrel. */
4
4
  export declare function _resetFenceGateForTests(): void;
5
5
  /**
@@ -8,13 +8,24 @@ export declare function _resetFenceGateForTests(): void;
8
8
  * `set_config(..., true)` tx-scoped GUCs, yields the tx, commits/rolls
9
9
  * back per the PgPool contract.
10
10
  *
11
+ * BREAKING (unforgeable tenant binding): the tenant is NO LONGER an
12
+ * argument. It is read from the request's verified claims, bound once by
13
+ * auth middleware via `runWithVerifiedTenantClaims`. The previous shape,
14
+ * `withUserContext({ userId, tenantId }, fn)`, let a handler pass a
15
+ * request-derived tenant instead of the verified claim and silently defeat
16
+ * RLS — the database layer accepted `app.tenant_id` without ever proving
17
+ * the principal held it. The callback is now the only argument, so every
18
+ * call site is a compile error until it has been examined.
19
+ *
11
20
  * On the FIRST call in a process it also runs the tenant-fence gate (see
12
21
  * {@link assertFenceOnce}) and refuses to yield the tx if the fence is broken.
13
22
  *
23
+ * Throws `NoVerifiedTenantClaims` if no claims are bound — never falls back
24
+ * to a default or to a passed value.
14
25
  * Throws `DbRlsNotInitialized` if `initDbRls` hasn't run.
15
26
  * Wraps any error thrown by `fn` in `TransactionRolledBack`.
16
27
  */
17
- export declare function withUserContext<T>(opts: UserContextOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
28
+ export declare function withUserContext<T>(fn: (tx: PgTx) => Promise<T>): Promise<T>;
18
29
  /**
19
30
  * @deprecated REMOVED — always throws {@link CrossTenantSessionRefused}.
20
31
  *
@@ -30,8 +41,11 @@ export declare function withUserContext<T>(opts: UserContextOpts, fn: (tx: PgTx)
30
41
  * task, got HTTP 200, and released a 250,000 INR refund. Rewriting predicates
31
42
  * while leaving this callable would have changed nothing for any caller of it.
32
43
  *
33
- * Migrate to `withUserContext({ userId, tenantId }, fn)` — the same pool, with
34
- * the tenant the request actually resolved to. If a caller genuinely needs to
44
+ * Migrate to `withUserContext(fn)` — the same pool, with the tenant the
45
+ * request's VERIFIED claims resolved to. That tenant is bound once by auth
46
+ * middleware (`runWithVerifiedTenantClaims`) and is no longer an argument at
47
+ * all, so the "pass the tenant the request resolved to" step this docblock
48
+ * used to ask for cannot be got wrong. If a caller genuinely needs to
35
49
  * know which tenants a user belongs to (a tenant switcher, a login
36
50
  * tenant-picker), query `auth.user_tenant_ids` directly from a services-pool
37
51
  * context; that function is deliberately left intact and still answers exactly
@@ -47,6 +61,22 @@ export declare function withUserMembershipContext<T>(_opts: UserMembershipContex
47
61
  * withSystemContext — opens a tx on the `services` (nodii_services) pool,
48
62
  * sets `app.tenant_id` via tx-scoped `set_config`, yields the tx, commits
49
63
  * / rolls back per the PgPool contract.
64
+ *
65
+ * THE STEP-1 FENCE (panel finding). The services-pool predicate is the bare
66
+ * `tenant_id = auth.get_tenant_id()` — no membership proof — so an
67
+ * authenticated request that falls through to this helper can aim the
68
+ * session at ANY tenant. That fall-through is the un-fenced seam where the
69
+ * ₹250,000 cross-tenant refund reproduced. Therefore:
70
+ *
71
+ * - **Claims UNBOUND** (boot, cron, workers, S2S, webhooks): unchanged.
72
+ * This is the compat property that keeps the fence from breaking every
73
+ * worker in the fleet.
74
+ * - **Claims BOUND** (`hasVerifiedTenantClaims()` is true): REFUSED with
75
+ * {@link SystemContextUnderClaimsRefused} — a throw, before any pool is
76
+ * acquired — unless the caller passes an explicit non-blank
77
+ * `{ reason }`. A sanctioned escape is emitted to telemetry as
78
+ * `db_rls.cross_pool_nesting_total{outer=authenticated,inner=services,reason}`
79
+ * so every crossing is named, counted, and audit-greppable.
50
80
  */
51
81
  export declare function withSystemContext<T>(opts: SystemContextOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
52
82
  /**
@@ -108,17 +138,28 @@ export declare function _resetWithPlatformOpsDeprecationWarnedForTests(): void;
108
138
  */
109
139
  export declare function withPlatformOps<T>(opts: PlatformOpsContextOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
110
140
  /**
111
- * autoDispatch — chooses the right pool based on what's bound.
112
- *
113
- * - `authContext` present → dispatch through withUserContext
114
- * (uses authContext.userId + authContext.tenantId).
115
- * - only `tenantId` present → dispatch through withSystemContext.
116
- * - neither → throw `MissingTenantContext`.
117
- *
118
- * This mirrors `withRequestContext` from the spec § 5.4 with one
119
- * simplification: the auth context is passed explicitly rather than
120
- * being read from ALS. Real auth-sdk integration lands in v0.1.x —
121
- * `// TODO db-rls-v0.1.x: ALS-bound auto-dispatch`.
141
+ * autoDispatch — chooses the pool from HOW IT IS CALLED, not from a bag of
142
+ * optional fields.
143
+ *
144
+ * - `autoDispatch(fn)` → authenticated pool, tenant from the request's
145
+ * verified claims. Throws `NoVerifiedTenantClaims` if none are bound.
146
+ * - `autoDispatch({ systemTenantId }, fn)` → services pool, explicit
147
+ * tenant. The system leg (workers, cron, S2S, webhooks) legitimately
148
+ * resolves its own tenant and keeps its parameter.
149
+ *
150
+ * This is the ALS-bound dispatch the original docblock deferred
151
+ * (`// TODO db-rls-v0.1.x: ALS-bound auto-dispatch`). The retired shape
152
+ * was `autoDispatch({ authContext?, tenantId? }, fn)` — `authContext` was
153
+ * assembled by the handler, so autoDispatch was a second way to hand the
154
+ * database a request-derived tenant. It is gone.
155
+ *
156
+ * The system leg dispatches through {@link withSystemContext}, so it
157
+ * inherits the step-1 fence: INSIDE a claims-bound request an explicit
158
+ * `systemTenantId` needs a non-blank `reason` too, or it throws
159
+ * `SystemContextUnderClaimsRefused`. There is deliberately no silent
160
+ * precedence rule — the caller either names the system leg (and, under
161
+ * claims, the reason for it) or takes the authenticated leg.
122
162
  */
163
+ export declare function autoDispatch<T>(fn: (tx: PgTx) => Promise<T>): Promise<T>;
123
164
  export declare function autoDispatch<T>(opts: AutoDispatchOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
124
165
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,gBAAgB,EAGrB,KAAK,IAAI,EACT,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC/B,MAAM,YAAY,CAAC;AAepB,yDAAyD;AACzD,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAyCD;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAmCZ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAC/C,KAAK,EAAE,yBAAyB,EAChC,GAAG,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5B,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,QAAQ,EAAE,gBAAgB,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,EACvD,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GACjC,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,IAAI,EAAE,sBAAsB,EAC5B,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAYZ;AAMD,4DAA4D;AAC5D,wBAAgB,8CAA8C,IAAI,IAAI,CAErE;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,sBAAsB,EAC5B,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAWZ;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,gBAAgB,EACtB,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAeZ"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAiBA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,gBAAgB,EAGrB,KAAK,IAAI,EACT,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAEtB,KAAK,yBAAyB,EAC/B,MAAM,YAAY,CAAC;AAepB,yDAAyD;AACzD,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAyCD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAmCZ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAC/C,KAAK,EAAE,yBAAyB,EAChC,GAAG,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5B,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,QAAQ,EAAE,gBAAgB,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,EACvD,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GACjC,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,IAAI,EAAE,sBAAsB,EAC5B,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAYZ;AAMD,4DAA4D;AAC5D,wBAAgB,8CAA8C,IAAI,IAAI,CAErE;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,sBAAsB,EAC5B,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAWZ;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAChF,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,gBAAgB,EACtB,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CAAC"}