@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/LICENSE +21 -0
- package/README.md +1341 -0
- package/dist/index.d.ts +705 -0
- package/dist/index.js +1471 -0
- package/dist/index.js.map +1 -0
- package/dist/native/index.d.ts +91 -0
- package/dist/native/index.js +253 -0
- package/dist/native/index.js.map +1 -0
- package/dist/types-Dtg6Lt1k.d.ts +633 -0
- package/package.json +93 -0
- package/src/context/index.ts +9 -0
- package/src/context/manager.ts +203 -0
- package/src/context/storage.ts +8 -0
- package/src/context/types.ts +5 -0
- package/src/errors.ts +280 -0
- package/src/index.ts +95 -0
- package/src/native/README.md +315 -0
- package/src/native/index.ts +11 -0
- package/src/native/migration.ts +92 -0
- package/src/native/postgres.ts +263 -0
- package/src/plugin.ts +464 -0
- package/src/policy/builder.ts +215 -0
- package/src/policy/index.ts +10 -0
- package/src/policy/registry.ts +403 -0
- package/src/policy/schema.ts +257 -0
- package/src/policy/types.ts +742 -0
- package/src/transformer/index.ts +2 -0
- package/src/transformer/mutation.ts +372 -0
- package/src/transformer/select.ts +150 -0
- package/src/utils/helpers.ts +139 -0
- package/src/utils/index.ts +12 -0
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>();
|
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';
|