@kysera/rls 0.8.4 → 0.8.6
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 +3 -17
- package/dist/index.js +62 -17
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/plugin.ts +12 -2
- package/src/transformer/mutation.ts +50 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kysera/rls",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "Row-Level Security plugin for Kysely - declarative policies, query transformation, native RLS support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
],
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"kysely": ">=0.28.8",
|
|
24
|
-
"zod": "
|
|
25
|
-
"@kysera/
|
|
26
|
-
"@kysera/
|
|
24
|
+
"zod": "^4.3.6",
|
|
25
|
+
"@kysera/executor": "0.8.6",
|
|
26
|
+
"@kysera/repository": "0.8.6"
|
|
27
27
|
},
|
|
28
28
|
"peerDependenciesMeta": {
|
|
29
29
|
"@kysera/executor": {
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"optional": true
|
|
34
34
|
},
|
|
35
35
|
"zod": {
|
|
36
|
-
"optional":
|
|
36
|
+
"optional": true
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@kysera/core": "0.8.
|
|
40
|
+
"@kysera/core": "0.8.6"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"tsup": "^8.5.1",
|
|
52
52
|
"typescript": "^5.9.3",
|
|
53
53
|
"vitest": "^4.0.16",
|
|
54
|
-
"zod": "^3.
|
|
55
|
-
"@kysera/
|
|
56
|
-
"@kysera/
|
|
54
|
+
"zod": "^4.3.6",
|
|
55
|
+
"@kysera/executor": "0.8.6",
|
|
56
|
+
"@kysera/repository": "0.8.6"
|
|
57
57
|
},
|
|
58
58
|
"keywords": [
|
|
59
59
|
"kysely",
|
package/src/plugin.ts
CHANGED
|
@@ -429,7 +429,12 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
429
429
|
},
|
|
430
430
|
|
|
431
431
|
/**
|
|
432
|
-
* Wrapped update with RLS check
|
|
432
|
+
* Wrapped update with RLS check.
|
|
433
|
+
*
|
|
434
|
+
* WARNING (TOCTOU): The existing row is fetched before the policy check.
|
|
435
|
+
* In concurrent environments, the row could be modified between fetch and
|
|
436
|
+
* check. For safety, call this method within a transaction. The underlying
|
|
437
|
+
* MutationGuard.checkUpdate documents this in detail.
|
|
433
438
|
*/
|
|
434
439
|
async update(id: unknown, data: unknown): Promise<unknown> {
|
|
435
440
|
if (!originalUpdate) {
|
|
@@ -499,7 +504,12 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
|
|
|
499
504
|
},
|
|
500
505
|
|
|
501
506
|
/**
|
|
502
|
-
* Wrapped delete with RLS check
|
|
507
|
+
* Wrapped delete with RLS check.
|
|
508
|
+
*
|
|
509
|
+
* WARNING (TOCTOU): The existing row is fetched before the policy check.
|
|
510
|
+
* In concurrent environments, the row could be modified between fetch and
|
|
511
|
+
* check. For safety, call this method within a transaction. The underlying
|
|
512
|
+
* MutationGuard.checkDelete documents this in detail.
|
|
503
513
|
*/
|
|
504
514
|
async delete(id: unknown): Promise<unknown> {
|
|
505
515
|
if (!originalDelete) {
|
|
@@ -39,18 +39,35 @@ export class MutationGuard<DB = unknown> {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Check if UPDATE operation is allowed
|
|
42
|
+
* Check if an UPDATE operation is allowed by RLS policies.
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* IMPORTANT: To prevent TOCTOU race conditions, always call this method
|
|
45
|
+
* within the same transaction as the actual update operation. The existingRow
|
|
46
|
+
* should be fetched with SELECT FOR UPDATE within the transaction.
|
|
47
|
+
*
|
|
48
|
+
* Without proper transaction isolation, the existingRow could be modified by
|
|
49
|
+
* a concurrent operation between the time it was fetched and the time this
|
|
50
|
+
* check runs, leading to policy decisions based on stale data.
|
|
51
|
+
*
|
|
52
|
+
* @param table - Target table name
|
|
53
|
+
* @param existingRow - Current row data (should be fetched within same transaction)
|
|
54
|
+
* @param data - New data being applied
|
|
55
|
+
* @throws RLSPolicyViolation if the operation is not allowed
|
|
48
56
|
*
|
|
49
57
|
* @example
|
|
50
58
|
* ```typescript
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
59
|
+
* // RECOMMENDED: Use within a transaction with row locking
|
|
60
|
+
* await db.transaction().execute(async (trx) => {
|
|
61
|
+
* const existingPost = await trx
|
|
62
|
+
* .selectFrom('posts')
|
|
63
|
+
* .where('id', '=', 1)
|
|
64
|
+
* .forUpdate()
|
|
65
|
+
* .selectAll()
|
|
66
|
+
* .executeTakeFirst();
|
|
67
|
+
*
|
|
68
|
+
* await guard.checkUpdate('posts', existingPost, { title: 'Updated' });
|
|
69
|
+
* await trx.updateTable('posts').set({ title: 'Updated' }).where('id', '=', 1).execute();
|
|
70
|
+
* });
|
|
54
71
|
* ```
|
|
55
72
|
*/
|
|
56
73
|
async checkUpdate(
|
|
@@ -62,17 +79,35 @@ export class MutationGuard<DB = unknown> {
|
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
/**
|
|
65
|
-
* Check if DELETE operation is allowed
|
|
82
|
+
* Check if a DELETE operation is allowed by RLS policies.
|
|
66
83
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
84
|
+
* IMPORTANT: To prevent TOCTOU race conditions, always call this method
|
|
85
|
+
* within the same transaction as the actual delete operation. The existingRow
|
|
86
|
+
* should be fetched with SELECT FOR UPDATE within the transaction.
|
|
87
|
+
*
|
|
88
|
+
* Without proper transaction isolation, the existingRow could be modified by
|
|
89
|
+
* a concurrent operation between the time it was fetched and the time this
|
|
90
|
+
* check runs, leading to policy decisions based on stale data (e.g., a row's
|
|
91
|
+
* ownership could change, allowing an unauthorized delete).
|
|
92
|
+
*
|
|
93
|
+
* @param table - Target table name
|
|
94
|
+
* @param existingRow - Current row data (should be fetched within same transaction)
|
|
95
|
+
* @throws RLSPolicyViolation if the operation is not allowed
|
|
70
96
|
*
|
|
71
97
|
* @example
|
|
72
98
|
* ```typescript
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* await
|
|
99
|
+
* // RECOMMENDED: Use within a transaction with row locking
|
|
100
|
+
* await db.transaction().execute(async (trx) => {
|
|
101
|
+
* const existingPost = await trx
|
|
102
|
+
* .selectFrom('posts')
|
|
103
|
+
* .where('id', '=', 1)
|
|
104
|
+
* .forUpdate()
|
|
105
|
+
* .selectAll()
|
|
106
|
+
* .executeTakeFirst();
|
|
107
|
+
*
|
|
108
|
+
* await guard.checkDelete('posts', existingPost);
|
|
109
|
+
* await trx.deleteFrom('posts').where('id', '=', 1).execute();
|
|
110
|
+
* });
|
|
76
111
|
* ```
|
|
77
112
|
*/
|
|
78
113
|
async checkDelete(table: string, existingRow: Record<string, unknown>): Promise<void> {
|