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-D3hQINlj.js'; export { k as PolicyActivationContext, j as PolicyType } from './types-D3hQINlj.js'; import { KyseraLogger, DatabaseError, ErrorCode } from '@kysera/core'; import { Plugin } from '@kysera/executor'; import { z } from 'zod'; import { SelectQueryBuilder } from 'kysely'; /** * RLS schema definition and validation * * Provides functions to define, validate, and merge RLS schemas. */ /** * Define RLS schema with full type safety * * @example * ```typescript * interface Database { * users: { id: number; email: string; tenant_id: number }; * posts: { id: number; user_id: number; tenant_id: number }; * } * * const schema = defineRLSSchema({ * users: { * policies: [ * // Users can read their own records * allow('read', ctx => ctx.auth.userId === ctx.row.id), * // Filter by tenant * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })), * // Admins bypass all checks * allow('all', ctx => ctx.auth.roles.includes('admin')), * ], * }, * posts: { * policies: [ * // Filter posts by tenant * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })), * // Users can only edit their own posts * allow(['update', 'delete'], ctx => ctx.auth.userId === ctx.row.user_id), * // Validate new posts belong to user's tenant * validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId), * ], * defaultDeny: true, // Require explicit allow * }, * }); * ``` */ declare function defineRLSSchema(schema: RLSSchema): RLSSchema; /** * Merge multiple RLS schemas * Later schemas override earlier ones for the same table * * @example * ```typescript * const baseSchema = defineRLSSchema({ * users: { * policies: [ * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })), * ], * }, * }); * * const adminSchema = defineRLSSchema({ * users: { * policies: [ * allow('all', ctx => ctx.auth.roles.includes('admin')), * ], * }, * }); * * // Merged schema will have both filters and admin allow * const merged = mergeRLSSchemas(baseSchema, adminSchema); * ``` */ declare function mergeRLSSchemas(...schemas: RLSSchema[]): RLSSchema; /** * Fluent policy builders for Row-Level Security * * Provides intuitive builder functions for creating RLS policies: * - allow: Grants access when condition is true * - deny: Blocks access when condition is true (overrides allow) * - filter: Adds WHERE conditions to SELECT queries * - validate: Validates mutation data before execution */ /** * Options for policy definitions */ interface PolicyOptions { /** Policy name for debugging and identification */ name?: string; /** Priority (higher runs first, deny policies default to 100) */ priority?: number; /** Performance optimization hints */ hints?: PolicyHints; /** * Condition that determines if this policy is active * The policy will only be evaluated if this returns true * * @example * ```typescript * // Only apply in production * allow('read', () => true, { * condition: ctx => ctx.meta?.environment === 'production' * }) * * // Feature-gated policy * filter('read', ctx => ({ strict: true }), { * condition: ctx => ctx.meta?.features?.strictMode * }) * ``` */ condition?: PolicyActivationCondition; } /** * Create an allow policy * Grants access when condition evaluates to true * * @example * ```typescript * // Allow users to read their own records * allow('read', ctx => ctx.auth.userId === ctx.row.userId) * * // Allow admins to do everything * allow('all', ctx => ctx.auth.roles.includes('admin')) * * // Allow with multiple operations * allow(['read', 'update'], ctx => ctx.auth.userId === ctx.row.userId) * * // Named policy with priority * allow('read', ctx => ctx.auth.roles.includes('verified'), { * name: 'verified-users-only', * priority: 10 * }) * ``` */ declare function allow(operation: Operation | Operation[], condition: PolicyCondition, options?: PolicyOptions): ConditionalPolicyDefinition; /** * Create a deny policy * Blocks access when condition evaluates to true (overrides allow) * If no condition is provided, always denies * * @example * ```typescript * // Deny access to banned users * deny('all', ctx => ctx.auth.attributes?.banned === true) * * // Deny deletions on archived records * deny('delete', ctx => ctx.row.archived === true) * * // Deny all access to sensitive table * deny('all') * * // Named deny with high priority * deny('all', ctx => ctx.auth.attributes?.suspended === true, { * name: 'block-suspended-users', * priority: 200 * }) * ``` */ declare function deny(operation: Operation | Operation[], condition?: PolicyCondition, options?: PolicyOptions): ConditionalPolicyDefinition; /** * Create a filter policy * Adds WHERE conditions to SELECT queries * * **IMPORTANT**: Filter conditions must be synchronous functions. * Async filter policies are not currently supported because filters are applied * directly to query builders at query construction time. * * @example * ```typescript * // ✅ CORRECT: Filter by tenant (synchronous) * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })) * * // ✅ CORRECT: Filter by organization with soft delete * filter('read', ctx => ({ * organization_id: ctx.auth.organizationIds?.[0], * deleted_at: null * })) * * // ❌ WRONG: Async filter (not supported) * // filter('read', async ctx => { * // const tenantId = await fetchTenantId(ctx.auth.userId) * // return { tenant_id: tenantId } * // }) * * // ✅ WORKAROUND: Fetch data before creating context * // const tenantId = await fetchTenantId(userId) * // const ctx = createRLSContext({ auth: { userId, tenantId, roles: [] } }) * // filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })) * * // Named filter * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }), { * name: 'tenant-filter' * }) * ``` */ declare function filter(operation: 'read' | 'all', condition: FilterCondition, options?: PolicyOptions): ConditionalPolicyDefinition; /** * Create a validate policy * Validates mutation data before execution * * @example * ```typescript * // Validate user can only set their own user_id * validate('create', ctx => ctx.data.userId === ctx.auth.userId) * * // Validate status transitions * validate('update', ctx => { * const { status } = ctx.data; * return !status || ['draft', 'published'].includes(status); * }) * * // Apply to both create and update * validate('all', ctx => ctx.data.price >= 0) * * // Named validation * validate('create', ctx => validateEmail(ctx.data.email), { * name: 'validate-email' * }) * ``` */ declare function validate(operation: 'create' | 'update' | 'all', condition: PolicyCondition, options?: PolicyOptions): ConditionalPolicyDefinition; /** * Create a policy that is only active in specific environments * * @param environments - Environments where the policy is active * @param policyFn - Function that creates the policy * @returns Policy with environment condition * * @example * ```typescript * // Policy only active in production * const prodPolicy = whenEnvironment(['production'], () => * allow('read', () => true, { name: 'prod-read' }) * ); * * // Policy active in staging and production * const nonDevPolicy = whenEnvironment(['staging', 'production'], () => * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })) * ); * ``` */ declare function whenEnvironment(environments: string[], policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition; /** * Create a policy that is only active when a feature flag is enabled * * @param feature - Feature flag name * @param policyFn - Function that creates the policy * @returns Policy with feature flag condition * * @example * ```typescript * // Policy only active when 'strict_rls' feature is enabled * const strictPolicy = whenFeature('strict_rls', () => * deny('delete', () => true, { name: 'strict-no-delete' }) * ); * ``` */ declare function whenFeature(feature: string, policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition; /** * Create a policy that is only active during specific hours * * @param startHour - Start hour (0-23) * @param endHour - End hour (0-23) * @param policyFn - Function that creates the policy * @returns Policy with time-based condition * * @example * ```typescript * // Policy only active during business hours (9 AM - 5 PM) * const businessHoursPolicy = whenTimeRange(9, 17, () => * allow('update', () => true, { name: 'business-hours-update' }) * ); * ``` */ declare function whenTimeRange(startHour: number, endHour: number, policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition; /** * Create a policy that is only active when a custom condition is met * * @param condition - Custom activation condition * @param policyFn - Function that creates the policy * @returns Policy with custom condition * * @example * ```typescript * // Policy only active when user is in beta program * const betaPolicy = whenCondition( * ctx => ctx.meta?.betaUser === true, * () => allow('read', () => true, { name: 'beta-read' }) * ); * ``` */ declare function whenCondition(condition: PolicyActivationCondition, policyFn: () => ConditionalPolicyDefinition): ConditionalPolicyDefinition; /** * Policy Registry * Central registry for managing RLS policies across all tables * * The PolicyRegistry compiles and stores RLS policies for efficient runtime lookup. * It categorizes policies by type (allow/deny/filter/validate) and operation, * and provides methods to query policies for specific tables and operations. */ /** * Policy Registry * Manages and provides access to RLS policies */ declare class PolicyRegistry { private tables; private compiled; private logger; constructor(schema?: RLSSchema, options?: { logger?: KyseraLogger; }); /** * Load and compile policies from schema * * @example * ```typescript * const registry = new PolicyRegistry(); * registry.loadSchema({ * users: { * policies: [ * allow('read', ctx => ctx.auth.userId === ctx.row.id), * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })), * ], * defaultDeny: true, * }, * }); * ``` */ loadSchema(schema: RLSSchema): void; /** * Register policies for a single table * * @param table - Table name * @param config - Table RLS configuration */ registerTable(table: string, config: TableRLSConfig): void; /** * Register policies - supports both schema and table-based registration * * @overload Register a full schema * @overload Register policies for a single table (deprecated) */ register(schemaOrTable: RLSSchema): void; register(schemaOrTable: keyof DB & string, policies: PolicyDefinition[], options?: { skipFor?: string[]; defaultDeny?: boolean; }): void; /** * Compile a policy definition into an internal compiled policy * * @param policy - Policy definition to compile * @param name - Policy name for debugging * @returns Compiled policy ready for evaluation */ private compilePolicy; /** * Compile a filter policy * * @param policy - Filter policy definition * @param name - Policy name for debugging * @returns Compiled filter policy */ private compileFilterPolicy; /** * Convert internal compiled policy to public CompiledPolicy */ private toCompiledPolicy; /** * Get allow policies for a table and operation */ getAllows(table: string, operation: Operation): CompiledPolicy[]; /** * Get deny policies for a table and operation */ getDenies(table: string, operation: Operation): CompiledPolicy[]; /** * Get validate policies for a table and operation */ getValidates(table: string, operation: Operation): CompiledPolicy[]; /** * Get filter policies for a table */ getFilters(table: string): CompiledFilterPolicy[]; /** * Get roles that skip RLS for a table */ getSkipFor(table: string): string[]; /** * Check if table has default deny */ hasDefaultDeny(table: string): boolean; /** * Check if a table is registered */ hasTable(table: string): boolean; /** * Get all registered table names */ getTables(): string[]; /** * Check if registry is compiled */ isCompiled(): boolean; /** * Validate that all policies are properly defined * * This method checks for common issues: * - Tables with no policies and defaultDeny=false (warns) * - Tables with skipFor operations but no corresponding policies */ validate(): void; /** * Clear all policies */ clear(): void; /** * Remove policies for a specific table */ remove(table: string): void; } /** * RLS Error Classes * * This module provides specialized error classes for Row-Level Security operations. * All errors extend base error classes from @kysera/core for consistency across * the Kysera ecosystem. * * @module @kysera/rls/errors */ /** * RLS-specific error codes * * These codes extend the unified error codes from @kysera/core with * RLS-specific error conditions. */ declare const RLSErrorCodes: { /** RLS context is missing or not set */ readonly RLS_CONTEXT_MISSING: ErrorCode; /** RLS policy violation occurred */ readonly RLS_POLICY_VIOLATION: ErrorCode; /** RLS policy definition is invalid */ readonly RLS_POLICY_INVALID: ErrorCode; /** RLS schema definition is invalid */ readonly RLS_SCHEMA_INVALID: ErrorCode; /** RLS context validation failed */ readonly RLS_CONTEXT_INVALID: ErrorCode; /** RLS policy evaluation threw an error */ readonly RLS_POLICY_EVALUATION_ERROR: ErrorCode; }; /** * Type for RLS error codes */ type RLSErrorCode = (typeof RLSErrorCodes)[keyof typeof RLSErrorCodes]; /** * Base class for all RLS-related errors * * Extends DatabaseError from @kysera/core for consistency with other Kysera packages. * Provides common error functionality including error codes and JSON serialization. * * @example * ```typescript * throw new RLSError('Something went wrong', RLSErrorCodes.RLS_POLICY_INVALID); * ``` */ declare class RLSError extends DatabaseError { /** * Creates a new RLS error * * @param message - Error message * @param code - RLS error code */ constructor(message: string, code: RLSErrorCode); } /** * Error thrown when RLS context is missing * * This error occurs when an operation requiring RLS context is executed * outside of a context scope (i.e., without calling withRLSContext()). * * @example * ```typescript * // This will throw RLSContextError * const result = await db.selectFrom('posts').execute(); * * // Correct usage with context * await withRLSContext(rlsContext, async () => { * const result = await db.selectFrom('posts').execute(); * }); * ``` */ declare class RLSContextError extends RLSError { /** * Creates a new RLS context error * * @param message - Error message (defaults to standard message) */ constructor(message?: string); } /** * Error thrown when RLS context validation fails * * Extends RLSError as context validation failures are RLS-specific errors. * * This error occurs when the provided RLS context is invalid or missing * required fields. * * @example * ```typescript * // Missing required userId field * const invalidContext = { * auth: { * roles: ['user'] * // userId is missing! * }, * timestamp: new Date() * }; * * // This will throw RLSContextValidationError * validateRLSContext(invalidContext); * ``` */ declare class RLSContextValidationError extends RLSError { readonly field: string; /** * Creates a new context validation error * * @param message - Error message * @param field - Field that failed validation */ constructor(message: string, field: string); toJSON(): Record; } /** * Error thrown when an RLS policy violation occurs * * This error is thrown when a database operation is denied by RLS policies. * It provides detailed information about the violation including the operation, * table, and reason for denial. * * @example * ```typescript * // User tries to update a post they don't own * throw new RLSPolicyViolation( * 'update', * 'posts', * 'User does not own this post', * 'ownership_policy' * ); * ``` */ declare class RLSPolicyViolation extends RLSError { readonly operation: string; readonly table: string; readonly reason: string; readonly policyName?: string; /** * Creates a new policy violation error * * @param operation - Database operation that was denied (read, create, update, delete) * @param table - Table name where violation occurred * @param reason - Reason for the policy violation * @param policyName - Name of the policy that denied access (optional) */ constructor(operation: string, table: string, reason: string, policyName?: string); toJSON(): Record; } /** * Error thrown when a policy condition throws an error during evaluation * * This error is distinct from RLSPolicyViolation - it indicates a bug in the * policy condition function itself, not a legitimate access denial. * * @example * ```typescript * // A policy with a bug * allow('read', ctx => { * return ctx.row.someField.value; // Throws if someField is undefined * }); * * // This will throw RLSPolicyEvaluationError, not RLSPolicyViolation * ``` */ declare class RLSPolicyEvaluationError extends RLSError { readonly operation: string; readonly table: string; readonly policyName?: string; readonly originalError?: Error; /** * Creates a new policy evaluation error * * @param operation - Database operation being performed * @param table - Table name where error occurred * @param message - Error message from the policy * @param policyName - Name of the policy that threw * @param originalError - The original error thrown by the policy */ constructor(operation: string, table: string, message: string, policyName?: string, originalError?: Error); toJSON(): Record; } /** * Error thrown when RLS schema validation fails * * Extends RLSError as schema validation failures are RLS-specific errors. * * This error occurs when the RLS schema definition is invalid or contains * configuration errors. * * @example * ```typescript * // Invalid policy definition * const invalidSchema = { * posts: { * policies: [ * { * type: 'invalid-type', // Invalid policy type! * operation: 'read', * condition: (ctx) => true * } * ] * } * }; * * // This will throw RLSSchemaError * validateRLSSchema(invalidSchema); * ``` */ declare class RLSSchemaError extends RLSError { readonly details: Record; /** * Creates a new schema validation error * * @param message - Error message * @param details - Additional details about the validation failure */ constructor(message: string, details?: Record); toJSON(): Record; } /** * RLS Plugin for Kysera Repository * * Implements Row-Level Security as a Kysera plugin, providing: * - Automatic query filtering for SELECT operations * - Policy enforcement for CREATE, UPDATE, DELETE operations * - Repository method extensions for RLS-aware operations * - System context bypass for privileged operations * * @module @kysera/rls */ /** * RLS Plugin configuration options */ interface RLSPluginOptions { /** RLS policy schema */ schema: RLSSchema; /** * Whitelist of tables to apply RLS to. * If provided, only these tables will have RLS enforced. * Takes precedence over excludeTables when both are provided. */ tables?: string[]; /** * Tables to exclude from RLS (always bypass policies). * Ignored if `tables` whitelist is provided. */ excludeTables?: string[]; /** Roles that bypass RLS entirely (e.g., ['admin', 'superuser']) */ bypassRoles?: string[]; /** Logger instance for RLS operations */ logger?: KyseraLogger; /** * Require RLS context for all operations (throws if missing) * * **Security**: Defaults to `true` for secure-by-default behavior. * When `true`, missing RLS context throws RLSContextError, preventing * unfiltered database access which could expose sensitive data. * * Only set to `false` if you explicitly want to allow queries without * RLS context (not recommended in production). * * @default true * @see allowUnfilteredQueries for explicit unfiltered query control */ requireContext?: boolean; /** * Allow unfiltered queries when RLS context is missing * * **SECURITY WARNING**: Setting this to `true` allows database queries * to execute without RLS filtering when context is missing. This can * expose sensitive data across tenant boundaries or user permissions. * * Only enable this if you: * 1. Understand the security implications * 2. Have other security controls in place * 3. Are running background jobs or system operations that don't have user context * * When both `requireContext: false` and `allowUnfilteredQueries: false`: * - Missing context logs a warning and returns empty results * * @default false (secure-by-default) */ allowUnfilteredQueries?: boolean; /** Enable audit logging of policy decisions */ auditDecisions?: boolean; /** Custom error handler for policy violations */ onViolation?: (violation: RLSPolicyViolation) => void; /** * Primary key column name for row lookups. * @default 'id' */ primaryKeyColumn?: string; } /** * Zod schema for RLSPluginOptions * Used for validation and configuration in the kysera-cli. * Note: 'schema' and 'onViolation' are not included as they are complex runtime objects. */ declare const RLSPluginOptionsSchema: z.ZodObject<{ tables: z.ZodOptional>; excludeTables: z.ZodOptional>; bypassRoles: z.ZodOptional>; requireContext: z.ZodOptional; allowUnfilteredQueries: z.ZodOptional; auditDecisions: z.ZodOptional; primaryKeyColumn: z.ZodOptional; }, z.core.$strip>; /** * Create RLS plugin for Kysera * * The RLS plugin provides declarative row-level security for your database operations. * It automatically filters SELECT queries and validates mutations (CREATE, UPDATE, DELETE) * against your policy schema. * * @example * ```typescript * import { rlsPlugin, defineRLSSchema, allow, filter } from '@kysera/rls'; * import { createORM } from '@kysera/repository'; * * // Define your RLS schema * const schema = defineRLSSchema({ * resources: { * policies: [ * // Filter reads by tenant * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })), * // Allow updates for resource owners * allow('update', ctx => ctx.auth.userId === ctx.row.owner_id), * // Validate creates belong to user's tenant * validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId), * ], * }, * }); * * // Create repository with RLS plugin * const orm = await createORM(db, [ * rlsPlugin({ schema }), * ]); * * // Use within RLS context * await rlsContext.runAsync( * { * auth: { userId: 1, tenantId: 100, roles: ['user'], isSystem: false }, * timestamp: new Date(), * }, * async () => { * // All queries automatically filtered by tenant_id * const resources = await orm.resources.findAll(); * } * ); * ``` * * @param options - Plugin configuration options * @returns Kysera plugin instance */ declare function rlsPlugin(options: RLSPluginOptions): Plugin; /** * Options for creating RLS context */ interface CreateRLSContextOptions { auth: RLSAuthContext; request?: Partial; meta?: TMeta; } /** * Create a new RLS context */ declare function createRLSContext(options: CreateRLSContextOptions): RLSContext; /** * RLS Context Manager * Manages RLS context using AsyncLocalStorage for automatic propagation */ declare class RLSContextManager { /** * Run a synchronous function within an RLS context */ run(context: RLSContext, fn: () => T): T; /** * Run an async function within an RLS context */ runAsync(context: RLSContext, fn: () => Promise): Promise; /** * Get current RLS context * @throws RLSContextError if no context is set */ getContext(): RLSContext; /** * Get current RLS context or null if not set */ getContextOrNull(): RLSContext | null; /** * Check if running within RLS context */ hasContext(): boolean; /** * Get current auth context * @throws RLSContextError if no context is set */ getAuth(): RLSAuthContext; /** * Get current user ID * @throws RLSContextError if no context is set */ getUserId(): string | number; /** * Get current tenant ID * @throws RLSContextError if no context is set */ getTenantId(): string | number | undefined; /** * Check if current user has a specific role */ hasRole(role: string): boolean; /** * Check if current user has a specific permission */ hasPermission(permission: string): boolean; /** * Check if current context is a system context (bypasses RLS) */ isSystem(): boolean; /** * Create a system context for operations that should bypass RLS */ asSystem(fn: () => T): T; /** * Create a system context for async operations */ asSystemAsync(fn: () => Promise): Promise; } declare const rlsContext: RLSContextManager; /** * Convenience function to run code within RLS context */ declare function withRLSContext(context: RLSContext, fn: () => T): T; /** * Convenience function to run async code within RLS context */ declare function withRLSContextAsync(context: RLSContext, fn: () => Promise): Promise; /** * Utility helper functions for RLS */ /** * Create a policy evaluation context from RLS context */ declare function createEvaluationContext(rlsCtx: RLSContext, options?: { row?: TRow; data?: TData; }): PolicyEvaluationContext; /** * Check if a condition function is async * * NOTE: This function checks both constructor.name (for native async functions) * and return type (for transpiled code that returns Promise). * Transpilers often convert async functions to regular functions that return Promise. */ declare function isAsyncFunction(fn: unknown): fn is (...args: unknown[]) => Promise; /** * Safely evaluate a policy condition */ declare function safeEvaluate(fn: () => T | Promise, defaultValue: T): Promise; /** * Deep merge two objects */ declare function deepMerge>(target: T, source: Partial): T; /** * Create a simple hash for cache keys */ declare function hashString(str: string): string; /** * Normalize operations to array format */ declare function normalizeOperations(operation: Operation | Operation[]): Operation[]; /** * Context Resolver Types * * Provides infrastructure for pre-resolving async data before RLS policy evaluation. * This allows synchronous filters to access data that would otherwise require async lookups. * * @module @kysera/rls/resolvers/types */ /** * Base interface for resolved data that can be added to RLS context * * @example * ```typescript * interface MyResolvedData extends ResolvedData { * organizationIds: string[]; * permissions: Set; * employeeRoles: Map; * } * ``` */ interface ResolvedData { /** * Timestamp when data was resolved * Used for cache validation */ resolvedAt: Date; /** * Cache key used for this resolution (if cached) */ cacheKey?: string; } /** * Extended auth context with pre-resolved data * * @typeParam TUser - Custom user type * @typeParam TResolved - Type of pre-resolved data * * @example * ```typescript * interface OrgPermissions extends ResolvedData { * organizationIds: string[]; * orgPermissions: Map>; * isOrgOwner: (orgId: string) => boolean; * hasOrgPermission: (orgId: string, permission: string) => boolean; * } * * type EnhancedAuth = EnhancedRLSAuthContext; * * // Use in policy * filter('read', ctx => ({ * organization_id: ctx.auth.resolved.organizationIds * })); * ``` */ interface EnhancedRLSAuthContext extends RLSAuthContext { /** * Pre-resolved data available synchronously in policies * * This data is populated by ContextResolvers before entering the RLS context. * Use this for async data lookups that policies need synchronously. */ resolved: TResolved; } /** * Extended RLS context with enhanced auth containing resolved data * * @typeParam TUser - Custom user type * @typeParam TResolved - Type of pre-resolved data * @typeParam TMeta - Custom metadata type */ interface EnhancedRLSContext extends Omit, 'auth'> { auth: EnhancedRLSAuthContext; } /** * Base context for resolver input (before resolution) */ interface BaseResolverContext { auth: { userId: string | number; roles: string[]; tenantId?: string | number; organizationIds?: (string | number)[]; permissions?: string[]; attributes?: Record; isSystem?: boolean; }; timestamp: Date; meta?: unknown; } /** * Context resolver that enriches base context with pre-resolved data * * Resolvers are responsible for fetching async data and making it available * synchronously in policy evaluation contexts. * * @typeParam TResolved - Type of resolved data this resolver produces * * @example * ```typescript * const orgPermissionResolver: ContextResolver = { * name: 'org-permissions', * * async resolve(base) { * const employments = await db.selectFrom('employees') * .where('user_id', '=', base.auth.userId) * .where('status', '=', 'active') * .execute(); * * const orgPermissions = new Map>(); * // ... resolve permissions ... * * return { * resolvedAt: new Date(), * organizationIds: employments.map(e => e.organization_id), * orgPermissions, * isOrgOwner: (orgId) => employments.some(e => e.organization_id === orgId && e.is_owner), * hasOrgPermission: (orgId, permission) => { * const perms = orgPermissions.get(orgId); * return perms?.has('*') || perms?.has(permission) || false; * } * }; * }, * * cacheKey: (base) => `rls:org-perms:${base.auth.userId}`, * cacheTtl: 300 // 5 minutes * }; * ``` */ interface ContextResolver { /** * Unique name for this resolver * Used for logging and debugging */ name: string; /** * Resolve async data for the context * * @param base - Base context with user info * @returns Pre-resolved data to be added to context */ resolve(base: BaseResolverContext): Promise; /** * Generate cache key for this context * Return undefined to disable caching for this resolver * * @param base - Base context * @returns Cache key string or undefined */ cacheKey?(base: BaseResolverContext): string | undefined; /** * Cache TTL in seconds * @default 300 (5 minutes) */ cacheTtl?: number; /** * Whether this resolver is required * If true, resolution failure will throw an error * If false, the resolver will be skipped on failure * * @default true */ required?: boolean; /** * Dependencies on other resolvers (by name) * This resolver will wait for dependencies to complete first */ dependsOn?: string[]; /** * Priority for resolver execution order (higher = earlier) * @default 0 */ priority?: number; } /** * Combined result of multiple resolvers * * @typeParam T - Union type of all resolved data types */ interface CompositeResolvedData> extends ResolvedData { /** * Individual resolver results keyed by resolver name */ resolvers: T; } /** * Cache provider interface for storing resolved context data */ interface ResolverCacheProvider { /** * Get cached data * @param key - Cache key * @returns Cached data or null if not found/expired */ get(key: string): Promise; /** * Set cached data * @param key - Cache key * @param value - Data to cache * @param ttlSeconds - Time to live in seconds */ set(key: string, value: T, ttlSeconds: number): Promise; /** * Delete cached data * @param key - Cache key */ delete(key: string): Promise; /** * Delete all cached data matching a pattern * @param pattern - Pattern to match (e.g., "rls:org-perms:*") */ deletePattern?(pattern: string): Promise; } /** * In-memory cache provider implementation * * Suitable for single-instance deployments or testing. * For distributed systems, use a Redis-based provider. */ declare class InMemoryCacheProvider implements ResolverCacheProvider { private cache; get(key: string): Promise; set(key: string, value: T, ttlSeconds: number): Promise; delete(key: string): Promise; deletePattern(pattern: string): Promise; /** * Clear all cached entries */ clear(): void; /** * Get current cache size */ get size(): number; } /** * Options for ResolverManager */ interface ResolverManagerOptions { /** * Cache provider for storing resolved data * @default InMemoryCacheProvider */ cacheProvider?: ResolverCacheProvider; /** * Default cache TTL in seconds * @default 300 (5 minutes) */ defaultCacheTtl?: number; /** * Whether to run resolvers in parallel when possible * @default true */ parallelResolution?: boolean; /** * Maximum time (ms) to wait for a single resolver * @default 5000 (5 seconds) */ resolverTimeout?: number; /** * Logger for resolver operations */ logger?: { debug?: (message: string, context?: Record) => void; info?: (message: string, context?: Record) => void; warn?: (message: string, context?: Record) => void; error?: (message: string, context?: Record) => void; }; } /** * Common resolved data for organization-based permissions * * Pre-built pattern for multi-organization systems where users can * belong to multiple organizations with different roles/permissions. */ interface OrganizationResolvedData extends ResolvedData { /** * List of organization IDs the user belongs to */ organizationIds: (string | number)[]; /** * Map of organization ID to user's permissions in that org */ orgPermissions: Map>; /** * Map of organization ID to user's roles in that org */ orgRoles: Map; /** * Check if user is owner of an organization * @param orgId - Organization ID */ isOrgOwner(orgId: string | number): boolean; /** * Check if user has a specific permission in an organization * @param orgId - Organization ID * @param permission - Permission to check */ hasOrgPermission(orgId: string | number, permission: string): boolean; /** * Check if user has a specific role in an organization * @param orgId - Organization ID * @param role - Role to check */ hasOrgRole(orgId: string | number, role: string): boolean; } /** * Common resolved data for tenant-based systems */ interface TenantResolvedData extends ResolvedData { /** * Current tenant ID (resolved from user context) */ tenantId: string | number; /** * Tenant-specific settings/restrictions */ tenantSettings?: Record; /** * Tenant-specific feature flags */ tenantFeatures?: Set; } /** * Common resolved data for hierarchical permissions * * For systems with resource hierarchies (e.g., team -> project -> task) */ interface HierarchyResolvedData extends ResolvedData { /** * Resources the user has direct access to */ directAccess: Set; /** * Resources the user has inherited access to (through hierarchy) */ inheritedAccess: Set; /** * Check if user can access a resource (direct or inherited) * @param resourceId - Resource ID */ canAccess(resourceId: string): boolean; /** * Get the access level for a resource * @param resourceId - Resource ID * @returns Access level or null if no access */ getAccessLevel(resourceId: string): string | null; } /** * Combined resolved data type for common use cases */ type CommonResolvedData = OrganizationResolvedData & TenantResolvedData; /** * Context Resolver Manager * * Orchestrates the resolution of context data from multiple resolvers, * handling caching, dependencies, and parallel execution. * * @module @kysera/rls/resolvers/manager */ /** * Manages context resolvers and orchestrates context resolution * * The ResolverManager is responsible for: * - Registering and organizing resolvers * - Resolving context data in the correct order (respecting dependencies) * - Caching resolved data * - Handling resolver failures * * @example * ```typescript * const manager = new ResolverManager({ * cacheProvider: new RedisCacheProvider(redis), * defaultCacheTtl: 300, * parallelResolution: true * }); * * // Register resolvers * manager.register(orgPermissionResolver); * manager.register(tenantSettingsResolver); * * // Resolve context * const enhancedCtx = await manager.resolve({ * auth: { userId: '123', roles: ['user'] }, * timestamp: new Date() * }); * * // Use in RLS * await rlsContext.runAsync(enhancedCtx, async () => { * // Policies can access resolved data synchronously * }); * ``` */ declare class ResolverManager { private resolvers; private cacheProvider; private defaultCacheTtl; private parallelResolution; private resolverTimeout; private logger; constructor(options?: ResolverManagerOptions); /** * Register a context resolver * * @param resolver - Resolver to register * @throws RLSError if resolver with same name already exists */ register(resolver: ContextResolver): void; /** * Unregister a context resolver * * @param name - Name of resolver to unregister * @returns true if resolver was removed, false if it didn't exist */ unregister(name: string): boolean; /** * Check if a resolver is registered * * @param name - Resolver name */ hasResolver(name: string): boolean; /** * Get all registered resolver names */ getResolverNames(): string[]; /** * Resolve context data using all registered resolvers * * @param baseContext - Base context to resolve * @returns Enhanced context with resolved data * * @example * ```typescript * const baseCtx = { * auth: { userId: '123', roles: ['user'], tenantId: 'acme' }, * timestamp: new Date() * }; * * const enhancedCtx = await manager.resolve(baseCtx); * // enhancedCtx.auth.resolved contains all resolved data * ``` */ resolve(baseContext: BaseResolverContext): Promise>; /** * Resolve a single resolver (useful for partial updates) * * @param name - Resolver name * @param baseContext - Base context * @returns Resolved data from the specific resolver */ resolveOne(name: string, baseContext: BaseResolverContext): Promise; /** * Invalidate cached data for a user * * @param userId - User ID whose cache should be invalidated * @param resolverName - Optional specific resolver to invalidate */ invalidateCache(userId: string | number, resolverName?: string): Promise; /** * Clear all cached data */ clearCache(): Promise; /** * Get resolvers in dependency order (topological sort) */ private getResolverOrder; /** * Resolve resolvers sequentially */ private resolveSequential; /** * Resolve resolvers in parallel (respecting dependencies) */ private resolveParallel; /** * Resolve a single resolver with caching */ private resolveWithCache; /** * Execute a promise with timeout */ private withTimeout; /** * Merge resolved data from multiple resolvers */ private mergeResolvedData; } /** * Create a context resolver manager with common defaults * * @param options - Manager options * @returns Configured ResolverManager */ declare function createResolverManager(options?: ResolverManagerOptions): ResolverManager; /** * Helper to create a context resolver * * @param config - Resolver configuration * @returns ContextResolver instance * * @example * ```typescript * const resolver = createResolver({ * name: 'org-permissions', * resolve: async (base) => { * const orgs = await getEmployeeOrganizations(base.auth.userId); * return { * resolvedAt: new Date(), * organizationIds: orgs.map(o => o.id) * }; * }, * cacheKey: (base) => `rls:org:${base.auth.userId}`, * cacheTtl: 300 * }); * ``` */ declare function createResolver(config: ContextResolver): ContextResolver; /** * ReBAC (Relationship-Based Access Control) Types * * Provides type definitions for relationship-based filtering in RLS policies. * ReBAC allows policies to be defined based on relationships between entities * in the database, enabling complex access control patterns like * "show products if user is employee of product's shop's organization". * * @module @kysera/rls/rebac/types */ /** * A single step in a relationship path * * Defines how to join from one table to another. * * @example * ```typescript * // Simple join: products.shop_id -> shops.id * const step: RelationshipStep = { * from: 'products', * to: 'shops', * fromColumn: 'shop_id', * toColumn: 'id' * }; * ``` */ interface RelationshipStep { /** * Source table name */ from: string; /** * Target table name */ to: string; /** * Column in source table for the join * @default 'id' on target table side, '{to}_id' on source side */ fromColumn?: string; /** * Column in target table for the join * @default 'id' */ toColumn?: string; /** * Optional alias for the target table in the join * Useful when joining the same table multiple times */ alias?: string; /** * Join type * @default 'inner' */ joinType?: 'inner' | 'left' | 'right'; /** * Additional conditions for this join step * @example { 'shops.deleted_at': null, 'shops.status': 'active' } */ additionalConditions?: Record; } /** * Complete relationship path definition * * Defines a chain of relationships from a source table to a target. * * @example * ```typescript * // Path: products -> shops -> organizations -> employees * const path: RelationshipPath = { * name: 'orgEmployee', * steps: [ * { from: 'products', to: 'shops', fromColumn: 'shop_id' }, * { from: 'shops', to: 'organizations', fromColumn: 'organization_id' }, * { from: 'organizations', to: 'employees', toColumn: 'organization_id' } * ] * }; * ``` */ interface RelationshipPath { /** * Unique name for this relationship path * Used in policy definitions to reference this path */ name: string; /** * Steps in the relationship chain */ steps: RelationshipStep[]; /** * Optional description for documentation */ description?: string; } /** * Condition to apply at the end of a relationship path * * @typeParam TCtx - Policy evaluation context type */ type RelationshipCondition = ((ctx: TCtx) => Record) | Record; /** * ReBAC policy definition * * Extends standard policy definition with relationship-based filtering. */ interface ReBAcPolicyDefinition extends Omit { /** * Name of the relationship path to use (defined in relationships config) */ relationshipPath: string; /** * Conditions to apply at the end of the relationship * These conditions filter the final table in the relationship chain. * * @example * ```typescript * // Filter employees table at end of relationship * endCondition: ctx => ({ * user_id: ctx.auth.userId, * status: 'active' * }) * ``` */ endCondition: RelationshipCondition; /** * Whether this is a permissive or restrictive policy * - 'allow': Row is accessible if relationship exists * - 'deny': Row is NOT accessible if relationship exists * @default 'allow' */ policyType?: 'allow' | 'deny'; } /** * ReBAC configuration for a single table */ interface TableReBAcConfig { /** * Relationship paths available for this table */ relationships: RelationshipPath[]; /** * ReBAC policies for this table */ policies: ReBAcPolicyDefinition[]; } /** * Complete ReBAC schema for all tables * * @typeParam DB - Database schema type */ type ReBAcSchema = { [K in keyof DB]?: TableReBAcConfig; }; /** * Compiled relationship path ready for query generation */ interface CompiledRelationshipPath { /** * Path name */ name: string; /** * Compiled join steps with defaults filled in */ steps: Required[]; /** * Source table (first table in the chain) */ sourceTable: string; /** * Target table (final table in the chain) */ targetTable: string; } /** * Compiled ReBAC policy ready for evaluation */ interface CompiledReBAcPolicy { /** * Policy name */ name: string; /** * Policy type (allow/deny) */ type: 'allow' | 'deny'; /** * Operations this policy applies to */ operations: Set; /** * Compiled relationship path */ relationshipPath: CompiledRelationshipPath; /** * Function to get end conditions */ getEndConditions: (ctx: TCtx) => Record; /** * Priority for policy evaluation */ priority: number; } /** * Generated EXISTS subquery for ReBAC filtering */ interface ReBAcSubquery { /** * SQL for the EXISTS subquery */ sql: string; /** * Parameter values for the subquery */ parameters: unknown[]; /** * Whether this is an allow (EXISTS) or deny (NOT EXISTS) check */ isNegated: boolean; } /** * Options for ReBAC query generation */ interface ReBAcQueryOptions { /** * Table alias for the main query table * @default table name */ mainTableAlias?: string; /** * Whether to use qualified column names * @default true */ qualifyColumns?: boolean; /** * Database dialect for query generation * @default 'postgres' */ dialect?: 'postgres' | 'mysql' | 'sqlite'; } /** * Common relationship pattern: Resource belongs to organization via owner * * @param resourceTable - Table containing the resource * @param organizationColumn - Column linking to organization * * @example * ```typescript * const path = orgMembershipPath('products', 'organization_id'); * // Creates path: products -> organizations -> employees * ``` */ declare function orgMembershipPath(resourceTable: string, organizationColumn?: string): RelationshipPath; /** * Common relationship pattern: Resource belongs to shop's organization * * @param resourceTable - Table containing the resource * @param shopColumn - Column linking to shop * * @example * ```typescript * const path = shopOrgMembershipPath('products', 'shop_id'); * // Creates path: products -> shops -> organizations -> employees * ``` */ declare function shopOrgMembershipPath(resourceTable: string, shopColumn?: string): RelationshipPath; /** * Common relationship pattern: Hierarchical team access * * @param resourceTable - Table containing the resource * @param teamColumn - Column linking to team * * @example * ```typescript * const path = teamHierarchyPath('tasks', 'team_id'); * // Creates path: tasks -> teams -> team_members * ``` */ declare function teamHierarchyPath(resourceTable: string, teamColumn?: string): RelationshipPath; /** * ReBAC Policy Registry * * Manages relationship definitions and ReBAC policies for RLS. * * @module @kysera/rls/rebac/registry */ /** * ReBAC Registry * * Manages relationship paths and ReBAC policies across tables. * * @example * ```typescript * const registry = new ReBAcRegistry(); * * // Register relationship paths and policies * registry.loadSchema({ * products: { * relationships: [ * shopOrgMembershipPath('products', 'shop_id') * ], * policies: [ * { * type: 'filter', * operation: 'read', * relationshipPath: 'products_shop_org_membership', * endCondition: ctx => ({ * user_id: ctx.auth.userId, * status: 'active' * }) * } * ] * } * }); * * // Get policies for a table * const policies = registry.getPolicies('products', 'read'); * ``` */ declare class ReBAcRegistry { private tables; private globalRelationships; private logger; constructor(schema?: ReBAcSchema, options?: { logger?: KyseraLogger; }); /** * Load ReBAC schema */ loadSchema(schema: ReBAcSchema): void; /** * Register ReBAC configuration for a single table */ registerTable(table: string, config: TableReBAcConfig): void; /** * Register a global relationship path (available to all tables) */ registerRelationship(path: RelationshipPath): void; /** * Get ReBAC policies for a table and operation */ getPolicies(table: string, operation: Operation): CompiledReBAcPolicy[]; /** * Get a specific relationship path */ getRelationship(name: string, table?: string): CompiledRelationshipPath | undefined; /** * Check if table has ReBAC configuration */ hasTable(table: string): boolean; /** * Get all registered table names */ getTables(): string[]; /** * Clear all registrations */ clear(): void; /** * Compile a relationship path definition */ private compileRelationshipPath; /** * Compile a ReBAC policy definition */ private compilePolicy; } /** * Create a ReBAC registry */ declare function createReBAcRegistry(schema?: ReBAcSchema, options?: { logger?: KyseraLogger; }): ReBAcRegistry; /** * ReBAC Query Transformer * * Transforms queries to apply relationship-based access control policies. * Generates EXISTS subqueries that filter rows based on relationship chains. * * @module @kysera/rls/rebac/transformer */ /** * ReBAC query transformer * * Applies relationship-based access control to SELECT queries by generating * EXISTS subqueries that follow relationship paths. * * @example * ```typescript * const transformer = new ReBAcTransformer(registry); * * // Transform query * let query = db.selectFrom('products').selectAll(); * query = transformer.transform(query, 'products', 'read'); * * // Generated SQL includes EXISTS subquery: * // SELECT * FROM products p * // WHERE EXISTS ( * // SELECT 1 FROM shops s * // JOIN organizations o ON s.organization_id = o.id * // JOIN employees e ON e.organization_id = o.id * // WHERE s.id = p.shop_id * // AND e.user_id = $1 * // AND e.status = 'active' * // ) * ``` */ declare class ReBAcTransformer { private registry; private options; constructor(registry: ReBAcRegistry, options?: ReBAcQueryOptions); /** * Transform a SELECT query by applying ReBAC policies * * @param qb - Query builder to transform * @param table - Table being queried * @param operation - Operation being performed * @returns Transformed query builder */ transform(qb: SelectQueryBuilder, table: string, operation?: Operation): SelectQueryBuilder; /** * Generate EXISTS condition SQL for a policy * * This method can be used to get the raw SQL for debugging or manual query building. * * @param policy - ReBAC policy to generate SQL for * @param ctx - RLS context * @param mainTable - Main query table * @param mainTableAlias - Alias for main table * @returns SQL string and parameters */ generateExistsSql(policy: CompiledReBAcPolicy, ctx: RLSContext, mainTable: string, mainTableAlias?: string): { sql: string; params: unknown[]; }; /** * Apply a single ReBAC policy to a query * * NOTE: Uses type casting for dynamic SQL because Kysely's type system * requires compile-time known types, but ReBAC policies work with * runtime-generated EXISTS clauses. */ private applyPolicy; /** * Create evaluation context for policy conditions */ private createEvalContext; /** * Quote an identifier for the target dialect */ private quote; /** * Generate parameter placeholder for the target dialect */ private param; } /** * Create a ReBAC allow policy * * Rows are accessible if the relationship EXISTS with the given end conditions. * * @param operation - Operation(s) this policy applies to * @param relationshipPath - Name of the relationship path to use * @param endCondition - Conditions to apply at the end of the relationship * @param options - Additional policy options * * @example * ```typescript * // Allow read if user is employee of product's shop's organization * allowRelation('read', 'products_shop_org_membership', ctx => ({ * user_id: ctx.auth.userId, * status: 'active' * })) * ``` */ declare function allowRelation(operation: Operation | Operation[], relationshipPath: string, endCondition: ((ctx: PolicyEvaluationContext) => Record) | Record, options?: { name?: string; priority?: number; }): ReBAcPolicyDefinition; /** * Create a ReBAC deny policy * * Rows are NOT accessible if the relationship EXISTS with the given conditions. * * @param operation - Operation(s) this policy applies to * @param relationshipPath - Name of the relationship path to use * @param endCondition - Conditions to apply at the end of the relationship * @param options - Additional policy options * * @example * ```typescript * // Deny access if user is blocked in the organization * denyRelation('all', 'products_shop_org_membership', ctx => ({ * user_id: ctx.auth.userId, * status: 'blocked' * })) * ``` */ declare function denyRelation(operation: Operation | Operation[], relationshipPath: string, endCondition: ((ctx: PolicyEvaluationContext) => Record) | Record, options?: { name?: string; priority?: number; }): ReBAcPolicyDefinition; /** * Create a ReBAC transformer */ declare function createReBAcTransformer(registry: ReBAcRegistry, options?: ReBAcQueryOptions): ReBAcTransformer; /** * Field-Level Access Control Types * * Provides type definitions for controlling access to individual columns * based on context. This allows hiding sensitive fields from unauthorized users. * * @module @kysera/rls/field-access/types */ /** * Operations that can be controlled at field level */ type FieldOperation = 'read' | 'write'; /** * Field access condition function * * Returns true if the field is accessible, false otherwise. * * @typeParam TCtx - Policy evaluation context type */ type FieldAccessCondition = (ctx: TCtx) => boolean | Promise; /** * Configuration for a single field's access control * * @example * ```typescript * const emailConfig: FieldAccessConfig = { * read: ctx => ctx.auth.userId === ctx.row.id || ctx.auth.roles.includes('admin'), * write: ctx => ctx.auth.userId === ctx.row.id * }; * ``` */ interface FieldAccessConfig { /** * Condition for read access * If undefined, uses table default */ read?: FieldAccessCondition; /** * Condition for write access * If undefined, uses table default */ write?: FieldAccessCondition; /** * Value to use when field is not readable * @default null */ maskedValue?: unknown; /** * Whether to completely omit the field when not readable * @default false (uses maskedValue instead) */ omitWhenHidden?: boolean; } /** * Table field access configuration * * @typeParam TRow - Type of the database row * @typeParam TCtx - Policy evaluation context type * * @example * ```typescript * const usersFieldAccess: TableFieldAccessConfig = { * default: 'allow', * fields: { * email: { * read: ctx => ctx.auth.userId === ctx.row.id || ctx.auth.roles.includes('admin') * }, * password_hash: { * read: () => false, * write: () => false * }, * mfa_totp_secret: { * read: ctx => ctx.auth.userId === ctx.row.id, * omitWhenHidden: true * } * } * }; * ``` */ interface TableFieldAccessConfig { /** * Default access policy for fields not explicitly configured * - 'allow': All fields are accessible by default * - 'deny': Only explicitly allowed fields are accessible * @default 'allow' */ default?: 'allow' | 'deny'; /** * Field-specific access configurations */ fields: { [K in keyof TRow]?: FieldAccessConfig; }; /** * Roles that bypass field access control */ skipFor?: string[]; } /** * Complete field access schema for all tables * * @typeParam DB - Database schema type */ type FieldAccessSchema = { [K in keyof DB]?: TableFieldAccessConfig; }; /** * Compiled field access configuration ready for evaluation */ interface CompiledFieldAccess { /** * Field name */ field: string; /** * Compiled read condition * Returns true if field is readable */ canRead: (ctx: PolicyEvaluationContext) => boolean | Promise; /** * Compiled write condition * Returns true if field is writable */ canWrite: (ctx: PolicyEvaluationContext) => boolean | Promise; /** * Value to use when field is masked */ maskedValue: unknown; /** * Whether to omit the field entirely when hidden */ omitWhenHidden: boolean; } /** * Compiled table field access configuration */ interface CompiledTableFieldAccess { /** * Table name */ table: string; /** * Default access policy */ defaultAccess: 'allow' | 'deny'; /** * Roles that bypass field access */ skipFor: string[]; /** * Field-specific configurations */ fields: Map; } /** * Result of field access evaluation */ interface FieldAccessResult { /** * Whether the field is accessible */ accessible: boolean; /** * If not accessible, the reason */ reason?: string; /** * Value to use (original or masked) */ value: unknown; /** * Whether the field should be omitted entirely */ omit: boolean; } /** * Result of applying field access to a row */ interface MaskedRow> { /** * The row with field access applied */ data: Partial; /** * Fields that were masked */ maskedFields: string[]; /** * Fields that were omitted */ omittedFields: string[]; } /** * Options for field access processing */ interface FieldAccessOptions { /** * Whether to throw an error when accessing a denied field * @default false (returns masked value instead) */ throwOnDenied?: boolean; /** * Whether to include metadata about masked fields in the result * @default false */ includeMetadata?: boolean; /** * Fields to explicitly include (whitelist) * If specified, only these fields are processed */ includeFields?: string[]; /** * Fields to explicitly exclude (blacklist) * These fields are never included regardless of access */ excludeFields?: string[]; } /** * Always deny access to a field * * @example * ```typescript * const config = { * fields: { * password_hash: neverAccessible(), * api_secret: neverAccessible() * } * }; * ``` */ declare function neverAccessible(): FieldAccessConfig; /** * Only the resource owner can access this field * * @param ownerField - Field name containing the owner ID * * @example * ```typescript * const config = { * fields: { * email: ownerOnly('user_id'), * phone: ownerOnly('user_id') * } * }; * ``` */ declare function ownerOnly(ownerField?: string): FieldAccessConfig; /** * Owner or users with specific roles can access this field * * @param roles - Roles that can access besides owner * @param ownerField - Field name containing the owner ID * * @example * ```typescript * const config = { * fields: { * email: ownerOrRoles(['admin', 'support'], 'user_id'), * address: ownerOrRoles(['admin'], 'user_id') * } * }; * ``` */ declare function ownerOrRoles(roles: string[], ownerField?: string): FieldAccessConfig; /** * Only users with specific roles can access this field * * @param roles - Roles that can access * * @example * ```typescript * const config = { * fields: { * internal_notes: rolesOnly(['admin', 'moderator']), * audit_log: rolesOnly(['admin']) * } * }; * ``` */ declare function rolesOnly(roles: string[]): FieldAccessConfig; /** * Field is read-only (no write access) * * @param readCondition - Optional condition for read access * * @example * ```typescript * const config = { * fields: { * created_at: readOnly(), * version: readOnly() * } * }; * ``` */ declare function readOnly(readCondition?: FieldAccessCondition): FieldAccessConfig; /** * Field has public read access but restricted write * * @param writeCondition - Condition for write access * * @example * ```typescript * const config = { * fields: { * display_name: publicReadRestrictedWrite(ctx => ctx.auth.userId === ctx.row.id), * bio: publicReadRestrictedWrite(ctx => ctx.auth.userId === ctx.row.id) * } * }; * ``` */ declare function publicReadRestrictedWrite(writeCondition: FieldAccessCondition): FieldAccessConfig; /** * Mask field value with custom masking function * * @param maskFn - Function to mask the value * @param readCondition - Condition for full read access * * @example * ```typescript * const config = { * fields: { * email: maskedField( * value => value.replace(/(.{2}).*@/, '$1***@'), * ctx => ctx.auth.userId === ctx.row.id * ), * phone: maskedField( * value => value.replace(/\d(?=\d{4})/g, '*'), * ctx => ctx.auth.userId === ctx.row.id * ) * } * }; * ``` */ declare function maskedField(maskFn: (value: unknown) => unknown, readCondition: FieldAccessCondition): FieldAccessConfig & { maskFn: (value: unknown) => unknown; }; /** * Field Access Registry * * Manages field-level access control configurations across tables. * * @module @kysera/rls/field-access/registry */ /** * Field Access Registry * * Manages field-level access control configurations for all tables. * * @example * ```typescript * const registry = new FieldAccessRegistry(); * * registry.loadSchema({ * users: { * default: 'allow', * fields: { * email: ownerOrRoles(['admin'], 'id'), * password_hash: neverAccessible(), * mfa_secret: ownerOnly('id') * } * } * }); * * // Check if field is accessible * const canRead = await registry.canReadField('users', 'email', evalCtx); * ``` */ declare class FieldAccessRegistry { private tables; private logger; constructor(schema?: FieldAccessSchema, options?: { logger?: KyseraLogger; }); /** * Load field access schema */ loadSchema(schema: FieldAccessSchema): void; /** * Register field access configuration for a table */ registerTable(table: string, config: TableFieldAccessConfig): void; /** * Check if a field is readable in the current context * * @param table - Table name * @param field - Field name * @param ctx - Evaluation context * @returns True if field is readable */ canReadField(table: string, field: string, ctx: PolicyEvaluationContext): Promise; /** * Check if a field is writable in the current context * * @param table - Table name * @param field - Field name * @param ctx - Evaluation context * @returns True if field is writable */ canWriteField(table: string, field: string, ctx: PolicyEvaluationContext): Promise; /** * Get field configuration * * @param table - Table name * @param field - Field name * @returns Compiled field access config or undefined */ getFieldConfig(table: string, field: string): CompiledFieldAccess | undefined; /** * Get table configuration * * @param table - Table name * @returns Compiled table field access config or undefined */ getTableConfig(table: string): CompiledTableFieldAccess | undefined; /** * Check if table has field access configuration */ hasTable(table: string): boolean; /** * Get all registered table names */ getTables(): string[]; /** * Get all fields with explicit configuration for a table * * @param table - Table name * @returns Array of field names */ getConfiguredFields(table: string): string[]; /** * Clear all configurations */ clear(): void; /** * Compile a field access configuration */ private compileFieldConfig; } /** * Create a field access registry */ declare function createFieldAccessRegistry(schema?: FieldAccessSchema, options?: { logger?: KyseraLogger; }): FieldAccessRegistry; /** * Field Access Processor * * Applies field-level access control to database rows and mutation data. * * @module @kysera/rls/field-access/processor */ /** * Field Access Processor * * Applies field-level access control rules to rows and mutation data. * * @example * ```typescript * const processor = new FieldAccessProcessor(registry); * * // Mask fields in a row * const result = await processor.maskRow('users', user, { * includeMetadata: true * }); * * console.log(result.data); // Row with masked fields * console.log(result.maskedFields); // ['email', 'phone'] * console.log(result.omittedFields); // ['mfa_secret'] * * // Validate write access * await processor.validateWrite('users', { email: 'new@example.com' }); * ``` */ declare class FieldAccessProcessor { private registry; private defaultMaskValue; constructor(registry: FieldAccessRegistry, defaultMaskValue?: unknown); /** * Apply field access control to a single row * * @param table - Table name * @param row - Row data * @param options - Processing options * @returns Masked row with metadata */ maskRow>(table: string, row: T, options?: FieldAccessOptions): Promise>; /** * Apply field access control to multiple rows * * @param table - Table name * @param rows - Array of rows * @param options - Processing options * @returns Array of masked rows */ maskRows>(table: string, rows: T[], options?: FieldAccessOptions): Promise[]>; /** * Validate that all fields in mutation data are writable * * @param table - Table name * @param data - Mutation data * @param existingRow - Existing row (for update operations) * @throws RLSPolicyViolation if any field is not writable */ validateWrite(table: string, data: Record, existingRow?: Record): Promise; /** * Filter mutation data to only include writable fields * * @param table - Table name * @param data - Mutation data * @param existingRow - Existing row (for update operations) * @returns Filtered data with only writable fields */ filterWritableFields(table: string, data: Record, existingRow?: Record): Promise<{ data: Record; removedFields: string[]; }>; /** * Get list of readable fields for a table * * @param table - Table name * @param row - Row data (for context-dependent fields) * @returns Array of readable field names */ getReadableFields(table: string, row: Record): Promise; /** * Get list of writable fields for a table * * @param table - Table name * @param row - Existing row data (for context-dependent fields) * @returns Array of writable field names */ getWritableFields(table: string, row: Record): Promise; /** * Get current RLS context */ private getContext; /** * Create evaluation context */ private createEvalContext; /** * Evaluate field access for a specific field */ private evaluateFieldAccess; } /** * Create a field access processor */ declare function createFieldAccessProcessor(registry: FieldAccessRegistry, defaultMaskValue?: unknown): FieldAccessProcessor; /** * Policy Composition Types * * Provides types for creating reusable, composable RLS policies. * * @module @kysera/rls/composition/types */ /** * A named, reusable policy template * * Can be composed with other policies and applied to multiple tables. * * @example * ```typescript * const tenantIsolation = definePolicy({ * name: 'tenantIsolation', * type: 'filter', * operation: 'read', * filter: ctx => ({ tenant_id: ctx.auth.tenantId }), * priority: 1000 * }); * ``` */ interface ReusablePolicy { /** * Unique name for this policy */ name: string; /** * Description for documentation */ description?: string; /** * Policy definitions (can include multiple policies) */ policies: PolicyDefinition[]; /** * Tags for categorization */ tags?: string[]; } /** * Configuration for a reusable policy template */ interface ReusablePolicyConfig { /** * Policy name */ name: string; /** * Description */ description?: string; /** * Tags for categorization */ tags?: string[]; } /** * Extended table RLS configuration with policy composition support */ interface ComposableTableConfig { /** * Reusable policies to extend from * Policies are applied in order (first = lowest priority) */ extends?: ReusablePolicy[]; /** * Additional table-specific policies */ policies?: PolicyDefinition[]; /** * Whether to allow access by default when no policies match * @default true */ defaultDeny?: boolean; /** * Roles that bypass RLS */ skipFor?: string[]; } /** * Complete schema with composition support * * @typeParam DB - Database schema type */ type ComposableRLSSchema = { [K in keyof DB]?: ComposableTableConfig; }; /** * Base policy that can be extended */ interface BasePolicyDefinition { /** * Unique identifier for this base policy */ id: string; /** * Human-readable name */ name: string; /** * Description */ description?: string; /** * Policies included in this base */ policies: PolicyDefinition[]; /** * Other base policies this extends */ extends?: string[]; /** * Priority offset applied to all policies * @default 0 */ priorityOffset?: number; } /** * Policy inheritance chain resolution */ interface ResolvedInheritance { /** * Final merged policies */ policies: PolicyDefinition[]; /** * Chain of base policies used */ inheritanceChain: string[]; /** * Any conflicts detected */ conflicts: { policy: string; reason: string; }[]; } /** * Multi-tenancy policy configuration */ interface TenantIsolationConfig { /** * Column name for tenant ID * @default 'tenant_id' */ tenantColumn?: string; /** * Operations to apply tenant isolation to * @default ['read', 'create', 'update', 'delete'] */ operations?: Operation[]; /** * Whether to validate tenant on create/update * @default true */ validateOnMutation?: boolean; } /** * Ownership policy configuration */ interface OwnershipConfig { /** * Column name for owner ID * @default 'owner_id' or 'user_id' */ ownerColumn?: string; /** * Operations owners can perform * @default ['read', 'update', 'delete'] */ ownerOperations?: Operation[]; /** * Whether owners can delete * @default true */ canDelete?: boolean; } /** * Soft delete policy configuration */ interface SoftDeleteConfig { /** * Column name for soft delete flag * @default 'deleted_at' */ deletedColumn?: string; /** * Whether to filter soft-deleted rows on read * @default true */ filterOnRead?: boolean; /** * Whether to prevent hard deletes * @default true */ preventHardDelete?: boolean; } /** * Status-based access configuration */ interface StatusAccessConfig { /** * Column name for status * @default 'status' */ statusColumn?: string; /** * Statuses that are publicly readable */ publicStatuses?: string[]; /** * Statuses that can be updated */ editableStatuses?: string[]; /** * Statuses that can be deleted */ deletableStatuses?: string[]; } /** * Policy Composition Builder * * Factory functions for creating reusable, composable RLS policies. * * @module @kysera/rls/composition/builder */ /** * Create a reusable policy template * * @param config - Policy configuration * @param policies - Array of policy definitions * @returns Reusable policy template * * @example * ```typescript * const tenantPolicy = definePolicy( * { * name: 'tenantIsolation', * description: 'Filter by tenant_id', * tags: ['multi-tenant'] * }, * [ * filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }), { * priority: 1000, * name: 'tenant-filter' * }), * validate('create', ctx => ctx.data?.tenant_id === ctx.auth.tenantId, { * name: 'tenant-validate' * }) * ] * ); * ``` */ declare function definePolicy(config: ReusablePolicyConfig, policies: PolicyDefinition[]): ReusablePolicy; /** * Create a filter-only policy * * @param name - Policy name * @param filterFn - Filter condition * @param options - Additional options * @returns Reusable filter policy */ declare function defineFilterPolicy(name: string, filterFn: (ctx: PolicyEvaluationContext) => Record, options?: { priority?: number; }): ReusablePolicy; /** * Create an allow-based policy * * @param name - Policy name * @param operation - Operations to allow * @param condition - Allow condition * @param options - Additional options * @returns Reusable allow policy */ declare function defineAllowPolicy(name: string, operation: Operation | Operation[], condition: (ctx: PolicyEvaluationContext) => boolean | Promise, options?: { priority?: number; }): ReusablePolicy; /** * Create a deny-based policy * * @param name - Policy name * @param operation - Operations to deny * @param condition - Deny condition (optional - if not provided, always denies) * @param options - Additional options * @returns Reusable deny policy */ declare function defineDenyPolicy(name: string, operation: Operation | Operation[], condition?: (ctx: PolicyEvaluationContext) => boolean | Promise, options?: { priority?: number; }): ReusablePolicy; /** * Create a validation policy * * @param name - Policy name * @param operation - Operations to validate * @param condition - Validation condition * @param options - Additional options * @returns Reusable validate policy */ declare function defineValidatePolicy(name: string, operation: 'create' | 'update' | 'all', condition: (ctx: PolicyEvaluationContext) => boolean | Promise, options?: { priority?: number; }): ReusablePolicy; /** * Create a combined policy with multiple types * * @param name - Policy name * @param config - Policy configurations * @returns Reusable combined policy */ declare function defineCombinedPolicy(name: string, config: { filter?: (ctx: PolicyEvaluationContext) => Record; allow?: Record boolean | Promise>; deny?: Record boolean | Promise>; validate?: { create?: (ctx: PolicyEvaluationContext) => boolean | Promise; update?: (ctx: PolicyEvaluationContext) => boolean | Promise; }; }): ReusablePolicy; /** * Create a tenant isolation policy * * Automatically filters by tenant_id and validates mutations. * * @param config - Tenant isolation configuration * @returns Reusable tenant isolation policy */ declare function createTenantIsolationPolicy(config?: TenantIsolationConfig): ReusablePolicy; /** * Create an ownership policy * * Allows owners to read/update/delete their own resources. * * @param config - Ownership configuration * @returns Reusable ownership policy */ declare function createOwnershipPolicy(config?: OwnershipConfig): ReusablePolicy; /** * Create a soft delete policy * * Filters out soft-deleted rows and optionally prevents hard deletes. * * @param config - Soft delete configuration * @returns Reusable soft delete policy */ declare function createSoftDeletePolicy(config?: SoftDeleteConfig): ReusablePolicy; /** * Create a status-based access policy * * Controls access based on resource status. * * @param config - Status access configuration * @returns Reusable status policy */ declare function createStatusAccessPolicy(config: StatusAccessConfig): ReusablePolicy; /** * Create an admin bypass policy * * Allows admin roles to perform all operations. * * @param roles - Roles that have admin access * @returns Reusable admin policy */ declare function createAdminPolicy(roles: string[]): ReusablePolicy; /** * Compose multiple reusable policies into one * * @param name - Name for the composed policy * @param policies - Policies to compose * @returns Composed policy */ declare function composePolicies(name: string, policies: ReusablePolicy[]): ReusablePolicy; /** * Extend a reusable policy with additional policies * * @param base - Base policy to extend * @param additional - Additional policies to add * @returns Extended policy */ declare function extendPolicy(base: ReusablePolicy, additional: PolicyDefinition[]): ReusablePolicy; /** * Override policies from a base with new conditions * * @param base - Base policy * @param overrides - Policy name to new policy mapping * @returns Policy with overrides applied */ declare function overridePolicy(base: ReusablePolicy, overrides: Record): ReusablePolicy; /** * Audit Trail Types * * Provides type definitions for auditing RLS policy decisions. * * @module @kysera/rls/audit/types */ /** * RLS policy decision result */ type AuditDecision = 'allow' | 'deny' | 'filter'; /** * RLS audit event * * Represents a single policy evaluation event for audit logging. * * @example * ```typescript * const event: RLSAuditEvent = { * timestamp: new Date(), * userId: '123', * operation: 'update', * table: 'posts', * policyName: 'ownership-allow', * decision: 'allow', * context: { rowId: '456', tenantId: 'acme' } * }; * ``` */ interface RLSAuditEvent { /** * Timestamp of the event */ timestamp: Date; /** * User ID who performed the action */ userId: string | number; /** * Tenant ID (if multi-tenant) */ tenantId?: string | number; /** * Database operation */ operation: Operation; /** * Table name */ table: string; /** * Name of the policy that made the decision */ policyName?: string; /** * Decision result */ decision: AuditDecision; /** * Reason for the decision (especially for denials) */ reason?: string; /** * Additional context about the event */ context?: Record; /** * Row ID(s) affected */ rowIds?: (string | number)[]; /** * Hash of the query (for grouping similar queries) */ queryHash?: string; /** * Request ID for tracing */ requestId?: string; /** * IP address of the requester */ ipAddress?: string; /** * User agent string */ userAgent?: string; /** * Duration of policy evaluation in milliseconds */ durationMs?: number; /** * Whether this event was filtered from logging * (set by filtering rules but still available for debugging) */ filtered?: boolean; } /** * Adapter for persisting audit events * * Implement this interface to store audit events in your preferred backend. * * @example * ```typescript * class DatabaseAuditAdapter implements RLSAuditAdapter { * constructor(private db: Kysely) {} * * async log(event: RLSAuditEvent): Promise { * await this.db.insertInto('rls_audit_log') * .values({ * user_id: event.userId, * operation: event.operation, * table_name: event.table, * decision: event.decision, * context: JSON.stringify(event.context), * created_at: event.timestamp * }) * .execute(); * } * * async logBatch(events: RLSAuditEvent[]): Promise { * await this.db.insertInto('rls_audit_log') * .values(events.map(e => ({ * user_id: e.userId, * operation: e.operation, * table_name: e.table, * decision: e.decision, * context: JSON.stringify(e.context), * created_at: e.timestamp * }))) * .execute(); * } * } * ``` */ interface RLSAuditAdapter { /** * Log a single audit event * * @param event - Event to log */ log(event: RLSAuditEvent): Promise; /** * Log multiple audit events (for batch processing) * * @param events - Events to log */ logBatch?(events: RLSAuditEvent[]): Promise; /** * Flush any buffered events */ flush?(): Promise; /** * Close the adapter and release resources */ close?(): Promise; } /** * Configuration for table-specific audit settings */ interface TableAuditConfig { /** * Whether audit is enabled for this table * @default true (if audit is globally enabled) */ enabled?: boolean; /** * Log allowed decisions * @default false */ logAllowed?: boolean; /** * Log denied decisions * @default true */ logDenied?: boolean; /** * Log filter applications * @default false */ logFilters?: boolean; /** * Context fields to include in audit logs * If empty, includes all available context */ includeContext?: string[]; /** * Context fields to exclude from audit logs */ excludeContext?: string[]; /** * Whether to include row data in audit logs * @default false (for privacy) */ includeRowData?: boolean; /** * Whether to include mutation data in audit logs * @default false (for privacy) */ includeMutationData?: boolean; /** * Custom filter function to determine if an event should be logged */ filter?: (event: RLSAuditEvent) => boolean; } /** * Global audit configuration */ interface AuditConfig { /** * Audit adapter for persisting events */ adapter: RLSAuditAdapter; /** * Whether audit is enabled globally * @default true */ enabled?: boolean; /** * Default settings for all tables */ defaults?: Omit; /** * Table-specific audit configurations */ tables?: Record; /** * Buffer size for batch logging * Events are batched until this size is reached * @default 100 */ bufferSize?: number; /** * Maximum time to buffer events before flushing (ms) * @default 5000 (5 seconds) */ flushInterval?: number; /** * Whether to log asynchronously (fire-and-forget) * @default true (for performance) */ async?: boolean; /** * Error handler for audit failures */ onError?: (error: Error, events: RLSAuditEvent[]) => void; /** * Sample rate for audit logging (0.0 to 1.0) * Use for high-traffic systems to reduce log volume * @default 1.0 (log all) */ sampleRate?: number; } /** * Query parameters for retrieving audit events */ interface AuditQueryParams { /** * Filter by user ID */ userId?: string | number; /** * Filter by tenant ID */ tenantId?: string | number; /** * Filter by table name */ table?: string; /** * Filter by operation */ operation?: Operation; /** * Filter by decision */ decision?: AuditDecision; /** * Start timestamp (inclusive) */ startTime?: Date; /** * End timestamp (exclusive) */ endTime?: Date; /** * Filter by request ID */ requestId?: string; /** * Maximum results to return */ limit?: number; /** * Offset for pagination */ offset?: number; } /** * Aggregated audit statistics */ interface AuditStats { /** * Total number of events */ totalEvents: number; /** * Events by decision type */ byDecision: Record; /** * Events by operation */ byOperation: Record; /** * Events by table */ byTable: Record; /** * Top denied users */ topDeniedUsers?: { userId: string | number; count: number; }[]; /** * Time range of stats */ timeRange: { start: Date; end: Date; }; } /** * Simple console-based audit adapter for development/testing * * @example * ```typescript * const adapter = new ConsoleAuditAdapter({ * format: 'json', * colors: true * }); * ``` */ interface ConsoleAuditAdapterOptions { /** * Output format * @default 'text' */ format?: 'text' | 'json'; /** * Use colors in output (for text format) * @default true */ colors?: boolean; /** * Include timestamp in output * @default true */ includeTimestamp?: boolean; } /** * Console audit adapter implementation */ declare class ConsoleAuditAdapter implements RLSAuditAdapter { private options; constructor(options?: ConsoleAuditAdapterOptions); log(event: RLSAuditEvent): Promise; logBatch(events: RLSAuditEvent[]): Promise; private getPrefix; } /** * In-memory audit adapter for testing * * Stores events in memory for later retrieval and assertion. */ declare class InMemoryAuditAdapter implements RLSAuditAdapter { private events; private maxSize; constructor(maxSize?: number); log(event: RLSAuditEvent): Promise; logBatch(events: RLSAuditEvent[]): Promise; /** * Get all logged events */ getEvents(): RLSAuditEvent[]; /** * Query events */ query(params: AuditQueryParams): RLSAuditEvent[]; /** * Get statistics */ getStats(params?: Pick): AuditStats; /** * Clear all events */ clear(): void; /** * Get event count */ get size(): number; } /** * Audit Logger * * Manages audit event logging with buffering and filtering. * * @module @kysera/rls/audit/logger */ /** * Audit Logger * * Manages RLS audit event logging with buffering, filtering, and sampling. * * @example * ```typescript * const logger = new AuditLogger({ * adapter: new DatabaseAuditAdapter(db), * bufferSize: 50, * flushInterval: 5000, * defaults: { * logAllowed: false, * logDenied: true, * logFilters: false * }, * tables: { * sensitive_data: { * logAllowed: true, * includeContext: ['requestId', 'ipAddress'] * } * } * }); * * // Log an event * await logger.logDecision('update', 'posts', 'allow', 'ownership-allow'); * * // Ensure all events are flushed * await logger.flush(); * ``` */ declare class AuditLogger { private adapter; private config; private buffer; private flushTimer; private isShuttingDown; constructor(config: AuditConfig); /** * Log a policy decision * * @param operation - Database operation * @param table - Table name * @param decision - Decision result * @param policyName - Name of the policy * @param options - Additional options */ logDecision(operation: Operation, table: string, decision: AuditDecision, policyName?: string, options?: { reason?: string; rowIds?: (string | number)[]; queryHash?: string; durationMs?: number; context?: Record; }): Promise; /** * Log an allow decision */ logAllow(operation: Operation, table: string, policyName?: string, options?: { reason?: string; rowIds?: (string | number)[]; context?: Record; }): Promise; /** * Log a deny decision */ logDeny(operation: Operation, table: string, policyName?: string, options?: { reason?: string; rowIds?: (string | number)[]; context?: Record; }): Promise; /** * Log a filter application */ logFilter(table: string, policyName?: string, options?: { context?: Record; }): Promise; /** * Flush buffered events */ flush(): Promise; /** * Close the logger */ close(): Promise; /** * Get buffer size */ get bufferSize(): number; /** * Check if logger is enabled */ get enabled(): boolean; /** * Enable or disable logging */ setEnabled(enabled: boolean): void; /** * Get table-specific config with defaults */ private getTableConfig; /** * Check if decision should be logged */ private shouldLog; /** * Build audit event */ private buildEvent; /** * Build context object with filtering */ private buildContext; /** * Log event to buffer or directly */ private logEvent; /** * Start the flush timer */ private startFlushTimer; } /** * Create an audit logger */ declare function createAuditLogger(config: AuditConfig): AuditLogger; /** * Policy Testing Utilities * * Provides tools for unit testing RLS policies without a database. * * @module @kysera/rls/testing */ /** * Result of policy evaluation */ interface PolicyEvaluationResult { /** * Whether the operation is allowed */ allowed: boolean; /** * Name of the policy that made the decision */ policyName?: string; /** * Type of decision */ decisionType: 'allow' | 'deny' | 'default'; /** * Reason for the decision */ reason?: string; /** * All policies that were evaluated */ evaluatedPolicies: { name: string; type: 'allow' | 'deny' | 'validate'; result: boolean; }[]; } /** * Result of filter evaluation */ interface FilterEvaluationResult { /** * Generated filter conditions */ conditions: Record; /** * Names of all filters applied */ appliedFilters: string[]; } /** * Test context for policy evaluation */ interface TestContext> { /** * Auth context */ auth: RLSAuthContext; /** * Row data (for read/update/delete operations) */ row?: TRow; /** * Mutation data (for create/update operations) */ data?: Record; /** * Additional metadata */ meta?: Record; } /** * Policy Tester * * Test RLS policies without a database connection. * * @example * ```typescript * const tester = createPolicyTester(rlsSchema); * * describe('Post RLS Policies', () => { * it('should allow owner to update their post', async () => { * const result = await tester.evaluate('posts', 'update', { * auth: { userId: 'user-1', roles: ['user'] }, * row: { id: 'post-1', author_id: 'user-1', status: 'draft' } * }); * * expect(result.allowed).toBe(true); * }); * * it('should deny non-owner update', async () => { * const result = await tester.evaluate('posts', 'update', { * auth: { userId: 'user-2', roles: ['user'] }, * row: { id: 'post-1', author_id: 'user-1', status: 'draft' } * }); * * expect(result.allowed).toBe(false); * expect(result.reason).toContain('not owner'); * }); * * it('should apply filters correctly', async () => { * const filters = await tester.getFilters('posts', 'read', { * auth: { userId: 'user-1', tenantId: 'tenant-1', roles: [] } * }); * * expect(filters.conditions).toEqual({ * tenant_id: 'tenant-1', * deleted_at: null * }); * }); * }); * ``` */ declare class PolicyTester { private registry; constructor(schema: RLSSchema); /** * Evaluate policies for an operation * * @param table - Table name * @param operation - Operation to test * @param context - Test context * @returns Evaluation result */ evaluate(table: string, operation: Operation, context: TestContext): Promise; /** * Get filter conditions for read operations * * @param table - Table name * @param operation - Must be 'read' * @param context - Test context * @returns Filter conditions */ getFilters(table: string, _operation: 'read', context: Pick): FilterEvaluationResult; /** * Test if a specific policy allows the operation * * @param table - Table name * @param policyName - Name of the policy to test * @param context - Test context * @returns True if policy allows */ testPolicy(table: string, policyName: string, context: TestContext): Promise<{ found: boolean; result?: boolean; }>; /** * List all policies for a table */ listPolicies(table: string): { allows: string[]; denies: string[]; filters: string[]; validates: string[]; }; /** * Get all registered tables */ getTables(): string[]; /** * Evaluate a single policy */ private evaluatePolicy; } /** * Create a policy tester * * @param schema - RLS schema to test * @returns PolicyTester instance */ declare function createPolicyTester(schema: RLSSchema): PolicyTester; /** * Create a test auth context * * @param overrides - Values to override * @returns RLSAuthContext for testing */ declare function createTestAuthContext(overrides: Partial & { userId: string | number; }): RLSAuthContext; /** * Create a test row * * @param data - Row data * @returns Row object */ declare function createTestRow>(data: T): T; /** * Assertion helpers for policy testing */ declare const policyAssertions: { /** * Assert that the result is allowed */ assertAllowed(result: PolicyEvaluationResult, message?: string): void; /** * Assert that the result is denied */ assertDenied(result: PolicyEvaluationResult, message?: string): void; /** * Assert that a specific policy made the decision */ assertPolicyUsed(result: PolicyEvaluationResult, policyName: string, message?: string): void; /** * Assert that filters include expected conditions */ assertFiltersInclude(result: FilterEvaluationResult, expected: Record, message?: string): void; }; 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 };