@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,81 @@
|
|
|
1
|
+
// withRls — emits the 3-policy DDL for a tenant-scoped table per § 5.5.
|
|
2
|
+
//
|
|
3
|
+
// One policy per role: nodii_owner (FOR ALL bypass), authenticated (FOR
|
|
4
|
+
// ALL gated on auth.user_tenant_ids), nodii_services (FOR ALL gated on
|
|
5
|
+
// app.tenant_id GUC). Per § 5.5 the `{ allow }` option restricts which
|
|
6
|
+
// CRUD verbs the non-owner roles get; owner ALWAYS gets FOR ALL.
|
|
7
|
+
//
|
|
8
|
+
// Output byte-equal across TS / Python / Go ports.
|
|
9
|
+
const DEFAULT_ALLOW = ["SELECT", "INSERT"];
|
|
10
|
+
/**
|
|
11
|
+
* Emits the policy DDL block for `table`.
|
|
12
|
+
*
|
|
13
|
+
* Returned string ends with `\n` for file-friendliness. Single
|
|
14
|
+
* deterministic output (no whitespace drift, stable ordering) so the
|
|
15
|
+
* parity-fence test is byte-equal.
|
|
16
|
+
*
|
|
17
|
+
* If `table` is schema-qualified (`schema.tbl`), the ALTER + ON clauses
|
|
18
|
+
* use the qualified form; the policy NAME uses only the unqualified
|
|
19
|
+
* tail (Postgres policy names live in the table's namespace and must
|
|
20
|
+
* be a plain identifier, not a dotted path).
|
|
21
|
+
*/
|
|
22
|
+
export function withRls(table, opts = {}) {
|
|
23
|
+
const tenantColumn = opts.tenantColumn ?? "tenant_id";
|
|
24
|
+
const verbs = opts.allow && opts.allow.length > 0 ? opts.allow : DEFAULT_ALLOW;
|
|
25
|
+
const policyBase = unqualifiedName(table);
|
|
26
|
+
const lines = [];
|
|
27
|
+
lines.push(`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;`);
|
|
28
|
+
// 1. Owner policy: FOR ALL bypass.
|
|
29
|
+
lines.push(`CREATE POLICY ${policyBase}_owner_policy ON ${table} FOR ALL TO nodii_owner USING (true) WITH CHECK (true);`);
|
|
30
|
+
// 2. Authenticated policy: gated on auth.user_tenant_ids(auth.get_user_id()).
|
|
31
|
+
// One policy per verb so the surface honestly reflects sanctioned
|
|
32
|
+
// mutations.
|
|
33
|
+
for (const verb of verbs) {
|
|
34
|
+
lines.push(authPolicyFor(table, policyBase, tenantColumn, verb));
|
|
35
|
+
}
|
|
36
|
+
// 3. Services policy: gated on app.tenant_id GUC.
|
|
37
|
+
for (const verb of verbs) {
|
|
38
|
+
lines.push(servicesPolicyFor(table, policyBase, tenantColumn, verb));
|
|
39
|
+
}
|
|
40
|
+
return `${lines.join("\n")}\n`;
|
|
41
|
+
}
|
|
42
|
+
function unqualifiedName(table) {
|
|
43
|
+
// Strip optional schema prefix + any surrounding quotes.
|
|
44
|
+
const tail = table.includes(".") ? (table.split(".").pop() ?? table) : table;
|
|
45
|
+
return tail.replace(/^"|"$/g, "");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Per-table grants — non-owner roles need GRANT on the verbs in `allow`.
|
|
49
|
+
* Owner doesn't need a grant (BYPASSRLS + table-owner).
|
|
50
|
+
*/
|
|
51
|
+
export function generateTableGrants(table, opts = {}) {
|
|
52
|
+
const verbs = opts.allow && opts.allow.length > 0 ? opts.allow : DEFAULT_ALLOW;
|
|
53
|
+
const verbList = verbs.join(", ");
|
|
54
|
+
const lines = [
|
|
55
|
+
`GRANT ${verbList} ON ${table} TO nodii_services;`,
|
|
56
|
+
`GRANT ${verbList} ON ${table} TO authenticated;`,
|
|
57
|
+
];
|
|
58
|
+
return `${lines.join("\n")}\n`;
|
|
59
|
+
}
|
|
60
|
+
function authPolicyFor(table, policyBase, tenantColumn, verb) {
|
|
61
|
+
// Authenticated role: tenant_id must be one of auth.user_tenant_ids(auth.get_user_id()).
|
|
62
|
+
const cond = `${tenantColumn} IN (SELECT auth.user_tenant_ids(auth.get_user_id()))`;
|
|
63
|
+
return policyLine(table, policyBase, "auth", verb, "authenticated", cond);
|
|
64
|
+
}
|
|
65
|
+
function servicesPolicyFor(table, policyBase, tenantColumn, verb) {
|
|
66
|
+
// Services role: tenant_id must equal current_setting('app.tenant_id').
|
|
67
|
+
const cond = `${tenantColumn} = auth.get_tenant_id()`;
|
|
68
|
+
return policyLine(table, policyBase, "svc", verb, "nodii_services", cond);
|
|
69
|
+
}
|
|
70
|
+
function policyLine(table, policyBase, flavor, verb, role, cond) {
|
|
71
|
+
const lcVerb = verb.toLowerCase();
|
|
72
|
+
const policyName = `${policyBase}_${flavor}_${lcVerb}_policy`;
|
|
73
|
+
if (verb === "INSERT") {
|
|
74
|
+
return `CREATE POLICY ${policyName} ON ${table} FOR INSERT TO ${role} WITH CHECK (${cond});`;
|
|
75
|
+
}
|
|
76
|
+
if (verb === "UPDATE") {
|
|
77
|
+
return `CREATE POLICY ${policyName} ON ${table} FOR UPDATE TO ${role} USING (${cond}) WITH CHECK (${cond});`;
|
|
78
|
+
}
|
|
79
|
+
return `CREATE POLICY ${policyName} ON ${table} FOR ${verb} TO ${role} USING (${cond});`;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=rls-policy-gen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rls-policy-gen.js","sourceRoot":"","sources":["../src/rls-policy-gen.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,wEAAwE;AACxE,uEAAuE;AACvE,uEAAuE;AACvE,iEAAiE;AACjE,EAAE;AACF,mDAAmD;AAWnD,MAAM,aAAa,GAAuB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO,CAAC,KAAa,EAAE,OAAoB,EAAE;IAC3D,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,WAAW,CAAC;IACtD,MAAM,KAAK,GACT,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC;IACnE,MAAM,UAAU,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IAE1C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,eAAe,KAAK,6BAA6B,CAAC,CAAC;IAC9D,mCAAmC;IACnC,KAAK,CAAC,IAAI,CACR,iBAAiB,UAAU,oBAAoB,KAAK,yDAAyD,CAC9G,CAAC;IAEF,8EAA8E;IAC9E,qEAAqE;IACrE,gBAAgB;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,kDAAkD;IAClD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC;IACvE,CAAC;IAED,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,yDAAyD;IACzD,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAC7E,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAa,EACb,OAAoB,EAAE;IAEtB,MAAM,KAAK,GACT,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC;IACnE,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG;QACZ,SAAS,QAAQ,OAAO,KAAK,qBAAqB;QAClD,SAAS,QAAQ,OAAO,KAAK,oBAAoB;KAClD,CAAC;IACF,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,SAAS,aAAa,CACpB,KAAa,EACb,UAAkB,EAClB,YAAoB,EACpB,IAAa;IAEb,yFAAyF;IACzF,MAAM,IAAI,GAAG,GAAG,YAAY,uDAAuD,CAAC;IACpF,OAAO,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,iBAAiB,CACxB,KAAa,EACb,UAAkB,EAClB,YAAoB,EACpB,IAAa;IAEb,wEAAwE;IACxE,MAAM,IAAI,GAAG,GAAG,YAAY,yBAAyB,CAAC;IACtD,OAAO,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,UAAU,CACjB,KAAa,EACb,UAAkB,EAClB,MAAsB,EACtB,IAAa,EACb,IAAY,EACZ,IAAY;IAEZ,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAClC,MAAM,UAAU,GAAG,GAAG,UAAU,IAAI,MAAM,IAAI,MAAM,SAAS,CAAC;IAC9D,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO,iBAAiB,UAAU,OAAO,KAAK,kBAAkB,IAAI,gBAAgB,IAAI,IAAI,CAAC;IAC/F,CAAC;IACD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO,iBAAiB,UAAU,OAAO,KAAK,kBAAkB,IAAI,WAAW,IAAI,iBAAiB,IAAI,IAAI,CAAC;IAC/G,CAAC;IACD,OAAO,iBAAiB,UAAU,OAAO,KAAK,QAAQ,IAAI,OAAO,IAAI,WAAW,IAAI,IAAI,CAAC;AAC3F,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GenerateRlsPolicyOpts } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Generates the canonical 3-policy DDL block:
|
|
4
|
+
*
|
|
5
|
+
* 1. `<table>_owner_policy` — nodii_owner FOR ALL USING (true) WITH CHECK (true)
|
|
6
|
+
* 2. `<table>_auth_policy` — authenticated FOR ALL USING (<tenant_col> IN auth.user_tenant_ids(...))
|
|
7
|
+
* 3. `<table>_services_policy` — nodii_services FOR ALL USING (<tenant_col> = current_setting('app.tenant_id')::uuid)
|
|
8
|
+
*
|
|
9
|
+
* Plus the `ALTER TABLE ... ENABLE ROW LEVEL SECURITY;` prelude.
|
|
10
|
+
*
|
|
11
|
+
* Output is deterministic (no whitespace drift, stable column ordering)
|
|
12
|
+
* for cross-language byte-equality.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateRlsPolicy(opts: GenerateRlsPolicyOpts): string;
|
|
15
|
+
/**
|
|
16
|
+
* authSchemaHelpers — returns the canonical SECURITY DEFINER SQL block
|
|
17
|
+
* defining the helper functions services rely on (`auth.set_user_id`,
|
|
18
|
+
* `auth.set_tenant_id`, `auth.set_source_ip`, `auth.get_user_id`,
|
|
19
|
+
* `auth.get_tenant_id`, `auth.user_tenant_ids`).
|
|
20
|
+
*
|
|
21
|
+
* Service-owned helpers (`auth.user_tenant_ids` joins
|
|
22
|
+
* `employees_mirror` + `customers_mirror`) — per spec § 5.7, the real
|
|
23
|
+
* version is generated by `migrate-gen` and JOINs the service's mirror
|
|
24
|
+
* tables. v0.1.0 ships the stub variant — returns the empty set so
|
|
25
|
+
* RLS fails closed if the service hasn't replaced the helper yet.
|
|
26
|
+
*
|
|
27
|
+
* Output is byte-equal across TS / Python / Go ports.
|
|
28
|
+
*/
|
|
29
|
+
export declare function authSchemaHelpers(): string;
|
|
30
|
+
//# sourceMappingURL=rls-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rls-policy.d.ts","sourceRoot":"","sources":["../src/rls-policy.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,MAAM,CAWrE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAkC1C"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// generateRlsPolicy — returns the canonical 3-policy DDL string for a
|
|
2
|
+
// tenant-scoped table per audit doctrine § 12 + PII doctrine § 8.
|
|
3
|
+
//
|
|
4
|
+
// Spec § 5.5 (A1 posture): every tenant-scoped table gets RLS via this
|
|
5
|
+
// helper. The caller writes the DDL into a migration. Output is
|
|
6
|
+
// byte-equal across TS / Python / Go ports — the polyglot parity-fence
|
|
7
|
+
// test asserts this directly.
|
|
8
|
+
/**
|
|
9
|
+
* Generates the canonical 3-policy DDL block:
|
|
10
|
+
*
|
|
11
|
+
* 1. `<table>_owner_policy` — nodii_owner FOR ALL USING (true) WITH CHECK (true)
|
|
12
|
+
* 2. `<table>_auth_policy` — authenticated FOR ALL USING (<tenant_col> IN auth.user_tenant_ids(...))
|
|
13
|
+
* 3. `<table>_services_policy` — nodii_services FOR ALL USING (<tenant_col> = current_setting('app.tenant_id')::uuid)
|
|
14
|
+
*
|
|
15
|
+
* Plus the `ALTER TABLE ... ENABLE ROW LEVEL SECURITY;` prelude.
|
|
16
|
+
*
|
|
17
|
+
* Output is deterministic (no whitespace drift, stable column ordering)
|
|
18
|
+
* for cross-language byte-equality.
|
|
19
|
+
*/
|
|
20
|
+
export function generateRlsPolicy(opts) {
|
|
21
|
+
const table = opts.table;
|
|
22
|
+
const tenantColumn = opts.tenantColumn ?? "tenant_id";
|
|
23
|
+
// Stable single-string with trailing newline per SQL file convention.
|
|
24
|
+
const lines = [
|
|
25
|
+
`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;`,
|
|
26
|
+
`CREATE POLICY ${table}_owner_policy ON ${table} FOR ALL TO nodii_owner USING (true) WITH CHECK (true);`,
|
|
27
|
+
`CREATE POLICY ${table}_auth_policy ON ${table} FOR ALL TO authenticated USING (${tenantColumn} IN (SELECT auth.user_tenant_ids(current_setting('app.user_id')::uuid))) WITH CHECK (${tenantColumn} IN (SELECT auth.user_tenant_ids(current_setting('app.user_id')::uuid)));`,
|
|
28
|
+
`CREATE POLICY ${table}_services_policy ON ${table} FOR ALL TO nodii_services USING (${tenantColumn} = current_setting('app.tenant_id')::uuid) WITH CHECK (${tenantColumn} = current_setting('app.tenant_id')::uuid);`,
|
|
29
|
+
];
|
|
30
|
+
return `${lines.join("\n")}\n`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* authSchemaHelpers — returns the canonical SECURITY DEFINER SQL block
|
|
34
|
+
* defining the helper functions services rely on (`auth.set_user_id`,
|
|
35
|
+
* `auth.set_tenant_id`, `auth.set_source_ip`, `auth.get_user_id`,
|
|
36
|
+
* `auth.get_tenant_id`, `auth.user_tenant_ids`).
|
|
37
|
+
*
|
|
38
|
+
* Service-owned helpers (`auth.user_tenant_ids` joins
|
|
39
|
+
* `employees_mirror` + `customers_mirror`) — per spec § 5.7, the real
|
|
40
|
+
* version is generated by `migrate-gen` and JOINs the service's mirror
|
|
41
|
+
* tables. v0.1.0 ships the stub variant — returns the empty set so
|
|
42
|
+
* RLS fails closed if the service hasn't replaced the helper yet.
|
|
43
|
+
*
|
|
44
|
+
* Output is byte-equal across TS / Python / Go ports.
|
|
45
|
+
*/
|
|
46
|
+
export function authSchemaHelpers() {
|
|
47
|
+
// Note: stable formatting (single space indent, no trailing whitespace)
|
|
48
|
+
// for cross-language byte-equality.
|
|
49
|
+
return [
|
|
50
|
+
"CREATE SCHEMA IF NOT EXISTS auth;",
|
|
51
|
+
"",
|
|
52
|
+
"CREATE OR REPLACE FUNCTION auth.set_user_id(p_user_id uuid) RETURNS void",
|
|
53
|
+
"LANGUAGE sql AS $$ SELECT set_config('app.user_id', p_user_id::text, true) $$;",
|
|
54
|
+
"",
|
|
55
|
+
"CREATE OR REPLACE FUNCTION auth.set_tenant_id(p_tenant_id uuid) RETURNS void",
|
|
56
|
+
"LANGUAGE sql AS $$ SELECT set_config('app.tenant_id', p_tenant_id::text, true) $$;",
|
|
57
|
+
"",
|
|
58
|
+
"CREATE OR REPLACE FUNCTION auth.set_source_ip(p_source_ip text) RETURNS void",
|
|
59
|
+
"LANGUAGE sql AS $$ SELECT set_config('app.source_ip', p_source_ip, true) $$;",
|
|
60
|
+
"",
|
|
61
|
+
"CREATE OR REPLACE FUNCTION auth.get_user_id() RETURNS uuid",
|
|
62
|
+
"LANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.user_id', true), '')::uuid $$;",
|
|
63
|
+
"",
|
|
64
|
+
"CREATE OR REPLACE FUNCTION auth.get_tenant_id() RETURNS uuid",
|
|
65
|
+
"LANGUAGE sql STABLE AS $$ SELECT nullif(current_setting('app.tenant_id', true), '')::uuid $$;",
|
|
66
|
+
"",
|
|
67
|
+
"-- TODO db-rls-v0.1.x: migrate-gen replaces this stub with a UNION over",
|
|
68
|
+
"-- the service's employees_mirror + customers_mirror tables.",
|
|
69
|
+
"CREATE OR REPLACE FUNCTION auth.user_tenant_ids(p_user_id uuid) RETURNS SETOF uuid",
|
|
70
|
+
"LANGUAGE sql STABLE AS $$ SELECT NULL::uuid WHERE false $$;",
|
|
71
|
+
"",
|
|
72
|
+
"GRANT EXECUTE ON FUNCTION auth.set_user_id(uuid) TO nodii_services, authenticated;",
|
|
73
|
+
"GRANT EXECUTE ON FUNCTION auth.set_tenant_id(uuid) TO nodii_services, authenticated;",
|
|
74
|
+
"GRANT EXECUTE ON FUNCTION auth.set_source_ip(text) TO nodii_services, authenticated;",
|
|
75
|
+
"GRANT EXECUTE ON FUNCTION auth.get_user_id() TO nodii_services, authenticated;",
|
|
76
|
+
"GRANT EXECUTE ON FUNCTION auth.get_tenant_id() TO nodii_services, authenticated;",
|
|
77
|
+
"GRANT EXECUTE ON FUNCTION auth.user_tenant_ids(uuid) TO nodii_services, authenticated;",
|
|
78
|
+
"",
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=rls-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rls-policy.js","sourceRoot":"","sources":["../src/rls-policy.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,kEAAkE;AAClE,EAAE;AACF,uEAAuE;AACvE,gEAAgE;AAChE,uEAAuE;AACvE,8BAA8B;AAI9B;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAA2B;IAC3D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACzB,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,WAAW,CAAC;IACtD,sEAAsE;IACtE,MAAM,KAAK,GAAG;QACZ,eAAe,KAAK,6BAA6B;QACjD,iBAAiB,KAAK,oBAAoB,KAAK,yDAAyD;QACxG,iBAAiB,KAAK,mBAAmB,KAAK,oCAAoC,YAAY,wFAAwF,YAAY,2EAA2E;QAC7Q,iBAAiB,KAAK,uBAAuB,KAAK,qCAAqC,YAAY,0DAA0D,YAAY,6CAA6C;KACvN,CAAC;IACF,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB;IAC/B,wEAAwE;IACxE,oCAAoC;IACpC,OAAO;QACL,mCAAmC;QACnC,EAAE;QACF,0EAA0E;QAC1E,gFAAgF;QAChF,EAAE;QACF,8EAA8E;QAC9E,oFAAoF;QACpF,EAAE;QACF,8EAA8E;QAC9E,8EAA8E;QAC9E,EAAE;QACF,4DAA4D;QAC5D,6FAA6F;QAC7F,EAAE;QACF,8DAA8D;QAC9D,+FAA+F;QAC/F,EAAE;QACF,yEAAyE;QACzE,8DAA8D;QAC9D,oFAAoF;QACpF,6DAA6D;QAC7D,EAAE;QACF,oFAAoF;QACpF,sFAAsF;QACtF,sFAAsF;QACtF,gFAAgF;QAChF,kFAAkF;QAClF,wFAAwF;QACxF,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** PoolKind names map 1:1 to the canonical three roles. */
|
|
2
|
+
export type PoolKind = "owner" | "services" | "authenticated";
|
|
3
|
+
/** Pluggable transaction handle — substrate-agnostic. v0.1.0 ships the
|
|
4
|
+
* in-memory transcript-recording test double; pg / psycopg / pgx adapters
|
|
5
|
+
* arrive in v0.1.x. */
|
|
6
|
+
export interface PgTx {
|
|
7
|
+
/** Execute a parameterized SQL statement. */
|
|
8
|
+
exec(sql: string, params?: readonly unknown[]): Promise<void>;
|
|
9
|
+
/** Set a per-tx GUC via `set_config($1, $2, true)`. */
|
|
10
|
+
setConfig(key: string, value: string): Promise<void>;
|
|
11
|
+
/** Internal — read the recorded transcript (test double only). */
|
|
12
|
+
readonly poolKind: PoolKind;
|
|
13
|
+
}
|
|
14
|
+
/** Pluggable Postgres pool — yields a tx for the lifetime of `fn`. */
|
|
15
|
+
export interface PgPool {
|
|
16
|
+
/** Identifier for telemetry + error messages. */
|
|
17
|
+
readonly kind: PoolKind;
|
|
18
|
+
/** Acquire a connection, BEGIN a tx, yield, and COMMIT/ROLLBACK. */
|
|
19
|
+
withTransaction<T>(fn: (tx: PgTx) => Promise<T>): Promise<T>;
|
|
20
|
+
}
|
|
21
|
+
/** Auth context bound by `@nodii/auth-sdk` middleware. Mirrors the
|
|
22
|
+
* fields autoDispatch reads — kept structural so the real auth-sdk
|
|
23
|
+
* context shape can grow without breaking us. */
|
|
24
|
+
export interface AuthContext {
|
|
25
|
+
/** Verified principal id (sub). */
|
|
26
|
+
userId: string;
|
|
27
|
+
/** Resolved tenant id (pinned by the JWT). */
|
|
28
|
+
tenantId: string;
|
|
29
|
+
/** Optional source IP for forensic GUC binding. */
|
|
30
|
+
sourceIp?: string;
|
|
31
|
+
}
|
|
32
|
+
/** withUserContext opts. */
|
|
33
|
+
export interface UserContextOpts {
|
|
34
|
+
userId: string;
|
|
35
|
+
tenantId: string;
|
|
36
|
+
sourceIp?: string;
|
|
37
|
+
}
|
|
38
|
+
/** withSystemContext opts. */
|
|
39
|
+
export interface SystemContextOpts {
|
|
40
|
+
tenantId: string;
|
|
41
|
+
}
|
|
42
|
+
/** autoDispatch opts. */
|
|
43
|
+
export interface AutoDispatchOpts {
|
|
44
|
+
authContext?: AuthContext;
|
|
45
|
+
tenantId?: string;
|
|
46
|
+
}
|
|
47
|
+
/** generateRlsPolicy opts. */
|
|
48
|
+
export interface GenerateRlsPolicyOpts {
|
|
49
|
+
/** Table name (unqualified). */
|
|
50
|
+
table: string;
|
|
51
|
+
/** Tenant column name. Defaults to `tenant_id`. */
|
|
52
|
+
tenantColumn?: string;
|
|
53
|
+
}
|
|
54
|
+
export declare class DbRlsError extends Error {
|
|
55
|
+
constructor(message: string);
|
|
56
|
+
}
|
|
57
|
+
export declare class DbRlsNotInitialized extends DbRlsError {
|
|
58
|
+
constructor();
|
|
59
|
+
}
|
|
60
|
+
export declare class PoolUnavailable extends DbRlsError {
|
|
61
|
+
readonly pool: PoolKind;
|
|
62
|
+
constructor(pool: PoolKind);
|
|
63
|
+
}
|
|
64
|
+
export declare class MissingTenantContext extends DbRlsError {
|
|
65
|
+
constructor();
|
|
66
|
+
}
|
|
67
|
+
export declare class TransactionRolledBack extends DbRlsError {
|
|
68
|
+
readonly cause: unknown;
|
|
69
|
+
constructor(cause: unknown);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAoBA,2DAA2D;AAC3D,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,eAAe,CAAC;AAE9D;;wBAEwB;AACxB,MAAM,WAAW,IAAI;IACnB,6CAA6C;IAC7C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,uDAAuD;IACvD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC7B;AAED,sEAAsE;AACtE,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,oEAAoE;IACpE,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC9D;AAED;;kDAEkD;AAClD,MAAM,WAAW,WAAW;IAC1B,mCAAmC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,4BAA4B;AAC5B,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,8BAA8B;AAC9B,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,yBAAyB;AACzB,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,8BAA8B;AAC9B,MAAM,WAAW,qBAAqB;IACpC,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAKD,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,mBAAoB,SAAQ,UAAU;;CAOlD;AAED,qBAAa,eAAgB,SAAQ,UAAU;IAC7C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;gBACZ,IAAI,EAAE,QAAQ;CAK3B;AAED,qBAAa,oBAAqB,SAAQ,UAAU;;CAOnD;AAED,qBAAa,qBAAsB,SAAQ,UAAU;IACnD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;gBACZ,KAAK,EAAE,OAAO;CAS3B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Core type surface for @nodii/db-rls v0.1.0.
|
|
2
|
+
//
|
|
3
|
+
// Spec: planning hub feature_doc serviceId=nodii-libs docKey=db-rls
|
|
4
|
+
// (A1 posture — RLS on every tenant-scoped table — locked 2026-05-25).
|
|
5
|
+
//
|
|
6
|
+
// v0.1.0 ships:
|
|
7
|
+
// - initDbRls / requireDbRls + DbRlsNotInitialized
|
|
8
|
+
// - PgPool / PgTx pluggable Postgres surface (in-memory test double)
|
|
9
|
+
// - withUserContext / withSystemContext / withOwnerContext entry points
|
|
10
|
+
// - autoDispatch (auth-sdk-style context routing)
|
|
11
|
+
// - generateRlsPolicy (canonical 3-policy DDL per audit doctrine § 12 +
|
|
12
|
+
// PII doctrine § 8)
|
|
13
|
+
// - authSchemaHelpers (canonical SECURITY DEFINER SQL block)
|
|
14
|
+
// - Typed-error hierarchy rooted at DbRlsError
|
|
15
|
+
//
|
|
16
|
+
// Deferred (`// TODO db-rls-v0.1.x:`):
|
|
17
|
+
// - Real pg / psycopg / pgx pool wiring
|
|
18
|
+
// - Drizzle / SQLAlchemy / sqlc client wrappers
|
|
19
|
+
// - migrate-gen CLI that emits per-service auth.sql + per-table RLS DDL
|
|
20
|
+
// ----------------------------------------------------------------------------
|
|
21
|
+
// Typed-error hierarchy.
|
|
22
|
+
export class DbRlsError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "DbRlsError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class DbRlsNotInitialized extends DbRlsError {
|
|
29
|
+
constructor() {
|
|
30
|
+
super("@nodii/db-rls is not initialized — call initDbRls({ owner, services, authenticated }) at process start.");
|
|
31
|
+
this.name = "DbRlsNotInitialized";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class PoolUnavailable extends DbRlsError {
|
|
35
|
+
pool;
|
|
36
|
+
constructor(pool) {
|
|
37
|
+
super(`pool_unavailable: the ${pool} pool was not supplied to initDbRls.`);
|
|
38
|
+
this.name = "PoolUnavailable";
|
|
39
|
+
this.pool = pool;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class MissingTenantContext extends DbRlsError {
|
|
43
|
+
constructor() {
|
|
44
|
+
super("missing_tenant_context: autoDispatch requires either authContext or tenantId.");
|
|
45
|
+
this.name = "MissingTenantContext";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class TransactionRolledBack extends DbRlsError {
|
|
49
|
+
cause;
|
|
50
|
+
constructor(cause) {
|
|
51
|
+
super(`transaction_rolled_back: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
52
|
+
this.name = "TransactionRolledBack";
|
|
53
|
+
this.cause = cause;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,oEAAoE;AACpE,uEAAuE;AACvE,EAAE;AACF,gBAAgB;AAChB,qDAAqD;AACrD,uEAAuE;AACvE,0EAA0E;AAC1E,oDAAoD;AACpD,0EAA0E;AAC1E,wBAAwB;AACxB,+DAA+D;AAC/D,iDAAiD;AACjD,EAAE;AACF,uCAAuC;AACvC,0CAA0C;AAC1C,kDAAkD;AAClD,0EAA0E;AA+D1E,+EAA+E;AAC/E,yBAAyB;AAEzB,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,OAAO,mBAAoB,SAAQ,UAAU;IACjD;QACE,KAAK,CACH,yGAAyG,CAC1G,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,MAAM,OAAO,eAAgB,SAAQ,UAAU;IACpC,IAAI,CAAW;IACxB,YAAY,IAAc;QACxB,KAAK,CAAC,yBAAyB,IAAI,sCAAsC,CAAC,CAAC;QAC3E,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,OAAO,oBAAqB,SAAQ,UAAU;IAClD;QACE,KAAK,CACH,+EAA+E,CAChF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED,MAAM,OAAO,qBAAsB,SAAQ,UAAU;IAC1C,KAAK,CAAU;IACxB,YAAY,KAAc;QACxB,KAAK,CACH,4BACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,CACH,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,33 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodii/db-rls",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Three-pool RLS dispatch + GUC-set + entry-point helpers + RLS policy DDL generator for the Nodii microservice stack. Polyglot per D159 (TS + Python + Go). Spec: planning hub docKey=db-rls.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"nodii-db-rls": "./bin/migrate-gen.mjs"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": {
|
|
11
14
|
"types": "./dist/index.d.ts",
|
|
12
15
|
"default": "./dist/index.js"
|
|
13
16
|
}
|
|
14
17
|
},
|
|
15
|
-
"files": [
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"bin",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
16
23
|
"publishConfig": {
|
|
17
|
-
"access": "public"
|
|
18
|
-
"provenance": true
|
|
24
|
+
"access": "public"
|
|
19
25
|
},
|
|
20
26
|
"scripts": {
|
|
21
27
|
"build": "tsc",
|
|
22
28
|
"typecheck": "tsc --noEmit",
|
|
23
|
-
"test": "
|
|
29
|
+
"test": "bun test"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@nodii/telemetry": "0.1.1",
|
|
33
|
+
"drizzle-orm": "^0.36.0",
|
|
34
|
+
"pg": "^8.13.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"drizzle-orm": {
|
|
38
|
+
"optional": false
|
|
39
|
+
},
|
|
40
|
+
"pg": {
|
|
41
|
+
"optional": false
|
|
42
|
+
}
|
|
24
43
|
},
|
|
25
44
|
"devDependencies": {
|
|
45
|
+
"@nodii/telemetry": "0.1.1",
|
|
46
|
+
"@types/bun": "^1.3.13",
|
|
47
|
+
"@types/pg": "^8.11.10",
|
|
48
|
+
"drizzle-orm": "^0.36.4",
|
|
49
|
+
"pg": "^8.13.1",
|
|
26
50
|
"typescript": "^5.9.3"
|
|
27
51
|
},
|
|
28
52
|
"repository": {
|
|
29
53
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/cognion-nucleus/nodii-libs.git",
|
|
54
|
+
"url": "git+https://github.com/cognion-nucleus/nodii-libs.git",
|
|
31
55
|
"directory": "ts/db-rls"
|
|
32
56
|
}
|
|
33
57
|
}
|