@nodii/db-rls 0.0.2 → 0.2.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 (50) hide show
  1. package/bin/migrate-gen.mjs +12 -0
  2. package/dist/auth-schema.d.ts +45 -0
  3. package/dist/auth-schema.d.ts.map +1 -0
  4. package/dist/auth-schema.js +101 -0
  5. package/dist/auth-schema.js.map +1 -0
  6. package/dist/configure.d.ts +45 -0
  7. package/dist/configure.d.ts.map +1 -0
  8. package/dist/configure.js +85 -0
  9. package/dist/configure.js.map +1 -0
  10. package/dist/context.d.ts +43 -0
  11. package/dist/context.d.ts.map +1 -0
  12. package/dist/context.js +98 -0
  13. package/dist/context.js.map +1 -0
  14. package/dist/drizzle-client.d.ts +22 -0
  15. package/dist/drizzle-client.d.ts.map +1 -0
  16. package/dist/drizzle-client.js +109 -0
  17. package/dist/drizzle-client.js.map +1 -0
  18. package/dist/in-memory-pool.d.ts +31 -0
  19. package/dist/in-memory-pool.d.ts.map +1 -0
  20. package/dist/in-memory-pool.js +53 -0
  21. package/dist/in-memory-pool.js.map +1 -0
  22. package/dist/index.d.ts +21 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +35 -4
  25. package/dist/index.js.map +1 -1
  26. package/dist/init.d.ts +18 -0
  27. package/dist/init.d.ts.map +1 -0
  28. package/dist/init.js +40 -0
  29. package/dist/init.js.map +1 -0
  30. package/dist/migrate-gen.d.ts +41 -0
  31. package/dist/migrate-gen.d.ts.map +1 -0
  32. package/dist/migrate-gen.js +123 -0
  33. package/dist/migrate-gen.js.map +1 -0
  34. package/dist/pg-pool-adapter.d.ts +44 -0
  35. package/dist/pg-pool-adapter.d.ts.map +1 -0
  36. package/dist/pg-pool-adapter.js +111 -0
  37. package/dist/pg-pool-adapter.js.map +1 -0
  38. package/dist/rls-policy-gen.d.ts +26 -0
  39. package/dist/rls-policy-gen.d.ts.map +1 -0
  40. package/dist/rls-policy-gen.js +81 -0
  41. package/dist/rls-policy-gen.js.map +1 -0
  42. package/dist/rls-policy.d.ts +30 -0
  43. package/dist/rls-policy.d.ts.map +1 -0
  44. package/dist/rls-policy.js +81 -0
  45. package/dist/rls-policy.js.map +1 -0
  46. package/dist/types.d.ts +71 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +56 -0
  49. package/dist/types.js.map +1 -0
  50. package/package.json +31 -7
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ // CLI shim for `npx @nodii/db-rls migrate-gen <serviceName>`.
3
+ //
4
+ // The real logic lives in src/migrate-gen.ts; this shim just unwraps
5
+ // argv and calls runMigrateGenCli + translates the return value to
6
+ // process.exit.
7
+
8
+ import { runMigrateGenCli } from "../dist/migrate-gen.js";
9
+
10
+ const args = process.argv.slice(2);
11
+ const code = runMigrateGenCli(args);
12
+ process.exit(code);
@@ -0,0 +1,45 @@
1
+ import type { Pool, PoolClient } from "pg";
2
+ /**
3
+ * Canonical SQL string exported as a module-level constant per the R1
4
+ * compliance requirement. Identical byte-equal payload across TS / Python
5
+ * / Go ports (asserted by the parity-fence CI).
6
+ */
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
+ * Grants for the 7 helper functions. Emitted separately because they
10
+ * reference roles that must exist before the GRANT runs — applied last
11
+ * after all role creation has settled.
12
+ */
13
+ export declare const AUTH_SCHEMA_GRANTS_SQL = "GRANT USAGE ON SCHEMA auth TO nodii_services, authenticated;\nGRANT EXECUTE ON FUNCTION auth.set_user_id(uuid) TO nodii_services, authenticated;\nGRANT EXECUTE ON FUNCTION auth.set_tenant_id(uuid) TO nodii_services, authenticated;\nGRANT EXECUTE ON FUNCTION auth.init(uuid, uuid) TO nodii_services, authenticated;\nGRANT EXECUTE ON FUNCTION auth.init_tenant(uuid) TO nodii_services, authenticated;\nGRANT EXECUTE ON FUNCTION auth.get_user_id() TO nodii_services, authenticated;\nGRANT EXECUTE ON FUNCTION auth.get_tenant_id() TO nodii_services, authenticated;\nGRANT EXECUTE ON FUNCTION auth.user_tenant_ids(uuid) TO nodii_services, authenticated;\n";
14
+ /**
15
+ * Minimal client surface — works with `pg.Pool`, `pg.PoolClient`, or
16
+ * `pg.Client`. We accept anything with a `.query(sql)` method.
17
+ */
18
+ export interface SqlExecutor {
19
+ query(sql: string): Promise<unknown>;
20
+ }
21
+ type AnyExecutor = Pool | PoolClient | SqlExecutor;
22
+ /**
23
+ * Applies the canonical auth schema + 7 helper functions idempotently.
24
+ *
25
+ * Caller MUST run this against the `owner` (nodii_owner BYPASSRLS) pool
26
+ * — function definitions require schema-creation privilege which is
27
+ * owner-only per § 5.7 terraform-bootstrap split.
28
+ *
29
+ * Per § 5.7: existing services migrating from the old 4-phase model run
30
+ * this as a one-shot before deploying. New services let terraform's
31
+ * `null_resource.auth_schema_bootstrap` run this; the library function
32
+ * exists for tests + local dev where terraform isn't in play.
33
+ */
34
+ export declare function applyAuthSchemaMigrations(client: AnyExecutor, opts?: {
35
+ includeGrants?: boolean;
36
+ }): Promise<void>;
37
+ /**
38
+ * Composite full DDL — the schema + helpers + grants, joined.
39
+ * Provided for migrate-gen + parity-fence byte-equality.
40
+ */
41
+ export declare function authSchemaFullSql(opts?: {
42
+ includeGrants?: boolean;
43
+ }): string;
44
+ export {};
45
+ //# sourceMappingURL=auth-schema.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,101 @@
1
+ // Auth schema SQL helpers — canonical SECURITY DEFINER block per § 5.7.
2
+ //
3
+ // Exports the 7 canonical helper functions as a single module-level
4
+ // constant + an apply helper that runs them idempotently against any
5
+ // `pg.Pool`-shaped client.
6
+ //
7
+ // The seven canonical helpers per `07-rls-doctrine.md` § canonical
8
+ // helper signatures:
9
+ //
10
+ // - auth.set_user_id(uuid) → set_config('app.user_id', ..., true)
11
+ // - auth.set_tenant_id(uuid) → set_config('app.tenant_id', ..., true)
12
+ // - auth.init(uuid, uuid) → both of the above in one shot
13
+ // - auth.init_tenant(uuid) → set_config('app.tenant_id', ..., true) (system path)
14
+ // - auth.get_user_id() → current_setting → uuid
15
+ // - auth.get_tenant_id() → current_setting → uuid
16
+ // - auth.user_tenant_ids(uuid) → stub here; replaced by migrate-gen
17
+ //
18
+ // The stub variant of `auth.user_tenant_ids` returns an empty set so RLS
19
+ // fails closed if a service applies the helpers but hasn't yet emitted
20
+ // the real per-service helper via `migrate-gen`. Once `migrate-gen` ships
21
+ // the real helper, `CREATE OR REPLACE` replaces this stub in place.
22
+ /**
23
+ * Canonical SQL string exported as a module-level constant per the R1
24
+ * compliance requirement. Identical byte-equal payload across TS / Python
25
+ * / Go ports (asserted by the parity-fence CI).
26
+ */
27
+ export const AUTH_SCHEMA_SQL = `CREATE SCHEMA IF NOT EXISTS auth;
28
+
29
+ CREATE OR REPLACE FUNCTION auth.set_user_id(p_user_id uuid) RETURNS void
30
+ LANGUAGE sql AS $$ SELECT set_config('app.user_id', p_user_id::text, true) $$;
31
+
32
+ CREATE OR REPLACE FUNCTION auth.set_tenant_id(p_tenant_id uuid) RETURNS void
33
+ LANGUAGE sql AS $$ SELECT set_config('app.tenant_id', p_tenant_id::text, true) $$;
34
+
35
+ CREATE OR REPLACE FUNCTION auth.init(p_user_id uuid, p_tenant_id uuid) RETURNS void
36
+ LANGUAGE sql AS $$
37
+ SELECT set_config('app.user_id', p_user_id::text, true);
38
+ SELECT set_config('app.tenant_id', p_tenant_id::text, true);
39
+ $$;
40
+
41
+ CREATE OR REPLACE FUNCTION auth.init_tenant(p_tenant_id uuid) RETURNS void
42
+ LANGUAGE sql AS $$ SELECT set_config('app.tenant_id', p_tenant_id::text, true) $$;
43
+
44
+ CREATE OR REPLACE FUNCTION auth.get_user_id() RETURNS uuid
45
+ LANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.user_id', true), '')::uuid $$;
46
+
47
+ CREATE OR REPLACE FUNCTION auth.get_tenant_id() RETURNS uuid
48
+ LANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.tenant_id', true), '')::uuid $$;
49
+
50
+ -- Stub variant. Replaced in-place by migrate-gen's 0001_user_tenant_ids.sql
51
+ -- once the service has employees_mirror + customers_mirror tables.
52
+ CREATE OR REPLACE FUNCTION auth.user_tenant_ids(p_user_id uuid) RETURNS SETOF uuid
53
+ LANGUAGE sql STABLE AS $$ SELECT NULL::uuid WHERE false $$;
54
+ `;
55
+ /**
56
+ * Grants for the 7 helper functions. Emitted separately because they
57
+ * reference roles that must exist before the GRANT runs — applied last
58
+ * after all role creation has settled.
59
+ */
60
+ export const AUTH_SCHEMA_GRANTS_SQL = `GRANT USAGE ON SCHEMA auth TO nodii_services, authenticated;
61
+ GRANT EXECUTE ON FUNCTION auth.set_user_id(uuid) TO nodii_services, authenticated;
62
+ GRANT EXECUTE ON FUNCTION auth.set_tenant_id(uuid) TO nodii_services, authenticated;
63
+ GRANT EXECUTE ON FUNCTION auth.init(uuid, uuid) TO nodii_services, authenticated;
64
+ GRANT EXECUTE ON FUNCTION auth.init_tenant(uuid) TO nodii_services, authenticated;
65
+ GRANT EXECUTE ON FUNCTION auth.get_user_id() TO nodii_services, authenticated;
66
+ GRANT EXECUTE ON FUNCTION auth.get_tenant_id() TO nodii_services, authenticated;
67
+ GRANT EXECUTE ON FUNCTION auth.user_tenant_ids(uuid) TO nodii_services, authenticated;
68
+ `;
69
+ /**
70
+ * Applies the canonical auth schema + 7 helper functions idempotently.
71
+ *
72
+ * Caller MUST run this against the `owner` (nodii_owner BYPASSRLS) pool
73
+ * — function definitions require schema-creation privilege which is
74
+ * owner-only per § 5.7 terraform-bootstrap split.
75
+ *
76
+ * Per § 5.7: existing services migrating from the old 4-phase model run
77
+ * this as a one-shot before deploying. New services let terraform's
78
+ * `null_resource.auth_schema_bootstrap` run this; the library function
79
+ * exists for tests + local dev where terraform isn't in play.
80
+ */
81
+ export async function applyAuthSchemaMigrations(client, opts = {}) {
82
+ const includeGrants = opts.includeGrants ?? true;
83
+ // pg's typings union overloads can't infer through `Pool | PoolClient |
84
+ // SqlExecutor`; cast to the minimal shape.
85
+ const exec = client;
86
+ await exec.query(AUTH_SCHEMA_SQL);
87
+ if (includeGrants) {
88
+ await exec.query(AUTH_SCHEMA_GRANTS_SQL);
89
+ }
90
+ }
91
+ /**
92
+ * Composite full DDL — the schema + helpers + grants, joined.
93
+ * Provided for migrate-gen + parity-fence byte-equality.
94
+ */
95
+ export function authSchemaFullSql(opts = {}) {
96
+ const includeGrants = opts.includeGrants ?? true;
97
+ return includeGrants
98
+ ? `${AUTH_SCHEMA_SQL}\n${AUTH_SCHEMA_GRANTS_SQL}`
99
+ : AUTH_SCHEMA_SQL;
100
+ }
101
+ //# sourceMappingURL=auth-schema.js.map
@@ -0,0 +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"}
@@ -0,0 +1,45 @@
1
+ import type { Pool } from "pg";
2
+ import { DbRlsError, type PoolKind } from "./types";
3
+ export interface ConfigureDbRlsOpts {
4
+ /** Per `nodii-libs/telemetry.md` § 5.4. Used in error messages + spans. */
5
+ serviceName: string;
6
+ /** Underlying pg.Pool instances per role. Caller owns lifecycle. */
7
+ pools: {
8
+ owner: Pool;
9
+ services: Pool;
10
+ authenticated: Pool;
11
+ };
12
+ /** Enable role-name validation at boot (default true). Set false in
13
+ * unit tests that wire raw postgres without dedicated roles. */
14
+ validateRoles?: boolean;
15
+ /** Slow-acquire threshold in ms — propagated to PgClientPool. */
16
+ slowAcquireMs?: number;
17
+ /** Hook fired on slow acquisitions. */
18
+ onSlowAcquire?: (info: {
19
+ pool: PoolKind;
20
+ durationMs: number;
21
+ }) => void;
22
+ /** Hook fired on every tx finish (commit/rollback). */
23
+ onTxFinish?: (info: {
24
+ pool: PoolKind;
25
+ outcome: "committed" | "rolled_back" | "error";
26
+ durationMs: number;
27
+ }) => void;
28
+ }
29
+ export declare class PoolCredentialMismatch extends DbRlsError {
30
+ readonly pool: PoolKind;
31
+ readonly expected: string;
32
+ readonly actual: string;
33
+ constructor(pool: PoolKind, expected: string, actual: string);
34
+ }
35
+ /**
36
+ * configureDbRls — call once at process start.
37
+ *
38
+ * On failure (credential mismatch, connection failure during validation),
39
+ * throws and leaves db-rls UNinitialized — the caller MUST surface and
40
+ * exit; running with the wrong credentials is a silent cross-tenant leak
41
+ * per the spec's failure modes.
42
+ */
43
+ export declare function configureDbRls(opts: ConfigureDbRlsOpts): Promise<void>;
44
+ export declare function _resetConfigureForTests(): void;
45
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAIpD,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,KAAK,EAAE;QACL,KAAK,EAAE,IAAI,CAAC;QACZ,QAAQ,EAAE,IAAI,CAAC;QACf,aAAa,EAAE,IAAI,CAAC;KACrB,CAAC;IACF;qEACiE;IACjE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACvE,uDAAuD;IACvD,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAClB,IAAI,EAAE,QAAQ,CAAC;QACf,OAAO,EAAE,WAAW,GAAG,aAAa,GAAG,OAAO,CAAC;QAC/C,UAAU,EAAE,MAAM,CAAC;KACpB,KAAK,IAAI,CAAC;CACZ;AAQD,qBAAa,sBAAuB,SAAQ,UAAU;IACpD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBACZ,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAS7D;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B5E;AAgBD,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
@@ -0,0 +1,85 @@
1
+ // configureDbRls — boot helper per § 5.2.
2
+ //
3
+ // Wraps initDbRls with:
4
+ // - Optional pool-credential validation (SELECT current_user per pool;
5
+ // fails boot loud on mismatch per § 5.2 + § 5.9 PoolCredentialMismatch).
6
+ // - Automatic conversion of `pg.Pool` instances → `PgClientPool` adapters
7
+ // so consumers just supply their raw pg.Pool from their existing
8
+ // wiring code.
9
+ // - Service name binding (currently used for log lines; future
10
+ // telemetry resource attribute).
11
+ import { DbRlsError } from "./types";
12
+ import { _resetForTests as resetInit, initDbRls } from "./init";
13
+ import { PgClientPool } from "./pg-pool-adapter";
14
+ const EXPECTED_ROLES = {
15
+ owner: "nodii_owner",
16
+ services: "nodii_services",
17
+ authenticated: "authenticated",
18
+ };
19
+ export class PoolCredentialMismatch extends DbRlsError {
20
+ pool;
21
+ expected;
22
+ actual;
23
+ constructor(pool, expected, actual) {
24
+ super(`pool_credential_mismatch: ${pool} pool connected as '${actual}' but expected '${expected}' — wire the correct role's credentials per § 5.2.`);
25
+ this.name = "PoolCredentialMismatch";
26
+ this.pool = pool;
27
+ this.expected = expected;
28
+ this.actual = actual;
29
+ }
30
+ }
31
+ /**
32
+ * configureDbRls — call once at process start.
33
+ *
34
+ * On failure (credential mismatch, connection failure during validation),
35
+ * throws and leaves db-rls UNinitialized — the caller MUST surface and
36
+ * exit; running with the wrong credentials is a silent cross-tenant leak
37
+ * per the spec's failure modes.
38
+ */
39
+ export async function configureDbRls(opts) {
40
+ const validate = opts.validateRoles ?? true;
41
+ if (validate) {
42
+ await assertPoolRole(opts.pools.owner, "owner");
43
+ await assertPoolRole(opts.pools.services, "services");
44
+ await assertPoolRole(opts.pools.authenticated, "authenticated");
45
+ }
46
+ const owner = new PgClientPool({
47
+ pool: opts.pools.owner,
48
+ kind: "owner",
49
+ slowAcquireMs: opts.slowAcquireMs,
50
+ onSlowAcquire: opts.onSlowAcquire,
51
+ onTxFinish: opts.onTxFinish,
52
+ });
53
+ const services = new PgClientPool({
54
+ pool: opts.pools.services,
55
+ kind: "services",
56
+ slowAcquireMs: opts.slowAcquireMs,
57
+ onSlowAcquire: opts.onSlowAcquire,
58
+ onTxFinish: opts.onTxFinish,
59
+ });
60
+ const authenticated = new PgClientPool({
61
+ pool: opts.pools.authenticated,
62
+ kind: "authenticated",
63
+ slowAcquireMs: opts.slowAcquireMs,
64
+ onSlowAcquire: opts.onSlowAcquire,
65
+ onTxFinish: opts.onTxFinish,
66
+ });
67
+ initDbRls({ owner, services, authenticated });
68
+ }
69
+ async function assertPoolRole(pool, kind) {
70
+ const expected = EXPECTED_ROLES[kind];
71
+ const res = await pool.query("SELECT current_user");
72
+ // pg returns the field lowercase.
73
+ // biome-ignore lint/suspicious/noExplicitAny: shape from pg lib
74
+ const actual = res.rows[0]?.current_user;
75
+ if (!actual) {
76
+ throw new PoolCredentialMismatch(kind, expected, "(unknown)");
77
+ }
78
+ if (actual !== expected) {
79
+ throw new PoolCredentialMismatch(kind, expected, actual);
80
+ }
81
+ }
82
+ export function _resetConfigureForTests() {
83
+ resetInit();
84
+ }
85
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configure.js","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { type AutoDispatchOpts, type PgTx, type SystemContextOpts, type UserContextOpts } from "./types";
2
+ /**
3
+ * withUserContext — opens a tx on the `authenticated` pool, sets
4
+ * `auth.user_id` + `auth.tenant_id` (+ optional `auth.source_ip`) via
5
+ * `set_config(..., true)` tx-scoped GUCs, yields the tx, commits/rolls
6
+ * back per the PgPool contract.
7
+ *
8
+ * Throws `DbRlsNotInitialized` if `initDbRls` hasn't run.
9
+ * Wraps any error thrown by `fn` in `TransactionRolledBack`.
10
+ */
11
+ export declare function withUserContext<T>(opts: UserContextOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
12
+ /**
13
+ * withSystemContext — opens a tx on the `services` (nodii_services) pool,
14
+ * sets `app.tenant_id` via tx-scoped `set_config`, yields the tx, commits
15
+ * / rolls back per the PgPool contract.
16
+ */
17
+ export declare function withSystemContext<T>(opts: SystemContextOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
18
+ /**
19
+ * withOwnerContext — opens a tx on the `owner` (nodii_owner BYPASSRLS)
20
+ * pool. No GUCs are set — owner is the migrations-only context.
21
+ *
22
+ * NOTE: v0.1.0 ships this on the runtime barrel for symmetry. The spec
23
+ * § 5.3 + § Open Q3 calls out that future v0.1.x moves this to
24
+ * `@nodii/db-rls/migrate` sub-package + adds a `NODII_ENV=migrate`
25
+ * runtime guard. Documented as `// TODO db-rls-v0.1.x: move to /migrate
26
+ * sub-package + NODII_ENV guard`.
27
+ */
28
+ export declare function withOwnerContext<T>(fn: (tx: PgTx) => Promise<T>): Promise<T>;
29
+ /**
30
+ * autoDispatch — chooses the right pool based on what's bound.
31
+ *
32
+ * - `authContext` present → dispatch through withUserContext
33
+ * (uses authContext.userId + authContext.tenantId).
34
+ * - only `tenantId` present → dispatch through withSystemContext.
35
+ * - neither → throw `MissingTenantContext`.
36
+ *
37
+ * This mirrors `withRequestContext` from the spec § 5.4 with one
38
+ * simplification: the auth context is passed explicitly rather than
39
+ * being read from ALS. Real auth-sdk integration lands in v0.1.x —
40
+ * `// TODO db-rls-v0.1.x: ALS-bound auto-dispatch`.
41
+ */
42
+ export declare function autoDispatch<T>(opts: AutoDispatchOpts, fn: (tx: PgTx) => Promise<T>): Promise<T>;
43
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,IAAI,EACT,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EACrB,MAAM,SAAS,CAAC;AAEjB;;;;;;;;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,CAcZ;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;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3B,OAAO,CAAC,CAAC,CAAC,CASZ;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"}
@@ -0,0 +1,98 @@
1
+ // Entry-point helpers — explicit pool dispatch.
2
+ //
3
+ // Spec § 5.3 + § 5.4. Each helper opens a tx on the correct pool, sets
4
+ // the canonical GUCs via `set_config(..., true)` (tx-scoped), yields, and
5
+ // commits / rollbacks per the underlying PgPool's withTransaction
6
+ // contract.
7
+ import { requirePool } from "./init";
8
+ import { MissingTenantContext, TransactionRolledBack, } from "./types";
9
+ /**
10
+ * withUserContext — opens a tx on the `authenticated` pool, sets
11
+ * `auth.user_id` + `auth.tenant_id` (+ optional `auth.source_ip`) via
12
+ * `set_config(..., true)` tx-scoped GUCs, yields the tx, commits/rolls
13
+ * back per the PgPool contract.
14
+ *
15
+ * Throws `DbRlsNotInitialized` if `initDbRls` hasn't run.
16
+ * Wraps any error thrown by `fn` in `TransactionRolledBack`.
17
+ */
18
+ export async function withUserContext(opts, fn) {
19
+ const pool = requirePool("authenticated");
20
+ return pool.withTransaction(async (tx) => {
21
+ await tx.setConfig("auth.user_id", opts.userId);
22
+ await tx.setConfig("auth.tenant_id", opts.tenantId);
23
+ if (opts.sourceIp !== undefined) {
24
+ await tx.setConfig("auth.source_ip", opts.sourceIp);
25
+ }
26
+ try {
27
+ return await fn(tx);
28
+ }
29
+ catch (cause) {
30
+ throw new TransactionRolledBack(cause);
31
+ }
32
+ });
33
+ }
34
+ /**
35
+ * withSystemContext — opens a tx on the `services` (nodii_services) pool,
36
+ * sets `app.tenant_id` via tx-scoped `set_config`, yields the tx, commits
37
+ * / rolls back per the PgPool contract.
38
+ */
39
+ export async function withSystemContext(opts, fn) {
40
+ const pool = requirePool("services");
41
+ return pool.withTransaction(async (tx) => {
42
+ await tx.setConfig("app.tenant_id", opts.tenantId);
43
+ try {
44
+ return await fn(tx);
45
+ }
46
+ catch (cause) {
47
+ throw new TransactionRolledBack(cause);
48
+ }
49
+ });
50
+ }
51
+ /**
52
+ * withOwnerContext — opens a tx on the `owner` (nodii_owner BYPASSRLS)
53
+ * pool. No GUCs are set — owner is the migrations-only context.
54
+ *
55
+ * NOTE: v0.1.0 ships this on the runtime barrel for symmetry. The spec
56
+ * § 5.3 + § Open Q3 calls out that future v0.1.x moves this to
57
+ * `@nodii/db-rls/migrate` sub-package + adds a `NODII_ENV=migrate`
58
+ * runtime guard. Documented as `// TODO db-rls-v0.1.x: move to /migrate
59
+ * sub-package + NODII_ENV guard`.
60
+ */
61
+ export async function withOwnerContext(fn) {
62
+ const pool = requirePool("owner");
63
+ return pool.withTransaction(async (tx) => {
64
+ try {
65
+ return await fn(tx);
66
+ }
67
+ catch (cause) {
68
+ throw new TransactionRolledBack(cause);
69
+ }
70
+ });
71
+ }
72
+ /**
73
+ * autoDispatch — chooses the right pool based on what's bound.
74
+ *
75
+ * - `authContext` present → dispatch through withUserContext
76
+ * (uses authContext.userId + authContext.tenantId).
77
+ * - only `tenantId` present → dispatch through withSystemContext.
78
+ * - neither → throw `MissingTenantContext`.
79
+ *
80
+ * This mirrors `withRequestContext` from the spec § 5.4 with one
81
+ * simplification: the auth context is passed explicitly rather than
82
+ * being read from ALS. Real auth-sdk integration lands in v0.1.x —
83
+ * `// TODO db-rls-v0.1.x: ALS-bound auto-dispatch`.
84
+ */
85
+ export async function autoDispatch(opts, fn) {
86
+ if (opts.authContext) {
87
+ return withUserContext({
88
+ userId: opts.authContext.userId,
89
+ tenantId: opts.authContext.tenantId,
90
+ sourceIp: opts.authContext.sourceIp,
91
+ }, fn);
92
+ }
93
+ if (opts.tenantId !== undefined) {
94
+ return withSystemContext({ tenantId: opts.tenantId }, fn);
95
+ }
96
+ throw new MissingTenantContext();
97
+ }
98
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,uEAAuE;AACvE,0EAA0E;AAC1E,kEAAkE;AAClE,YAAY;AAEZ,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAEL,oBAAoB,EAGpB,qBAAqB,GAEtB,MAAM,SAAS,CAAC;AAEjB;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAqB,EACrB,EAA4B;IAE5B,MAAM,IAAI,GAAG,WAAW,CAAC,eAAe,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACvC,MAAM,EAAE,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAChD,MAAM,EAAE,CAAC,SAAS,CAAC,gBAAgB,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,EAAE,CAAC,SAAS,CAAC,gBAAgB,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtD,CAAC;QACD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,qBAAqB,CAAC,KAAK,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAuB,EACvB,EAA4B;IAE5B,MAAM,IAAI,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACrC,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACvC,MAAM,EAAE,CAAC,SAAS,CAAC,eAAe,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,qBAAqB,CAAC,KAAK,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,EAA4B;IAE5B,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACvC,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,qBAAqB,CAAC,KAAK,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAsB,EACtB,EAA4B;IAE5B,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,OAAO,eAAe,CACpB;YACE,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,MAAM;YAC/B,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ;YACnC,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ;SACpC,EACD,EAAE,CACH,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,iBAAiB,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,IAAI,oBAAoB,EAAE,CAAC;AACnC,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { type NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import type { SystemContextOpts, UserContextOpts } from "./types";
3
+ type DrizzleClient<TSchema extends Record<string, unknown> = Record<string, unknown>> = NodePgDatabase<TSchema>;
4
+ /**
5
+ * withUserContext (Drizzle variant) — opens a tx on the `authenticated`
6
+ * pool, sets `app.user_id` + `app.tenant_id` via `auth.init(...)` (the
7
+ * canonical helper from § 5.7), yields a Drizzle client bound to the tx,
8
+ * commits/rolls back.
9
+ */
10
+ export declare function withUserContextDb<T, TSchema extends Record<string, unknown> = Record<string, unknown>>(opts: UserContextOpts, fn: (db: DrizzleClient<TSchema>) => Promise<T>): Promise<T>;
11
+ /**
12
+ * withSystemContext (Drizzle variant) — opens a tx on the `services` pool
13
+ * with `app.tenant_id` GUC bound; yields a Drizzle client.
14
+ */
15
+ export declare function withSystemContextDb<T, TSchema extends Record<string, unknown> = Record<string, unknown>>(opts: SystemContextOpts, fn: (db: DrizzleClient<TSchema>) => Promise<T>): Promise<T>;
16
+ /**
17
+ * withOwnerContext (Drizzle variant) — opens a tx on the `owner`
18
+ * (BYPASSRLS) pool; no GUCs. Migrations only per § 5.3 + Q3.
19
+ */
20
+ export declare function withOwnerContextDb<T, TSchema extends Record<string, unknown> = Record<string, unknown>>(fn: (db: DrizzleClient<TSchema>) => Promise<T>): Promise<T>;
21
+ export type { DrizzleClient, NodePgDatabase };
22
+ //# sourceMappingURL=drizzle-client.d.ts.map
@@ -0,0 +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,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAOlE,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;AAmD5B;;;;;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;;;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;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,CAAC,EACD,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,EAAE,EAAE,CAAC,EAAE,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO5D;AAED,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,109 @@
1
+ // Real Drizzle client integration per § 5.8 + R1 § 4.
2
+ //
3
+ // withUserContext / withSystemContext / withOwnerContext return a usable
4
+ // Drizzle client bound to the transaction. The consumer uses it exactly
5
+ // as they would Drizzle's `drizzle(pool)` output — same methods,
6
+ // same chaining, same query builder — but every statement runs through
7
+ // the BEGIN/set_config/COMMIT lifecycle on the right pool.
8
+ //
9
+ // Compatibility:
10
+ // - We import `drizzle` from `drizzle-orm/node-postgres` (which targets
11
+ // the `pg` driver) — this is what every adopter uses today (saga,
12
+ // audit-chain, dsar).
13
+ // - The returned client is a `NodePgDatabase<any>` so consumers can
14
+ // pass their schema typing through.
15
+ // - We expose `withUserContextDb` / `withSystemContextDb` /
16
+ // `withOwnerContextDb` variants that emit the Drizzle client. The
17
+ // original `withUserContext` (in src/context.ts) still works for
18
+ // services using the raw `PgTx` surface.
19
+ import { drizzle } from "drizzle-orm/node-postgres";
20
+ import { requirePool } from "./init";
21
+ import { PgClientPool } from "./pg-pool-adapter";
22
+ import { MissingTenantContext, TransactionRolledBack } from "./types";
23
+ function clientFor(_pool) {
24
+ // Only PgClientPool exposes a real pg PoolClient. In-memory pools don't.
25
+ // The caller is responsible for using *Db helpers only against real pools.
26
+ return null;
27
+ }
28
+ // (unused — keeps the symbol referenced; trimmed by tree-shake)
29
+ void clientFor;
30
+ async function withRealTx(pool, setup, fn) {
31
+ const acquireStart = Date.now();
32
+ const client = await pool.rawPool.connect();
33
+ void acquireStart;
34
+ try {
35
+ await client.query("BEGIN");
36
+ try {
37
+ await setup(client);
38
+ const db = drizzle(client);
39
+ const out = await fn(db, client);
40
+ await client.query("COMMIT");
41
+ return out;
42
+ }
43
+ catch (err) {
44
+ try {
45
+ await client.query("ROLLBACK");
46
+ }
47
+ catch {
48
+ /* swallow */
49
+ }
50
+ throw new TransactionRolledBack(err);
51
+ }
52
+ }
53
+ finally {
54
+ client.release();
55
+ }
56
+ }
57
+ function requireRealPool(kind) {
58
+ const pool = requirePool(kind);
59
+ if (!(pool instanceof PgClientPool)) {
60
+ throw new Error(`db-rls: ${kind} pool is not a PgClientPool — Drizzle helpers require a real pg-backed pool. Did you wire an InMemoryPgPool by mistake?`);
61
+ }
62
+ return pool;
63
+ }
64
+ /**
65
+ * withUserContext (Drizzle variant) — opens a tx on the `authenticated`
66
+ * pool, sets `app.user_id` + `app.tenant_id` via `auth.init(...)` (the
67
+ * canonical helper from § 5.7), yields a Drizzle client bound to the tx,
68
+ * commits/rolls back.
69
+ */
70
+ export async function withUserContextDb(opts, fn) {
71
+ if (!opts.userId)
72
+ throw new MissingTenantContext();
73
+ if (!opts.tenantId)
74
+ throw new MissingTenantContext();
75
+ const pool = requireRealPool("authenticated");
76
+ return withRealTx(pool, async (client) => {
77
+ // Set both GUCs via the canonical auth.init helper.
78
+ await client.query("SELECT auth.init($1::uuid, $2::uuid)", [
79
+ opts.userId,
80
+ opts.tenantId,
81
+ ]);
82
+ if (opts.sourceIp !== undefined) {
83
+ await client.query("SELECT set_config('app.source_ip', $1, true)", [
84
+ opts.sourceIp,
85
+ ]);
86
+ }
87
+ }, (db) => fn(db));
88
+ }
89
+ /**
90
+ * withSystemContext (Drizzle variant) — opens a tx on the `services` pool
91
+ * with `app.tenant_id` GUC bound; yields a Drizzle client.
92
+ */
93
+ export async function withSystemContextDb(opts, fn) {
94
+ if (!opts.tenantId)
95
+ throw new MissingTenantContext();
96
+ const pool = requireRealPool("services");
97
+ return withRealTx(pool, async (client) => {
98
+ await client.query("SELECT auth.init_tenant($1::uuid)", [opts.tenantId]);
99
+ }, (db) => fn(db));
100
+ }
101
+ /**
102
+ * withOwnerContext (Drizzle variant) — opens a tx on the `owner`
103
+ * (BYPASSRLS) pool; no GUCs. Migrations only per § 5.3 + Q3.
104
+ */
105
+ export async function withOwnerContextDb(fn) {
106
+ const pool = requireRealPool("owner");
107
+ return withRealTx(pool, async () => { }, (db) => fn(db));
108
+ }
109
+ //# sourceMappingURL=drizzle-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle-client.js","sourceRoot":"","sources":["../src/drizzle-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { PgPool, PgTx, PoolKind } from "./types";
2
+ /** One transcript entry — either a set_config or a raw exec. */
3
+ export type TranscriptEntry = {
4
+ kind: "begin";
5
+ pool: PoolKind;
6
+ } | {
7
+ kind: "set_config";
8
+ pool: PoolKind;
9
+ key: string;
10
+ value: string;
11
+ } | {
12
+ kind: "exec";
13
+ pool: PoolKind;
14
+ sql: string;
15
+ params: readonly unknown[];
16
+ } | {
17
+ kind: "commit";
18
+ pool: PoolKind;
19
+ } | {
20
+ kind: "rollback";
21
+ pool: PoolKind;
22
+ };
23
+ /** In-memory PgPool. Each instance owns its own transcript so tests can
24
+ * inspect what each pool was asked to do. */
25
+ export declare class InMemoryPgPool implements PgPool {
26
+ readonly kind: PoolKind;
27
+ readonly transcript: TranscriptEntry[];
28
+ constructor(kind: PoolKind);
29
+ withTransaction<T>(fn: (tx: PgTx) => Promise<T>): Promise<T>;
30
+ }
31
+ //# sourceMappingURL=in-memory-pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory-pool.d.ts","sourceRoot":"","sources":["../src/in-memory-pool.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEtD,gEAAgE;AAChE,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAA;CAAE,GACzE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEzC;8CAC8C;AAC9C,qBAAa,cAAe,YAAW,MAAM;IAC3C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,eAAe,EAAE,CAAM;gBAEhC,IAAI,EAAE,QAAQ;IAIpB,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAYnE"}