@kysera/rls 0.5.1 → 0.6.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/README.md CHANGED
@@ -622,13 +622,15 @@ Create an RLS context object with validation.
622
622
  import { createRLSContext } from '@kysera/rls';
623
623
 
624
624
  const ctx = createRLSContext({
625
- userId: 123,
626
- roles: ['user', 'editor'],
627
- tenantId: 'acme-corp',
628
- // Optional fields
629
- organizationIds: ['org-1'],
630
- permissions: ['posts:read', 'posts:write'],
631
- isSystem: false,
625
+ auth: {
626
+ userId: 123,
627
+ roles: ['user', 'editor'],
628
+ tenantId: 'acme-corp',
629
+ organizationIds: ['org-1'],
630
+ permissions: ['posts:read', 'posts:write'],
631
+ isSystem: false,
632
+ },
633
+ timestamp: new Date(),
632
634
  });
633
635
 
634
636
  // Use with runAsync
@@ -832,12 +834,9 @@ Generate PostgreSQL `CREATE POLICY` statements from your RLS schema.
832
834
  import { PostgresRLSGenerator } from '@kysera/rls/native';
833
835
 
834
836
  const generator = new PostgresRLSGenerator(rlsSchema, {
835
- contextFunctions: {
836
- // Define SQL functions to access context
837
- userId: 'current_setting(\'app.user_id\')::integer',
838
- tenantId: 'current_setting(\'app.tenant_id\')::uuid',
839
- roles: 'current_setting(\'app.roles\')::text[]',
840
- },
837
+ force: true, // Force RLS on table owners
838
+ schemaName: 'public', // Schema name (default: public)
839
+ policyPrefix: 'rls_', // Prefix for generated policy names
841
840
  });
842
841
 
843
842
  // Generate policies for a table
@@ -847,14 +846,15 @@ console.log(sql);
847
846
  /*
848
847
  Output:
849
848
  ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
849
+ ALTER TABLE posts FORCE ROW LEVEL SECURITY;
850
850
 
851
- CREATE POLICY tenant_isolation ON posts
851
+ CREATE POLICY rls_tenant_isolation ON posts
852
852
  FOR ALL
853
- USING (tenant_id = current_setting('app.tenant_id')::uuid);
853
+ USING (tenant_id = current_user_tenant_id());
854
854
 
855
- CREATE POLICY author_access ON posts
855
+ CREATE POLICY rls_author_access ON posts
856
856
  FOR UPDATE
857
- USING (author_id = current_setting('app.user_id')::integer);
857
+ USING (author_id = current_user_id());
858
858
  */
859
859
  ```
860
860
 
@@ -866,10 +866,9 @@ Generate migration files for PostgreSQL RLS policies.
866
866
  import { RLSMigrationGenerator } from '@kysera/rls/native';
867
867
 
868
868
  const migrationGen = new RLSMigrationGenerator(rlsSchema, {
869
- contextFunctions: {
870
- userId: 'current_setting(\'app.user_id\')::integer',
871
- tenantId: 'current_setting(\'app.tenant_id\')::uuid',
872
- },
869
+ force: true, // Force RLS on table owners
870
+ schemaName: 'public', // Schema name (default: public)
871
+ policyPrefix: 'rls_', // Prefix for generated policy names
873
872
  migrationPath: './migrations',
874
873
  timestamp: true,
875
874
  });
@@ -922,6 +921,7 @@ import {
922
921
  RLSError,
923
922
  RLSContextError,
924
923
  RLSPolicyViolation,
924
+ RLSPolicyEvaluationError,
925
925
  RLSSchemaError,
926
926
  RLSContextValidationError,
927
927
  RLSErrorCodes,
@@ -946,7 +946,7 @@ try {
946
946
 
947
947
  #### `RLSPolicyViolation`
948
948
 
949
- Thrown when a database operation is denied by RLS policies.
949
+ Thrown when a database operation is denied by RLS policies (legitimate access denial).
950
950
 
951
951
  ```typescript
952
952
  try {
@@ -964,6 +964,30 @@ try {
964
964
  }
965
965
  ```
966
966
 
967
+ #### `RLSPolicyEvaluationError`
968
+
969
+ Thrown when a policy fails to evaluate due to an error in the policy code itself (bug in policy implementation). This is distinct from `RLSPolicyViolation`, which represents legitimate access denial.
970
+
971
+ ```typescript
972
+ try {
973
+ // Policy throws an error during evaluation
974
+ await orm.posts.findAll();
975
+ } catch (error) {
976
+ if (error instanceof RLSPolicyEvaluationError) {
977
+ console.error('Policy evaluation failed:', {
978
+ operation: error.operation, // 'read'
979
+ table: error.table, // 'posts'
980
+ policyName: error.policyName, // 'tenant_filter'
981
+ originalError: error.originalError, // The underlying error
982
+ message: error.message, // Error message with context
983
+ });
984
+
985
+ // Original stack trace is preserved for debugging
986
+ console.error('Stack trace:', error.stack);
987
+ }
988
+ }
989
+ ```
990
+
967
991
  ### Handling Violations
968
992
 
969
993
  ```typescript
@@ -1146,12 +1170,14 @@ export {
1146
1170
  withRLSContext,
1147
1171
  withRLSContextAsync,
1148
1172
  } from '@kysera/rls';
1173
+ export type { RLSContext } from '@kysera/rls';
1149
1174
 
1150
1175
  // Errors
1151
1176
  export {
1152
1177
  RLSError,
1153
1178
  RLSContextError,
1154
1179
  RLSPolicyViolation,
1180
+ RLSPolicyEvaluationError,
1155
1181
  RLSSchemaError,
1156
1182
  RLSContextValidationError,
1157
1183
  RLSErrorCodes,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RLSSchema, O as Operation, P as PolicyCondition, a as PolicyHints, b as PolicyDefinition, F as FilterCondition, T as TableRLSConfig, C as CompiledPolicy, c as CompiledFilterPolicy, d as RLSContext, e as RLSAuthContext, f as RLSRequestContext, g as PolicyEvaluationContext } from './types-Dtg6Lt1k.js';
2
- export { h as PolicyType } from './types-Dtg6Lt1k.js';
1
+ import { R as RLSSchema, O as Operation, P as PolicyCondition, a as PolicyHints, b as PolicyDefinition, F as FilterCondition, T as TableRLSConfig, C as CompiledPolicy, c as CompiledFilterPolicy, d as RLSContext, e as RLSAuthContext, f as RLSRequestContext, g as PolicyEvaluationContext } from './types-6eCXh_Jd.js';
2
+ export { h as PolicyType } from './types-6eCXh_Jd.js';
3
3
  import { KyseraLogger, ErrorCode } from '@kysera/core';
4
4
  import { Plugin } from '@kysera/repository';
5
5
  import 'kysely';
@@ -347,6 +347,8 @@ declare const RLSErrorCodes: {
347
347
  readonly RLS_SCHEMA_INVALID: ErrorCode;
348
348
  /** RLS context validation failed */
349
349
  readonly RLS_CONTEXT_INVALID: ErrorCode;
350
+ /** RLS policy evaluation threw an error */
351
+ readonly RLS_POLICY_EVALUATION_ERROR: ErrorCode;
350
352
  };
351
353
  /**
352
354
  * Type for RLS error codes
@@ -469,6 +471,39 @@ declare class RLSPolicyViolation extends RLSError {
469
471
  constructor(operation: string, table: string, reason: string, policyName?: string);
470
472
  toJSON(): Record<string, unknown>;
471
473
  }
474
+ /**
475
+ * Error thrown when a policy condition throws an error during evaluation
476
+ *
477
+ * This error is distinct from RLSPolicyViolation - it indicates a bug in the
478
+ * policy condition function itself, not a legitimate access denial.
479
+ *
480
+ * @example
481
+ * ```typescript
482
+ * // A policy with a bug
483
+ * allow('read', ctx => {
484
+ * return ctx.row.someField.value; // Throws if someField is undefined
485
+ * });
486
+ *
487
+ * // This will throw RLSPolicyEvaluationError, not RLSPolicyViolation
488
+ * ```
489
+ */
490
+ declare class RLSPolicyEvaluationError extends RLSError {
491
+ readonly operation: string;
492
+ readonly table: string;
493
+ readonly policyName?: string;
494
+ readonly originalError?: Error;
495
+ /**
496
+ * Creates a new policy evaluation error
497
+ *
498
+ * @param operation - Database operation being performed
499
+ * @param table - Table name where error occurred
500
+ * @param message - Error message from the policy
501
+ * @param policyName - Name of the policy that threw
502
+ * @param originalError - The original error thrown by the policy
503
+ */
504
+ constructor(operation: string, table: string, message: string, policyName?: string, originalError?: Error);
505
+ toJSON(): Record<string, unknown>;
506
+ }
472
507
  /**
473
508
  * Error thrown when RLS schema validation fails
474
509
  *
@@ -702,4 +737,4 @@ declare function hashString(str: string): string;
702
737
  */
703
738
  declare function normalizeOperations(operation: Operation | Operation[]): Operation[];
704
739
 
705
- export { CompiledFilterPolicy, CompiledPolicy, type CreateRLSContextOptions, FilterCondition, Operation, PolicyCondition, PolicyDefinition, PolicyEvaluationContext, PolicyHints, type PolicyOptions, PolicyRegistry, RLSAuthContext, RLSContext, RLSContextError, RLSContextValidationError, RLSError, type RLSErrorCode, RLSErrorCodes, type RLSPluginOptions, RLSPolicyViolation, RLSRequestContext, RLSSchema, RLSSchemaError, TableRLSConfig, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
740
+ export { CompiledFilterPolicy, CompiledPolicy, type CreateRLSContextOptions, FilterCondition, Operation, PolicyCondition, PolicyDefinition, PolicyEvaluationContext, PolicyHints, type PolicyOptions, PolicyRegistry, RLSAuthContext, RLSContext, RLSContextError, RLSContextValidationError, RLSError, type RLSErrorCode, RLSErrorCodes, type RLSPluginOptions, RLSPolicyEvaluationError, RLSPolicyViolation, RLSRequestContext, RLSSchema, RLSSchemaError, TableRLSConfig, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { silentLogger } from '@kysera/core';
2
+ import { sql } from 'kysely';
2
3
  import { AsyncLocalStorage } from 'async_hooks';
3
4
 
4
5
  // src/errors.ts
@@ -12,7 +13,9 @@ var RLSErrorCodes = {
12
13
  /** RLS schema definition is invalid */
13
14
  RLS_SCHEMA_INVALID: "RLS_SCHEMA_INVALID",
14
15
  /** RLS context validation failed */
15
- RLS_CONTEXT_INVALID: "RLS_CONTEXT_INVALID"
16
+ RLS_CONTEXT_INVALID: "RLS_CONTEXT_INVALID",
17
+ /** RLS policy evaluation threw an error */
18
+ RLS_POLICY_EVALUATION_ERROR: "RLS_POLICY_EVALUATION_ERROR"
16
19
  };
17
20
  var RLSError = class extends Error {
18
21
  code;
@@ -110,6 +113,59 @@ var RLSPolicyViolation = class extends RLSError {
110
113
  return json;
111
114
  }
112
115
  };
116
+ var RLSPolicyEvaluationError = class extends RLSError {
117
+ operation;
118
+ table;
119
+ policyName;
120
+ originalError;
121
+ /**
122
+ * Creates a new policy evaluation error
123
+ *
124
+ * @param operation - Database operation being performed
125
+ * @param table - Table name where error occurred
126
+ * @param message - Error message from the policy
127
+ * @param policyName - Name of the policy that threw
128
+ * @param originalError - The original error thrown by the policy
129
+ */
130
+ constructor(operation, table, message, policyName, originalError) {
131
+ super(
132
+ `RLS policy evaluation error during ${operation} on ${table}: ${message}`,
133
+ RLSErrorCodes.RLS_POLICY_EVALUATION_ERROR
134
+ );
135
+ this.name = "RLSPolicyEvaluationError";
136
+ this.operation = operation;
137
+ this.table = table;
138
+ if (policyName !== void 0) {
139
+ this.policyName = policyName;
140
+ }
141
+ if (originalError !== void 0) {
142
+ this.originalError = originalError;
143
+ if (originalError.stack) {
144
+ this.stack = `${this.stack}
145
+
146
+ Caused by:
147
+ ${originalError.stack}`;
148
+ }
149
+ }
150
+ }
151
+ toJSON() {
152
+ const json = {
153
+ ...super.toJSON(),
154
+ operation: this.operation,
155
+ table: this.table
156
+ };
157
+ if (this.policyName !== void 0) {
158
+ json["policyName"] = this.policyName;
159
+ }
160
+ if (this.originalError !== void 0) {
161
+ json["originalError"] = {
162
+ name: this.originalError.name,
163
+ message: this.originalError.message
164
+ };
165
+ }
166
+ return json;
167
+ }
168
+ };
113
169
  var RLSSchemaError = class extends RLSError {
114
170
  details;
115
171
  /**
@@ -810,6 +866,18 @@ var SelectTransformer = class {
810
866
  /**
811
867
  * Apply filter conditions to query builder
812
868
  *
869
+ * NOTE ON TYPE CASTS:
870
+ * The `as any` casts in this method are intentional architectural boundaries.
871
+ * Kysely's type system is designed for static query building where column names
872
+ * are known at compile time. RLS policies work with dynamic column names at runtime.
873
+ *
874
+ * Type safety is maintained through:
875
+ * 1. Policy conditions are validated during schema registration
876
+ * 2. Column names come from policy definitions (developer-controlled)
877
+ * 3. Values are type-checked by the policy condition functions
878
+ *
879
+ * This is the same pattern used in @kysera/repository/table-operations.ts
880
+ *
813
881
  * @param qb - Query builder to modify
814
882
  * @param conditions - WHERE clause conditions
815
883
  * @param table - Table name (for qualified column names)
@@ -825,7 +893,7 @@ var SelectTransformer = class {
825
893
  continue;
826
894
  } else if (Array.isArray(value)) {
827
895
  if (value.length === 0) {
828
- result = result.where(qualifiedColumn, "=", "__RLS_NO_MATCH__");
896
+ result = result.where(sql`FALSE`);
829
897
  } else {
830
898
  result = result.where(qualifiedColumn, "in", value);
831
899
  }
@@ -839,9 +907,8 @@ var SelectTransformer = class {
839
907
 
840
908
  // src/transformer/mutation.ts
841
909
  var MutationGuard = class {
842
- constructor(registry, executor) {
910
+ constructor(registry) {
843
911
  this.registry = registry;
844
- this.executor = executor;
845
912
  }
846
913
  /**
847
914
  * Check if CREATE operation is allowed
@@ -852,7 +919,7 @@ var MutationGuard = class {
852
919
  *
853
920
  * @example
854
921
  * ```typescript
855
- * const guard = new MutationGuard(registry, db);
922
+ * const guard = new MutationGuard(registry);
856
923
  * await guard.checkCreate('posts', { title: 'Hello', tenant_id: 1 });
857
924
  * ```
858
925
  */
@@ -869,7 +936,7 @@ var MutationGuard = class {
869
936
  *
870
937
  * @example
871
938
  * ```typescript
872
- * const guard = new MutationGuard(registry, db);
939
+ * const guard = new MutationGuard(registry);
873
940
  * const existingPost = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
874
941
  * await guard.checkUpdate('posts', existingPost, { title: 'Updated' });
875
942
  * ```
@@ -886,7 +953,7 @@ var MutationGuard = class {
886
953
  *
887
954
  * @example
888
955
  * ```typescript
889
- * const guard = new MutationGuard(registry, db);
956
+ * const guard = new MutationGuard(registry);
890
957
  * const existingPost = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
891
958
  * await guard.checkDelete('posts', existingPost);
892
959
  * ```
@@ -903,7 +970,7 @@ var MutationGuard = class {
903
970
  *
904
971
  * @example
905
972
  * ```typescript
906
- * const guard = new MutationGuard(registry, db);
973
+ * const guard = new MutationGuard(registry);
907
974
  * const post = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
908
975
  * const canRead = await guard.checkRead('posts', post);
909
976
  * ```
@@ -913,7 +980,7 @@ var MutationGuard = class {
913
980
  await this.checkMutation(table, "read", row);
914
981
  return true;
915
982
  } catch (error) {
916
- if (error instanceof RLSPolicyViolation) {
983
+ if (error instanceof RLSPolicyViolation || error instanceof RLSPolicyEvaluationError) {
917
984
  return false;
918
985
  }
919
986
  throw error;
@@ -933,7 +1000,7 @@ var MutationGuard = class {
933
1000
  await this.checkMutation(table, operation, row, data);
934
1001
  return true;
935
1002
  } catch (error) {
936
- if (error instanceof RLSPolicyViolation) {
1003
+ if (error instanceof RLSPolicyViolation || error instanceof RLSPolicyEvaluationError) {
937
1004
  return false;
938
1005
  }
939
1006
  throw error;
@@ -973,7 +1040,7 @@ var MutationGuard = class {
973
1040
  ...ctx.meta !== void 0 && { meta: ctx.meta }
974
1041
  };
975
1042
  for (const validate2 of validates) {
976
- const result = await this.evaluatePolicy(validate2.evaluate, evalCtx);
1043
+ const result = await this.evaluatePolicy(validate2.evaluate, evalCtx, validate2.name);
977
1044
  if (!result) {
978
1045
  return false;
979
1046
  }
@@ -1008,7 +1075,7 @@ var MutationGuard = class {
1008
1075
  const denies = this.registry.getDenies(table, operation);
1009
1076
  for (const deny2 of denies) {
1010
1077
  const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
1011
- const result = await this.evaluatePolicy(deny2.evaluate, evalCtx);
1078
+ const result = await this.evaluatePolicy(deny2.evaluate, evalCtx, deny2.name);
1012
1079
  if (result) {
1013
1080
  throw new RLSPolicyViolation(
1014
1081
  operation,
@@ -1021,7 +1088,7 @@ var MutationGuard = class {
1021
1088
  const validates = this.registry.getValidates(table, operation);
1022
1089
  for (const validate2 of validates) {
1023
1090
  const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
1024
- const result = await this.evaluatePolicy(validate2.evaluate, evalCtx);
1091
+ const result = await this.evaluatePolicy(validate2.evaluate, evalCtx, validate2.name);
1025
1092
  if (!result) {
1026
1093
  throw new RLSPolicyViolation(
1027
1094
  operation,
@@ -1044,7 +1111,7 @@ var MutationGuard = class {
1044
1111
  let allowed = false;
1045
1112
  for (const allow2 of allows) {
1046
1113
  const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
1047
- const result = await this.evaluatePolicy(allow2.evaluate, evalCtx);
1114
+ const result = await this.evaluatePolicy(allow2.evaluate, evalCtx, allow2.name);
1048
1115
  if (result) {
1049
1116
  allowed = true;
1050
1117
  break;
@@ -1069,21 +1136,30 @@ var MutationGuard = class {
1069
1136
  data,
1070
1137
  table,
1071
1138
  operation,
1072
- metadata: ctx.meta
1139
+ ...ctx.meta !== void 0 && { meta: ctx.meta }
1073
1140
  };
1074
1141
  }
1075
1142
  /**
1076
1143
  * Evaluate a policy condition
1144
+ *
1145
+ * @param condition - Policy condition function
1146
+ * @param evalCtx - Policy evaluation context
1147
+ * @param policyName - Name of the policy being evaluated (for error reporting)
1148
+ * @returns Boolean result of policy evaluation
1149
+ * @throws RLSPolicyEvaluationError if the policy throws an error
1077
1150
  */
1078
- async evaluatePolicy(condition, evalCtx) {
1151
+ async evaluatePolicy(condition, evalCtx, policyName) {
1079
1152
  try {
1080
1153
  const result = condition(evalCtx);
1081
1154
  return result instanceof Promise ? await result : result;
1082
1155
  } catch (error) {
1083
- throw new RLSPolicyViolation(
1084
- evalCtx.operation,
1085
- evalCtx.table,
1086
- `Policy evaluation error: ${error instanceof Error ? error.message : "Unknown error"}`
1156
+ const originalError = error instanceof Error ? error : void 0;
1157
+ throw new RLSPolicyEvaluationError(
1158
+ evalCtx.operation ?? "unknown",
1159
+ evalCtx.table ?? "unknown",
1160
+ error instanceof Error ? error.message : "Unknown error",
1161
+ policyName,
1162
+ originalError
1087
1163
  );
1088
1164
  }
1089
1165
  }
@@ -1097,7 +1173,7 @@ var MutationGuard = class {
1097
1173
  *
1098
1174
  * @example
1099
1175
  * ```typescript
1100
- * const guard = new MutationGuard(registry, db);
1176
+ * const guard = new MutationGuard(registry);
1101
1177
  * const allPosts = await db.selectFrom('posts').selectAll().execute();
1102
1178
  * const accessiblePosts = await guard.filterRows('posts', allPosts);
1103
1179
  * ```
@@ -1135,7 +1211,7 @@ function rlsPlugin(options) {
1135
1211
  /**
1136
1212
  * Initialize plugin - compile policies
1137
1213
  */
1138
- async onInit(executor) {
1214
+ async onInit(_executor) {
1139
1215
  logger.info?.("[RLS] Initializing RLS plugin", {
1140
1216
  tables: Object.keys(schema).length,
1141
1217
  skipTables: skipTables.length,
@@ -1144,7 +1220,7 @@ function rlsPlugin(options) {
1144
1220
  registry = new PolicyRegistry(schema);
1145
1221
  registry.validate();
1146
1222
  selectTransformer = new SelectTransformer(registry);
1147
- mutationGuard = new MutationGuard(registry, executor);
1223
+ mutationGuard = new MutationGuard(registry);
1148
1224
  logger.info?.("[RLS] RLS plugin initialized successfully");
1149
1225
  },
1150
1226
  /**
@@ -1466,6 +1542,6 @@ function normalizeOperations(operation) {
1466
1542
  return [operation];
1467
1543
  }
1468
1544
 
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 };
1545
+ export { PolicyRegistry, RLSContextError, RLSContextValidationError, RLSError, RLSErrorCodes, RLSPolicyEvaluationError, RLSPolicyViolation, RLSSchemaError, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
1470
1546
  //# sourceMappingURL=index.js.map
1471
1547
  //# sourceMappingURL=index.js.map