@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/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