@stratum-hq/db-adapters 1.2.0 → 1.4.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
@@ -2,6 +2,8 @@
2
2
 
3
3
  PostgreSQL adapters for [Stratum](https://github.com/stratum-hq/Stratum) that automatically scope queries to the current tenant using Row-Level Security. Supports raw `pg`, Prisma, Drizzle, and Sequelize, plus helpers for enabling RLS on your tables.
4
4
 
5
+ Read the documentation at [docs.stratum-hq.org/packages/db-adapters](https://docs.stratum-hq.org/packages/db-adapters/).
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -35,7 +37,7 @@ import { prismaWithTenant } from "@stratum-hq/db-adapters"; // Prisma
35
37
  import { drizzleWithTenant } from "@stratum-hq/db-adapters"; // Drizzle
36
38
  import { SequelizeAdapter, sequelizeWithTenantScope } from "@stratum-hq/db-adapters"; // Sequelize
37
39
 
38
- // Prisma — all queries scoped to the current tenant
40
+ // Prisma: all queries scoped to the current tenant
39
41
  const tenantPrisma = prismaWithTenant(prisma, () => getTenantContext().tenant_id, pool);
40
42
  const orders = await tenantPrisma.order.findMany();
41
43
  ```
@@ -56,6 +58,16 @@ try {
56
58
  }
57
59
  ```
58
60
 
61
+ `createPolicy` checks every row-level security policy already on the table (the table the name resolves to through the `search_path`) before it adds the `tenant_isolation` policy or keeps an existing one, and throws without changing anything if any check fails:
62
+
63
+ - PostgreSQL ORs permissive policies together, so every permissive policy, whatever its name or roles, must compare `tenant_id` with the current tenant setting `app.current_tenant_id` for the commands it covers (`USING` for reads, updates and deletes; `WITH CHECK` for inserts, and for updates when set). A role-specific permissive policy counts too, because `createPolicy` cannot know which role your application connects as.
64
+ - Restrictive policies can only narrow access, so they may check anything.
65
+ - An existing `tenant_isolation` policy must be permissive, apply to all commands, and apply to `PUBLIC` (no `TO` clause), like the one `createPolicy` generates and every policy Stratum ships.
66
+
67
+ The check recognizes the form Stratum generates, with the operands in either order, with casts, ANDed with other conditions, or ORed with Stratum's `app.bypass_rls` bypass. A policy that isolates correctly but is written in another form is also refused; replace it with the generated form. This is a breaking change for callers that relied on the old skip, shipped in a minor release.
68
+
69
+ `isRLSEnabled` reports on the table that the name resolves to through the `search_path`, not on a table with the same name in another schema.
70
+
59
71
  Also available: `disableRLS`, `dropPolicy`, `isRLSEnabled`, `addTenantColumn`, `createIsolationPolicy`, and low-level session helpers `setTenantContext` / `resetTenantContext` / `getCurrentTenantId`. Schema-per-tenant and database-per-tenant variants (`SchemaRawAdapter`, `DatabasePoolManager`, …) are exported too.
60
72
 
61
73
  ## Schema-per-tenant search_path
@@ -93,10 +105,55 @@ When the pool count reaches `maxPools`, the manager ends the least recently used
93
105
 
94
106
  Concurrent first requests for one tenant share one pool.
95
107
 
108
+ ## PGlite
109
+
110
+ `@stratum-hq/db-adapters/pglite` runs `@stratum-hq/lib` on [PGlite](https://pglite.dev), which is PostgreSQL compiled to WebAssembly. It works in Node and in the browser, and it needs no database server. Use it for fast local tests and for demos.
111
+
112
+ Install PGlite. It is an optional peer dependency, so npm does not install it for you.
113
+
114
+ ```bash
115
+ npm install @electric-sql/pglite
116
+ ```
117
+
118
+ ```typescript
119
+ import { Stratum } from "@stratum-hq/lib";
120
+ import { withTenantContext } from "@stratum-hq/db-adapters";
121
+ import { createPglitePool, createRestrictedPool } from "@stratum-hq/db-adapters/pglite";
122
+
123
+ // In-memory database. Pass PGlite options, such as { dataDir: "idb://my-db" }, to keep data.
124
+ const pool = await createPglitePool();
125
+ const stratum = new Stratum({ pool, autoMigrate: true });
126
+ await stratum.initialize();
127
+
128
+ const acme = await stratum.createTenant({ name: "Acme", slug: "acme" });
129
+
130
+ // Row-level security applies only to a role that is not a superuser.
131
+ const appPool = await createRestrictedPool(pool);
132
+ await withTenantContext(appPool, acme.id, (client) => client.query("SELECT * FROM config_entries"));
133
+ ```
134
+
135
+ `createPglitePool(source?)` returns a `pg.Pool`-compatible object:
136
+
137
+ - `source` is a PGlite instance or PGlite options. When you pass options, the pool creates the instance and loads the `ltree` and `uuid_ossp` extensions. `pool.end()` then closes the instance.
138
+ - When you pass your own instance, load `ltree` and `uuid_ossp` yourself. `pool.end()` does not close your instance.
139
+ - `pool.pglite` is the PGlite instance.
140
+
141
+ `createRestrictedPool(pool, { role? })` creates the role `stratum_app` (or the name you give) with `NOSUPERUSER NOBYPASSRLS`. It grants the role read and write access to every table and sequence in the `public` schema. Default privileges extend the grants to tables that the superuser creates later. The returned pool runs every query as that role.
142
+
143
+ The restricted role is a test and demo convenience, not a security boundary: any query can leave it with `RESET ROLE`.
144
+
145
+ Limits:
146
+
147
+ - **One connection.** A client from `connect()` holds the only connection until `release()`. Other callers wait in order. If you hold a client and call `pool.query()`, the call waits forever. `release()` rolls back a transaction the client left open and resets the session settings and the role, so the next client starts clean.
148
+ - **No concurrency.** Queries run one at a time. Do not use this adapter to test race conditions or lock contention.
149
+ - **Superuser by default.** PGlite connects as the superuser `postgres`, and a superuser bypasses row-level security. Use `createRestrictedPool` when a test must prove isolation.
150
+ - **Only part of `pg.Pool`.** The pool supports `query`, `connect`, `end` and `on`. Callbacks, cursors, `totalCount` and the other pool counters do not exist.
151
+ - PGlite `^0.4.2` is the supported version.
152
+
96
153
  ## Security
97
154
 
98
155
  - All DDL validates table names against `/^[a-zA-Z_][a-zA-Z0-9_]*$/`.
99
- - Tenant ID is always set via `set_config($1, true)` — fully parameterized.
156
+ - Tenant ID is always set via `set_config($1, true)`, fully parameterized.
100
157
  - `enableRLS()` always applies `FORCE ROW LEVEL SECURITY`, preventing bypass by table owners.
101
158
  - Always reset the tenant context when returning connections to the pool; `createTenantPool` handles this for you.
102
159
 
@@ -21,7 +21,7 @@ export declare function createDatabase(client: pg.PoolClient | pg.Client, tenant
21
21
  * Drops the per-tenant database if it exists.
22
22
  *
23
23
  * IMPORTANT: DROP DATABASE cannot run inside a transaction block.
24
- * Same constraint as createDatabase — use a standalone client.
24
+ * Same constraint as createDatabase: use a standalone client.
25
25
  */
26
26
  export declare function dropDatabase(client: pg.PoolClient | pg.Client, tenantSlug: string): Promise<void>;
27
27
  //# sourceMappingURL=manager.d.ts.map
@@ -38,7 +38,7 @@ async function databaseExists(client, tenantSlug) {
38
38
  */
39
39
  async function createDatabase(client, tenantSlug, templateDb) {
40
40
  const dbName = getDatabaseName(tenantSlug);
41
- // Identifiers validated by regex — no user-supplied interpolation outside of validated values.
41
+ // Identifiers validated by regex: no user-supplied interpolation outside of validated values.
42
42
  // pg does not support parameterized DDL identifiers, so we construct the SQL string directly.
43
43
  let sql = `CREATE DATABASE "${dbName}"`;
44
44
  if (templateDb) {
@@ -54,7 +54,7 @@ async function createDatabase(client, tenantSlug, templateDb) {
54
54
  * Drops the per-tenant database if it exists.
55
55
  *
56
56
  * IMPORTANT: DROP DATABASE cannot run inside a transaction block.
57
- * Same constraint as createDatabase — use a standalone client.
57
+ * Same constraint as createDatabase: use a standalone client.
58
58
  */
59
59
  async function dropDatabase(client, tenantSlug) {
60
60
  const dbName = getDatabaseName(tenantSlug);
@@ -1 +1 @@
1
- {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/database/manager.ts"],"names":[],"mappings":";;AAWA,0CASC;AAMD,wCAUC;AASD,wCAiBC;AAQD,oCAMC;AA3ED,2CAAgD;AAEhD,MAAM,SAAS,GAAG,iBAAiB,CAAC;AACpC,4EAA4E;AAC5E,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;GAGG;AACH,SAAgB,eAAe,CAAC,UAAkB;IAChD,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;IACzB,MAAM,MAAM,GAAG,GAAG,SAAS,GAAG,UAAU,EAAE,CAAC;IAC3C,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,oBAAoB,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,kCAAkC,UAAU,aAAa,oBAAoB,QAAQ,CACtF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB;IAElB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,uEAAuE,EACvE,CAAC,MAAM,CAAC,CACT,CAAC;IACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB,EAClB,UAAmB;IAEnB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,+FAA+F;IAC/F,8FAA8F;IAC9F,IAAI,GAAG,GAAG,oBAAoB,MAAM,GAAG,CAAC;IACxC,IAAI,UAAU,EAAE,CAAC;QACf,iEAAiE;QACjE,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,oCAAoC,UAAU,GAAG,CAAC,CAAC;QACrE,CAAC;QACD,GAAG,IAAI,cAAc,UAAU,GAAG,CAAC;IACrC,CAAC;IACD,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,YAAY,CAChC,MAAiC,EACjC,UAAkB;IAElB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,MAAM,CAAC,KAAK,CAAC,4BAA4B,MAAM,GAAG,CAAC,CAAC;AAC5D,CAAC"}
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/database/manager.ts"],"names":[],"mappings":";;AAWA,0CASC;AAMD,wCAUC;AASD,wCAiBC;AAQD,oCAMC;AA3ED,2CAAgD;AAEhD,MAAM,SAAS,GAAG,iBAAiB,CAAC;AACpC,4EAA4E;AAC5E,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;GAGG;AACH,SAAgB,eAAe,CAAC,UAAkB;IAChD,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;IACzB,MAAM,MAAM,GAAG,GAAG,SAAS,GAAG,UAAU,EAAE,CAAC;IAC3C,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,oBAAoB,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,kCAAkC,UAAU,aAAa,oBAAoB,QAAQ,CACtF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB;IAElB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,uEAAuE,EACvE,CAAC,MAAM,CAAC,CACT,CAAC;IACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB,EAClB,UAAmB;IAEnB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,8FAA8F;IAC9F,8FAA8F;IAC9F,IAAI,GAAG,GAAG,oBAAoB,MAAM,GAAG,CAAC;IACxC,IAAI,UAAU,EAAE,CAAC;QACf,iEAAiE;QACjE,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,oCAAoC,UAAU,GAAG,CAAC,CAAC;QACrE,CAAC;QACD,GAAG,IAAI,cAAc,UAAU,GAAG,CAAC;IACrC,CAAC;IACD,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,YAAY,CAChC,MAAiC,EACjC,UAAkB;IAElB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,MAAM,CAAC,KAAK,CAAC,4BAA4B,MAAM,GAAG,CAAC,CAAC;AAC5D,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import pg from "pg";
2
2
  export interface DatabasePoolManagerOptions {
3
- /** Template connection config (host, port, user, password, ssl, etc.) — database name is overridden per tenant. */
3
+ /** Template connection config (host, port, user, password, ssl, etc.). The database name is overridden per tenant. */
4
4
  baseConnectionConfig: pg.PoolConfig;
5
5
  /** Maximum number of tenant pools to keep open simultaneously. Default: 50. */
6
6
  maxPools?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"pool-manager.d.ts","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,MAAM,WAAW,0BAA0B;IACzC,mHAAmH;IACnH,oBAAoB,EAAE,EAAE,CAAC,UAAU,CAAC;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;GAQG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,8FAA8F;IAC9F,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,OAAO,EAAE,0BAA0B;IAM/C;;;;;;;OAOG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC;IAgCtE;;;;;;;;OAQG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAqBxD;gFAC4E;IACtE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBrE,kEAAkE;IAC5D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAO/B,uGAAuG;IACvG,OAAO,CAAC,iBAAiB;IAKzB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAWtB,qDAAqD;IACrD,QAAQ,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE;IAW5D;;;OAGG;IACH,OAAO,CAAC,OAAO;CAgBhB"}
1
+ {"version":3,"file":"pool-manager.d.ts","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,MAAM,WAAW,0BAA0B;IACzC,sHAAsH;IACtH,oBAAoB,EAAE,EAAE,CAAC,UAAU,CAAC;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;GAQG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,8FAA8F;IAC9F,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,OAAO,EAAE,0BAA0B;IAM/C;;;;;;;OAOG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC;IAgCtE;;;;;;;;OAQG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAqBxD;gFAC4E;IACtE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBrE,kEAAkE;IAC5D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAO/B,uGAAuG;IACvG,OAAO,CAAC,iBAAiB;IAKzB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAWtB,qDAAqD;IACrD,QAAQ,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE;IAW5D;;;OAGG;IACH,OAAO,CAAC,OAAO;CAgBhB"}
package/dist/index.d.ts CHANGED
@@ -5,7 +5,9 @@ export { SchemaRawAdapter, createSchemaTenantPool, } from "./adapters/schema-raw
5
5
  export type { SchemaAdapterOptions } from "./adapters/schema-raw.js";
6
6
  export { SchemaPrismaAdapter, withSchemaTenant, } from "./adapters/schema-prisma.js";
7
7
  export { createPolicy, dropPolicy, enableRLS, disableRLS, isRLSEnabled, } from "./rls/manager.js";
8
+ export type { CreatePolicyOptions } from "./rls/manager.js";
8
9
  export { setTenantContext, resetTenantContext, withTenantContext, withRlsBypass, getCurrentTenantId, } from "./rls/session.js";
10
+ export type { TenantScope, TenantContextOptions } from "./rls/session.js";
9
11
  export { createSchema, dropSchema, schemaExists, listTenantSchemas, replicateTableToSchema, tenantSchemaName, } from "./schema/manager.js";
10
12
  export { setSchemaSearchPath, resetSearchPath, getCurrentSearchPath, } from "./schema/session.js";
11
13
  export { addTenantColumn, enableRLS as enableRLSForMigration, createIsolationPolicy, migrateTable, } from "./migration-helpers.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EACL,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,YAAY,EACZ,UAAU,EACV,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,eAAe,EACf,SAAS,IAAI,qBAAqB,EAClC,qBAAqB,EACrB,YAAY,GACb,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,GACb,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAE7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,eAAe,IAAI,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxG,OAAO,EACL,cAAc,EACd,UAAU,IAAI,iBAAiB,EAC/B,eAAe,IAAI,sBAAsB,GAC1C,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EACL,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,YAAY,EACZ,UAAU,EACV,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5D,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE1E,OAAO,EACL,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,eAAe,EACf,SAAS,IAAI,qBAAqB,EAClC,qBAAqB,EACrB,YAAY,GACb,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,GACb,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAE7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,eAAe,IAAI,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxG,OAAO,EACL,cAAc,EACd,UAAU,IAAI,iBAAiB,EAC/B,eAAe,IAAI,sBAAsB,GAC1C,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAAvC,8GAAA,WAAW,OAAA;AAEpB,4CAAiE;AAAxD,oGAAA,UAAU,OAAA;AAAE,0GAAA,gBAAgB,OAAA;AACrC,kDAAqF;AAA5E,0GAAA,aAAa,OAAA;AAAE,6GAAA,UAAU,OAAoB;AACtD,0DAGkC;AAFhC,iHAAA,gBAAgB,OAAA;AAChB,uHAAA,sBAAsB,OAAA;AAGxB,gEAGqC;AAFnC,uHAAA,mBAAmB,OAAA;AACnB,oHAAA,gBAAgB,OAAA;AAGlB,+CAM0B;AALxB,0GAAA,YAAY,OAAA;AACZ,wGAAA,UAAU,OAAA;AACV,uGAAA,SAAS,OAAA;AACT,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;AAGd,+CAM0B;AALxB,8GAAA,gBAAgB,OAAA;AAChB,gHAAA,kBAAkB,OAAA;AAClB,+GAAA,iBAAiB,OAAA;AACjB,2GAAA,aAAa,OAAA;AACb,gHAAA,kBAAkB,OAAA;AAGpB,kDAO6B;AAN3B,0GAAA,YAAY,OAAA;AACZ,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;AACZ,+GAAA,iBAAiB,OAAA;AACjB,oHAAA,sBAAsB,OAAA;AACtB,8GAAA,gBAAgB,OAAA;AAGlB,kDAI6B;AAH3B,iHAAA,mBAAmB,OAAA;AACnB,6GAAA,eAAe,OAAA;AACf,kHAAA,oBAAoB,OAAA;AAGtB,+DAKgC;AAJ9B,uHAAA,eAAe,OAAA;AACf,6HAAA,SAAS,OAAyB;AAClC,6HAAA,qBAAqB,OAAA;AACrB,oHAAA,YAAY,OAAA;AAGd,0BAA0B;AAC1B,oDAK+B;AAJ7B,6GAAA,eAAe,OAAA;AACf,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,0GAAA,YAAY,OAAA;AAGd,8DAAiE;AAAxD,sHAAA,mBAAmB,OAAA;AAG5B,8DAAgE;AAAvD,qHAAA,kBAAkB,OAAA;AAC3B,oEAAsE;AAA7D,2HAAA,qBAAqB,OAAA;AAC9B,wDAAwG;AAA/F,gHAAA,gBAAgB,OAAA;AAAE,wHAAA,eAAe,OAA4B;AACtE,oDAI+B;AAH7B,4GAAA,cAAc,OAAA;AACd,+GAAA,UAAU,OAAqB;AAC/B,oHAAA,eAAe,OAA0B"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAAvC,8GAAA,WAAW,OAAA;AAEpB,4CAAiE;AAAxD,oGAAA,UAAU,OAAA;AAAE,0GAAA,gBAAgB,OAAA;AACrC,kDAAqF;AAA5E,0GAAA,aAAa,OAAA;AAAE,6GAAA,UAAU,OAAoB;AACtD,0DAGkC;AAFhC,iHAAA,gBAAgB,OAAA;AAChB,uHAAA,sBAAsB,OAAA;AAGxB,gEAGqC;AAFnC,uHAAA,mBAAmB,OAAA;AACnB,oHAAA,gBAAgB,OAAA;AAGlB,+CAM0B;AALxB,0GAAA,YAAY,OAAA;AACZ,wGAAA,UAAU,OAAA;AACV,uGAAA,SAAS,OAAA;AACT,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;AAId,+CAM0B;AALxB,8GAAA,gBAAgB,OAAA;AAChB,gHAAA,kBAAkB,OAAA;AAClB,+GAAA,iBAAiB,OAAA;AACjB,2GAAA,aAAa,OAAA;AACb,gHAAA,kBAAkB,OAAA;AAIpB,kDAO6B;AAN3B,0GAAA,YAAY,OAAA;AACZ,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;AACZ,+GAAA,iBAAiB,OAAA;AACjB,oHAAA,sBAAsB,OAAA;AACtB,8GAAA,gBAAgB,OAAA;AAGlB,kDAI6B;AAH3B,iHAAA,mBAAmB,OAAA;AACnB,6GAAA,eAAe,OAAA;AACf,kHAAA,oBAAoB,OAAA;AAGtB,+DAKgC;AAJ9B,uHAAA,eAAe,OAAA;AACf,6HAAA,SAAS,OAAyB;AAClC,6HAAA,qBAAqB,OAAA;AACrB,oHAAA,YAAY,OAAA;AAGd,0BAA0B;AAC1B,oDAK+B;AAJ7B,6GAAA,eAAe,OAAA;AACf,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,0GAAA,YAAY,OAAA;AAGd,8DAAiE;AAAxD,sHAAA,mBAAmB,OAAA;AAG5B,8DAAgE;AAAvD,qHAAA,kBAAkB,OAAA;AAC3B,oEAAsE;AAA7D,2HAAA,qBAAqB,OAAA;AAC9B,wDAAwG;AAA/F,gHAAA,gBAAgB,OAAA;AAAE,wHAAA,eAAe,OAA4B;AACtE,oDAI+B;AAH7B,4GAAA,cAAc,OAAA;AACd,+GAAA,UAAU,OAAqB;AAC/B,oHAAA,eAAe,OAA0B"}
@@ -1,6 +1,7 @@
1
1
  import pg from "pg";
2
+ import { type CreatePolicyOptions } from "./rls/manager.js";
2
3
  export declare function addTenantColumn(client: pg.PoolClient, tableName: string): Promise<void>;
3
4
  export declare function enableRLS(client: pg.PoolClient, tableName: string): Promise<void>;
4
- export declare function createIsolationPolicy(client: pg.PoolClient, tableName: string): Promise<void>;
5
+ export declare function createIsolationPolicy(client: pg.PoolClient, tableName: string, options?: CreatePolicyOptions): Promise<void>;
5
6
  export declare function migrateTable(client: pg.PoolClient, tableName: string): Promise<void>;
6
7
  //# sourceMappingURL=migration-helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"migration-helpers.d.ts","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAUpB,wBAAsB,eAAe,CACnC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf"}
1
+ {"version":3,"file":"migration-helpers.d.ts","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAA8C,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAUxG,wBAAsB,eAAe,CACnC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf"}
@@ -4,6 +4,7 @@ exports.addTenantColumn = addTenantColumn;
4
4
  exports.enableRLS = enableRLS;
5
5
  exports.createIsolationPolicy = createIsolationPolicy;
6
6
  exports.migrateTable = migrateTable;
7
+ const manager_js_1 = require("./rls/manager.js");
7
8
  // Validate table name to prevent SQL injection (only allows alphanumeric + underscores)
8
9
  function validateTableName(name) {
9
10
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
@@ -20,10 +21,16 @@ async function enableRLS(client, tableName) {
20
21
  await client.query(`ALTER TABLE ${safe} ENABLE ROW LEVEL SECURITY`);
21
22
  await client.query(`ALTER TABLE ${safe} FORCE ROW LEVEL SECURITY`);
22
23
  }
23
- async function createIsolationPolicy(client, tableName) {
24
+ async function createIsolationPolicy(client, tableName, options = {}) {
24
25
  const safe = validateTableName(tableName);
26
+ if (options.subtreeRead) {
27
+ await (0, manager_js_1.assertSubtreeFunction)(client);
28
+ }
25
29
  await client.query(`CREATE POLICY tenant_isolation ON ${safe}
26
30
  USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid)`);
31
+ if (options.subtreeRead) {
32
+ await client.query(`CREATE POLICY tenant_subtree_read ON ${safe} FOR SELECT USING (${manager_js_1.SUBTREE_READ_FILTER})`);
33
+ }
27
34
  }
28
35
  async function migrateTable(client, tableName) {
29
36
  await addTenantColumn(client, tableName);
@@ -1 +1 @@
1
- {"version":3,"file":"migration-helpers.js","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":";;AAUA,0CAQC;AAED,8BAOC;AAED,sDASC;AAED,oCAOC;AA7CD,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAEM,KAAK,UAAU,eAAe,CACnC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAChB,eAAe,IAAI,qCAAqC,CACzD,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,4BAA4B,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,2BAA2B,CAAC,CAAC;AACrE,CAAC;AAEM,KAAK,UAAU,qBAAqB,CACzC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAChB,qCAAqC,IAAI;0FAC6C,CACvF,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,MAAM,qBAAqB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"migration-helpers.js","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":";;AAWA,0CAQC;AAED,8BAOC;AAED,sDAkBC;AAED,oCAOC;AAxDD,iDAAwG;AAExG,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAEM,KAAK,UAAU,eAAe,CACnC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAChB,eAAe,IAAI,qCAAqC,CACzD,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,4BAA4B,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,2BAA2B,CAAC,CAAC;AACrE,CAAC;AAEM,KAAK,UAAU,qBAAqB,CACzC,MAAqB,EACrB,SAAiB,EACjB,UAA+B,EAAE;IAEjC,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,MAAM,IAAA,kCAAqB,EAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,MAAM,CAAC,KAAK,CAChB,qCAAqC,IAAI;0FAC6C,CACvF,CAAC;IACF,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,MAAM,MAAM,CAAC,KAAK,CAChB,wCAAwC,IAAI,sBAAsB,gCAAmB,GAAG,CACzF,CAAC;IACJ,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,MAAM,qBAAqB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AACjD,CAAC"}
@@ -0,0 +1,60 @@
1
+ import type pg from "pg";
2
+ import type { PGliteInterface, PGliteOptions } from "@electric-sql/pglite" with { "resolution-mode": "import" };
3
+ /**
4
+ * A `pg.Pool`-compatible object over one PGlite instance.
5
+ *
6
+ * Only these members work: `query`, `connect` (a client with `query` and
7
+ * `release`), `end` and `on`. Other `pg.Pool` members are undefined.
8
+ */
9
+ export type PglitePool = pg.Pool & {
10
+ /** The PGlite instance that runs every query of this pool. */
11
+ readonly pglite: PGliteInterface;
12
+ };
13
+ export interface RestrictedPoolOptions {
14
+ /**
15
+ * The name of the role. It must match `^[a-z_][a-z0-9_]{0,62}$`.
16
+ * Defaults to `stratum_app`.
17
+ */
18
+ role?: string;
19
+ }
20
+ /**
21
+ * Return a `pg.Pool`-compatible pool that runs every query on one PGlite instance.
22
+ *
23
+ * The pool has one connection. A client from `connect()` holds it until
24
+ * `release()`, and other callers wait in order. `release()` rolls back a
25
+ * transaction the client left open and resets the session settings, so the
26
+ * next client does not inherit them. Queries run as the PGlite
27
+ * superuser, so row-level security does not apply; use
28
+ * {@link createRestrictedPool} for that.
29
+ *
30
+ * @param source - A PGlite instance, or options to create one. When the
31
+ * pool creates the instance, it loads the `ltree` and `uuid_ossp`
32
+ * extensions, and `end()` closes the instance. When you pass an instance,
33
+ * you load the extensions and you close it.
34
+ * @returns The pool, after PGlite is ready.
35
+ */
36
+ export declare function createPglitePool(source?: PGliteInterface | PGliteOptions): Promise<PglitePool>;
37
+ /**
38
+ * Return a pool that runs every query as a role that is not a superuser, so
39
+ * that row-level security applies.
40
+ *
41
+ * The function creates the role when it does not exist, with NOSUPERUSER and
42
+ * NOBYPASSRLS. It grants the role SELECT, INSERT, UPDATE and DELETE on every
43
+ * table in the `public` schema, and usage of every sequence. Default
44
+ * privileges extend these grants to tables that the current user creates
45
+ * later.
46
+ *
47
+ * The returned pool shares the connection and the lock of `pool`. Each client
48
+ * runs `SET ROLE` when it gets the connection and `RESET ROLE` at release.
49
+ * Its `end()` closes nothing; end the source pool instead.
50
+ *
51
+ * The restricted role is a convenience for tests and demos, not a security
52
+ * boundary: any query can leave it with `RESET ROLE`, because the session
53
+ * belongs to the superuser.
54
+ *
55
+ * @param pool - A pool from {@link createPglitePool} that runs as the superuser.
56
+ * @param options - The role name.
57
+ * @throws Error - The role name is not a plain lowercase identifier.
58
+ */
59
+ export declare function createRestrictedPool(pool: PglitePool, options?: RestrictedPoolOptions): Promise<PglitePool>;
60
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAW,MAAM,sBAAsB,CAAC,OAAO,iBAAiB,EAAE,QAAQ,EAAE,CAAC;AAMzH;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,EAAE,CAAC,IAAI,GAAG;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;CAClC,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAuID;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBpG;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,UAAU,CAAC,CAoBrB"}
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createPglitePool = createPglitePool;
4
+ exports.createRestrictedPool = createRestrictedPool;
5
+ // pg returns int8 and numeric values as strings, because a JavaScript number
6
+ // loses precision above 2^53. PGlite returns numbers, so these parsers
7
+ // restore the pg behavior.
8
+ const PG_PARSERS = { 20: (value) => value, 1700: (value) => value };
9
+ const ROLE_NAME = /^[a-z_][a-z0-9_]{0,62}$/;
10
+ // A restricted pool must share the lock of its source pool, because both
11
+ // use the same connection.
12
+ const locks = new WeakMap();
13
+ /**
14
+ * Return a lock that gives one holder at a time the connection, in request order.
15
+ */
16
+ function createLock() {
17
+ let tail = Promise.resolve();
18
+ return () => {
19
+ let unlock;
20
+ const held = new Promise((resolve) => (unlock = resolve));
21
+ const acquired = tail.then(() => unlock);
22
+ tail = tail.then(() => held);
23
+ return acquired;
24
+ };
25
+ }
26
+ // pg sends a plain object as JSON text for every parameter type. PGlite
27
+ // serializes by the inferred parameter type and rejects an object for a text
28
+ // parameter, so the conversion happens here.
29
+ function prepareValue(value) {
30
+ if (value === undefined)
31
+ return null;
32
+ if (value !== null &&
33
+ typeof value === "object" &&
34
+ !(value instanceof Date) &&
35
+ !(value instanceof Uint8Array) &&
36
+ !Array.isArray(value)) {
37
+ return JSON.stringify(value);
38
+ }
39
+ return value;
40
+ }
41
+ function toQueryResult(result) {
42
+ // PGlite reports affectedRows 0 for a SELECT. pg reports the number of rows
43
+ // for any statement that returns rows.
44
+ const rowCount = result.fields.length > 0 ? result.rows.length : (result.affectedRows ?? 0);
45
+ return { rows: result.rows, rowCount, fields: result.fields };
46
+ }
47
+ async function run(db, textOrConfig, values) {
48
+ const text = typeof textOrConfig === "string" ? textOrConfig : textOrConfig.text;
49
+ const params = values ?? (typeof textOrConfig === "string" ? undefined : textOrConfig.values);
50
+ if (!params || params.length === 0) {
51
+ // pg uses the simple query protocol when there are no parameters, and
52
+ // that protocol accepts several statements, as migration files hold.
53
+ // PGlite exec() is the same protocol. It returns one result per statement,
54
+ // and pg returns the last one.
55
+ const results = await db.exec(text, { parsers: PG_PARSERS });
56
+ return toQueryResult(results[results.length - 1] ?? { rows: [], fields: [] });
57
+ }
58
+ return toQueryResult(await db.query(text, params.map(prepareValue), { parsers: PG_PARSERS }));
59
+ }
60
+ function makePool(db, acquire, ownsInstance, role) {
61
+ const connect = async () => {
62
+ const unlock = await acquire();
63
+ try {
64
+ if (role)
65
+ await db.exec(`SET ROLE "${role}"`);
66
+ }
67
+ catch (err) {
68
+ unlock();
69
+ throw err;
70
+ }
71
+ let released = false;
72
+ return {
73
+ query: (textOrConfig, values) => run(db, textOrConfig, values),
74
+ // pg gives each client its own session and destroys the connection when
75
+ // release() receives an error. This pool has one session for every
76
+ // client, so release cleans it instead: it rolls back a transaction the
77
+ // client left open, and resets the settings and the role the client
78
+ // set. The lock stays held until the cleanup ends, so the next client
79
+ // gets a clean session.
80
+ release: () => {
81
+ if (released)
82
+ return;
83
+ released = true;
84
+ // RESET ALL leaves the role alone, so RESET ROLE follows it.
85
+ db.exec("ROLLBACK; RESET ALL; RESET ROLE").then(unlock, unlock);
86
+ },
87
+ };
88
+ };
89
+ const pool = {
90
+ pglite: db,
91
+ connect,
92
+ async query(textOrConfig, values) {
93
+ const client = await connect();
94
+ try {
95
+ return await client.query(textOrConfig, values);
96
+ }
97
+ finally {
98
+ client.release();
99
+ }
100
+ },
101
+ async end() {
102
+ if (ownsInstance && !db.closed)
103
+ await db.close();
104
+ },
105
+ on() {
106
+ return pool;
107
+ },
108
+ };
109
+ const result = pool;
110
+ locks.set(result, acquire);
111
+ return result;
112
+ }
113
+ function isPglite(value) {
114
+ return (typeof value === "object" &&
115
+ value !== null &&
116
+ typeof value.exec === "function" &&
117
+ typeof value.query === "function");
118
+ }
119
+ /**
120
+ * Return a `pg.Pool`-compatible pool that runs every query on one PGlite instance.
121
+ *
122
+ * The pool has one connection. A client from `connect()` holds it until
123
+ * `release()`, and other callers wait in order. `release()` rolls back a
124
+ * transaction the client left open and resets the session settings, so the
125
+ * next client does not inherit them. Queries run as the PGlite
126
+ * superuser, so row-level security does not apply; use
127
+ * {@link createRestrictedPool} for that.
128
+ *
129
+ * @param source - A PGlite instance, or options to create one. When the
130
+ * pool creates the instance, it loads the `ltree` and `uuid_ossp`
131
+ * extensions, and `end()` closes the instance. When you pass an instance,
132
+ * you load the extensions and you close it.
133
+ * @returns The pool, after PGlite is ready.
134
+ */
135
+ async function createPglitePool(source) {
136
+ if (isPglite(source)) {
137
+ await source.waitReady;
138
+ return makePool(source, createLock(), false, null);
139
+ }
140
+ const [{ PGlite }, { ltree }, { uuid_ossp }] = await Promise.all([
141
+ import("@electric-sql/pglite"),
142
+ import("@electric-sql/pglite/contrib/ltree"),
143
+ import("@electric-sql/pglite/contrib/uuid_ossp"),
144
+ ]);
145
+ const options = source ?? {};
146
+ const db = await PGlite.create({
147
+ ...options,
148
+ extensions: { ltree, uuid_ossp, ...options.extensions },
149
+ });
150
+ return makePool(db, createLock(), true, null);
151
+ }
152
+ /**
153
+ * Return a pool that runs every query as a role that is not a superuser, so
154
+ * that row-level security applies.
155
+ *
156
+ * The function creates the role when it does not exist, with NOSUPERUSER and
157
+ * NOBYPASSRLS. It grants the role SELECT, INSERT, UPDATE and DELETE on every
158
+ * table in the `public` schema, and usage of every sequence. Default
159
+ * privileges extend these grants to tables that the current user creates
160
+ * later.
161
+ *
162
+ * The returned pool shares the connection and the lock of `pool`. Each client
163
+ * runs `SET ROLE` when it gets the connection and `RESET ROLE` at release.
164
+ * Its `end()` closes nothing; end the source pool instead.
165
+ *
166
+ * The restricted role is a convenience for tests and demos, not a security
167
+ * boundary: any query can leave it with `RESET ROLE`, because the session
168
+ * belongs to the superuser.
169
+ *
170
+ * @param pool - A pool from {@link createPglitePool} that runs as the superuser.
171
+ * @param options - The role name.
172
+ * @throws Error - The role name is not a plain lowercase identifier.
173
+ */
174
+ async function createRestrictedPool(pool, options = {}) {
175
+ const role = options.role ?? "stratum_app";
176
+ if (!ROLE_NAME.test(role)) {
177
+ throw new Error(`Invalid role name '${role}': use lowercase letters, digits and underscores`);
178
+ }
179
+ await pool.query(`
180
+ DO $$ BEGIN
181
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
182
+ CREATE ROLE "${role}" NOLOGIN NOSUPERUSER NOBYPASSRLS;
183
+ END IF;
184
+ END $$;
185
+ GRANT USAGE ON SCHEMA public TO "${role}";
186
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "${role}";
187
+ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO "${role}";
188
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "${role}";
189
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO "${role}";
190
+ `);
191
+ const acquire = locks.get(pool);
192
+ if (!acquire)
193
+ throw new Error("createRestrictedPool needs a pool from createPglitePool");
194
+ return makePool(pool.pglite, acquire, false, role);
195
+ }
196
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,25 @@
1
1
  import pg from "pg";
2
- export declare function createPolicy(client: pg.PoolClient, tableName: string): Promise<void>;
2
+ /**
3
+ * The read-only subtree predicate of migration 031. It admits the rows of the
4
+ * current tenant's descendants only when the session set app.tenant_scope to
5
+ * 'subtree'. It goes in a FOR SELECT policy only, so writes stay exact.
6
+ */
7
+ export declare const SUBTREE_READ_FILTER: string;
8
+ export interface CreatePolicyOptions {
9
+ /**
10
+ * Also create the tenant_subtree_read policy, so that a session in the
11
+ * "subtree" scope reads the rows of the tenant's descendants. Needs
12
+ * migration 031 of @stratum-hq/lib. Default false.
13
+ */
14
+ subtreeRead?: boolean;
15
+ }
16
+ /**
17
+ * Throws when the database has no stratum_subtree_tenant_ids() function. The
18
+ * subtree read policy calls it, and without it CREATE POLICY fails with a
19
+ * less clear error.
20
+ */
21
+ export declare function assertSubtreeFunction(client: pg.PoolClient): Promise<void>;
22
+ export declare function createPolicy(client: pg.PoolClient, tableName: string, options?: CreatePolicyOptions): Promise<void>;
3
23
  export declare function dropPolicy(client: pg.PoolClient, tableName: string): Promise<void>;
4
24
  export declare function enableRLS(client: pg.PoolClient, tableName: string): Promise<void>;
5
25
  export declare function disableRLS(client: pg.PoolClient, tableName: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAUpB,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CASlB"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAKpB;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAEuC,CAAC;AAExE,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhF;AAUD,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAWlB"}
@@ -1,10 +1,33 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SUBTREE_READ_FILTER = void 0;
4
+ exports.assertSubtreeFunction = assertSubtreeFunction;
3
5
  exports.createPolicy = createPolicy;
4
6
  exports.dropPolicy = dropPolicy;
5
7
  exports.enableRLS = enableRLS;
6
8
  exports.disableRLS = disableRLS;
7
9
  exports.isRLSEnabled = isRLSEnabled;
10
+ const policy_check_js_1 = require("./policy-check.js");
11
+ const TENANT_FILTER = "tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid";
12
+ /**
13
+ * The read-only subtree predicate of migration 031. It admits the rows of the
14
+ * current tenant's descendants only when the session set app.tenant_scope to
15
+ * 'subtree'. It goes in a FOR SELECT policy only, so writes stay exact.
16
+ */
17
+ exports.SUBTREE_READ_FILTER = "current_setting('app.tenant_scope', true) = 'subtree' " +
18
+ "AND tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[])";
19
+ /**
20
+ * Throws when the database has no stratum_subtree_tenant_ids() function. The
21
+ * subtree read policy calls it, and without it CREATE POLICY fails with a
22
+ * less clear error.
23
+ */
24
+ async function assertSubtreeFunction(client) {
25
+ const res = await client.query("SELECT to_regprocedure('stratum_subtree_tenant_ids()') IS NOT NULL AS present");
26
+ if (!res.rows[0]?.present) {
27
+ throw new Error("[stratum] The subtree read policy needs the function stratum_subtree_tenant_ids(), " +
28
+ "which migration 031 of @stratum-hq/lib creates. Run the Stratum migrations first.");
29
+ }
30
+ }
8
31
  // Validate table name to prevent SQL injection (only allows alphanumeric + underscores)
9
32
  function validateTableName(tableName) {
10
33
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
@@ -12,18 +35,42 @@ function validateTableName(tableName) {
12
35
  }
13
36
  return tableName;
14
37
  }
15
- async function createPolicy(client, tableName) {
38
+ async function createPolicy(client, tableName, options = {}) {
16
39
  const safe = validateTableName(tableName);
17
- // Cannot use parameterized queries inside DO blocks or for DDL identifiers.
18
- // Table name is validated via allowlist regex above.
19
- const exists = await client.query(`SELECT count(*) FROM pg_policies WHERE tablename = $1 AND policyname = 'tenant_isolation'`, [safe]);
20
- if (parseInt(exists.rows[0].count, 10) === 0) {
21
- await client.query(`CREATE POLICY tenant_isolation ON ${safe} USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid)`);
40
+ if (options.subtreeRead) {
41
+ await assertSubtreeFunction(client);
42
+ }
43
+ // Read every policy on the table that the name resolves to, in whichever
44
+ // schema that is. PostgreSQL ORs permissive policies together, so each one
45
+ // must filter by tenant, and a policy's name proves nothing.
46
+ // Each row also carries the schema of the tenants table, the only schema
47
+ // that may qualify the subtree function (migration 031 creates it there).
48
+ const existing = await client.query(`SELECT p.policyname, p.permissive, p.cmd, p.qual, p.with_check, p.roles::text[] AS roles,
49
+ (SELECT tn.nspname FROM pg_class tc JOIN pg_namespace tn ON tn.oid = tc.relnamespace
50
+ WHERE tc.oid = to_regclass('tenants')) AS tenants_schema
51
+ FROM pg_policies p
52
+ JOIN pg_class c ON c.relname = p.tablename
53
+ JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = p.schemaname
54
+ WHERE c.oid = to_regclass($1)`, [safe]);
55
+ const issues = (0, policy_check_js_1.tablePolicyIssues)(existing.rows, existing.rows[0]?.tenants_schema ?? undefined);
56
+ if (issues.length > 0) {
57
+ throw new Error(`[stratum] Table ${safe} has row-level security policies that do not isolate it by tenant: ` +
58
+ `${issues.join("; ")} (expected ${TENANT_FILTER}). ` +
59
+ `Drop or correct those policies, then call createPolicy again.`);
60
+ }
61
+ if (!existing.rows.some((p) => p.policyname === "tenant_isolation")) {
62
+ // Cannot use parameterized queries inside DO blocks or for DDL identifiers.
63
+ // Table name is validated via allowlist regex above.
64
+ await client.query(`CREATE POLICY tenant_isolation ON ${safe} USING (${TENANT_FILTER})`);
65
+ }
66
+ if (options.subtreeRead && !existing.rows.some((p) => p.policyname === "tenant_subtree_read")) {
67
+ await client.query(`CREATE POLICY tenant_subtree_read ON ${safe} FOR SELECT USING (${exports.SUBTREE_READ_FILTER})`);
22
68
  }
23
69
  }
24
70
  async function dropPolicy(client, tableName) {
25
71
  const safe = validateTableName(tableName);
26
72
  await client.query(`DROP POLICY IF EXISTS tenant_isolation ON ${safe}`);
73
+ await client.query(`DROP POLICY IF EXISTS tenant_subtree_read ON ${safe}`);
27
74
  }
28
75
  async function enableRLS(client, tableName) {
29
76
  const safe = validateTableName(tableName);
@@ -36,7 +83,9 @@ async function disableRLS(client, tableName) {
36
83
  await client.query(`ALTER TABLE ${safe} DISABLE ROW LEVEL SECURITY`);
37
84
  }
38
85
  async function isRLSEnabled(client, tableName) {
39
- const res = await client.query(`SELECT relrowsecurity FROM pg_class WHERE relname = $1`, [tableName]);
86
+ const safe = validateTableName(tableName);
87
+ // The table the name resolves to, in whichever schema that is.
88
+ const res = await client.query(`SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass($1)`, [safe]);
40
89
  if (res.rows.length === 0) {
41
90
  return false;
42
91
  }
@@ -1 +1 @@
1
- {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":";;AAUA,oCAgBC;AAED,gCAMC;AAED,8BAOC;AAED,gCAOC;AAED,oCAYC;AAhED,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,SAAiB;IAC1C,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,uBAAuB,SAAS,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,4EAA4E;IAC5E,qDAAqD;IACrD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAC/B,2FAA2F,EAC3F,CAAC,IAAI,CAAC,CACP,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7C,MAAM,MAAM,CAAC,KAAK,CAChB,qCAAqC,IAAI,uFAAuF,CACjI,CAAC;IACJ,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,UAAU,CAC9B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,6CAA6C,IAAI,EAAE,CAAC,CAAC;AAC1E,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,4BAA4B,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,2BAA2B,CAAC,CAAC;AACrE,CAAC;AAEM,KAAK,UAAU,UAAU,CAC9B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,8BAA8B,CAAC,CAAC;IACtE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,6BAA6B,CAAC,CAAC;AACvE,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,wDAAwD,EACxD,CAAC,SAAS,CAAC,CACZ,CAAC;IACF,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Decides whether the row-level security policies on a table restrict rows to
3
+ * the current tenant, from the expressions PostgreSQL stores in pg_policies.
4
+ *
5
+ * `createPolicy` uses this before it keeps an existing tenant_isolation policy
6
+ * or adds one, because a policy's name proves nothing about what it checks, and
7
+ * PostgreSQL ORs permissive policies together: one permissive policy that does
8
+ * not filter by tenant opens the table whatever tenant_isolation says.
9
+ *
10
+ * Rules:
11
+ * - Every permissive policy must filter by tenant for the commands it covers:
12
+ * USING for SELECT, UPDATE, DELETE and ALL, WITH CHECK for INSERT, and WITH
13
+ * CHECK for UPDATE and ALL when it is set. This applies whatever roles the
14
+ * policy names, because this package cannot know which role the
15
+ * application connects as.
16
+ * - Restrictive policies can only narrow access, so they may check anything.
17
+ * - The tenant_isolation policy itself must be permissive, apply to ALL
18
+ * commands, and apply to PUBLIC, like the one `createPolicy` generates and
19
+ * every policy Stratum ships.
20
+ *
21
+ * The check recognises the form this package generates, with the operands in
22
+ * either order, with casts, inside an AND with other conditions, and ORed with
23
+ * Stratum's administrative bypass. Anything else is reported as not filtering
24
+ * by tenant, so an unusual but correct policy is refused, never accepted.
25
+ *
26
+ * It also recognises the subtree read of migration 031: the
27
+ * stratum_subtree_tenant_ids() match ANDed with the app.tenant_scope =
28
+ * 'subtree' check. That form counts only in the USING clause of a SELECT
29
+ * policy. In any other policy, the same clause would let UPDATE or DELETE
30
+ * reach a descendant's rows, and in WITH CHECK it would let a write name a
31
+ * descendant.
32
+ */
33
+ /** One row of pg_policies. */
34
+ export interface PolicyRow {
35
+ policyname: string;
36
+ /** "PERMISSIVE" or "RESTRICTIVE". */
37
+ permissive: string;
38
+ /** "ALL", "SELECT", "INSERT", "UPDATE" or "DELETE". */
39
+ cmd: string;
40
+ qual: string | null;
41
+ with_check: string | null;
42
+ /** The roles the policy applies to; ["public"] for PUBLIC. */
43
+ roles: string[];
44
+ }
45
+ /**
46
+ * Every reason the table's policies do not isolate it by tenant. Empty when
47
+ * they do, or when the table has no policies.
48
+ *
49
+ * @param functionSchema - The schema of Stratum's tenants table, the only
50
+ * schema that may qualify stratum_subtree_tenant_ids(). When it is
51
+ * undefined, only the unqualified function counts.
52
+ */
53
+ export declare function tablePolicyIssues(policies: PolicyRow[], functionSchema?: string): string[];
54
+ //# sourceMappingURL=policy-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-check.d.ts","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,8BAA8B;AAC9B,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAoMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAiB1F"}
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ /**
3
+ * Decides whether the row-level security policies on a table restrict rows to
4
+ * the current tenant, from the expressions PostgreSQL stores in pg_policies.
5
+ *
6
+ * `createPolicy` uses this before it keeps an existing tenant_isolation policy
7
+ * or adds one, because a policy's name proves nothing about what it checks, and
8
+ * PostgreSQL ORs permissive policies together: one permissive policy that does
9
+ * not filter by tenant opens the table whatever tenant_isolation says.
10
+ *
11
+ * Rules:
12
+ * - Every permissive policy must filter by tenant for the commands it covers:
13
+ * USING for SELECT, UPDATE, DELETE and ALL, WITH CHECK for INSERT, and WITH
14
+ * CHECK for UPDATE and ALL when it is set. This applies whatever roles the
15
+ * policy names, because this package cannot know which role the
16
+ * application connects as.
17
+ * - Restrictive policies can only narrow access, so they may check anything.
18
+ * - The tenant_isolation policy itself must be permissive, apply to ALL
19
+ * commands, and apply to PUBLIC, like the one `createPolicy` generates and
20
+ * every policy Stratum ships.
21
+ *
22
+ * The check recognises the form this package generates, with the operands in
23
+ * either order, with casts, inside an AND with other conditions, and ORed with
24
+ * Stratum's administrative bypass. Anything else is reported as not filtering
25
+ * by tenant, so an unusual but correct policy is refused, never accepted.
26
+ *
27
+ * It also recognises the subtree read of migration 031: the
28
+ * stratum_subtree_tenant_ids() match ANDed with the app.tenant_scope =
29
+ * 'subtree' check. That form counts only in the USING clause of a SELECT
30
+ * policy. In any other policy, the same clause would let UPDATE or DELETE
31
+ * reach a descendant's rows, and in WITH CHECK it would let a write name a
32
+ * descendant.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.tablePolicyIssues = tablePolicyIssues;
36
+ /** The session setting that holds the current tenant (see rls/session.ts). */
37
+ const TENANT_SETTING = "app.current_tenant_id";
38
+ /** The setting Stratum's own policies admit for administrative access. */
39
+ const BYPASS_SETTING = "app.bypass_rls";
40
+ /** The setting that opts a session in to the subtree read scope (migration 031). */
41
+ const SCOPE_SETTING = "app.tenant_scope";
42
+ /** The function that returns the current tenant and its descendants (migration 031). */
43
+ const SUBTREE_FUNCTION = "stratum_subtree_tenant_ids";
44
+ /** Drops whitespace and casts, lower-cases, and keeps string literals intact. */
45
+ function normalize(expr) {
46
+ let out = "";
47
+ for (let i = 0; i < expr.length; i++) {
48
+ const ch = expr[i];
49
+ if (ch === "'") {
50
+ const end = expr.indexOf("'", i + 1);
51
+ const stop = end === -1 ? expr.length : end + 1;
52
+ out += expr.slice(i, stop);
53
+ i = stop - 1;
54
+ }
55
+ else if (!/\s/.test(ch)) {
56
+ out += ch.toLowerCase();
57
+ }
58
+ }
59
+ // Casts such as ::text, ::uuid and ::character varying (spaces already gone).
60
+ return out.replace(/::[a-z_]+(?:\[\])?/g, "");
61
+ }
62
+ /** Index of the parenthesis that closes the one at `open`. */
63
+ function closingParen(expr, open) {
64
+ let depth = 0;
65
+ for (let i = open; i < expr.length; i++) {
66
+ const ch = expr[i];
67
+ if (ch === "'") {
68
+ const end = expr.indexOf("'", i + 1);
69
+ if (end === -1)
70
+ return -1;
71
+ i = end;
72
+ }
73
+ else if (ch === "(") {
74
+ depth++;
75
+ }
76
+ else if (ch === ")") {
77
+ depth--;
78
+ if (depth === 0)
79
+ return i;
80
+ }
81
+ }
82
+ return -1;
83
+ }
84
+ /** Removes parentheses that wrap the whole expression. */
85
+ function stripOuterParens(expr) {
86
+ let e = expr;
87
+ while (e.startsWith("(") && e.endsWith(")") && closingParen(e, 0) === e.length - 1) {
88
+ e = e.slice(1, -1);
89
+ }
90
+ return e;
91
+ }
92
+ /**
93
+ * Splits a normalized expression on a top-level boolean operator. Whitespace
94
+ * is gone, so the operator is found between a closing parenthesis and an
95
+ * opening one, which is how PostgreSQL prints every operand of AND / OR.
96
+ */
97
+ function splitTopLevel(expr, op) {
98
+ const parts = [];
99
+ let depth = 0;
100
+ let start = 0;
101
+ for (let i = 0; i < expr.length; i++) {
102
+ const ch = expr[i];
103
+ if (ch === "'") {
104
+ const end = expr.indexOf("'", i + 1);
105
+ if (end === -1)
106
+ break;
107
+ i = end;
108
+ }
109
+ else if (ch === "(") {
110
+ depth++;
111
+ }
112
+ else if (ch === ")") {
113
+ depth--;
114
+ if (depth === 0 && expr.startsWith(`${op}(`, i + 1)) {
115
+ parts.push(expr.slice(start, i + 1));
116
+ start = i + 1 + op.length;
117
+ }
118
+ }
119
+ }
120
+ parts.push(expr.slice(start));
121
+ return parts.map(stripOuterParens);
122
+ }
123
+ /** Removes every parenthesis; only safe on an operand with no AND / OR in it. */
124
+ function flatten(expr) {
125
+ return expr.replace(/[()]/g, "");
126
+ }
127
+ const SETTING_READ = `current_setting'${TENANT_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?`;
128
+ /** The current tenant, read directly or through NULLIF(..., ''). */
129
+ const CURRENT_TENANT = `(?:nullif${SETTING_READ},''|${SETTING_READ})`;
130
+ const TENANT_MATCH = new RegExp(`^(?:tenant_id=${CURRENT_TENANT}|${CURRENT_TENANT}=tenant_id)$`);
131
+ const BYPASS_MATCH = new RegExp(`^current_setting'${BYPASS_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?='on'$`);
132
+ const SCOPE_MATCH = new RegExp(`^current_setting'${SCOPE_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?='subtree'$`);
133
+ /**
134
+ * tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[]) after
135
+ * normalize and flatten. PostgreSQL qualifies the function with its schema
136
+ * when that schema is not on the search path of the reading session. Only the
137
+ * schema that holds Stratum's tenants table, where migration 031 creates the
138
+ * function, may qualify it: a function of the same name in another schema
139
+ * could return any ids.
140
+ */
141
+ function subtreeMatch(functionSchema) {
142
+ const qualifier = functionSchema === undefined ? "" : `(?:${escapeRegExp(quoteIdent(functionSchema).toLowerCase())}\\.)?`;
143
+ return new RegExp(`^tenant_id=anyselect${qualifier}${SUBTREE_FUNCTION}(?:as${SUBTREE_FUNCTION})?$`);
144
+ }
145
+ function escapeRegExp(text) {
146
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
147
+ }
148
+ /** The identifier as PostgreSQL prints it: quoted unless it is a plain lowercase name. */
149
+ function quoteIdent(name) {
150
+ return /^[a-z_][a-z0-9_$]*$/.test(name) ? name : `"${name.replace(/"/g, '""')}"`;
151
+ }
152
+ /**
153
+ * True when the normalized expression only admits rows of the current tenant.
154
+ * `subtree` matches the subtree function call that may appear in the
155
+ * expression, or is null where the subtree read is not allowed.
156
+ */
157
+ function filtersByTenant(expr, subtree) {
158
+ const e = stripOuterParens(expr);
159
+ const disjuncts = splitTopLevel(e, "or");
160
+ if (disjuncts.length > 1) {
161
+ // Every branch must be safe; Stratum's administrative bypass is one of them.
162
+ return (disjuncts.every((d) => BYPASS_MATCH.test(flatten(d)) || filtersByTenant(d, subtree)) &&
163
+ disjuncts.some((d) => !BYPASS_MATCH.test(flatten(d))));
164
+ }
165
+ const conjuncts = splitTopLevel(e, "and");
166
+ if (conjuncts.length > 1) {
167
+ if (conjuncts.some((c) => filtersByTenant(c, subtree)))
168
+ return true;
169
+ // The subtree read of migration 031: the descendants of the current
170
+ // tenant, only when the session opted in with app.tenant_scope.
171
+ return (subtree !== null &&
172
+ conjuncts.some((c) => SCOPE_MATCH.test(flatten(c))) &&
173
+ conjuncts.some((c) => subtree.test(flatten(c))));
174
+ }
175
+ return TENANT_MATCH.test(flatten(e));
176
+ }
177
+ /** An expression on one line, for messages. */
178
+ function oneLine(expr) {
179
+ return expr === null ? "none" : expr.replace(/\s+/g, " ");
180
+ }
181
+ /** Why one permissive policy lets rows of other tenants through, or null. */
182
+ function permissiveIssue(p, subtree) {
183
+ const name = `policy "${oneLine(p.policyname)}" (${p.cmd})`;
184
+ // INSERT policies have only WITH CHECK; SELECT and DELETE only USING. For
185
+ // ALL and UPDATE a missing WITH CHECK means PostgreSQL reuses USING.
186
+ if (p.cmd !== "INSERT") {
187
+ // Only a SELECT policy may widen to the subtree: in any other policy the
188
+ // same USING clause would let UPDATE or DELETE reach a descendant's rows.
189
+ if (p.qual === null || !filtersByTenant(normalize(p.qual), p.cmd === "SELECT" ? subtree : null)) {
190
+ return `${name} USING (${oneLine(p.qual)}) does not filter by tenant`;
191
+ }
192
+ }
193
+ if (p.cmd === "INSERT" || p.with_check !== null) {
194
+ if (p.with_check === null || !filtersByTenant(normalize(p.with_check), null)) {
195
+ return `${name} WITH CHECK (${oneLine(p.with_check)}) does not filter by tenant`;
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ /** Why the tenant_isolation policy is not the shape createPolicy generates, or null. */
201
+ function tenantIsolationShapeIssue(p) {
202
+ const name = `policy "${oneLine(p.policyname)}"`;
203
+ if (p.permissive !== "PERMISSIVE") {
204
+ return `${name} is ${p.permissive.toLowerCase()}, not permissive`;
205
+ }
206
+ if (p.cmd !== "ALL") {
207
+ return `${name} applies to ${p.cmd} only, not ALL commands`;
208
+ }
209
+ if (p.roles.length !== 1 || p.roles[0] !== "public") {
210
+ return `${name} applies to roles ${p.roles.join(", ")}, not PUBLIC`;
211
+ }
212
+ return null;
213
+ }
214
+ /**
215
+ * Every reason the table's policies do not isolate it by tenant. Empty when
216
+ * they do, or when the table has no policies.
217
+ *
218
+ * @param functionSchema - The schema of Stratum's tenants table, the only
219
+ * schema that may qualify stratum_subtree_tenant_ids(). When it is
220
+ * undefined, only the unqualified function counts.
221
+ */
222
+ function tablePolicyIssues(policies, functionSchema) {
223
+ const subtree = subtreeMatch(functionSchema);
224
+ const issues = [];
225
+ for (const p of policies) {
226
+ if (p.policyname === "tenant_isolation") {
227
+ const shape = tenantIsolationShapeIssue(p);
228
+ if (shape !== null) {
229
+ issues.push(shape);
230
+ continue;
231
+ }
232
+ }
233
+ if (p.permissive === "PERMISSIVE") {
234
+ const issue = permissiveIssue(p, subtree);
235
+ if (issue !== null)
236
+ issues.push(issue);
237
+ }
238
+ }
239
+ return issues;
240
+ }
241
+ //# sourceMappingURL=policy-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-check.js","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,34 @@
1
1
  import pg from "pg";
2
- export declare function setTenantContext(client: pg.PoolClient, tenantId: string): Promise<void>;
2
+ /**
3
+ * Which rows a tenant context can read.
4
+ *
5
+ * - "exact": the rows of the tenant only. This is the default.
6
+ * - "subtree": the rows of the tenant and of every descendant, through the
7
+ * tenant_subtree_read policies of migration 031. Writes stay limited to the
8
+ * exact tenant in both scopes, and so do reads of credential-bearing rows
9
+ * (api_keys, webhooks and sensitive config_entries).
10
+ */
11
+ export type TenantScope = "exact" | "subtree";
12
+ export interface TenantContextOptions {
13
+ /** The read scope. Default "exact". */
14
+ scope?: TenantScope;
15
+ }
16
+ /**
17
+ * Sets the tenant and the read scope for the current transaction.
18
+ *
19
+ * It always writes both settings, so a second call in the same transaction
20
+ * replaces an earlier "subtree" scope instead of keeping it.
21
+ */
22
+ export declare function setTenantContext(client: pg.PoolClient, tenantId: string, options?: TenantContextOptions): Promise<void>;
3
23
  export declare function resetTenantContext(client: pg.PoolClient): Promise<void>;
4
- export declare function withTenantContext<T>(pool: pg.Pool, tenantId: string, fn: (client: pg.PoolClient) => Promise<T>): Promise<T>;
24
+ /**
25
+ * Runs `fn` in a transaction that is scoped to one tenant.
26
+ *
27
+ * @param options `{ scope: "subtree" }` also lets `fn` read the rows of every
28
+ * descendant of the tenant. Writes stay limited to the tenant.
29
+ * @throws Error when `options.scope` is not "exact" or "subtree".
30
+ */
31
+ export declare function withTenantContext<T>(pool: pg.Pool, tenantId: string, fn: (client: pg.PoolClient) => Promise<T>, options?: TenantContextOptions): Promise<T>;
5
32
  /**
6
33
  * Runs `fn` inside a transaction with the RLS bypass flag set, so control-plane
7
34
  * / system operations that legitimately span tenant boundaries (provisioning,
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7E;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAcZ;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAcZ;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,EAAE,CAAC,UAAU,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMxB"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACnC,uCAAuC;IACvC,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAaD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7E;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,EACzC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,CAAC,CAAC,CAgBZ;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAcZ;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,EAAE,CAAC,UAAU,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMxB"}
@@ -5,17 +5,40 @@ exports.resetTenantContext = resetTenantContext;
5
5
  exports.withTenantContext = withTenantContext;
6
6
  exports.withRlsBypass = withRlsBypass;
7
7
  exports.getCurrentTenantId = getCurrentTenantId;
8
- async function setTenantContext(client, tenantId) {
9
- await client.query("SELECT set_config('app.current_tenant_id', $1, true)", [tenantId]);
8
+ /** The value of app.tenant_scope for a scope. Only 'subtree' widens reads. */
9
+ function scopeSetting(options) {
10
+ const scope = options.scope ?? "exact";
11
+ if (scope !== "exact" && scope !== "subtree") {
12
+ throw new Error(`[stratum] Unknown tenant scope: ${String(scope)} (expected "exact" or "subtree")`);
13
+ }
14
+ return scope === "subtree" ? "subtree" : "";
15
+ }
16
+ /**
17
+ * Sets the tenant and the read scope for the current transaction.
18
+ *
19
+ * It always writes both settings, so a second call in the same transaction
20
+ * replaces an earlier "subtree" scope instead of keeping it.
21
+ */
22
+ async function setTenantContext(client, tenantId, options = {}) {
23
+ await client.query("SELECT set_config('app.current_tenant_id', $1, true), set_config('app.tenant_scope', $2, true)", [tenantId, scopeSetting(options)]);
10
24
  }
11
25
  async function resetTenantContext(client) {
12
- await client.query("SELECT set_config('app.current_tenant_id', '', true)");
26
+ await client.query("SELECT set_config('app.current_tenant_id', '', true), set_config('app.tenant_scope', '', true)");
13
27
  }
14
- async function withTenantContext(pool, tenantId, fn) {
28
+ /**
29
+ * Runs `fn` in a transaction that is scoped to one tenant.
30
+ *
31
+ * @param options `{ scope: "subtree" }` also lets `fn` read the rows of every
32
+ * descendant of the tenant. Writes stay limited to the tenant.
33
+ * @throws Error when `options.scope` is not "exact" or "subtree".
34
+ */
35
+ async function withTenantContext(pool, tenantId, fn, options = {}) {
36
+ // Fail on an unknown scope before a connection is taken.
37
+ scopeSetting(options);
15
38
  const client = await pool.connect();
16
39
  try {
17
40
  await client.query("BEGIN");
18
- await setTenantContext(client, tenantId);
41
+ await setTenantContext(client, tenantId, options);
19
42
  const result = await fn(client);
20
43
  await client.query("COMMIT");
21
44
  return result;
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":";;AAEA,4CAKC;AAED,gDAEC;AAED,8CAkBC;AAeD,sCAiBC;AAED,gDAQC;AAvEM,KAAK,UAAU,gBAAgB,CACpC,MAAqB,EACrB,QAAgB;IAEhB,MAAM,MAAM,CAAC,KAAK,CAAC,sDAAsD,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AACzF,CAAC;AAEM,KAAK,UAAU,kBAAkB,CAAC,MAAqB;IAC5D,MAAM,MAAM,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;AAC7E,CAAC;AAEM,KAAK,UAAU,iBAAiB,CACrC,IAAa,EACb,QAAgB,EAChB,EAAyC;IAEzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,aAAa,CACjC,IAAa,EACb,EAAyC;IAEzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,MAAM,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACtE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,kBAAkB,CACtC,MAAqB;IAErB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,0EAA0E,CAC3E,CAAC;IACF,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC;IAC3C,OAAO,KAAK,IAAI,IAAI,CAAC;AACvB,CAAC"}
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":";;AAmCA,4CASC;AAED,gDAIC;AASD,8CAqBC;AAeD,sCAiBC;AAED,gDAQC;AAxGD,8EAA8E;AAC9E,SAAS,YAAY,CAAC,OAA6B;IACjD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC;IACvC,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CACb,mCAAmC,MAAM,CAAC,KAAK,CAAC,kCAAkC,CACnF,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9C,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,gBAAgB,CACpC,MAAqB,EACrB,QAAgB,EAChB,UAAgC,EAAE;IAElC,MAAM,MAAM,CAAC,KAAK,CAChB,gGAAgG,EAChG,CAAC,QAAQ,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,CAClC,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,kBAAkB,CAAC,MAAqB;IAC5D,MAAM,MAAM,CAAC,KAAK,CAChB,gGAAgG,CACjG,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,iBAAiB,CACrC,IAAa,EACb,QAAgB,EAChB,EAAyC,EACzC,UAAgC,EAAE;IAElC,yDAAyD;IACzD,YAAY,CAAC,OAAO,CAAC,CAAC;IACtB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,gBAAgB,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,aAAa,CACjC,IAAa,EACb,EAAyC;IAEzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,MAAM,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACtE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,kBAAkB,CACtC,MAAqB;IAErB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,0EAA0E,CAC3E,CAAC;IACF,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC;IAC3C,OAAO,KAAK,IAAI,IAAI,CAAC;AACvB,CAAC"}
package/package.json CHANGED
@@ -1,17 +1,26 @@
1
1
  {
2
2
  "name": "@stratum-hq/db-adapters",
3
- "version": "1.2.0",
4
- "description": "Stratum PostgreSQL adapters for tenant-scoped query isolation",
3
+ "version": "1.4.0",
4
+ "description": "Multi-tenant PostgreSQL row-level security for pg, Prisma, Drizzle, and Sequelize, plus schema-per-tenant and database-per-tenant isolation",
5
5
  "keywords": [
6
6
  "multi-tenancy",
7
+ "multitenancy",
7
8
  "multi-tenant",
8
9
  "saas",
9
10
  "tenant",
10
11
  "tenant-isolation",
11
- "postgresql",
12
- "postgres",
13
12
  "row-level-security",
14
13
  "rls",
14
+ "postgresql",
15
+ "prisma",
16
+ "drizzle",
17
+ "drizzle-orm",
18
+ "sequelize",
19
+ "pg",
20
+ "node-postgres",
21
+ "schema-per-tenant",
22
+ "database-per-tenant",
23
+ "postgres",
15
24
  "schema-isolation",
16
25
  "database",
17
26
  "stratum"
@@ -22,6 +31,10 @@
22
31
  ".": {
23
32
  "types": "./dist/index.d.ts",
24
33
  "default": "./dist/index.js"
34
+ },
35
+ "./pglite": {
36
+ "types": "./dist/pglite/index.d.ts",
37
+ "default": "./dist/pglite/index.js"
25
38
  }
26
39
  },
27
40
  "files": [
@@ -41,18 +54,23 @@
41
54
  "registry": "https://registry.npmjs.org/"
42
55
  },
43
56
  "dependencies": {
44
- "@stratum-hq/core": "^1.4.0",
57
+ "@stratum-hq/core": "^1.5.1",
45
58
  "pg": "^8.11.0"
46
59
  },
47
60
  "peerDependencies": {
61
+ "@electric-sql/pglite": "^0.4.2",
48
62
  "drizzle-orm": ">=0.29.0"
49
63
  },
50
64
  "peerDependenciesMeta": {
65
+ "@electric-sql/pglite": {
66
+ "optional": true
67
+ },
51
68
  "drizzle-orm": {
52
69
  "optional": true
53
70
  }
54
71
  },
55
72
  "devDependencies": {
73
+ "@electric-sql/pglite": "^0.4.6",
56
74
  "@prisma/client": "^5.10.0",
57
75
  "@types/pg": "^8.11.0",
58
76
  "drizzle-orm": "^0.45.3",
@@ -61,7 +79,7 @@
61
79
  },
62
80
  "license": "MIT",
63
81
  "author": "Christian Crank",
64
- "homepage": "https://github.com/stratum-hq/Stratum/tree/main/packages/db-adapters#readme",
82
+ "homepage": "https://docs.stratum-hq.org/packages/db-adapters/",
65
83
  "bugs": "https://github.com/stratum-hq/Stratum/issues",
66
84
  "engines": {
67
85
  "node": ">=20.0.0"