@kysera/rls 0.8.0 → 0.8.2

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
@@ -1,9 +1,9 @@
1
- import { R as RLSSchema, O as Operation, P as PolicyCondition, a as PolicyHints, b as PolicyDefinition, F as FilterCondition, T as TableRLSConfig, C as CompiledPolicy, c as CompiledFilterPolicy, d as RLSContext, e as RLSAuthContext, f as RLSRequestContext, g as PolicyEvaluationContext } from './types-Dowjd6zG.js';
2
- export { h as PolicyType } from './types-Dowjd6zG.js';
1
+ import { R as RLSSchema, O as Operation, P as PolicyCondition, a as PolicyHints, b as PolicyActivationCondition, C as ConditionalPolicyDefinition, F as FilterCondition, T as TableRLSConfig, c as PolicyDefinition, d as CompiledPolicy, e as CompiledFilterPolicy, f as RLSContext, g as RLSAuthContext, h as RLSRequestContext, i as PolicyEvaluationContext } from './types-CyqksFKU.js';
2
+ export { k as PolicyActivationContext, j as PolicyType } from './types-CyqksFKU.js';
3
3
  import { KyseraLogger, DatabaseError, ErrorCode } from '@kysera/core';
4
4
  import { Plugin } from '@kysera/executor';
5
5
  import { z } from 'zod';
6
- import 'kysely';
6
+ import { SelectQueryBuilder } from 'kysely';
7
7
 
8
8
  /**
9
9
  * RLS schema definition and validation
@@ -95,6 +95,24 @@ interface PolicyOptions {
95
95
  priority?: number;
96
96
  /** Performance optimization hints */
97
97
  hints?: PolicyHints;
98
+ /**
99
+ * Condition that determines if this policy is active
100
+ * The policy will only be evaluated if this returns true
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * // Only apply in production
105
+ * allow('read', () => true, {
106
+ * condition: ctx => ctx.meta?.environment === 'production'
107
+ * })
108
+ *
109
+ * // Feature-gated policy
110
+ * filter('read', ctx => ({ strict: true }), {
111
+ * condition: ctx => ctx.meta?.features?.strictMode
112
+ * })
113
+ * ```
114
+ */
115
+ condition?: PolicyActivationCondition;
98
116
  }
99
117
  /**
100
118
  * Create an allow policy
@@ -118,7 +136,7 @@ interface PolicyOptions {
118
136
  * })
119
137
  * ```
120
138
  */
121
- declare function allow(operation: Operation | Operation[], condition: PolicyCondition, options?: PolicyOptions): PolicyDefinition;
139
+ declare function allow(operation: Operation | Operation[], condition: PolicyCondition, options?: PolicyOptions): ConditionalPolicyDefinition;
122
140
  /**
123
141
  * Create a deny policy
124
142
  * Blocks access when condition evaluates to true (overrides allow)
@@ -142,7 +160,7 @@ declare function allow(operation: Operation | Operation[], condition: PolicyCond
142
160
  * })
143
161
  * ```
144
162
  */
145
- declare function deny(operation: Operation | Operation[], condition?: PolicyCondition, options?: PolicyOptions): PolicyDefinition;
163
+ declare function deny(operation: Operation | Operation[], condition?: PolicyCondition, options?: PolicyOptions): ConditionalPolicyDefinition;
146
164
  /**
147
165
  * Create a filter policy
148
166
  * Adds WHERE conditions to SELECT queries
@@ -179,7 +197,7 @@ declare function deny(operation: Operation | Operation[], condition?: PolicyCond
179
197
  * })
180
198
  * ```
181
199
  */
182
- declare function filter(operation: 'read' | 'all', condition: FilterCondition, options?: PolicyOptions): PolicyDefinition;
200
+ declare function filter(operation: 'read' | 'all', condition: FilterCondition, options?: PolicyOptions): ConditionalPolicyDefinition;
183
201
  /**
184
202
  * Create a validate policy
185
203
  * Validates mutation data before execution
@@ -204,7 +222,78 @@ declare function filter(operation: 'read' | 'all', condition: FilterCondition, o
204
222
  * })
205
223
  * ```
206
224
  */
207
- declare function validate(operation: 'create' | 'update' | 'all', condition: PolicyCondition, options?: PolicyOptions): PolicyDefinition;
225
+ declare function validate(operation: 'create' | 'update' | 'all', condition: PolicyCondition, options?: PolicyOptions): ConditionalPolicyDefinition;
226
+ /**
227
+ * Create a policy that is only active in specific environments
228
+ *
229
+ * @param environments - Environments where the policy is active
230
+ * @param policyFn - Function that creates the policy
231
+ * @returns Policy with environment condition
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * // Policy only active in production
236
+ * const prodPolicy = whenEnvironment(['production'], () =>
237
+ * allow('read', () => true, { name: 'prod-read' })
238
+ * );
239
+ *
240
+ * // Policy active in staging and production
241
+ * const nonDevPolicy = whenEnvironment(['staging', 'production'], () =>
242
+ * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }))
243
+ * );
244
+ * ```
245
+ */
246
+ declare function whenEnvironment(environments: string[], policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition;
247
+ /**
248
+ * Create a policy that is only active when a feature flag is enabled
249
+ *
250
+ * @param feature - Feature flag name
251
+ * @param policyFn - Function that creates the policy
252
+ * @returns Policy with feature flag condition
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * // Policy only active when 'strict_rls' feature is enabled
257
+ * const strictPolicy = whenFeature('strict_rls', () =>
258
+ * deny('delete', () => true, { name: 'strict-no-delete' })
259
+ * );
260
+ * ```
261
+ */
262
+ declare function whenFeature(feature: string, policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition;
263
+ /**
264
+ * Create a policy that is only active during specific hours
265
+ *
266
+ * @param startHour - Start hour (0-23)
267
+ * @param endHour - End hour (0-23)
268
+ * @param policyFn - Function that creates the policy
269
+ * @returns Policy with time-based condition
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * // Policy only active during business hours (9 AM - 5 PM)
274
+ * const businessHoursPolicy = whenTimeRange(9, 17, () =>
275
+ * allow('update', () => true, { name: 'business-hours-update' })
276
+ * );
277
+ * ```
278
+ */
279
+ declare function whenTimeRange(startHour: number, endHour: number, policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition;
280
+ /**
281
+ * Create a policy that is only active when a custom condition is met
282
+ *
283
+ * @param condition - Custom activation condition
284
+ * @param policyFn - Function that creates the policy
285
+ * @returns Policy with custom condition
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * // Policy only active when user is in beta program
290
+ * const betaPolicy = whenCondition(
291
+ * ctx => ctx.meta?.betaUser === true,
292
+ * () => allow('read', () => true, { name: 'beta-read' })
293
+ * );
294
+ * ```
295
+ */
296
+ declare function whenCondition(condition: PolicyActivationCondition, policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition;
208
297
 
209
298
  /**
210
299
  * Policy Registry
@@ -806,4 +895,2731 @@ declare function hashString(str: string): string;
806
895
  */
807
896
  declare function normalizeOperations(operation: Operation | Operation[]): Operation[];
808
897
 
809
- export { CompiledFilterPolicy, CompiledPolicy, type CreateRLSContextOptions, FilterCondition, Operation, PolicyCondition, PolicyDefinition, PolicyEvaluationContext, PolicyHints, type PolicyOptions, PolicyRegistry, RLSAuthContext, RLSContext, RLSContextError, RLSContextValidationError, RLSError, type RLSErrorCode, RLSErrorCodes, type RLSPluginOptions, RLSPluginOptionsSchema, RLSPolicyEvaluationError, RLSPolicyViolation, RLSRequestContext, RLSSchema, RLSSchemaError, TableRLSConfig, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
898
+ /**
899
+ * Context Resolver Types
900
+ *
901
+ * Provides infrastructure for pre-resolving async data before RLS policy evaluation.
902
+ * This allows synchronous filters to access data that would otherwise require async lookups.
903
+ *
904
+ * @module @kysera/rls/resolvers/types
905
+ */
906
+
907
+ /**
908
+ * Base interface for resolved data that can be added to RLS context
909
+ *
910
+ * @example
911
+ * ```typescript
912
+ * interface MyResolvedData extends ResolvedData {
913
+ * organizationIds: string[];
914
+ * permissions: Set<string>;
915
+ * employeeRoles: Map<string, string[]>;
916
+ * }
917
+ * ```
918
+ */
919
+ interface ResolvedData {
920
+ /**
921
+ * Timestamp when data was resolved
922
+ * Used for cache validation
923
+ */
924
+ resolvedAt: Date;
925
+ /**
926
+ * Cache key used for this resolution (if cached)
927
+ */
928
+ cacheKey?: string;
929
+ }
930
+ /**
931
+ * Extended auth context with pre-resolved data
932
+ *
933
+ * @typeParam TUser - Custom user type
934
+ * @typeParam TResolved - Type of pre-resolved data
935
+ *
936
+ * @example
937
+ * ```typescript
938
+ * interface OrgPermissions extends ResolvedData {
939
+ * organizationIds: string[];
940
+ * orgPermissions: Map<string, Set<string>>;
941
+ * isOrgOwner: (orgId: string) => boolean;
942
+ * hasOrgPermission: (orgId: string, permission: string) => boolean;
943
+ * }
944
+ *
945
+ * type EnhancedAuth = EnhancedRLSAuthContext<User, OrgPermissions>;
946
+ *
947
+ * // Use in policy
948
+ * filter('read', ctx => ({
949
+ * organization_id: ctx.auth.resolved.organizationIds
950
+ * }));
951
+ * ```
952
+ */
953
+ interface EnhancedRLSAuthContext<TUser = unknown, TResolved extends ResolvedData = ResolvedData> extends RLSAuthContext<TUser> {
954
+ /**
955
+ * Pre-resolved data available synchronously in policies
956
+ *
957
+ * This data is populated by ContextResolvers before entering the RLS context.
958
+ * Use this for async data lookups that policies need synchronously.
959
+ */
960
+ resolved: TResolved;
961
+ }
962
+ /**
963
+ * Extended RLS context with enhanced auth containing resolved data
964
+ *
965
+ * @typeParam TUser - Custom user type
966
+ * @typeParam TResolved - Type of pre-resolved data
967
+ * @typeParam TMeta - Custom metadata type
968
+ */
969
+ interface EnhancedRLSContext<TUser = unknown, TResolved extends ResolvedData = ResolvedData, TMeta = unknown> extends Omit<RLSContext<TUser, TMeta>, 'auth'> {
970
+ auth: EnhancedRLSAuthContext<TUser, TResolved>;
971
+ }
972
+ /**
973
+ * Base context for resolver input (before resolution)
974
+ */
975
+ interface BaseResolverContext {
976
+ auth: {
977
+ userId: string | number;
978
+ roles: string[];
979
+ tenantId?: string | number;
980
+ organizationIds?: (string | number)[];
981
+ permissions?: string[];
982
+ attributes?: Record<string, unknown>;
983
+ isSystem?: boolean;
984
+ };
985
+ timestamp: Date;
986
+ meta?: unknown;
987
+ }
988
+ /**
989
+ * Context resolver that enriches base context with pre-resolved data
990
+ *
991
+ * Resolvers are responsible for fetching async data and making it available
992
+ * synchronously in policy evaluation contexts.
993
+ *
994
+ * @typeParam TResolved - Type of resolved data this resolver produces
995
+ *
996
+ * @example
997
+ * ```typescript
998
+ * const orgPermissionResolver: ContextResolver<OrgPermissions> = {
999
+ * name: 'org-permissions',
1000
+ *
1001
+ * async resolve(base) {
1002
+ * const employments = await db.selectFrom('employees')
1003
+ * .where('user_id', '=', base.auth.userId)
1004
+ * .where('status', '=', 'active')
1005
+ * .execute();
1006
+ *
1007
+ * const orgPermissions = new Map<string, Set<string>>();
1008
+ * // ... resolve permissions ...
1009
+ *
1010
+ * return {
1011
+ * resolvedAt: new Date(),
1012
+ * organizationIds: employments.map(e => e.organization_id),
1013
+ * orgPermissions,
1014
+ * isOrgOwner: (orgId) => employments.some(e => e.organization_id === orgId && e.is_owner),
1015
+ * hasOrgPermission: (orgId, permission) => {
1016
+ * const perms = orgPermissions.get(orgId);
1017
+ * return perms?.has('*') || perms?.has(permission) || false;
1018
+ * }
1019
+ * };
1020
+ * },
1021
+ *
1022
+ * cacheKey: (base) => `rls:org-perms:${base.auth.userId}`,
1023
+ * cacheTtl: 300 // 5 minutes
1024
+ * };
1025
+ * ```
1026
+ */
1027
+ interface ContextResolver<TResolved extends ResolvedData = ResolvedData> {
1028
+ /**
1029
+ * Unique name for this resolver
1030
+ * Used for logging and debugging
1031
+ */
1032
+ name: string;
1033
+ /**
1034
+ * Resolve async data for the context
1035
+ *
1036
+ * @param base - Base context with user info
1037
+ * @returns Pre-resolved data to be added to context
1038
+ */
1039
+ resolve(base: BaseResolverContext): Promise<TResolved>;
1040
+ /**
1041
+ * Generate cache key for this context
1042
+ * Return undefined to disable caching for this resolver
1043
+ *
1044
+ * @param base - Base context
1045
+ * @returns Cache key string or undefined
1046
+ */
1047
+ cacheKey?(base: BaseResolverContext): string | undefined;
1048
+ /**
1049
+ * Cache TTL in seconds
1050
+ * @default 300 (5 minutes)
1051
+ */
1052
+ cacheTtl?: number;
1053
+ /**
1054
+ * Whether this resolver is required
1055
+ * If true, resolution failure will throw an error
1056
+ * If false, the resolver will be skipped on failure
1057
+ *
1058
+ * @default true
1059
+ */
1060
+ required?: boolean;
1061
+ /**
1062
+ * Dependencies on other resolvers (by name)
1063
+ * This resolver will wait for dependencies to complete first
1064
+ */
1065
+ dependsOn?: string[];
1066
+ /**
1067
+ * Priority for resolver execution order (higher = earlier)
1068
+ * @default 0
1069
+ */
1070
+ priority?: number;
1071
+ }
1072
+ /**
1073
+ * Combined result of multiple resolvers
1074
+ *
1075
+ * @typeParam T - Union type of all resolved data types
1076
+ */
1077
+ interface CompositeResolvedData<T extends Record<string, ResolvedData>> extends ResolvedData {
1078
+ /**
1079
+ * Individual resolver results keyed by resolver name
1080
+ */
1081
+ resolvers: T;
1082
+ }
1083
+ /**
1084
+ * Cache provider interface for storing resolved context data
1085
+ */
1086
+ interface ResolverCacheProvider {
1087
+ /**
1088
+ * Get cached data
1089
+ * @param key - Cache key
1090
+ * @returns Cached data or null if not found/expired
1091
+ */
1092
+ get<T>(key: string): Promise<T | null>;
1093
+ /**
1094
+ * Set cached data
1095
+ * @param key - Cache key
1096
+ * @param value - Data to cache
1097
+ * @param ttlSeconds - Time to live in seconds
1098
+ */
1099
+ set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
1100
+ /**
1101
+ * Delete cached data
1102
+ * @param key - Cache key
1103
+ */
1104
+ delete(key: string): Promise<void>;
1105
+ /**
1106
+ * Delete all cached data matching a pattern
1107
+ * @param pattern - Pattern to match (e.g., "rls:org-perms:*")
1108
+ */
1109
+ deletePattern?(pattern: string): Promise<void>;
1110
+ }
1111
+ /**
1112
+ * In-memory cache provider implementation
1113
+ *
1114
+ * Suitable for single-instance deployments or testing.
1115
+ * For distributed systems, use a Redis-based provider.
1116
+ */
1117
+ declare class InMemoryCacheProvider implements ResolverCacheProvider {
1118
+ private cache;
1119
+ get<T>(key: string): Promise<T | null>;
1120
+ set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
1121
+ delete(key: string): Promise<void>;
1122
+ deletePattern(pattern: string): Promise<void>;
1123
+ /**
1124
+ * Clear all cached entries
1125
+ */
1126
+ clear(): void;
1127
+ /**
1128
+ * Get current cache size
1129
+ */
1130
+ get size(): number;
1131
+ }
1132
+ /**
1133
+ * Options for ResolverManager
1134
+ */
1135
+ interface ResolverManagerOptions {
1136
+ /**
1137
+ * Cache provider for storing resolved data
1138
+ * @default InMemoryCacheProvider
1139
+ */
1140
+ cacheProvider?: ResolverCacheProvider;
1141
+ /**
1142
+ * Default cache TTL in seconds
1143
+ * @default 300 (5 minutes)
1144
+ */
1145
+ defaultCacheTtl?: number;
1146
+ /**
1147
+ * Whether to run resolvers in parallel when possible
1148
+ * @default true
1149
+ */
1150
+ parallelResolution?: boolean;
1151
+ /**
1152
+ * Maximum time (ms) to wait for a single resolver
1153
+ * @default 5000 (5 seconds)
1154
+ */
1155
+ resolverTimeout?: number;
1156
+ /**
1157
+ * Logger for resolver operations
1158
+ */
1159
+ logger?: {
1160
+ debug?: (message: string, context?: Record<string, unknown>) => void;
1161
+ info?: (message: string, context?: Record<string, unknown>) => void;
1162
+ warn?: (message: string, context?: Record<string, unknown>) => void;
1163
+ error?: (message: string, context?: Record<string, unknown>) => void;
1164
+ };
1165
+ }
1166
+ /**
1167
+ * Common resolved data for organization-based permissions
1168
+ *
1169
+ * Pre-built pattern for multi-organization systems where users can
1170
+ * belong to multiple organizations with different roles/permissions.
1171
+ */
1172
+ interface OrganizationResolvedData extends ResolvedData {
1173
+ /**
1174
+ * List of organization IDs the user belongs to
1175
+ */
1176
+ organizationIds: (string | number)[];
1177
+ /**
1178
+ * Map of organization ID to user's permissions in that org
1179
+ */
1180
+ orgPermissions: Map<string | number, Set<string>>;
1181
+ /**
1182
+ * Map of organization ID to user's roles in that org
1183
+ */
1184
+ orgRoles: Map<string | number, string[]>;
1185
+ /**
1186
+ * Check if user is owner of an organization
1187
+ * @param orgId - Organization ID
1188
+ */
1189
+ isOrgOwner(orgId: string | number): boolean;
1190
+ /**
1191
+ * Check if user has a specific permission in an organization
1192
+ * @param orgId - Organization ID
1193
+ * @param permission - Permission to check
1194
+ */
1195
+ hasOrgPermission(orgId: string | number, permission: string): boolean;
1196
+ /**
1197
+ * Check if user has a specific role in an organization
1198
+ * @param orgId - Organization ID
1199
+ * @param role - Role to check
1200
+ */
1201
+ hasOrgRole(orgId: string | number, role: string): boolean;
1202
+ }
1203
+ /**
1204
+ * Common resolved data for tenant-based systems
1205
+ */
1206
+ interface TenantResolvedData extends ResolvedData {
1207
+ /**
1208
+ * Current tenant ID (resolved from user context)
1209
+ */
1210
+ tenantId: string | number;
1211
+ /**
1212
+ * Tenant-specific settings/restrictions
1213
+ */
1214
+ tenantSettings?: Record<string, unknown>;
1215
+ /**
1216
+ * Tenant-specific feature flags
1217
+ */
1218
+ tenantFeatures?: Set<string>;
1219
+ }
1220
+ /**
1221
+ * Common resolved data for hierarchical permissions
1222
+ *
1223
+ * For systems with resource hierarchies (e.g., team -> project -> task)
1224
+ */
1225
+ interface HierarchyResolvedData extends ResolvedData {
1226
+ /**
1227
+ * Resources the user has direct access to
1228
+ */
1229
+ directAccess: Set<string>;
1230
+ /**
1231
+ * Resources the user has inherited access to (through hierarchy)
1232
+ */
1233
+ inheritedAccess: Set<string>;
1234
+ /**
1235
+ * Check if user can access a resource (direct or inherited)
1236
+ * @param resourceId - Resource ID
1237
+ */
1238
+ canAccess(resourceId: string): boolean;
1239
+ /**
1240
+ * Get the access level for a resource
1241
+ * @param resourceId - Resource ID
1242
+ * @returns Access level or null if no access
1243
+ */
1244
+ getAccessLevel(resourceId: string): string | null;
1245
+ }
1246
+ /**
1247
+ * Combined resolved data type for common use cases
1248
+ */
1249
+ type CommonResolvedData = OrganizationResolvedData & TenantResolvedData;
1250
+
1251
+ /**
1252
+ * Context Resolver Manager
1253
+ *
1254
+ * Orchestrates the resolution of context data from multiple resolvers,
1255
+ * handling caching, dependencies, and parallel execution.
1256
+ *
1257
+ * @module @kysera/rls/resolvers/manager
1258
+ */
1259
+
1260
+ /**
1261
+ * Manages context resolvers and orchestrates context resolution
1262
+ *
1263
+ * The ResolverManager is responsible for:
1264
+ * - Registering and organizing resolvers
1265
+ * - Resolving context data in the correct order (respecting dependencies)
1266
+ * - Caching resolved data
1267
+ * - Handling resolver failures
1268
+ *
1269
+ * @example
1270
+ * ```typescript
1271
+ * const manager = new ResolverManager({
1272
+ * cacheProvider: new RedisCacheProvider(redis),
1273
+ * defaultCacheTtl: 300,
1274
+ * parallelResolution: true
1275
+ * });
1276
+ *
1277
+ * // Register resolvers
1278
+ * manager.register(orgPermissionResolver);
1279
+ * manager.register(tenantSettingsResolver);
1280
+ *
1281
+ * // Resolve context
1282
+ * const enhancedCtx = await manager.resolve({
1283
+ * auth: { userId: '123', roles: ['user'] },
1284
+ * timestamp: new Date()
1285
+ * });
1286
+ *
1287
+ * // Use in RLS
1288
+ * await rlsContext.runAsync(enhancedCtx, async () => {
1289
+ * // Policies can access resolved data synchronously
1290
+ * });
1291
+ * ```
1292
+ */
1293
+ declare class ResolverManager<TResolved extends ResolvedData = ResolvedData> {
1294
+ private resolvers;
1295
+ private cacheProvider;
1296
+ private defaultCacheTtl;
1297
+ private parallelResolution;
1298
+ private resolverTimeout;
1299
+ private logger;
1300
+ constructor(options?: ResolverManagerOptions);
1301
+ /**
1302
+ * Register a context resolver
1303
+ *
1304
+ * @param resolver - Resolver to register
1305
+ * @throws RLSError if resolver with same name already exists
1306
+ */
1307
+ register<T extends ResolvedData>(resolver: ContextResolver<T>): void;
1308
+ /**
1309
+ * Unregister a context resolver
1310
+ *
1311
+ * @param name - Name of resolver to unregister
1312
+ * @returns true if resolver was removed, false if it didn't exist
1313
+ */
1314
+ unregister(name: string): boolean;
1315
+ /**
1316
+ * Check if a resolver is registered
1317
+ *
1318
+ * @param name - Resolver name
1319
+ */
1320
+ hasResolver(name: string): boolean;
1321
+ /**
1322
+ * Get all registered resolver names
1323
+ */
1324
+ getResolverNames(): string[];
1325
+ /**
1326
+ * Resolve context data using all registered resolvers
1327
+ *
1328
+ * @param baseContext - Base context to resolve
1329
+ * @returns Enhanced context with resolved data
1330
+ *
1331
+ * @example
1332
+ * ```typescript
1333
+ * const baseCtx = {
1334
+ * auth: { userId: '123', roles: ['user'], tenantId: 'acme' },
1335
+ * timestamp: new Date()
1336
+ * };
1337
+ *
1338
+ * const enhancedCtx = await manager.resolve(baseCtx);
1339
+ * // enhancedCtx.auth.resolved contains all resolved data
1340
+ * ```
1341
+ */
1342
+ resolve(baseContext: BaseResolverContext): Promise<EnhancedRLSContext<unknown, TResolved>>;
1343
+ /**
1344
+ * Resolve a single resolver (useful for partial updates)
1345
+ *
1346
+ * @param name - Resolver name
1347
+ * @param baseContext - Base context
1348
+ * @returns Resolved data from the specific resolver
1349
+ */
1350
+ resolveOne<T extends ResolvedData>(name: string, baseContext: BaseResolverContext): Promise<T | null>;
1351
+ /**
1352
+ * Invalidate cached data for a user
1353
+ *
1354
+ * @param userId - User ID whose cache should be invalidated
1355
+ * @param resolverName - Optional specific resolver to invalidate
1356
+ */
1357
+ invalidateCache(userId: string | number, resolverName?: string): Promise<void>;
1358
+ /**
1359
+ * Clear all cached data
1360
+ */
1361
+ clearCache(): Promise<void>;
1362
+ /**
1363
+ * Get resolvers in dependency order (topological sort)
1364
+ */
1365
+ private getResolverOrder;
1366
+ /**
1367
+ * Resolve resolvers sequentially
1368
+ */
1369
+ private resolveSequential;
1370
+ /**
1371
+ * Resolve resolvers in parallel (respecting dependencies)
1372
+ */
1373
+ private resolveParallel;
1374
+ /**
1375
+ * Resolve a single resolver with caching
1376
+ */
1377
+ private resolveWithCache;
1378
+ /**
1379
+ * Execute a promise with timeout
1380
+ */
1381
+ private withTimeout;
1382
+ /**
1383
+ * Merge resolved data from multiple resolvers
1384
+ */
1385
+ private mergeResolvedData;
1386
+ }
1387
+ /**
1388
+ * Create a context resolver manager with common defaults
1389
+ *
1390
+ * @param options - Manager options
1391
+ * @returns Configured ResolverManager
1392
+ */
1393
+ declare function createResolverManager<TResolved extends ResolvedData = ResolvedData>(options?: ResolverManagerOptions): ResolverManager<TResolved>;
1394
+ /**
1395
+ * Helper to create a context resolver
1396
+ *
1397
+ * @param config - Resolver configuration
1398
+ * @returns ContextResolver instance
1399
+ *
1400
+ * @example
1401
+ * ```typescript
1402
+ * const resolver = createResolver({
1403
+ * name: 'org-permissions',
1404
+ * resolve: async (base) => {
1405
+ * const orgs = await getEmployeeOrganizations(base.auth.userId);
1406
+ * return {
1407
+ * resolvedAt: new Date(),
1408
+ * organizationIds: orgs.map(o => o.id)
1409
+ * };
1410
+ * },
1411
+ * cacheKey: (base) => `rls:org:${base.auth.userId}`,
1412
+ * cacheTtl: 300
1413
+ * });
1414
+ * ```
1415
+ */
1416
+ declare function createResolver<TResolved extends ResolvedData>(config: ContextResolver<TResolved>): ContextResolver<TResolved>;
1417
+
1418
+ /**
1419
+ * ReBAC (Relationship-Based Access Control) Types
1420
+ *
1421
+ * Provides type definitions for relationship-based filtering in RLS policies.
1422
+ * ReBAC allows policies to be defined based on relationships between entities
1423
+ * in the database, enabling complex access control patterns like
1424
+ * "show products if user is employee of product's shop's organization".
1425
+ *
1426
+ * @module @kysera/rls/rebac/types
1427
+ */
1428
+
1429
+ /**
1430
+ * A single step in a relationship path
1431
+ *
1432
+ * Defines how to join from one table to another.
1433
+ *
1434
+ * @example
1435
+ * ```typescript
1436
+ * // Simple join: products.shop_id -> shops.id
1437
+ * const step: RelationshipStep = {
1438
+ * from: 'products',
1439
+ * to: 'shops',
1440
+ * fromColumn: 'shop_id',
1441
+ * toColumn: 'id'
1442
+ * };
1443
+ * ```
1444
+ */
1445
+ interface RelationshipStep {
1446
+ /**
1447
+ * Source table name
1448
+ */
1449
+ from: string;
1450
+ /**
1451
+ * Target table name
1452
+ */
1453
+ to: string;
1454
+ /**
1455
+ * Column in source table for the join
1456
+ * @default 'id' on target table side, '{to}_id' on source side
1457
+ */
1458
+ fromColumn?: string;
1459
+ /**
1460
+ * Column in target table for the join
1461
+ * @default 'id'
1462
+ */
1463
+ toColumn?: string;
1464
+ /**
1465
+ * Optional alias for the target table in the join
1466
+ * Useful when joining the same table multiple times
1467
+ */
1468
+ alias?: string;
1469
+ /**
1470
+ * Join type
1471
+ * @default 'inner'
1472
+ */
1473
+ joinType?: 'inner' | 'left' | 'right';
1474
+ /**
1475
+ * Additional conditions for this join step
1476
+ * @example { 'shops.deleted_at': null, 'shops.status': 'active' }
1477
+ */
1478
+ additionalConditions?: Record<string, unknown>;
1479
+ }
1480
+ /**
1481
+ * Complete relationship path definition
1482
+ *
1483
+ * Defines a chain of relationships from a source table to a target.
1484
+ *
1485
+ * @example
1486
+ * ```typescript
1487
+ * // Path: products -> shops -> organizations -> employees
1488
+ * const path: RelationshipPath = {
1489
+ * name: 'orgEmployee',
1490
+ * steps: [
1491
+ * { from: 'products', to: 'shops', fromColumn: 'shop_id' },
1492
+ * { from: 'shops', to: 'organizations', fromColumn: 'organization_id' },
1493
+ * { from: 'organizations', to: 'employees', toColumn: 'organization_id' }
1494
+ * ]
1495
+ * };
1496
+ * ```
1497
+ */
1498
+ interface RelationshipPath {
1499
+ /**
1500
+ * Unique name for this relationship path
1501
+ * Used in policy definitions to reference this path
1502
+ */
1503
+ name: string;
1504
+ /**
1505
+ * Steps in the relationship chain
1506
+ */
1507
+ steps: RelationshipStep[];
1508
+ /**
1509
+ * Optional description for documentation
1510
+ */
1511
+ description?: string;
1512
+ }
1513
+ /**
1514
+ * Condition to apply at the end of a relationship path
1515
+ *
1516
+ * @typeParam TCtx - Policy evaluation context type
1517
+ */
1518
+ type RelationshipCondition<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> = ((ctx: TCtx) => Record<string, unknown>) | Record<string, unknown>;
1519
+ /**
1520
+ * ReBAC policy definition
1521
+ *
1522
+ * Extends standard policy definition with relationship-based filtering.
1523
+ */
1524
+ interface ReBAcPolicyDefinition<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> extends Omit<PolicyDefinition, 'condition'> {
1525
+ /**
1526
+ * Name of the relationship path to use (defined in relationships config)
1527
+ */
1528
+ relationshipPath: string;
1529
+ /**
1530
+ * Conditions to apply at the end of the relationship
1531
+ * These conditions filter the final table in the relationship chain.
1532
+ *
1533
+ * @example
1534
+ * ```typescript
1535
+ * // Filter employees table at end of relationship
1536
+ * endCondition: ctx => ({
1537
+ * user_id: ctx.auth.userId,
1538
+ * status: 'active'
1539
+ * })
1540
+ * ```
1541
+ */
1542
+ endCondition: RelationshipCondition<TCtx>;
1543
+ /**
1544
+ * Whether this is a permissive or restrictive policy
1545
+ * - 'allow': Row is accessible if relationship exists
1546
+ * - 'deny': Row is NOT accessible if relationship exists
1547
+ * @default 'allow'
1548
+ */
1549
+ policyType?: 'allow' | 'deny';
1550
+ }
1551
+ /**
1552
+ * ReBAC configuration for a single table
1553
+ */
1554
+ interface TableReBAcConfig {
1555
+ /**
1556
+ * Relationship paths available for this table
1557
+ */
1558
+ relationships: RelationshipPath[];
1559
+ /**
1560
+ * ReBAC policies for this table
1561
+ */
1562
+ policies: ReBAcPolicyDefinition[];
1563
+ }
1564
+ /**
1565
+ * Complete ReBAC schema for all tables
1566
+ *
1567
+ * @typeParam DB - Database schema type
1568
+ */
1569
+ type ReBAcSchema<DB> = {
1570
+ [K in keyof DB]?: TableReBAcConfig;
1571
+ };
1572
+ /**
1573
+ * Compiled relationship path ready for query generation
1574
+ */
1575
+ interface CompiledRelationshipPath {
1576
+ /**
1577
+ * Path name
1578
+ */
1579
+ name: string;
1580
+ /**
1581
+ * Compiled join steps with defaults filled in
1582
+ */
1583
+ steps: Required<RelationshipStep>[];
1584
+ /**
1585
+ * Source table (first table in the chain)
1586
+ */
1587
+ sourceTable: string;
1588
+ /**
1589
+ * Target table (final table in the chain)
1590
+ */
1591
+ targetTable: string;
1592
+ }
1593
+ /**
1594
+ * Compiled ReBAC policy ready for evaluation
1595
+ */
1596
+ interface CompiledReBAcPolicy<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> {
1597
+ /**
1598
+ * Policy name
1599
+ */
1600
+ name: string;
1601
+ /**
1602
+ * Policy type (allow/deny)
1603
+ */
1604
+ type: 'allow' | 'deny';
1605
+ /**
1606
+ * Operations this policy applies to
1607
+ */
1608
+ operations: Set<string>;
1609
+ /**
1610
+ * Compiled relationship path
1611
+ */
1612
+ relationshipPath: CompiledRelationshipPath;
1613
+ /**
1614
+ * Function to get end conditions
1615
+ */
1616
+ getEndConditions: (ctx: TCtx) => Record<string, unknown>;
1617
+ /**
1618
+ * Priority for policy evaluation
1619
+ */
1620
+ priority: number;
1621
+ }
1622
+ /**
1623
+ * Generated EXISTS subquery for ReBAC filtering
1624
+ */
1625
+ interface ReBAcSubquery {
1626
+ /**
1627
+ * SQL for the EXISTS subquery
1628
+ */
1629
+ sql: string;
1630
+ /**
1631
+ * Parameter values for the subquery
1632
+ */
1633
+ parameters: unknown[];
1634
+ /**
1635
+ * Whether this is an allow (EXISTS) or deny (NOT EXISTS) check
1636
+ */
1637
+ isNegated: boolean;
1638
+ }
1639
+ /**
1640
+ * Options for ReBAC query generation
1641
+ */
1642
+ interface ReBAcQueryOptions {
1643
+ /**
1644
+ * Table alias for the main query table
1645
+ * @default table name
1646
+ */
1647
+ mainTableAlias?: string;
1648
+ /**
1649
+ * Whether to use qualified column names
1650
+ * @default true
1651
+ */
1652
+ qualifyColumns?: boolean;
1653
+ /**
1654
+ * Database dialect for query generation
1655
+ * @default 'postgres'
1656
+ */
1657
+ dialect?: 'postgres' | 'mysql' | 'sqlite';
1658
+ }
1659
+ /**
1660
+ * Common relationship pattern: Resource belongs to organization via owner
1661
+ *
1662
+ * @param resourceTable - Table containing the resource
1663
+ * @param organizationColumn - Column linking to organization
1664
+ *
1665
+ * @example
1666
+ * ```typescript
1667
+ * const path = orgMembershipPath('products', 'organization_id');
1668
+ * // Creates path: products -> organizations -> employees
1669
+ * ```
1670
+ */
1671
+ declare function orgMembershipPath(resourceTable: string, organizationColumn?: string): RelationshipPath;
1672
+ /**
1673
+ * Common relationship pattern: Resource belongs to shop's organization
1674
+ *
1675
+ * @param resourceTable - Table containing the resource
1676
+ * @param shopColumn - Column linking to shop
1677
+ *
1678
+ * @example
1679
+ * ```typescript
1680
+ * const path = shopOrgMembershipPath('products', 'shop_id');
1681
+ * // Creates path: products -> shops -> organizations -> employees
1682
+ * ```
1683
+ */
1684
+ declare function shopOrgMembershipPath(resourceTable: string, shopColumn?: string): RelationshipPath;
1685
+ /**
1686
+ * Common relationship pattern: Hierarchical team access
1687
+ *
1688
+ * @param resourceTable - Table containing the resource
1689
+ * @param teamColumn - Column linking to team
1690
+ *
1691
+ * @example
1692
+ * ```typescript
1693
+ * const path = teamHierarchyPath('tasks', 'team_id');
1694
+ * // Creates path: tasks -> teams -> team_members
1695
+ * ```
1696
+ */
1697
+ declare function teamHierarchyPath(resourceTable: string, teamColumn?: string): RelationshipPath;
1698
+
1699
+ /**
1700
+ * ReBAC Policy Registry
1701
+ *
1702
+ * Manages relationship definitions and ReBAC policies for RLS.
1703
+ *
1704
+ * @module @kysera/rls/rebac/registry
1705
+ */
1706
+
1707
+ /**
1708
+ * ReBAC Registry
1709
+ *
1710
+ * Manages relationship paths and ReBAC policies across tables.
1711
+ *
1712
+ * @example
1713
+ * ```typescript
1714
+ * const registry = new ReBAcRegistry();
1715
+ *
1716
+ * // Register relationship paths and policies
1717
+ * registry.loadSchema({
1718
+ * products: {
1719
+ * relationships: [
1720
+ * shopOrgMembershipPath('products', 'shop_id')
1721
+ * ],
1722
+ * policies: [
1723
+ * {
1724
+ * type: 'filter',
1725
+ * operation: 'read',
1726
+ * relationshipPath: 'products_shop_org_membership',
1727
+ * endCondition: ctx => ({
1728
+ * user_id: ctx.auth.userId,
1729
+ * status: 'active'
1730
+ * })
1731
+ * }
1732
+ * ]
1733
+ * }
1734
+ * });
1735
+ *
1736
+ * // Get policies for a table
1737
+ * const policies = registry.getPolicies('products', 'read');
1738
+ * ```
1739
+ */
1740
+ declare class ReBAcRegistry<DB = unknown> {
1741
+ private tables;
1742
+ private globalRelationships;
1743
+ private logger;
1744
+ constructor(schema?: ReBAcSchema<DB>, options?: {
1745
+ logger?: KyseraLogger;
1746
+ });
1747
+ /**
1748
+ * Load ReBAC schema
1749
+ */
1750
+ loadSchema(schema: ReBAcSchema<DB>): void;
1751
+ /**
1752
+ * Register ReBAC configuration for a single table
1753
+ */
1754
+ registerTable(table: string, config: TableReBAcConfig): void;
1755
+ /**
1756
+ * Register a global relationship path (available to all tables)
1757
+ */
1758
+ registerRelationship(path: RelationshipPath): void;
1759
+ /**
1760
+ * Get ReBAC policies for a table and operation
1761
+ */
1762
+ getPolicies(table: string, operation: Operation): CompiledReBAcPolicy[];
1763
+ /**
1764
+ * Get a specific relationship path
1765
+ */
1766
+ getRelationship(name: string, table?: string): CompiledRelationshipPath | undefined;
1767
+ /**
1768
+ * Check if table has ReBAC configuration
1769
+ */
1770
+ hasTable(table: string): boolean;
1771
+ /**
1772
+ * Get all registered table names
1773
+ */
1774
+ getTables(): string[];
1775
+ /**
1776
+ * Clear all registrations
1777
+ */
1778
+ clear(): void;
1779
+ /**
1780
+ * Compile a relationship path definition
1781
+ */
1782
+ private compileRelationshipPath;
1783
+ /**
1784
+ * Compile a ReBAC policy definition
1785
+ */
1786
+ private compilePolicy;
1787
+ }
1788
+ /**
1789
+ * Create a ReBAC registry
1790
+ */
1791
+ declare function createReBAcRegistry<DB = unknown>(schema?: ReBAcSchema<DB>, options?: {
1792
+ logger?: KyseraLogger;
1793
+ }): ReBAcRegistry<DB>;
1794
+
1795
+ /**
1796
+ * ReBAC Query Transformer
1797
+ *
1798
+ * Transforms queries to apply relationship-based access control policies.
1799
+ * Generates EXISTS subqueries that filter rows based on relationship chains.
1800
+ *
1801
+ * @module @kysera/rls/rebac/transformer
1802
+ */
1803
+
1804
+ /**
1805
+ * ReBAC query transformer
1806
+ *
1807
+ * Applies relationship-based access control to SELECT queries by generating
1808
+ * EXISTS subqueries that follow relationship paths.
1809
+ *
1810
+ * @example
1811
+ * ```typescript
1812
+ * const transformer = new ReBAcTransformer(registry);
1813
+ *
1814
+ * // Transform query
1815
+ * let query = db.selectFrom('products').selectAll();
1816
+ * query = transformer.transform(query, 'products', 'read');
1817
+ *
1818
+ * // Generated SQL includes EXISTS subquery:
1819
+ * // SELECT * FROM products p
1820
+ * // WHERE EXISTS (
1821
+ * // SELECT 1 FROM shops s
1822
+ * // JOIN organizations o ON s.organization_id = o.id
1823
+ * // JOIN employees e ON e.organization_id = o.id
1824
+ * // WHERE s.id = p.shop_id
1825
+ * // AND e.user_id = $1
1826
+ * // AND e.status = 'active'
1827
+ * // )
1828
+ * ```
1829
+ */
1830
+ declare class ReBAcTransformer<DB = unknown> {
1831
+ private registry;
1832
+ private options;
1833
+ constructor(registry: ReBAcRegistry<DB>, options?: ReBAcQueryOptions);
1834
+ /**
1835
+ * Transform a SELECT query by applying ReBAC policies
1836
+ *
1837
+ * @param qb - Query builder to transform
1838
+ * @param table - Table being queried
1839
+ * @param operation - Operation being performed
1840
+ * @returns Transformed query builder
1841
+ */
1842
+ transform<TB extends keyof DB & string, O>(qb: SelectQueryBuilder<DB, TB, O>, table: string, operation?: Operation): SelectQueryBuilder<DB, TB, O>;
1843
+ /**
1844
+ * Generate EXISTS condition SQL for a policy
1845
+ *
1846
+ * This method can be used to get the raw SQL for debugging or manual query building.
1847
+ *
1848
+ * @param policy - ReBAC policy to generate SQL for
1849
+ * @param ctx - RLS context
1850
+ * @param mainTable - Main query table
1851
+ * @param mainTableAlias - Alias for main table
1852
+ * @returns SQL string and parameters
1853
+ */
1854
+ generateExistsSql(policy: CompiledReBAcPolicy, ctx: RLSContext, mainTable: string, mainTableAlias?: string): {
1855
+ sql: string;
1856
+ params: unknown[];
1857
+ };
1858
+ /**
1859
+ * Apply a single ReBAC policy to a query
1860
+ *
1861
+ * NOTE: Uses type casting for dynamic SQL because Kysely's type system
1862
+ * requires compile-time known types, but ReBAC policies work with
1863
+ * runtime-generated EXISTS clauses.
1864
+ */
1865
+ private applyPolicy;
1866
+ /**
1867
+ * Create evaluation context for policy conditions
1868
+ */
1869
+ private createEvalContext;
1870
+ /**
1871
+ * Quote an identifier for the target dialect
1872
+ */
1873
+ private quote;
1874
+ /**
1875
+ * Generate parameter placeholder for the target dialect
1876
+ */
1877
+ private param;
1878
+ }
1879
+ /**
1880
+ * Create a ReBAC allow policy
1881
+ *
1882
+ * Rows are accessible if the relationship EXISTS with the given end conditions.
1883
+ *
1884
+ * @param operation - Operation(s) this policy applies to
1885
+ * @param relationshipPath - Name of the relationship path to use
1886
+ * @param endCondition - Conditions to apply at the end of the relationship
1887
+ * @param options - Additional policy options
1888
+ *
1889
+ * @example
1890
+ * ```typescript
1891
+ * // Allow read if user is employee of product's shop's organization
1892
+ * allowRelation('read', 'products_shop_org_membership', ctx => ({
1893
+ * user_id: ctx.auth.userId,
1894
+ * status: 'active'
1895
+ * }))
1896
+ * ```
1897
+ */
1898
+ declare function allowRelation(operation: Operation | Operation[], relationshipPath: string, endCondition: ((ctx: PolicyEvaluationContext) => Record<string, unknown>) | Record<string, unknown>, options?: {
1899
+ name?: string;
1900
+ priority?: number;
1901
+ }): ReBAcPolicyDefinition;
1902
+ /**
1903
+ * Create a ReBAC deny policy
1904
+ *
1905
+ * Rows are NOT accessible if the relationship EXISTS with the given conditions.
1906
+ *
1907
+ * @param operation - Operation(s) this policy applies to
1908
+ * @param relationshipPath - Name of the relationship path to use
1909
+ * @param endCondition - Conditions to apply at the end of the relationship
1910
+ * @param options - Additional policy options
1911
+ *
1912
+ * @example
1913
+ * ```typescript
1914
+ * // Deny access if user is blocked in the organization
1915
+ * denyRelation('all', 'products_shop_org_membership', ctx => ({
1916
+ * user_id: ctx.auth.userId,
1917
+ * status: 'blocked'
1918
+ * }))
1919
+ * ```
1920
+ */
1921
+ declare function denyRelation(operation: Operation | Operation[], relationshipPath: string, endCondition: ((ctx: PolicyEvaluationContext) => Record<string, unknown>) | Record<string, unknown>, options?: {
1922
+ name?: string;
1923
+ priority?: number;
1924
+ }): ReBAcPolicyDefinition;
1925
+ /**
1926
+ * Create a ReBAC transformer
1927
+ */
1928
+ declare function createReBAcTransformer<DB = unknown>(registry: ReBAcRegistry<DB>, options?: ReBAcQueryOptions): ReBAcTransformer<DB>;
1929
+
1930
+ /**
1931
+ * Field-Level Access Control Types
1932
+ *
1933
+ * Provides type definitions for controlling access to individual columns
1934
+ * based on context. This allows hiding sensitive fields from unauthorized users.
1935
+ *
1936
+ * @module @kysera/rls/field-access/types
1937
+ */
1938
+
1939
+ /**
1940
+ * Operations that can be controlled at field level
1941
+ */
1942
+ type FieldOperation = 'read' | 'write';
1943
+ /**
1944
+ * Field access condition function
1945
+ *
1946
+ * Returns true if the field is accessible, false otherwise.
1947
+ *
1948
+ * @typeParam TCtx - Policy evaluation context type
1949
+ */
1950
+ type FieldAccessCondition<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> = (ctx: TCtx) => boolean | Promise<boolean>;
1951
+ /**
1952
+ * Configuration for a single field's access control
1953
+ *
1954
+ * @example
1955
+ * ```typescript
1956
+ * const emailConfig: FieldAccessConfig = {
1957
+ * read: ctx => ctx.auth.userId === ctx.row.id || ctx.auth.roles.includes('admin'),
1958
+ * write: ctx => ctx.auth.userId === ctx.row.id
1959
+ * };
1960
+ * ```
1961
+ */
1962
+ interface FieldAccessConfig<TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> {
1963
+ /**
1964
+ * Condition for read access
1965
+ * If undefined, uses table default
1966
+ */
1967
+ read?: FieldAccessCondition<TCtx>;
1968
+ /**
1969
+ * Condition for write access
1970
+ * If undefined, uses table default
1971
+ */
1972
+ write?: FieldAccessCondition<TCtx>;
1973
+ /**
1974
+ * Value to use when field is not readable
1975
+ * @default null
1976
+ */
1977
+ maskedValue?: unknown;
1978
+ /**
1979
+ * Whether to completely omit the field when not readable
1980
+ * @default false (uses maskedValue instead)
1981
+ */
1982
+ omitWhenHidden?: boolean;
1983
+ }
1984
+ /**
1985
+ * Table field access configuration
1986
+ *
1987
+ * @typeParam TRow - Type of the database row
1988
+ * @typeParam TCtx - Policy evaluation context type
1989
+ *
1990
+ * @example
1991
+ * ```typescript
1992
+ * const usersFieldAccess: TableFieldAccessConfig<User> = {
1993
+ * default: 'allow',
1994
+ * fields: {
1995
+ * email: {
1996
+ * read: ctx => ctx.auth.userId === ctx.row.id || ctx.auth.roles.includes('admin')
1997
+ * },
1998
+ * password_hash: {
1999
+ * read: () => false,
2000
+ * write: () => false
2001
+ * },
2002
+ * mfa_totp_secret: {
2003
+ * read: ctx => ctx.auth.userId === ctx.row.id,
2004
+ * omitWhenHidden: true
2005
+ * }
2006
+ * }
2007
+ * };
2008
+ * ```
2009
+ */
2010
+ interface TableFieldAccessConfig<TRow = unknown, TCtx extends PolicyEvaluationContext = PolicyEvaluationContext> {
2011
+ /**
2012
+ * Default access policy for fields not explicitly configured
2013
+ * - 'allow': All fields are accessible by default
2014
+ * - 'deny': Only explicitly allowed fields are accessible
2015
+ * @default 'allow'
2016
+ */
2017
+ default?: 'allow' | 'deny';
2018
+ /**
2019
+ * Field-specific access configurations
2020
+ */
2021
+ fields: {
2022
+ [K in keyof TRow]?: FieldAccessConfig<TCtx>;
2023
+ };
2024
+ /**
2025
+ * Roles that bypass field access control
2026
+ */
2027
+ skipFor?: string[];
2028
+ }
2029
+ /**
2030
+ * Complete field access schema for all tables
2031
+ *
2032
+ * @typeParam DB - Database schema type
2033
+ */
2034
+ type FieldAccessSchema<DB> = {
2035
+ [K in keyof DB]?: TableFieldAccessConfig<DB[K]>;
2036
+ };
2037
+ /**
2038
+ * Compiled field access configuration ready for evaluation
2039
+ */
2040
+ interface CompiledFieldAccess {
2041
+ /**
2042
+ * Field name
2043
+ */
2044
+ field: string;
2045
+ /**
2046
+ * Compiled read condition
2047
+ * Returns true if field is readable
2048
+ */
2049
+ canRead: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>;
2050
+ /**
2051
+ * Compiled write condition
2052
+ * Returns true if field is writable
2053
+ */
2054
+ canWrite: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>;
2055
+ /**
2056
+ * Value to use when field is masked
2057
+ */
2058
+ maskedValue: unknown;
2059
+ /**
2060
+ * Whether to omit the field entirely when hidden
2061
+ */
2062
+ omitWhenHidden: boolean;
2063
+ }
2064
+ /**
2065
+ * Compiled table field access configuration
2066
+ */
2067
+ interface CompiledTableFieldAccess {
2068
+ /**
2069
+ * Table name
2070
+ */
2071
+ table: string;
2072
+ /**
2073
+ * Default access policy
2074
+ */
2075
+ defaultAccess: 'allow' | 'deny';
2076
+ /**
2077
+ * Roles that bypass field access
2078
+ */
2079
+ skipFor: string[];
2080
+ /**
2081
+ * Field-specific configurations
2082
+ */
2083
+ fields: Map<string, CompiledFieldAccess>;
2084
+ }
2085
+ /**
2086
+ * Result of field access evaluation
2087
+ */
2088
+ interface FieldAccessResult {
2089
+ /**
2090
+ * Whether the field is accessible
2091
+ */
2092
+ accessible: boolean;
2093
+ /**
2094
+ * If not accessible, the reason
2095
+ */
2096
+ reason?: string;
2097
+ /**
2098
+ * Value to use (original or masked)
2099
+ */
2100
+ value: unknown;
2101
+ /**
2102
+ * Whether the field should be omitted entirely
2103
+ */
2104
+ omit: boolean;
2105
+ }
2106
+ /**
2107
+ * Result of applying field access to a row
2108
+ */
2109
+ interface MaskedRow<T = Record<string, unknown>> {
2110
+ /**
2111
+ * The row with field access applied
2112
+ */
2113
+ data: Partial<T>;
2114
+ /**
2115
+ * Fields that were masked
2116
+ */
2117
+ maskedFields: string[];
2118
+ /**
2119
+ * Fields that were omitted
2120
+ */
2121
+ omittedFields: string[];
2122
+ }
2123
+ /**
2124
+ * Options for field access processing
2125
+ */
2126
+ interface FieldAccessOptions {
2127
+ /**
2128
+ * Whether to throw an error when accessing a denied field
2129
+ * @default false (returns masked value instead)
2130
+ */
2131
+ throwOnDenied?: boolean;
2132
+ /**
2133
+ * Whether to include metadata about masked fields in the result
2134
+ * @default false
2135
+ */
2136
+ includeMetadata?: boolean;
2137
+ /**
2138
+ * Fields to explicitly include (whitelist)
2139
+ * If specified, only these fields are processed
2140
+ */
2141
+ includeFields?: string[];
2142
+ /**
2143
+ * Fields to explicitly exclude (blacklist)
2144
+ * These fields are never included regardless of access
2145
+ */
2146
+ excludeFields?: string[];
2147
+ }
2148
+ /**
2149
+ * Always deny access to a field
2150
+ *
2151
+ * @example
2152
+ * ```typescript
2153
+ * const config = {
2154
+ * fields: {
2155
+ * password_hash: neverAccessible(),
2156
+ * api_secret: neverAccessible()
2157
+ * }
2158
+ * };
2159
+ * ```
2160
+ */
2161
+ declare function neverAccessible(): FieldAccessConfig;
2162
+ /**
2163
+ * Only the resource owner can access this field
2164
+ *
2165
+ * @param ownerField - Field name containing the owner ID
2166
+ *
2167
+ * @example
2168
+ * ```typescript
2169
+ * const config = {
2170
+ * fields: {
2171
+ * email: ownerOnly('user_id'),
2172
+ * phone: ownerOnly('user_id')
2173
+ * }
2174
+ * };
2175
+ * ```
2176
+ */
2177
+ declare function ownerOnly(ownerField?: string): FieldAccessConfig;
2178
+ /**
2179
+ * Owner or users with specific roles can access this field
2180
+ *
2181
+ * @param roles - Roles that can access besides owner
2182
+ * @param ownerField - Field name containing the owner ID
2183
+ *
2184
+ * @example
2185
+ * ```typescript
2186
+ * const config = {
2187
+ * fields: {
2188
+ * email: ownerOrRoles(['admin', 'support'], 'user_id'),
2189
+ * address: ownerOrRoles(['admin'], 'user_id')
2190
+ * }
2191
+ * };
2192
+ * ```
2193
+ */
2194
+ declare function ownerOrRoles(roles: string[], ownerField?: string): FieldAccessConfig;
2195
+ /**
2196
+ * Only users with specific roles can access this field
2197
+ *
2198
+ * @param roles - Roles that can access
2199
+ *
2200
+ * @example
2201
+ * ```typescript
2202
+ * const config = {
2203
+ * fields: {
2204
+ * internal_notes: rolesOnly(['admin', 'moderator']),
2205
+ * audit_log: rolesOnly(['admin'])
2206
+ * }
2207
+ * };
2208
+ * ```
2209
+ */
2210
+ declare function rolesOnly(roles: string[]): FieldAccessConfig;
2211
+ /**
2212
+ * Field is read-only (no write access)
2213
+ *
2214
+ * @param readCondition - Optional condition for read access
2215
+ *
2216
+ * @example
2217
+ * ```typescript
2218
+ * const config = {
2219
+ * fields: {
2220
+ * created_at: readOnly(),
2221
+ * version: readOnly()
2222
+ * }
2223
+ * };
2224
+ * ```
2225
+ */
2226
+ declare function readOnly(readCondition?: FieldAccessCondition): FieldAccessConfig;
2227
+ /**
2228
+ * Field has public read access but restricted write
2229
+ *
2230
+ * @param writeCondition - Condition for write access
2231
+ *
2232
+ * @example
2233
+ * ```typescript
2234
+ * const config = {
2235
+ * fields: {
2236
+ * display_name: publicReadRestrictedWrite(ctx => ctx.auth.userId === ctx.row.id),
2237
+ * bio: publicReadRestrictedWrite(ctx => ctx.auth.userId === ctx.row.id)
2238
+ * }
2239
+ * };
2240
+ * ```
2241
+ */
2242
+ declare function publicReadRestrictedWrite(writeCondition: FieldAccessCondition): FieldAccessConfig;
2243
+ /**
2244
+ * Mask field value with custom masking function
2245
+ *
2246
+ * @param maskFn - Function to mask the value
2247
+ * @param readCondition - Condition for full read access
2248
+ *
2249
+ * @example
2250
+ * ```typescript
2251
+ * const config = {
2252
+ * fields: {
2253
+ * email: maskedField(
2254
+ * value => value.replace(/(.{2}).*@/, '$1***@'),
2255
+ * ctx => ctx.auth.userId === ctx.row.id
2256
+ * ),
2257
+ * phone: maskedField(
2258
+ * value => value.replace(/\d(?=\d{4})/g, '*'),
2259
+ * ctx => ctx.auth.userId === ctx.row.id
2260
+ * )
2261
+ * }
2262
+ * };
2263
+ * ```
2264
+ */
2265
+ declare function maskedField(maskFn: (value: unknown) => unknown, readCondition: FieldAccessCondition): FieldAccessConfig & {
2266
+ maskFn: (value: unknown) => unknown;
2267
+ };
2268
+
2269
+ /**
2270
+ * Field Access Registry
2271
+ *
2272
+ * Manages field-level access control configurations across tables.
2273
+ *
2274
+ * @module @kysera/rls/field-access/registry
2275
+ */
2276
+
2277
+ /**
2278
+ * Field Access Registry
2279
+ *
2280
+ * Manages field-level access control configurations for all tables.
2281
+ *
2282
+ * @example
2283
+ * ```typescript
2284
+ * const registry = new FieldAccessRegistry();
2285
+ *
2286
+ * registry.loadSchema<Database>({
2287
+ * users: {
2288
+ * default: 'allow',
2289
+ * fields: {
2290
+ * email: ownerOrRoles(['admin'], 'id'),
2291
+ * password_hash: neverAccessible(),
2292
+ * mfa_secret: ownerOnly('id')
2293
+ * }
2294
+ * }
2295
+ * });
2296
+ *
2297
+ * // Check if field is accessible
2298
+ * const canRead = await registry.canReadField('users', 'email', evalCtx);
2299
+ * ```
2300
+ */
2301
+ declare class FieldAccessRegistry<DB = unknown> {
2302
+ private tables;
2303
+ private logger;
2304
+ constructor(schema?: FieldAccessSchema<DB>, options?: {
2305
+ logger?: KyseraLogger;
2306
+ });
2307
+ /**
2308
+ * Load field access schema
2309
+ */
2310
+ loadSchema(schema: FieldAccessSchema<DB>): void;
2311
+ /**
2312
+ * Register field access configuration for a table
2313
+ */
2314
+ registerTable(table: string, config: TableFieldAccessConfig): void;
2315
+ /**
2316
+ * Check if a field is readable in the current context
2317
+ *
2318
+ * @param table - Table name
2319
+ * @param field - Field name
2320
+ * @param ctx - Evaluation context
2321
+ * @returns True if field is readable
2322
+ */
2323
+ canReadField(table: string, field: string, ctx: PolicyEvaluationContext): Promise<boolean>;
2324
+ /**
2325
+ * Check if a field is writable in the current context
2326
+ *
2327
+ * @param table - Table name
2328
+ * @param field - Field name
2329
+ * @param ctx - Evaluation context
2330
+ * @returns True if field is writable
2331
+ */
2332
+ canWriteField(table: string, field: string, ctx: PolicyEvaluationContext): Promise<boolean>;
2333
+ /**
2334
+ * Get field configuration
2335
+ *
2336
+ * @param table - Table name
2337
+ * @param field - Field name
2338
+ * @returns Compiled field access config or undefined
2339
+ */
2340
+ getFieldConfig(table: string, field: string): CompiledFieldAccess | undefined;
2341
+ /**
2342
+ * Get table configuration
2343
+ *
2344
+ * @param table - Table name
2345
+ * @returns Compiled table field access config or undefined
2346
+ */
2347
+ getTableConfig(table: string): CompiledTableFieldAccess | undefined;
2348
+ /**
2349
+ * Check if table has field access configuration
2350
+ */
2351
+ hasTable(table: string): boolean;
2352
+ /**
2353
+ * Get all registered table names
2354
+ */
2355
+ getTables(): string[];
2356
+ /**
2357
+ * Get all fields with explicit configuration for a table
2358
+ *
2359
+ * @param table - Table name
2360
+ * @returns Array of field names
2361
+ */
2362
+ getConfiguredFields(table: string): string[];
2363
+ /**
2364
+ * Clear all configurations
2365
+ */
2366
+ clear(): void;
2367
+ /**
2368
+ * Compile a field access configuration
2369
+ */
2370
+ private compileFieldConfig;
2371
+ }
2372
+ /**
2373
+ * Create a field access registry
2374
+ */
2375
+ declare function createFieldAccessRegistry<DB = unknown>(schema?: FieldAccessSchema<DB>, options?: {
2376
+ logger?: KyseraLogger;
2377
+ }): FieldAccessRegistry<DB>;
2378
+
2379
+ /**
2380
+ * Field Access Processor
2381
+ *
2382
+ * Applies field-level access control to database rows and mutation data.
2383
+ *
2384
+ * @module @kysera/rls/field-access/processor
2385
+ */
2386
+
2387
+ /**
2388
+ * Field Access Processor
2389
+ *
2390
+ * Applies field-level access control rules to rows and mutation data.
2391
+ *
2392
+ * @example
2393
+ * ```typescript
2394
+ * const processor = new FieldAccessProcessor(registry);
2395
+ *
2396
+ * // Mask fields in a row
2397
+ * const result = await processor.maskRow('users', user, {
2398
+ * includeMetadata: true
2399
+ * });
2400
+ *
2401
+ * console.log(result.data); // Row with masked fields
2402
+ * console.log(result.maskedFields); // ['email', 'phone']
2403
+ * console.log(result.omittedFields); // ['mfa_secret']
2404
+ *
2405
+ * // Validate write access
2406
+ * await processor.validateWrite('users', { email: 'new@example.com' });
2407
+ * ```
2408
+ */
2409
+ declare class FieldAccessProcessor<DB = unknown> {
2410
+ private registry;
2411
+ private defaultMaskValue;
2412
+ constructor(registry: FieldAccessRegistry<DB>, defaultMaskValue?: unknown);
2413
+ /**
2414
+ * Apply field access control to a single row
2415
+ *
2416
+ * @param table - Table name
2417
+ * @param row - Row data
2418
+ * @param options - Processing options
2419
+ * @returns Masked row with metadata
2420
+ */
2421
+ maskRow<T extends Record<string, unknown>>(table: string, row: T, options?: FieldAccessOptions): Promise<MaskedRow<T>>;
2422
+ /**
2423
+ * Apply field access control to multiple rows
2424
+ *
2425
+ * @param table - Table name
2426
+ * @param rows - Array of rows
2427
+ * @param options - Processing options
2428
+ * @returns Array of masked rows
2429
+ */
2430
+ maskRows<T extends Record<string, unknown>>(table: string, rows: T[], options?: FieldAccessOptions): Promise<MaskedRow<T>[]>;
2431
+ /**
2432
+ * Validate that all fields in mutation data are writable
2433
+ *
2434
+ * @param table - Table name
2435
+ * @param data - Mutation data
2436
+ * @param existingRow - Existing row (for update operations)
2437
+ * @throws RLSPolicyViolation if any field is not writable
2438
+ */
2439
+ validateWrite(table: string, data: Record<string, unknown>, existingRow?: Record<string, unknown>): Promise<void>;
2440
+ /**
2441
+ * Filter mutation data to only include writable fields
2442
+ *
2443
+ * @param table - Table name
2444
+ * @param data - Mutation data
2445
+ * @param existingRow - Existing row (for update operations)
2446
+ * @returns Filtered data with only writable fields
2447
+ */
2448
+ filterWritableFields(table: string, data: Record<string, unknown>, existingRow?: Record<string, unknown>): Promise<{
2449
+ data: Record<string, unknown>;
2450
+ removedFields: string[];
2451
+ }>;
2452
+ /**
2453
+ * Get list of readable fields for a table
2454
+ *
2455
+ * @param table - Table name
2456
+ * @param row - Row data (for context-dependent fields)
2457
+ * @returns Array of readable field names
2458
+ */
2459
+ getReadableFields(table: string, row: Record<string, unknown>): Promise<string[]>;
2460
+ /**
2461
+ * Get list of writable fields for a table
2462
+ *
2463
+ * @param table - Table name
2464
+ * @param row - Existing row data (for context-dependent fields)
2465
+ * @returns Array of writable field names
2466
+ */
2467
+ getWritableFields(table: string, row: Record<string, unknown>): Promise<string[]>;
2468
+ /**
2469
+ * Get current RLS context
2470
+ */
2471
+ private getContext;
2472
+ /**
2473
+ * Create evaluation context
2474
+ */
2475
+ private createEvalContext;
2476
+ /**
2477
+ * Evaluate field access for a specific field
2478
+ */
2479
+ private evaluateFieldAccess;
2480
+ }
2481
+ /**
2482
+ * Create a field access processor
2483
+ */
2484
+ declare function createFieldAccessProcessor<DB = unknown>(registry: FieldAccessRegistry<DB>, defaultMaskValue?: unknown): FieldAccessProcessor<DB>;
2485
+
2486
+ /**
2487
+ * Policy Composition Types
2488
+ *
2489
+ * Provides types for creating reusable, composable RLS policies.
2490
+ *
2491
+ * @module @kysera/rls/composition/types
2492
+ */
2493
+
2494
+ /**
2495
+ * A named, reusable policy template
2496
+ *
2497
+ * Can be composed with other policies and applied to multiple tables.
2498
+ *
2499
+ * @example
2500
+ * ```typescript
2501
+ * const tenantIsolation = definePolicy({
2502
+ * name: 'tenantIsolation',
2503
+ * type: 'filter',
2504
+ * operation: 'read',
2505
+ * filter: ctx => ({ tenant_id: ctx.auth.tenantId }),
2506
+ * priority: 1000
2507
+ * });
2508
+ * ```
2509
+ */
2510
+ interface ReusablePolicy {
2511
+ /**
2512
+ * Unique name for this policy
2513
+ */
2514
+ name: string;
2515
+ /**
2516
+ * Description for documentation
2517
+ */
2518
+ description?: string;
2519
+ /**
2520
+ * Policy definitions (can include multiple policies)
2521
+ */
2522
+ policies: PolicyDefinition[];
2523
+ /**
2524
+ * Tags for categorization
2525
+ */
2526
+ tags?: string[];
2527
+ }
2528
+ /**
2529
+ * Configuration for a reusable policy template
2530
+ */
2531
+ interface ReusablePolicyConfig {
2532
+ /**
2533
+ * Policy name
2534
+ */
2535
+ name: string;
2536
+ /**
2537
+ * Description
2538
+ */
2539
+ description?: string;
2540
+ /**
2541
+ * Tags for categorization
2542
+ */
2543
+ tags?: string[];
2544
+ }
2545
+ /**
2546
+ * Extended table RLS configuration with policy composition support
2547
+ */
2548
+ interface ComposableTableConfig {
2549
+ /**
2550
+ * Reusable policies to extend from
2551
+ * Policies are applied in order (first = lowest priority)
2552
+ */
2553
+ extends?: ReusablePolicy[];
2554
+ /**
2555
+ * Additional table-specific policies
2556
+ */
2557
+ policies?: PolicyDefinition[];
2558
+ /**
2559
+ * Whether to allow access by default when no policies match
2560
+ * @default true
2561
+ */
2562
+ defaultDeny?: boolean;
2563
+ /**
2564
+ * Roles that bypass RLS
2565
+ */
2566
+ skipFor?: string[];
2567
+ }
2568
+ /**
2569
+ * Complete schema with composition support
2570
+ *
2571
+ * @typeParam DB - Database schema type
2572
+ */
2573
+ type ComposableRLSSchema<DB> = {
2574
+ [K in keyof DB]?: ComposableTableConfig;
2575
+ };
2576
+ /**
2577
+ * Base policy that can be extended
2578
+ */
2579
+ interface BasePolicyDefinition {
2580
+ /**
2581
+ * Unique identifier for this base policy
2582
+ */
2583
+ id: string;
2584
+ /**
2585
+ * Human-readable name
2586
+ */
2587
+ name: string;
2588
+ /**
2589
+ * Description
2590
+ */
2591
+ description?: string;
2592
+ /**
2593
+ * Policies included in this base
2594
+ */
2595
+ policies: PolicyDefinition[];
2596
+ /**
2597
+ * Other base policies this extends
2598
+ */
2599
+ extends?: string[];
2600
+ /**
2601
+ * Priority offset applied to all policies
2602
+ * @default 0
2603
+ */
2604
+ priorityOffset?: number;
2605
+ }
2606
+ /**
2607
+ * Policy inheritance chain resolution
2608
+ */
2609
+ interface ResolvedInheritance {
2610
+ /**
2611
+ * Final merged policies
2612
+ */
2613
+ policies: PolicyDefinition[];
2614
+ /**
2615
+ * Chain of base policies used
2616
+ */
2617
+ inheritanceChain: string[];
2618
+ /**
2619
+ * Any conflicts detected
2620
+ */
2621
+ conflicts: {
2622
+ policy: string;
2623
+ reason: string;
2624
+ }[];
2625
+ }
2626
+ /**
2627
+ * Multi-tenancy policy configuration
2628
+ */
2629
+ interface TenantIsolationConfig {
2630
+ /**
2631
+ * Column name for tenant ID
2632
+ * @default 'tenant_id'
2633
+ */
2634
+ tenantColumn?: string;
2635
+ /**
2636
+ * Operations to apply tenant isolation to
2637
+ * @default ['read', 'create', 'update', 'delete']
2638
+ */
2639
+ operations?: Operation[];
2640
+ /**
2641
+ * Whether to validate tenant on create/update
2642
+ * @default true
2643
+ */
2644
+ validateOnMutation?: boolean;
2645
+ }
2646
+ /**
2647
+ * Ownership policy configuration
2648
+ */
2649
+ interface OwnershipConfig {
2650
+ /**
2651
+ * Column name for owner ID
2652
+ * @default 'owner_id' or 'user_id'
2653
+ */
2654
+ ownerColumn?: string;
2655
+ /**
2656
+ * Operations owners can perform
2657
+ * @default ['read', 'update', 'delete']
2658
+ */
2659
+ ownerOperations?: Operation[];
2660
+ /**
2661
+ * Whether owners can delete
2662
+ * @default true
2663
+ */
2664
+ canDelete?: boolean;
2665
+ }
2666
+ /**
2667
+ * Soft delete policy configuration
2668
+ */
2669
+ interface SoftDeleteConfig {
2670
+ /**
2671
+ * Column name for soft delete flag
2672
+ * @default 'deleted_at'
2673
+ */
2674
+ deletedColumn?: string;
2675
+ /**
2676
+ * Whether to filter soft-deleted rows on read
2677
+ * @default true
2678
+ */
2679
+ filterOnRead?: boolean;
2680
+ /**
2681
+ * Whether to prevent hard deletes
2682
+ * @default true
2683
+ */
2684
+ preventHardDelete?: boolean;
2685
+ }
2686
+ /**
2687
+ * Status-based access configuration
2688
+ */
2689
+ interface StatusAccessConfig {
2690
+ /**
2691
+ * Column name for status
2692
+ * @default 'status'
2693
+ */
2694
+ statusColumn?: string;
2695
+ /**
2696
+ * Statuses that are publicly readable
2697
+ */
2698
+ publicStatuses?: string[];
2699
+ /**
2700
+ * Statuses that can be updated
2701
+ */
2702
+ editableStatuses?: string[];
2703
+ /**
2704
+ * Statuses that can be deleted
2705
+ */
2706
+ deletableStatuses?: string[];
2707
+ }
2708
+
2709
+ /**
2710
+ * Policy Composition Builder
2711
+ *
2712
+ * Factory functions for creating reusable, composable RLS policies.
2713
+ *
2714
+ * @module @kysera/rls/composition/builder
2715
+ */
2716
+
2717
+ /**
2718
+ * Create a reusable policy template
2719
+ *
2720
+ * @param config - Policy configuration
2721
+ * @param policies - Array of policy definitions
2722
+ * @returns Reusable policy template
2723
+ *
2724
+ * @example
2725
+ * ```typescript
2726
+ * const tenantPolicy = definePolicy(
2727
+ * {
2728
+ * name: 'tenantIsolation',
2729
+ * description: 'Filter by tenant_id',
2730
+ * tags: ['multi-tenant']
2731
+ * },
2732
+ * [
2733
+ * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }), {
2734
+ * priority: 1000,
2735
+ * name: 'tenant-filter'
2736
+ * }),
2737
+ * validate('create', ctx => ctx.data?.tenant_id === ctx.auth.tenantId, {
2738
+ * name: 'tenant-validate'
2739
+ * })
2740
+ * ]
2741
+ * );
2742
+ * ```
2743
+ */
2744
+ declare function definePolicy(config: ReusablePolicyConfig, policies: PolicyDefinition[]): ReusablePolicy;
2745
+ /**
2746
+ * Create a filter-only policy
2747
+ *
2748
+ * @param name - Policy name
2749
+ * @param filterFn - Filter condition
2750
+ * @param options - Additional options
2751
+ * @returns Reusable filter policy
2752
+ */
2753
+ declare function defineFilterPolicy(name: string, filterFn: (ctx: PolicyEvaluationContext) => Record<string, unknown>, options?: {
2754
+ priority?: number;
2755
+ }): ReusablePolicy;
2756
+ /**
2757
+ * Create an allow-based policy
2758
+ *
2759
+ * @param name - Policy name
2760
+ * @param operation - Operations to allow
2761
+ * @param condition - Allow condition
2762
+ * @param options - Additional options
2763
+ * @returns Reusable allow policy
2764
+ */
2765
+ declare function defineAllowPolicy(name: string, operation: Operation | Operation[], condition: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>, options?: {
2766
+ priority?: number;
2767
+ }): ReusablePolicy;
2768
+ /**
2769
+ * Create a deny-based policy
2770
+ *
2771
+ * @param name - Policy name
2772
+ * @param operation - Operations to deny
2773
+ * @param condition - Deny condition (optional - if not provided, always denies)
2774
+ * @param options - Additional options
2775
+ * @returns Reusable deny policy
2776
+ */
2777
+ declare function defineDenyPolicy(name: string, operation: Operation | Operation[], condition?: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>, options?: {
2778
+ priority?: number;
2779
+ }): ReusablePolicy;
2780
+ /**
2781
+ * Create a validation policy
2782
+ *
2783
+ * @param name - Policy name
2784
+ * @param operation - Operations to validate
2785
+ * @param condition - Validation condition
2786
+ * @param options - Additional options
2787
+ * @returns Reusable validate policy
2788
+ */
2789
+ declare function defineValidatePolicy(name: string, operation: 'create' | 'update' | 'all', condition: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>, options?: {
2790
+ priority?: number;
2791
+ }): ReusablePolicy;
2792
+ /**
2793
+ * Create a combined policy with multiple types
2794
+ *
2795
+ * @param name - Policy name
2796
+ * @param config - Policy configurations
2797
+ * @returns Reusable combined policy
2798
+ */
2799
+ declare function defineCombinedPolicy(name: string, config: {
2800
+ filter?: (ctx: PolicyEvaluationContext) => Record<string, unknown>;
2801
+ allow?: Record<string, (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>>;
2802
+ deny?: Record<string, (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>>;
2803
+ validate?: {
2804
+ create?: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>;
2805
+ update?: (ctx: PolicyEvaluationContext) => boolean | Promise<boolean>;
2806
+ };
2807
+ }): ReusablePolicy;
2808
+ /**
2809
+ * Create a tenant isolation policy
2810
+ *
2811
+ * Automatically filters by tenant_id and validates mutations.
2812
+ *
2813
+ * @param config - Tenant isolation configuration
2814
+ * @returns Reusable tenant isolation policy
2815
+ */
2816
+ declare function createTenantIsolationPolicy(config?: TenantIsolationConfig): ReusablePolicy;
2817
+ /**
2818
+ * Create an ownership policy
2819
+ *
2820
+ * Allows owners to read/update/delete their own resources.
2821
+ *
2822
+ * @param config - Ownership configuration
2823
+ * @returns Reusable ownership policy
2824
+ */
2825
+ declare function createOwnershipPolicy(config?: OwnershipConfig): ReusablePolicy;
2826
+ /**
2827
+ * Create a soft delete policy
2828
+ *
2829
+ * Filters out soft-deleted rows and optionally prevents hard deletes.
2830
+ *
2831
+ * @param config - Soft delete configuration
2832
+ * @returns Reusable soft delete policy
2833
+ */
2834
+ declare function createSoftDeletePolicy(config?: SoftDeleteConfig): ReusablePolicy;
2835
+ /**
2836
+ * Create a status-based access policy
2837
+ *
2838
+ * Controls access based on resource status.
2839
+ *
2840
+ * @param config - Status access configuration
2841
+ * @returns Reusable status policy
2842
+ */
2843
+ declare function createStatusAccessPolicy(config: StatusAccessConfig): ReusablePolicy;
2844
+ /**
2845
+ * Create an admin bypass policy
2846
+ *
2847
+ * Allows admin roles to perform all operations.
2848
+ *
2849
+ * @param roles - Roles that have admin access
2850
+ * @returns Reusable admin policy
2851
+ */
2852
+ declare function createAdminPolicy(roles: string[]): ReusablePolicy;
2853
+ /**
2854
+ * Compose multiple reusable policies into one
2855
+ *
2856
+ * @param name - Name for the composed policy
2857
+ * @param policies - Policies to compose
2858
+ * @returns Composed policy
2859
+ */
2860
+ declare function composePolicies(name: string, policies: ReusablePolicy[]): ReusablePolicy;
2861
+ /**
2862
+ * Extend a reusable policy with additional policies
2863
+ *
2864
+ * @param base - Base policy to extend
2865
+ * @param additional - Additional policies to add
2866
+ * @returns Extended policy
2867
+ */
2868
+ declare function extendPolicy(base: ReusablePolicy, additional: PolicyDefinition[]): ReusablePolicy;
2869
+ /**
2870
+ * Override policies from a base with new conditions
2871
+ *
2872
+ * @param base - Base policy
2873
+ * @param overrides - Policy name to new policy mapping
2874
+ * @returns Policy with overrides applied
2875
+ */
2876
+ declare function overridePolicy(base: ReusablePolicy, overrides: Record<string, PolicyDefinition>): ReusablePolicy;
2877
+
2878
+ /**
2879
+ * Audit Trail Types
2880
+ *
2881
+ * Provides type definitions for auditing RLS policy decisions.
2882
+ *
2883
+ * @module @kysera/rls/audit/types
2884
+ */
2885
+
2886
+ /**
2887
+ * RLS policy decision result
2888
+ */
2889
+ type AuditDecision = 'allow' | 'deny' | 'filter';
2890
+ /**
2891
+ * RLS audit event
2892
+ *
2893
+ * Represents a single policy evaluation event for audit logging.
2894
+ *
2895
+ * @example
2896
+ * ```typescript
2897
+ * const event: RLSAuditEvent = {
2898
+ * timestamp: new Date(),
2899
+ * userId: '123',
2900
+ * operation: 'update',
2901
+ * table: 'posts',
2902
+ * policyName: 'ownership-allow',
2903
+ * decision: 'allow',
2904
+ * context: { rowId: '456', tenantId: 'acme' }
2905
+ * };
2906
+ * ```
2907
+ */
2908
+ interface RLSAuditEvent {
2909
+ /**
2910
+ * Timestamp of the event
2911
+ */
2912
+ timestamp: Date;
2913
+ /**
2914
+ * User ID who performed the action
2915
+ */
2916
+ userId: string | number;
2917
+ /**
2918
+ * Tenant ID (if multi-tenant)
2919
+ */
2920
+ tenantId?: string | number;
2921
+ /**
2922
+ * Database operation
2923
+ */
2924
+ operation: Operation;
2925
+ /**
2926
+ * Table name
2927
+ */
2928
+ table: string;
2929
+ /**
2930
+ * Name of the policy that made the decision
2931
+ */
2932
+ policyName?: string;
2933
+ /**
2934
+ * Decision result
2935
+ */
2936
+ decision: AuditDecision;
2937
+ /**
2938
+ * Reason for the decision (especially for denials)
2939
+ */
2940
+ reason?: string;
2941
+ /**
2942
+ * Additional context about the event
2943
+ */
2944
+ context?: Record<string, unknown>;
2945
+ /**
2946
+ * Row ID(s) affected
2947
+ */
2948
+ rowIds?: (string | number)[];
2949
+ /**
2950
+ * Hash of the query (for grouping similar queries)
2951
+ */
2952
+ queryHash?: string;
2953
+ /**
2954
+ * Request ID for tracing
2955
+ */
2956
+ requestId?: string;
2957
+ /**
2958
+ * IP address of the requester
2959
+ */
2960
+ ipAddress?: string;
2961
+ /**
2962
+ * User agent string
2963
+ */
2964
+ userAgent?: string;
2965
+ /**
2966
+ * Duration of policy evaluation in milliseconds
2967
+ */
2968
+ durationMs?: number;
2969
+ /**
2970
+ * Whether this event was filtered from logging
2971
+ * (set by filtering rules but still available for debugging)
2972
+ */
2973
+ filtered?: boolean;
2974
+ }
2975
+ /**
2976
+ * Adapter for persisting audit events
2977
+ *
2978
+ * Implement this interface to store audit events in your preferred backend.
2979
+ *
2980
+ * @example
2981
+ * ```typescript
2982
+ * class DatabaseAuditAdapter implements RLSAuditAdapter {
2983
+ * constructor(private db: Kysely<AuditDB>) {}
2984
+ *
2985
+ * async log(event: RLSAuditEvent): Promise<void> {
2986
+ * await this.db.insertInto('rls_audit_log')
2987
+ * .values({
2988
+ * user_id: event.userId,
2989
+ * operation: event.operation,
2990
+ * table_name: event.table,
2991
+ * decision: event.decision,
2992
+ * context: JSON.stringify(event.context),
2993
+ * created_at: event.timestamp
2994
+ * })
2995
+ * .execute();
2996
+ * }
2997
+ *
2998
+ * async logBatch(events: RLSAuditEvent[]): Promise<void> {
2999
+ * await this.db.insertInto('rls_audit_log')
3000
+ * .values(events.map(e => ({
3001
+ * user_id: e.userId,
3002
+ * operation: e.operation,
3003
+ * table_name: e.table,
3004
+ * decision: e.decision,
3005
+ * context: JSON.stringify(e.context),
3006
+ * created_at: e.timestamp
3007
+ * })))
3008
+ * .execute();
3009
+ * }
3010
+ * }
3011
+ * ```
3012
+ */
3013
+ interface RLSAuditAdapter {
3014
+ /**
3015
+ * Log a single audit event
3016
+ *
3017
+ * @param event - Event to log
3018
+ */
3019
+ log(event: RLSAuditEvent): Promise<void>;
3020
+ /**
3021
+ * Log multiple audit events (for batch processing)
3022
+ *
3023
+ * @param events - Events to log
3024
+ */
3025
+ logBatch?(events: RLSAuditEvent[]): Promise<void>;
3026
+ /**
3027
+ * Flush any buffered events
3028
+ */
3029
+ flush?(): Promise<void>;
3030
+ /**
3031
+ * Close the adapter and release resources
3032
+ */
3033
+ close?(): Promise<void>;
3034
+ }
3035
+ /**
3036
+ * Configuration for table-specific audit settings
3037
+ */
3038
+ interface TableAuditConfig {
3039
+ /**
3040
+ * Whether audit is enabled for this table
3041
+ * @default true (if audit is globally enabled)
3042
+ */
3043
+ enabled?: boolean;
3044
+ /**
3045
+ * Log allowed decisions
3046
+ * @default false
3047
+ */
3048
+ logAllowed?: boolean;
3049
+ /**
3050
+ * Log denied decisions
3051
+ * @default true
3052
+ */
3053
+ logDenied?: boolean;
3054
+ /**
3055
+ * Log filter applications
3056
+ * @default false
3057
+ */
3058
+ logFilters?: boolean;
3059
+ /**
3060
+ * Context fields to include in audit logs
3061
+ * If empty, includes all available context
3062
+ */
3063
+ includeContext?: string[];
3064
+ /**
3065
+ * Context fields to exclude from audit logs
3066
+ */
3067
+ excludeContext?: string[];
3068
+ /**
3069
+ * Whether to include row data in audit logs
3070
+ * @default false (for privacy)
3071
+ */
3072
+ includeRowData?: boolean;
3073
+ /**
3074
+ * Whether to include mutation data in audit logs
3075
+ * @default false (for privacy)
3076
+ */
3077
+ includeMutationData?: boolean;
3078
+ /**
3079
+ * Custom filter function to determine if an event should be logged
3080
+ */
3081
+ filter?: (event: RLSAuditEvent) => boolean;
3082
+ }
3083
+ /**
3084
+ * Global audit configuration
3085
+ */
3086
+ interface AuditConfig {
3087
+ /**
3088
+ * Audit adapter for persisting events
3089
+ */
3090
+ adapter: RLSAuditAdapter;
3091
+ /**
3092
+ * Whether audit is enabled globally
3093
+ * @default true
3094
+ */
3095
+ enabled?: boolean;
3096
+ /**
3097
+ * Default settings for all tables
3098
+ */
3099
+ defaults?: Omit<TableAuditConfig, 'enabled'>;
3100
+ /**
3101
+ * Table-specific audit configurations
3102
+ */
3103
+ tables?: Record<string, TableAuditConfig>;
3104
+ /**
3105
+ * Buffer size for batch logging
3106
+ * Events are batched until this size is reached
3107
+ * @default 100
3108
+ */
3109
+ bufferSize?: number;
3110
+ /**
3111
+ * Maximum time to buffer events before flushing (ms)
3112
+ * @default 5000 (5 seconds)
3113
+ */
3114
+ flushInterval?: number;
3115
+ /**
3116
+ * Whether to log asynchronously (fire-and-forget)
3117
+ * @default true (for performance)
3118
+ */
3119
+ async?: boolean;
3120
+ /**
3121
+ * Error handler for audit failures
3122
+ */
3123
+ onError?: (error: Error, events: RLSAuditEvent[]) => void;
3124
+ /**
3125
+ * Sample rate for audit logging (0.0 to 1.0)
3126
+ * Use for high-traffic systems to reduce log volume
3127
+ * @default 1.0 (log all)
3128
+ */
3129
+ sampleRate?: number;
3130
+ }
3131
+ /**
3132
+ * Query parameters for retrieving audit events
3133
+ */
3134
+ interface AuditQueryParams {
3135
+ /**
3136
+ * Filter by user ID
3137
+ */
3138
+ userId?: string | number;
3139
+ /**
3140
+ * Filter by tenant ID
3141
+ */
3142
+ tenantId?: string | number;
3143
+ /**
3144
+ * Filter by table name
3145
+ */
3146
+ table?: string;
3147
+ /**
3148
+ * Filter by operation
3149
+ */
3150
+ operation?: Operation;
3151
+ /**
3152
+ * Filter by decision
3153
+ */
3154
+ decision?: AuditDecision;
3155
+ /**
3156
+ * Start timestamp (inclusive)
3157
+ */
3158
+ startTime?: Date;
3159
+ /**
3160
+ * End timestamp (exclusive)
3161
+ */
3162
+ endTime?: Date;
3163
+ /**
3164
+ * Filter by request ID
3165
+ */
3166
+ requestId?: string;
3167
+ /**
3168
+ * Maximum results to return
3169
+ */
3170
+ limit?: number;
3171
+ /**
3172
+ * Offset for pagination
3173
+ */
3174
+ offset?: number;
3175
+ }
3176
+ /**
3177
+ * Aggregated audit statistics
3178
+ */
3179
+ interface AuditStats {
3180
+ /**
3181
+ * Total number of events
3182
+ */
3183
+ totalEvents: number;
3184
+ /**
3185
+ * Events by decision type
3186
+ */
3187
+ byDecision: Record<AuditDecision, number>;
3188
+ /**
3189
+ * Events by operation
3190
+ */
3191
+ byOperation: Record<Operation, number>;
3192
+ /**
3193
+ * Events by table
3194
+ */
3195
+ byTable: Record<string, number>;
3196
+ /**
3197
+ * Top denied users
3198
+ */
3199
+ topDeniedUsers?: {
3200
+ userId: string | number;
3201
+ count: number;
3202
+ }[];
3203
+ /**
3204
+ * Time range of stats
3205
+ */
3206
+ timeRange: {
3207
+ start: Date;
3208
+ end: Date;
3209
+ };
3210
+ }
3211
+ /**
3212
+ * Simple console-based audit adapter for development/testing
3213
+ *
3214
+ * @example
3215
+ * ```typescript
3216
+ * const adapter = new ConsoleAuditAdapter({
3217
+ * format: 'json',
3218
+ * colors: true
3219
+ * });
3220
+ * ```
3221
+ */
3222
+ interface ConsoleAuditAdapterOptions {
3223
+ /**
3224
+ * Output format
3225
+ * @default 'text'
3226
+ */
3227
+ format?: 'text' | 'json';
3228
+ /**
3229
+ * Use colors in output (for text format)
3230
+ * @default true
3231
+ */
3232
+ colors?: boolean;
3233
+ /**
3234
+ * Include timestamp in output
3235
+ * @default true
3236
+ */
3237
+ includeTimestamp?: boolean;
3238
+ }
3239
+ /**
3240
+ * Console audit adapter implementation
3241
+ */
3242
+ declare class ConsoleAuditAdapter implements RLSAuditAdapter {
3243
+ private options;
3244
+ constructor(options?: ConsoleAuditAdapterOptions);
3245
+ log(event: RLSAuditEvent): Promise<void>;
3246
+ logBatch(events: RLSAuditEvent[]): Promise<void>;
3247
+ private getPrefix;
3248
+ }
3249
+ /**
3250
+ * In-memory audit adapter for testing
3251
+ *
3252
+ * Stores events in memory for later retrieval and assertion.
3253
+ */
3254
+ declare class InMemoryAuditAdapter implements RLSAuditAdapter {
3255
+ private events;
3256
+ private maxSize;
3257
+ constructor(maxSize?: number);
3258
+ log(event: RLSAuditEvent): Promise<void>;
3259
+ logBatch(events: RLSAuditEvent[]): Promise<void>;
3260
+ /**
3261
+ * Get all logged events
3262
+ */
3263
+ getEvents(): RLSAuditEvent[];
3264
+ /**
3265
+ * Query events
3266
+ */
3267
+ query(params: AuditQueryParams): RLSAuditEvent[];
3268
+ /**
3269
+ * Get statistics
3270
+ */
3271
+ getStats(params?: Pick<AuditQueryParams, 'startTime' | 'endTime'>): AuditStats;
3272
+ /**
3273
+ * Clear all events
3274
+ */
3275
+ clear(): void;
3276
+ /**
3277
+ * Get event count
3278
+ */
3279
+ get size(): number;
3280
+ }
3281
+
3282
+ /**
3283
+ * Audit Logger
3284
+ *
3285
+ * Manages audit event logging with buffering and filtering.
3286
+ *
3287
+ * @module @kysera/rls/audit/logger
3288
+ */
3289
+
3290
+ /**
3291
+ * Audit Logger
3292
+ *
3293
+ * Manages RLS audit event logging with buffering, filtering, and sampling.
3294
+ *
3295
+ * @example
3296
+ * ```typescript
3297
+ * const logger = new AuditLogger({
3298
+ * adapter: new DatabaseAuditAdapter(db),
3299
+ * bufferSize: 50,
3300
+ * flushInterval: 5000,
3301
+ * defaults: {
3302
+ * logAllowed: false,
3303
+ * logDenied: true,
3304
+ * logFilters: false
3305
+ * },
3306
+ * tables: {
3307
+ * sensitive_data: {
3308
+ * logAllowed: true,
3309
+ * includeContext: ['requestId', 'ipAddress']
3310
+ * }
3311
+ * }
3312
+ * });
3313
+ *
3314
+ * // Log an event
3315
+ * await logger.logDecision('update', 'posts', 'allow', 'ownership-allow');
3316
+ *
3317
+ * // Ensure all events are flushed
3318
+ * await logger.flush();
3319
+ * ```
3320
+ */
3321
+ declare class AuditLogger {
3322
+ private adapter;
3323
+ private config;
3324
+ private buffer;
3325
+ private flushTimer;
3326
+ private isShuttingDown;
3327
+ constructor(config: AuditConfig);
3328
+ /**
3329
+ * Log a policy decision
3330
+ *
3331
+ * @param operation - Database operation
3332
+ * @param table - Table name
3333
+ * @param decision - Decision result
3334
+ * @param policyName - Name of the policy
3335
+ * @param options - Additional options
3336
+ */
3337
+ logDecision(operation: Operation, table: string, decision: AuditDecision, policyName?: string, options?: {
3338
+ reason?: string;
3339
+ rowIds?: (string | number)[];
3340
+ queryHash?: string;
3341
+ durationMs?: number;
3342
+ context?: Record<string, unknown>;
3343
+ }): Promise<void>;
3344
+ /**
3345
+ * Log an allow decision
3346
+ */
3347
+ logAllow(operation: Operation, table: string, policyName?: string, options?: {
3348
+ reason?: string;
3349
+ rowIds?: (string | number)[];
3350
+ context?: Record<string, unknown>;
3351
+ }): Promise<void>;
3352
+ /**
3353
+ * Log a deny decision
3354
+ */
3355
+ logDeny(operation: Operation, table: string, policyName?: string, options?: {
3356
+ reason?: string;
3357
+ rowIds?: (string | number)[];
3358
+ context?: Record<string, unknown>;
3359
+ }): Promise<void>;
3360
+ /**
3361
+ * Log a filter application
3362
+ */
3363
+ logFilter(table: string, policyName?: string, options?: {
3364
+ context?: Record<string, unknown>;
3365
+ }): Promise<void>;
3366
+ /**
3367
+ * Flush buffered events
3368
+ */
3369
+ flush(): Promise<void>;
3370
+ /**
3371
+ * Close the logger
3372
+ */
3373
+ close(): Promise<void>;
3374
+ /**
3375
+ * Get buffer size
3376
+ */
3377
+ get bufferSize(): number;
3378
+ /**
3379
+ * Check if logger is enabled
3380
+ */
3381
+ get enabled(): boolean;
3382
+ /**
3383
+ * Enable or disable logging
3384
+ */
3385
+ setEnabled(enabled: boolean): void;
3386
+ /**
3387
+ * Get table-specific config with defaults
3388
+ */
3389
+ private getTableConfig;
3390
+ /**
3391
+ * Check if decision should be logged
3392
+ */
3393
+ private shouldLog;
3394
+ /**
3395
+ * Build audit event
3396
+ */
3397
+ private buildEvent;
3398
+ /**
3399
+ * Build context object with filtering
3400
+ */
3401
+ private buildContext;
3402
+ /**
3403
+ * Log event to buffer or directly
3404
+ */
3405
+ private logEvent;
3406
+ /**
3407
+ * Start the flush timer
3408
+ */
3409
+ private startFlushTimer;
3410
+ }
3411
+ /**
3412
+ * Create an audit logger
3413
+ */
3414
+ declare function createAuditLogger(config: AuditConfig): AuditLogger;
3415
+
3416
+ /**
3417
+ * Policy Testing Utilities
3418
+ *
3419
+ * Provides tools for unit testing RLS policies without a database.
3420
+ *
3421
+ * @module @kysera/rls/testing
3422
+ */
3423
+
3424
+ /**
3425
+ * Result of policy evaluation
3426
+ */
3427
+ interface PolicyEvaluationResult {
3428
+ /**
3429
+ * Whether the operation is allowed
3430
+ */
3431
+ allowed: boolean;
3432
+ /**
3433
+ * Name of the policy that made the decision
3434
+ */
3435
+ policyName?: string;
3436
+ /**
3437
+ * Type of decision
3438
+ */
3439
+ decisionType: 'allow' | 'deny' | 'default';
3440
+ /**
3441
+ * Reason for the decision
3442
+ */
3443
+ reason?: string;
3444
+ /**
3445
+ * All policies that were evaluated
3446
+ */
3447
+ evaluatedPolicies: {
3448
+ name: string;
3449
+ type: 'allow' | 'deny' | 'validate';
3450
+ result: boolean;
3451
+ }[];
3452
+ }
3453
+ /**
3454
+ * Result of filter evaluation
3455
+ */
3456
+ interface FilterEvaluationResult {
3457
+ /**
3458
+ * Generated filter conditions
3459
+ */
3460
+ conditions: Record<string, unknown>;
3461
+ /**
3462
+ * Names of all filters applied
3463
+ */
3464
+ appliedFilters: string[];
3465
+ }
3466
+ /**
3467
+ * Test context for policy evaluation
3468
+ */
3469
+ interface TestContext<TRow = Record<string, unknown>> {
3470
+ /**
3471
+ * Auth context
3472
+ */
3473
+ auth: RLSAuthContext;
3474
+ /**
3475
+ * Row data (for read/update/delete operations)
3476
+ */
3477
+ row?: TRow;
3478
+ /**
3479
+ * Mutation data (for create/update operations)
3480
+ */
3481
+ data?: Record<string, unknown>;
3482
+ /**
3483
+ * Additional metadata
3484
+ */
3485
+ meta?: Record<string, unknown>;
3486
+ }
3487
+ /**
3488
+ * Policy Tester
3489
+ *
3490
+ * Test RLS policies without a database connection.
3491
+ *
3492
+ * @example
3493
+ * ```typescript
3494
+ * const tester = createPolicyTester(rlsSchema);
3495
+ *
3496
+ * describe('Post RLS Policies', () => {
3497
+ * it('should allow owner to update their post', async () => {
3498
+ * const result = await tester.evaluate('posts', 'update', {
3499
+ * auth: { userId: 'user-1', roles: ['user'] },
3500
+ * row: { id: 'post-1', author_id: 'user-1', status: 'draft' }
3501
+ * });
3502
+ *
3503
+ * expect(result.allowed).toBe(true);
3504
+ * });
3505
+ *
3506
+ * it('should deny non-owner update', async () => {
3507
+ * const result = await tester.evaluate('posts', 'update', {
3508
+ * auth: { userId: 'user-2', roles: ['user'] },
3509
+ * row: { id: 'post-1', author_id: 'user-1', status: 'draft' }
3510
+ * });
3511
+ *
3512
+ * expect(result.allowed).toBe(false);
3513
+ * expect(result.reason).toContain('not owner');
3514
+ * });
3515
+ *
3516
+ * it('should apply filters correctly', async () => {
3517
+ * const filters = await tester.getFilters('posts', 'read', {
3518
+ * auth: { userId: 'user-1', tenantId: 'tenant-1', roles: [] }
3519
+ * });
3520
+ *
3521
+ * expect(filters.conditions).toEqual({
3522
+ * tenant_id: 'tenant-1',
3523
+ * deleted_at: null
3524
+ * });
3525
+ * });
3526
+ * });
3527
+ * ```
3528
+ */
3529
+ declare class PolicyTester<DB = unknown> {
3530
+ private registry;
3531
+ constructor(schema: RLSSchema<DB>);
3532
+ /**
3533
+ * Evaluate policies for an operation
3534
+ *
3535
+ * @param table - Table name
3536
+ * @param operation - Operation to test
3537
+ * @param context - Test context
3538
+ * @returns Evaluation result
3539
+ */
3540
+ evaluate(table: string, operation: Operation, context: TestContext): Promise<PolicyEvaluationResult>;
3541
+ /**
3542
+ * Get filter conditions for read operations
3543
+ *
3544
+ * @param table - Table name
3545
+ * @param operation - Must be 'read'
3546
+ * @param context - Test context
3547
+ * @returns Filter conditions
3548
+ */
3549
+ getFilters(table: string, _operation: 'read', context: Pick<TestContext, 'auth' | 'meta'>): FilterEvaluationResult;
3550
+ /**
3551
+ * Test if a specific policy allows the operation
3552
+ *
3553
+ * @param table - Table name
3554
+ * @param policyName - Name of the policy to test
3555
+ * @param context - Test context
3556
+ * @returns True if policy allows
3557
+ */
3558
+ testPolicy(table: string, policyName: string, context: TestContext): Promise<{
3559
+ found: boolean;
3560
+ result?: boolean;
3561
+ }>;
3562
+ /**
3563
+ * List all policies for a table
3564
+ */
3565
+ listPolicies(table: string): {
3566
+ allows: string[];
3567
+ denies: string[];
3568
+ filters: string[];
3569
+ validates: string[];
3570
+ };
3571
+ /**
3572
+ * Get all registered tables
3573
+ */
3574
+ getTables(): string[];
3575
+ /**
3576
+ * Evaluate a single policy
3577
+ */
3578
+ private evaluatePolicy;
3579
+ }
3580
+ /**
3581
+ * Create a policy tester
3582
+ *
3583
+ * @param schema - RLS schema to test
3584
+ * @returns PolicyTester instance
3585
+ */
3586
+ declare function createPolicyTester<DB = unknown>(schema: RLSSchema<DB>): PolicyTester<DB>;
3587
+ /**
3588
+ * Create a test auth context
3589
+ *
3590
+ * @param overrides - Values to override
3591
+ * @returns RLSAuthContext for testing
3592
+ */
3593
+ declare function createTestAuthContext(overrides: Partial<RLSAuthContext> & {
3594
+ userId: string | number;
3595
+ }): RLSAuthContext;
3596
+ /**
3597
+ * Create a test row
3598
+ *
3599
+ * @param data - Row data
3600
+ * @returns Row object
3601
+ */
3602
+ declare function createTestRow<T extends Record<string, unknown>>(data: T): T;
3603
+ /**
3604
+ * Assertion helpers for policy testing
3605
+ */
3606
+ declare const policyAssertions: {
3607
+ /**
3608
+ * Assert that the result is allowed
3609
+ */
3610
+ assertAllowed(result: PolicyEvaluationResult, message?: string): void;
3611
+ /**
3612
+ * Assert that the result is denied
3613
+ */
3614
+ assertDenied(result: PolicyEvaluationResult, message?: string): void;
3615
+ /**
3616
+ * Assert that a specific policy made the decision
3617
+ */
3618
+ assertPolicyUsed(result: PolicyEvaluationResult, policyName: string, message?: string): void;
3619
+ /**
3620
+ * Assert that filters include expected conditions
3621
+ */
3622
+ assertFiltersInclude(result: FilterEvaluationResult, expected: Record<string, unknown>, message?: string): void;
3623
+ };
3624
+
3625
+ export { type AuditConfig, type AuditDecision, AuditLogger, type AuditQueryParams, type AuditStats, type BasePolicyDefinition, type BaseResolverContext, type CommonResolvedData, type CompiledFieldAccess, CompiledFilterPolicy, CompiledPolicy, type CompiledReBAcPolicy, type CompiledRelationshipPath, type CompiledTableFieldAccess, type ComposableRLSSchema, type ComposableTableConfig, type CompositeResolvedData, ConditionalPolicyDefinition, ConsoleAuditAdapter, type ConsoleAuditAdapterOptions, type ContextResolver, type CreateRLSContextOptions, type EnhancedRLSAuthContext, type EnhancedRLSContext, type FieldAccessCondition, type FieldAccessConfig, type FieldAccessOptions, FieldAccessProcessor, FieldAccessRegistry, type FieldAccessResult, type FieldAccessSchema, type FieldOperation, FilterCondition, type FilterEvaluationResult, type HierarchyResolvedData, InMemoryAuditAdapter, InMemoryCacheProvider, type MaskedRow, Operation, type OrganizationResolvedData, type OwnershipConfig, PolicyActivationCondition, PolicyCondition, PolicyDefinition, PolicyEvaluationContext, type PolicyEvaluationResult, PolicyHints, type PolicyOptions, PolicyRegistry, PolicyTester, type RLSAuditAdapter, type RLSAuditEvent, RLSAuthContext, RLSContext, RLSContextError, RLSContextValidationError, RLSError, type RLSErrorCode, RLSErrorCodes, type RLSPluginOptions, RLSPluginOptionsSchema, RLSPolicyEvaluationError, RLSPolicyViolation, RLSRequestContext, RLSSchema, RLSSchemaError, type ReBAcPolicyDefinition, type ReBAcQueryOptions, ReBAcRegistry, type ReBAcSchema, type ReBAcSubquery, ReBAcTransformer, type RelationshipCondition, type RelationshipPath, type RelationshipStep, type ResolvedData, type ResolvedInheritance, type ResolverCacheProvider, ResolverManager, type ResolverManagerOptions, type ReusablePolicy, type ReusablePolicyConfig, type SoftDeleteConfig, type StatusAccessConfig, type TableAuditConfig, type TableFieldAccessConfig, TableRLSConfig, type TableReBAcConfig, type TenantIsolationConfig, type TenantResolvedData, type TestContext, allow, allowRelation, composePolicies, createAdminPolicy, createAuditLogger, createEvaluationContext, createFieldAccessProcessor, createFieldAccessRegistry, createOwnershipPolicy, createPolicyTester, createRLSContext, createReBAcRegistry, createReBAcTransformer, createResolver, createResolverManager, createSoftDeletePolicy, createStatusAccessPolicy, createTenantIsolationPolicy, createTestAuthContext, createTestRow, deepMerge, defineAllowPolicy, defineCombinedPolicy, defineDenyPolicy, defineFilterPolicy, definePolicy, defineRLSSchema, defineValidatePolicy, deny, denyRelation, extendPolicy, filter, hashString, isAsyncFunction, maskedField, mergeRLSSchemas, neverAccessible, normalizeOperations, orgMembershipPath, overridePolicy, ownerOnly, ownerOrRoles, policyAssertions, publicReadRestrictedWrite, readOnly, rlsContext, rlsPlugin, rolesOnly, safeEvaluate, shopOrgMembershipPath, teamHierarchyPath, validate, whenCondition, whenEnvironment, whenFeature, whenTimeRange, withRLSContext, withRLSContextAsync };