@nodii/db-rls 0.10.0 → 0.12.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.
Files changed (44) hide show
  1. package/dist/auth-schema.d.ts +120 -0
  2. package/dist/auth-schema.d.ts.map +1 -1
  3. package/dist/auth-schema.js +177 -0
  4. package/dist/auth-schema.js.map +1 -1
  5. package/dist/context.d.ts +26 -8
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/context.js +35 -24
  8. package/dist/context.js.map +1 -1
  9. package/dist/drizzle-client.d.ts +17 -5
  10. package/dist/drizzle-client.d.ts.map +1 -1
  11. package/dist/drizzle-client.js +19 -18
  12. package/dist/drizzle-client.js.map +1 -1
  13. package/dist/index.d.ts +6 -4
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +20 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/postgres-js-client.d.ts +17 -10
  18. package/dist/postgres-js-client.d.ts.map +1 -1
  19. package/dist/postgres-js-client.js +19 -21
  20. package/dist/postgres-js-client.js.map +1 -1
  21. package/dist/rls-policy-gen.d.ts +43 -27
  22. package/dist/rls-policy-gen.d.ts.map +1 -1
  23. package/dist/rls-policy-gen.js +40 -15
  24. package/dist/rls-policy-gen.js.map +1 -1
  25. package/dist/rls-policy.d.ts +11 -1
  26. package/dist/rls-policy.d.ts.map +1 -1
  27. package/dist/rls-policy.js +31 -4
  28. package/dist/rls-policy.js.map +1 -1
  29. package/dist/tenant-fence.d.ts +165 -0
  30. package/dist/tenant-fence.d.ts.map +1 -0
  31. package/dist/tenant-fence.js +542 -0
  32. package/dist/tenant-fence.js.map +1 -0
  33. package/dist/types.d.ts +18 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js +22 -0
  36. package/dist/types.js.map +1 -1
  37. package/dist/verify-rls-cli.d.ts.map +1 -1
  38. package/dist/verify-rls-cli.js +86 -10
  39. package/dist/verify-rls-cli.js.map +1 -1
  40. package/dist/verify-rls.d.ts +26 -5
  41. package/dist/verify-rls.d.ts.map +1 -1
  42. package/dist/verify-rls.js +73 -5
  43. package/dist/verify-rls.js.map +1 -1
  44. package/package.json +2 -2
@@ -5,6 +5,125 @@ import type { Pool, PoolClient } from "pg";
5
5
  * / Go ports (asserted by the parity-fence CI).
6
6
  */
7
7
  export declare const AUTH_SCHEMA_SQL = "CREATE SCHEMA IF NOT EXISTS auth;\n\nCREATE OR REPLACE FUNCTION auth.set_user_id(p_user_id uuid) RETURNS void\nLANGUAGE sql AS $$ SELECT set_config('app.user_id', p_user_id::text, true) $$;\n\nCREATE OR REPLACE FUNCTION auth.set_tenant_id(p_tenant_id uuid) RETURNS void\nLANGUAGE sql AS $$ SELECT set_config('app.tenant_id', p_tenant_id::text, true) $$;\n\nCREATE OR REPLACE FUNCTION auth.init(p_user_id uuid, p_tenant_id uuid) RETURNS void\nLANGUAGE sql AS $$\n SELECT set_config('app.user_id', p_user_id::text, true);\n SELECT set_config('app.tenant_id', p_tenant_id::text, true);\n$$;\n\nCREATE OR REPLACE FUNCTION auth.init_tenant(p_tenant_id uuid) RETURNS void\nLANGUAGE sql AS $$ SELECT set_config('app.tenant_id', p_tenant_id::text, true) $$;\n\nCREATE OR REPLACE FUNCTION auth.get_user_id() RETURNS uuid\nLANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.user_id', true), '')::uuid $$;\n\nCREATE OR REPLACE FUNCTION auth.get_tenant_id() RETURNS uuid\nLANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.tenant_id', true), '')::uuid $$;\n\n-- Stub variant. Replaced in-place by migrate-gen's 0001_user_tenant_ids.sql\n-- once the service has employees_mirror + customers_mirror tables.\nCREATE OR REPLACE FUNCTION auth.user_tenant_ids(p_user_id uuid) RETURNS SETOF uuid\nLANGUAGE sql STABLE AS $$ SELECT NULL::uuid WHERE false $$;\n";
8
+ /**
9
+ * `auth.session_tenant_ids()` — THE authenticated-pool resolver.
10
+ *
11
+ * Returns AT MOST ONE tenant: the one bound to `app.tenant_id`, and only if the
12
+ * principal in `app.user_id` genuinely holds it per `auth.user_tenant_ids`.
13
+ * Edge-signed session tenant **INTERSECT** server-resolved membership.
14
+ *
15
+ * WHY THIS EXISTS. The `authenticated` predicate used to be
16
+ * `tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))` — the UNION
17
+ * of every tenant the user belongs to, with no tenant fence at all. A session
18
+ * opened for a user with memberships in tenants A and B could read AND write
19
+ * both. Reproduced end-to-end: a legitimate tenant-A session transitioned a
20
+ * tenant-B approval task, got HTTP 200, and released a 250,000 INR refund.
21
+ *
22
+ * WHY SUBSTITUTION RATHER THAN APPENDING A FENCE. The obvious repair is to
23
+ * append `AND tenant_id = auth.get_tenant_id()` at the top level of each
24
+ * policy. That breaks every policy whose shape is not a bare tenant equality,
25
+ * and the fleet is full of them:
26
+ *
27
+ * - tables with NO `tenant_id` column that reach it through a nested
28
+ * `EXISTS` or a sub-`SELECT` (`tenant_role_permissions`, `fx_rate_cache`)
29
+ * - platform-default escape hatches — `tenant_id IS NULL OR tenant_id IN (…)`
30
+ * (hr's statutory masters, `pricing_snapshots`)
31
+ * - additive ORs on a differently-named column — `is_public_listing = TRUE OR
32
+ * developer_tenant_id IN (…)` (`oauth_apps` marketplace listings)
33
+ * - policies fencing on a column that is not `tenant_id` at all (`tenants.id`,
34
+ * `legal_documents.scope`, `impersonation_sessions.target_tenant_id`)
35
+ * - TEXT-vs-uuid casts that need fixing in opposite directions per table
36
+ *
37
+ * Swapping the resolver INSIDE the subquery leaves the column, the cast and the
38
+ * surrounding OR untouched, so all of those keep working while the set the
39
+ * subquery yields narrows from "every tenant you belong to" to "the one you are
40
+ * logged into, if you really belong to it". One substitution, zero shape
41
+ * changes, and NO `AS RESTRICTIVE` (which would AND against the additive
42
+ * permissive master policies and kill every platform-default row).
43
+ *
44
+ * `user_tenant_ids` is deliberately LEFT INTACT. It still answers the honest
45
+ * question "which tenants does this user belong to", which a tenant-switcher or
46
+ * a login tenant-picker legitimately needs. Overloading it with session scope
47
+ * would have silently broken those callers with no error and no test coverage.
48
+ *
49
+ * SECURITY DEFINER is required, not decorative:
50
+ * - `authenticated` has no privilege on the membership mirror, so an INVOKER
51
+ * function raises `permission denied for table user_memberships`;
52
+ * - if the mirror is itself RLS-policied, an INVOKER function re-enters its
53
+ * own policy and dies with `stack depth limit exceeded` (54001).
54
+ *
55
+ * FAIL-CLOSED SPLIT, deliberate:
56
+ * - GUCs unset → RAISE 42501. Misconfiguration is LOUD. An unbound session
57
+ * silently yielding an empty grid reads to a human as "there is no data
58
+ * here", which is how a broken session survives a week of QA.
59
+ * - bound, but no membership → EMPTY SET, no error. A legitimate non-member
60
+ * seeing nothing is not an error condition, and platform-default rows
61
+ * reached through a sibling `OR` must still resolve.
62
+ */
63
+ export declare const SESSION_TENANT_IDS_SQL = "CREATE OR REPLACE FUNCTION auth.session_tenant_ids() RETURNS SETOF uuid\nLANGUAGE plpgsql STABLE SECURITY DEFINER\nSET search_path = pg_catalog, public, pg_temp\nAS $session$\nDECLARE\n v_user uuid;\n v_tenant uuid;\nBEGIN\n v_user := nullif(current_setting('app.user_id', true), '')::uuid;\n v_tenant := nullif(current_setting('app.tenant_id', true), '')::uuid;\n IF v_user IS NULL THEN\n RAISE EXCEPTION 'db_rls_session_user_missing: app.user_id is not bound on this connection'\n USING ERRCODE = '42501',\n HINT = 'The authenticated pool is the user-session pool; a session with no principal cannot be membership-checked. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';\n END IF;\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'db_rls_session_tenant_missing: app.tenant_id is not bound on this connection'\n USING ERRCODE = '42501',\n HINT = 'Every authenticated-pool session must be EXPLICITLY tenant-scoped. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';\n END IF;\n RETURN QUERY SELECT v_tenant WHERE v_tenant IN (SELECT auth.user_tenant_ids(v_user));\nEND\n$session$;\n";
64
+ /**
65
+ * Marker embedded in the DEGRADED body so `pg_proc.prosrc` introspection can
66
+ * tell the two variants apart. {@link verifyTenantFence} reports any database
67
+ * running the degraded resolver — the point is that a service is never on it by
68
+ * accident.
69
+ */
70
+ export declare const SESSION_TENANT_IDS_DEGRADED_MARKER = "DB_RLS_DEGRADED_NO_MEMBERSHIP_MIRROR";
71
+ /**
72
+ * DEGRADED variant of {@link SESSION_TENANT_IDS_SQL} — fence ONLY, no
73
+ * membership intersect.
74
+ *
75
+ * FOR SERVICES THAT SHIP NO MEMBERSHIP MIRROR, AND NOTHING ELSE. Such a service
76
+ * has `auth.user_tenant_ids` as the bootstrap stub (`SELECT NULL::uuid WHERE
77
+ * false`), so the strict resolver intersects to the empty set and the entire
78
+ * authenticated pool becomes silently deny-all — the P0 shape of drift
79
+ * 199feabc, where crm's 430 policies were dead and nobody noticed.
80
+ *
81
+ * THE LIST BELOW IS DERIVED FROM THE REPOS, NOT FROM MEMORY. An earlier
82
+ * revision of this comment asserted "nodii-crm-service alone", which was wrong
83
+ * by six services and wrong about crm's urgency. Several repos ship a phase-004
84
+ * or phase-005 `0002_create_rls_helpers.sql` whose real `user_tenant_ids` is
85
+ * FULLY COMMENTED OUT, so the phase-002 stub is still the winner:
86
+ *
87
+ * STUB + a LIVE authenticated pool → deny-all TODAY, needs this variant:
88
+ * - nodii-billing-service 77 call sites; saga + dsar migrations run at
89
+ * BOOT (`src/lib/bootstrap.ts`), so its
90
+ * `saga_outbox` / `lifecycle_requests` /
91
+ * `data_residency_holds` are library-owned. The
92
+ * money path. Highest risk in the fleet.
93
+ * - nodii-finance-service 14 call sites. Already self-mitigates via
94
+ * `010-lib-auth-pool-mode`, which rewrites the
95
+ * lib policies to `= auth.get_tenant_id()`.
96
+ * - nodii-notification-service 3 call sites. Its own policies use
97
+ * `= auth.get_tenant_id()` and keep working, so
98
+ * only the library-owned tables go dark — which
99
+ * is exactly the shape nobody notices.
100
+ *
101
+ * STUB + a pool that is built but has ZERO handler call sites → not broken
102
+ * today, breaks the moment a handler uses it:
103
+ * - nodii-crm-service, nodii-kyc-service, nodii-ticketing-service,
104
+ * nodii-product-service
105
+ *
106
+ * Does NOT need it: nodii-geo-service (no authenticated pool at all —
107
+ * 2-pool), nodii-edge (no `user_tenant_ids`, RLS mode `single`),
108
+ * nodii-customer-service and nodii-vendor-service (already GUC-bodied),
109
+ * nodii-auth / nodii-hr-service / nodii-task-tracking / nodii-tenant-service
110
+ * (REAL membership helpers).
111
+ *
112
+ * WHAT YOU GIVE UP. The database stops verifying the principal. Isolation rests
113
+ * entirely on the application binding `app.tenant_id` correctly — RLS degrades
114
+ * from an independent second layer into "the app said so". A forged or simply
115
+ * mistaken tenant binding becomes access. That is a real reduction in
116
+ * defence-in-depth and it is why this is a separate, named, reported constant
117
+ * rather than a boolean flag on the strict one.
118
+ *
119
+ * THE EXIT. Land a membership mirror, point `auth.user_tenant_ids` at it, then
120
+ * apply {@link SESSION_TENANT_IDS_SQL} over this. No policy changes — the
121
+ * predicate at every call site is identical for both variants.
122
+ */
123
+ export declare const SESSION_TENANT_IDS_DEGRADED_SQL = "CREATE OR REPLACE FUNCTION auth.session_tenant_ids() RETURNS SETOF uuid\nLANGUAGE plpgsql STABLE SECURITY DEFINER\nSET search_path = pg_catalog, public, pg_temp\nAS $session$\nDECLARE\n v_tenant uuid;\nBEGIN\n -- DB_RLS_DEGRADED_NO_MEMBERSHIP_MIRROR: no membership mirror in this service,\n -- so the session tenant is accepted WITHOUT proving the principal holds it.\n -- Replace with the strict resolver as soon as a mirror exists.\n IF nullif(current_setting('app.user_id', true), '') IS NULL THEN\n RAISE EXCEPTION 'db_rls_session_user_missing: app.user_id is not bound on this connection'\n USING ERRCODE = '42501',\n HINT = 'Open the session with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';\n END IF;\n v_tenant := nullif(current_setting('app.tenant_id', true), '')::uuid;\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'db_rls_session_tenant_missing: app.tenant_id is not bound on this connection'\n USING ERRCODE = '42501',\n HINT = 'Every authenticated-pool session must be EXPLICITLY tenant-scoped. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';\n END IF;\n RETURN QUERY SELECT v_tenant;\nEND\n$session$;\n";
124
+ /** Grant for the resolver. Separate from {@link AUTH_SCHEMA_GRANTS_SQL} for the
125
+ * same reason: the roles must exist before the GRANT runs. */
126
+ export declare const SESSION_TENANT_IDS_GRANTS_SQL = "GRANT EXECUTE ON FUNCTION auth.session_tenant_ids() TO nodii_services, authenticated;\n";
8
127
  /**
9
128
  * Grants for the 7 helper functions. Emitted separately because they
10
129
  * reference roles that must exist before the GRANT runs — applied last
@@ -33,6 +152,7 @@ type AnyExecutor = Pool | PoolClient | SqlExecutor;
33
152
  */
34
153
  export declare function applyAuthSchemaMigrations(client: AnyExecutor, opts?: {
35
154
  includeGrants?: boolean;
155
+ degradedSessionResolver?: boolean;
36
156
  }): Promise<void>;
37
157
  /**
38
158
  * Composite full DDL — the schema + helpers + grants, joined.
@@ -1 +1 @@
1
- {"version":3,"file":"auth-schema.d.ts","sourceRoot":"","sources":["../src/auth-schema.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,eAAe,s1CA2B3B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,8oBAQlC,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC;AAED,KAAK,WAAW,GAAG,IAAI,GAAG,UAAU,GAAG,WAAW,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,MAAM,CAKR"}
1
+ {"version":3,"file":"auth-schema.d.ts","sourceRoot":"","sources":["../src/auth-schema.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,eAAe,s1CA2B3B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,sBAAsB,iqCAuBlC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kCAAkC,yCACP,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,+BAA+B,isCAwB3C,CAAC;AAEF;+DAC+D;AAC/D,eAAO,MAAM,6BAA6B,4FACzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,8oBAQlC,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC;AAED,KAAK,WAAW,GAAG,IAAI,GAAG,UAAU,GAAG,WAAW,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAC;IAAC,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GACxE,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,MAAM,CAKR"}
@@ -52,6 +52,173 @@ LANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.tenant_id', true),
52
52
  CREATE OR REPLACE FUNCTION auth.user_tenant_ids(p_user_id uuid) RETURNS SETOF uuid
53
53
  LANGUAGE sql STABLE AS $$ SELECT NULL::uuid WHERE false $$;
54
54
  `;
55
+ /**
56
+ * `auth.session_tenant_ids()` — THE authenticated-pool resolver.
57
+ *
58
+ * Returns AT MOST ONE tenant: the one bound to `app.tenant_id`, and only if the
59
+ * principal in `app.user_id` genuinely holds it per `auth.user_tenant_ids`.
60
+ * Edge-signed session tenant **INTERSECT** server-resolved membership.
61
+ *
62
+ * WHY THIS EXISTS. The `authenticated` predicate used to be
63
+ * `tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))` — the UNION
64
+ * of every tenant the user belongs to, with no tenant fence at all. A session
65
+ * opened for a user with memberships in tenants A and B could read AND write
66
+ * both. Reproduced end-to-end: a legitimate tenant-A session transitioned a
67
+ * tenant-B approval task, got HTTP 200, and released a 250,000 INR refund.
68
+ *
69
+ * WHY SUBSTITUTION RATHER THAN APPENDING A FENCE. The obvious repair is to
70
+ * append `AND tenant_id = auth.get_tenant_id()` at the top level of each
71
+ * policy. That breaks every policy whose shape is not a bare tenant equality,
72
+ * and the fleet is full of them:
73
+ *
74
+ * - tables with NO `tenant_id` column that reach it through a nested
75
+ * `EXISTS` or a sub-`SELECT` (`tenant_role_permissions`, `fx_rate_cache`)
76
+ * - platform-default escape hatches — `tenant_id IS NULL OR tenant_id IN (…)`
77
+ * (hr's statutory masters, `pricing_snapshots`)
78
+ * - additive ORs on a differently-named column — `is_public_listing = TRUE OR
79
+ * developer_tenant_id IN (…)` (`oauth_apps` marketplace listings)
80
+ * - policies fencing on a column that is not `tenant_id` at all (`tenants.id`,
81
+ * `legal_documents.scope`, `impersonation_sessions.target_tenant_id`)
82
+ * - TEXT-vs-uuid casts that need fixing in opposite directions per table
83
+ *
84
+ * Swapping the resolver INSIDE the subquery leaves the column, the cast and the
85
+ * surrounding OR untouched, so all of those keep working while the set the
86
+ * subquery yields narrows from "every tenant you belong to" to "the one you are
87
+ * logged into, if you really belong to it". One substitution, zero shape
88
+ * changes, and NO `AS RESTRICTIVE` (which would AND against the additive
89
+ * permissive master policies and kill every platform-default row).
90
+ *
91
+ * `user_tenant_ids` is deliberately LEFT INTACT. It still answers the honest
92
+ * question "which tenants does this user belong to", which a tenant-switcher or
93
+ * a login tenant-picker legitimately needs. Overloading it with session scope
94
+ * would have silently broken those callers with no error and no test coverage.
95
+ *
96
+ * SECURITY DEFINER is required, not decorative:
97
+ * - `authenticated` has no privilege on the membership mirror, so an INVOKER
98
+ * function raises `permission denied for table user_memberships`;
99
+ * - if the mirror is itself RLS-policied, an INVOKER function re-enters its
100
+ * own policy and dies with `stack depth limit exceeded` (54001).
101
+ *
102
+ * FAIL-CLOSED SPLIT, deliberate:
103
+ * - GUCs unset → RAISE 42501. Misconfiguration is LOUD. An unbound session
104
+ * silently yielding an empty grid reads to a human as "there is no data
105
+ * here", which is how a broken session survives a week of QA.
106
+ * - bound, but no membership → EMPTY SET, no error. A legitimate non-member
107
+ * seeing nothing is not an error condition, and platform-default rows
108
+ * reached through a sibling `OR` must still resolve.
109
+ */
110
+ export const SESSION_TENANT_IDS_SQL = `CREATE OR REPLACE FUNCTION auth.session_tenant_ids() RETURNS SETOF uuid
111
+ LANGUAGE plpgsql STABLE SECURITY DEFINER
112
+ SET search_path = pg_catalog, public, pg_temp
113
+ AS $session$
114
+ DECLARE
115
+ v_user uuid;
116
+ v_tenant uuid;
117
+ BEGIN
118
+ v_user := nullif(current_setting('app.user_id', true), '')::uuid;
119
+ v_tenant := nullif(current_setting('app.tenant_id', true), '')::uuid;
120
+ IF v_user IS NULL THEN
121
+ RAISE EXCEPTION 'db_rls_session_user_missing: app.user_id is not bound on this connection'
122
+ USING ERRCODE = '42501',
123
+ HINT = 'The authenticated pool is the user-session pool; a session with no principal cannot be membership-checked. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';
124
+ END IF;
125
+ IF v_tenant IS NULL THEN
126
+ RAISE EXCEPTION 'db_rls_session_tenant_missing: app.tenant_id is not bound on this connection'
127
+ USING ERRCODE = '42501',
128
+ HINT = 'Every authenticated-pool session must be EXPLICITLY tenant-scoped. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';
129
+ END IF;
130
+ RETURN QUERY SELECT v_tenant WHERE v_tenant IN (SELECT auth.user_tenant_ids(v_user));
131
+ END
132
+ $session$;
133
+ `;
134
+ /**
135
+ * Marker embedded in the DEGRADED body so `pg_proc.prosrc` introspection can
136
+ * tell the two variants apart. {@link verifyTenantFence} reports any database
137
+ * running the degraded resolver — the point is that a service is never on it by
138
+ * accident.
139
+ */
140
+ export const SESSION_TENANT_IDS_DEGRADED_MARKER = "DB_RLS_DEGRADED_NO_MEMBERSHIP_MIRROR";
141
+ /**
142
+ * DEGRADED variant of {@link SESSION_TENANT_IDS_SQL} — fence ONLY, no
143
+ * membership intersect.
144
+ *
145
+ * FOR SERVICES THAT SHIP NO MEMBERSHIP MIRROR, AND NOTHING ELSE. Such a service
146
+ * has `auth.user_tenant_ids` as the bootstrap stub (`SELECT NULL::uuid WHERE
147
+ * false`), so the strict resolver intersects to the empty set and the entire
148
+ * authenticated pool becomes silently deny-all — the P0 shape of drift
149
+ * 199feabc, where crm's 430 policies were dead and nobody noticed.
150
+ *
151
+ * THE LIST BELOW IS DERIVED FROM THE REPOS, NOT FROM MEMORY. An earlier
152
+ * revision of this comment asserted "nodii-crm-service alone", which was wrong
153
+ * by six services and wrong about crm's urgency. Several repos ship a phase-004
154
+ * or phase-005 `0002_create_rls_helpers.sql` whose real `user_tenant_ids` is
155
+ * FULLY COMMENTED OUT, so the phase-002 stub is still the winner:
156
+ *
157
+ * STUB + a LIVE authenticated pool → deny-all TODAY, needs this variant:
158
+ * - nodii-billing-service 77 call sites; saga + dsar migrations run at
159
+ * BOOT (`src/lib/bootstrap.ts`), so its
160
+ * `saga_outbox` / `lifecycle_requests` /
161
+ * `data_residency_holds` are library-owned. The
162
+ * money path. Highest risk in the fleet.
163
+ * - nodii-finance-service 14 call sites. Already self-mitigates via
164
+ * `010-lib-auth-pool-mode`, which rewrites the
165
+ * lib policies to `= auth.get_tenant_id()`.
166
+ * - nodii-notification-service 3 call sites. Its own policies use
167
+ * `= auth.get_tenant_id()` and keep working, so
168
+ * only the library-owned tables go dark — which
169
+ * is exactly the shape nobody notices.
170
+ *
171
+ * STUB + a pool that is built but has ZERO handler call sites → not broken
172
+ * today, breaks the moment a handler uses it:
173
+ * - nodii-crm-service, nodii-kyc-service, nodii-ticketing-service,
174
+ * nodii-product-service
175
+ *
176
+ * Does NOT need it: nodii-geo-service (no authenticated pool at all —
177
+ * 2-pool), nodii-edge (no `user_tenant_ids`, RLS mode `single`),
178
+ * nodii-customer-service and nodii-vendor-service (already GUC-bodied),
179
+ * nodii-auth / nodii-hr-service / nodii-task-tracking / nodii-tenant-service
180
+ * (REAL membership helpers).
181
+ *
182
+ * WHAT YOU GIVE UP. The database stops verifying the principal. Isolation rests
183
+ * entirely on the application binding `app.tenant_id` correctly — RLS degrades
184
+ * from an independent second layer into "the app said so". A forged or simply
185
+ * mistaken tenant binding becomes access. That is a real reduction in
186
+ * defence-in-depth and it is why this is a separate, named, reported constant
187
+ * rather than a boolean flag on the strict one.
188
+ *
189
+ * THE EXIT. Land a membership mirror, point `auth.user_tenant_ids` at it, then
190
+ * apply {@link SESSION_TENANT_IDS_SQL} over this. No policy changes — the
191
+ * predicate at every call site is identical for both variants.
192
+ */
193
+ export const SESSION_TENANT_IDS_DEGRADED_SQL = `CREATE OR REPLACE FUNCTION auth.session_tenant_ids() RETURNS SETOF uuid
194
+ LANGUAGE plpgsql STABLE SECURITY DEFINER
195
+ SET search_path = pg_catalog, public, pg_temp
196
+ AS $session$
197
+ DECLARE
198
+ v_tenant uuid;
199
+ BEGIN
200
+ -- ${SESSION_TENANT_IDS_DEGRADED_MARKER}: no membership mirror in this service,
201
+ -- so the session tenant is accepted WITHOUT proving the principal holds it.
202
+ -- Replace with the strict resolver as soon as a mirror exists.
203
+ IF nullif(current_setting('app.user_id', true), '') IS NULL THEN
204
+ RAISE EXCEPTION 'db_rls_session_user_missing: app.user_id is not bound on this connection'
205
+ USING ERRCODE = '42501',
206
+ HINT = 'Open the session with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';
207
+ END IF;
208
+ v_tenant := nullif(current_setting('app.tenant_id', true), '')::uuid;
209
+ IF v_tenant IS NULL THEN
210
+ RAISE EXCEPTION 'db_rls_session_tenant_missing: app.tenant_id is not bound on this connection'
211
+ USING ERRCODE = '42501',
212
+ HINT = 'Every authenticated-pool session must be EXPLICITLY tenant-scoped. Open it with withUserContext({ userId, tenantId }, fn) from @nodii/db-rls.';
213
+ END IF;
214
+ RETURN QUERY SELECT v_tenant;
215
+ END
216
+ $session$;
217
+ `;
218
+ /** Grant for the resolver. Separate from {@link AUTH_SCHEMA_GRANTS_SQL} for the
219
+ * same reason: the roles must exist before the GRANT runs. */
220
+ export const SESSION_TENANT_IDS_GRANTS_SQL = `GRANT EXECUTE ON FUNCTION auth.session_tenant_ids() TO nodii_services, authenticated;
221
+ `;
55
222
  /**
56
223
  * Grants for the 7 helper functions. Emitted separately because they
57
224
  * reference roles that must exist before the GRANT runs — applied last
@@ -84,8 +251,18 @@ export async function applyAuthSchemaMigrations(client, opts = {}) {
84
251
  // SqlExecutor`; cast to the minimal shape.
85
252
  const exec = client;
86
253
  await exec.query(AUTH_SCHEMA_SQL);
254
+ // The resolver is applied unconditionally alongside the seven. A database
255
+ // where the policies reference `auth.session_tenant_ids()` but the function is
256
+ // absent does not merely misbehave — Postgres validates the reference at
257
+ // CREATE POLICY time, so the migration itself fails with `function
258
+ // auth.session_tenant_ids() does not exist`. Installing both together makes
259
+ // that unreachable.
260
+ await exec.query(opts.degradedSessionResolver
261
+ ? SESSION_TENANT_IDS_DEGRADED_SQL
262
+ : SESSION_TENANT_IDS_SQL);
87
263
  if (includeGrants) {
88
264
  await exec.query(AUTH_SCHEMA_GRANTS_SQL);
265
+ await exec.query(SESSION_TENANT_IDS_GRANTS_SQL);
89
266
  }
90
267
  }
91
268
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"auth-schema.js","sourceRoot":"","sources":["../src/auth-schema.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,oEAAoE;AACpE,qEAAqE;AACrE,2BAA2B;AAC3B,EAAE;AACF,mEAAmE;AACnE,qBAAqB;AACrB,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,qEAAqE;AACrE,4FAA4F;AAC5F,8DAA8D;AAC9D,8DAA8D;AAC9D,0EAA0E;AAC1E,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,oEAAoE;AAIpE;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B9B,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;CAQrC,CAAC;AAYF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,MAAmB,EACnB,OAAoC,EAAE;IAEtC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC;IACjD,wEAAwE;IACxE,2CAA2C;IAC3C,MAAM,IAAI,GAAG,MAAqB,CAAC;IACnC,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAClC,IAAI,aAAa,EAAE,CAAC;QAClB,MAAM,IAAI,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAoC,EAAE;IAEtC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC;IACjD,OAAO,aAAa;QAClB,CAAC,CAAC,GAAG,eAAe,KAAK,sBAAsB,EAAE;QACjD,CAAC,CAAC,eAAe,CAAC;AACtB,CAAC"}
1
+ {"version":3,"file":"auth-schema.js","sourceRoot":"","sources":["../src/auth-schema.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,oEAAoE;AACpE,qEAAqE;AACrE,2BAA2B;AAC3B,EAAE;AACF,mEAAmE;AACnE,qBAAqB;AACrB,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,qEAAqE;AACrE,4FAA4F;AAC5F,8DAA8D;AAC9D,8DAA8D;AAC9D,0EAA0E;AAC1E,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,oEAAoE;AAIpE;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBrC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAC7C,sCAAsC,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG;;;;;;;OAOxC,kCAAkC;;;;;;;;;;;;;;;;;CAiBxC,CAAC;AAEF;+DAC+D;AAC/D,MAAM,CAAC,MAAM,6BAA6B,GAAG;CAC5C,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;CAQrC,CAAC;AAYF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,MAAmB,EACnB,OAAuE,EAAE;IAEzE,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC;IACjD,wEAAwE;IACxE,2CAA2C;IAC3C,MAAM,IAAI,GAAG,MAAqB,CAAC;IACnC,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAClC,0EAA0E;IAC1E,+EAA+E;IAC/E,yEAAyE;IACzE,mEAAmE;IACnE,4EAA4E;IAC5E,oBAAoB;IACpB,MAAM,IAAI,CAAC,KAAK,CACd,IAAI,CAAC,uBAAuB;QAC1B,CAAC,CAAC,+BAA+B;QACjC,CAAC,CAAC,sBAAsB,CAC3B,CAAC;IACF,IAAI,aAAa,EAAE,CAAC;QAClB,MAAM,IAAI,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACzC,MAAM,IAAI,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;IAClD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAoC,EAAE;IAEtC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC;IACjD,OAAO,aAAa;QAClB,CAAC,CAAC,GAAG,eAAe,KAAK,sBAAsB,EAAE;QACjD,CAAC,CAAC,eAAe,CAAC;AACtB,CAAC"}
package/dist/context.d.ts CHANGED
@@ -11,15 +11,33 @@ import { type AutoDispatchOpts, type PgTx, type PlatformOpsContextOpts, type Sys
11
11
  */
12
12
  export declare function withUserContext<T>(opts: UserContextOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
13
13
  /**
14
- * withUserMembershipContext — the doctrine 08-rls §4 multi-membership
15
- * variant of `withUserContext`. Opens a tx on the `authenticated` pool and
16
- * sets ONLY `app.user_id` (no `app.tenant_id` pin), so the authenticated
17
- * predicate `tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))`
18
- * resolves across ALL of the user's tenant memberships. Use this for §4
19
- * multi-membership query paths (e.g. task-tracking); use `withUserContext`
20
- * when a single tenant must be pinned.
14
+ * @deprecated REMOVED — always throws {@link CrossTenantSessionRefused}.
15
+ *
16
+ * This helper opened an `authenticated`-pool transaction binding ONLY
17
+ * `app.user_id`, deliberately leaving `app.tenant_id` unset so that the old
18
+ * union predicate `tenant_id IN (SELECT auth.user_tenant_ids(...))` would
19
+ * resolve across EVERY tenant the principal was an active member of. Its own
20
+ * docblock named task-tracking as the use case.
21
+ *
22
+ * That is not a policy bug that the predicate fix cleans up incidentally — it
23
+ * is the MECHANISM of the incident, sitting in the library. Reproduced
24
+ * end-to-end: a legitimate tenant-A session transitioned a tenant-B approval
25
+ * task, got HTTP 200, and released a 250,000 INR refund. Rewriting predicates
26
+ * while leaving this callable would have changed nothing for any caller of it.
27
+ *
28
+ * Migrate to `withUserContext({ userId, tenantId }, fn)` — the same pool, with
29
+ * the tenant the request actually resolved to. If a caller genuinely needs to
30
+ * know which tenants a user belongs to (a tenant switcher, a login
31
+ * tenant-picker), query `auth.user_tenant_ids` directly from a services-pool
32
+ * context; that function is deliberately left intact and still answers exactly
33
+ * that question. What is no longer available is opening a DATA session that
34
+ * spans them.
35
+ *
36
+ * The symbol is retained rather than deleted so a JavaScript consumer (no
37
+ * typechecker) gets this explanation at runtime instead of
38
+ * `undefined is not a function`.
21
39
  */
22
- export declare function withUserMembershipContext<T>(opts: UserMembershipContextOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
40
+ export declare function withUserMembershipContext<T>(_opts: UserMembershipContextOpts, _fn: (tx: PgTx) => Promise<T>): Promise<T>;
23
41
  /**
24
42
  * withSystemContext — opens a tx on the `services` (nodii_services) pool,
25
43
  * sets `app.tenant_id` via tx-scoped `set_config`, yields the tx, commits
@@ -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;AAC5B,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,IAAI,EACT,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC/B,MAAM,YAAY,CAAC;AAEpB;;;;;;;;GAQG;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,CAoBZ;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAC/C,IAAI,EAAE,yBAAyB,EAC/B,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,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":"AASA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,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;;;;;;;;GAQG;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,CAyBZ;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"}
package/dist/context.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import { requirePool } from "./init.js";
8
8
  import { recordOwnerContextInvocation } from "./metrics.js";
9
9
  import { assertMigrateAllowed, } from "./migrate-guard.js";
10
- import { MissingTenantContext, TransactionRolledBack, } from "./types.js";
10
+ import { CrossTenantSessionRefused, MissingTenantContext, TransactionRolledBack, } from "./types.js";
11
11
  /**
12
12
  * withUserContext — opens a tx on the `authenticated` pool, sets
13
13
  * `app.user_id` + `app.tenant_id` (+ optional `app.source_ip`) via
@@ -18,6 +18,13 @@ import { MissingTenantContext, TransactionRolledBack, } from "./types.js";
18
18
  * Wraps any error thrown by `fn` in `TransactionRolledBack`.
19
19
  */
20
20
  export async function withUserContext(opts, fn) {
21
+ // Reject a blank principal or tenant HERE rather than binding an empty GUC
22
+ // and letting `auth.session_tenant_ids()` raise 42501 on the first query.
23
+ // Both fail closed, but this one names the caller instead of a policy.
24
+ if (!opts?.userId)
25
+ throw new MissingTenantContext();
26
+ if (!opts?.tenantId)
27
+ throw new MissingTenantContext();
21
28
  const pool = requirePool("authenticated");
22
29
  return pool.withTransaction(async (tx) => {
23
30
  // Canonical GUC namespace is `app.*` (08-rls §2/§6): the generated
@@ -40,30 +47,34 @@ export async function withUserContext(opts, fn) {
40
47
  });
41
48
  }
42
49
  /**
43
- * withUserMembershipContext — the doctrine 08-rls §4 multi-membership
44
- * variant of `withUserContext`. Opens a tx on the `authenticated` pool and
45
- * sets ONLY `app.user_id` (no `app.tenant_id` pin), so the authenticated
46
- * predicate `tenant_id IN (SELECT auth.user_tenant_ids(auth.get_user_id()))`
47
- * resolves across ALL of the user's tenant memberships. Use this for §4
48
- * multi-membership query paths (e.g. task-tracking); use `withUserContext`
49
- * when a single tenant must be pinned.
50
+ * @deprecated REMOVED — always throws {@link CrossTenantSessionRefused}.
51
+ *
52
+ * This helper opened an `authenticated`-pool transaction binding ONLY
53
+ * `app.user_id`, deliberately leaving `app.tenant_id` unset so that the old
54
+ * union predicate `tenant_id IN (SELECT auth.user_tenant_ids(...))` would
55
+ * resolve across EVERY tenant the principal was an active member of. Its own
56
+ * docblock named task-tracking as the use case.
57
+ *
58
+ * That is not a policy bug that the predicate fix cleans up incidentally — it
59
+ * is the MECHANISM of the incident, sitting in the library. Reproduced
60
+ * end-to-end: a legitimate tenant-A session transitioned a tenant-B approval
61
+ * task, got HTTP 200, and released a 250,000 INR refund. Rewriting predicates
62
+ * while leaving this callable would have changed nothing for any caller of it.
63
+ *
64
+ * Migrate to `withUserContext({ userId, tenantId }, fn)` — the same pool, with
65
+ * the tenant the request actually resolved to. If a caller genuinely needs to
66
+ * know which tenants a user belongs to (a tenant switcher, a login
67
+ * tenant-picker), query `auth.user_tenant_ids` directly from a services-pool
68
+ * context; that function is deliberately left intact and still answers exactly
69
+ * that question. What is no longer available is opening a DATA session that
70
+ * spans them.
71
+ *
72
+ * The symbol is retained rather than deleted so a JavaScript consumer (no
73
+ * typechecker) gets this explanation at runtime instead of
74
+ * `undefined is not a function`.
50
75
  */
51
- export async function withUserMembershipContext(opts, fn) {
52
- const pool = requirePool("authenticated");
53
- return pool.withTransaction(async (tx) => {
54
- // ONLY app.user_id — leaving app.tenant_id unset is intentional: the §4
55
- // predicate keys on user_tenant_ids(get_user_id()), not a pinned tenant.
56
- await tx.setConfig("app.user_id", opts.userId);
57
- if (opts.sourceIp !== undefined) {
58
- await tx.setConfig("app.source_ip", opts.sourceIp);
59
- }
60
- try {
61
- return await fn(tx);
62
- }
63
- catch (cause) {
64
- throw new TransactionRolledBack(cause);
65
- }
66
- });
76
+ export async function withUserMembershipContext(_opts, _fn) {
77
+ throw new CrossTenantSessionRefused("withUserMembershipContext");
67
78
  }
68
79
  /**
69
80
  * withSystemContext — opens a tx on the `services` (nodii_services) pool,
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
@@ -10,12 +10,24 @@ type DrizzleClient<TSchema extends Record<string, unknown> = Record<string, unkn
10
10
  */
11
11
  export declare function withUserContextDb<T, TSchema extends Record<string, unknown> = Record<string, unknown>>(opts: UserContextOpts, fn: (db: DrizzleClient<TSchema>) => Promise<T>): Promise<T>;
12
12
  /**
13
- * withUserMembershipContextDb — the doctrine 08-rls §4 multi-membership
14
- * Drizzle variant. Sets ONLY `app.user_id` via the canonical
15
- * `auth.set_user_id(...)` helper (no `auth.init` tenant pin), so the
16
- * authenticated predicate spans all of the user's tenant memberships.
13
+ * @deprecated REMOVED — always throws {@link CrossTenantSessionRefused}.
14
+ *
15
+ * Opened an `authenticated`-pool transaction binding ONLY `app.user_id`,
16
+ * leaving `app.tenant_id` deliberately unset so the old union predicate
17
+ * resolved across EVERY tenant the principal belonged to. That is the
18
+ * mechanism of the cross-tenant approval incident, not a side effect of it:
19
+ * a legitimate tenant-A session transitioned a tenant-B approval task, got
20
+ * HTTP 200, and released a 250,000 INR refund.
21
+ *
22
+ * Migrate to `withUserContextDb({ userId, tenantId }, fn)`.
23
+ * To LIST a user's tenants (switcher / login picker), query
24
+ * `auth.user_tenant_ids` from a services-pool context — that function is
25
+ * intact; what is gone is opening a DATA session that spans them.
26
+ *
27
+ * Retained as a throwing stub rather than deleted so a JavaScript consumer
28
+ * gets this explanation instead of `undefined is not a function`.
17
29
  */
18
- export declare function withUserMembershipContextDb<T, TSchema extends Record<string, unknown> = Record<string, unknown>>(opts: UserMembershipContextOpts, fn: (db: DrizzleClient<TSchema>) => Promise<T>): Promise<T>;
30
+ export declare function withUserMembershipContextDb<T, TSchema extends Record<string, unknown> = Record<string, unknown>>(_opts: UserMembershipContextOpts, _fn: (db: DrizzleClient<TSchema>) => Promise<T>): Promise<T>;
19
31
  /**
20
32
  * withSystemContext (Drizzle variant) — opens a tx on the `services` pool
21
33
  * with `app.tenant_id` GUC bound; yields a Drizzle client.
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle-client.d.ts","sourceRoot":"","sources":["../src/drizzle-client.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAKzE,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAOpB,KAAK,aAAa,CAChB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAC/D,cAAc,CAAC,OAAO,CAAC,CAAC;AA2D5B;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAoBZ;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,IAAI,EAAE,yBAAyB,EAC/B,EAAE,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAgBZ;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,QAAQ,EAAE,gBAAgB,GAAG,CAAC,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,EACzE,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACnD,OAAO,CAAC,CAAC,CAAC,CAeZ;AAoBD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"drizzle-client.d.ts","sourceRoot":"","sources":["../src/drizzle-client.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAKzE,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAWpB,KAAK,aAAa,CAChB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAC/D,cAAc,CAAC,OAAO,CAAC,CAAC;AA2D5B;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAoBZ;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,2BAA2B,CAC/C,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,KAAK,EAAE,yBAAyB,EAChC,GAAG,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC9C,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjE,QAAQ,EAAE,gBAAgB,GAAG,CAAC,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,EACzE,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACnD,OAAO,CAAC,CAAC,CAAC,CAeZ;AAoBD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC"}
@@ -21,7 +21,7 @@ import { requirePool } from "./init.js";
21
21
  import { recordOwnerContextInvocation, withTxSpan } from "./metrics.js";
22
22
  import { assertMigrateAllowed, } from "./migrate-guard.js";
23
23
  import { PgClientPool } from "./pg-pool-adapter.js";
24
- import { MissingTenantContext, TransactionRolledBack } from "./types.js";
24
+ import { CrossTenantSessionRefused, MissingTenantContext, TransactionRolledBack, } from "./types.js";
25
25
  function clientFor(_pool) {
26
26
  // Only PgClientPool exposes a real pg PoolClient. In-memory pools don't.
27
27
  // The caller is responsible for using *Db helpers only against real pools.
@@ -97,24 +97,25 @@ export async function withUserContextDb(opts, fn) {
97
97
  }, (db) => fn(db));
98
98
  }
99
99
  /**
100
- * withUserMembershipContextDb — the doctrine 08-rls §4 multi-membership
101
- * Drizzle variant. Sets ONLY `app.user_id` via the canonical
102
- * `auth.set_user_id(...)` helper (no `auth.init` tenant pin), so the
103
- * authenticated predicate spans all of the user's tenant memberships.
100
+ * @deprecated REMOVED — always throws {@link CrossTenantSessionRefused}.
101
+ *
102
+ * Opened an `authenticated`-pool transaction binding ONLY `app.user_id`,
103
+ * leaving `app.tenant_id` deliberately unset so the old union predicate
104
+ * resolved across EVERY tenant the principal belonged to. That is the
105
+ * mechanism of the cross-tenant approval incident, not a side effect of it:
106
+ * a legitimate tenant-A session transitioned a tenant-B approval task, got
107
+ * HTTP 200, and released a 250,000 INR refund.
108
+ *
109
+ * Migrate to `withUserContextDb({ userId, tenantId }, fn)`.
110
+ * To LIST a user's tenants (switcher / login picker), query
111
+ * `auth.user_tenant_ids` from a services-pool context — that function is
112
+ * intact; what is gone is opening a DATA session that spans them.
113
+ *
114
+ * Retained as a throwing stub rather than deleted so a JavaScript consumer
115
+ * gets this explanation instead of `undefined is not a function`.
104
116
  */
105
- export async function withUserMembershipContextDb(opts, fn) {
106
- if (!opts.userId)
107
- throw new MissingTenantContext();
108
- const pool = requireRealPool("authenticated");
109
- return withRealTx(pool, async (client) => {
110
- // ONLY app.user_id — set_user_id, not init, so no tenant is pinned.
111
- await client.query("SELECT auth.set_user_id($1::uuid)", [opts.userId]);
112
- if (opts.sourceIp !== undefined) {
113
- await client.query("SELECT set_config('app.source_ip', $1, true)", [
114
- opts.sourceIp,
115
- ]);
116
- }
117
- }, (db) => fn(db));
117
+ export async function withUserMembershipContextDb(_opts, _fn) {
118
+ throw new CrossTenantSessionRefused("withUserMembershipContextDb");
118
119
  }
119
120
  /**
120
121
  * withSystemContext (Drizzle variant) — opens a tx on the `services` pool
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle-client.js","sourceRoot":"","sources":["../src/drizzle-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"drizzle-client.js","sourceRoot":"","sources":["../src/drizzle-client.ts"],"names":[],"mappings":"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"}