@kysera/rls 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import { silentLogger } from '@kysera/core';
2
- import { getRawDb } from '@kysera/executor';
3
- import { sql } from 'kysely';
1
+ import { DatabaseError, silentLogger } from '@kysera/core';
2
+ import { isRepositoryLike, getRawDb } from '@kysera/executor';
3
+ import { z } from 'zod';
4
4
  import { AsyncLocalStorage } from 'async_hooks';
5
+ import { sql } from 'kysely';
5
6
 
6
7
  // src/errors.ts
7
8
  var RLSErrorCodes = {
@@ -18,8 +19,7 @@ var RLSErrorCodes = {
18
19
  /** RLS policy evaluation threw an error */
19
20
  RLS_POLICY_EVALUATION_ERROR: "RLS_POLICY_EVALUATION_ERROR"
20
21
  };
21
- var RLSError = class extends Error {
22
- code;
22
+ var RLSError = class extends DatabaseError {
23
23
  /**
24
24
  * Creates a new RLS error
25
25
  *
@@ -27,21 +27,8 @@ var RLSError = class extends Error {
27
27
  * @param code - RLS error code
28
28
  */
29
29
  constructor(message, code) {
30
- super(message);
30
+ super(message, code);
31
31
  this.name = "RLSError";
32
- this.code = code;
33
- }
34
- /**
35
- * Serializes the error to JSON
36
- *
37
- * @returns JSON representation of the error
38
- */
39
- toJSON() {
40
- return {
41
- name: this.name,
42
- message: this.message,
43
- code: this.code
44
- };
45
32
  }
46
33
  };
47
34
  var RLSContextError = class extends RLSError {
@@ -198,10 +185,7 @@ function validateSchema(schema) {
198
185
  if (!config) continue;
199
186
  const tableConfig = config;
200
187
  if (!Array.isArray(tableConfig.policies)) {
201
- throw new RLSSchemaError(
202
- `Invalid policies for table "${table}": must be an array`,
203
- { table }
204
- );
188
+ throw new RLSSchemaError(`Invalid policies for table "${table}": must be an array`, { table });
205
189
  }
206
190
  for (let i = 0; i < tableConfig.policies.length; i++) {
207
191
  const policy = tableConfig.policies[i];
@@ -226,19 +210,15 @@ function validateSchema(schema) {
226
210
  }
227
211
  }
228
212
  if (tableConfig.defaultDeny !== void 0 && typeof tableConfig.defaultDeny !== "boolean") {
229
- throw new RLSSchemaError(
230
- `Invalid defaultDeny for table "${table}": must be a boolean`,
231
- { table }
232
- );
213
+ throw new RLSSchemaError(`Invalid defaultDeny for table "${table}": must be a boolean`, {
214
+ table
215
+ });
233
216
  }
234
217
  }
235
218
  }
236
219
  function validatePolicy(policy, table, index) {
237
220
  if (!policy.type) {
238
- throw new RLSSchemaError(
239
- `Policy ${index} for table "${table}" missing type`,
240
- { table, index }
241
- );
221
+ throw new RLSSchemaError(`Policy ${index} for table "${table}" missing type`, { table, index });
242
222
  }
243
223
  const validTypes = ["allow", "deny", "filter", "validate"];
244
224
  if (!validTypes.includes(policy.type)) {
@@ -248,10 +228,10 @@ function validatePolicy(policy, table, index) {
248
228
  );
249
229
  }
250
230
  if (!policy.operation) {
251
- throw new RLSSchemaError(
252
- `Policy ${index} for table "${table}" missing operation`,
253
- { table, index }
254
- );
231
+ throw new RLSSchemaError(`Policy ${index} for table "${table}" missing operation`, {
232
+ table,
233
+ index
234
+ });
255
235
  }
256
236
  const validOps = ["read", "create", "update", "delete", "all"];
257
237
  const ops = Array.isArray(policy.operation) ? policy.operation : [policy.operation];
@@ -264,10 +244,10 @@ function validatePolicy(policy, table, index) {
264
244
  }
265
245
  }
266
246
  if (policy.condition === void 0 || policy.condition === null) {
267
- throw new RLSSchemaError(
268
- `Policy ${index} for table "${table}" missing condition`,
269
- { table, index }
270
- );
247
+ throw new RLSSchemaError(`Policy ${index} for table "${table}" missing condition`, {
248
+ table,
249
+ index
250
+ });
271
251
  }
272
252
  if (typeof policy.condition !== "function" && typeof policy.condition !== "string") {
273
253
  throw new RLSSchemaError(
@@ -276,16 +256,16 @@ function validatePolicy(policy, table, index) {
276
256
  );
277
257
  }
278
258
  if (policy.priority !== void 0 && typeof policy.priority !== "number") {
279
- throw new RLSSchemaError(
280
- `Policy ${index} for table "${table}" priority must be a number`,
281
- { table, index }
282
- );
259
+ throw new RLSSchemaError(`Policy ${index} for table "${table}" priority must be a number`, {
260
+ table,
261
+ index
262
+ });
283
263
  }
284
264
  if (policy.name !== void 0 && typeof policy.name !== "string") {
285
- throw new RLSSchemaError(
286
- `Policy ${index} for table "${table}" name must be a string`,
287
- { table, index }
288
- );
265
+ throw new RLSSchemaError(`Policy ${index} for table "${table}" name must be a string`, {
266
+ table,
267
+ index
268
+ });
289
269
  }
290
270
  }
291
271
  function mergeRLSSchemas(...schemas) {
@@ -296,10 +276,7 @@ function mergeRLSSchemas(...schemas) {
296
276
  const existingConfig = merged[table];
297
277
  const newConfig = config;
298
278
  if (existingConfig) {
299
- existingConfig.policies = [
300
- ...existingConfig.policies,
301
- ...newConfig.policies
302
- ];
279
+ existingConfig.policies = [...existingConfig.policies, ...newConfig.policies];
303
280
  if (newConfig.skipFor) {
304
281
  const existingSkipFor = existingConfig.skipFor ?? [];
305
282
  const combinedSkipFor = [...existingSkipFor, ...newConfig.skipFor];
@@ -695,7 +672,7 @@ var RLSContextManager = class {
695
672
  * Run an async function within an RLS context
696
673
  */
697
674
  async runAsync(context, fn) {
698
- return rlsStorage.run(context, fn);
675
+ return await rlsStorage.run(context, fn);
699
676
  }
700
677
  /**
701
678
  * Get current RLS context
@@ -788,7 +765,7 @@ var RLSContextManager = class {
788
765
  ...currentCtx,
789
766
  auth: { ...currentCtx.auth, isSystem: true }
790
767
  };
791
- return this.runAsync(systemCtx, fn);
768
+ return await this.runAsync(systemCtx, fn);
792
769
  }
793
770
  };
794
771
  var rlsContext = new RLSContextManager();
@@ -796,7 +773,32 @@ function withRLSContext(context, fn) {
796
773
  return rlsContext.run(context, fn);
797
774
  }
798
775
  async function withRLSContextAsync(context, fn) {
799
- return rlsContext.runAsync(context, fn);
776
+ return await rlsContext.runAsync(context, fn);
777
+ }
778
+ function createQualifiedColumn(table, column) {
779
+ return `${table}.${column}`;
780
+ }
781
+ function applyWhereCondition(qb, column, operator, value) {
782
+ return qb.where(column, operator, value);
783
+ }
784
+ function createRawCondition(expression) {
785
+ return sql`${sql.raw(expression)}`;
786
+ }
787
+ function selectFromDynamicTable(db, table) {
788
+ return db.selectFrom(table).selectAll();
789
+ }
790
+ function whereIdEquals(qb, id, primaryKeyColumn = "id") {
791
+ return qb.where(primaryKeyColumn, "=", id);
792
+ }
793
+ function transformQueryBuilder(qb, operation, transform) {
794
+ if (operation !== "select") {
795
+ return qb;
796
+ }
797
+ const transformed = transform(qb);
798
+ return transformed;
799
+ }
800
+ function hasRawDb(executor) {
801
+ return "__rawDb" in executor && executor.__rawDb !== void 0;
800
802
  }
801
803
 
802
804
  // src/transformer/select.ts
@@ -822,7 +824,12 @@ var SelectTransformer = class {
822
824
  transform(qb, table) {
823
825
  const ctx = rlsContext.getContextOrNull();
824
826
  if (!ctx) {
825
- return qb;
827
+ return applyWhereCondition(
828
+ qb,
829
+ createRawCondition("FALSE"),
830
+ "=",
831
+ true
832
+ );
826
833
  }
827
834
  if (ctx.auth.isSystem) {
828
835
  return qb;
@@ -867,18 +874,14 @@ var SelectTransformer = class {
867
874
  /**
868
875
  * Apply filter conditions to query builder
869
876
  *
870
- * NOTE ON TYPE CASTS:
871
- * The `as any` casts in this method are intentional architectural boundaries.
872
- * Kysely's type system is designed for static query building where column names
873
- * are known at compile time. RLS policies work with dynamic column names at runtime.
877
+ * Uses type-safe wrappers from utils/type-utils.ts to encapsulate the boundary
878
+ * between runtime policy conditions and Kysely's compile-time type system.
874
879
  *
875
880
  * Type safety is maintained through:
876
881
  * 1. Policy conditions are validated during schema registration
877
882
  * 2. Column names come from policy definitions (developer-controlled)
878
883
  * 3. Values are type-checked by the policy condition functions
879
884
  *
880
- * This is the same pattern used in @kysera/repository/table-operations.ts
881
- *
882
885
  * @param qb - Query builder to modify
883
886
  * @param conditions - WHERE clause conditions
884
887
  * @param table - Table name (for qualified column names)
@@ -887,19 +890,24 @@ var SelectTransformer = class {
887
890
  applyConditions(qb, conditions, table) {
888
891
  let result = qb;
889
892
  for (const [column, value] of Object.entries(conditions)) {
890
- const qualifiedColumn = `${table}.${column}`;
893
+ const qualifiedColumn = createQualifiedColumn(table, column);
891
894
  if (value === null) {
892
- result = result.where(qualifiedColumn, "is", null);
895
+ result = applyWhereCondition(result, qualifiedColumn, "is", null);
893
896
  } else if (value === void 0) {
894
897
  continue;
895
898
  } else if (Array.isArray(value)) {
896
899
  if (value.length === 0) {
897
- result = result.where(sql`FALSE`);
900
+ result = applyWhereCondition(
901
+ result,
902
+ createRawCondition("FALSE"),
903
+ "=",
904
+ true
905
+ );
898
906
  } else {
899
- result = result.where(qualifiedColumn, "in", value);
907
+ result = applyWhereCondition(result, qualifiedColumn, "in", value);
900
908
  }
901
909
  } else {
902
- result = result.where(qualifiedColumn, "=", value);
910
+ result = applyWhereCondition(result, qualifiedColumn, "=", value);
903
911
  }
904
912
  }
905
913
  return result;
@@ -907,6 +915,7 @@ var SelectTransformer = class {
907
915
  };
908
916
 
909
917
  // src/transformer/mutation.ts
918
+ var DEFAULT_CHUNK_SIZE = 100;
910
919
  var MutationGuard = class {
911
920
  constructor(registry) {
912
921
  this.registry = registry;
@@ -1060,11 +1069,7 @@ var MutationGuard = class {
1060
1069
  async checkMutation(table, operation, row, data) {
1061
1070
  const ctx = rlsContext.getContextOrNull();
1062
1071
  if (!ctx) {
1063
- throw new RLSPolicyViolation(
1064
- operation,
1065
- table,
1066
- "No RLS context available"
1067
- );
1072
+ throw new RLSPolicyViolation(operation, table, "No RLS context available");
1068
1073
  }
1069
1074
  if (ctx.auth.isSystem) {
1070
1075
  return;
@@ -1078,11 +1083,7 @@ var MutationGuard = class {
1078
1083
  const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
1079
1084
  const result = await this.evaluatePolicy(deny2.evaluate, evalCtx, deny2.name);
1080
1085
  if (result) {
1081
- throw new RLSPolicyViolation(
1082
- operation,
1083
- table,
1084
- `Denied by policy: ${deny2.name}`
1085
- );
1086
+ throw new RLSPolicyViolation(operation, table, `Denied by policy: ${deny2.name}`);
1086
1087
  }
1087
1088
  }
1088
1089
  if ((operation === "create" || operation === "update") && data) {
@@ -1091,22 +1092,14 @@ var MutationGuard = class {
1091
1092
  const evalCtx = this.createEvalContext(ctx, table, operation, row, data);
1092
1093
  const result = await this.evaluatePolicy(validate2.evaluate, evalCtx, validate2.name);
1093
1094
  if (!result) {
1094
- throw new RLSPolicyViolation(
1095
- operation,
1096
- table,
1097
- `Validation failed: ${validate2.name}`
1098
- );
1095
+ throw new RLSPolicyViolation(operation, table, `Validation failed: ${validate2.name}`);
1099
1096
  }
1100
1097
  }
1101
1098
  }
1102
1099
  const allows = this.registry.getAllows(table, operation);
1103
1100
  const defaultDeny = this.registry.hasDefaultDeny(table);
1104
1101
  if (defaultDeny && allows.length === 0) {
1105
- throw new RLSPolicyViolation(
1106
- operation,
1107
- table,
1108
- "No allow policies defined (default deny)"
1109
- );
1102
+ throw new RLSPolicyViolation(operation, table, "No allow policies defined (default deny)");
1110
1103
  }
1111
1104
  if (allows.length > 0) {
1112
1105
  let allowed = false;
@@ -1119,11 +1112,7 @@ var MutationGuard = class {
1119
1112
  }
1120
1113
  }
1121
1114
  if (!allowed) {
1122
- throw new RLSPolicyViolation(
1123
- operation,
1124
- table,
1125
- "No allow policies matched"
1126
- );
1115
+ throw new RLSPolicyViolation(operation, table, "No allow policies matched");
1127
1116
  }
1128
1117
  }
1129
1118
  }
@@ -1165,11 +1154,12 @@ var MutationGuard = class {
1165
1154
  }
1166
1155
  }
1167
1156
  /**
1168
- * Filter an array of rows, keeping only accessible ones
1169
- * Useful for post-query filtering when query-level filtering is not possible
1157
+ * Filter rows based on read policies.
1158
+ * Uses parallel processing with chunking for performance.
1170
1159
  *
1171
1160
  * @param table - Table name
1172
- * @param rows - Array of rows to filter
1161
+ * @param rows - Rows to filter
1162
+ * @param chunkSize - Number of rows to process in parallel (default: 100)
1173
1163
  * @returns Filtered array containing only accessible rows
1174
1164
  *
1175
1165
  * @example
@@ -1177,34 +1167,63 @@ var MutationGuard = class {
1177
1167
  * const guard = new MutationGuard(registry);
1178
1168
  * const allPosts = await db.selectFrom('posts').selectAll().execute();
1179
1169
  * const accessiblePosts = await guard.filterRows('posts', allPosts);
1170
+ *
1171
+ * // With custom chunk size for large datasets
1172
+ * const accessiblePosts = await guard.filterRows('posts', allPosts, 50);
1180
1173
  * ```
1181
1174
  */
1182
- async filterRows(table, rows) {
1175
+ async filterRows(table, rows, chunkSize = DEFAULT_CHUNK_SIZE) {
1176
+ if (rows.length === 0) return [];
1183
1177
  const results = [];
1184
- for (const row of rows) {
1185
- if (await this.checkRead(table, row)) {
1186
- results.push(row);
1178
+ for (let i = 0; i < rows.length; i += chunkSize) {
1179
+ const chunk = rows.slice(i, i + chunkSize);
1180
+ const chunkResults = await Promise.all(
1181
+ chunk.map(async (row) => {
1182
+ const allowed = await this.checkRead(table, row);
1183
+ return allowed ? row : null;
1184
+ })
1185
+ );
1186
+ for (const row of chunkResults) {
1187
+ if (row !== null) {
1188
+ results.push(row);
1189
+ }
1187
1190
  }
1188
1191
  }
1189
1192
  return results;
1190
1193
  }
1191
1194
  };
1195
+
1196
+ // src/version.ts
1197
+ var RAW_VERSION = "__VERSION__";
1198
+ var VERSION = RAW_VERSION.startsWith("__") ? "0.0.0-dev" : RAW_VERSION;
1199
+ var RLSPluginOptionsSchema = z.object({
1200
+ excludeTables: z.array(z.string()).optional(),
1201
+ bypassRoles: z.array(z.string()).optional(),
1202
+ requireContext: z.boolean().optional(),
1203
+ allowUnfilteredQueries: z.boolean().optional(),
1204
+ auditDecisions: z.boolean().optional(),
1205
+ primaryKeyColumn: z.string().optional()
1206
+ });
1192
1207
  function rlsPlugin(options) {
1193
1208
  const {
1194
1209
  schema,
1195
- skipTables = [],
1210
+ excludeTables = [],
1196
1211
  bypassRoles = [],
1197
1212
  logger = silentLogger,
1198
- requireContext = false,
1213
+ requireContext = true,
1214
+ // SECURITY: Changed to true for secure-by-default (CRIT-2 fix)
1215
+ allowUnfilteredQueries = false,
1216
+ // SECURITY: Explicit opt-in for unfiltered queries
1199
1217
  auditDecisions = false,
1200
- onViolation
1218
+ onViolation,
1219
+ primaryKeyColumn = "id"
1201
1220
  } = options;
1202
1221
  let registry;
1203
1222
  let selectTransformer;
1204
1223
  let mutationGuard;
1205
1224
  return {
1206
1225
  name: "@kysera/rls",
1207
- version: "0.7.0",
1226
+ version: VERSION,
1208
1227
  // Run after soft-delete (priority 0), before audit
1209
1228
  priority: 50,
1210
1229
  // No dependencies by default
@@ -1212,10 +1231,10 @@ function rlsPlugin(options) {
1212
1231
  /**
1213
1232
  * Initialize plugin - compile policies
1214
1233
  */
1215
- async onInit(_executor) {
1234
+ onInit(_executor) {
1216
1235
  logger.info?.("[RLS] Initializing RLS plugin", {
1217
1236
  tables: Object.keys(schema).length,
1218
- skipTables: skipTables.length,
1237
+ excludeTables: excludeTables.length,
1219
1238
  bypassRoles: bypassRoles.length
1220
1239
  });
1221
1240
  registry = new PolicyRegistry(schema);
@@ -1224,6 +1243,13 @@ function rlsPlugin(options) {
1224
1243
  mutationGuard = new MutationGuard(registry);
1225
1244
  logger.info?.("[RLS] RLS plugin initialized successfully");
1226
1245
  },
1246
+ /**
1247
+ * Cleanup resources when executor is destroyed
1248
+ */
1249
+ async onDestroy() {
1250
+ registry.clear();
1251
+ logger.info?.("[RLS] RLS plugin destroyed, cleared policy registry");
1252
+ },
1227
1253
  /**
1228
1254
  * Intercept queries to apply RLS filtering
1229
1255
  *
@@ -1233,7 +1259,7 @@ function rlsPlugin(options) {
1233
1259
  */
1234
1260
  interceptQuery(qb, context) {
1235
1261
  const { operation, table, metadata } = context;
1236
- if (skipTables.includes(table)) {
1262
+ if (excludeTables.includes(table)) {
1237
1263
  logger.debug?.(`[RLS] Skipping RLS for excluded table: ${table}`);
1238
1264
  return qb;
1239
1265
  }
@@ -1244,9 +1270,29 @@ function rlsPlugin(options) {
1244
1270
  const ctx = rlsContext.getContextOrNull();
1245
1271
  if (!ctx) {
1246
1272
  if (requireContext) {
1247
- throw new RLSContextError("RLS context required but not found");
1273
+ throw new RLSContextError(
1274
+ `RLS context required but not found for ${operation} on ${table}. This prevents unfiltered database access. Either provide RLS context or set 'requireContext: false' with 'allowUnfilteredQueries: true' if intentional.`
1275
+ );
1276
+ }
1277
+ if (!allowUnfilteredQueries) {
1278
+ logger.warn?.(
1279
+ `[RLS] Missing context for ${operation} on ${table}. Queries will return empty results for security. Set 'allowUnfilteredQueries: true' to allow unfiltered access (not recommended).`
1280
+ );
1281
+ if (operation === "select") {
1282
+ return transformQueryBuilder(qb, operation, (selectQb) => {
1283
+ return applyWhereCondition(
1284
+ selectQb,
1285
+ createRawCondition("FALSE"),
1286
+ "=",
1287
+ true
1288
+ );
1289
+ });
1290
+ }
1291
+ return qb;
1248
1292
  }
1249
- logger.warn?.(`[RLS] No context for ${operation} on ${table}`);
1293
+ logger.warn?.(
1294
+ `[RLS] No context for ${operation} on ${table}. Allowing unfiltered query due to 'allowUnfilteredQueries: true'. This may expose sensitive data.`
1295
+ );
1250
1296
  return qb;
1251
1297
  }
1252
1298
  if (ctx.auth.isSystem) {
@@ -1259,7 +1305,12 @@ function rlsPlugin(options) {
1259
1305
  }
1260
1306
  if (operation === "select") {
1261
1307
  try {
1262
- const transformed = selectTransformer.transform(qb, table);
1308
+ const transformed = transformQueryBuilder(
1309
+ qb,
1310
+ operation,
1311
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1312
+ (selectQb) => selectTransformer.transform(selectQb, table)
1313
+ );
1263
1314
  if (auditDecisions) {
1264
1315
  logger.info?.("[RLS] Filter applied", {
1265
1316
  table,
@@ -1286,12 +1337,13 @@ function rlsPlugin(options) {
1286
1337
  * Also adds utility methods for bypassing RLS and checking access.
1287
1338
  */
1288
1339
  extendRepository(repo) {
1289
- const baseRepo = repo;
1290
- if (!("tableName" in baseRepo) || !("executor" in baseRepo)) {
1340
+ if (!isRepositoryLike(repo)) {
1291
1341
  return repo;
1292
1342
  }
1343
+ const baseRepo = repo;
1293
1344
  const table = baseRepo.tableName;
1294
- if (skipTables.includes(table)) {
1345
+ if (excludeTables.includes(table)) {
1346
+ logger.debug?.(`[RLS] Skipping repository extension for excluded table: ${table}`);
1295
1347
  return repo;
1296
1348
  }
1297
1349
  if (!registry.hasTable(table)) {
@@ -1304,7 +1356,7 @@ function rlsPlugin(options) {
1304
1356
  const originalDelete = baseRepo.delete?.bind(baseRepo);
1305
1357
  const originalFindById = baseRepo.findById?.bind(baseRepo);
1306
1358
  const rawDb = getRawDb(baseRepo.executor);
1307
- const hasRawDb = baseRepo.executor.__rawDb !== void 0;
1359
+ const hasRawDbInstance = hasRawDb(baseRepo.executor);
1308
1360
  const extendedRepo = {
1309
1361
  ...baseRepo,
1310
1362
  /**
@@ -1312,7 +1364,10 @@ function rlsPlugin(options) {
1312
1364
  */
1313
1365
  async create(data) {
1314
1366
  if (!originalCreate) {
1315
- throw new RLSError("Repository does not support create operation", RLSErrorCodes.RLS_POLICY_INVALID);
1367
+ throw new RLSError(
1368
+ "Repository does not support create operation",
1369
+ RLSErrorCodes.RLS_POLICY_INVALID
1370
+ );
1316
1371
  }
1317
1372
  const ctx = rlsContext.getContextOrNull();
1318
1373
  if (ctx && !ctx.auth.isSystem && !bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
@@ -1335,27 +1390,34 @@ function rlsPlugin(options) {
1335
1390
  throw error;
1336
1391
  }
1337
1392
  }
1338
- return originalCreate(data);
1393
+ return await originalCreate(data);
1339
1394
  },
1340
1395
  /**
1341
1396
  * Wrapped update with RLS check
1342
1397
  */
1343
1398
  async update(id, data) {
1344
1399
  if (!originalUpdate) {
1345
- throw new RLSError("Repository does not support update operation", RLSErrorCodes.RLS_POLICY_INVALID);
1400
+ throw new RLSError(
1401
+ "Repository does not support update operation",
1402
+ RLSErrorCodes.RLS_POLICY_INVALID
1403
+ );
1346
1404
  }
1347
1405
  const ctx = rlsContext.getContextOrNull();
1348
1406
  if (ctx && !ctx.auth.isSystem && !bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
1349
1407
  let existingRow;
1350
- if (hasRawDb) {
1351
- existingRow = await rawDb.selectFrom(table).selectAll().where("id", "=", id).executeTakeFirst();
1408
+ if (hasRawDbInstance) {
1409
+ const query = selectFromDynamicTable(rawDb, table);
1410
+ existingRow = await whereIdEquals(query, id, primaryKeyColumn).executeTakeFirst();
1352
1411
  } else if (originalFindById) {
1353
1412
  existingRow = await originalFindById(id);
1354
1413
  } else {
1355
- throw new RLSError("Repository does not support update operation", RLSErrorCodes.RLS_POLICY_INVALID);
1414
+ throw new RLSError(
1415
+ "Repository does not support update operation",
1416
+ RLSErrorCodes.RLS_POLICY_INVALID
1417
+ );
1356
1418
  }
1357
1419
  if (!existingRow) {
1358
- return originalUpdate(id, data);
1420
+ return await originalUpdate(id, data);
1359
1421
  }
1360
1422
  try {
1361
1423
  await mutationGuard.checkUpdate(
@@ -1381,27 +1443,34 @@ function rlsPlugin(options) {
1381
1443
  throw error;
1382
1444
  }
1383
1445
  }
1384
- return originalUpdate(id, data);
1446
+ return await originalUpdate(id, data);
1385
1447
  },
1386
1448
  /**
1387
1449
  * Wrapped delete with RLS check
1388
1450
  */
1389
1451
  async delete(id) {
1390
1452
  if (!originalDelete) {
1391
- throw new RLSError("Repository does not support delete operation", RLSErrorCodes.RLS_POLICY_INVALID);
1453
+ throw new RLSError(
1454
+ "Repository does not support delete operation",
1455
+ RLSErrorCodes.RLS_POLICY_INVALID
1456
+ );
1392
1457
  }
1393
1458
  const ctx = rlsContext.getContextOrNull();
1394
1459
  if (ctx && !ctx.auth.isSystem && !bypassRoles.some((role) => ctx.auth.roles.includes(role))) {
1395
1460
  let existingRow;
1396
- if (hasRawDb) {
1397
- existingRow = await rawDb.selectFrom(table).selectAll().where("id", "=", id).executeTakeFirst();
1461
+ if (hasRawDbInstance) {
1462
+ const query = selectFromDynamicTable(rawDb, table);
1463
+ existingRow = await whereIdEquals(query, id, primaryKeyColumn).executeTakeFirst();
1398
1464
  } else if (originalFindById) {
1399
1465
  existingRow = await originalFindById(id);
1400
1466
  } else {
1401
- throw new RLSError("Repository does not support delete operation", RLSErrorCodes.RLS_POLICY_INVALID);
1467
+ throw new RLSError(
1468
+ "Repository does not support delete operation",
1469
+ RLSErrorCodes.RLS_POLICY_INVALID
1470
+ );
1402
1471
  }
1403
1472
  if (!existingRow) {
1404
- return originalDelete(id);
1473
+ return await originalDelete(id);
1405
1474
  }
1406
1475
  try {
1407
1476
  await mutationGuard.checkDelete(table, existingRow);
@@ -1423,7 +1492,7 @@ function rlsPlugin(options) {
1423
1492
  throw error;
1424
1493
  }
1425
1494
  }
1426
- return originalDelete(id);
1495
+ return await originalDelete(id);
1427
1496
  },
1428
1497
  /**
1429
1498
  * Bypass RLS for specific operation
@@ -1438,7 +1507,7 @@ function rlsPlugin(options) {
1438
1507
  * ```
1439
1508
  */
1440
1509
  async withoutRLS(fn) {
1441
- return rlsContext.asSystemAsync(fn);
1510
+ return await rlsContext.asSystemAsync(fn);
1442
1511
  },
1443
1512
  /**
1444
1513
  * Check if current user can perform operation on a row
@@ -1508,7 +1577,18 @@ function createEvaluationContext(rlsCtx, options) {
1508
1577
  return ctx;
1509
1578
  }
1510
1579
  function isAsyncFunction(fn) {
1511
- return fn instanceof Function && fn.constructor.name === "AsyncFunction";
1580
+ if (!(fn instanceof Function)) {
1581
+ return false;
1582
+ }
1583
+ if (fn.constructor.name === "AsyncFunction") {
1584
+ return true;
1585
+ }
1586
+ try {
1587
+ const result = fn();
1588
+ return result instanceof Promise;
1589
+ } catch {
1590
+ return false;
1591
+ }
1512
1592
  }
1513
1593
  async function safeEvaluate(fn, defaultValue) {
1514
1594
  try {
@@ -1517,7 +1597,7 @@ async function safeEvaluate(fn, defaultValue) {
1517
1597
  return await result;
1518
1598
  }
1519
1599
  return result;
1520
- } catch (error) {
1600
+ } catch (_error) {
1521
1601
  return defaultValue;
1522
1602
  }
1523
1603
  }
@@ -1559,6 +1639,6 @@ function normalizeOperations(operation) {
1559
1639
  return [operation];
1560
1640
  }
1561
1641
 
1562
- 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 };
1642
+ export { PolicyRegistry, RLSContextError, RLSContextValidationError, RLSError, RLSErrorCodes, RLSPluginOptionsSchema, RLSPolicyEvaluationError, RLSPolicyViolation, RLSSchemaError, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
1563
1643
  //# sourceMappingURL=index.js.map
1564
1644
  //# sourceMappingURL=index.js.map