@nodii/db-rls 0.0.2 → 0.2.1
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/bin/migrate-gen.mjs +12 -0
- package/dist/auth-schema.d.ts +45 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +101 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/configure.d.ts +45 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +85 -0
- package/dist/configure.js.map +1 -0
- package/dist/context.d.ts +43 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +98 -0
- package/dist/context.js.map +1 -0
- package/dist/drizzle-client.d.ts +22 -0
- package/dist/drizzle-client.d.ts.map +1 -0
- package/dist/drizzle-client.js +109 -0
- package/dist/drizzle-client.js.map +1 -0
- package/dist/in-memory-pool.d.ts +31 -0
- package/dist/in-memory-pool.d.ts.map +1 -0
- package/dist/in-memory-pool.js +53 -0
- package/dist/in-memory-pool.js.map +1 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +18 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +40 -0
- package/dist/init.js.map +1 -0
- package/dist/migrate-gen.d.ts +41 -0
- package/dist/migrate-gen.d.ts.map +1 -0
- package/dist/migrate-gen.js +123 -0
- package/dist/migrate-gen.js.map +1 -0
- package/dist/pg-pool-adapter.d.ts +44 -0
- package/dist/pg-pool-adapter.d.ts.map +1 -0
- package/dist/pg-pool-adapter.js +111 -0
- package/dist/pg-pool-adapter.js.map +1 -0
- package/dist/rls-policy-gen.d.ts +26 -0
- package/dist/rls-policy-gen.d.ts.map +1 -0
- package/dist/rls-policy-gen.js +81 -0
- package/dist/rls-policy-gen.js.map +1 -0
- package/dist/rls-policy.d.ts +30 -0
- package/dist/rls-policy.d.ts.map +1 -0
- package/dist/rls-policy.js +81 -0
- package/dist/rls-policy.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +56 -0
- package/dist/types.js.map +1 -0
- 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"}
|
package/dist/context.js
ADDED
|
@@ -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"}
|