@ingram-tech/nk-db 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,7 +39,7 @@ import * as schema from "./schema";
39
39
 
40
40
  export const pool = createPool(); // TLS-aware; local socket → max:1
41
41
  export const db = createDb(pool, schema); // Drizzle — the default query path
42
- export const { query, one, maybeOne, execute, withTx } = createQueries(pool);
42
+ export const { query, one, maybeOne, execute, withTx, withRls } = createQueries(pool);
43
43
  export { schema };
44
44
  ```
45
45
 
@@ -55,6 +55,55 @@ same pool: `betterAuth({ database: pool, … })`.
55
55
  `timestamptz` as ISO strings (on the golden path, prefer Drizzle's
56
56
  `timestamp(..., { mode: "string" })` per column).
57
57
 
58
+ ## Keeping RLS on a direct connection (`withRls` / `withRlsTransaction`)
59
+
60
+ A direct `pg`/Drizzle connection has no PostgREST, so the `SET ROLE authenticated`
61
+ + `request.jwt.claims` setup that made `auth.uid()` policies fire is gone — a
62
+ plain query runs as the connection's role with no claims. These helpers reproduce
63
+ that setup **per transaction**, so your **existing RLS policies keep working
64
+ unchanged**, whether you're still on Supabase Postgres or already on DO. It's
65
+ pure Postgres and behaves identically on both.
66
+
67
+ ```ts
68
+ import { withRlsTransaction } from "@ingram-tech/nk-db";
69
+ import { auth } from "@/lib/auth"; // your Better Auth instance
70
+ import { db } from "@/lib/db";
71
+
72
+ const session = await auth.api.getSession({ headers });
73
+ // scoped: sets request.jwt.claims + SET LOCAL ROLE authenticated, then runs fn
74
+ const notes = await withRlsTransaction(db, { sub: session.user.id }, (tx) =>
75
+ tx.select().from(schema.notes), // returns only this user's rows
76
+ );
77
+ ```
78
+
79
+ The claims come **straight from the Better Auth session** (`sub` = `user.id`) — no
80
+ JWT minting, no JWKS issuer, no `supabase.auth`. The raw helpers expose the same
81
+ thing as `withRls` (sibling of `withTx`):
82
+
83
+ ```ts
84
+ const { withRls } = createQueries(pool);
85
+ const rows = await withRls({ sub: userId }, (tx) =>
86
+ tx.query<Note>("select * from notes"),
87
+ );
88
+ ```
89
+
90
+ Two requirements **you** own (they can't be enforced from the library):
91
+
92
+ - **Connect as a role that doesn't bypass RLS** for user-facing rows — not the
93
+ table owner, not a `BYPASSRLS` superuser. After `SET ROLE authenticated`, RLS
94
+ applies even if the underlying connection is `postgres` (exactly what PostgREST
95
+ relied on). Service-role/admin paths keep using plain `db` / `query` and bypass
96
+ RLS as before.
97
+ - **The connecting role must be allowed to `SET ROLE`** to the target
98
+ (Supabase's `authenticator`/`postgres` already can; on DO, `GRANT app_user TO …`).
99
+
100
+ Override the role / claims GUC when your DB role name differs from the JWT claim:
101
+ `withRlsTransaction(db, { sub }, fn, { role: "app_user" })`. Both helpers set the
102
+ GUCs **transaction-locally** (`is_local = true`), so they reset at
103
+ commit/rollback and never leak across pooled connections. See
104
+ [`docs/db-package.md` §RLS](https://github.com/ingram-technologies/nextkit/blob/main/docs/db-package.md)
105
+ and [`docs/better-auth-migration.md`](https://github.com/ingram-technologies/nextkit/blob/main/docs/better-auth-migration.md).
106
+
58
107
  ## PGlite dev & test (`@ingram-tech/nk-db/pglite`)
59
108
 
60
109
  `nk dev` runs the `nk-pglite-dev` bin automatically when this package is
package/dist/drizzle.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import type { Pool } from "pg";
3
+ import { type RlsClaims, type RlsOptions } from "./rls.js";
3
4
  /**
4
5
  * Build the Drizzle instance on the shared pool. This is the default query path
5
6
  * for app tables — schema-first, with `drizzle-kit` generating the migrations
@@ -11,4 +12,21 @@ import type { Pool } from "pg";
11
12
  * export const db = createDb(pool, schema);
12
13
  */
13
14
  export declare const createDb: <TSchema extends Record<string, unknown>>(pool: Pool, schema: TSchema) => NodePgDatabase<TSchema>;
15
+ /** The transaction handle Drizzle hands its `db.transaction(fn)` callback. */
16
+ type DrizzleTx<TSchema extends Record<string, unknown>> = Parameters<Parameters<NodePgDatabase<TSchema>["transaction"]>[0]>[0];
17
+ /**
18
+ * Run `fn` inside a Drizzle transaction **scoped to a user**, so RLS policies
19
+ * apply on a direct connection (no PostgREST). It sets `request.jwt.claims` +
20
+ * `SET LOCAL ROLE` (default `authenticated`) as the first statement, so existing
21
+ * `auth.uid()` policies fire unchanged; the `tx` passed to `fn` is a normal
22
+ * Drizzle transaction (full query builder + `tx.execute`). Use for user-facing
23
+ * reads/writes; use plain `db` for service-role paths. The connection role must
24
+ * not bypass RLS for the rows touched — see `rls.ts`.
25
+ *
26
+ * const notes = await withRlsTransaction(db, { sub: user.id }, (tx) =>
27
+ * tx.select().from(schema.notes),
28
+ * );
29
+ */
30
+ export declare const withRlsTransaction: <TSchema extends Record<string, unknown>, T>(db: NodePgDatabase<TSchema>, claims: RlsClaims, fn: (tx: DrizzleTx<TSchema>) => Promise<T>, options?: RlsOptions) => Promise<T>;
31
+ export {};
14
32
  //# sourceMappingURL=drizzle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,IAAI,EACV,QAAQ,OAAO,KACb,cAAc,CAAC,OAAO,CAA8B,CAAC"}
1
+ {"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAoB,MAAM,UAAU,CAAC;AAE7E;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,IAAI,EACV,QAAQ,OAAO,KACb,cAAc,CAAC,OAAO,CAA8B,CAAC;AAExD,8EAA8E;AAC9E,KAAK,SAAS,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,UAAU,CACnE,UAAU,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CACrD,CAAC,CAAC,CAAC,CAAC;AAEL;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EAC5E,IAAI,cAAc,CAAC,OAAO,CAAC,EAC3B,QAAQ,SAAS,EACjB,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAC1C,UAAS,UAAe,KACtB,OAAO,CAAC,CAAC,CAOT,CAAC"}
package/dist/drizzle.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { sql } from "drizzle-orm";
1
2
  import { drizzle } from "drizzle-orm/node-postgres";
3
+ import { resolveRlsConfig } from "./rls.js";
2
4
  /**
3
5
  * Build the Drizzle instance on the shared pool. This is the default query path
4
6
  * for app tables — schema-first, with `drizzle-kit` generating the migrations
@@ -10,4 +12,22 @@ import { drizzle } from "drizzle-orm/node-postgres";
10
12
  * export const db = createDb(pool, schema);
11
13
  */
12
14
  export const createDb = (pool, schema) => drizzle(pool, { schema });
15
+ /**
16
+ * Run `fn` inside a Drizzle transaction **scoped to a user**, so RLS policies
17
+ * apply on a direct connection (no PostgREST). It sets `request.jwt.claims` +
18
+ * `SET LOCAL ROLE` (default `authenticated`) as the first statement, so existing
19
+ * `auth.uid()` policies fire unchanged; the `tx` passed to `fn` is a normal
20
+ * Drizzle transaction (full query builder + `tx.execute`). Use for user-facing
21
+ * reads/writes; use plain `db` for service-role paths. The connection role must
22
+ * not bypass RLS for the rows touched — see `rls.ts`.
23
+ *
24
+ * const notes = await withRlsTransaction(db, { sub: user.id }, (tx) =>
25
+ * tx.select().from(schema.notes),
26
+ * );
27
+ */
28
+ export const withRlsTransaction = (db, claims, fn, options = {}) => db.transaction(async (tx) => {
29
+ const { role, claimsSetting, claimsJson } = resolveRlsConfig(claims, options);
30
+ await tx.execute(sql `select set_config(${claimsSetting}, ${claimsJson}, true), set_config('role', ${role}, true)`);
31
+ return fn(tx);
32
+ });
13
33
  //# sourceMappingURL=drizzle.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle.js","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAuB,MAAM,2BAA2B,CAAC;AAGzE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CACvB,IAAU,EACV,MAAe,EACW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC"}
1
+ {"version":3,"file":"drizzle.js","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,OAAO,EAAuB,MAAM,2BAA2B,CAAC;AAEzE,OAAO,EAAmC,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE7E;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CACvB,IAAU,EACV,MAAe,EACW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAOxD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CACjC,EAA2B,EAC3B,MAAiB,EACjB,EAA0C,EAC1C,UAAsB,EAAE,EACX,EAAE,CACf,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;IAC3B,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9E,MAAM,EAAE,CAAC,OAAO,CACf,GAAG,CAAA,qBAAqB,aAAa,KAAK,UAAU,+BAA+B,IAAI,SAAS,CAChG,CAAC;IACF,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;AACf,CAAC,CAAC,CAAC"}
package/dist/id.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The Ingram id codec — a UUIDv7 and its base58 skin. It lives here in nk-db
3
+ * (the lowest package in the graph) behind the `@ingram-tech/nk-db/id` subpath,
4
+ * so it stays `node:crypto`-only — no `pg` / `drizzle` — and any site can mint
5
+ * ids without pulling a heavier slice.
6
+ *
7
+ * The Python twin lives in cloud.ingram.tech's `v1/core.py` (`new_id`); the
8
+ * byte → string vectors in `id.test.ts` and that repo's `tests/test_ids.py` are
9
+ * kept identical, so a stored UUIDv7 and an `agt_` / `smt_` id from the API are
10
+ * the same encoding of the same 16 bytes. That cross-impl contract is the most
11
+ * important property of this file — keep the vectors in lockstep.
12
+ *
13
+ * Store the hyphenated UUIDv7 at rest (uuid columns stay native and time-ordered
14
+ * for index locality) and use {@link toPrefixedId} to skin it as a prefixed
15
+ * base58 id for the wire / display, {@link fromPrefixedId} to recover it.
16
+ * {@link base58Id} mints a fresh one directly; {@link createIdRegistry} builds a
17
+ * typed, prefix-validated set of helpers for a project's entities.
18
+ */
19
+ /**
20
+ * Mint a **UUIDv7** (RFC 9562): a 48-bit Unix-ms timestamp prefix + random tail,
21
+ * version `7`, variant `10`. UUID-shaped (drops straight into a `uuid` column)
22
+ * while staying time-ordered for index locality. Used as Better Auth's
23
+ * `advanced.database.generateId`. Node/Bun's `randomUUID` is v4-only, so we lay
24
+ * the bytes out by hand.
25
+ */
26
+ export declare const uuidGenerateId: () => string;
27
+ /** Skin a stored hyphenated UUIDv7 as a prefixed base58 id, e.g. `team_…`. */
28
+ export declare function toPrefixedId(uuid: string, prefix: string): string;
29
+ /** Inverse of {@link toPrefixedId}: recover the hyphenated UUIDv7. */
30
+ export declare function fromPrefixedId(id: string): string;
31
+ /** Mint a fresh prefixed base58 id (UUIDv7 core), for text-id sites / API parity. */
32
+ export declare function base58Id(prefix: string): string;
33
+ /** Typed helpers for one entity's prefixed ids — built by {@link createIdRegistry}. */
34
+ export interface IdHelper {
35
+ /** This entity's prefix, e.g. `"agt"` (no trailing underscore). */
36
+ readonly prefix: string;
37
+ /** Mint a fresh prefixed id (new UUIDv7 core). */
38
+ mint(): string;
39
+ /** Skin a stored hyphenated UUIDv7 as this entity's prefixed id. */
40
+ encode(uuid: string): string;
41
+ /** Recover the hyphenated UUIDv7; throws if the prefix / shape doesn't match. */
42
+ decode(id: string): string;
43
+ /** Whether `id` is a well-formed prefixed id for this entity. */
44
+ is(id: string): boolean;
45
+ }
46
+ /** The map returned by {@link createIdRegistry}: one {@link IdHelper} per key. */
47
+ export type IdRegistry<K extends string> = Record<K, IdHelper>;
48
+ /**
49
+ * Build a typed, prefix-validated id registry for a project's entities — one
50
+ * place to declare every prefix instead of hand-rolling encode/decode wrappers:
51
+ *
52
+ * ```ts
53
+ * const ids = createIdRegistry({ org: "org", agent: "agt", session: "cs" });
54
+ * ids.org.mint(); // "org_3k9…"
55
+ * ids.org.encode(uuid); // "org_…"
56
+ * ids.org.decode("org_…"); // uuid — throws on a wrong / missing prefix
57
+ * ids.agent.is(someId); // boolean
58
+ * ```
59
+ */
60
+ export declare function createIdRegistry<const T extends Record<string, string>>(prefixes: T): {
61
+ [K in keyof T]: IdHelper;
62
+ };
63
+ //# sourceMappingURL=id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../src/id.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,QAAO,MAYjC,CAAC;AAuDF,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,qFAAqF;AACrF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,uFAAuF;AACvF,MAAM,WAAW,QAAQ;IACxB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,IAAI,IAAI,MAAM,CAAC;IACf,oEAAoE;IACpE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,iFAAiF;IACjF,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,iEAAiE;IACjE,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,kFAAkF;AAClF,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAiB/D;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACtE,QAAQ,EAAE,CAAC,GACT;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ;CAAE,CAM9B"}
package/dist/id.js ADDED
@@ -0,0 +1,138 @@
1
+ import { randomBytes } from "node:crypto";
2
+ /**
3
+ * The Ingram id codec — a UUIDv7 and its base58 skin. It lives here in nk-db
4
+ * (the lowest package in the graph) behind the `@ingram-tech/nk-db/id` subpath,
5
+ * so it stays `node:crypto`-only — no `pg` / `drizzle` — and any site can mint
6
+ * ids without pulling a heavier slice.
7
+ *
8
+ * The Python twin lives in cloud.ingram.tech's `v1/core.py` (`new_id`); the
9
+ * byte → string vectors in `id.test.ts` and that repo's `tests/test_ids.py` are
10
+ * kept identical, so a stored UUIDv7 and an `agt_` / `smt_` id from the API are
11
+ * the same encoding of the same 16 bytes. That cross-impl contract is the most
12
+ * important property of this file — keep the vectors in lockstep.
13
+ *
14
+ * Store the hyphenated UUIDv7 at rest (uuid columns stay native and time-ordered
15
+ * for index locality) and use {@link toPrefixedId} to skin it as a prefixed
16
+ * base58 id for the wire / display, {@link fromPrefixedId} to recover it.
17
+ * {@link base58Id} mints a fresh one directly; {@link createIdRegistry} builds a
18
+ * typed, prefix-validated set of helpers for a project's entities.
19
+ */
20
+ /**
21
+ * Mint a **UUIDv7** (RFC 9562): a 48-bit Unix-ms timestamp prefix + random tail,
22
+ * version `7`, variant `10`. UUID-shaped (drops straight into a `uuid` column)
23
+ * while staying time-ordered for index locality. Used as Better Auth's
24
+ * `advanced.database.generateId`. Node/Bun's `randomUUID` is v4-only, so we lay
25
+ * the bytes out by hand.
26
+ */
27
+ export const uuidGenerateId = () => {
28
+ const bytes = randomBytes(16);
29
+ const ts = Date.now();
30
+ bytes[0] = Math.floor(ts / 2 ** 40) & 0xff;
31
+ bytes[1] = Math.floor(ts / 2 ** 32) & 0xff;
32
+ bytes[2] = Math.floor(ts / 2 ** 24) & 0xff;
33
+ bytes[3] = Math.floor(ts / 2 ** 16) & 0xff;
34
+ bytes[4] = Math.floor(ts / 2 ** 8) & 0xff;
35
+ bytes[5] = ts & 0xff;
36
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x70; // version 7
37
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80; // variant 10
38
+ return bytesToUuid(bytes);
39
+ };
40
+ const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
41
+ // ceil(128 / log2(58)): a 16-byte value never needs more than 22 digits. We
42
+ // left-pad to it so every body is uniform width and sorts lexically ==
43
+ // chronologically (UUIDv7's ms-timestamp prefix lives in the high bytes).
44
+ const WIDTH = 22;
45
+ // A bare base58 body: 22 chars, Bitcoin alphabet (no 0 / I / O / l).
46
+ const BODY = "[1-9A-HJ-NP-Za-km-z]{22}";
47
+ /** Big-endian base58 (Bitcoin alphabet) of 16 bytes, left-padded to `WIDTH`. */
48
+ function encode58(bytes) {
49
+ let n = 0n;
50
+ for (const b of bytes)
51
+ n = (n << 8n) | BigInt(b);
52
+ let out = "";
53
+ while (n > 0n) {
54
+ out = B58.charAt(Number(n % 58n)) + out;
55
+ n /= 58n;
56
+ }
57
+ return out.padStart(WIDTH, B58.charAt(0));
58
+ }
59
+ /** Inverse of {@link encode58}: a base58 body back to 16 bytes. */
60
+ function decode58(body) {
61
+ let n = 0n;
62
+ for (const ch of body) {
63
+ const v = B58.indexOf(ch);
64
+ if (v < 0)
65
+ throw new Error(`invalid base58 char: ${ch}`);
66
+ n = n * 58n + BigInt(v);
67
+ }
68
+ const bytes = new Uint8Array(16);
69
+ for (let i = 15; i >= 0; i--) {
70
+ bytes[i] = Number(n & 0xffn);
71
+ n >>= 8n;
72
+ }
73
+ return bytes;
74
+ }
75
+ /** A hyphenated UUID string → its 16 raw bytes. */
76
+ function uuidToBytes(uuid) {
77
+ const hex = uuid.replace(/-/g, "");
78
+ if (!/^[0-9a-fA-F]{32}$/.test(hex))
79
+ throw new Error(`not a uuid: ${uuid}`);
80
+ const bytes = new Uint8Array(16);
81
+ for (let i = 0; i < 16; i++) {
82
+ bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
83
+ }
84
+ return bytes;
85
+ }
86
+ /** 16 raw bytes → a canonical hyphenated UUID string. */
87
+ function bytesToUuid(bytes) {
88
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
89
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
90
+ }
91
+ /** Skin a stored hyphenated UUIDv7 as a prefixed base58 id, e.g. `team_…`. */
92
+ export function toPrefixedId(uuid, prefix) {
93
+ return `${prefix}_${encode58(uuidToBytes(uuid))}`;
94
+ }
95
+ /** Inverse of {@link toPrefixedId}: recover the hyphenated UUIDv7. */
96
+ export function fromPrefixedId(id) {
97
+ const body = id.slice(id.indexOf("_") + 1);
98
+ return bytesToUuid(decode58(body));
99
+ }
100
+ /** Mint a fresh prefixed base58 id (UUIDv7 core), for text-id sites / API parity. */
101
+ export function base58Id(prefix) {
102
+ return toPrefixedId(uuidGenerateId(), prefix);
103
+ }
104
+ function makeHelper(prefix) {
105
+ // Prefixes are developer-controlled identifier constants, never user input.
106
+ const matcher = new RegExp(`^${prefix}_${BODY}$`);
107
+ return {
108
+ prefix,
109
+ mint: () => base58Id(prefix),
110
+ encode: (uuid) => toPrefixedId(uuid, prefix),
111
+ decode: (id) => {
112
+ if (!matcher.test(id))
113
+ throw new Error(`not a ${prefix}_ id: ${id}`);
114
+ return fromPrefixedId(id);
115
+ },
116
+ is: (id) => matcher.test(id),
117
+ };
118
+ }
119
+ /**
120
+ * Build a typed, prefix-validated id registry for a project's entities — one
121
+ * place to declare every prefix instead of hand-rolling encode/decode wrappers:
122
+ *
123
+ * ```ts
124
+ * const ids = createIdRegistry({ org: "org", agent: "agt", session: "cs" });
125
+ * ids.org.mint(); // "org_3k9…"
126
+ * ids.org.encode(uuid); // "org_…"
127
+ * ids.org.decode("org_…"); // uuid — throws on a wrong / missing prefix
128
+ * ids.agent.is(someId); // boolean
129
+ * ```
130
+ */
131
+ export function createIdRegistry(prefixes) {
132
+ const reg = {};
133
+ for (const [key, prefix] of Object.entries(prefixes)) {
134
+ reg[key] = makeHelper(prefix);
135
+ }
136
+ return reg;
137
+ }
138
+ //# sourceMappingURL=id.js.map
package/dist/id.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id.js","sourceRoot":"","sources":["../src/id.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export { createDb } from "./drizzle.js";
1
+ export { createDb, withRlsTransaction } from "./drizzle.js";
2
2
  export { type DbEnv, dbEnv, getDatabaseUrl, isConfigured } from "./keys.js";
3
3
  export { type CreatePoolConfig, createPool } from "./pool.js";
4
4
  export { createQueries, type PoolQueries, type Queries } from "./queries.js";
5
+ export { RLS_CLAIMS_SETTING, RLS_DEFAULT_ROLE, type ResolvedRlsConfig, type RlsClaims, type RlsOptions, resolveRlsConfig, rlsPreamble, } from "./rls.js";
5
6
  export { configureTimestampsAsStrings } from "./types.js";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAE,KAAK,gBAAgB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAE,KAAK,gBAAgB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAC7E,OAAO,EACN,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,gBAAgB,EAChB,WAAW,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // The Ingram Postgres data layer. The PGlite dev/test harness lives behind the
2
2
  // "@ingram-tech/nk-db/pglite" subpath so its WASM/dev-only deps never reach a
3
3
  // production bundle.
4
- export { createDb } from "./drizzle.js";
4
+ export { createDb, withRlsTransaction } from "./drizzle.js";
5
5
  export { dbEnv, getDatabaseUrl, isConfigured } from "./keys.js";
6
6
  export { createPool } from "./pool.js";
7
7
  export { createQueries } from "./queries.js";
8
+ export { RLS_CLAIMS_SETTING, RLS_DEFAULT_ROLE, resolveRlsConfig, rlsPreamble, } from "./rls.js";
8
9
  export { configureTimestampsAsStrings } from "./types.js";
9
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,qBAAqB;AAErB,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAc,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAyB,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAkC,MAAM,cAAc,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,qBAAqB;AAErB,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAc,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAyB,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAkC,MAAM,cAAc,CAAC;AAC7E,OAAO,EACN,kBAAkB,EAClB,gBAAgB,EAIhB,gBAAgB,EAChB,WAAW,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC"}
File without changes
package/dist/queries.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Pool, QueryResultRow } from "pg";
2
+ import { type RlsClaims, type RlsOptions } from "./rls.js";
2
3
  export interface Queries {
3
4
  /** Run a query and return all rows. */
4
5
  query: <T extends QueryResultRow = QueryResultRow>(text: string, params?: unknown[]) => Promise<T[]>;
@@ -17,6 +18,15 @@ export interface PoolQueries extends Queries {
17
18
  * statements that must be atomic.
18
19
  */
19
20
  withTx: <T>(fn: (tx: Queries) => Promise<T>) => Promise<T>;
21
+ /**
22
+ * Like `withTx`, but first **scopes the transaction to a user** so RLS
23
+ * policies apply: it sets `request.jwt.claims` + `SET LOCAL ROLE` (default
24
+ * `authenticated`) before `fn`, reproducing what PostgREST did. Use this for
25
+ * user-facing reads/writes on a direct connection; keep `withTx` / `query`
26
+ * for service-role paths that should bypass RLS. The connection role must not
27
+ * bypass RLS for the rows touched — see `rls.ts`.
28
+ */
29
+ withRls: <T>(claims: RlsClaims, fn: (tx: Queries) => Promise<T>, options?: RlsOptions) => Promise<T>;
20
30
  }
21
31
  /**
22
32
  * Bind the raw-SQL helpers (`query` / `one` / `maybeOne` / `execute` / `withTx`)
@@ -1 +1 @@
1
- {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAc,cAAc,EAAE,MAAM,IAAI,CAAC;AAK3D,MAAM,WAAW,OAAO;IACvB,uCAAuC;IACvC,KAAK,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAChD,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAClB,wEAAwE;IACxE,QAAQ,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EACnD,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACvB,sEAAsE;IACtE,GAAG,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAC9C,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/D;AAoCD,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC3C;;;;;OAKG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,IAAI,KAAG,WAiB1C,CAAC"}
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAc,cAAc,EAAE,MAAM,IAAI,CAAC;AAC3D,OAAO,EACN,KAAK,SAAS,EACd,KAAK,UAAU,EAGf,MAAM,UAAU,CAAC;AAKlB,MAAM,WAAW,OAAO;IACvB,uCAAuC;IACvC,KAAK,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAChD,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAClB,wEAAwE;IACxE,QAAQ,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EACnD,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACvB,sEAAsE;IACtE,GAAG,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAC9C,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/D;AAoCD,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC3C;;;;;OAKG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3D;;;;;;;OAOG;IACH,OAAO,EAAE,CAAC,CAAC,EACV,MAAM,EAAE,SAAS,EACjB,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,EAC/B,OAAO,CAAC,EAAE,UAAU,KAChB,OAAO,CAAC,CAAC,CAAC,CAAC;CAChB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,IAAI,KAAG,WAkC1C,CAAC"}
package/dist/queries.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolveRlsConfig, rlsPreamble, } from "./rls.js";
1
2
  const bind = (executor) => {
2
3
  const query = async (text, params = []) => {
3
4
  const result = await executor.query(text, params);
@@ -32,10 +33,14 @@ const bind = (executor) => {
32
33
  */
33
34
  export const createQueries = (pool) => {
34
35
  const base = bind(pool);
35
- const withTx = async (fn) => {
36
+ // One transaction runner; `setup` (if any) runs after `begin`, before `fn` —
37
+ // that's where withRls injects the claims/role GUCs.
38
+ const runTx = async (setup, fn) => {
36
39
  const client = await pool.connect();
37
40
  try {
38
41
  await client.query("begin");
42
+ if (setup)
43
+ await setup(client);
39
44
  const result = await fn(bind(client));
40
45
  await client.query("commit");
41
46
  return result;
@@ -48,6 +53,11 @@ export const createQueries = (pool) => {
48
53
  client.release();
49
54
  }
50
55
  };
51
- return { ...base, withTx };
56
+ const withTx = (fn) => runTx(undefined, fn);
57
+ const withRls = (claims, fn, options) => runTx(async (client) => {
58
+ const { text, values } = rlsPreamble(resolveRlsConfig(claims, options));
59
+ await client.query(text, values);
60
+ }, fn);
61
+ return { ...base, withTx, withRls };
52
62
  };
53
63
  //# sourceMappingURL=queries.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAyBA,MAAM,IAAI,GAAG,CAAC,QAAkB,EAAW,EAAE;IAC5C,MAAM,KAAK,GAAG,KAAK,EAClB,IAAY,EACZ,SAAoB,EAAE,EACP,EAAE;QACjB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;QACrD,OAAO,MAAM,CAAC,IAAI,CAAC;IACpB,CAAC,CAAC;IACF,MAAM,QAAQ,GAAG,KAAK,EACrB,IAAY,EACZ,SAAoB,EAAE,EACF,EAAE;QACtB,MAAM,IAAI,GAAG,MAAM,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACxB,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,KAAK,EAChB,IAAY,EACZ,SAAoB,EAAE,EACT,EAAE;QACf,MAAM,IAAI,GAAG,MAAM,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QAChE,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,GAAG,CAAC;IACZ,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,KAAK,EAAE,IAAY,EAAE,SAAoB,EAAE,EAAmB,EAAE;QAC/E,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAClD,OAAO,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;IAC7B,CAAC,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AAC1C,CAAC,CAAC;AAYF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAU,EAAe,EAAE;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,MAAM,GAAG,KAAK,EAAK,EAA+B,EAAc,EAAE;QACvE,MAAM,MAAM,GAAe,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAChD,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;YACtC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC7B,OAAO,MAAM,CAAC;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC/B,MAAM,KAAK,CAAC;QACb,CAAC;gBAAS,CAAC;YACV,MAAM,CAAC,OAAO,EAAE,CAAC;QAClB,CAAC;IACF,CAAC,CAAC;IACF,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC,CAAC"}
1
+ {"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"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"}
package/dist/rls.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Row-Level Security for a **direct Postgres connection** (no PostgREST).
3
+ *
4
+ * On Supabase, PostgREST is what made RLS work: per request it did
5
+ * `SET ROLE authenticated` and set `request.jwt.claims` from the user's JWT, so
6
+ * policies written against `auth.uid()` fired. The moment a site queries Postgres
7
+ * directly (`pg` / Drizzle) — whether it's still on Supabase Postgres or has
8
+ * moved to our DigitalOcean cluster — that setup is gone, and a plain connection
9
+ * runs as the connection's role with **no claims**, so RLS is either bypassed
10
+ * (privileged role) or denies everything.
11
+ *
12
+ * These helpers reproduce exactly what PostgREST did, per transaction: set the
13
+ * claims GUC and `SET LOCAL ROLE`, so **existing `auth.uid()` policies keep
14
+ * working unchanged**. It is pure Postgres, so it behaves identically on Supabase
15
+ * and on DO. See `docs/db-package.md` (§RLS) and `docs/better-auth-migration.md`.
16
+ *
17
+ * Two requirements the caller owns (documented, not enforceable here):
18
+ * 1. The pool must connect as a role that **does not bypass RLS** for the rows
19
+ * it touches — i.e. not the table owner and not a `BYPASSRLS` superuser for
20
+ * user-facing reads. (After `SET ROLE authenticated`, RLS applies even if the
21
+ * underlying connection is `postgres`, exactly as PostgREST relied on.)
22
+ * 2. The connecting role must be allowed to `SET ROLE` to the target role
23
+ * (Supabase's `authenticator`/`postgres` can; on DO, `GRANT app_user TO …`).
24
+ */
25
+ /**
26
+ * The JWT-style claims to scope a transaction by. `sub` becomes `auth.uid()`;
27
+ * `role` (default `"authenticated"`) is the Postgres role assumed. Any further
28
+ * keys are written into the claims GUC for policies that read them
29
+ * (`request.jwt.claims ->> 'org_id'`, etc.).
30
+ */
31
+ export interface RlsClaims {
32
+ /**
33
+ * The user id. Becomes the `sub` claim → `auth.uid()`. For Supabase-style
34
+ * policies that cast `sub` to `uuid`, this MUST be a valid UUID.
35
+ */
36
+ sub: string;
37
+ /** The Postgres role to assume (the JWT `role` claim). Defaults to `"authenticated"`. */
38
+ role?: string;
39
+ /** Any further claims your policies read from `request.jwt.claims`. */
40
+ [claim: string]: unknown;
41
+ }
42
+ /** Overrides for {@link resolveRlsConfig} (and the helpers that build on it). */
43
+ export interface RlsOptions {
44
+ /**
45
+ * Force the Postgres role to `SET LOCAL`, ignoring `claims.role`. Use when the
46
+ * DB role name differs from the JWT `role` claim (e.g. a dedicated `app_user`
47
+ * on DO while the claim stays `"authenticated"`).
48
+ */
49
+ role?: string;
50
+ /** The GUC the claims JSON is written to. Defaults to `"request.jwt.claims"`. */
51
+ claimsSetting?: string;
52
+ }
53
+ /** The role assumed when neither `options.role` nor `claims.role` is set. */
54
+ export declare const RLS_DEFAULT_ROLE = "authenticated";
55
+ /** The GUC `auth.uid()` / `auth.role()` read; what PostgREST populated. */
56
+ export declare const RLS_CLAIMS_SETTING = "request.jwt.claims";
57
+ /** The concrete values a transaction is scoped with. */
58
+ export interface ResolvedRlsConfig {
59
+ /** Postgres role to assume for the transaction. */
60
+ role: string;
61
+ /** GUC the claims JSON is written to. */
62
+ claimsSetting: string;
63
+ /** The claims serialized for `set_config` (a JSON string). */
64
+ claimsJson: string;
65
+ }
66
+ /** Resolve claims + options into the role, GUC name, and serialized claims. */
67
+ export declare const resolveRlsConfig: (claims: RlsClaims, options?: RlsOptions) => ResolvedRlsConfig;
68
+ /**
69
+ * The single parameterized statement that scopes a transaction: writes the
70
+ * claims GUC and the `role` GUC, **both transaction-local** (`is_local = true`,
71
+ * so they reset at `commit`/`rollback` and never leak across pooled
72
+ * connections). Everything — including the GUC name and role — is bound, never
73
+ * interpolated, so it's injection-safe.
74
+ *
75
+ * Exposed for callers that manage their own connection/transaction (e.g. a
76
+ * framework middleware): run it as the first statement after `begin`.
77
+ */
78
+ export declare const rlsPreamble: (config: ResolvedRlsConfig) => {
79
+ text: string;
80
+ values: string[];
81
+ };
82
+ //# sourceMappingURL=rls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls.d.ts","sourceRoot":"","sources":["../src/rls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,yFAAyF;IACzF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED,iFAAiF;AACjF,MAAM,WAAW,UAAU;IAC1B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,kBAAkB,CAAC;AAChD,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IACjC,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,GAC5B,QAAQ,SAAS,EACjB,UAAS,UAAe,KACtB,iBAID,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GACvB,QAAQ,iBAAiB,KACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAGjC,CAAC"}
package/dist/rls.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Row-Level Security for a **direct Postgres connection** (no PostgREST).
3
+ *
4
+ * On Supabase, PostgREST is what made RLS work: per request it did
5
+ * `SET ROLE authenticated` and set `request.jwt.claims` from the user's JWT, so
6
+ * policies written against `auth.uid()` fired. The moment a site queries Postgres
7
+ * directly (`pg` / Drizzle) — whether it's still on Supabase Postgres or has
8
+ * moved to our DigitalOcean cluster — that setup is gone, and a plain connection
9
+ * runs as the connection's role with **no claims**, so RLS is either bypassed
10
+ * (privileged role) or denies everything.
11
+ *
12
+ * These helpers reproduce exactly what PostgREST did, per transaction: set the
13
+ * claims GUC and `SET LOCAL ROLE`, so **existing `auth.uid()` policies keep
14
+ * working unchanged**. It is pure Postgres, so it behaves identically on Supabase
15
+ * and on DO. See `docs/db-package.md` (§RLS) and `docs/better-auth-migration.md`.
16
+ *
17
+ * Two requirements the caller owns (documented, not enforceable here):
18
+ * 1. The pool must connect as a role that **does not bypass RLS** for the rows
19
+ * it touches — i.e. not the table owner and not a `BYPASSRLS` superuser for
20
+ * user-facing reads. (After `SET ROLE authenticated`, RLS applies even if the
21
+ * underlying connection is `postgres`, exactly as PostgREST relied on.)
22
+ * 2. The connecting role must be allowed to `SET ROLE` to the target role
23
+ * (Supabase's `authenticator`/`postgres` can; on DO, `GRANT app_user TO …`).
24
+ */
25
+ /** The role assumed when neither `options.role` nor `claims.role` is set. */
26
+ export const RLS_DEFAULT_ROLE = "authenticated";
27
+ /** The GUC `auth.uid()` / `auth.role()` read; what PostgREST populated. */
28
+ export const RLS_CLAIMS_SETTING = "request.jwt.claims";
29
+ /** Resolve claims + options into the role, GUC name, and serialized claims. */
30
+ export const resolveRlsConfig = (claims, options = {}) => ({
31
+ role: options.role ?? claims.role ?? RLS_DEFAULT_ROLE,
32
+ claimsSetting: options.claimsSetting ?? RLS_CLAIMS_SETTING,
33
+ claimsJson: JSON.stringify(claims),
34
+ });
35
+ /**
36
+ * The single parameterized statement that scopes a transaction: writes the
37
+ * claims GUC and the `role` GUC, **both transaction-local** (`is_local = true`,
38
+ * so they reset at `commit`/`rollback` and never leak across pooled
39
+ * connections). Everything — including the GUC name and role — is bound, never
40
+ * interpolated, so it's injection-safe.
41
+ *
42
+ * Exposed for callers that manage their own connection/transaction (e.g. a
43
+ * framework middleware): run it as the first statement after `begin`.
44
+ */
45
+ export const rlsPreamble = (config) => ({
46
+ text: "select set_config($1, $2, true), set_config('role', $3, true)",
47
+ values: [config.claimsSetting, config.claimsJson, config.role],
48
+ });
49
+ //# sourceMappingURL=rls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls.js","sourceRoot":"","sources":["../src/rls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAgCH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,eAAe,CAAC;AAChD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAYvD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC/B,MAAiB,EACjB,UAAsB,EAAE,EACJ,EAAE,CAAC,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,gBAAgB;IACrD,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,kBAAkB;IAC1D,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;CAClC,CAAC,CAAC;AAEH;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAC1B,MAAyB,EACY,EAAE,CAAC,CAAC;IACzC,IAAI,EAAE,+DAA+D;IACrE,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC;CAC9D,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/nk-db",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "The Ingram Postgres data layer: one TLS-aware pg pool, raw-SQL helpers, Drizzle wiring, and a PGlite (no-Docker) dev/test harness for Next.js sites.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,6 +23,10 @@
23
23
  "types": "./dist/index.d.ts",
24
24
  "import": "./dist/index.js"
25
25
  },
26
+ "./id": {
27
+ "types": "./dist/id.d.ts",
28
+ "import": "./dist/id.js"
29
+ },
26
30
  "./pglite": {
27
31
  "types": "./dist/pglite/index.d.ts",
28
32
  "import": "./dist/pglite/index.js"
@@ -57,7 +61,7 @@
57
61
  "devDependencies": {
58
62
  "@electric-sql/pglite": "^0.5.0",
59
63
  "@electric-sql/pglite-socket": "^0.2.4",
60
- "@ingram-tech/typescript-config": "workspace:*",
64
+ "@ingram-tech/typescript-config": "0.1.0",
61
65
  "@types/node": "^25.0.0",
62
66
  "@types/pg": "^8.11.0",
63
67
  "drizzle-orm": "^0.45.0",