@stratum-hq/db-adapters 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -3
- package/dist/adapters/drizzle.d.ts +3 -2
- package/dist/adapters/drizzle.d.ts.map +1 -1
- package/dist/adapters/drizzle.js +3 -2
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/sequelize.d.ts +3 -2
- package/dist/adapters/sequelize.d.ts.map +1 -1
- package/dist/adapters/sequelize.js +3 -2
- package/dist/adapters/sequelize.js.map +1 -1
- package/dist/database/manager.d.ts +1 -1
- package/dist/database/manager.js +2 -2
- package/dist/database/manager.js.map +1 -1
- package/dist/database/pool-manager.d.ts +1 -1
- package/dist/database/pool-manager.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/migration-helpers.d.ts +2 -1
- package/dist/migration-helpers.d.ts.map +1 -1
- package/dist/migration-helpers.js +8 -1
- package/dist/migration-helpers.js.map +1 -1
- package/dist/pglite/index.d.ts +60 -0
- package/dist/pglite/index.d.ts.map +1 -0
- package/dist/pglite/index.js +196 -0
- package/dist/pglite/index.js.map +1 -0
- package/dist/rls/manager.d.ts +27 -1
- package/dist/rls/manager.d.ts.map +1 -1
- package/dist/rls/manager.js +43 -6
- package/dist/rls/manager.js.map +1 -1
- package/dist/rls/policy-check.d.ts +49 -1
- package/dist/rls/policy-check.d.ts.map +1 -1
- package/dist/rls/policy-check.js +136 -11
- package/dist/rls/policy-check.js.map +1 -1
- package/dist/rls/session.d.ts +34 -2
- package/dist/rls/session.d.ts.map +1 -1
- package/dist/rls/session.js +39 -5
- package/dist/rls/session.js.map +1 -1
- package/dist/schema/manager.js +1 -1
- package/dist/schema/manager.js.map +1 -1
- package/package.json +24 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"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"}
|
package/dist/rls/manager.d.ts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
-
|
|
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
|
+
* The control role of migration 032 (default `stratum_control`). An
|
|
17
|
+
* existing stratum_control_plane policy is accepted only when it applies to
|
|
18
|
+
* exactly this role.
|
|
19
|
+
*/
|
|
20
|
+
controlRole?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Throws when the database has no stratum_subtree_tenant_ids() function. The
|
|
24
|
+
* subtree read policy calls it, and without it CREATE POLICY fails with a
|
|
25
|
+
* less clear error.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertSubtreeFunction(client: pg.PoolClient): Promise<void>;
|
|
28
|
+
export declare function createPolicy(client: pg.PoolClient, tableName: string, options?: CreatePolicyOptions): Promise<void>;
|
|
3
29
|
export declare function dropPolicy(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
4
30
|
export declare function enableRLS(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
5
31
|
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;
|
|
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;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;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,CA+Cf;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"}
|
package/dist/rls/manager.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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;
|
|
@@ -7,6 +9,25 @@ exports.disableRLS = disableRLS;
|
|
|
7
9
|
exports.isRLSEnabled = isRLSEnabled;
|
|
8
10
|
const policy_check_js_1 = require("./policy-check.js");
|
|
9
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
|
+
}
|
|
10
31
|
// Validate table name to prevent SQL injection (only allows alphanumeric + underscores)
|
|
11
32
|
function validateTableName(tableName) {
|
|
12
33
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
@@ -14,31 +35,47 @@ function validateTableName(tableName) {
|
|
|
14
35
|
}
|
|
15
36
|
return tableName;
|
|
16
37
|
}
|
|
17
|
-
async function createPolicy(client, tableName) {
|
|
38
|
+
async function createPolicy(client, tableName, options = {}) {
|
|
18
39
|
const safe = validateTableName(tableName);
|
|
40
|
+
if (options.subtreeRead) {
|
|
41
|
+
await assertSubtreeFunction(client);
|
|
42
|
+
}
|
|
19
43
|
// Read every policy on the table that the name resolves to, in whichever
|
|
20
|
-
// schema that is.
|
|
44
|
+
// schema that is. The caller's search path stays as it is (this may run in
|
|
45
|
+
// the caller's transaction), so every comparison here has an exact
|
|
46
|
+
// pg_catalog operator: the regclass lookups are cast to oid. PostgreSQL ORs permissive policies together, so each one
|
|
21
47
|
// must filter by tenant, and a policy's name proves nothing.
|
|
22
|
-
|
|
48
|
+
// Each row also carries the schema of the tenants table, the only schema
|
|
49
|
+
// that may qualify the subtree function (migration 031 creates it there).
|
|
50
|
+
const existing = await client.query(`SELECT p.policyname, p.permissive, p.cmd, p.qual, p.with_check, p.roles::text[] AS roles,
|
|
51
|
+
(SELECT tn.nspname FROM pg_class tc JOIN pg_namespace tn ON tn.oid = tc.relnamespace
|
|
52
|
+
WHERE tc.oid = pg_catalog.to_regclass('tenants')::pg_catalog.oid) AS tenants_schema
|
|
23
53
|
FROM pg_policies p
|
|
24
54
|
JOIN pg_class c ON c.relname = p.tablename
|
|
25
55
|
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = p.schemaname
|
|
26
|
-
WHERE c.oid = to_regclass($1)`, [safe]);
|
|
27
|
-
const issues = (0, policy_check_js_1.tablePolicyIssues)(existing.rows);
|
|
56
|
+
WHERE c.oid = pg_catalog.to_regclass($1::pg_catalog.text)::pg_catalog.oid`, [safe]);
|
|
57
|
+
const issues = (0, policy_check_js_1.tablePolicyIssues)(existing.rows, existing.rows[0]?.tenants_schema ?? undefined, options.controlRole);
|
|
28
58
|
if (issues.length > 0) {
|
|
29
59
|
throw new Error(`[stratum] Table ${safe} has row-level security policies that do not isolate it by tenant: ` +
|
|
30
60
|
`${issues.join("; ")} (expected ${TENANT_FILTER}). ` +
|
|
31
61
|
`Drop or correct those policies, then call createPolicy again.`);
|
|
32
62
|
}
|
|
63
|
+
for (const warning of (0, policy_check_js_1.tablePolicyWarnings)(existing.rows)) {
|
|
64
|
+
process.emitWarning(`[stratum] Table ${safe}: ${warning}`, { code: "STRATUM_GUC_BYPASS_POLICY" });
|
|
65
|
+
}
|
|
33
66
|
if (!existing.rows.some((p) => p.policyname === "tenant_isolation")) {
|
|
34
67
|
// Cannot use parameterized queries inside DO blocks or for DDL identifiers.
|
|
35
68
|
// Table name is validated via allowlist regex above.
|
|
36
69
|
await client.query(`CREATE POLICY tenant_isolation ON ${safe} USING (${TENANT_FILTER})`);
|
|
37
70
|
}
|
|
71
|
+
if (options.subtreeRead && !existing.rows.some((p) => p.policyname === "tenant_subtree_read")) {
|
|
72
|
+
await client.query(`CREATE POLICY tenant_subtree_read ON ${safe} FOR SELECT USING (${exports.SUBTREE_READ_FILTER})`);
|
|
73
|
+
}
|
|
38
74
|
}
|
|
39
75
|
async function dropPolicy(client, tableName) {
|
|
40
76
|
const safe = validateTableName(tableName);
|
|
41
77
|
await client.query(`DROP POLICY IF EXISTS tenant_isolation ON ${safe}`);
|
|
78
|
+
await client.query(`DROP POLICY IF EXISTS tenant_subtree_read ON ${safe}`);
|
|
42
79
|
}
|
|
43
80
|
async function enableRLS(client, tableName) {
|
|
44
81
|
const safe = validateTableName(tableName);
|
|
@@ -53,7 +90,7 @@ async function disableRLS(client, tableName) {
|
|
|
53
90
|
async function isRLSEnabled(client, tableName) {
|
|
54
91
|
const safe = validateTableName(tableName);
|
|
55
92
|
// The table the name resolves to, in whichever schema that is.
|
|
56
|
-
const res = await client.query(`SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass($1)`, [safe]);
|
|
93
|
+
const res = await client.query(`SELECT relrowsecurity FROM pg_catalog.pg_class WHERE oid = pg_catalog.to_regclass($1::pg_catalog.text)::pg_catalog.oid`, [safe]);
|
|
57
94
|
if (res.rows.length === 0) {
|
|
58
95
|
return false;
|
|
59
96
|
}
|
package/dist/rls/manager.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,21 @@
|
|
|
22
22
|
* either order, with casts, inside an AND with other conditions, and ORed with
|
|
23
23
|
* Stratum's administrative bypass. Anything else is reported as not filtering
|
|
24
24
|
* by tenant, so an unusual but correct policy is refused, never accepted.
|
|
25
|
+
*
|
|
26
|
+
* It recognises the legacy bypass of migration 032, `(SELECT
|
|
27
|
+
* stratum_legacy_bypass()) OR <tenant match>`, like the older direct check of
|
|
28
|
+
* app.bypass_rls. A policy named stratum_control_plane counts as isolating
|
|
29
|
+
* only when it applies to exactly the control role (default stratum_control):
|
|
30
|
+
* that role is the library's own, and a session cannot join it by setting
|
|
31
|
+
* anything. tablePolicyWarnings flags every policy that still checks
|
|
32
|
+
* app.bypass_rls directly; 2.0 will refuse those.
|
|
33
|
+
*
|
|
34
|
+
* It also recognises the subtree read of migration 031: the
|
|
35
|
+
* stratum_subtree_tenant_ids() match ANDed with the app.tenant_scope =
|
|
36
|
+
* 'subtree' check. That form counts only in the USING clause of a SELECT
|
|
37
|
+
* policy. In any other policy, the same clause would let UPDATE or DELETE
|
|
38
|
+
* reach a descendant's rows, and in WITH CHECK it would let a write name a
|
|
39
|
+
* descendant.
|
|
25
40
|
*/
|
|
26
41
|
/** One row of pg_policies. */
|
|
27
42
|
export interface PolicyRow {
|
|
@@ -35,9 +50,42 @@ export interface PolicyRow {
|
|
|
35
50
|
/** The roles the policy applies to; ["public"] for PUBLIC. */
|
|
36
51
|
roles: string[];
|
|
37
52
|
}
|
|
53
|
+
/** The default control role of migration 032. */
|
|
54
|
+
export declare const DEFAULT_CONTROL_ROLE = "stratum_control";
|
|
55
|
+
/**
|
|
56
|
+
* Why one permissive policy lets rows of other tenants through, or null when
|
|
57
|
+
* it filters by tenant for every command it covers. Shared with the policy
|
|
58
|
+
* checks of `@stratum-hq/cli`, so both apply the same expression rules.
|
|
59
|
+
*
|
|
60
|
+
* @param functionSchema - As for {@link tablePolicyIssues}.
|
|
61
|
+
*/
|
|
62
|
+
export declare function permissivePolicyIssue(p: Pick<PolicyRow, "policyname" | "cmd" | "qual" | "with_check">, functionSchema?: string): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* True for the stratum_control_plane policy of migration 032 when it is
|
|
65
|
+
* permissive and applies to exactly `controlRole`. Only that role's members
|
|
66
|
+
* can use it, whatever it admits.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isControlPlanePolicy(p: Pick<PolicyRow, "policyname" | "permissive"> & {
|
|
69
|
+
roles?: string[];
|
|
70
|
+
}, controlRole?: string): boolean;
|
|
38
71
|
/**
|
|
39
72
|
* Every reason the table's policies do not isolate it by tenant. Empty when
|
|
40
73
|
* they do, or when the table has no policies.
|
|
74
|
+
*
|
|
75
|
+
* @param functionSchema - The schema of Stratum's tenants table, the only
|
|
76
|
+
* schema that may qualify stratum_subtree_tenant_ids() and
|
|
77
|
+
* stratum_legacy_bypass(). When it is undefined, only the unqualified
|
|
78
|
+
* functions count.
|
|
79
|
+
* @param controlRole - The control role of migration 032. A policy named
|
|
80
|
+
* stratum_control_plane that applies to exactly this role is accepted.
|
|
81
|
+
*/
|
|
82
|
+
export declare function tablePolicyIssues(policies: PolicyRow[], functionSchema?: string, controlRole?: string): string[];
|
|
83
|
+
/**
|
|
84
|
+
* The policies that admit the app.bypass_rls setting directly. A session that
|
|
85
|
+
* can run SQL can set it, so such a policy holds only against a client that
|
|
86
|
+
* never sets it. The legacy form of migration 032 is not flagged: it admits
|
|
87
|
+
* the setting only while the stratum_security switch is on, which only the
|
|
88
|
+
* control role can change.
|
|
41
89
|
*/
|
|
42
|
-
export declare function
|
|
90
|
+
export declare function tablePolicyWarnings(policies: PolicyRow[]): string[];
|
|
43
91
|
//# sourceMappingURL=policy-check.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy-check.d.ts","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"policy-check.d.ts","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;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;AAcD,iDAAiD;AACjD,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AA4LtD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,YAAY,GAAG,KAAK,GAAG,MAAM,GAAG,YAAY,CAAC,EAChE,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,GAAG,IAAI,CAEf;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,YAAY,GAAG,YAAY,CAAC,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EACtE,WAAW,GAAE,MAA6B,GACzC,OAAO,CAOT;AAiBD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,SAAS,EAAE,EACrB,cAAc,CAAC,EAAE,MAAM,EACvB,WAAW,GAAE,MAA6B,GACzC,MAAM,EAAE,CAsBV;AAID;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAanE"}
|
package/dist/rls/policy-check.js
CHANGED
|
@@ -23,13 +23,42 @@
|
|
|
23
23
|
* either order, with casts, inside an AND with other conditions, and ORed with
|
|
24
24
|
* Stratum's administrative bypass. Anything else is reported as not filtering
|
|
25
25
|
* by tenant, so an unusual but correct policy is refused, never accepted.
|
|
26
|
+
*
|
|
27
|
+
* It recognises the legacy bypass of migration 032, `(SELECT
|
|
28
|
+
* stratum_legacy_bypass()) OR <tenant match>`, like the older direct check of
|
|
29
|
+
* app.bypass_rls. A policy named stratum_control_plane counts as isolating
|
|
30
|
+
* only when it applies to exactly the control role (default stratum_control):
|
|
31
|
+
* that role is the library's own, and a session cannot join it by setting
|
|
32
|
+
* anything. tablePolicyWarnings flags every policy that still checks
|
|
33
|
+
* app.bypass_rls directly; 2.0 will refuse those.
|
|
34
|
+
*
|
|
35
|
+
* It also recognises the subtree read of migration 031: the
|
|
36
|
+
* stratum_subtree_tenant_ids() match ANDed with the app.tenant_scope =
|
|
37
|
+
* 'subtree' check. That form counts only in the USING clause of a SELECT
|
|
38
|
+
* policy. In any other policy, the same clause would let UPDATE or DELETE
|
|
39
|
+
* reach a descendant's rows, and in WITH CHECK it would let a write name a
|
|
40
|
+
* descendant.
|
|
26
41
|
*/
|
|
27
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.DEFAULT_CONTROL_ROLE = void 0;
|
|
44
|
+
exports.permissivePolicyIssue = permissivePolicyIssue;
|
|
45
|
+
exports.isControlPlanePolicy = isControlPlanePolicy;
|
|
28
46
|
exports.tablePolicyIssues = tablePolicyIssues;
|
|
47
|
+
exports.tablePolicyWarnings = tablePolicyWarnings;
|
|
29
48
|
/** The session setting that holds the current tenant (see rls/session.ts). */
|
|
30
49
|
const TENANT_SETTING = "app.current_tenant_id";
|
|
31
50
|
/** The setting Stratum's own policies admit for administrative access. */
|
|
32
51
|
const BYPASS_SETTING = "app.bypass_rls";
|
|
52
|
+
/** The setting that opts a session in to the subtree read scope (migration 031). */
|
|
53
|
+
const SCOPE_SETTING = "app.tenant_scope";
|
|
54
|
+
/** The function that returns the current tenant and its descendants (migration 031). */
|
|
55
|
+
const SUBTREE_FUNCTION = "stratum_subtree_tenant_ids";
|
|
56
|
+
/** The function of migration 032 that gates the legacy bypass behind stratum_security. */
|
|
57
|
+
const LEGACY_FUNCTION = "stratum_legacy_bypass";
|
|
58
|
+
/** The policy of migration 032 that admits the control role. */
|
|
59
|
+
const CONTROL_POLICY = "stratum_control_plane";
|
|
60
|
+
/** The default control role of migration 032. */
|
|
61
|
+
exports.DEFAULT_CONTROL_ROLE = "stratum_control";
|
|
33
62
|
/** Drops whitespace and casts, lower-cases, and keeps string literals intact. */
|
|
34
63
|
function normalize(expr) {
|
|
35
64
|
let out = "";
|
|
@@ -118,18 +147,58 @@ const SETTING_READ = `current_setting'${TENANT_SETTING.replace(".", "\\.")}'(?:,
|
|
|
118
147
|
const CURRENT_TENANT = `(?:nullif${SETTING_READ},''|${SETTING_READ})`;
|
|
119
148
|
const TENANT_MATCH = new RegExp(`^(?:tenant_id=${CURRENT_TENANT}|${CURRENT_TENANT}=tenant_id)$`);
|
|
120
149
|
const BYPASS_MATCH = new RegExp(`^current_setting'${BYPASS_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?='on'$`);
|
|
121
|
-
|
|
122
|
-
|
|
150
|
+
const SCOPE_MATCH = new RegExp(`^current_setting'${SCOPE_SETTING.replace(".", "\\.")}'(?:,(?:true|false))?='subtree'$`);
|
|
151
|
+
/**
|
|
152
|
+
* tenant_id = ANY ((SELECT stratum_subtree_tenant_ids())::uuid[]) after
|
|
153
|
+
* normalize and flatten. PostgreSQL qualifies the function with its schema
|
|
154
|
+
* when that schema is not on the search path of the reading session. Only the
|
|
155
|
+
* schema that holds Stratum's tenants table, where migration 031 creates the
|
|
156
|
+
* function, may qualify it: a function of the same name in another schema
|
|
157
|
+
* could return any ids.
|
|
158
|
+
*/
|
|
159
|
+
function subtreeMatch(functionSchema) {
|
|
160
|
+
const qualifier = functionSchema === undefined ? "" : `(?:${escapeRegExp(quoteIdent(functionSchema).toLowerCase())}\\.)?`;
|
|
161
|
+
return new RegExp(`^tenant_id=anyselect${qualifier}${SUBTREE_FUNCTION}(?:as${SUBTREE_FUNCTION})?$`);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* (SELECT stratum_legacy_bypass()) after normalize and flatten, with the same
|
|
165
|
+
* rule for a schema qualifier as the subtree function.
|
|
166
|
+
*/
|
|
167
|
+
function legacyMatch(functionSchema) {
|
|
168
|
+
const qualifier = functionSchema === undefined ? "" : `(?:${escapeRegExp(quoteIdent(functionSchema).toLowerCase())}\\.)?`;
|
|
169
|
+
return new RegExp(`^select${qualifier}${LEGACY_FUNCTION}(?:as${LEGACY_FUNCTION})?$`);
|
|
170
|
+
}
|
|
171
|
+
function escapeRegExp(text) {
|
|
172
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
173
|
+
}
|
|
174
|
+
/** The identifier as PostgreSQL prints it: quoted unless it is a plain lowercase name. */
|
|
175
|
+
function quoteIdent(name) {
|
|
176
|
+
return /^[a-z_][a-z0-9_$]*$/.test(name) ? name : `"${name.replace(/"/g, '""')}"`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* True when the normalized expression only admits rows of the current tenant.
|
|
180
|
+
* `subtree` matches the subtree function call that may appear in the
|
|
181
|
+
* expression, or is null where the subtree read is not allowed.
|
|
182
|
+
*/
|
|
183
|
+
function filtersByTenant(expr, subtree, legacy) {
|
|
123
184
|
const e = stripOuterParens(expr);
|
|
124
185
|
const disjuncts = splitTopLevel(e, "or");
|
|
125
186
|
if (disjuncts.length > 1) {
|
|
126
|
-
// Every branch must be safe; Stratum's administrative bypass
|
|
127
|
-
|
|
128
|
-
|
|
187
|
+
// Every branch must be safe; Stratum's administrative bypass, direct or
|
|
188
|
+
// through the legacy function of migration 032, is one of them.
|
|
189
|
+
const isBypass = (d) => BYPASS_MATCH.test(flatten(d)) || legacy.test(flatten(d));
|
|
190
|
+
return (disjuncts.every((d) => isBypass(d) || filtersByTenant(d, subtree, legacy)) &&
|
|
191
|
+
disjuncts.some((d) => !isBypass(d)));
|
|
129
192
|
}
|
|
130
193
|
const conjuncts = splitTopLevel(e, "and");
|
|
131
194
|
if (conjuncts.length > 1) {
|
|
132
|
-
|
|
195
|
+
if (conjuncts.some((c) => filtersByTenant(c, subtree, legacy)))
|
|
196
|
+
return true;
|
|
197
|
+
// The subtree read of migration 031: the descendants of the current
|
|
198
|
+
// tenant, only when the session opted in with app.tenant_scope.
|
|
199
|
+
return (subtree !== null &&
|
|
200
|
+
conjuncts.some((c) => SCOPE_MATCH.test(flatten(c))) &&
|
|
201
|
+
conjuncts.some((c) => subtree.test(flatten(c))));
|
|
133
202
|
}
|
|
134
203
|
return TENANT_MATCH.test(flatten(e));
|
|
135
204
|
}
|
|
@@ -138,22 +207,45 @@ function oneLine(expr) {
|
|
|
138
207
|
return expr === null ? "none" : expr.replace(/\s+/g, " ");
|
|
139
208
|
}
|
|
140
209
|
/** Why one permissive policy lets rows of other tenants through, or null. */
|
|
141
|
-
function permissiveIssue(p) {
|
|
210
|
+
function permissiveIssue(p, subtree, legacy) {
|
|
142
211
|
const name = `policy "${oneLine(p.policyname)}" (${p.cmd})`;
|
|
143
212
|
// INSERT policies have only WITH CHECK; SELECT and DELETE only USING. For
|
|
144
213
|
// ALL and UPDATE a missing WITH CHECK means PostgreSQL reuses USING.
|
|
145
214
|
if (p.cmd !== "INSERT") {
|
|
146
|
-
|
|
215
|
+
// Only a SELECT policy may widen to the subtree: in any other policy the
|
|
216
|
+
// same USING clause would let UPDATE or DELETE reach a descendant's rows.
|
|
217
|
+
if (p.qual === null || !filtersByTenant(normalize(p.qual), p.cmd === "SELECT" ? subtree : null, legacy)) {
|
|
147
218
|
return `${name} USING (${oneLine(p.qual)}) does not filter by tenant`;
|
|
148
219
|
}
|
|
149
220
|
}
|
|
150
221
|
if (p.cmd === "INSERT" || p.with_check !== null) {
|
|
151
|
-
if (p.with_check === null || !filtersByTenant(normalize(p.with_check))) {
|
|
222
|
+
if (p.with_check === null || !filtersByTenant(normalize(p.with_check), null, legacy)) {
|
|
152
223
|
return `${name} WITH CHECK (${oneLine(p.with_check)}) does not filter by tenant`;
|
|
153
224
|
}
|
|
154
225
|
}
|
|
155
226
|
return null;
|
|
156
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Why one permissive policy lets rows of other tenants through, or null when
|
|
230
|
+
* it filters by tenant for every command it covers. Shared with the policy
|
|
231
|
+
* checks of `@stratum-hq/cli`, so both apply the same expression rules.
|
|
232
|
+
*
|
|
233
|
+
* @param functionSchema - As for {@link tablePolicyIssues}.
|
|
234
|
+
*/
|
|
235
|
+
function permissivePolicyIssue(p, functionSchema) {
|
|
236
|
+
return permissiveIssue(p, subtreeMatch(functionSchema), legacyMatch(functionSchema));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* True for the stratum_control_plane policy of migration 032 when it is
|
|
240
|
+
* permissive and applies to exactly `controlRole`. Only that role's members
|
|
241
|
+
* can use it, whatever it admits.
|
|
242
|
+
*/
|
|
243
|
+
function isControlPlanePolicy(p, controlRole = exports.DEFAULT_CONTROL_ROLE) {
|
|
244
|
+
return (p.policyname === CONTROL_POLICY &&
|
|
245
|
+
p.permissive === "PERMISSIVE" &&
|
|
246
|
+
p.roles?.length === 1 &&
|
|
247
|
+
p.roles[0] === controlRole);
|
|
248
|
+
}
|
|
157
249
|
/** Why the tenant_isolation policy is not the shape createPolicy generates, or null. */
|
|
158
250
|
function tenantIsolationShapeIssue(p) {
|
|
159
251
|
const name = `policy "${oneLine(p.policyname)}"`;
|
|
@@ -171,10 +263,23 @@ function tenantIsolationShapeIssue(p) {
|
|
|
171
263
|
/**
|
|
172
264
|
* Every reason the table's policies do not isolate it by tenant. Empty when
|
|
173
265
|
* they do, or when the table has no policies.
|
|
266
|
+
*
|
|
267
|
+
* @param functionSchema - The schema of Stratum's tenants table, the only
|
|
268
|
+
* schema that may qualify stratum_subtree_tenant_ids() and
|
|
269
|
+
* stratum_legacy_bypass(). When it is undefined, only the unqualified
|
|
270
|
+
* functions count.
|
|
271
|
+
* @param controlRole - The control role of migration 032. A policy named
|
|
272
|
+
* stratum_control_plane that applies to exactly this role is accepted.
|
|
174
273
|
*/
|
|
175
|
-
function tablePolicyIssues(policies) {
|
|
274
|
+
function tablePolicyIssues(policies, functionSchema, controlRole = exports.DEFAULT_CONTROL_ROLE) {
|
|
275
|
+
const subtree = subtreeMatch(functionSchema);
|
|
276
|
+
const legacy = legacyMatch(functionSchema);
|
|
176
277
|
const issues = [];
|
|
177
278
|
for (const p of policies) {
|
|
279
|
+
if (isControlPlanePolicy(p, controlRole)) {
|
|
280
|
+
// Only the library's own role can use it, whatever it admits.
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
178
283
|
if (p.policyname === "tenant_isolation") {
|
|
179
284
|
const shape = tenantIsolationShapeIssue(p);
|
|
180
285
|
if (shape !== null) {
|
|
@@ -183,11 +288,31 @@ function tablePolicyIssues(policies) {
|
|
|
183
288
|
}
|
|
184
289
|
}
|
|
185
290
|
if (p.permissive === "PERMISSIVE") {
|
|
186
|
-
const issue = permissiveIssue(p);
|
|
291
|
+
const issue = permissiveIssue(p, subtree, legacy);
|
|
187
292
|
if (issue !== null)
|
|
188
293
|
issues.push(issue);
|
|
189
294
|
}
|
|
190
295
|
}
|
|
191
296
|
return issues;
|
|
192
297
|
}
|
|
298
|
+
const DIRECT_BYPASS = `current_setting'${BYPASS_SETTING.replace(".", "\\.")}'`;
|
|
299
|
+
/**
|
|
300
|
+
* The policies that admit the app.bypass_rls setting directly. A session that
|
|
301
|
+
* can run SQL can set it, so such a policy holds only against a client that
|
|
302
|
+
* never sets it. The legacy form of migration 032 is not flagged: it admits
|
|
303
|
+
* the setting only while the stratum_security switch is on, which only the
|
|
304
|
+
* control role can change.
|
|
305
|
+
*/
|
|
306
|
+
function tablePolicyWarnings(policies) {
|
|
307
|
+
const direct = new RegExp(DIRECT_BYPASS);
|
|
308
|
+
const warnings = [];
|
|
309
|
+
for (const p of policies) {
|
|
310
|
+
const exprs = [p.qual, p.with_check].filter((e) => e !== null);
|
|
311
|
+
if (exprs.some((e) => direct.test(flatten(normalize(e))))) {
|
|
312
|
+
warnings.push(`policy "${oneLine(p.policyname)}" admits app.bypass_rls directly. ` +
|
|
313
|
+
`Use the control role of migration 032, which PostgreSQL role membership decides, instead.`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return warnings;
|
|
317
|
+
}
|
|
193
318
|
//# sourceMappingURL=policy-check.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy-check.js","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"policy-check.js","sourceRoot":"","sources":["../../src/rls/policy-check.ts"],"names":[],"mappings":"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"}
|
package/dist/rls/session.d.ts
CHANGED
|
@@ -1,8 +1,40 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
-
|
|
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>;
|
|
5
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>;
|
|
32
|
+
/**
|
|
33
|
+
* @deprecated Since 1.8 the library reaches across tenants through the control
|
|
34
|
+
* role of migration 032 (Stratum's `adminPool`), not through this setting.
|
|
35
|
+
* Removed in 2.0. Once the stratum_security legacy switch is off, the setting
|
|
36
|
+
* opens nothing, so this helper sees only what the pool's role sees.
|
|
37
|
+
*
|
|
6
38
|
* Runs `fn` inside a transaction with the RLS bypass flag set, so control-plane
|
|
7
39
|
* / system operations that legitimately span tenant boundaries (provisioning,
|
|
8
40
|
* cascade ops, ancestry reads) can see and write all rows.
|
|
@@ -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,
|
|
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;AAID;;;;;;;;;;;;;;;;;GAiBG;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,CAsBZ;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,EAAE,CAAC,UAAU,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMxB"}
|
package/dist/rls/session.js
CHANGED
|
@@ -5,17 +5,40 @@ exports.resetTenantContext = resetTenantContext;
|
|
|
5
5
|
exports.withTenantContext = withTenantContext;
|
|
6
6
|
exports.withRlsBypass = withRlsBypass;
|
|
7
7
|
exports.getCurrentTenantId = getCurrentTenantId;
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -28,7 +51,13 @@ async function withTenantContext(pool, tenantId, fn) {
|
|
|
28
51
|
client.release();
|
|
29
52
|
}
|
|
30
53
|
}
|
|
54
|
+
let warnedRlsBypass = false;
|
|
31
55
|
/**
|
|
56
|
+
* @deprecated Since 1.8 the library reaches across tenants through the control
|
|
57
|
+
* role of migration 032 (Stratum's `adminPool`), not through this setting.
|
|
58
|
+
* Removed in 2.0. Once the stratum_security legacy switch is off, the setting
|
|
59
|
+
* opens nothing, so this helper sees only what the pool's role sees.
|
|
60
|
+
*
|
|
32
61
|
* Runs `fn` inside a transaction with the RLS bypass flag set, so control-plane
|
|
33
62
|
* / system operations that legitimately span tenant boundaries (provisioning,
|
|
34
63
|
* cascade ops, ancestry reads) can see and write all rows.
|
|
@@ -42,6 +71,11 @@ async function withTenantContext(pool, tenantId, fn) {
|
|
|
42
71
|
* access. Tenant-scoped work should use `withTenantContext` instead.
|
|
43
72
|
*/
|
|
44
73
|
async function withRlsBypass(pool, fn) {
|
|
74
|
+
if (!warnedRlsBypass) {
|
|
75
|
+
warnedRlsBypass = true;
|
|
76
|
+
process.emitWarning("withRlsBypass is deprecated and will be removed in 2.0. Run cross-tenant work on a pool whose " +
|
|
77
|
+
"login is a member of the Stratum control role (migration 032) instead.", { type: "DeprecationWarning", code: "STRATUM_WITH_RLS_BYPASS" });
|
|
78
|
+
}
|
|
45
79
|
const client = await pool.connect();
|
|
46
80
|
try {
|
|
47
81
|
await client.query("BEGIN");
|
package/dist/rls/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/rls/session.ts"],"names":[],"mappings":"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"}
|