@kysera/rls 0.6.0 → 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
@@ -921,6 +921,7 @@ import {
921
921
  RLSError,
922
922
  RLSContextError,
923
923
  RLSPolicyViolation,
924
+ RLSPolicyEvaluationError,
924
925
  RLSSchemaError,
925
926
  RLSContextValidationError,
926
927
  RLSErrorCodes,
@@ -945,7 +946,7 @@ try {
945
946
 
946
947
  #### `RLSPolicyViolation`
947
948
 
948
- Thrown when a database operation is denied by RLS policies.
949
+ Thrown when a database operation is denied by RLS policies (legitimate access denial).
949
950
 
950
951
  ```typescript
951
952
  try {
@@ -963,6 +964,30 @@ try {
963
964
  }
964
965
  ```
965
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
+
966
991
  ### Handling Violations
967
992
 
968
993
  ```typescript
@@ -1152,6 +1177,7 @@ 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
@@ -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,8 +907,7 @@ var SelectTransformer = class {
839
907
 
840
908
  // src/transformer/mutation.ts
841
909
  var MutationGuard = class {
842
- // executor is kept for future use with database-dependent policies
843
- constructor(registry, _executor) {
910
+ constructor(registry) {
844
911
  this.registry = registry;
845
912
  }
846
913
  /**
@@ -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;
@@ -1074,16 +1141,25 @@ var MutationGuard = class {
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(
1156
+ const originalError = error instanceof Error ? error : void 0;
1157
+ throw new RLSPolicyEvaluationError(
1084
1158
  evalCtx.operation ?? "unknown",
1085
1159
  evalCtx.table ?? "unknown",
1086
- `Policy evaluation error: ${error instanceof Error ? error.message : "Unknown error"}`
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