@kysera/rls 0.6.1 → 0.7.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kysera/rls",
3
- "version": "0.6.1",
4
- "description": "Row-Level Security plugin for Kysera ORM - declarative policies, query transformation, native RLS support",
3
+ "version": "0.7.1",
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",
7
7
  "types": "./dist/index.d.ts",
@@ -24,8 +24,9 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "zod": "^4.1.13",
27
- "@kysera/core": "0.6.1",
28
- "@kysera/repository": "0.6.1"
27
+ "@kysera/core": "0.7.1",
28
+ "@kysera/executor": "0.7.1",
29
+ "@kysera/repository": "0.7.1"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/better-sqlite3": "^7.6.13",
@@ -33,7 +34,7 @@
33
34
  "@types/pg": "^8.15.6",
34
35
  "@vitest/coverage-v8": "^4.0.15",
35
36
  "better-sqlite3": "^12.5.0",
36
- "kysely": "^0.28.8",
37
+ "kysely": "^0.28.9",
37
38
  "mysql2": "^3.15.2",
38
39
  "pg": "^8.16.3",
39
40
  "tsup": "^8.5.1",
@@ -42,7 +43,7 @@
42
43
  },
43
44
  "keywords": [
44
45
  "kysely",
45
- "orm",
46
+ "data-access",
46
47
  "database",
47
48
  "typescript",
48
49
  "sql",
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @kysera/rls - Row-Level Security Plugin for Kysera ORM
2
+ * @kysera/rls - Row-Level Security Plugin for Kysera
3
3
  *
4
4
  * Provides declarative policy definition, automatic query transformation,
5
5
  * and optional native PostgreSQL RLS generation.
package/src/plugin.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  * @module @kysera/rls
11
11
  */
12
12
 
13
- import type { Plugin, QueryBuilderContext, AnyQueryBuilder } from '@kysera/repository';
13
+ import type { Plugin, QueryBuilderContext } from '@kysera/executor';
14
+ import { getRawDb } from '@kysera/executor';
14
15
  import type { Kysely } from 'kysely';
15
16
  import type { RLSSchema, Operation } from './policy/types.js';
16
17
  import { PolicyRegistry } from './policy/registry.js';
@@ -85,7 +86,7 @@ interface BaseRepository {
85
86
  * },
86
87
  * });
87
88
  *
88
- * // Create ORM with RLS plugin
89
+ * // Create repository with RLS plugin
89
90
  * const orm = await createORM(db, [
90
91
  * rlsPlugin({ schema }),
91
92
  * ]);
@@ -124,7 +125,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
124
125
 
125
126
  return {
126
127
  name: '@kysera/rls',
127
- version: '0.5.1',
128
+ version: '0.7.0',
128
129
 
129
130
  // Run after soft-delete (priority 0), before audit
130
131
  priority: 50,
@@ -160,7 +161,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
160
161
  * it applies filter policies as WHERE conditions. For mutations, it marks
161
162
  * that RLS validation is required (performed in extendRepository).
162
163
  */
163
- interceptQuery<QB extends AnyQueryBuilder>(
164
+ interceptQuery<QB>(
164
165
  qb: QB,
165
166
  context: QueryBuilderContext
166
167
  ): QB {
@@ -265,6 +266,11 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
265
266
  const originalDelete = baseRepo.delete?.bind(baseRepo);
266
267
  const originalFindById = baseRepo.findById?.bind(baseRepo);
267
268
 
269
+ // Get raw db for internal queries that need to bypass RLS
270
+ // If executor doesn't have __rawDb (e.g., in tests), we'll use originalFindById
271
+ const rawDb = getRawDb(baseRepo.executor);
272
+ const hasRawDb = (baseRepo.executor as any).__rawDb !== undefined;
273
+
268
274
  const extendedRepo = {
269
275
  ...baseRepo,
270
276
 
@@ -309,7 +315,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
309
315
  * Wrapped update with RLS check
310
316
  */
311
317
  async update(id: unknown, data: unknown): Promise<unknown> {
312
- if (!originalUpdate || !originalFindById) {
318
+ if (!originalUpdate) {
313
319
  throw new RLSError('Repository does not support update operation', RLSErrorCodes.RLS_POLICY_INVALID);
314
320
  }
315
321
 
@@ -318,7 +324,23 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
318
324
  if (ctx && !ctx.auth.isSystem &&
319
325
  !bypassRoles.some(role => ctx.auth.roles.includes(role))) {
320
326
  // Fetch existing row for policy evaluation
321
- const existingRow = await originalFindById(id);
327
+ // Use raw db if available to bypass RLS filtering and prevent self-interception
328
+ let existingRow: unknown;
329
+
330
+ if (hasRawDb) {
331
+ // Use raw db to bypass RLS filtering
332
+ existingRow = await rawDb
333
+ .selectFrom(table as any)
334
+ .selectAll()
335
+ .where('id' as any, '=', id as any)
336
+ .executeTakeFirst();
337
+ } else if (originalFindById) {
338
+ // Fallback to originalFindById for tests/mocks
339
+ existingRow = await originalFindById(id);
340
+ } else {
341
+ throw new RLSError('Repository does not support update operation', RLSErrorCodes.RLS_POLICY_INVALID);
342
+ }
343
+
322
344
  if (!existingRow) {
323
345
  // Let the original method handle not found
324
346
  return originalUpdate(id, data);
@@ -357,7 +379,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
357
379
  * Wrapped delete with RLS check
358
380
  */
359
381
  async delete(id: unknown): Promise<unknown> {
360
- if (!originalDelete || !originalFindById) {
382
+ if (!originalDelete) {
361
383
  throw new RLSError('Repository does not support delete operation', RLSErrorCodes.RLS_POLICY_INVALID);
362
384
  }
363
385
 
@@ -366,7 +388,23 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
366
388
  if (ctx && !ctx.auth.isSystem &&
367
389
  !bypassRoles.some(role => ctx.auth.roles.includes(role))) {
368
390
  // Fetch existing row for policy evaluation
369
- const existingRow = await originalFindById(id);
391
+ // Use raw db if available to bypass RLS filtering and prevent self-interception
392
+ let existingRow: unknown;
393
+
394
+ if (hasRawDb) {
395
+ // Use raw db to bypass RLS filtering
396
+ existingRow = await rawDb
397
+ .selectFrom(table as any)
398
+ .selectAll()
399
+ .where('id' as any, '=', id as any)
400
+ .executeTakeFirst();
401
+ } else if (originalFindById) {
402
+ // Fallback to originalFindById for tests/mocks
403
+ existingRow = await originalFindById(id);
404
+ } else {
405
+ throw new RLSError('Repository does not support delete operation', RLSErrorCodes.RLS_POLICY_INVALID);
406
+ }
407
+
370
408
  if (!existingRow) {
371
409
  // Let the original method handle not found
372
410
  return originalDelete(id);