@kysera/rls 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1341 -0
- package/dist/index.d.ts +705 -0
- package/dist/index.js +1471 -0
- package/dist/index.js.map +1 -0
- package/dist/native/index.d.ts +91 -0
- package/dist/native/index.js +253 -0
- package/dist/native/index.js.map +1 -0
- package/dist/types-Dtg6Lt1k.d.ts +633 -0
- package/package.json +93 -0
- package/src/context/index.ts +9 -0
- package/src/context/manager.ts +203 -0
- package/src/context/storage.ts +8 -0
- package/src/context/types.ts +5 -0
- package/src/errors.ts +280 -0
- package/src/index.ts +95 -0
- package/src/native/README.md +315 -0
- package/src/native/index.ts +11 -0
- package/src/native/migration.ts +92 -0
- package/src/native/postgres.ts +263 -0
- package/src/plugin.ts +464 -0
- package/src/policy/builder.ts +215 -0
- package/src/policy/index.ts +10 -0
- package/src/policy/registry.ts +403 -0
- package/src/policy/schema.ts +257 -0
- package/src/policy/types.ts +742 -0
- package/src/transformer/index.ts +2 -0
- package/src/transformer/mutation.ts +372 -0
- package/src/transformer/select.ts +150 -0
- package/src/utils/helpers.ts +139 -0
- package/src/utils/index.ts +12 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1471 @@
|
|
|
1
|
+
import { silentLogger } from '@kysera/core';
|
|
2
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var RLSErrorCodes = {
|
|
6
|
+
/** RLS context is missing or not set */
|
|
7
|
+
RLS_CONTEXT_MISSING: "RLS_CONTEXT_MISSING",
|
|
8
|
+
/** RLS policy violation occurred */
|
|
9
|
+
RLS_POLICY_VIOLATION: "RLS_POLICY_VIOLATION",
|
|
10
|
+
/** RLS policy definition is invalid */
|
|
11
|
+
RLS_POLICY_INVALID: "RLS_POLICY_INVALID",
|
|
12
|
+
/** RLS schema definition is invalid */
|
|
13
|
+
RLS_SCHEMA_INVALID: "RLS_SCHEMA_INVALID",
|
|
14
|
+
/** RLS context validation failed */
|
|
15
|
+
RLS_CONTEXT_INVALID: "RLS_CONTEXT_INVALID"
|
|
16
|
+
};
|
|
17
|
+
var RLSError = class extends Error {
|
|
18
|
+
code;
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new RLS error
|
|
21
|
+
*
|
|
22
|
+
* @param message - Error message
|
|
23
|
+
* @param code - RLS error code
|
|
24
|
+
*/
|
|
25
|
+
constructor(message, code) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "RLSError";
|
|
28
|
+
this.code = code;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Serializes the error to JSON
|
|
32
|
+
*
|
|
33
|
+
* @returns JSON representation of the error
|
|
34
|
+
*/
|
|
35
|
+
toJSON() {
|
|
36
|
+
return {
|
|
37
|
+
name: this.name,
|
|
38
|
+
message: this.message,
|
|
39
|
+
code: this.code
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var RLSContextError = class extends RLSError {
|
|
44
|
+
/**
|
|
45
|
+
* Creates a new RLS context error
|
|
46
|
+
*
|
|
47
|
+
* @param message - Error message (defaults to standard message)
|
|
48
|
+
*/
|
|
49
|
+
constructor(message = "No RLS context found. Ensure code runs within withRLSContext()") {
|
|
50
|
+
super(message, RLSErrorCodes.RLS_CONTEXT_MISSING);
|
|
51
|
+
this.name = "RLSContextError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var RLSContextValidationError = class extends RLSError {
|
|
55
|
+
field;
|
|
56
|
+
/**
|
|
57
|
+
* Creates a new context validation error
|
|
58
|
+
*
|
|
59
|
+
* @param message - Error message
|
|
60
|
+
* @param field - Field that failed validation
|
|
61
|
+
*/
|
|
62
|
+
constructor(message, field) {
|
|
63
|
+
super(message, RLSErrorCodes.RLS_CONTEXT_INVALID);
|
|
64
|
+
this.name = "RLSContextValidationError";
|
|
65
|
+
this.field = field;
|
|
66
|
+
}
|
|
67
|
+
toJSON() {
|
|
68
|
+
return {
|
|
69
|
+
...super.toJSON(),
|
|
70
|
+
field: this.field
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var RLSPolicyViolation = class extends RLSError {
|
|
75
|
+
operation;
|
|
76
|
+
table;
|
|
77
|
+
reason;
|
|
78
|
+
policyName;
|
|
79
|
+
/**
|
|
80
|
+
* Creates a new policy violation error
|
|
81
|
+
*
|
|
82
|
+
* @param operation - Database operation that was denied (read, create, update, delete)
|
|
83
|
+
* @param table - Table name where violation occurred
|
|
84
|
+
* @param reason - Reason for the policy violation
|
|
85
|
+
* @param policyName - Name of the policy that denied access (optional)
|
|
86
|
+
*/
|
|
87
|
+
constructor(operation, table, reason, policyName) {
|
|
88
|
+
super(
|
|
89
|
+
`RLS policy violation: ${operation} on ${table} - ${reason}`,
|
|
90
|
+
RLSErrorCodes.RLS_POLICY_VIOLATION
|
|
91
|
+
);
|
|
92
|
+
this.name = "RLSPolicyViolation";
|
|
93
|
+
this.operation = operation;
|
|
94
|
+
this.table = table;
|
|
95
|
+
this.reason = reason;
|
|
96
|
+
if (policyName !== void 0) {
|
|
97
|
+
this.policyName = policyName;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
toJSON() {
|
|
101
|
+
const json = {
|
|
102
|
+
...super.toJSON(),
|
|
103
|
+
operation: this.operation,
|
|
104
|
+
table: this.table,
|
|
105
|
+
reason: this.reason
|
|
106
|
+
};
|
|
107
|
+
if (this.policyName !== void 0) {
|
|
108
|
+
json["policyName"] = this.policyName;
|
|
109
|
+
}
|
|
110
|
+
return json;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var RLSSchemaError = class extends RLSError {
|
|
114
|
+
details;
|
|
115
|
+
/**
|
|
116
|
+
* Creates a new schema validation error
|
|
117
|
+
*
|
|
118
|
+
* @param message - Error message
|
|
119
|
+
* @param details - Additional details about the validation failure
|
|
120
|
+
*/
|
|
121
|
+
constructor(message, details = {}) {
|
|
122
|
+
super(message, RLSErrorCodes.RLS_SCHEMA_INVALID);
|
|
123
|
+
this.name = "RLSSchemaError";
|
|
124
|
+
this.details = details;
|
|
125
|
+
}
|
|
126
|
+
toJSON() {
|
|
127
|
+
return {
|
|
128
|
+
...super.toJSON(),
|
|
129
|
+
details: this.details
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/policy/schema.ts
|
|
135
|
+
function defineRLSSchema(schema) {
|
|
136
|
+
validateSchema(schema);
|
|
137
|
+
return schema;
|
|
138
|
+
}
|
|
139
|
+
function validateSchema(schema) {
|
|
140
|
+
for (const [table, config] of Object.entries(schema)) {
|
|
141
|
+
if (!config) continue;
|
|
142
|
+
const tableConfig = config;
|
|
143
|
+
if (!Array.isArray(tableConfig.policies)) {
|
|
144
|
+
throw new RLSSchemaError(
|
|
145
|
+
`Invalid policies for table "${table}": must be an array`,
|
|
146
|
+
{ table }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
for (let i = 0; i < tableConfig.policies.length; i++) {
|
|
150
|
+
const policy = tableConfig.policies[i];
|
|
151
|
+
if (policy !== void 0) {
|
|
152
|
+
validatePolicy(policy, table, i);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (tableConfig.skipFor !== void 0) {
|
|
156
|
+
if (!Array.isArray(tableConfig.skipFor)) {
|
|
157
|
+
throw new RLSSchemaError(
|
|
158
|
+
`Invalid skipFor for table "${table}": must be an array of role names`,
|
|
159
|
+
{ table }
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
for (const role of tableConfig.skipFor) {
|
|
163
|
+
if (typeof role !== "string" || role.trim() === "") {
|
|
164
|
+
throw new RLSSchemaError(
|
|
165
|
+
`Invalid role in skipFor for table "${table}": must be a non-empty string`,
|
|
166
|
+
{ table }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (tableConfig.defaultDeny !== void 0 && typeof tableConfig.defaultDeny !== "boolean") {
|
|
172
|
+
throw new RLSSchemaError(
|
|
173
|
+
`Invalid defaultDeny for table "${table}": must be a boolean`,
|
|
174
|
+
{ table }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function validatePolicy(policy, table, index) {
|
|
180
|
+
if (!policy.type) {
|
|
181
|
+
throw new RLSSchemaError(
|
|
182
|
+
`Policy ${index} for table "${table}" missing type`,
|
|
183
|
+
{ table, index }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const validTypes = ["allow", "deny", "filter", "validate"];
|
|
187
|
+
if (!validTypes.includes(policy.type)) {
|
|
188
|
+
throw new RLSSchemaError(
|
|
189
|
+
`Policy ${index} for table "${table}" has invalid type: ${policy.type}`,
|
|
190
|
+
{ table, index, type: policy.type }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (!policy.operation) {
|
|
194
|
+
throw new RLSSchemaError(
|
|
195
|
+
`Policy ${index} for table "${table}" missing operation`,
|
|
196
|
+
{ table, index }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const validOps = ["read", "create", "update", "delete", "all"];
|
|
200
|
+
const ops = Array.isArray(policy.operation) ? policy.operation : [policy.operation];
|
|
201
|
+
for (const op of ops) {
|
|
202
|
+
if (!validOps.includes(op)) {
|
|
203
|
+
throw new RLSSchemaError(
|
|
204
|
+
`Policy ${index} for table "${table}" has invalid operation: ${op}`,
|
|
205
|
+
{ table, index, operation: op }
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (policy.condition === void 0 || policy.condition === null) {
|
|
210
|
+
throw new RLSSchemaError(
|
|
211
|
+
`Policy ${index} for table "${table}" missing condition`,
|
|
212
|
+
{ table, index }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (typeof policy.condition !== "function" && typeof policy.condition !== "string") {
|
|
216
|
+
throw new RLSSchemaError(
|
|
217
|
+
`Policy ${index} for table "${table}" condition must be a function or string`,
|
|
218
|
+
{ table, index }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (policy.priority !== void 0 && typeof policy.priority !== "number") {
|
|
222
|
+
throw new RLSSchemaError(
|
|
223
|
+
`Policy ${index} for table "${table}" priority must be a number`,
|
|
224
|
+
{ table, index }
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (policy.name !== void 0 && typeof policy.name !== "string") {
|
|
228
|
+
throw new RLSSchemaError(
|
|
229
|
+
`Policy ${index} for table "${table}" name must be a string`,
|
|
230
|
+
{ table, index }
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function mergeRLSSchemas(...schemas) {
|
|
235
|
+
const merged = {};
|
|
236
|
+
for (const schema of schemas) {
|
|
237
|
+
for (const [table, config] of Object.entries(schema)) {
|
|
238
|
+
if (!config) continue;
|
|
239
|
+
const existingConfig = merged[table];
|
|
240
|
+
const newConfig = config;
|
|
241
|
+
if (existingConfig) {
|
|
242
|
+
existingConfig.policies = [
|
|
243
|
+
...existingConfig.policies,
|
|
244
|
+
...newConfig.policies
|
|
245
|
+
];
|
|
246
|
+
if (newConfig.skipFor) {
|
|
247
|
+
const existingSkipFor = existingConfig.skipFor ?? [];
|
|
248
|
+
const combinedSkipFor = [...existingSkipFor, ...newConfig.skipFor];
|
|
249
|
+
existingConfig.skipFor = Array.from(new Set(combinedSkipFor));
|
|
250
|
+
}
|
|
251
|
+
if (newConfig.defaultDeny !== void 0) {
|
|
252
|
+
existingConfig.defaultDeny = newConfig.defaultDeny;
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
merged[table] = {
|
|
256
|
+
policies: [...newConfig.policies],
|
|
257
|
+
skipFor: newConfig.skipFor ? [...newConfig.skipFor] : void 0,
|
|
258
|
+
defaultDeny: newConfig.defaultDeny
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
validateSchema(merged);
|
|
264
|
+
return merged;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/policy/builder.ts
|
|
268
|
+
function allow(operation, condition, options) {
|
|
269
|
+
const policy = {
|
|
270
|
+
type: "allow",
|
|
271
|
+
operation,
|
|
272
|
+
condition,
|
|
273
|
+
priority: options?.priority ?? 0
|
|
274
|
+
};
|
|
275
|
+
if (options?.name !== void 0) {
|
|
276
|
+
policy.name = options.name;
|
|
277
|
+
}
|
|
278
|
+
if (options?.hints !== void 0) {
|
|
279
|
+
policy.hints = options.hints;
|
|
280
|
+
}
|
|
281
|
+
return policy;
|
|
282
|
+
}
|
|
283
|
+
function deny(operation, condition, options) {
|
|
284
|
+
const policy = {
|
|
285
|
+
type: "deny",
|
|
286
|
+
operation,
|
|
287
|
+
condition: condition ?? (() => true),
|
|
288
|
+
priority: options?.priority ?? 100
|
|
289
|
+
// Deny policies run first by default
|
|
290
|
+
};
|
|
291
|
+
if (options?.name !== void 0) {
|
|
292
|
+
policy.name = options.name;
|
|
293
|
+
}
|
|
294
|
+
if (options?.hints !== void 0) {
|
|
295
|
+
policy.hints = options.hints;
|
|
296
|
+
}
|
|
297
|
+
return policy;
|
|
298
|
+
}
|
|
299
|
+
function filter(operation, condition, options) {
|
|
300
|
+
const policy = {
|
|
301
|
+
type: "filter",
|
|
302
|
+
operation: operation === "all" ? "read" : operation,
|
|
303
|
+
condition,
|
|
304
|
+
priority: options?.priority ?? 0
|
|
305
|
+
};
|
|
306
|
+
if (options?.name !== void 0) {
|
|
307
|
+
policy.name = options.name;
|
|
308
|
+
}
|
|
309
|
+
if (options?.hints !== void 0) {
|
|
310
|
+
policy.hints = options.hints;
|
|
311
|
+
}
|
|
312
|
+
return policy;
|
|
313
|
+
}
|
|
314
|
+
function validate(operation, condition, options) {
|
|
315
|
+
const ops = operation === "all" ? ["create", "update"] : [operation];
|
|
316
|
+
const policy = {
|
|
317
|
+
type: "validate",
|
|
318
|
+
operation: ops,
|
|
319
|
+
condition,
|
|
320
|
+
priority: options?.priority ?? 0
|
|
321
|
+
};
|
|
322
|
+
if (options?.name !== void 0) {
|
|
323
|
+
policy.name = options.name;
|
|
324
|
+
}
|
|
325
|
+
if (options?.hints !== void 0) {
|
|
326
|
+
policy.hints = options.hints;
|
|
327
|
+
}
|
|
328
|
+
return policy;
|
|
329
|
+
}
|
|
330
|
+
var PolicyRegistry = class {
|
|
331
|
+
tables = /* @__PURE__ */ new Map();
|
|
332
|
+
compiled = false;
|
|
333
|
+
logger;
|
|
334
|
+
constructor(schema, options) {
|
|
335
|
+
this.logger = options?.logger ?? silentLogger;
|
|
336
|
+
if (schema) {
|
|
337
|
+
this.loadSchema(schema);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Load and compile policies from schema
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```typescript
|
|
345
|
+
* const registry = new PolicyRegistry<Database>();
|
|
346
|
+
* registry.loadSchema({
|
|
347
|
+
* users: {
|
|
348
|
+
* policies: [
|
|
349
|
+
* allow('read', ctx => ctx.auth.userId === ctx.row.id),
|
|
350
|
+
* filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
|
|
351
|
+
* ],
|
|
352
|
+
* defaultDeny: true,
|
|
353
|
+
* },
|
|
354
|
+
* });
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
loadSchema(schema) {
|
|
358
|
+
for (const [table, config] of Object.entries(schema)) {
|
|
359
|
+
if (!config) continue;
|
|
360
|
+
this.registerTable(table, config);
|
|
361
|
+
}
|
|
362
|
+
this.compiled = true;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Register policies for a single table
|
|
366
|
+
*
|
|
367
|
+
* @param table - Table name
|
|
368
|
+
* @param config - Table RLS configuration
|
|
369
|
+
*/
|
|
370
|
+
registerTable(table, config) {
|
|
371
|
+
const tableConfig = {
|
|
372
|
+
allows: [],
|
|
373
|
+
denies: [],
|
|
374
|
+
filters: [],
|
|
375
|
+
validates: [],
|
|
376
|
+
skipFor: config.skipFor ?? [],
|
|
377
|
+
defaultDeny: config.defaultDeny ?? true
|
|
378
|
+
};
|
|
379
|
+
for (let i = 0; i < config.policies.length; i++) {
|
|
380
|
+
const policy = config.policies[i];
|
|
381
|
+
if (!policy) continue;
|
|
382
|
+
const policyName = policy.name ?? `${table}_policy_${i}`;
|
|
383
|
+
try {
|
|
384
|
+
if (policy.type === "filter") {
|
|
385
|
+
const compiled = this.compileFilterPolicy(policy, policyName);
|
|
386
|
+
tableConfig.filters.push(compiled);
|
|
387
|
+
} else {
|
|
388
|
+
const compiled = this.compilePolicy(policy, policyName);
|
|
389
|
+
switch (policy.type) {
|
|
390
|
+
case "allow":
|
|
391
|
+
tableConfig.allows.push(compiled);
|
|
392
|
+
break;
|
|
393
|
+
case "deny":
|
|
394
|
+
tableConfig.denies.push(compiled);
|
|
395
|
+
break;
|
|
396
|
+
case "validate":
|
|
397
|
+
tableConfig.validates.push(compiled);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw new RLSSchemaError(
|
|
403
|
+
`Failed to compile policy "${policyName}" for table "${table}": ${error instanceof Error ? error.message : String(error)}`,
|
|
404
|
+
{ table, policy: policyName }
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
tableConfig.allows.sort((a, b) => b.priority - a.priority);
|
|
409
|
+
tableConfig.denies.sort((a, b) => b.priority - a.priority);
|
|
410
|
+
tableConfig.validates.sort((a, b) => b.priority - a.priority);
|
|
411
|
+
this.tables.set(table, tableConfig);
|
|
412
|
+
}
|
|
413
|
+
register(schemaOrTable, policies, options) {
|
|
414
|
+
if (typeof schemaOrTable === "object" && schemaOrTable !== null) {
|
|
415
|
+
this.loadSchema(schemaOrTable);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const table = schemaOrTable;
|
|
419
|
+
if (!policies) {
|
|
420
|
+
throw new RLSSchemaError("Policies are required when registering by table name", { table });
|
|
421
|
+
}
|
|
422
|
+
const config = {
|
|
423
|
+
policies
|
|
424
|
+
};
|
|
425
|
+
if (options?.skipFor !== void 0) {
|
|
426
|
+
config.skipFor = options.skipFor;
|
|
427
|
+
}
|
|
428
|
+
if (options?.defaultDeny !== void 0) {
|
|
429
|
+
config.defaultDeny = options.defaultDeny;
|
|
430
|
+
}
|
|
431
|
+
this.registerTable(table, config);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Compile a policy definition into an internal compiled policy
|
|
435
|
+
*
|
|
436
|
+
* @param policy - Policy definition to compile
|
|
437
|
+
* @param name - Policy name for debugging
|
|
438
|
+
* @returns Compiled policy ready for evaluation
|
|
439
|
+
*/
|
|
440
|
+
compilePolicy(policy, name) {
|
|
441
|
+
const operations = Array.isArray(policy.operation) ? policy.operation : [policy.operation];
|
|
442
|
+
const expandedOps = operations.flatMap(
|
|
443
|
+
(op) => op === "all" ? ["read", "create", "update", "delete"] : [op]
|
|
444
|
+
);
|
|
445
|
+
return {
|
|
446
|
+
name,
|
|
447
|
+
operations: new Set(expandedOps),
|
|
448
|
+
type: policy.type,
|
|
449
|
+
evaluate: policy.condition,
|
|
450
|
+
priority: policy.priority ?? (policy.type === "deny" ? 100 : 0)
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Compile a filter policy
|
|
455
|
+
*
|
|
456
|
+
* @param policy - Filter policy definition
|
|
457
|
+
* @param name - Policy name for debugging
|
|
458
|
+
* @returns Compiled filter policy
|
|
459
|
+
*/
|
|
460
|
+
compileFilterPolicy(policy, name) {
|
|
461
|
+
const condition = policy.condition;
|
|
462
|
+
return {
|
|
463
|
+
operation: "read",
|
|
464
|
+
getConditions: condition,
|
|
465
|
+
name
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Convert internal compiled policy to public CompiledPolicy
|
|
470
|
+
*/
|
|
471
|
+
toCompiledPolicy(internal) {
|
|
472
|
+
return {
|
|
473
|
+
name: internal.name,
|
|
474
|
+
type: internal.type,
|
|
475
|
+
operation: Array.from(internal.operations),
|
|
476
|
+
evaluate: internal.evaluate,
|
|
477
|
+
priority: internal.priority
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get allow policies for a table and operation
|
|
482
|
+
*/
|
|
483
|
+
getAllows(table, operation) {
|
|
484
|
+
const config = this.tables.get(table);
|
|
485
|
+
if (!config) return [];
|
|
486
|
+
return config.allows.filter((p) => p.operations.has(operation)).map((p) => this.toCompiledPolicy(p));
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get deny policies for a table and operation
|
|
490
|
+
*/
|
|
491
|
+
getDenies(table, operation) {
|
|
492
|
+
const config = this.tables.get(table);
|
|
493
|
+
if (!config) return [];
|
|
494
|
+
return config.denies.filter((p) => p.operations.has(operation)).map((p) => this.toCompiledPolicy(p));
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Get validate policies for a table and operation
|
|
498
|
+
*/
|
|
499
|
+
getValidates(table, operation) {
|
|
500
|
+
const config = this.tables.get(table);
|
|
501
|
+
if (!config) return [];
|
|
502
|
+
return config.validates.filter((p) => p.operations.has(operation)).map((p) => this.toCompiledPolicy(p));
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Get filter policies for a table
|
|
506
|
+
*/
|
|
507
|
+
getFilters(table) {
|
|
508
|
+
const config = this.tables.get(table);
|
|
509
|
+
return config?.filters ?? [];
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get roles that skip RLS for a table
|
|
513
|
+
*/
|
|
514
|
+
getSkipFor(table) {
|
|
515
|
+
const config = this.tables.get(table);
|
|
516
|
+
return config?.skipFor ?? [];
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Check if table has default deny
|
|
520
|
+
*/
|
|
521
|
+
hasDefaultDeny(table) {
|
|
522
|
+
const config = this.tables.get(table);
|
|
523
|
+
return config?.defaultDeny ?? true;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Check if a table is registered
|
|
527
|
+
*/
|
|
528
|
+
hasTable(table) {
|
|
529
|
+
return this.tables.has(table);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get all registered table names
|
|
533
|
+
*/
|
|
534
|
+
getTables() {
|
|
535
|
+
return Array.from(this.tables.keys());
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Check if registry is compiled
|
|
539
|
+
*/
|
|
540
|
+
isCompiled() {
|
|
541
|
+
return this.compiled;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Validate that all policies are properly defined
|
|
545
|
+
*
|
|
546
|
+
* This method checks for common issues:
|
|
547
|
+
* - Tables with no policies and defaultDeny=false (warns)
|
|
548
|
+
* - Tables with skipFor operations but no corresponding policies
|
|
549
|
+
*/
|
|
550
|
+
validate() {
|
|
551
|
+
for (const [table, config] of this.tables) {
|
|
552
|
+
const hasPolicy = config.allows.length > 0 || config.denies.length > 0 || config.filters.length > 0 || config.validates.length > 0;
|
|
553
|
+
if (!hasPolicy && !config.defaultDeny) {
|
|
554
|
+
this.logger.warn?.(
|
|
555
|
+
`[RLS] Table "${table}" has no policies and defaultDeny is false. All operations will be allowed.`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (config.skipFor.length > 0) {
|
|
559
|
+
const opsWithPolicies = /* @__PURE__ */ new Set();
|
|
560
|
+
for (const allow2 of config.allows) {
|
|
561
|
+
allow2.operations.forEach((op) => opsWithPolicies.add(op));
|
|
562
|
+
}
|
|
563
|
+
for (const deny2 of config.denies) {
|
|
564
|
+
deny2.operations.forEach((op) => opsWithPolicies.add(op));
|
|
565
|
+
}
|
|
566
|
+
for (const validate2 of config.validates) {
|
|
567
|
+
validate2.operations.forEach((op) => opsWithPolicies.add(op));
|
|
568
|
+
}
|
|
569
|
+
if (config.filters.length > 0) {
|
|
570
|
+
opsWithPolicies.add("read");
|
|
571
|
+
}
|
|
572
|
+
const skippedOpsWithPolicies = config.skipFor.filter((op) => {
|
|
573
|
+
if (op === "all") return opsWithPolicies.size > 0;
|
|
574
|
+
return opsWithPolicies.has(op);
|
|
575
|
+
});
|
|
576
|
+
if (skippedOpsWithPolicies.length > 0) {
|
|
577
|
+
this.logger.warn?.(
|
|
578
|
+
`[RLS] Table "${table}" has skipFor operations that also have policies: ${skippedOpsWithPolicies.join(", ")}. The policies will be ignored for these operations.`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Clear all policies
|
|
586
|
+
*/
|
|
587
|
+
clear() {
|
|
588
|
+
this.tables.clear();
|
|
589
|
+
this.compiled = false;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Remove policies for a specific table
|
|
593
|
+
*/
|
|
594
|
+
remove(table) {
|
|
595
|
+
this.tables.delete(table);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
var rlsStorage = new AsyncLocalStorage();
|
|
599
|
+
|
|
600
|
+
// src/context/manager.ts
|
|
601
|
+
function createRLSContext(options) {
|
|
602
|
+
validateAuthContext(options.auth);
|
|
603
|
+
const context = {
|
|
604
|
+
auth: {
|
|
605
|
+
...options.auth,
|
|
606
|
+
isSystem: options.auth.isSystem ?? false
|
|
607
|
+
// Default to false if not provided
|
|
608
|
+
},
|
|
609
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
610
|
+
};
|
|
611
|
+
if (options.request) {
|
|
612
|
+
context.request = {
|
|
613
|
+
...options.request,
|
|
614
|
+
timestamp: options.request.timestamp ?? /* @__PURE__ */ new Date()
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
if (options.meta !== void 0) {
|
|
618
|
+
context.meta = options.meta;
|
|
619
|
+
}
|
|
620
|
+
return context;
|
|
621
|
+
}
|
|
622
|
+
function validateAuthContext(auth) {
|
|
623
|
+
if (auth.userId === void 0 || auth.userId === null) {
|
|
624
|
+
throw new RLSContextValidationError("userId is required in auth context", "userId");
|
|
625
|
+
}
|
|
626
|
+
if (!Array.isArray(auth.roles)) {
|
|
627
|
+
throw new RLSContextValidationError("roles must be an array", "roles");
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
var RLSContextManager = class {
|
|
631
|
+
/**
|
|
632
|
+
* Run a synchronous function within an RLS context
|
|
633
|
+
*/
|
|
634
|
+
run(context, fn) {
|
|
635
|
+
return rlsStorage.run(context, fn);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Run an async function within an RLS context
|
|
639
|
+
*/
|
|
640
|
+
async runAsync(context, fn) {
|
|
641
|
+
return rlsStorage.run(context, fn);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Get current RLS context
|
|
645
|
+
* @throws RLSContextError if no context is set
|
|
646
|
+
*/
|
|
647
|
+
getContext() {
|
|
648
|
+
const ctx = rlsStorage.getStore();
|
|
649
|
+
if (!ctx) {
|
|
650
|
+
throw new RLSContextError();
|
|
651
|
+
}
|
|
652
|
+
return ctx;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Get current RLS context or null if not set
|
|
656
|
+
*/
|
|
657
|
+
getContextOrNull() {
|
|
658
|
+
return rlsStorage.getStore() ?? null;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Check if running within RLS context
|
|
662
|
+
*/
|
|
663
|
+
hasContext() {
|
|
664
|
+
return rlsStorage.getStore() !== void 0;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Get current auth context
|
|
668
|
+
* @throws RLSContextError if no context is set
|
|
669
|
+
*/
|
|
670
|
+
getAuth() {
|
|
671
|
+
return this.getContext().auth;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Get current user ID
|
|
675
|
+
* @throws RLSContextError if no context is set
|
|
676
|
+
*/
|
|
677
|
+
getUserId() {
|
|
678
|
+
return this.getAuth().userId;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Get current tenant ID
|
|
682
|
+
* @throws RLSContextError if no context is set
|
|
683
|
+
*/
|
|
684
|
+
getTenantId() {
|
|
685
|
+
return this.getAuth().tenantId;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Check if current user has a specific role
|
|
689
|
+
*/
|
|
690
|
+
hasRole(role) {
|
|
691
|
+
const ctx = this.getContextOrNull();
|
|
692
|
+
return ctx?.auth.roles.includes(role) ?? false;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Check if current user has a specific permission
|
|
696
|
+
*/
|
|
697
|
+
hasPermission(permission) {
|
|
698
|
+
const ctx = this.getContextOrNull();
|
|
699
|
+
return ctx?.auth.permissions?.includes(permission) ?? false;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Check if current context is a system context (bypasses RLS)
|
|
703
|
+
*/
|
|
704
|
+
isSystem() {
|
|
705
|
+
const ctx = this.getContextOrNull();
|
|
706
|
+
return ctx?.auth.isSystem ?? false;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Create a system context for operations that should bypass RLS
|
|
710
|
+
*/
|
|
711
|
+
asSystem(fn) {
|
|
712
|
+
const currentCtx = this.getContextOrNull();
|
|
713
|
+
if (!currentCtx) {
|
|
714
|
+
throw new RLSContextError("Cannot create system context without existing context");
|
|
715
|
+
}
|
|
716
|
+
const systemCtx = {
|
|
717
|
+
...currentCtx,
|
|
718
|
+
auth: { ...currentCtx.auth, isSystem: true }
|
|
719
|
+
};
|
|
720
|
+
return this.run(systemCtx, fn);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Create a system context for async operations
|
|
724
|
+
*/
|
|
725
|
+
async asSystemAsync(fn) {
|
|
726
|
+
const currentCtx = this.getContextOrNull();
|
|
727
|
+
if (!currentCtx) {
|
|
728
|
+
throw new RLSContextError("Cannot create system context without existing context");
|
|
729
|
+
}
|
|
730
|
+
const systemCtx = {
|
|
731
|
+
...currentCtx,
|
|
732
|
+
auth: { ...currentCtx.auth, isSystem: true }
|
|
733
|
+
};
|
|
734
|
+
return this.runAsync(systemCtx, fn);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
var rlsContext = new RLSContextManager();
|
|
738
|
+
function withRLSContext(context, fn) {
|
|
739
|
+
return rlsContext.run(context, fn);
|
|
740
|
+
}
|
|
741
|
+
async function withRLSContextAsync(context, fn) {
|
|
742
|
+
return rlsContext.runAsync(context, fn);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/transformer/select.ts
|
|
746
|
+
var SelectTransformer = class {
|
|
747
|
+
constructor(registry) {
|
|
748
|
+
this.registry = registry;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Transform a SELECT query by applying filter policies
|
|
752
|
+
*
|
|
753
|
+
* @param qb - The query builder to transform
|
|
754
|
+
* @param table - Table name being queried
|
|
755
|
+
* @returns Transformed query builder with RLS filters applied
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* ```typescript
|
|
759
|
+
* const transformer = new SelectTransformer(registry);
|
|
760
|
+
* let query = db.selectFrom('posts').selectAll();
|
|
761
|
+
* query = transformer.transform(query, 'posts');
|
|
762
|
+
* // Query now includes WHERE conditions from RLS policies
|
|
763
|
+
* ```
|
|
764
|
+
*/
|
|
765
|
+
transform(qb, table) {
|
|
766
|
+
const ctx = rlsContext.getContextOrNull();
|
|
767
|
+
if (!ctx) {
|
|
768
|
+
return qb;
|
|
769
|
+
}
|
|
770
|
+
if (ctx.auth.isSystem) {
|
|
771
|
+
return qb;
|
|
772
|
+
}
|
|
773
|
+
const skipFor = this.registry.getSkipFor(table);
|
|
774
|
+
if (skipFor.some((role) => ctx.auth.roles.includes(role))) {
|
|
775
|
+
return qb;
|
|
776
|
+
}
|
|
777
|
+
const filters = this.registry.getFilters(table);
|
|
778
|
+
if (filters.length === 0) {
|
|
779
|
+
return qb;
|
|
780
|
+
}
|
|
781
|
+
let result = qb;
|
|
782
|
+
for (const filter2 of filters) {
|
|
783
|
+
const conditions = this.evaluateFilter(filter2, ctx, table);
|
|
784
|
+
result = this.applyConditions(result, conditions, table);
|
|
785
|
+
}
|
|
786
|
+
return result;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Evaluate a filter policy to get WHERE conditions
|
|
790
|
+
*
|
|
791
|
+
* @param filter - The filter policy to evaluate
|
|
792
|
+
* @param ctx - RLS context
|
|
793
|
+
* @param table - Table name
|
|
794
|
+
* @returns WHERE clause conditions as key-value pairs
|
|
795
|
+
*/
|
|
796
|
+
evaluateFilter(filter2, ctx, table) {
|
|
797
|
+
const evalCtx = {
|
|
798
|
+
auth: ctx.auth,
|
|
799
|
+
...ctx.meta !== void 0 && { meta: ctx.meta }
|
|
800
|
+
};
|
|
801
|
+
const result = filter2.getConditions(evalCtx);
|
|
802
|
+
if (result instanceof Promise) {
|
|
803
|
+
throw new RLSError(
|
|
804
|
+
`Async filter policies are not supported in SELECT transformers. Filter '${filter2.name}' on table '${table}' returned a Promise. Use synchronous conditions for filter policies.`,
|
|
805
|
+
RLSErrorCodes.RLS_POLICY_INVALID
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Apply filter conditions to query builder
|
|
812
|
+
*
|
|
813
|
+
* @param qb - Query builder to modify
|
|
814
|
+
* @param conditions - WHERE clause conditions
|
|
815
|
+
* @param table - Table name (for qualified column names)
|
|
816
|
+
* @returns Modified query builder
|
|
817
|
+
*/
|
|
818
|
+
applyConditions(qb, conditions, table) {
|
|
819
|
+
let result = qb;
|
|
820
|
+
for (const [column, value] of Object.entries(conditions)) {
|
|
821
|
+
const qualifiedColumn = `${table}.${column}`;
|
|
822
|
+
if (value === null) {
|
|
823
|
+
result = result.where(qualifiedColumn, "is", null);
|
|
824
|
+
} else if (value === void 0) {
|
|
825
|
+
continue;
|
|
826
|
+
} else if (Array.isArray(value)) {
|
|
827
|
+
if (value.length === 0) {
|
|
828
|
+
result = result.where(qualifiedColumn, "=", "__RLS_NO_MATCH__");
|
|
829
|
+
} else {
|
|
830
|
+
result = result.where(qualifiedColumn, "in", value);
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
result = result.where(qualifiedColumn, "=", value);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return result;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// src/transformer/mutation.ts
|
|
841
|
+
var MutationGuard = class {
|
|
842
|
+
constructor(registry, executor) {
|
|
843
|
+
this.registry = registry;
|
|
844
|
+
this.executor = executor;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Check if CREATE operation is allowed
|
|
848
|
+
*
|
|
849
|
+
* @param table - Table name
|
|
850
|
+
* @param data - Data being inserted
|
|
851
|
+
* @throws RLSPolicyViolation if access is denied
|
|
852
|
+
*
|
|
853
|
+
* @example
|
|
854
|
+
* ```typescript
|
|
855
|
+
* const guard = new MutationGuard(registry, db);
|
|
856
|
+
* await guard.checkCreate('posts', { title: 'Hello', tenant_id: 1 });
|
|
857
|
+
* ```
|
|
858
|
+
*/
|
|
859
|
+
async checkCreate(table, data) {
|
|
860
|
+
await this.checkMutation(table, "create", void 0, data);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Check if UPDATE operation is allowed
|
|
864
|
+
*
|
|
865
|
+
* @param table - Table name
|
|
866
|
+
* @param existingRow - Current row data
|
|
867
|
+
* @param data - Data being updated
|
|
868
|
+
* @throws RLSPolicyViolation if access is denied
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* ```typescript
|
|
872
|
+
* const guard = new MutationGuard(registry, db);
|
|
873
|
+
* const existingPost = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
|
|
874
|
+
* await guard.checkUpdate('posts', existingPost, { title: 'Updated' });
|
|
875
|
+
* ```
|
|
876
|
+
*/
|
|
877
|
+
async checkUpdate(table, existingRow, data) {
|
|
878
|
+
await this.checkMutation(table, "update", existingRow, data);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Check if DELETE operation is allowed
|
|
882
|
+
*
|
|
883
|
+
* @param table - Table name
|
|
884
|
+
* @param existingRow - Row to be deleted
|
|
885
|
+
* @throws RLSPolicyViolation if access is denied
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```typescript
|
|
889
|
+
* const guard = new MutationGuard(registry, db);
|
|
890
|
+
* const existingPost = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
|
|
891
|
+
* await guard.checkDelete('posts', existingPost);
|
|
892
|
+
* ```
|
|
893
|
+
*/
|
|
894
|
+
async checkDelete(table, existingRow) {
|
|
895
|
+
await this.checkMutation(table, "delete", existingRow);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Check if READ operation is allowed on a specific row
|
|
899
|
+
*
|
|
900
|
+
* @param table - Table name
|
|
901
|
+
* @param row - Row to check access for
|
|
902
|
+
* @returns true if access is allowed
|
|
903
|
+
*
|
|
904
|
+
* @example
|
|
905
|
+
* ```typescript
|
|
906
|
+
* const guard = new MutationGuard(registry, db);
|
|
907
|
+
* const post = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
|
|
908
|
+
* const canRead = await guard.checkRead('posts', post);
|
|
909
|
+
* ```
|
|
910
|
+
*/
|
|
911
|
+
async checkRead(table, row) {
|
|
912
|
+
try {
|
|
913
|
+
await this.checkMutation(table, "read", row);
|
|
914
|
+
return true;
|
|
915
|
+
} catch (error) {
|
|
916
|
+
if (error instanceof RLSPolicyViolation) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Generic check for any operation (helper for testing)
|
|
924
|
+
*
|
|
925
|
+
* @param operation - Operation type
|
|
926
|
+
* @param table - Table name
|
|
927
|
+
* @param row - Existing row (for UPDATE/DELETE/READ)
|
|
928
|
+
* @param data - New data (for CREATE/UPDATE)
|
|
929
|
+
* @returns true if access is allowed, false otherwise
|
|
930
|
+
*/
|
|
931
|
+
async canMutate(operation, table, row, data) {
|
|
932
|
+
try {
|
|
933
|
+
await this.checkMutation(table, operation, row, data);
|
|
934
|
+
return true;
|
|
935
|
+
} catch (error) {
|
|
936
|
+
if (error instanceof RLSPolicyViolation) {
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Validate mutation data (for validate policies)
|
|
944
|
+
*
|
|
945
|
+
* @param operation - Operation type
|
|
946
|
+
* @param table - Table name
|
|
947
|
+
* @param data - New data (for CREATE/UPDATE)
|
|
948
|
+
* @param row - Existing row (for UPDATE)
|
|
949
|
+
* @returns true if valid, false otherwise
|
|
950
|
+
*/
|
|
951
|
+
async validateMutation(operation, table, data, row) {
|
|
952
|
+
const ctx = rlsContext.getContextOrNull();
|
|
953
|
+
if (!ctx) {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
if (ctx.auth.isSystem) {
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
const skipFor = this.registry.getSkipFor(table);
|
|
960
|
+
if (skipFor.some((role) => ctx.auth.roles.includes(role))) {
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
const validates = this.registry.getValidates(table, operation);
|
|
964
|
+
if (validates.length === 0) {
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
const evalCtx = {
|
|
968
|
+
auth: ctx.auth,
|
|
969
|
+
row,
|
|
970
|
+
data,
|
|
971
|
+
table,
|
|
972
|
+
operation,
|
|
973
|
+
...ctx.meta !== void 0 && { meta: ctx.meta }
|
|
974
|
+
};
|
|
975
|
+
for (const validate2 of validates) {
|
|
976
|
+
const result = await this.evaluatePolicy(validate2.evaluate, evalCtx);
|
|
977
|
+
if (!result) {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Check mutation against RLS policies
|
|
985
|
+
*
|
|
986
|
+
* @param table - Table name
|
|
987
|
+
* @param operation - Operation type
|
|
988
|
+
* @param row - Existing row (for UPDATE/DELETE/READ)
|
|
989
|
+
* @param data - New data (for CREATE/UPDATE)
|
|
990
|
+
* @throws RLSPolicyViolation if access is denied
|
|
991
|
+
*/
|
|
992
|
+
async checkMutation(table, operation, row, data) {
|
|
993
|
+
const ctx = rlsContext.getContextOrNull();
|
|
994
|
+
if (!ctx) {
|
|
995
|
+
throw new RLSPolicyViolation(
|
|
996
|
+
operation,
|
|
997
|
+
table,
|
|
998
|
+
"No RLS context available"
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
if (ctx.auth.isSystem) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const skipFor = this.registry.getSkipFor(table);
|
|
1005
|
+
if (skipFor.some((role) => ctx.auth.roles.includes(role))) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const denies = this.registry.getDenies(table, operation);
|
|
1009
|
+
for (const deny2 of denies) {
|
|
1010
|
+
const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
|
|
1011
|
+
const result = await this.evaluatePolicy(deny2.evaluate, evalCtx);
|
|
1012
|
+
if (result) {
|
|
1013
|
+
throw new RLSPolicyViolation(
|
|
1014
|
+
operation,
|
|
1015
|
+
table,
|
|
1016
|
+
`Denied by policy: ${deny2.name}`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if ((operation === "create" || operation === "update") && data) {
|
|
1021
|
+
const validates = this.registry.getValidates(table, operation);
|
|
1022
|
+
for (const validate2 of validates) {
|
|
1023
|
+
const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
|
|
1024
|
+
const result = await this.evaluatePolicy(validate2.evaluate, evalCtx);
|
|
1025
|
+
if (!result) {
|
|
1026
|
+
throw new RLSPolicyViolation(
|
|
1027
|
+
operation,
|
|
1028
|
+
table,
|
|
1029
|
+
`Validation failed: ${validate2.name}`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const allows = this.registry.getAllows(table, operation);
|
|
1035
|
+
const defaultDeny = this.registry.hasDefaultDeny(table);
|
|
1036
|
+
if (defaultDeny && allows.length === 0) {
|
|
1037
|
+
throw new RLSPolicyViolation(
|
|
1038
|
+
operation,
|
|
1039
|
+
table,
|
|
1040
|
+
"No allow policies defined (default deny)"
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
if (allows.length > 0) {
|
|
1044
|
+
let allowed = false;
|
|
1045
|
+
for (const allow2 of allows) {
|
|
1046
|
+
const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
|
|
1047
|
+
const result = await this.evaluatePolicy(allow2.evaluate, evalCtx);
|
|
1048
|
+
if (result) {
|
|
1049
|
+
allowed = true;
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (!allowed) {
|
|
1054
|
+
throw new RLSPolicyViolation(
|
|
1055
|
+
operation,
|
|
1056
|
+
table,
|
|
1057
|
+
"No allow policies matched"
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Create policy evaluation context
|
|
1064
|
+
*/
|
|
1065
|
+
createEvalContext(ctx, table, operation, row, data) {
|
|
1066
|
+
return {
|
|
1067
|
+
auth: ctx.auth,
|
|
1068
|
+
row,
|
|
1069
|
+
data,
|
|
1070
|
+
table,
|
|
1071
|
+
operation,
|
|
1072
|
+
metadata: ctx.meta
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Evaluate a policy condition
|
|
1077
|
+
*/
|
|
1078
|
+
async evaluatePolicy(condition, evalCtx) {
|
|
1079
|
+
try {
|
|
1080
|
+
const result = condition(evalCtx);
|
|
1081
|
+
return result instanceof Promise ? await result : result;
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
throw new RLSPolicyViolation(
|
|
1084
|
+
evalCtx.operation,
|
|
1085
|
+
evalCtx.table,
|
|
1086
|
+
`Policy evaluation error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Filter an array of rows, keeping only accessible ones
|
|
1092
|
+
* Useful for post-query filtering when query-level filtering is not possible
|
|
1093
|
+
*
|
|
1094
|
+
* @param table - Table name
|
|
1095
|
+
* @param rows - Array of rows to filter
|
|
1096
|
+
* @returns Filtered array containing only accessible rows
|
|
1097
|
+
*
|
|
1098
|
+
* @example
|
|
1099
|
+
* ```typescript
|
|
1100
|
+
* const guard = new MutationGuard(registry, db);
|
|
1101
|
+
* const allPosts = await db.selectFrom('posts').selectAll().execute();
|
|
1102
|
+
* const accessiblePosts = await guard.filterRows('posts', allPosts);
|
|
1103
|
+
* ```
|
|
1104
|
+
*/
|
|
1105
|
+
async filterRows(table, rows) {
|
|
1106
|
+
const results = [];
|
|
1107
|
+
for (const row of rows) {
|
|
1108
|
+
if (await this.checkRead(table, row)) {
|
|
1109
|
+
results.push(row);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return results;
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
function rlsPlugin(options) {
|
|
1116
|
+
const {
|
|
1117
|
+
schema,
|
|
1118
|
+
skipTables = [],
|
|
1119
|
+
bypassRoles = [],
|
|
1120
|
+
logger = silentLogger,
|
|
1121
|
+
requireContext = false,
|
|
1122
|
+
auditDecisions = false,
|
|
1123
|
+
onViolation
|
|
1124
|
+
} = options;
|
|
1125
|
+
let registry;
|
|
1126
|
+
let selectTransformer;
|
|
1127
|
+
let mutationGuard;
|
|
1128
|
+
return {
|
|
1129
|
+
name: "@kysera/rls",
|
|
1130
|
+
version: "0.5.1",
|
|
1131
|
+
// Run after soft-delete (priority 0), before audit
|
|
1132
|
+
priority: 50,
|
|
1133
|
+
// No dependencies by default
|
|
1134
|
+
dependencies: [],
|
|
1135
|
+
/**
|
|
1136
|
+
* Initialize plugin - compile policies
|
|
1137
|
+
*/
|
|
1138
|
+
async onInit(executor) {
|
|
1139
|
+
logger.info?.("[RLS] Initializing RLS plugin", {
|
|
1140
|
+
tables: Object.keys(schema).length,
|
|
1141
|
+
skipTables: skipTables.length,
|
|
1142
|
+
bypassRoles: bypassRoles.length
|
|
1143
|
+
});
|
|
1144
|
+
registry = new PolicyRegistry(schema);
|
|
1145
|
+
registry.validate();
|
|
1146
|
+
selectTransformer = new SelectTransformer(registry);
|
|
1147
|
+
mutationGuard = new MutationGuard(registry, executor);
|
|
1148
|
+
logger.info?.("[RLS] RLS plugin initialized successfully");
|
|
1149
|
+
},
|
|
1150
|
+
/**
|
|
1151
|
+
* Intercept queries to apply RLS filtering
|
|
1152
|
+
*
|
|
1153
|
+
* This hook is called for every query builder operation. For SELECT queries,
|
|
1154
|
+
* it applies filter policies as WHERE conditions. For mutations, it marks
|
|
1155
|
+
* that RLS validation is required (performed in extendRepository).
|
|
1156
|
+
*/
|
|
1157
|
+
interceptQuery(qb, context) {
|
|
1158
|
+
const { operation, table, metadata } = context;
|
|
1159
|
+
if (skipTables.includes(table)) {
|
|
1160
|
+
logger.debug?.(`[RLS] Skipping RLS for excluded table: ${table}`);
|
|
1161
|
+
return qb;
|
|
1162
|
+
}
|
|
1163
|
+
if (metadata["skipRLS"] === true) {
|
|
1164
|
+
logger.debug?.(`[RLS] Skipping RLS (explicit skip): ${table}`);
|
|
1165
|
+
return qb;
|
|
1166
|
+
}
|
|
1167
|
+
const ctx = rlsContext.getContextOrNull();
|
|
1168
|
+
if (!ctx) {
|
|
1169
|
+
if (requireContext) {
|
|
1170
|
+
throw new RLSContextError("RLS context required but not found");
|
|
1171
|
+
}
|
|
1172
|
+
logger.warn?.(`[RLS] No context for ${operation} on ${table}`);
|
|
1173
|
+
return qb;
|
|
1174
|
+
}
|
|
1175
|
+
if (ctx.auth.isSystem) {
|
|
1176
|
+
logger.debug?.(`[RLS] Bypassing RLS (system user): ${table}`);
|
|
1177
|
+
return qb;
|
|
1178
|
+
}
|
|
1179
|
+
if (bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
|
|
1180
|
+
logger.debug?.(`[RLS] Bypassing RLS (bypass role): ${table}`);
|
|
1181
|
+
return qb;
|
|
1182
|
+
}
|
|
1183
|
+
if (operation === "select") {
|
|
1184
|
+
try {
|
|
1185
|
+
const transformed = selectTransformer.transform(qb, table);
|
|
1186
|
+
if (auditDecisions) {
|
|
1187
|
+
logger.info?.("[RLS] Filter applied", {
|
|
1188
|
+
table,
|
|
1189
|
+
operation,
|
|
1190
|
+
userId: ctx.auth.userId
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
return transformed;
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
logger.error?.("[RLS] Error applying filter", { table, error });
|
|
1196
|
+
throw error;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (operation === "insert" || operation === "update" || operation === "delete") {
|
|
1200
|
+
metadata["__rlsRequired"] = true;
|
|
1201
|
+
metadata["__rlsTable"] = table;
|
|
1202
|
+
}
|
|
1203
|
+
return qb;
|
|
1204
|
+
},
|
|
1205
|
+
/**
|
|
1206
|
+
* Extend repository with RLS-aware methods
|
|
1207
|
+
*
|
|
1208
|
+
* Wraps create, update, and delete methods to enforce RLS policies.
|
|
1209
|
+
* Also adds utility methods for bypassing RLS and checking access.
|
|
1210
|
+
*/
|
|
1211
|
+
extendRepository(repo) {
|
|
1212
|
+
const baseRepo = repo;
|
|
1213
|
+
if (!("tableName" in baseRepo) || !("executor" in baseRepo)) {
|
|
1214
|
+
return repo;
|
|
1215
|
+
}
|
|
1216
|
+
const table = baseRepo.tableName;
|
|
1217
|
+
if (skipTables.includes(table)) {
|
|
1218
|
+
return repo;
|
|
1219
|
+
}
|
|
1220
|
+
if (!registry.hasTable(table)) {
|
|
1221
|
+
logger.debug?.(`[RLS] Table "${table}" not in RLS schema, skipping`);
|
|
1222
|
+
return repo;
|
|
1223
|
+
}
|
|
1224
|
+
logger.debug?.(`[RLS] Extending repository for table: ${table}`);
|
|
1225
|
+
const originalCreate = baseRepo.create?.bind(baseRepo);
|
|
1226
|
+
const originalUpdate = baseRepo.update?.bind(baseRepo);
|
|
1227
|
+
const originalDelete = baseRepo.delete?.bind(baseRepo);
|
|
1228
|
+
const originalFindById = baseRepo.findById?.bind(baseRepo);
|
|
1229
|
+
const extendedRepo = {
|
|
1230
|
+
...baseRepo,
|
|
1231
|
+
/**
|
|
1232
|
+
* Wrapped create with RLS check
|
|
1233
|
+
*/
|
|
1234
|
+
async create(data) {
|
|
1235
|
+
if (!originalCreate) {
|
|
1236
|
+
throw new RLSError("Repository does not support create operation", RLSErrorCodes.RLS_POLICY_INVALID);
|
|
1237
|
+
}
|
|
1238
|
+
const ctx = rlsContext.getContextOrNull();
|
|
1239
|
+
if (ctx && !ctx.auth.isSystem && !bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
|
|
1240
|
+
try {
|
|
1241
|
+
await mutationGuard.checkCreate(table, data);
|
|
1242
|
+
if (auditDecisions) {
|
|
1243
|
+
logger.info?.("[RLS] Create allowed", { table, userId: ctx.auth.userId });
|
|
1244
|
+
}
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
if (error instanceof RLSPolicyViolation) {
|
|
1247
|
+
onViolation?.(error);
|
|
1248
|
+
if (auditDecisions) {
|
|
1249
|
+
logger.warn?.("[RLS] Create denied", {
|
|
1250
|
+
table,
|
|
1251
|
+
userId: ctx.auth.userId,
|
|
1252
|
+
reason: error.reason
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return originalCreate(data);
|
|
1260
|
+
},
|
|
1261
|
+
/**
|
|
1262
|
+
* Wrapped update with RLS check
|
|
1263
|
+
*/
|
|
1264
|
+
async update(id, data) {
|
|
1265
|
+
if (!originalUpdate || !originalFindById) {
|
|
1266
|
+
throw new RLSError("Repository does not support update operation", RLSErrorCodes.RLS_POLICY_INVALID);
|
|
1267
|
+
}
|
|
1268
|
+
const ctx = rlsContext.getContextOrNull();
|
|
1269
|
+
if (ctx && !ctx.auth.isSystem && !bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
|
|
1270
|
+
const existingRow = await originalFindById(id);
|
|
1271
|
+
if (!existingRow) {
|
|
1272
|
+
return originalUpdate(id, data);
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
await mutationGuard.checkUpdate(
|
|
1276
|
+
table,
|
|
1277
|
+
existingRow,
|
|
1278
|
+
data
|
|
1279
|
+
);
|
|
1280
|
+
if (auditDecisions) {
|
|
1281
|
+
logger.info?.("[RLS] Update allowed", { table, id, userId: ctx.auth.userId });
|
|
1282
|
+
}
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
if (error instanceof RLSPolicyViolation) {
|
|
1285
|
+
onViolation?.(error);
|
|
1286
|
+
if (auditDecisions) {
|
|
1287
|
+
logger.warn?.("[RLS] Update denied", {
|
|
1288
|
+
table,
|
|
1289
|
+
id,
|
|
1290
|
+
userId: ctx.auth.userId,
|
|
1291
|
+
reason: error.reason
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
throw error;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return originalUpdate(id, data);
|
|
1299
|
+
},
|
|
1300
|
+
/**
|
|
1301
|
+
* Wrapped delete with RLS check
|
|
1302
|
+
*/
|
|
1303
|
+
async delete(id) {
|
|
1304
|
+
if (!originalDelete || !originalFindById) {
|
|
1305
|
+
throw new RLSError("Repository does not support delete operation", RLSErrorCodes.RLS_POLICY_INVALID);
|
|
1306
|
+
}
|
|
1307
|
+
const ctx = rlsContext.getContextOrNull();
|
|
1308
|
+
if (ctx && !ctx.auth.isSystem && !bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
|
|
1309
|
+
const existingRow = await originalFindById(id);
|
|
1310
|
+
if (!existingRow) {
|
|
1311
|
+
return originalDelete(id);
|
|
1312
|
+
}
|
|
1313
|
+
try {
|
|
1314
|
+
await mutationGuard.checkDelete(table, existingRow);
|
|
1315
|
+
if (auditDecisions) {
|
|
1316
|
+
logger.info?.("[RLS] Delete allowed", { table, id, userId: ctx.auth.userId });
|
|
1317
|
+
}
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
if (error instanceof RLSPolicyViolation) {
|
|
1320
|
+
onViolation?.(error);
|
|
1321
|
+
if (auditDecisions) {
|
|
1322
|
+
logger.warn?.("[RLS] Delete denied", {
|
|
1323
|
+
table,
|
|
1324
|
+
id,
|
|
1325
|
+
userId: ctx.auth.userId,
|
|
1326
|
+
reason: error.reason
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
throw error;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return originalDelete(id);
|
|
1334
|
+
},
|
|
1335
|
+
/**
|
|
1336
|
+
* Bypass RLS for specific operation
|
|
1337
|
+
* Requires existing context
|
|
1338
|
+
*
|
|
1339
|
+
* @example
|
|
1340
|
+
* ```typescript
|
|
1341
|
+
* // Perform operation as system user
|
|
1342
|
+
* const result = await repo.withoutRLS(async () => {
|
|
1343
|
+
* return repo.findAll(); // No RLS filtering
|
|
1344
|
+
* });
|
|
1345
|
+
* ```
|
|
1346
|
+
*/
|
|
1347
|
+
async withoutRLS(fn) {
|
|
1348
|
+
return rlsContext.asSystemAsync(fn);
|
|
1349
|
+
},
|
|
1350
|
+
/**
|
|
1351
|
+
* Check if current user can perform operation on a row
|
|
1352
|
+
*
|
|
1353
|
+
* @example
|
|
1354
|
+
* ```typescript
|
|
1355
|
+
* const post = await repo.findById(1);
|
|
1356
|
+
* const canUpdate = await repo.canAccess('update', post);
|
|
1357
|
+
* if (canUpdate) {
|
|
1358
|
+
* await repo.update(1, { title: 'New title' });
|
|
1359
|
+
* }
|
|
1360
|
+
* ```
|
|
1361
|
+
*/
|
|
1362
|
+
async canAccess(operation, row) {
|
|
1363
|
+
const ctx = rlsContext.getContextOrNull();
|
|
1364
|
+
if (!ctx) return false;
|
|
1365
|
+
if (ctx.auth.isSystem) return true;
|
|
1366
|
+
if (bypassRoles.some((role) => ctx.auth.roles.includes(role))) return true;
|
|
1367
|
+
try {
|
|
1368
|
+
switch (operation) {
|
|
1369
|
+
case "read":
|
|
1370
|
+
return await mutationGuard.checkRead(table, row);
|
|
1371
|
+
case "create":
|
|
1372
|
+
await mutationGuard.checkCreate(table, row);
|
|
1373
|
+
return true;
|
|
1374
|
+
case "update":
|
|
1375
|
+
await mutationGuard.checkUpdate(table, row, {});
|
|
1376
|
+
return true;
|
|
1377
|
+
case "delete":
|
|
1378
|
+
await mutationGuard.checkDelete(table, row);
|
|
1379
|
+
return true;
|
|
1380
|
+
default:
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
logger.debug?.("[RLS] Access check failed", {
|
|
1385
|
+
table,
|
|
1386
|
+
operation,
|
|
1387
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1388
|
+
});
|
|
1389
|
+
return false;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
return extendedRepo;
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/utils/helpers.ts
|
|
1399
|
+
function createEvaluationContext(rlsCtx, options) {
|
|
1400
|
+
const ctx = {
|
|
1401
|
+
auth: rlsCtx.auth
|
|
1402
|
+
};
|
|
1403
|
+
if (options?.row !== void 0) {
|
|
1404
|
+
ctx.row = options.row;
|
|
1405
|
+
}
|
|
1406
|
+
if (options?.data !== void 0) {
|
|
1407
|
+
ctx.data = options.data;
|
|
1408
|
+
}
|
|
1409
|
+
if (rlsCtx.request !== void 0) {
|
|
1410
|
+
ctx.request = rlsCtx.request;
|
|
1411
|
+
}
|
|
1412
|
+
if (rlsCtx.meta !== void 0) {
|
|
1413
|
+
ctx.meta = rlsCtx.meta;
|
|
1414
|
+
}
|
|
1415
|
+
return ctx;
|
|
1416
|
+
}
|
|
1417
|
+
function isAsyncFunction(fn) {
|
|
1418
|
+
return fn instanceof Function && fn.constructor.name === "AsyncFunction";
|
|
1419
|
+
}
|
|
1420
|
+
async function safeEvaluate(fn, defaultValue) {
|
|
1421
|
+
try {
|
|
1422
|
+
const result = fn();
|
|
1423
|
+
if (result instanceof Promise) {
|
|
1424
|
+
return await result;
|
|
1425
|
+
}
|
|
1426
|
+
return result;
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
return defaultValue;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
function deepMerge(target, source) {
|
|
1432
|
+
const result = { ...target };
|
|
1433
|
+
for (const key of Object.keys(source)) {
|
|
1434
|
+
const sourceValue = source[key];
|
|
1435
|
+
const targetValue = result[key];
|
|
1436
|
+
if (sourceValue !== void 0 && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) {
|
|
1437
|
+
result[key] = deepMerge(
|
|
1438
|
+
targetValue,
|
|
1439
|
+
sourceValue
|
|
1440
|
+
);
|
|
1441
|
+
} else if (sourceValue !== void 0) {
|
|
1442
|
+
result[key] = sourceValue;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return result;
|
|
1446
|
+
}
|
|
1447
|
+
function hashString(str) {
|
|
1448
|
+
let hash = 0;
|
|
1449
|
+
for (let i = 0; i < str.length; i++) {
|
|
1450
|
+
const char = str.charCodeAt(i);
|
|
1451
|
+
hash = (hash << 5) - hash + char;
|
|
1452
|
+
hash = hash & hash;
|
|
1453
|
+
}
|
|
1454
|
+
return hash.toString(36);
|
|
1455
|
+
}
|
|
1456
|
+
function normalizeOperations(operation) {
|
|
1457
|
+
if (Array.isArray(operation)) {
|
|
1458
|
+
if (operation.includes("all")) {
|
|
1459
|
+
return ["read", "create", "update", "delete"];
|
|
1460
|
+
}
|
|
1461
|
+
return operation;
|
|
1462
|
+
}
|
|
1463
|
+
if (operation === "all") {
|
|
1464
|
+
return ["read", "create", "update", "delete"];
|
|
1465
|
+
}
|
|
1466
|
+
return [operation];
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
export { PolicyRegistry, RLSContextError, RLSContextValidationError, RLSError, RLSErrorCodes, RLSPolicyViolation, RLSSchemaError, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
|
|
1470
|
+
//# sourceMappingURL=index.js.map
|
|
1471
|
+
//# sourceMappingURL=index.js.map
|