@kysera/rls 0.5.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 ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@kysera/rls",
3
+ "version": "0.5.1",
4
+ "description": "Row-Level Security plugin for Kysera ORM - declarative policies, query transformation, native RLS support",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./native": {
14
+ "types": "./dist/native/index.d.ts",
15
+ "import": "./dist/native/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
22
+ "peerDependencies": {
23
+ "kysely": ">=0.28.8"
24
+ },
25
+ "dependencies": {
26
+ "zod": "^4.1.13",
27
+ "@kysera/core": "0.5.1",
28
+ "@kysera/repository": "0.5.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/better-sqlite3": "^7.6.13",
32
+ "@types/node": "^24.10.1",
33
+ "@types/pg": "^8.15.6",
34
+ "@vitest/coverage-v8": "^4.0.15",
35
+ "better-sqlite3": "^12.5.0",
36
+ "kysely": "^0.28.8",
37
+ "mysql2": "^3.15.2",
38
+ "pg": "^8.16.3",
39
+ "tsup": "^8.5.1",
40
+ "typescript": "^5.9.3",
41
+ "vitest": "^4.0.15"
42
+ },
43
+ "keywords": [
44
+ "kysely",
45
+ "orm",
46
+ "database",
47
+ "typescript",
48
+ "sql",
49
+ "rls",
50
+ "row-level-security",
51
+ "authorization",
52
+ "access-control",
53
+ "postgres",
54
+ "mysql",
55
+ "sqlite"
56
+ ],
57
+ "author": "Kysera Team",
58
+ "license": "MIT",
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/kysera-dev/kysera.git",
62
+ "directory": "packages/kysera-rls"
63
+ },
64
+ "bugs": {
65
+ "url": "https://github.com/kysera-dev/kysera/issues"
66
+ },
67
+ "homepage": "https://github.com/kysera-dev/kysera#readme",
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "sideEffects": false,
72
+ "engines": {
73
+ "node": ">=20.0.0",
74
+ "bun": ">=1.0.0"
75
+ },
76
+ "scripts": {
77
+ "build": "tsup",
78
+ "dev": "tsup --watch",
79
+ "test": "vitest run",
80
+ "test:watch": "vitest watch",
81
+ "test:coverage": "vitest run --coverage",
82
+ "test:unit": "vitest run test/unit",
83
+ "test:integration": "vitest run test/integration",
84
+ "test:postgres": "TEST_POSTGRES=true vitest run test/integration",
85
+ "test:mysql": "TEST_MYSQL=true vitest run test/integration",
86
+ "test:all-dbs": "TEST_POSTGRES=true TEST_MYSQL=true vitest run test/integration",
87
+ "docker:up": "docker compose -f test/docker/docker-compose.test.yml up -d",
88
+ "docker:down": "docker compose -f test/docker/docker-compose.test.yml down -v",
89
+ "docker:logs": "docker compose -f test/docker/docker-compose.test.yml logs -f",
90
+ "typecheck": "tsc --noEmit",
91
+ "lint": "eslint ."
92
+ }
93
+ }
@@ -0,0 +1,9 @@
1
+ export { rlsStorage } from './storage.js';
2
+ export {
3
+ rlsContext,
4
+ createRLSContext,
5
+ withRLSContext,
6
+ withRLSContextAsync,
7
+ type CreateRLSContextOptions,
8
+ } from './manager.js';
9
+ export type { RLSContext, RLSAuthContext, RLSRequestContext } from './types.js';
@@ -0,0 +1,203 @@
1
+ import { rlsStorage } from './storage.js';
2
+ import type { RLSContext, RLSAuthContext, RLSRequestContext } from './types.js';
3
+ import { RLSContextError, RLSContextValidationError } from '../errors.js';
4
+
5
+ /**
6
+ * Options for creating RLS context
7
+ */
8
+ export interface CreateRLSContextOptions<TUser = unknown, TMeta = unknown> {
9
+ auth: RLSAuthContext<TUser>;
10
+ request?: Partial<RLSRequestContext>;
11
+ meta?: TMeta;
12
+ }
13
+
14
+ /**
15
+ * Create a new RLS context
16
+ */
17
+ export function createRLSContext<TUser = unknown, TMeta = unknown>(
18
+ options: CreateRLSContextOptions<TUser, TMeta>
19
+ ): RLSContext<TUser, TMeta> {
20
+ validateAuthContext(options.auth);
21
+
22
+ const context: RLSContext<TUser, TMeta> = {
23
+ auth: {
24
+ ...options.auth,
25
+ isSystem: options.auth.isSystem ?? false, // Default to false if not provided
26
+ },
27
+ timestamp: new Date(),
28
+ };
29
+
30
+ if (options.request) {
31
+ context.request = {
32
+ ...options.request,
33
+ timestamp: options.request.timestamp ?? new Date(),
34
+ } as RLSRequestContext;
35
+ }
36
+
37
+ if (options.meta !== undefined) {
38
+ context.meta = options.meta;
39
+ }
40
+
41
+ return context;
42
+ }
43
+
44
+ /**
45
+ * Validate auth context
46
+ */
47
+ function validateAuthContext(auth: RLSAuthContext): void {
48
+ if (auth.userId === undefined || auth.userId === null) {
49
+ throw new RLSContextValidationError('userId is required in auth context', 'userId');
50
+ }
51
+
52
+ if (!Array.isArray(auth.roles)) {
53
+ throw new RLSContextValidationError('roles must be an array', 'roles');
54
+ }
55
+ }
56
+
57
+ /**
58
+ * RLS Context Manager
59
+ * Manages RLS context using AsyncLocalStorage for automatic propagation
60
+ */
61
+ class RLSContextManager {
62
+ /**
63
+ * Run a synchronous function within an RLS context
64
+ */
65
+ run<T>(context: RLSContext, fn: () => T): T {
66
+ return rlsStorage.run(context, fn);
67
+ }
68
+
69
+ /**
70
+ * Run an async function within an RLS context
71
+ */
72
+ async runAsync<T>(context: RLSContext, fn: () => Promise<T>): Promise<T> {
73
+ return rlsStorage.run(context, fn);
74
+ }
75
+
76
+ /**
77
+ * Get current RLS context
78
+ * @throws RLSContextError if no context is set
79
+ */
80
+ getContext(): RLSContext {
81
+ const ctx = rlsStorage.getStore();
82
+ if (!ctx) {
83
+ throw new RLSContextError();
84
+ }
85
+ return ctx;
86
+ }
87
+
88
+ /**
89
+ * Get current RLS context or null if not set
90
+ */
91
+ getContextOrNull(): RLSContext | null {
92
+ return rlsStorage.getStore() ?? null;
93
+ }
94
+
95
+ /**
96
+ * Check if running within RLS context
97
+ */
98
+ hasContext(): boolean {
99
+ return rlsStorage.getStore() !== undefined;
100
+ }
101
+
102
+ /**
103
+ * Get current auth context
104
+ * @throws RLSContextError if no context is set
105
+ */
106
+ getAuth(): RLSAuthContext {
107
+ return this.getContext().auth;
108
+ }
109
+
110
+ /**
111
+ * Get current user ID
112
+ * @throws RLSContextError if no context is set
113
+ */
114
+ getUserId(): string | number {
115
+ return this.getAuth().userId;
116
+ }
117
+
118
+ /**
119
+ * Get current tenant ID
120
+ * @throws RLSContextError if no context is set
121
+ */
122
+ getTenantId(): string | number | undefined {
123
+ return this.getAuth().tenantId;
124
+ }
125
+
126
+ /**
127
+ * Check if current user has a specific role
128
+ */
129
+ hasRole(role: string): boolean {
130
+ const ctx = this.getContextOrNull();
131
+ return ctx?.auth.roles.includes(role) ?? false;
132
+ }
133
+
134
+ /**
135
+ * Check if current user has a specific permission
136
+ */
137
+ hasPermission(permission: string): boolean {
138
+ const ctx = this.getContextOrNull();
139
+ return ctx?.auth.permissions?.includes(permission) ?? false;
140
+ }
141
+
142
+ /**
143
+ * Check if current context is a system context (bypasses RLS)
144
+ */
145
+ isSystem(): boolean {
146
+ const ctx = this.getContextOrNull();
147
+ return ctx?.auth.isSystem ?? false;
148
+ }
149
+
150
+ /**
151
+ * Create a system context for operations that should bypass RLS
152
+ */
153
+ asSystem<T>(fn: () => T): T {
154
+ const currentCtx = this.getContextOrNull();
155
+ if (!currentCtx) {
156
+ throw new RLSContextError('Cannot create system context without existing context');
157
+ }
158
+
159
+ const systemCtx: RLSContext = {
160
+ ...currentCtx,
161
+ auth: { ...currentCtx.auth, isSystem: true },
162
+ };
163
+
164
+ return this.run(systemCtx, fn);
165
+ }
166
+
167
+ /**
168
+ * Create a system context for async operations
169
+ */
170
+ async asSystemAsync<T>(fn: () => Promise<T>): Promise<T> {
171
+ const currentCtx = this.getContextOrNull();
172
+ if (!currentCtx) {
173
+ throw new RLSContextError('Cannot create system context without existing context');
174
+ }
175
+
176
+ const systemCtx: RLSContext = {
177
+ ...currentCtx,
178
+ auth: { ...currentCtx.auth, isSystem: true },
179
+ };
180
+
181
+ return this.runAsync(systemCtx, fn);
182
+ }
183
+ }
184
+
185
+ // Export singleton instance
186
+ export const rlsContext = new RLSContextManager();
187
+
188
+ /**
189
+ * Convenience function to run code within RLS context
190
+ */
191
+ export function withRLSContext<T>(context: RLSContext, fn: () => T): T {
192
+ return rlsContext.run(context, fn);
193
+ }
194
+
195
+ /**
196
+ * Convenience function to run async code within RLS context
197
+ */
198
+ export async function withRLSContextAsync<T>(
199
+ context: RLSContext,
200
+ fn: () => Promise<T>
201
+ ): Promise<T> {
202
+ return rlsContext.runAsync(context, fn);
203
+ }
@@ -0,0 +1,8 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { RLSContext } from './types.js';
3
+
4
+ /**
5
+ * AsyncLocalStorage instance for RLS context
6
+ * Provides automatic context propagation across async boundaries
7
+ */
8
+ export const rlsStorage = new AsyncLocalStorage<RLSContext>();
@@ -0,0 +1,5 @@
1
+ export type {
2
+ RLSContext,
3
+ RLSAuthContext,
4
+ RLSRequestContext,
5
+ } from '../policy/types.js';
package/src/errors.ts ADDED
@@ -0,0 +1,280 @@
1
+ /**
2
+ * RLS Error Classes
3
+ *
4
+ * This module provides specialized error classes for Row-Level Security operations.
5
+ * All errors extend the base RLSError class and use unified error codes from @kysera/core
6
+ * for consistency across the Kysera ecosystem.
7
+ *
8
+ * @module @kysera/rls/errors
9
+ */
10
+
11
+ import type { ErrorCode } from '@kysera/core';
12
+
13
+ // ============================================================================
14
+ // RLS Error Codes
15
+ // ============================================================================
16
+
17
+ /**
18
+ * RLS-specific error codes
19
+ *
20
+ * These codes extend the unified error codes from @kysera/core with
21
+ * RLS-specific error conditions.
22
+ */
23
+ export const RLSErrorCodes = {
24
+ /** RLS context is missing or not set */
25
+ RLS_CONTEXT_MISSING: 'RLS_CONTEXT_MISSING' as ErrorCode,
26
+ /** RLS policy violation occurred */
27
+ RLS_POLICY_VIOLATION: 'RLS_POLICY_VIOLATION' as ErrorCode,
28
+ /** RLS policy definition is invalid */
29
+ RLS_POLICY_INVALID: 'RLS_POLICY_INVALID' as ErrorCode,
30
+ /** RLS schema definition is invalid */
31
+ RLS_SCHEMA_INVALID: 'RLS_SCHEMA_INVALID' as ErrorCode,
32
+ /** RLS context validation failed */
33
+ RLS_CONTEXT_INVALID: 'RLS_CONTEXT_INVALID' as ErrorCode,
34
+ } as const;
35
+
36
+ /**
37
+ * Type for RLS error codes
38
+ */
39
+ export type RLSErrorCode = typeof RLSErrorCodes[keyof typeof RLSErrorCodes];
40
+
41
+ // ============================================================================
42
+ // Base RLS Error
43
+ // ============================================================================
44
+
45
+ /**
46
+ * Base class for all RLS-related errors
47
+ *
48
+ * Provides common error functionality including error codes and JSON serialization.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * throw new RLSError('Something went wrong', RLSErrorCodes.RLS_POLICY_INVALID);
53
+ * ```
54
+ */
55
+ export class RLSError extends Error {
56
+ public readonly code: RLSErrorCode;
57
+
58
+ /**
59
+ * Creates a new RLS error
60
+ *
61
+ * @param message - Error message
62
+ * @param code - RLS error code
63
+ */
64
+ constructor(message: string, code: RLSErrorCode) {
65
+ super(message);
66
+ this.name = 'RLSError';
67
+ this.code = code;
68
+ }
69
+
70
+ /**
71
+ * Serializes the error to JSON
72
+ *
73
+ * @returns JSON representation of the error
74
+ */
75
+ toJSON(): Record<string, unknown> {
76
+ return {
77
+ name: this.name,
78
+ message: this.message,
79
+ code: this.code,
80
+ };
81
+ }
82
+ }
83
+
84
+ // ============================================================================
85
+ // Context Errors
86
+ // ============================================================================
87
+
88
+ /**
89
+ * Error thrown when RLS context is missing
90
+ *
91
+ * This error occurs when an operation requiring RLS context is executed
92
+ * outside of a context scope (i.e., without calling withRLSContext()).
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // This will throw RLSContextError
97
+ * const result = await db.selectFrom('posts').execute();
98
+ *
99
+ * // Correct usage with context
100
+ * await withRLSContext(rlsContext, async () => {
101
+ * const result = await db.selectFrom('posts').execute();
102
+ * });
103
+ * ```
104
+ */
105
+ export class RLSContextError extends RLSError {
106
+ /**
107
+ * Creates a new RLS context error
108
+ *
109
+ * @param message - Error message (defaults to standard message)
110
+ */
111
+ constructor(message: string = 'No RLS context found. Ensure code runs within withRLSContext()') {
112
+ super(message, RLSErrorCodes.RLS_CONTEXT_MISSING);
113
+ this.name = 'RLSContextError';
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Error thrown when RLS context validation fails
119
+ *
120
+ * This error occurs when the provided RLS context is invalid or missing
121
+ * required fields.
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * // Missing required userId field
126
+ * const invalidContext = {
127
+ * auth: {
128
+ * roles: ['user']
129
+ * // userId is missing!
130
+ * },
131
+ * timestamp: new Date()
132
+ * };
133
+ *
134
+ * // This will throw RLSContextValidationError
135
+ * validateRLSContext(invalidContext);
136
+ * ```
137
+ */
138
+ export class RLSContextValidationError extends RLSError {
139
+ public readonly field: string;
140
+
141
+ /**
142
+ * Creates a new context validation error
143
+ *
144
+ * @param message - Error message
145
+ * @param field - Field that failed validation
146
+ */
147
+ constructor(message: string, field: string) {
148
+ super(message, RLSErrorCodes.RLS_CONTEXT_INVALID);
149
+ this.name = 'RLSContextValidationError';
150
+ this.field = field;
151
+ }
152
+
153
+ override toJSON(): Record<string, unknown> {
154
+ return {
155
+ ...super.toJSON(),
156
+ field: this.field,
157
+ };
158
+ }
159
+ }
160
+
161
+ // ============================================================================
162
+ // Policy Errors
163
+ // ============================================================================
164
+
165
+ /**
166
+ * Error thrown when an RLS policy violation occurs
167
+ *
168
+ * This error is thrown when a database operation is denied by RLS policies.
169
+ * It provides detailed information about the violation including the operation,
170
+ * table, and reason for denial.
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * // User tries to update a post they don't own
175
+ * throw new RLSPolicyViolation(
176
+ * 'update',
177
+ * 'posts',
178
+ * 'User does not own this post',
179
+ * 'ownership_policy'
180
+ * );
181
+ * ```
182
+ */
183
+ export class RLSPolicyViolation extends RLSError {
184
+ public readonly operation: string;
185
+ public readonly table: string;
186
+ public readonly reason: string;
187
+ public readonly policyName?: string;
188
+
189
+ /**
190
+ * Creates a new policy violation error
191
+ *
192
+ * @param operation - Database operation that was denied (read, create, update, delete)
193
+ * @param table - Table name where violation occurred
194
+ * @param reason - Reason for the policy violation
195
+ * @param policyName - Name of the policy that denied access (optional)
196
+ */
197
+ constructor(
198
+ operation: string,
199
+ table: string,
200
+ reason: string,
201
+ policyName?: string
202
+ ) {
203
+ super(
204
+ `RLS policy violation: ${operation} on ${table} - ${reason}`,
205
+ RLSErrorCodes.RLS_POLICY_VIOLATION
206
+ );
207
+ this.name = 'RLSPolicyViolation';
208
+ this.operation = operation;
209
+ this.table = table;
210
+ this.reason = reason;
211
+ if (policyName !== undefined) {
212
+ this.policyName = policyName;
213
+ }
214
+ }
215
+
216
+ override toJSON(): Record<string, unknown> {
217
+ const json: Record<string, unknown> = {
218
+ ...super.toJSON(),
219
+ operation: this.operation,
220
+ table: this.table,
221
+ reason: this.reason,
222
+ };
223
+ if (this.policyName !== undefined) {
224
+ json['policyName'] = this.policyName;
225
+ }
226
+ return json;
227
+ }
228
+ }
229
+
230
+ // ============================================================================
231
+ // Schema Errors
232
+ // ============================================================================
233
+
234
+ /**
235
+ * Error thrown when RLS schema validation fails
236
+ *
237
+ * This error occurs when the RLS schema definition is invalid or contains
238
+ * configuration errors.
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * // Invalid policy definition
243
+ * const invalidSchema = {
244
+ * posts: {
245
+ * policies: [
246
+ * {
247
+ * type: 'invalid-type', // Invalid policy type!
248
+ * operation: 'read',
249
+ * condition: (ctx) => true
250
+ * }
251
+ * ]
252
+ * }
253
+ * };
254
+ *
255
+ * // This will throw RLSSchemaError
256
+ * validateRLSSchema(invalidSchema);
257
+ * ```
258
+ */
259
+ export class RLSSchemaError extends RLSError {
260
+ public readonly details: Record<string, unknown>;
261
+
262
+ /**
263
+ * Creates a new schema validation error
264
+ *
265
+ * @param message - Error message
266
+ * @param details - Additional details about the validation failure
267
+ */
268
+ constructor(message: string, details: Record<string, unknown> = {}) {
269
+ super(message, RLSErrorCodes.RLS_SCHEMA_INVALID);
270
+ this.name = 'RLSSchemaError';
271
+ this.details = details;
272
+ }
273
+
274
+ override toJSON(): Record<string, unknown> {
275
+ return {
276
+ ...super.toJSON(),
277
+ details: this.details,
278
+ };
279
+ }
280
+ }
package/src/index.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @kysera/rls - Row-Level Security Plugin for Kysera ORM
3
+ *
4
+ * Provides declarative policy definition, automatic query transformation,
5
+ * and optional native PostgreSQL RLS generation.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ // ============================================================================
11
+ // Policy Definition
12
+ // ============================================================================
13
+
14
+ // Schema definition
15
+ export { defineRLSSchema, mergeRLSSchemas } from './policy/schema.js';
16
+
17
+ // Policy builders
18
+ export { allow, deny, filter, validate, type PolicyOptions } from './policy/builder.js';
19
+
20
+ // Policy registry (for advanced use cases)
21
+ export { PolicyRegistry } from './policy/registry.js';
22
+
23
+ // ============================================================================
24
+ // Plugin
25
+ // ============================================================================
26
+
27
+ export { rlsPlugin } from './plugin.js';
28
+ export type { RLSPluginOptions } from './plugin.js';
29
+
30
+ // ============================================================================
31
+ // Context Management
32
+ // ============================================================================
33
+
34
+ export {
35
+ rlsContext,
36
+ createRLSContext,
37
+ withRLSContext,
38
+ withRLSContextAsync,
39
+ type CreateRLSContextOptions,
40
+ } from './context/index.js';
41
+
42
+ // ============================================================================
43
+ // Types
44
+ // ============================================================================
45
+
46
+ export type {
47
+ // Core types
48
+ Operation,
49
+ PolicyType,
50
+ PolicyDefinition,
51
+ PolicyCondition,
52
+ FilterCondition,
53
+ PolicyHints,
54
+
55
+ // Schema types
56
+ RLSSchema,
57
+ TableRLSConfig,
58
+
59
+ // Context types
60
+ RLSContext,
61
+ RLSAuthContext,
62
+ RLSRequestContext,
63
+
64
+ // Evaluation types
65
+ PolicyEvaluationContext,
66
+ CompiledPolicy,
67
+ CompiledFilterPolicy,
68
+ } from './policy/types.js';
69
+
70
+ // ============================================================================
71
+ // Errors
72
+ // ============================================================================
73
+
74
+ export {
75
+ RLSError,
76
+ RLSContextError,
77
+ RLSPolicyViolation,
78
+ RLSSchemaError,
79
+ RLSContextValidationError,
80
+ RLSErrorCodes,
81
+ type RLSErrorCode,
82
+ } from './errors.js';
83
+
84
+ // ============================================================================
85
+ // Utilities
86
+ // ============================================================================
87
+
88
+ export {
89
+ createEvaluationContext,
90
+ normalizeOperations,
91
+ isAsyncFunction,
92
+ safeEvaluate,
93
+ deepMerge,
94
+ hashString,
95
+ } from './utils/index.js';