@kysera/rls 0.8.5 → 0.8.7

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.d.ts CHANGED
@@ -719,27 +719,13 @@ interface RLSPluginOptions<DB = unknown> {
719
719
  * Note: 'schema' and 'onViolation' are not included as they are complex runtime objects.
720
720
  */
721
721
  declare const RLSPluginOptionsSchema: z.ZodObject<{
722
- excludeTables: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
723
- bypassRoles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
722
+ excludeTables: z.ZodOptional<z.ZodArray<z.ZodString>>;
723
+ bypassRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
724
724
  requireContext: z.ZodOptional<z.ZodBoolean>;
725
725
  allowUnfilteredQueries: z.ZodOptional<z.ZodBoolean>;
726
726
  auditDecisions: z.ZodOptional<z.ZodBoolean>;
727
727
  primaryKeyColumn: z.ZodOptional<z.ZodString>;
728
- }, "strip", z.ZodTypeAny, {
729
- excludeTables?: string[] | undefined;
730
- bypassRoles?: string[] | undefined;
731
- requireContext?: boolean | undefined;
732
- allowUnfilteredQueries?: boolean | undefined;
733
- auditDecisions?: boolean | undefined;
734
- primaryKeyColumn?: string | undefined;
735
- }, {
736
- excludeTables?: string[] | undefined;
737
- bypassRoles?: string[] | undefined;
738
- requireContext?: boolean | undefined;
739
- allowUnfilteredQueries?: boolean | undefined;
740
- auditDecisions?: boolean | undefined;
741
- primaryKeyColumn?: string | undefined;
742
- }>;
728
+ }, z.core.$strip>;
743
729
  /**
744
730
  * Create RLS plugin for Kysera
745
731
  *
package/dist/index.js CHANGED
@@ -997,35 +997,70 @@ var MutationGuard = class {
997
997
  await this.checkMutation(table, "create", void 0, data);
998
998
  }
999
999
  /**
1000
- * Check if UPDATE operation is allowed
1000
+ * Check if an UPDATE operation is allowed by RLS policies.
1001
1001
  *
1002
- * @param table - Table name
1003
- * @param existingRow - Current row data
1004
- * @param data - Data being updated
1005
- * @throws RLSPolicyViolation if access is denied
1002
+ * IMPORTANT: To prevent TOCTOU race conditions, always call this method
1003
+ * within the same transaction as the actual update operation. The existingRow
1004
+ * should be fetched with SELECT FOR UPDATE within the transaction.
1005
+ *
1006
+ * Without proper transaction isolation, the existingRow could be modified by
1007
+ * a concurrent operation between the time it was fetched and the time this
1008
+ * check runs, leading to policy decisions based on stale data.
1009
+ *
1010
+ * @param table - Target table name
1011
+ * @param existingRow - Current row data (should be fetched within same transaction)
1012
+ * @param data - New data being applied
1013
+ * @throws RLSPolicyViolation if the operation is not allowed
1006
1014
  *
1007
1015
  * @example
1008
1016
  * ```typescript
1009
- * const guard = new MutationGuard(registry);
1010
- * const existingPost = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
1011
- * await guard.checkUpdate('posts', existingPost, { title: 'Updated' });
1017
+ * // RECOMMENDED: Use within a transaction with row locking
1018
+ * await db.transaction().execute(async (trx) => {
1019
+ * const existingPost = await trx
1020
+ * .selectFrom('posts')
1021
+ * .where('id', '=', 1)
1022
+ * .forUpdate()
1023
+ * .selectAll()
1024
+ * .executeTakeFirst();
1025
+ *
1026
+ * await guard.checkUpdate('posts', existingPost, { title: 'Updated' });
1027
+ * await trx.updateTable('posts').set({ title: 'Updated' }).where('id', '=', 1).execute();
1028
+ * });
1012
1029
  * ```
1013
1030
  */
1014
1031
  async checkUpdate(table, existingRow, data) {
1015
1032
  await this.checkMutation(table, "update", existingRow, data);
1016
1033
  }
1017
1034
  /**
1018
- * Check if DELETE operation is allowed
1035
+ * Check if a DELETE operation is allowed by RLS policies.
1019
1036
  *
1020
- * @param table - Table name
1021
- * @param existingRow - Row to be deleted
1022
- * @throws RLSPolicyViolation if access is denied
1037
+ * IMPORTANT: To prevent TOCTOU race conditions, always call this method
1038
+ * within the same transaction as the actual delete operation. The existingRow
1039
+ * should be fetched with SELECT FOR UPDATE within the transaction.
1040
+ *
1041
+ * Without proper transaction isolation, the existingRow could be modified by
1042
+ * a concurrent operation between the time it was fetched and the time this
1043
+ * check runs, leading to policy decisions based on stale data (e.g., a row's
1044
+ * ownership could change, allowing an unauthorized delete).
1045
+ *
1046
+ * @param table - Target table name
1047
+ * @param existingRow - Current row data (should be fetched within same transaction)
1048
+ * @throws RLSPolicyViolation if the operation is not allowed
1023
1049
  *
1024
1050
  * @example
1025
1051
  * ```typescript
1026
- * const guard = new MutationGuard(registry);
1027
- * const existingPost = await db.selectFrom('posts').where('id', '=', 1).selectAll().executeTakeFirst();
1028
- * await guard.checkDelete('posts', existingPost);
1052
+ * // RECOMMENDED: Use within a transaction with row locking
1053
+ * await db.transaction().execute(async (trx) => {
1054
+ * const existingPost = await trx
1055
+ * .selectFrom('posts')
1056
+ * .where('id', '=', 1)
1057
+ * .forUpdate()
1058
+ * .selectAll()
1059
+ * .executeTakeFirst();
1060
+ *
1061
+ * await guard.checkDelete('posts', existingPost);
1062
+ * await trx.deleteFrom('posts').where('id', '=', 1).execute();
1063
+ * });
1029
1064
  * ```
1030
1065
  */
1031
1066
  async checkDelete(table, existingRow) {
@@ -1454,7 +1489,12 @@ function rlsPlugin(options) {
1454
1489
  return await originalCreate(data);
1455
1490
  },
1456
1491
  /**
1457
- * Wrapped update with RLS check
1492
+ * Wrapped update with RLS check.
1493
+ *
1494
+ * WARNING (TOCTOU): The existing row is fetched before the policy check.
1495
+ * In concurrent environments, the row could be modified between fetch and
1496
+ * check. For safety, call this method within a transaction. The underlying
1497
+ * MutationGuard.checkUpdate documents this in detail.
1458
1498
  */
1459
1499
  async update(id, data) {
1460
1500
  if (!originalUpdate) {
@@ -1507,7 +1547,12 @@ function rlsPlugin(options) {
1507
1547
  return await originalUpdate(id, data);
1508
1548
  },
1509
1549
  /**
1510
- * Wrapped delete with RLS check
1550
+ * Wrapped delete with RLS check.
1551
+ *
1552
+ * WARNING (TOCTOU): The existing row is fetched before the policy check.
1553
+ * In concurrent environments, the row could be modified between fetch and
1554
+ * check. For safety, call this method within a transaction. The underlying
1555
+ * MutationGuard.checkDelete documents this in detail.
1511
1556
  */
1512
1557
  async delete(id) {
1513
1558
  if (!originalDelete) {