@kysera/rls 0.7.3 → 0.8.0
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/README.md +389 -279
- package/dist/index.d.ts +89 -20
- package/dist/index.js +210 -130
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +3 -14
- package/dist/native/index.js.map +1 -1
- package/dist/{types-6eCXh_Jd.d.ts → types-Dowjd6zG.d.ts} +3 -3
- package/package.json +16 -7
- package/src/context/index.ts +4 -4
- package/src/context/manager.ts +45 -45
- package/src/context/storage.ts +3 -3
- package/src/context/types.ts +1 -5
- package/src/errors.ts +62 -77
- package/src/index.ts +13 -13
- package/src/native/README.md +49 -46
- package/src/native/index.ts +3 -6
- package/src/native/migration.ts +29 -27
- package/src/native/postgres.ts +63 -74
- package/src/plugin.ts +286 -159
- package/src/policy/builder.ts +46 -33
- package/src/policy/index.ts +4 -4
- package/src/policy/registry.ts +100 -105
- package/src/policy/schema.ts +58 -71
- package/src/policy/types.ts +58 -58
- package/src/transformer/index.ts +2 -2
- package/src/transformer/mutation.ts +95 -98
- package/src/transformer/select.ts +59 -43
- package/src/utils/helpers.ts +57 -50
- package/src/utils/index.ts +13 -2
- package/src/utils/type-utils.ts +155 -0
- package/src/version.ts +7 -0
package/dist/native/index.d.ts
CHANGED
package/dist/native/index.js
CHANGED
|
@@ -6,11 +6,7 @@ var PostgresRLSGenerator = class {
|
|
|
6
6
|
* Generate all PostgreSQL RLS statements from schema
|
|
7
7
|
*/
|
|
8
8
|
generateStatements(schema, options = {}) {
|
|
9
|
-
const {
|
|
10
|
-
force = true,
|
|
11
|
-
schemaName = "public",
|
|
12
|
-
policyPrefix = "rls"
|
|
13
|
-
} = options;
|
|
9
|
+
const { force = true, schemaName = "public", policyPrefix = "rls" } = options;
|
|
14
10
|
const statements = [];
|
|
15
11
|
for (const [table, config] of Object.entries(schema)) {
|
|
16
12
|
if (!config) continue;
|
|
@@ -41,10 +37,7 @@ var PostgresRLSGenerator = class {
|
|
|
41
37
|
if (!policy.using && !policy.withCheck) {
|
|
42
38
|
return null;
|
|
43
39
|
}
|
|
44
|
-
const parts = [
|
|
45
|
-
`CREATE POLICY "${name}"`,
|
|
46
|
-
`ON ${table}`
|
|
47
|
-
];
|
|
40
|
+
const parts = [`CREATE POLICY "${name}"`, `ON ${table}`];
|
|
48
41
|
if (policy.type === "deny") {
|
|
49
42
|
parts.push("AS RESTRICTIVE");
|
|
50
43
|
} else {
|
|
@@ -192,11 +185,7 @@ var RLSMigrationGenerator = class {
|
|
|
192
185
|
* Generate migration file content
|
|
193
186
|
*/
|
|
194
187
|
generateMigration(schema, options = {}) {
|
|
195
|
-
const {
|
|
196
|
-
name = "rls_policies",
|
|
197
|
-
includeContextFunctions = true,
|
|
198
|
-
...generatorOptions
|
|
199
|
-
} = options;
|
|
188
|
+
const { name = "rls_policies", includeContextFunctions = true, ...generatorOptions } = options;
|
|
200
189
|
const upStatements = this.generator.generateStatements(schema, generatorOptions);
|
|
201
190
|
const downStatements = this.generator.generateDropStatements(schema, generatorOptions);
|
|
202
191
|
const contextFunctions = includeContextFunctions ? this.generator.generateContextFunctions() : "";
|
package/dist/native/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/native/postgres.ts","../../src/native/migration.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Kysely } from 'kysely';\nimport { sql } from 'kysely';\nimport type { RLSSchema, TableRLSConfig, PolicyDefinition, Operation } from '../policy/types.js';\n\n/**\n * Options for PostgreSQL RLS generation\n */\nexport interface PostgresRLSOptions {\n /** Force RLS on table owners */\n force?: boolean;\n /** Schema name (default: public) */\n schemaName?: string;\n /** Prefix for generated policy names */\n policyPrefix?: string;\n}\n\n/**\n * PostgreSQL RLS Generator\n * Generates native PostgreSQL RLS statements from Kysera RLS schema\n */\nexport class PostgresRLSGenerator {\n /**\n * Generate all PostgreSQL RLS statements from schema\n */\n generateStatements<DB>(\n schema: RLSSchema<DB>,\n options: PostgresRLSOptions = {}\n ): string[] {\n const {\n force = true,\n schemaName = 'public',\n policyPrefix = 'rls',\n } = options;\n\n const statements: string[] = [];\n\n for (const [table, config] of Object.entries(schema)) {\n if (!config) continue;\n\n const qualifiedTable = `${schemaName}.${table}`;\n const tableConfig = config as TableRLSConfig;\n\n // Enable RLS on table\n statements.push(`ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY;`);\n\n if (force) {\n statements.push(`ALTER TABLE ${qualifiedTable} FORCE ROW LEVEL SECURITY;`);\n }\n\n // Generate policies\n let policyIndex = 0;\n for (const policy of tableConfig.policies) {\n const policyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`;\n const policySQL = this.generatePolicy(qualifiedTable, policyName, policy);\n if (policySQL) {\n statements.push(policySQL);\n }\n }\n }\n\n return statements;\n }\n\n /**\n * Generate a single policy statement\n */\n private generatePolicy(\n table: string,\n name: string,\n policy: PolicyDefinition\n ): string | null {\n // Skip filter policies (they're ORM-only)\n if (policy.type === 'filter' || policy.type === 'validate') {\n return null;\n }\n\n // Need USING or WITH CHECK clause for native RLS\n if (!policy.using && !policy.withCheck) {\n return null;\n }\n\n const parts: string[] = [\n `CREATE POLICY \"${name}\"`,\n `ON ${table}`,\n ];\n\n // Policy type\n if (policy.type === 'deny') {\n parts.push('AS RESTRICTIVE');\n } else {\n parts.push('AS PERMISSIVE');\n }\n\n // Target role\n parts.push(`TO ${policy.role ?? 'public'}`);\n\n // Operation\n parts.push(`FOR ${this.mapOperation(policy.operation)}`);\n\n // USING clause\n if (policy.using) {\n parts.push(`USING (${policy.using})`);\n }\n\n // WITH CHECK clause\n if (policy.withCheck) {\n parts.push(`WITH CHECK (${policy.withCheck})`);\n }\n\n return parts.join('\\n ') + ';';\n }\n\n /**\n * Map Kysera operation to PostgreSQL operation\n */\n private mapOperation(operation: Operation | Operation[]): string {\n if (Array.isArray(operation)) {\n if (operation.length === 0) {\n return 'ALL';\n }\n if (operation.length === 4 || operation.includes('all')) {\n return 'ALL';\n }\n // PostgreSQL doesn't support multiple operations in one policy\n // Return first operation\n return this.mapSingleOperation(operation[0]!);\n }\n return this.mapSingleOperation(operation);\n }\n\n /**\n * Map single operation\n */\n private mapSingleOperation(op: Operation): string {\n switch (op) {\n case 'read': return 'SELECT';\n case 'create': return 'INSERT';\n case 'update': return 'UPDATE';\n case 'delete': return 'DELETE';\n case 'all': return 'ALL';\n default: return 'ALL';\n }\n }\n\n /**\n * Generate context-setting functions for PostgreSQL\n * These functions should be STABLE for optimal performance\n */\n generateContextFunctions(): string {\n return `\n-- RLS Context Functions (STABLE for query planner optimization)\n-- These functions read session variables set by the application\n\nCREATE OR REPLACE FUNCTION rls_current_user_id()\nRETURNS text\nLANGUAGE SQL STABLE\nAS $$ SELECT current_setting('app.user_id', true) $$;\n\nCREATE OR REPLACE FUNCTION rls_current_tenant_id()\nRETURNS uuid\nLANGUAGE SQL STABLE\nAS $$ SELECT NULLIF(current_setting('app.tenant_id', true), '')::uuid $$;\n\nCREATE OR REPLACE FUNCTION rls_current_roles()\nRETURNS text[]\nLANGUAGE SQL STABLE\nAS $$ SELECT string_to_array(COALESCE(current_setting('app.roles', true), ''), ',') $$;\n\nCREATE OR REPLACE FUNCTION rls_has_role(role_name text)\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT role_name = ANY(rls_current_roles()) $$;\n\nCREATE OR REPLACE FUNCTION rls_current_permissions()\nRETURNS text[]\nLANGUAGE SQL STABLE\nAS $$ SELECT string_to_array(COALESCE(current_setting('app.permissions', true), ''), ',') $$;\n\nCREATE OR REPLACE FUNCTION rls_has_permission(permission_name text)\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT permission_name = ANY(rls_current_permissions()) $$;\n\nCREATE OR REPLACE FUNCTION rls_is_system()\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT COALESCE(current_setting('app.is_system', true), 'false')::boolean $$;\n`;\n }\n\n /**\n * Generate DROP statements for cleaning up\n */\n generateDropStatements<DB>(\n schema: RLSSchema<DB>,\n options: PostgresRLSOptions = {}\n ): string[] {\n const { schemaName = 'public', policyPrefix = 'rls' } = options;\n const statements: string[] = [];\n\n for (const table of Object.keys(schema)) {\n const qualifiedTable = `${schemaName}.${table}`;\n\n // Drop all policies with prefix\n statements.push(\n `DO $$ BEGIN\n EXECUTE (\n SELECT string_agg('DROP POLICY IF EXISTS ' || quote_ident(policyname) || ' ON ${qualifiedTable};', E'\\\\n')\n FROM pg_policies\n WHERE tablename = '${table}'\n AND schemaname = '${schemaName}'\n AND policyname LIKE '${policyPrefix}_%'\n );\nEND $$;`\n );\n\n // Disable RLS\n statements.push(`ALTER TABLE ${qualifiedTable} DISABLE ROW LEVEL SECURITY;`);\n }\n\n return statements;\n }\n}\n\n/**\n * Sync RLS context to PostgreSQL session settings\n * Call this at the start of each request/transaction\n */\nexport async function syncContextToPostgres<DB>(\n db: Kysely<DB>,\n context: {\n userId: string | number;\n tenantId?: string | number;\n roles?: string[];\n permissions?: string[];\n isSystem?: boolean;\n }\n): Promise<void> {\n const { userId, tenantId, roles, permissions, isSystem } = context;\n\n await sql`\n SELECT\n set_config('app.user_id', ${String(userId)}, true),\n set_config('app.tenant_id', ${tenantId ? String(tenantId) : ''}, true),\n set_config('app.roles', ${(roles ?? []).join(',')}, true),\n set_config('app.permissions', ${(permissions ?? []).join(',')}, true),\n set_config('app.is_system', ${isSystem ? 'true' : 'false'}, true)\n `.execute(db);\n}\n\n/**\n * Clear RLS context from PostgreSQL session\n */\nexport async function clearPostgresContext<DB>(db: Kysely<DB>): Promise<void> {\n await sql`\n SELECT\n set_config('app.user_id', '', true),\n set_config('app.tenant_id', '', true),\n set_config('app.roles', '', true),\n set_config('app.permissions', '', true),\n set_config('app.is_system', 'false', true)\n `.execute(db);\n}\n","import type { RLSSchema } from '../policy/types.js';\nimport { PostgresRLSGenerator, type PostgresRLSOptions } from './postgres.js';\n\n/**\n * Options for migration generation\n */\nexport interface MigrationOptions extends PostgresRLSOptions {\n /** Migration name */\n name?: string;\n /** Include context functions in migration */\n includeContextFunctions?: boolean;\n}\n\n/**\n * RLS Migration Generator\n * Generates Kysely migration files for RLS policies\n */\nexport class RLSMigrationGenerator {\n private generator = new PostgresRLSGenerator();\n\n /**\n * Generate migration file content\n */\n generateMigration<DB>(\n schema: RLSSchema<DB>,\n options: MigrationOptions = {}\n ): string {\n const {\n name = 'rls_policies',\n includeContextFunctions = true,\n ...generatorOptions\n } = options;\n\n const upStatements = this.generator.generateStatements(schema, generatorOptions);\n const downStatements = this.generator.generateDropStatements(schema, generatorOptions);\n\n const contextFunctions = includeContextFunctions\n ? this.generator.generateContextFunctions()\n : '';\n\n return `import { Kysely, sql } from 'kysely';\n\n/**\n * Migration: ${name}\n * Generated by @kysera/rls\n *\n * This migration sets up Row-Level Security policies for the database.\n */\n\nexport async function up(db: Kysely<any>): Promise<void> {\n${includeContextFunctions ? ` // Create RLS context functions\n await sql.raw(\\`${this.escapeTemplate(contextFunctions)}\\`).execute(db);\n\n` : ''} // Enable RLS and create policies\n${upStatements.map(s => ` await sql.raw(\\`${this.escapeTemplate(s)}\\`).execute(db);`).join('\\n')}\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n${downStatements.map(s => ` await sql.raw(\\`${this.escapeTemplate(s)}\\`).execute(db);`).join('\\n')}\n${includeContextFunctions ? `\n // Drop RLS context functions\n await sql.raw(\\`\n DROP FUNCTION IF EXISTS rls_current_user_id();\n DROP FUNCTION IF EXISTS rls_current_tenant_id();\n DROP FUNCTION IF EXISTS rls_current_roles();\n DROP FUNCTION IF EXISTS rls_has_role(text);\n DROP FUNCTION IF EXISTS rls_current_permissions();\n DROP FUNCTION IF EXISTS rls_has_permission(text);\n DROP FUNCTION IF EXISTS rls_is_system();\n \\`).execute(db);` : ''}\n}\n`;\n }\n\n /**\n * Escape template literal for embedding in string\n */\n private escapeTemplate(str: string): string {\n return str.replace(/`/g, '\\\\`').replace(/\\$/g, '\\\\$');\n }\n\n /**\n * Generate migration filename with timestamp\n */\n generateFilename(name: string = 'rls_policies'): string {\n const timestamp = new Date().toISOString()\n .replace(/[-:]/g, '')\n .replace('T', '_')\n .replace(/\\..+/, '');\n return `${timestamp}_${name}.ts`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/native/postgres.ts","../../src/native/migration.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Kysely } from 'kysely'\nimport { sql } from 'kysely'\nimport type { RLSSchema, TableRLSConfig, PolicyDefinition, Operation } from '../policy/types.js'\n\n/**\n * Options for PostgreSQL RLS generation\n */\nexport interface PostgresRLSOptions {\n /** Force RLS on table owners */\n force?: boolean\n /** Schema name (default: public) */\n schemaName?: string\n /** Prefix for generated policy names */\n policyPrefix?: string\n}\n\n/**\n * PostgreSQL RLS Generator\n * Generates native PostgreSQL RLS statements from Kysera RLS schema\n */\nexport class PostgresRLSGenerator {\n /**\n * Generate all PostgreSQL RLS statements from schema\n */\n generateStatements<DB>(schema: RLSSchema<DB>, options: PostgresRLSOptions = {}): string[] {\n const { force = true, schemaName = 'public', policyPrefix = 'rls' } = options\n\n const statements: string[] = []\n\n for (const [table, config] of Object.entries(schema)) {\n if (!config) continue\n\n const qualifiedTable = `${schemaName}.${table}`\n const tableConfig = config as TableRLSConfig\n\n // Enable RLS on table\n statements.push(`ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY;`)\n\n if (force) {\n statements.push(`ALTER TABLE ${qualifiedTable} FORCE ROW LEVEL SECURITY;`)\n }\n\n // Generate policies\n let policyIndex = 0\n for (const policy of tableConfig.policies) {\n const policyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`\n const policySQL = this.generatePolicy(qualifiedTable, policyName, policy)\n if (policySQL) {\n statements.push(policySQL)\n }\n }\n }\n\n return statements\n }\n\n /**\n * Generate a single policy statement\n */\n private generatePolicy(table: string, name: string, policy: PolicyDefinition): string | null {\n // Skip filter policies (they're ORM-only)\n if (policy.type === 'filter' || policy.type === 'validate') {\n return null\n }\n\n // Need USING or WITH CHECK clause for native RLS\n if (!policy.using && !policy.withCheck) {\n return null\n }\n\n const parts: string[] = [`CREATE POLICY \"${name}\"`, `ON ${table}`]\n\n // Policy type\n if (policy.type === 'deny') {\n parts.push('AS RESTRICTIVE')\n } else {\n parts.push('AS PERMISSIVE')\n }\n\n // Target role\n parts.push(`TO ${policy.role ?? 'public'}`)\n\n // Operation\n parts.push(`FOR ${this.mapOperation(policy.operation)}`)\n\n // USING clause\n if (policy.using) {\n parts.push(`USING (${policy.using})`)\n }\n\n // WITH CHECK clause\n if (policy.withCheck) {\n parts.push(`WITH CHECK (${policy.withCheck})`)\n }\n\n return parts.join('\\n ') + ';'\n }\n\n /**\n * Map Kysera operation to PostgreSQL operation\n */\n private mapOperation(operation: Operation | Operation[]): string {\n if (Array.isArray(operation)) {\n if (operation.length === 0) {\n return 'ALL'\n }\n if (operation.length === 4 || operation.includes('all')) {\n return 'ALL'\n }\n // PostgreSQL doesn't support multiple operations in one policy\n // Return first operation\n return this.mapSingleOperation(operation[0]!)\n }\n return this.mapSingleOperation(operation)\n }\n\n /**\n * Map single operation\n */\n private mapSingleOperation(op: Operation): string {\n switch (op) {\n case 'read':\n return 'SELECT'\n case 'create':\n return 'INSERT'\n case 'update':\n return 'UPDATE'\n case 'delete':\n return 'DELETE'\n case 'all':\n return 'ALL'\n default:\n return 'ALL'\n }\n }\n\n /**\n * Generate context-setting functions for PostgreSQL\n * These functions should be STABLE for optimal performance\n */\n generateContextFunctions(): string {\n return `\n-- RLS Context Functions (STABLE for query planner optimization)\n-- These functions read session variables set by the application\n\nCREATE OR REPLACE FUNCTION rls_current_user_id()\nRETURNS text\nLANGUAGE SQL STABLE\nAS $$ SELECT current_setting('app.user_id', true) $$;\n\nCREATE OR REPLACE FUNCTION rls_current_tenant_id()\nRETURNS uuid\nLANGUAGE SQL STABLE\nAS $$ SELECT NULLIF(current_setting('app.tenant_id', true), '')::uuid $$;\n\nCREATE OR REPLACE FUNCTION rls_current_roles()\nRETURNS text[]\nLANGUAGE SQL STABLE\nAS $$ SELECT string_to_array(COALESCE(current_setting('app.roles', true), ''), ',') $$;\n\nCREATE OR REPLACE FUNCTION rls_has_role(role_name text)\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT role_name = ANY(rls_current_roles()) $$;\n\nCREATE OR REPLACE FUNCTION rls_current_permissions()\nRETURNS text[]\nLANGUAGE SQL STABLE\nAS $$ SELECT string_to_array(COALESCE(current_setting('app.permissions', true), ''), ',') $$;\n\nCREATE OR REPLACE FUNCTION rls_has_permission(permission_name text)\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT permission_name = ANY(rls_current_permissions()) $$;\n\nCREATE OR REPLACE FUNCTION rls_is_system()\nRETURNS boolean\nLANGUAGE SQL STABLE\nAS $$ SELECT COALESCE(current_setting('app.is_system', true), 'false')::boolean $$;\n`\n }\n\n /**\n * Generate DROP statements for cleaning up\n */\n generateDropStatements<DB>(schema: RLSSchema<DB>, options: PostgresRLSOptions = {}): string[] {\n const { schemaName = 'public', policyPrefix = 'rls' } = options\n const statements: string[] = []\n\n for (const table of Object.keys(schema)) {\n const qualifiedTable = `${schemaName}.${table}`\n\n // Drop all policies with prefix\n statements.push(\n `DO $$ BEGIN\n EXECUTE (\n SELECT string_agg('DROP POLICY IF EXISTS ' || quote_ident(policyname) || ' ON ${qualifiedTable};', E'\\\\n')\n FROM pg_policies\n WHERE tablename = '${table}'\n AND schemaname = '${schemaName}'\n AND policyname LIKE '${policyPrefix}_%'\n );\nEND $$;`\n )\n\n // Disable RLS\n statements.push(`ALTER TABLE ${qualifiedTable} DISABLE ROW LEVEL SECURITY;`)\n }\n\n return statements\n }\n}\n\n/**\n * Sync RLS context to PostgreSQL session settings\n * Call this at the start of each request/transaction\n */\nexport async function syncContextToPostgres<DB>(\n db: Kysely<DB>,\n context: {\n userId: string | number\n tenantId?: string | number\n roles?: string[]\n permissions?: string[]\n isSystem?: boolean\n }\n): Promise<void> {\n const { userId, tenantId, roles, permissions, isSystem } = context\n\n await sql`\n SELECT\n set_config('app.user_id', ${String(userId)}, true),\n set_config('app.tenant_id', ${tenantId ? String(tenantId) : ''}, true),\n set_config('app.roles', ${(roles ?? []).join(',')}, true),\n set_config('app.permissions', ${(permissions ?? []).join(',')}, true),\n set_config('app.is_system', ${isSystem ? 'true' : 'false'}, true)\n `.execute(db)\n}\n\n/**\n * Clear RLS context from PostgreSQL session\n */\nexport async function clearPostgresContext<DB>(db: Kysely<DB>): Promise<void> {\n await sql`\n SELECT\n set_config('app.user_id', '', true),\n set_config('app.tenant_id', '', true),\n set_config('app.roles', '', true),\n set_config('app.permissions', '', true),\n set_config('app.is_system', 'false', true)\n `.execute(db)\n}\n","import type { RLSSchema } from '../policy/types.js'\nimport { PostgresRLSGenerator, type PostgresRLSOptions } from './postgres.js'\n\n/**\n * Options for migration generation\n */\nexport interface MigrationOptions extends PostgresRLSOptions {\n /** Migration name */\n name?: string\n /** Include context functions in migration */\n includeContextFunctions?: boolean\n}\n\n/**\n * RLS Migration Generator\n * Generates Kysely migration files for RLS policies\n */\nexport class RLSMigrationGenerator {\n private generator = new PostgresRLSGenerator()\n\n /**\n * Generate migration file content\n */\n generateMigration<DB>(schema: RLSSchema<DB>, options: MigrationOptions = {}): string {\n const { name = 'rls_policies', includeContextFunctions = true, ...generatorOptions } = options\n\n const upStatements = this.generator.generateStatements(schema, generatorOptions)\n const downStatements = this.generator.generateDropStatements(schema, generatorOptions)\n\n const contextFunctions = includeContextFunctions\n ? this.generator.generateContextFunctions()\n : ''\n\n return `import { Kysely, sql } from 'kysely';\n\n/**\n * Migration: ${name}\n * Generated by @kysera/rls\n *\n * This migration sets up Row-Level Security policies for the database.\n */\n\nexport async function up(db: Kysely<any>): Promise<void> {\n${\n includeContextFunctions\n ? ` // Create RLS context functions\n await sql.raw(\\`${this.escapeTemplate(contextFunctions)}\\`).execute(db);\n\n`\n : ''\n} // Enable RLS and create policies\n${upStatements.map(s => ` await sql.raw(\\`${this.escapeTemplate(s)}\\`).execute(db);`).join('\\n')}\n}\n\nexport async function down(db: Kysely<any>): Promise<void> {\n${downStatements.map(s => ` await sql.raw(\\`${this.escapeTemplate(s)}\\`).execute(db);`).join('\\n')}\n${\n includeContextFunctions\n ? `\n // Drop RLS context functions\n await sql.raw(\\`\n DROP FUNCTION IF EXISTS rls_current_user_id();\n DROP FUNCTION IF EXISTS rls_current_tenant_id();\n DROP FUNCTION IF EXISTS rls_current_roles();\n DROP FUNCTION IF EXISTS rls_has_role(text);\n DROP FUNCTION IF EXISTS rls_current_permissions();\n DROP FUNCTION IF EXISTS rls_has_permission(text);\n DROP FUNCTION IF EXISTS rls_is_system();\n \\`).execute(db);`\n : ''\n}\n}\n`\n }\n\n /**\n * Escape template literal for embedding in string\n */\n private escapeTemplate(str: string): string {\n return str.replace(/`/g, '\\\\`').replace(/\\$/g, '\\\\$')\n }\n\n /**\n * Generate migration filename with timestamp\n */\n generateFilename(name = 'rls_policies'): string {\n const timestamp = new Date()\n .toISOString()\n .replace(/[-:]/g, '')\n .replace('T', '_')\n .replace(/\\..+/, '')\n return `${timestamp}_${name}.ts`\n }\n}\n"]}
|
|
@@ -199,7 +199,7 @@ interface RLSContext<TUser = unknown, TMeta = unknown> {
|
|
|
199
199
|
* @typeParam TAuth - Custom user type for auth context
|
|
200
200
|
* @typeParam TRow - Type of the database row being evaluated
|
|
201
201
|
* @typeParam TData - Type of the data being inserted/updated
|
|
202
|
-
* @typeParam
|
|
202
|
+
* @typeParam DB - Database schema type for Kysely
|
|
203
203
|
*
|
|
204
204
|
* @example
|
|
205
205
|
* ```typescript
|
|
@@ -217,7 +217,7 @@ interface RLSContext<TUser = unknown, TMeta = unknown> {
|
|
|
217
217
|
* };
|
|
218
218
|
* ```
|
|
219
219
|
*/
|
|
220
|
-
interface PolicyEvaluationContext<TAuth = unknown, TRow = unknown, TData = unknown,
|
|
220
|
+
interface PolicyEvaluationContext<TAuth = unknown, TRow = unknown, TData = unknown, DB = unknown> {
|
|
221
221
|
/**
|
|
222
222
|
* Authentication context
|
|
223
223
|
* Contains user identity and authorization information
|
|
@@ -245,7 +245,7 @@ interface PolicyEvaluationContext<TAuth = unknown, TRow = unknown, TData = unkno
|
|
|
245
245
|
* Available for policies that need to perform additional queries
|
|
246
246
|
* Use sparingly as it can impact performance
|
|
247
247
|
*/
|
|
248
|
-
db?: Kysely<
|
|
248
|
+
db?: Kysely<DB>;
|
|
249
249
|
/**
|
|
250
250
|
* Custom metadata (optional)
|
|
251
251
|
* Can contain any additional context needed for policy evaluation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kysera/rls",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Row-Level Security plugin for Kysely - declarative policies, query transformation, native RLS support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,13 +20,20 @@
|
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"kysely": ">=0.28.8"
|
|
23
|
+
"kysely": ">=0.28.8",
|
|
24
|
+
"@kysera/executor": "0.8.0",
|
|
25
|
+
"@kysera/repository": "0.8.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependenciesMeta": {
|
|
28
|
+
"@kysera/executor": {
|
|
29
|
+
"optional": true
|
|
30
|
+
},
|
|
31
|
+
"@kysera/repository": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
24
34
|
},
|
|
25
35
|
"dependencies": {
|
|
26
|
-
"
|
|
27
|
-
"@kysera/core": "0.7.3",
|
|
28
|
-
"@kysera/repository": "0.7.3",
|
|
29
|
-
"@kysera/executor": "0.7.3"
|
|
36
|
+
"@kysera/core": "0.8.0"
|
|
30
37
|
},
|
|
31
38
|
"devDependencies": {
|
|
32
39
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -39,7 +46,9 @@
|
|
|
39
46
|
"pg": "^8.16.3",
|
|
40
47
|
"tsup": "^8.5.1",
|
|
41
48
|
"typescript": "^5.9.3",
|
|
42
|
-
"vitest": "^4.0.16"
|
|
49
|
+
"vitest": "^4.0.16",
|
|
50
|
+
"@kysera/executor": "0.8.0",
|
|
51
|
+
"@kysera/repository": "0.8.0"
|
|
43
52
|
},
|
|
44
53
|
"keywords": [
|
|
45
54
|
"kysely",
|
package/src/context/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export { rlsStorage } from './storage.js'
|
|
1
|
+
export { rlsStorage } from './storage.js'
|
|
2
2
|
export {
|
|
3
3
|
rlsContext,
|
|
4
4
|
createRLSContext,
|
|
5
5
|
withRLSContext,
|
|
6
6
|
withRLSContextAsync,
|
|
7
|
-
type CreateRLSContextOptions
|
|
8
|
-
} from './manager.js'
|
|
9
|
-
export type { RLSContext, RLSAuthContext, RLSRequestContext } from './types.js'
|
|
7
|
+
type CreateRLSContextOptions
|
|
8
|
+
} from './manager.js'
|
|
9
|
+
export type { RLSContext, RLSAuthContext, RLSRequestContext } from './types.js'
|
package/src/context/manager.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { rlsStorage } from './storage.js'
|
|
2
|
-
import type { RLSContext, RLSAuthContext, RLSRequestContext } from './types.js'
|
|
3
|
-
import { RLSContextError, RLSContextValidationError } from '../errors.js'
|
|
1
|
+
import { rlsStorage } from './storage.js'
|
|
2
|
+
import type { RLSContext, RLSAuthContext, RLSRequestContext } from './types.js'
|
|
3
|
+
import { RLSContextError, RLSContextValidationError } from '../errors.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Options for creating RLS context
|
|
7
7
|
*/
|
|
8
8
|
export interface CreateRLSContextOptions<TUser = unknown, TMeta = unknown> {
|
|
9
|
-
auth: RLSAuthContext<TUser
|
|
10
|
-
request?: Partial<RLSRequestContext
|
|
11
|
-
meta?: TMeta
|
|
9
|
+
auth: RLSAuthContext<TUser>
|
|
10
|
+
request?: Partial<RLSRequestContext>
|
|
11
|
+
meta?: TMeta
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -17,28 +17,28 @@ export interface CreateRLSContextOptions<TUser = unknown, TMeta = unknown> {
|
|
|
17
17
|
export function createRLSContext<TUser = unknown, TMeta = unknown>(
|
|
18
18
|
options: CreateRLSContextOptions<TUser, TMeta>
|
|
19
19
|
): RLSContext<TUser, TMeta> {
|
|
20
|
-
validateAuthContext(options.auth)
|
|
20
|
+
validateAuthContext(options.auth)
|
|
21
21
|
|
|
22
22
|
const context: RLSContext<TUser, TMeta> = {
|
|
23
23
|
auth: {
|
|
24
24
|
...options.auth,
|
|
25
|
-
isSystem: options.auth.isSystem ?? false
|
|
25
|
+
isSystem: options.auth.isSystem ?? false // Default to false if not provided
|
|
26
26
|
},
|
|
27
|
-
timestamp: new Date()
|
|
28
|
-
}
|
|
27
|
+
timestamp: new Date()
|
|
28
|
+
}
|
|
29
29
|
|
|
30
30
|
if (options.request) {
|
|
31
31
|
context.request = {
|
|
32
32
|
...options.request,
|
|
33
|
-
timestamp: options.request.timestamp ?? new Date()
|
|
34
|
-
} as RLSRequestContext
|
|
33
|
+
timestamp: options.request.timestamp ?? new Date()
|
|
34
|
+
} as RLSRequestContext
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (options.meta !== undefined) {
|
|
38
|
-
context.meta = options.meta
|
|
38
|
+
context.meta = options.meta
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
return context
|
|
41
|
+
return context
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -46,11 +46,11 @@ export function createRLSContext<TUser = unknown, TMeta = unknown>(
|
|
|
46
46
|
*/
|
|
47
47
|
function validateAuthContext(auth: RLSAuthContext): void {
|
|
48
48
|
if (auth.userId === undefined || auth.userId === null) {
|
|
49
|
-
throw new RLSContextValidationError('userId is required in auth context', 'userId')
|
|
49
|
+
throw new RLSContextValidationError('userId is required in auth context', 'userId')
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (!Array.isArray(auth.roles)) {
|
|
53
|
-
throw new RLSContextValidationError('roles must be an array', 'roles')
|
|
53
|
+
throw new RLSContextValidationError('roles must be an array', 'roles')
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -63,14 +63,14 @@ class RLSContextManager {
|
|
|
63
63
|
* Run a synchronous function within an RLS context
|
|
64
64
|
*/
|
|
65
65
|
run<T>(context: RLSContext, fn: () => T): T {
|
|
66
|
-
return rlsStorage.run(context, fn)
|
|
66
|
+
return rlsStorage.run(context, fn)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Run an async function within an RLS context
|
|
71
71
|
*/
|
|
72
72
|
async runAsync<T>(context: RLSContext, fn: () => Promise<T>): Promise<T> {
|
|
73
|
-
return rlsStorage.run(context, fn)
|
|
73
|
+
return await rlsStorage.run(context, fn)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -78,25 +78,25 @@ class RLSContextManager {
|
|
|
78
78
|
* @throws RLSContextError if no context is set
|
|
79
79
|
*/
|
|
80
80
|
getContext(): RLSContext {
|
|
81
|
-
const ctx = rlsStorage.getStore()
|
|
81
|
+
const ctx = rlsStorage.getStore()
|
|
82
82
|
if (!ctx) {
|
|
83
|
-
throw new RLSContextError()
|
|
83
|
+
throw new RLSContextError()
|
|
84
84
|
}
|
|
85
|
-
return ctx
|
|
85
|
+
return ctx
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Get current RLS context or null if not set
|
|
90
90
|
*/
|
|
91
91
|
getContextOrNull(): RLSContext | null {
|
|
92
|
-
return rlsStorage.getStore() ?? null
|
|
92
|
+
return rlsStorage.getStore() ?? null
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Check if running within RLS context
|
|
97
97
|
*/
|
|
98
98
|
hasContext(): boolean {
|
|
99
|
-
return rlsStorage.getStore() !== undefined
|
|
99
|
+
return rlsStorage.getStore() !== undefined
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|
|
@@ -104,7 +104,7 @@ class RLSContextManager {
|
|
|
104
104
|
* @throws RLSContextError if no context is set
|
|
105
105
|
*/
|
|
106
106
|
getAuth(): RLSAuthContext {
|
|
107
|
-
return this.getContext().auth
|
|
107
|
+
return this.getContext().auth
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
@@ -112,7 +112,7 @@ class RLSContextManager {
|
|
|
112
112
|
* @throws RLSContextError if no context is set
|
|
113
113
|
*/
|
|
114
114
|
getUserId(): string | number {
|
|
115
|
-
return this.getAuth().userId
|
|
115
|
+
return this.getAuth().userId
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
@@ -120,76 +120,76 @@ class RLSContextManager {
|
|
|
120
120
|
* @throws RLSContextError if no context is set
|
|
121
121
|
*/
|
|
122
122
|
getTenantId(): string | number | undefined {
|
|
123
|
-
return this.getAuth().tenantId
|
|
123
|
+
return this.getAuth().tenantId
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
127
|
* Check if current user has a specific role
|
|
128
128
|
*/
|
|
129
129
|
hasRole(role: string): boolean {
|
|
130
|
-
const ctx = this.getContextOrNull()
|
|
131
|
-
return ctx?.auth.roles.includes(role) ?? false
|
|
130
|
+
const ctx = this.getContextOrNull()
|
|
131
|
+
return ctx?.auth.roles.includes(role) ?? false
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* Check if current user has a specific permission
|
|
136
136
|
*/
|
|
137
137
|
hasPermission(permission: string): boolean {
|
|
138
|
-
const ctx = this.getContextOrNull()
|
|
139
|
-
return ctx?.auth.permissions?.includes(permission) ?? false
|
|
138
|
+
const ctx = this.getContextOrNull()
|
|
139
|
+
return ctx?.auth.permissions?.includes(permission) ?? false
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* Check if current context is a system context (bypasses RLS)
|
|
144
144
|
*/
|
|
145
145
|
isSystem(): boolean {
|
|
146
|
-
const ctx = this.getContextOrNull()
|
|
147
|
-
return ctx?.auth.isSystem ?? false
|
|
146
|
+
const ctx = this.getContextOrNull()
|
|
147
|
+
return ctx?.auth.isSystem ?? false
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
151
|
* Create a system context for operations that should bypass RLS
|
|
152
152
|
*/
|
|
153
153
|
asSystem<T>(fn: () => T): T {
|
|
154
|
-
const currentCtx = this.getContextOrNull()
|
|
154
|
+
const currentCtx = this.getContextOrNull()
|
|
155
155
|
if (!currentCtx) {
|
|
156
|
-
throw new RLSContextError('Cannot create system context without existing context')
|
|
156
|
+
throw new RLSContextError('Cannot create system context without existing context')
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
const systemCtx: RLSContext = {
|
|
160
160
|
...currentCtx,
|
|
161
|
-
auth: { ...currentCtx.auth, isSystem: true }
|
|
162
|
-
}
|
|
161
|
+
auth: { ...currentCtx.auth, isSystem: true }
|
|
162
|
+
}
|
|
163
163
|
|
|
164
|
-
return this.run(systemCtx, fn)
|
|
164
|
+
return this.run(systemCtx, fn)
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
168
|
* Create a system context for async operations
|
|
169
169
|
*/
|
|
170
170
|
async asSystemAsync<T>(fn: () => Promise<T>): Promise<T> {
|
|
171
|
-
const currentCtx = this.getContextOrNull()
|
|
171
|
+
const currentCtx = this.getContextOrNull()
|
|
172
172
|
if (!currentCtx) {
|
|
173
|
-
throw new RLSContextError('Cannot create system context without existing context')
|
|
173
|
+
throw new RLSContextError('Cannot create system context without existing context')
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
const systemCtx: RLSContext = {
|
|
177
177
|
...currentCtx,
|
|
178
|
-
auth: { ...currentCtx.auth, isSystem: true }
|
|
179
|
-
}
|
|
178
|
+
auth: { ...currentCtx.auth, isSystem: true }
|
|
179
|
+
}
|
|
180
180
|
|
|
181
|
-
return this.runAsync(systemCtx, fn)
|
|
181
|
+
return await this.runAsync(systemCtx, fn)
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// Export singleton instance
|
|
186
|
-
export const rlsContext = new RLSContextManager()
|
|
186
|
+
export const rlsContext = new RLSContextManager()
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
189
|
* Convenience function to run code within RLS context
|
|
190
190
|
*/
|
|
191
191
|
export function withRLSContext<T>(context: RLSContext, fn: () => T): T {
|
|
192
|
-
return rlsContext.run(context, fn)
|
|
192
|
+
return rlsContext.run(context, fn)
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
@@ -199,5 +199,5 @@ export async function withRLSContextAsync<T>(
|
|
|
199
199
|
context: RLSContext,
|
|
200
200
|
fn: () => Promise<T>
|
|
201
201
|
): Promise<T> {
|
|
202
|
-
return rlsContext.runAsync(context, fn)
|
|
202
|
+
return await rlsContext.runAsync(context, fn)
|
|
203
203
|
}
|
package/src/context/storage.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
-
import type { RLSContext } from './types.js'
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import type { RLSContext } from './types.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* AsyncLocalStorage instance for RLS context
|
|
6
6
|
* Provides automatic context propagation across async boundaries
|
|
7
7
|
*/
|
|
8
|
-
export const rlsStorage = new AsyncLocalStorage<RLSContext>()
|
|
8
|
+
export const rlsStorage = new AsyncLocalStorage<RLSContext>()
|
package/src/context/types.ts
CHANGED