@btravstack/prisma 0.32.0 → 0.34.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 +28 -8
- package/dist/rls.cjs +173 -0
- package/dist/rls.d.cts +75 -2
- package/dist/rls.d.mts +75 -2
- package/dist/rls.mjs +172 -1
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -17,6 +17,14 @@ copy of each. The `prisma` CLI is yours, as a dev dependency: the client is
|
|
|
17
17
|
built from _your_ emitted contract, so there is none for this package to ship.
|
|
18
18
|
Node `>=22.12`.
|
|
19
19
|
|
|
20
|
+
**Production packaging:** the pinned Postgres facade requires
|
|
21
|
+
`@prisma/orm-toolchain`, including its CLI-engine peer and build-tool
|
|
22
|
+
closure. A `/runtime` import and `pnpm deploy --prod` do not remove those
|
|
23
|
+
transitive dependencies. Keep contract emission in the build stage and
|
|
24
|
+
migrations in a deployment Job, with strict peers enabled; a toolchain-free
|
|
25
|
+
runtime install remains [blocked upstream](https://btravstack.github.io/btravstack/reference/prisma#production-dependency-closure).
|
|
26
|
+
That section documents the manifest split and the tracked dependency.
|
|
27
|
+
|
|
20
28
|
**This is Prisma 8**, which is a different package family rather than a version
|
|
21
29
|
bump — `@prisma/orm-postgres` replaces `@prisma/client` and `@prisma/adapter-pg`
|
|
22
30
|
both, and there is no `@prisma/instrumentation`. For Prisma 7 use
|
|
@@ -162,9 +170,19 @@ nor a superuser — the boot refuses either with `RowSecurityBypassed`. The
|
|
|
162
170
|
[reference page](https://btravstack.github.io/btravstack/reference/prisma)
|
|
163
171
|
carries the DDL and the smaller lines that fail the same way.
|
|
164
172
|
|
|
173
|
+
## Tenant discovery for scheduled work
|
|
174
|
+
|
|
175
|
+
`tenantListing` on `/rls` declares a tenant-ID-only query (100 IDs per page,
|
|
176
|
+
resuming after the last ID) and a
|
|
177
|
+
checked Prisma migration installing its function, owner policy and execution
|
|
178
|
+
grant. It works under forced row-level security while ordinary application
|
|
179
|
+
reads remain tenant-scoped. Shared data does not require a privileged
|
|
180
|
+
application connection. See
|
|
181
|
+
[tenant listing](https://btravstack.github.io/btravstack/reference/prisma#list-tenants-without-cross-tenant-row-access).
|
|
182
|
+
|
|
165
183
|
## What it does not
|
|
166
184
|
|
|
167
|
-
**
|
|
185
|
+
**Running migrations.** A deployment runs `prisma db migrate` against the same database
|
|
168
186
|
_before the process starts_ — and, where row security is on, as its **owner**
|
|
169
187
|
rather than the role `DATABASE_URL` carries. An application that migrates itself
|
|
170
188
|
at boot races every other replica.
|
|
@@ -184,13 +202,15 @@ package's.
|
|
|
184
202
|
|
|
185
203
|
## Options
|
|
186
204
|
|
|
187
|
-
| Option
|
|
188
|
-
|
|
|
189
|
-
| `name`
|
|
190
|
-
| `client`
|
|
191
|
-
| `DATABASE_URL`
|
|
192
|
-
| `setting`
|
|
193
|
-
| `raises`
|
|
205
|
+
| Option | Where | What it is |
|
|
206
|
+
| --------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
207
|
+
| `name` | `prismaDatabase(name)` | the port's id and the health check's name — required |
|
|
208
|
+
| `client` | `prismaDatabase(name)({ client })` | builds your client from the URL and middleware this package bound — required |
|
|
209
|
+
| `DATABASE_URL` | environment, read by `prismaDatabase(name)` | the connection string — required, validated at graph build; blank is an error, exit `78` |
|
|
210
|
+
| `setting` | `tenantPinned(db, tenant, work, { setting })` | the PostgreSQL run-time setting the policy reads — default `app.tenant_id` |
|
|
211
|
+
| `raises` | `tryQuery(run, { raises })`, `tenantPinned(…, { raises })` | SQLSTATEs modeled as `RaisedException` rather than defects — default none |
|
|
212
|
+
| `schema`, `table`, `tenantColumn` | `tenantListing(options)` | the physical table and tenant column for a narrow listing |
|
|
213
|
+
| `name`, `owner`, `role` | `tenantListing(options)` | the listing function, its trusted owner and application caller |
|
|
194
214
|
|
|
195
215
|
There is **no `instrumented` flag**: observation is a set port every call is
|
|
196
216
|
handed to, so a graph composing no observability pays one inert call and no
|
package/dist/rls.cjs
CHANGED
|
@@ -1,5 +1,176 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_result = require("./result.cjs");
|
|
3
|
+
let unthrown = require("unthrown");
|
|
4
|
+
//#region src/tenant-listing.ts
|
|
5
|
+
/** A tenant-listing declaration names an invalid identifier or shares its privileged owner with its caller. */
|
|
6
|
+
var TenantListingInvalid = class extends (0, unthrown.TaggedError)("TenantListingInvalid") {};
|
|
7
|
+
const identifier = (value) => `"${value.replaceAll("\"", "\"\"")}"`;
|
|
8
|
+
const literal = (value) => `E'${value.replaceAll("\\", "\\\\").replaceAll("'", "''")}'`;
|
|
9
|
+
const statement = (parts) => Object.assign(parts, { raw: [...parts] });
|
|
10
|
+
/**
|
|
11
|
+
* Declare a paged SECURITY DEFINER function returning distinct, non-null
|
|
12
|
+
* tenant IDs as text, and a typed query calling only that function. Each page
|
|
13
|
+
* holds at most 100 IDs in bytewise order; resume after its last ID until empty.
|
|
14
|
+
*
|
|
15
|
+
* Pass `migration` to Prisma 8's `rawSql` inside a reviewed migration. Its
|
|
16
|
+
* transaction installs an owner-only SELECT policy, grants the owner access
|
|
17
|
+
* to the tenant column, fixes the function's search path, revokes PUBLIC
|
|
18
|
+
* execution and grants execution to `role`. Existing table policies and FORCE
|
|
19
|
+
* ROW LEVEL SECURITY are left intact. No application table privilege is added.
|
|
20
|
+
*
|
|
21
|
+
* Roles and the table must already exist. The migrator must be able to SET ROLE
|
|
22
|
+
* to the owner; provision that membership for an ordinary migration role.
|
|
23
|
+
* The migration refuses a privileged application or function-owner role, caller membership in the owner, schema
|
|
24
|
+
* CREATE for the caller, owner payload SELECT, or any same-name function overload. Its postcheck verifies the
|
|
25
|
+
* function, policy and execution grants before Prisma records it as applied.
|
|
26
|
+
* Apply the operation transactionally; the function is initially private only
|
|
27
|
+
* once the REVOKE runs. Do not grant the application the owner role afterwards.
|
|
28
|
+
*/
|
|
29
|
+
const tenantListing = (options) => {
|
|
30
|
+
const names = {
|
|
31
|
+
schema: options.schema,
|
|
32
|
+
table: options.table,
|
|
33
|
+
tenantColumn: options.tenantColumn ?? "tenantId",
|
|
34
|
+
name: options.name,
|
|
35
|
+
owner: options.owner,
|
|
36
|
+
role: options.role
|
|
37
|
+
};
|
|
38
|
+
for (const [field, value] of Object.entries(names)) if (typeof value !== "string" || value.length === 0 || value.includes("\0") || Buffer.from(value, "utf8").toString("utf8") !== value || Buffer.byteLength(value, "utf8") > 63) return (0, unthrown.Err)(new TenantListingInvalid({
|
|
39
|
+
field,
|
|
40
|
+
reason: "invalid-identifier"
|
|
41
|
+
}));
|
|
42
|
+
if (names.owner === names.role) return (0, unthrown.Err)(new TenantListingInvalid({
|
|
43
|
+
field: "role",
|
|
44
|
+
reason: "shared-role"
|
|
45
|
+
}));
|
|
46
|
+
const schema = identifier(names.schema);
|
|
47
|
+
const table = `${schema}.${identifier(names.table)}`;
|
|
48
|
+
const fn = `${schema}.${identifier(names.name)}`;
|
|
49
|
+
const owner = identifier(names.owner);
|
|
50
|
+
const role = identifier(names.role);
|
|
51
|
+
const column = identifier(names.tenantColumn);
|
|
52
|
+
const functionRef = `pg_catalog.to_regprocedure(${literal(`${fn}(pg_catalog.text)`)})`;
|
|
53
|
+
const tableRef = `pg_catalog.to_regclass(${literal(table)})`;
|
|
54
|
+
const ownerRef = `(SELECT oid FROM pg_catalog.pg_roles WHERE rolname = ${literal(names.owner)})`;
|
|
55
|
+
const roleRef = `(SELECT oid FROM pg_catalog.pg_roles WHERE rolname = ${literal(names.role)})`;
|
|
56
|
+
const body = `SELECT DISTINCT ${column}::pg_catalog.text COLLATE pg_catalog."C" AS "tenantId" FROM ${table} WHERE ${column} IS NOT NULL AND ($1 IS NULL OR ${column}::pg_catalog.text COLLATE pg_catalog."C" > $1 COLLATE pg_catalog."C") ORDER BY "tenantId" LIMIT 100`;
|
|
57
|
+
const policy = `CURRENT_USER = ${literal(names.owner)}::pg_catalog.name`;
|
|
58
|
+
const assumption = "CASE WHEN pg_catalog.current_setting('server_version_num')::pg_catalog.int4 >= 160000 THEN 'SET' ELSE 'MEMBER' END";
|
|
59
|
+
const requirements = `EXISTS (
|
|
60
|
+
SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
|
|
61
|
+
JOIN pg_catalog.pg_roles o ON o.rolname = ${literal(names.owner)}
|
|
62
|
+
JOIN pg_catalog.pg_roles r ON r.rolname = ${literal(names.role)}
|
|
63
|
+
WHERE c.oid = ${tableRef} AND c.relrowsecurity AND a.attname = ${literal(names.tenantColumn)} AND a.attnum > 0 AND NOT a.attisdropped
|
|
64
|
+
AND NOT o.rolsuper AND NOT o.rolbypassrls AND NOT r.rolsuper AND NOT r.rolbypassrls
|
|
65
|
+
AND NOT pg_catalog.pg_has_role(r.oid, o.oid, 'MEMBER')
|
|
66
|
+
AND NOT EXISTS (
|
|
67
|
+
SELECT 1 FROM pg_catalog.pg_attribute payload
|
|
68
|
+
WHERE payload.attrelid = c.oid AND payload.attnum > 0 AND NOT payload.attisdropped
|
|
69
|
+
AND payload.attname <> ${literal(names.tenantColumn)}
|
|
70
|
+
AND pg_catalog.has_column_privilege(o.oid, c.oid, payload.attname, 'SELECT')
|
|
71
|
+
)
|
|
72
|
+
AND pg_catalog.has_schema_privilege(o.oid, ${literal(names.schema)}, 'CREATE')
|
|
73
|
+
AND NOT EXISTS (
|
|
74
|
+
SELECT 1 FROM pg_catalog.pg_roles creator
|
|
75
|
+
WHERE pg_catalog.pg_has_role(r.oid, creator.oid, ${assumption})
|
|
76
|
+
AND pg_catalog.has_schema_privilege(creator.oid, ${literal(names.schema)}, 'CREATE')
|
|
77
|
+
)
|
|
78
|
+
AND NOT EXISTS (
|
|
79
|
+
SELECT 1 FROM pg_catalog.pg_policy blocked WHERE blocked.polrelid = c.oid
|
|
80
|
+
AND NOT blocked.polpermissive AND blocked.polcmd IN ('r', '*')
|
|
81
|
+
AND EXISTS (SELECT 1 FROM pg_catalog.unnest(blocked.polroles) inherited(role)
|
|
82
|
+
WHERE CASE WHEN inherited.role = 0 THEN true ELSE pg_catalog.pg_has_role(o.oid, inherited.role, 'USAGE') END)
|
|
83
|
+
)
|
|
84
|
+
)`;
|
|
85
|
+
const functions = `SELECT p.oid FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = ${literal(names.schema)} AND p.proname = ${literal(names.name)}`;
|
|
86
|
+
const ready = `SELECT ${requirements} AND (SELECT count(*) FROM (${functions}) candidates) = 1 AND EXISTS (
|
|
87
|
+
SELECT 1 FROM pg_catalog.pg_proc p
|
|
88
|
+
WHERE p.oid = ${functionRef}
|
|
89
|
+
AND p.prosecdef AND NOT p.proisstrict AND p.proretset AND p.pronargs = 1 AND p.pronargdefaults = 1 AND pg_catalog.pg_get_expr(p.proargdefaults, 0) IN ('NULL::text', 'NULL::pg_catalog.text') AND p.provolatile = 's'
|
|
90
|
+
AND p.prolang = (SELECT oid FROM pg_catalog.pg_language WHERE lanname = 'sql')
|
|
91
|
+
AND p.proowner = ${ownerRef} AND p.prosrc = ${literal(body)}
|
|
92
|
+
AND p.proallargtypes = ARRAY['pg_catalog.text'::pg_catalog.regtype, 'pg_catalog.text'::pg_catalog.regtype]::pg_catalog.oid[]
|
|
93
|
+
AND p.proargnames = ARRAY['after', 'tenantId']::pg_catalog.text[] AND p.proargmodes = ARRAY['i', 't']::pg_catalog."char"[]
|
|
94
|
+
AND p.proconfig = ARRAY['search_path=pg_catalog, pg_temp', 'row_security=on']::pg_catalog.text[]
|
|
95
|
+
AND pg_catalog.has_function_privilege(${roleRef}, p.oid, 'EXECUTE')
|
|
96
|
+
AND pg_catalog.has_schema_privilege(${roleRef}, ${literal(names.schema)}, 'USAGE')
|
|
97
|
+
AND pg_catalog.has_schema_privilege(${ownerRef}, ${literal(names.schema)}, 'USAGE')
|
|
98
|
+
AND pg_catalog.has_column_privilege(${ownerRef}, ${tableRef}, ${literal(names.tenantColumn)}, 'SELECT')
|
|
99
|
+
AND NOT EXISTS (
|
|
100
|
+
SELECT 1 FROM pg_catalog.aclexplode(COALESCE(p.proacl, pg_catalog.acldefault('f', p.proowner))) a
|
|
101
|
+
WHERE a.privilege_type = 'EXECUTE' AND (a.grantee NOT IN (${ownerRef}, ${roleRef}) OR (a.grantee = ${roleRef} AND a.is_grantable))
|
|
102
|
+
)
|
|
103
|
+
AND EXISTS (
|
|
104
|
+
SELECT 1 FROM pg_catalog.pg_policy pol WHERE pol.polrelid = ${tableRef}
|
|
105
|
+
AND pol.polname = ${literal(names.name)} AND pol.polcmd = 'r' AND pol.polpermissive
|
|
106
|
+
AND pol.polroles = ARRAY[${ownerRef}]::pg_catalog.oid[]
|
|
107
|
+
AND pg_catalog.pg_get_expr(pol.polqual, pol.polrelid) = '(CURRENT_USER = ' || pg_catalog.quote_literal(${literal(names.owner)}) || '::name)'
|
|
108
|
+
)
|
|
109
|
+
) AS "result"`;
|
|
110
|
+
const parts = [`SELECT "tenantId" FROM ${fn}(`, `) ORDER BY "tenantId" COLLATE pg_catalog."C"`];
|
|
111
|
+
return (0, unthrown.Ok)({
|
|
112
|
+
migration: {
|
|
113
|
+
id: `tenantListing.${encodeURIComponent(names.schema).replaceAll(".", "%2E")}.${encodeURIComponent(names.name).replaceAll(".", "%2E")}`,
|
|
114
|
+
label: `List tenant IDs from ${table}`,
|
|
115
|
+
operationClass: "additive",
|
|
116
|
+
target: {
|
|
117
|
+
id: "postgres",
|
|
118
|
+
details: {
|
|
119
|
+
schema: names.schema,
|
|
120
|
+
objectType: "rlsPolicy",
|
|
121
|
+
table: names.table,
|
|
122
|
+
name: names.name
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
precheck: [{
|
|
126
|
+
description: "require a row-secured table, restricted roles, and unused function and policy names",
|
|
127
|
+
sql: `SELECT ${requirements} AND pg_catalog.pg_has_role(CURRENT_USER, ${ownerRef}, ${assumption}) AND NOT EXISTS (${functions}) AND NOT EXISTS (
|
|
128
|
+
SELECT 1 FROM pg_catalog.pg_policy WHERE polrelid = ${tableRef} AND polname = ${literal(names.name)}
|
|
129
|
+
) AS "result"`
|
|
130
|
+
}],
|
|
131
|
+
execute: [
|
|
132
|
+
{
|
|
133
|
+
description: "allow the function owner to read tenant IDs under forced RLS",
|
|
134
|
+
sql: `CREATE POLICY ${identifier(names.name)} ON ${table} FOR SELECT TO ${owner} USING (${policy})`
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
description: "grant schema access to the function owner and caller",
|
|
138
|
+
sql: `GRANT USAGE ON SCHEMA ${schema} TO ${owner}, ${role}`
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
description: "grant the function owner the tenant column",
|
|
142
|
+
sql: `GRANT SELECT (${column}) ON TABLE ${table} TO ${owner}`
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
description: "create the tenant-id-only function with a fixed search path",
|
|
146
|
+
sql: `CREATE FUNCTION ${fn}("after" pg_catalog.text DEFAULT NULL) RETURNS TABLE ("tenantId" pg_catalog.text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET row_security = on AS ${literal(body)}`
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
description: "remove public execution",
|
|
150
|
+
sql: `REVOKE ALL ON FUNCTION ${fn}(pg_catalog.text) FROM PUBLIC`
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
description: "grant execution to the application role",
|
|
154
|
+
sql: `GRANT EXECUTE ON FUNCTION ${fn}(pg_catalog.text) TO ${role}`
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
description: "assign the restricted function owner",
|
|
158
|
+
sql: `ALTER FUNCTION ${fn}(pg_catalog.text) OWNER TO ${owner}`
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
postcheck: [{
|
|
162
|
+
description: "verify the listing function, owner policy, and narrow execution grants",
|
|
163
|
+
sql: ready
|
|
164
|
+
}]
|
|
165
|
+
},
|
|
166
|
+
list: (db, after) => require_result.tryQuery(() => {
|
|
167
|
+
const client = db;
|
|
168
|
+
const plan = (after === void 0 ? client.raw.sql(statement([`${parts[0]}NULL${parts[1]}`])) : client.raw.sql(statement(parts), after)).returnsRow({ tenantId: "pg/text@1" }).build();
|
|
169
|
+
return client.runtime().query(plan);
|
|
170
|
+
}).map((rows) => rows.map((row) => row.tenantId))
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
3
174
|
//#region src/rls.ts
|
|
4
175
|
/**
|
|
5
176
|
* Runs `work` in a transaction whose connection is pinned to `tenant`, so a
|
|
@@ -38,4 +209,6 @@ const tenantPinned = (db, tenant, work, options) => {
|
|
|
38
209
|
}), options);
|
|
39
210
|
};
|
|
40
211
|
//#endregion
|
|
212
|
+
exports.TenantListingInvalid = TenantListingInvalid;
|
|
213
|
+
exports.tenantListing = tenantListing;
|
|
41
214
|
exports.tenantPinned = tenantPinned;
|
package/dist/rls.d.cts
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
import { QualifyOptions, Raised, SqlError } from "./result.cjs";
|
|
2
|
-
import { AsyncResult } from "unthrown";
|
|
2
|
+
import { AsyncResult, Result } from "unthrown";
|
|
3
|
+
//#region src/tenant-listing.d.ts
|
|
4
|
+
declare const TenantListingInvalid_base: import("unthrown").TaggedErrorConstructor<"TenantListingInvalid">;
|
|
5
|
+
/** A tenant-listing declaration names an invalid identifier or shares its privileged owner with its caller. */
|
|
6
|
+
export declare class TenantListingInvalid extends TenantListingInvalid_base<{
|
|
7
|
+
readonly field: keyof TenantListingOptions;
|
|
8
|
+
readonly reason: "invalid-identifier" | "shared-role";
|
|
9
|
+
}> {}
|
|
10
|
+
/** Physical database names and existing roles for a narrow tenant-listing capability. */
|
|
11
|
+
type TenantListingOptions = {
|
|
12
|
+
readonly schema: string;
|
|
13
|
+
readonly table: string;
|
|
14
|
+
/** The physical tenant column. Default `tenantId`. */
|
|
15
|
+
readonly tenantColumn?: string;
|
|
16
|
+
/** A new paged function in `schema`; also names its select policy. */
|
|
17
|
+
readonly name: string;
|
|
18
|
+
/** Trusted function owner with no payload SELECT, superuser or BYPASSRLS; needs schema CREATE. */
|
|
19
|
+
readonly owner: string;
|
|
20
|
+
/** Application role granted execution, with no membership in `owner` and no schema CREATE privilege. */
|
|
21
|
+
readonly role: string;
|
|
22
|
+
};
|
|
23
|
+
type Statement = {
|
|
24
|
+
readonly description: string;
|
|
25
|
+
readonly sql: string;
|
|
26
|
+
};
|
|
27
|
+
/** A declaration's migration, ready for Prisma 8's `rawSql`, and its tenant-id-only query. */
|
|
28
|
+
type TenantListing = {
|
|
29
|
+
readonly migration: {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly label: string;
|
|
32
|
+
readonly operationClass: "additive";
|
|
33
|
+
readonly target: {
|
|
34
|
+
readonly id: "postgres";
|
|
35
|
+
readonly details: {
|
|
36
|
+
readonly schema: string;
|
|
37
|
+
readonly objectType: "rlsPolicy";
|
|
38
|
+
readonly table: string;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
readonly precheck: readonly Statement[];
|
|
43
|
+
readonly execute: readonly Statement[];
|
|
44
|
+
readonly postcheck: readonly Statement[];
|
|
45
|
+
};
|
|
46
|
+
/** At most 100 IDs in bytewise order. Resume with the last ID; stop on an empty page. */
|
|
47
|
+
readonly list: (db: {
|
|
48
|
+
readonly raw: {
|
|
49
|
+
readonly sql: unknown;
|
|
50
|
+
};
|
|
51
|
+
readonly runtime: unknown;
|
|
52
|
+
}, after?: string) => AsyncResult<readonly string[], SqlError>;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Declare a paged SECURITY DEFINER function returning distinct, non-null
|
|
56
|
+
* tenant IDs as text, and a typed query calling only that function. Each page
|
|
57
|
+
* holds at most 100 IDs in bytewise order; resume after its last ID until empty.
|
|
58
|
+
*
|
|
59
|
+
* Pass `migration` to Prisma 8's `rawSql` inside a reviewed migration. Its
|
|
60
|
+
* transaction installs an owner-only SELECT policy, grants the owner access
|
|
61
|
+
* to the tenant column, fixes the function's search path, revokes PUBLIC
|
|
62
|
+
* execution and grants execution to `role`. Existing table policies and FORCE
|
|
63
|
+
* ROW LEVEL SECURITY are left intact. No application table privilege is added.
|
|
64
|
+
*
|
|
65
|
+
* Roles and the table must already exist. The migrator must be able to SET ROLE
|
|
66
|
+
* to the owner; provision that membership for an ordinary migration role.
|
|
67
|
+
* The migration refuses a privileged application or function-owner role, caller membership in the owner, schema
|
|
68
|
+
* CREATE for the caller, owner payload SELECT, or any same-name function overload. Its postcheck verifies the
|
|
69
|
+
* function, policy and execution grants before Prisma records it as applied.
|
|
70
|
+
* Apply the operation transactionally; the function is initially private only
|
|
71
|
+
* once the REVOKE runs. Do not grant the application the owner role afterwards.
|
|
72
|
+
*/
|
|
73
|
+
export declare const tenantListing: (options: TenantListingOptions) => Result<TenantListing, TenantListingInvalid>;
|
|
74
|
+
//#endregion
|
|
3
75
|
//#region src/rls.d.ts
|
|
4
76
|
/**
|
|
5
77
|
* Options of {@link tenantPinned}: `raises`, as {@link tryQuery} takes it,
|
|
@@ -63,4 +135,5 @@ type Queryable = {
|
|
|
63
135
|
* ```
|
|
64
136
|
*/
|
|
65
137
|
export declare const tenantPinned: <Tx extends Queryable, R, const S extends string = never>(db: Pinnable<Tx>, tenant: string, work: (tx: Tx) => PromiseLike<R>, options?: TenantPinnedOptions<S>) => AsyncResult<R, SqlError | Raised<S>>;
|
|
66
|
-
//#endregion
|
|
138
|
+
//#endregion
|
|
139
|
+
export type { TenantListing, TenantListingOptions };
|
package/dist/rls.d.mts
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
import { QualifyOptions, Raised, SqlError } from "./result.mjs";
|
|
2
|
-
import { AsyncResult } from "unthrown";
|
|
2
|
+
import { AsyncResult, Result } from "unthrown";
|
|
3
|
+
//#region src/tenant-listing.d.ts
|
|
4
|
+
declare const TenantListingInvalid_base: import("unthrown").TaggedErrorConstructor<"TenantListingInvalid">;
|
|
5
|
+
/** A tenant-listing declaration names an invalid identifier or shares its privileged owner with its caller. */
|
|
6
|
+
export declare class TenantListingInvalid extends TenantListingInvalid_base<{
|
|
7
|
+
readonly field: keyof TenantListingOptions;
|
|
8
|
+
readonly reason: "invalid-identifier" | "shared-role";
|
|
9
|
+
}> {}
|
|
10
|
+
/** Physical database names and existing roles for a narrow tenant-listing capability. */
|
|
11
|
+
type TenantListingOptions = {
|
|
12
|
+
readonly schema: string;
|
|
13
|
+
readonly table: string;
|
|
14
|
+
/** The physical tenant column. Default `tenantId`. */
|
|
15
|
+
readonly tenantColumn?: string;
|
|
16
|
+
/** A new paged function in `schema`; also names its select policy. */
|
|
17
|
+
readonly name: string;
|
|
18
|
+
/** Trusted function owner with no payload SELECT, superuser or BYPASSRLS; needs schema CREATE. */
|
|
19
|
+
readonly owner: string;
|
|
20
|
+
/** Application role granted execution, with no membership in `owner` and no schema CREATE privilege. */
|
|
21
|
+
readonly role: string;
|
|
22
|
+
};
|
|
23
|
+
type Statement = {
|
|
24
|
+
readonly description: string;
|
|
25
|
+
readonly sql: string;
|
|
26
|
+
};
|
|
27
|
+
/** A declaration's migration, ready for Prisma 8's `rawSql`, and its tenant-id-only query. */
|
|
28
|
+
type TenantListing = {
|
|
29
|
+
readonly migration: {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly label: string;
|
|
32
|
+
readonly operationClass: "additive";
|
|
33
|
+
readonly target: {
|
|
34
|
+
readonly id: "postgres";
|
|
35
|
+
readonly details: {
|
|
36
|
+
readonly schema: string;
|
|
37
|
+
readonly objectType: "rlsPolicy";
|
|
38
|
+
readonly table: string;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
readonly precheck: readonly Statement[];
|
|
43
|
+
readonly execute: readonly Statement[];
|
|
44
|
+
readonly postcheck: readonly Statement[];
|
|
45
|
+
};
|
|
46
|
+
/** At most 100 IDs in bytewise order. Resume with the last ID; stop on an empty page. */
|
|
47
|
+
readonly list: (db: {
|
|
48
|
+
readonly raw: {
|
|
49
|
+
readonly sql: unknown;
|
|
50
|
+
};
|
|
51
|
+
readonly runtime: unknown;
|
|
52
|
+
}, after?: string) => AsyncResult<readonly string[], SqlError>;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Declare a paged SECURITY DEFINER function returning distinct, non-null
|
|
56
|
+
* tenant IDs as text, and a typed query calling only that function. Each page
|
|
57
|
+
* holds at most 100 IDs in bytewise order; resume after its last ID until empty.
|
|
58
|
+
*
|
|
59
|
+
* Pass `migration` to Prisma 8's `rawSql` inside a reviewed migration. Its
|
|
60
|
+
* transaction installs an owner-only SELECT policy, grants the owner access
|
|
61
|
+
* to the tenant column, fixes the function's search path, revokes PUBLIC
|
|
62
|
+
* execution and grants execution to `role`. Existing table policies and FORCE
|
|
63
|
+
* ROW LEVEL SECURITY are left intact. No application table privilege is added.
|
|
64
|
+
*
|
|
65
|
+
* Roles and the table must already exist. The migrator must be able to SET ROLE
|
|
66
|
+
* to the owner; provision that membership for an ordinary migration role.
|
|
67
|
+
* The migration refuses a privileged application or function-owner role, caller membership in the owner, schema
|
|
68
|
+
* CREATE for the caller, owner payload SELECT, or any same-name function overload. Its postcheck verifies the
|
|
69
|
+
* function, policy and execution grants before Prisma records it as applied.
|
|
70
|
+
* Apply the operation transactionally; the function is initially private only
|
|
71
|
+
* once the REVOKE runs. Do not grant the application the owner role afterwards.
|
|
72
|
+
*/
|
|
73
|
+
export declare const tenantListing: (options: TenantListingOptions) => Result<TenantListing, TenantListingInvalid>;
|
|
74
|
+
//#endregion
|
|
3
75
|
//#region src/rls.d.ts
|
|
4
76
|
/**
|
|
5
77
|
* Options of {@link tenantPinned}: `raises`, as {@link tryQuery} takes it,
|
|
@@ -63,4 +135,5 @@ type Queryable = {
|
|
|
63
135
|
* ```
|
|
64
136
|
*/
|
|
65
137
|
export declare const tenantPinned: <Tx extends Queryable, R, const S extends string = never>(db: Pinnable<Tx>, tenant: string, work: (tx: Tx) => PromiseLike<R>, options?: TenantPinnedOptions<S>) => AsyncResult<R, SqlError | Raised<S>>;
|
|
66
|
-
//#endregion
|
|
138
|
+
//#endregion
|
|
139
|
+
export type { TenantListing, TenantListingOptions };
|
package/dist/rls.mjs
CHANGED
|
@@ -1,4 +1,175 @@
|
|
|
1
1
|
import { tryQuery } from "./result.mjs";
|
|
2
|
+
import { Err, Ok, TaggedError } from "unthrown";
|
|
3
|
+
//#region src/tenant-listing.ts
|
|
4
|
+
/** A tenant-listing declaration names an invalid identifier or shares its privileged owner with its caller. */
|
|
5
|
+
var TenantListingInvalid = class extends TaggedError("TenantListingInvalid") {};
|
|
6
|
+
const identifier = (value) => `"${value.replaceAll("\"", "\"\"")}"`;
|
|
7
|
+
const literal = (value) => `E'${value.replaceAll("\\", "\\\\").replaceAll("'", "''")}'`;
|
|
8
|
+
const statement = (parts) => Object.assign(parts, { raw: [...parts] });
|
|
9
|
+
/**
|
|
10
|
+
* Declare a paged SECURITY DEFINER function returning distinct, non-null
|
|
11
|
+
* tenant IDs as text, and a typed query calling only that function. Each page
|
|
12
|
+
* holds at most 100 IDs in bytewise order; resume after its last ID until empty.
|
|
13
|
+
*
|
|
14
|
+
* Pass `migration` to Prisma 8's `rawSql` inside a reviewed migration. Its
|
|
15
|
+
* transaction installs an owner-only SELECT policy, grants the owner access
|
|
16
|
+
* to the tenant column, fixes the function's search path, revokes PUBLIC
|
|
17
|
+
* execution and grants execution to `role`. Existing table policies and FORCE
|
|
18
|
+
* ROW LEVEL SECURITY are left intact. No application table privilege is added.
|
|
19
|
+
*
|
|
20
|
+
* Roles and the table must already exist. The migrator must be able to SET ROLE
|
|
21
|
+
* to the owner; provision that membership for an ordinary migration role.
|
|
22
|
+
* The migration refuses a privileged application or function-owner role, caller membership in the owner, schema
|
|
23
|
+
* CREATE for the caller, owner payload SELECT, or any same-name function overload. Its postcheck verifies the
|
|
24
|
+
* function, policy and execution grants before Prisma records it as applied.
|
|
25
|
+
* Apply the operation transactionally; the function is initially private only
|
|
26
|
+
* once the REVOKE runs. Do not grant the application the owner role afterwards.
|
|
27
|
+
*/
|
|
28
|
+
const tenantListing = (options) => {
|
|
29
|
+
const names = {
|
|
30
|
+
schema: options.schema,
|
|
31
|
+
table: options.table,
|
|
32
|
+
tenantColumn: options.tenantColumn ?? "tenantId",
|
|
33
|
+
name: options.name,
|
|
34
|
+
owner: options.owner,
|
|
35
|
+
role: options.role
|
|
36
|
+
};
|
|
37
|
+
for (const [field, value] of Object.entries(names)) if (typeof value !== "string" || value.length === 0 || value.includes("\0") || Buffer.from(value, "utf8").toString("utf8") !== value || Buffer.byteLength(value, "utf8") > 63) return Err(new TenantListingInvalid({
|
|
38
|
+
field,
|
|
39
|
+
reason: "invalid-identifier"
|
|
40
|
+
}));
|
|
41
|
+
if (names.owner === names.role) return Err(new TenantListingInvalid({
|
|
42
|
+
field: "role",
|
|
43
|
+
reason: "shared-role"
|
|
44
|
+
}));
|
|
45
|
+
const schema = identifier(names.schema);
|
|
46
|
+
const table = `${schema}.${identifier(names.table)}`;
|
|
47
|
+
const fn = `${schema}.${identifier(names.name)}`;
|
|
48
|
+
const owner = identifier(names.owner);
|
|
49
|
+
const role = identifier(names.role);
|
|
50
|
+
const column = identifier(names.tenantColumn);
|
|
51
|
+
const functionRef = `pg_catalog.to_regprocedure(${literal(`${fn}(pg_catalog.text)`)})`;
|
|
52
|
+
const tableRef = `pg_catalog.to_regclass(${literal(table)})`;
|
|
53
|
+
const ownerRef = `(SELECT oid FROM pg_catalog.pg_roles WHERE rolname = ${literal(names.owner)})`;
|
|
54
|
+
const roleRef = `(SELECT oid FROM pg_catalog.pg_roles WHERE rolname = ${literal(names.role)})`;
|
|
55
|
+
const body = `SELECT DISTINCT ${column}::pg_catalog.text COLLATE pg_catalog."C" AS "tenantId" FROM ${table} WHERE ${column} IS NOT NULL AND ($1 IS NULL OR ${column}::pg_catalog.text COLLATE pg_catalog."C" > $1 COLLATE pg_catalog."C") ORDER BY "tenantId" LIMIT 100`;
|
|
56
|
+
const policy = `CURRENT_USER = ${literal(names.owner)}::pg_catalog.name`;
|
|
57
|
+
const assumption = "CASE WHEN pg_catalog.current_setting('server_version_num')::pg_catalog.int4 >= 160000 THEN 'SET' ELSE 'MEMBER' END";
|
|
58
|
+
const requirements = `EXISTS (
|
|
59
|
+
SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
|
|
60
|
+
JOIN pg_catalog.pg_roles o ON o.rolname = ${literal(names.owner)}
|
|
61
|
+
JOIN pg_catalog.pg_roles r ON r.rolname = ${literal(names.role)}
|
|
62
|
+
WHERE c.oid = ${tableRef} AND c.relrowsecurity AND a.attname = ${literal(names.tenantColumn)} AND a.attnum > 0 AND NOT a.attisdropped
|
|
63
|
+
AND NOT o.rolsuper AND NOT o.rolbypassrls AND NOT r.rolsuper AND NOT r.rolbypassrls
|
|
64
|
+
AND NOT pg_catalog.pg_has_role(r.oid, o.oid, 'MEMBER')
|
|
65
|
+
AND NOT EXISTS (
|
|
66
|
+
SELECT 1 FROM pg_catalog.pg_attribute payload
|
|
67
|
+
WHERE payload.attrelid = c.oid AND payload.attnum > 0 AND NOT payload.attisdropped
|
|
68
|
+
AND payload.attname <> ${literal(names.tenantColumn)}
|
|
69
|
+
AND pg_catalog.has_column_privilege(o.oid, c.oid, payload.attname, 'SELECT')
|
|
70
|
+
)
|
|
71
|
+
AND pg_catalog.has_schema_privilege(o.oid, ${literal(names.schema)}, 'CREATE')
|
|
72
|
+
AND NOT EXISTS (
|
|
73
|
+
SELECT 1 FROM pg_catalog.pg_roles creator
|
|
74
|
+
WHERE pg_catalog.pg_has_role(r.oid, creator.oid, ${assumption})
|
|
75
|
+
AND pg_catalog.has_schema_privilege(creator.oid, ${literal(names.schema)}, 'CREATE')
|
|
76
|
+
)
|
|
77
|
+
AND NOT EXISTS (
|
|
78
|
+
SELECT 1 FROM pg_catalog.pg_policy blocked WHERE blocked.polrelid = c.oid
|
|
79
|
+
AND NOT blocked.polpermissive AND blocked.polcmd IN ('r', '*')
|
|
80
|
+
AND EXISTS (SELECT 1 FROM pg_catalog.unnest(blocked.polroles) inherited(role)
|
|
81
|
+
WHERE CASE WHEN inherited.role = 0 THEN true ELSE pg_catalog.pg_has_role(o.oid, inherited.role, 'USAGE') END)
|
|
82
|
+
)
|
|
83
|
+
)`;
|
|
84
|
+
const functions = `SELECT p.oid FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = ${literal(names.schema)} AND p.proname = ${literal(names.name)}`;
|
|
85
|
+
const ready = `SELECT ${requirements} AND (SELECT count(*) FROM (${functions}) candidates) = 1 AND EXISTS (
|
|
86
|
+
SELECT 1 FROM pg_catalog.pg_proc p
|
|
87
|
+
WHERE p.oid = ${functionRef}
|
|
88
|
+
AND p.prosecdef AND NOT p.proisstrict AND p.proretset AND p.pronargs = 1 AND p.pronargdefaults = 1 AND pg_catalog.pg_get_expr(p.proargdefaults, 0) IN ('NULL::text', 'NULL::pg_catalog.text') AND p.provolatile = 's'
|
|
89
|
+
AND p.prolang = (SELECT oid FROM pg_catalog.pg_language WHERE lanname = 'sql')
|
|
90
|
+
AND p.proowner = ${ownerRef} AND p.prosrc = ${literal(body)}
|
|
91
|
+
AND p.proallargtypes = ARRAY['pg_catalog.text'::pg_catalog.regtype, 'pg_catalog.text'::pg_catalog.regtype]::pg_catalog.oid[]
|
|
92
|
+
AND p.proargnames = ARRAY['after', 'tenantId']::pg_catalog.text[] AND p.proargmodes = ARRAY['i', 't']::pg_catalog."char"[]
|
|
93
|
+
AND p.proconfig = ARRAY['search_path=pg_catalog, pg_temp', 'row_security=on']::pg_catalog.text[]
|
|
94
|
+
AND pg_catalog.has_function_privilege(${roleRef}, p.oid, 'EXECUTE')
|
|
95
|
+
AND pg_catalog.has_schema_privilege(${roleRef}, ${literal(names.schema)}, 'USAGE')
|
|
96
|
+
AND pg_catalog.has_schema_privilege(${ownerRef}, ${literal(names.schema)}, 'USAGE')
|
|
97
|
+
AND pg_catalog.has_column_privilege(${ownerRef}, ${tableRef}, ${literal(names.tenantColumn)}, 'SELECT')
|
|
98
|
+
AND NOT EXISTS (
|
|
99
|
+
SELECT 1 FROM pg_catalog.aclexplode(COALESCE(p.proacl, pg_catalog.acldefault('f', p.proowner))) a
|
|
100
|
+
WHERE a.privilege_type = 'EXECUTE' AND (a.grantee NOT IN (${ownerRef}, ${roleRef}) OR (a.grantee = ${roleRef} AND a.is_grantable))
|
|
101
|
+
)
|
|
102
|
+
AND EXISTS (
|
|
103
|
+
SELECT 1 FROM pg_catalog.pg_policy pol WHERE pol.polrelid = ${tableRef}
|
|
104
|
+
AND pol.polname = ${literal(names.name)} AND pol.polcmd = 'r' AND pol.polpermissive
|
|
105
|
+
AND pol.polroles = ARRAY[${ownerRef}]::pg_catalog.oid[]
|
|
106
|
+
AND pg_catalog.pg_get_expr(pol.polqual, pol.polrelid) = '(CURRENT_USER = ' || pg_catalog.quote_literal(${literal(names.owner)}) || '::name)'
|
|
107
|
+
)
|
|
108
|
+
) AS "result"`;
|
|
109
|
+
const parts = [`SELECT "tenantId" FROM ${fn}(`, `) ORDER BY "tenantId" COLLATE pg_catalog."C"`];
|
|
110
|
+
return Ok({
|
|
111
|
+
migration: {
|
|
112
|
+
id: `tenantListing.${encodeURIComponent(names.schema).replaceAll(".", "%2E")}.${encodeURIComponent(names.name).replaceAll(".", "%2E")}`,
|
|
113
|
+
label: `List tenant IDs from ${table}`,
|
|
114
|
+
operationClass: "additive",
|
|
115
|
+
target: {
|
|
116
|
+
id: "postgres",
|
|
117
|
+
details: {
|
|
118
|
+
schema: names.schema,
|
|
119
|
+
objectType: "rlsPolicy",
|
|
120
|
+
table: names.table,
|
|
121
|
+
name: names.name
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
precheck: [{
|
|
125
|
+
description: "require a row-secured table, restricted roles, and unused function and policy names",
|
|
126
|
+
sql: `SELECT ${requirements} AND pg_catalog.pg_has_role(CURRENT_USER, ${ownerRef}, ${assumption}) AND NOT EXISTS (${functions}) AND NOT EXISTS (
|
|
127
|
+
SELECT 1 FROM pg_catalog.pg_policy WHERE polrelid = ${tableRef} AND polname = ${literal(names.name)}
|
|
128
|
+
) AS "result"`
|
|
129
|
+
}],
|
|
130
|
+
execute: [
|
|
131
|
+
{
|
|
132
|
+
description: "allow the function owner to read tenant IDs under forced RLS",
|
|
133
|
+
sql: `CREATE POLICY ${identifier(names.name)} ON ${table} FOR SELECT TO ${owner} USING (${policy})`
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
description: "grant schema access to the function owner and caller",
|
|
137
|
+
sql: `GRANT USAGE ON SCHEMA ${schema} TO ${owner}, ${role}`
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
description: "grant the function owner the tenant column",
|
|
141
|
+
sql: `GRANT SELECT (${column}) ON TABLE ${table} TO ${owner}`
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
description: "create the tenant-id-only function with a fixed search path",
|
|
145
|
+
sql: `CREATE FUNCTION ${fn}("after" pg_catalog.text DEFAULT NULL) RETURNS TABLE ("tenantId" pg_catalog.text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET row_security = on AS ${literal(body)}`
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
description: "remove public execution",
|
|
149
|
+
sql: `REVOKE ALL ON FUNCTION ${fn}(pg_catalog.text) FROM PUBLIC`
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
description: "grant execution to the application role",
|
|
153
|
+
sql: `GRANT EXECUTE ON FUNCTION ${fn}(pg_catalog.text) TO ${role}`
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
description: "assign the restricted function owner",
|
|
157
|
+
sql: `ALTER FUNCTION ${fn}(pg_catalog.text) OWNER TO ${owner}`
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
postcheck: [{
|
|
161
|
+
description: "verify the listing function, owner policy, and narrow execution grants",
|
|
162
|
+
sql: ready
|
|
163
|
+
}]
|
|
164
|
+
},
|
|
165
|
+
list: (db, after) => tryQuery(() => {
|
|
166
|
+
const client = db;
|
|
167
|
+
const plan = (after === void 0 ? client.raw.sql(statement([`${parts[0]}NULL${parts[1]}`])) : client.raw.sql(statement(parts), after)).returnsRow({ tenantId: "pg/text@1" }).build();
|
|
168
|
+
return client.runtime().query(plan);
|
|
169
|
+
}).map((rows) => rows.map((row) => row.tenantId))
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
//#endregion
|
|
2
173
|
//#region src/rls.ts
|
|
3
174
|
/**
|
|
4
175
|
* Runs `work` in a transaction whose connection is pinned to `tenant`, so a
|
|
@@ -37,4 +208,4 @@ const tenantPinned = (db, tenant, work, options) => {
|
|
|
37
208
|
}), options);
|
|
38
209
|
};
|
|
39
210
|
//#endregion
|
|
40
|
-
export { tenantPinned };
|
|
211
|
+
export { TenantListingInvalid, tenantListing, tenantPinned };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/prisma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "The Prisma starter for @btravstack/core: DATABASE_URL through Config, the Postgres driver adapter, and a client whose pool is the application scope's",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"database",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"./package.json": "./package.json"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@btravstack/config": "0.
|
|
67
|
-
"@btravstack/core": "0.
|
|
68
|
-
"@btravstack/di": "0.
|
|
66
|
+
"@btravstack/config": "0.34.0",
|
|
67
|
+
"@btravstack/core": "0.34.0",
|
|
68
|
+
"@btravstack/di": "0.34.0",
|
|
69
69
|
"@btravstack/tsconfig": "0.4.0",
|
|
70
70
|
"@prisma/orm-postgres": "8.0.0-rc.11",
|
|
71
71
|
"@types/node": "26.6.4",
|
|
@@ -73,13 +73,14 @@
|
|
|
73
73
|
"@vitest/coverage-v8": "5.0.3",
|
|
74
74
|
"tsdown": "0.23.0",
|
|
75
75
|
"typescript": "7.0.2",
|
|
76
|
+
"typescript-consumer": "npm:typescript@5.9.3",
|
|
76
77
|
"unthrown": "5.12.1",
|
|
77
78
|
"vitest": "5.0.3"
|
|
78
79
|
},
|
|
79
80
|
"peerDependencies": {
|
|
80
|
-
"@btravstack/config": "^0.
|
|
81
|
-
"@btravstack/core": "^0.
|
|
82
|
-
"@btravstack/di": "^0.
|
|
81
|
+
"@btravstack/config": "^0.34.0",
|
|
82
|
+
"@btravstack/core": "^0.34.0",
|
|
83
|
+
"@btravstack/di": "^0.34.0",
|
|
83
84
|
"@prisma/orm-postgres": "^8.0.0-rc.11",
|
|
84
85
|
"unthrown": "^5.0.0"
|
|
85
86
|
},
|
|
@@ -90,6 +91,6 @@
|
|
|
90
91
|
"build": "tsdown src/index.ts src/rls.ts src/result.ts --format cjs,esm --dts --clean",
|
|
91
92
|
"dev": "tsdown src/index.ts src/rls.ts src/result.ts --format cjs,esm --dts --watch",
|
|
92
93
|
"test": "vitest run --coverage",
|
|
93
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json"
|
|
94
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json && node ./node_modules/typescript-consumer/bin/tsc --noEmit -p tsconfig.test-d.json"
|
|
94
95
|
}
|
|
95
96
|
}
|