@kysera/rls 0.8.7 → 0.8.8

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.
@@ -1,5 +1,5 @@
1
1
  import { Kysely } from 'kysely';
2
- import { R as RLSSchema } from '../types-CyqksFKU.js';
2
+ import { R as RLSSchema } from '../types-D3hQINlj.js';
3
3
 
4
4
  /**
5
5
  * Options for PostgreSQL RLS generation
@@ -1,6 +1,63 @@
1
1
  import { sql } from 'kysely';
2
+ import { DatabaseError } from '@kysera/core';
2
3
 
3
4
  // src/native/postgres.ts
5
+ var RLSErrorCodes = {
6
+ /** RLS schema definition is invalid */
7
+ RLS_SCHEMA_INVALID: "RLS_SCHEMA_INVALID"};
8
+ var RLSError = class extends DatabaseError {
9
+ /**
10
+ * Creates a new RLS error
11
+ *
12
+ * @param message - Error message
13
+ * @param code - RLS error code
14
+ */
15
+ constructor(message, code) {
16
+ super(message, code);
17
+ this.name = "RLSError";
18
+ }
19
+ };
20
+ var RLSSchemaError = class extends RLSError {
21
+ details;
22
+ /**
23
+ * Creates a new schema validation error
24
+ *
25
+ * @param message - Error message
26
+ * @param details - Additional details about the validation failure
27
+ */
28
+ constructor(message, details = {}) {
29
+ super(message, RLSErrorCodes.RLS_SCHEMA_INVALID);
30
+ this.name = "RLSSchemaError";
31
+ this.details = details;
32
+ }
33
+ toJSON() {
34
+ return {
35
+ ...super.toJSON(),
36
+ details: this.details
37
+ };
38
+ }
39
+ };
40
+
41
+ // src/native/postgres.ts
42
+ var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
43
+ function quoteIdent(name, context) {
44
+ if (!SAFE_IDENTIFIER.test(name)) {
45
+ throw new RLSSchemaError(
46
+ `Unsafe ${context}: "${name}". Only alphanumeric and underscore characters are allowed.`,
47
+ { identifier: name, context }
48
+ );
49
+ }
50
+ return '"' + name.replace(/"/g, '""') + '"';
51
+ }
52
+ function quoteLiteral(value, context) {
53
+ if (!SAFE_IDENTIFIER.test(value)) {
54
+ throw new RLSSchemaError(
55
+ `Unsafe ${context}: "${value}". Only alphanumeric and underscore characters are allowed.`,
56
+ { identifier: value, context }
57
+ );
58
+ }
59
+ return "'" + value.replace(/'/g, "''") + "'";
60
+ }
4
61
  var PostgresRLSGenerator = class {
5
62
  /**
6
63
  * Generate all PostgreSQL RLS statements from schema
@@ -8,9 +65,11 @@ var PostgresRLSGenerator = class {
8
65
  generateStatements(schema, options = {}) {
9
66
  const { force = true, schemaName = "public", policyPrefix = "rls" } = options;
10
67
  const statements = [];
68
+ const quotedSchema = quoteIdent(schemaName, "schema name");
11
69
  for (const [table, config] of Object.entries(schema)) {
12
70
  if (!config) continue;
13
- const qualifiedTable = `${schemaName}.${table}`;
71
+ const quotedTable = quoteIdent(table, "table name");
72
+ const qualifiedTable = `${quotedSchema}.${quotedTable}`;
14
73
  const tableConfig = config;
15
74
  statements.push(`ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY;`);
16
75
  if (force) {
@@ -18,8 +77,8 @@ var PostgresRLSGenerator = class {
18
77
  }
19
78
  let policyIndex = 0;
20
79
  for (const policy of tableConfig.policies) {
21
- const policyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`;
22
- const policySQL = this.generatePolicy(qualifiedTable, policyName, policy);
80
+ const rawPolicyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`;
81
+ const policySQL = this.generatePolicy(qualifiedTable, rawPolicyName, policy);
23
82
  if (policySQL) {
24
83
  statements.push(policySQL);
25
84
  }
@@ -37,13 +96,15 @@ var PostgresRLSGenerator = class {
37
96
  if (!policy.using && !policy.withCheck) {
38
97
  return null;
39
98
  }
40
- const parts = [`CREATE POLICY "${name}"`, `ON ${table}`];
99
+ const quotedName = quoteIdent(name, "policy name");
100
+ const parts = [`CREATE POLICY ${quotedName}`, `ON ${table}`];
41
101
  if (policy.type === "deny") {
42
102
  parts.push("AS RESTRICTIVE");
43
103
  } else {
44
104
  parts.push("AS PERMISSIVE");
45
105
  }
46
- parts.push(`TO ${policy.role ?? "public"}`);
106
+ const role = policy.role ?? "public";
107
+ parts.push(`TO ${quoteIdent(role, "role name")}`);
47
108
  parts.push(`FOR ${this.mapOperation(policy.operation)}`);
48
109
  if (policy.using) {
49
110
  parts.push(`USING (${policy.using})`);
@@ -138,15 +199,23 @@ AS $$ SELECT COALESCE(current_setting('app.is_system', true), 'false')::boolean
138
199
  generateDropStatements(schema, options = {}) {
139
200
  const { schemaName = "public", policyPrefix = "rls" } = options;
140
201
  const statements = [];
202
+ const quotedSchema = quoteIdent(schemaName, "schema name");
203
+ if (!SAFE_IDENTIFIER.test(policyPrefix)) {
204
+ throw new RLSSchemaError(
205
+ `Unsafe policy prefix: "${policyPrefix}". Only alphanumeric and underscore characters are allowed.`,
206
+ { identifier: policyPrefix, context: "policy prefix" }
207
+ );
208
+ }
141
209
  for (const table of Object.keys(schema)) {
142
- const qualifiedTable = `${schemaName}.${table}`;
210
+ const quotedTable = quoteIdent(table, "table name");
211
+ const qualifiedTable = `${quotedSchema}.${quotedTable}`;
143
212
  statements.push(
144
213
  `DO $$ BEGIN
145
214
  EXECUTE (
146
215
  SELECT string_agg('DROP POLICY IF EXISTS ' || quote_ident(policyname) || ' ON ${qualifiedTable};', E'\\n')
147
216
  FROM pg_policies
148
- WHERE tablename = '${table}'
149
- AND schemaname = '${schemaName}'
217
+ WHERE tablename = ${quoteLiteral(table, "table name")}
218
+ AND schemaname = ${quoteLiteral(schemaName, "schema name")}
150
219
  AND policyname LIKE '${policyPrefix}_%'
151
220
  );
152
221
  END $$;`
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/native/postgres.ts","../../src/native/migration.ts"],"names":[],"mappings":";;;AAoBO,IAAM,uBAAN,MAA2B;AAAA;AAAA;AAAA;AAAA,EAIhC,kBAAA,CAAuB,MAAA,EAAuB,OAAA,GAA8B,EAAC,EAAa;AACxF,IAAA,MAAM,EAAE,KAAA,GAAQ,IAAA,EAAM,aAAa,QAAA,EAAU,YAAA,GAAe,OAAM,GAAI,OAAA;AAEtE,IAAA,MAAM,aAAuB,EAAC;AAE9B,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,MAAM,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AACpD,MAAA,IAAI,CAAC,MAAA,EAAQ;AAEb,MAAA,MAAM,cAAA,GAAiB,CAAA,EAAG,UAAU,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA;AAC7C,MAAA,MAAM,WAAA,GAAc,MAAA;AAGpB,MAAA,UAAA,CAAW,IAAA,CAAK,CAAA,YAAA,EAAe,cAAc,CAAA,2BAAA,CAA6B,CAAA;AAE1E,MAAA,IAAI,KAAA,EAAO;AACT,QAAA,UAAA,CAAW,IAAA,CAAK,CAAA,YAAA,EAAe,cAAc,CAAA,0BAAA,CAA4B,CAAA;AAAA,MAC3E;AAGA,MAAA,IAAI,WAAA,GAAc,CAAA;AAClB,MAAA,KAAA,MAAW,MAAA,IAAU,YAAY,QAAA,EAAU;AACzC,QAAA,MAAM,UAAA,GAAa,MAAA,CAAO,IAAA,IAAQ,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,EAAI,MAAA,CAAO,IAAI,CAAA,CAAA,EAAI,WAAA,EAAa,CAAA,CAAA;AAC1F,QAAA,MAAM,SAAA,GAAY,IAAA,CAAK,cAAA,CAAe,cAAA,EAAgB,YAAY,MAAM,CAAA;AACxE,QAAA,IAAI,SAAA,EAAW;AACb,UAAA,UAAA,CAAW,KAAK,SAAS,CAAA;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AAEA,IAAA,OAAO,UAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,cAAA,CAAe,KAAA,EAAe,IAAA,EAAc,MAAA,EAAyC;AAE3F,IAAA,IAAI,MAAA,CAAO,IAAA,KAAS,QAAA,IAAY,MAAA,CAAO,SAAS,UAAA,EAAY;AAC1D,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,IAAI,CAAC,MAAA,CAAO,KAAA,IAAS,CAAC,OAAO,SAAA,EAAW;AACtC,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,MAAM,QAAkB,CAAC,CAAA,eAAA,EAAkB,IAAI,CAAA,CAAA,CAAA,EAAK,CAAA,GAAA,EAAM,KAAK,CAAA,CAAE,CAAA;AAGjE,IAAA,IAAI,MAAA,CAAO,SAAS,MAAA,EAAQ;AAC1B,MAAA,KAAA,CAAM,KAAK,gBAAgB,CAAA;AAAA,IAC7B,CAAA,MAAO;AACL,MAAA,KAAA,CAAM,KAAK,eAAe,CAAA;AAAA,IAC5B;AAGA,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,GAAA,EAAM,MAAA,CAAO,IAAA,IAAQ,QAAQ,CAAA,CAAE,CAAA;AAG1C,IAAA,KAAA,CAAM,KAAK,CAAA,IAAA,EAAO,IAAA,CAAK,aAAa,MAAA,CAAO,SAAS,CAAC,CAAA,CAAE,CAAA;AAGvD,IAAA,IAAI,OAAO,KAAA,EAAO;AAChB,MAAA,KAAA,CAAM,IAAA,CAAK,CAAA,OAAA,EAAU,MAAA,CAAO,KAAK,CAAA,CAAA,CAAG,CAAA;AAAA,IACtC;AAGA,IAAA,IAAI,OAAO,SAAA,EAAW;AACpB,MAAA,KAAA,CAAM,IAAA,CAAK,CAAA,YAAA,EAAe,MAAA,CAAO,SAAS,CAAA,CAAA,CAAG,CAAA;AAAA,IAC/C;AAEA,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA,GAAI,GAAA;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKQ,aAAa,SAAA,EAA4C;AAC/D,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,SAAS,CAAA,EAAG;AAC5B,MAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,QAAA,OAAO,KAAA;AAAA,MACT;AACA,MAAA,IAAI,UAAU,MAAA,KAAW,CAAA,IAAK,SAAA,CAAU,QAAA,CAAS,KAAK,CAAA,EAAG;AACvD,QAAA,OAAO,KAAA;AAAA,MACT;AAGA,MAAA,OAAO,IAAA,CAAK,kBAAA,CAAmB,SAAA,CAAU,CAAC,CAAE,CAAA;AAAA,IAC9C;AACA,IAAA,OAAO,IAAA,CAAK,mBAAmB,SAAS,CAAA;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKQ,mBAAmB,EAAA,EAAuB;AAChD,IAAA,QAAQ,EAAA;AAAI,MACV,KAAK,MAAA;AACH,QAAA,OAAO,QAAA;AAAA,MACT,KAAK,QAAA;AACH,QAAA,OAAO,QAAA;AAAA,MACT,KAAK,QAAA;AACH,QAAA,OAAO,QAAA;AAAA,MACT,KAAK,QAAA;AACH,QAAA,OAAO,QAAA;AAAA,MACT,KAAK,KAAA;AACH,QAAA,OAAO,KAAA;AAAA,MACT;AACE,QAAA,OAAO,KAAA;AAAA;AACX,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,wBAAA,GAAmC;AACjC,IAAA,OAAO;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAAA,EAuCT;AAAA;AAAA;AAAA;AAAA,EAKA,sBAAA,CAA2B,MAAA,EAAuB,OAAA,GAA8B,EAAC,EAAa;AAC5F,IAAA,MAAM,EAAE,UAAA,GAAa,QAAA,EAAU,YAAA,GAAe,OAAM,GAAI,OAAA;AACxD,IAAA,MAAM,aAAuB,EAAC;AAE9B,IAAA,KAAA,MAAW,KAAA,IAAS,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA,EAAG;AACvC,MAAA,MAAM,cAAA,GAAiB,CAAA,EAAG,UAAU,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA;AAG7C,MAAA,UAAA,CAAW,IAAA;AAAA,QACT,CAAA;AAAA;AAAA,kFAAA,EAE4E,cAAc,CAAA;AAAA;AAAA,uBAAA,EAEzE,KAAK,CAAA;AAAA,wBAAA,EACJ,UAAU,CAAA;AAAA,2BAAA,EACP,YAAY,CAAA;AAAA;AAAA,OAAA;AAAA,OAGnC;AAGA,MAAA,UAAA,CAAW,IAAA,CAAK,CAAA,YAAA,EAAe,cAAc,CAAA,4BAAA,CAA8B,CAAA;AAAA,IAC7E;AAEA,IAAA,OAAO,UAAA;AAAA,EACT;AACF;AAMA,eAAsB,qBAAA,CACpB,IACA,OAAA,EAOe;AACf,EAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAU,KAAA,EAAO,WAAA,EAAa,UAAS,GAAI,OAAA;AAE3D,EAAA,MAAM,GAAA;AAAA;AAAA,gCAAA,EAE0B,MAAA,CAAO,MAAM,CAAC,CAAA;AAAA,kCAAA,EACZ,QAAA,GAAW,MAAA,CAAO,QAAQ,CAAA,GAAI,EAAE,CAAA;AAAA,8BAAA,EAAA,CACnC,KAAA,IAAS,EAAC,EAAG,IAAA,CAAK,GAAG,CAAC,CAAA;AAAA,oCAAA,EAAA,CAChB,WAAA,IAAe,EAAC,EAAG,IAAA,CAAK,GAAG,CAAC,CAAA;AAAA,kCAAA,EAC/B,QAAA,GAAW,SAAS,OAAO,CAAA;AAAA,EAAA,CAAA,CAC3D,QAAQ,EAAE,CAAA;AACd;AAKA,eAAsB,qBAAyB,EAAA,EAA+B;AAC5E,EAAA,MAAM,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAAA,CAAA,CAOJ,QAAQ,EAAE,CAAA;AACd;;;AC1OO,IAAM,wBAAN,MAA4B;AAAA,EACzB,SAAA,GAAY,IAAI,oBAAA,EAAqB;AAAA;AAAA;AAAA;AAAA,EAK7C,iBAAA,CAAsB,MAAA,EAAuB,OAAA,GAA4B,EAAC,EAAW;AACnF,IAAA,MAAM,EAAE,IAAA,GAAO,cAAA,EAAgB,0BAA0B,IAAA,EAAM,GAAG,kBAAiB,GAAI,OAAA;AAEvF,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,SAAA,CAAU,kBAAA,CAAmB,QAAQ,gBAAgB,CAAA;AAC/E,IAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,SAAA,CAAU,sBAAA,CAAuB,QAAQ,gBAAgB,CAAA;AAErF,IAAA,MAAM,gBAAA,GAAmB,uBAAA,GACrB,IAAA,CAAK,SAAA,CAAU,0BAAyB,GACxC,EAAA;AAEJ,IAAA,OAAO,CAAA;;AAAA;AAAA,cAAA,EAGK,IAAI;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA,EAQlB,uBAAA,GACI,CAAA;AAAA,kBAAA,EACc,IAAA,CAAK,cAAA,CAAe,gBAAgB,CAAC,CAAA;;AAAA,CAAA,GAGnD,EACN,CAAA;AAAA,EACE,YAAA,CAAa,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,kBAAA,EAAqB,IAAA,CAAK,cAAA,CAAe,CAAC,CAAC,CAAA,gBAAA,CAAkB,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC;AAAA;;AAAA;AAAA,EAI/F,cAAA,CAAe,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,kBAAA,EAAqB,IAAA,CAAK,cAAA,CAAe,CAAC,CAAC,CAAA,gBAAA,CAAkB,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC;AAAA,EAEjG,uBAAA,GACI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAA,CAAA,GAWA,EACN;AAAA;AAAA,CAAA;AAAA,EAGE;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAe,GAAA,EAAqB;AAC1C,IAAA,OAAO,IAAI,OAAA,CAAQ,IAAA,EAAM,KAAK,CAAA,CAAE,OAAA,CAAQ,OAAO,KAAK,CAAA;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAA,CAAiB,OAAO,cAAA,EAAwB;AAC9C,IAAA,MAAM,6BAAY,IAAI,IAAA,EAAK,EACxB,WAAA,GACA,OAAA,CAAQ,OAAA,EAAS,EAAE,CAAA,CACnB,QAAQ,GAAA,EAAK,GAAG,CAAA,CAChB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACrB,IAAA,OAAO,CAAA,EAAG,SAAS,CAAA,CAAA,EAAI,IAAI,CAAA,GAAA,CAAA;AAAA,EAC7B;AACF","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"]}
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/native/postgres.ts","../../src/native/migration.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * RLS Error Classes\n *\n * This module provides specialized error classes for Row-Level Security operations.\n * All errors extend base error classes from @kysera/core for consistency across\n * the Kysera ecosystem.\n *\n * @module @kysera/rls/errors\n */\n\nimport { DatabaseError } from '@kysera/core'\nimport type { ErrorCode } from '@kysera/core'\n\n// ============================================================================\n// RLS Error Codes\n// ============================================================================\n\n/**\n * RLS-specific error codes\n *\n * These codes extend the unified error codes from @kysera/core with\n * RLS-specific error conditions.\n */\nexport const RLSErrorCodes = {\n /** RLS context is missing or not set */\n RLS_CONTEXT_MISSING: 'RLS_CONTEXT_MISSING' as ErrorCode,\n /** RLS policy violation occurred */\n RLS_POLICY_VIOLATION: 'RLS_POLICY_VIOLATION' as ErrorCode,\n /** RLS policy definition is invalid */\n RLS_POLICY_INVALID: 'RLS_POLICY_INVALID' as ErrorCode,\n /** RLS schema definition is invalid */\n RLS_SCHEMA_INVALID: 'RLS_SCHEMA_INVALID' as ErrorCode,\n /** RLS context validation failed */\n RLS_CONTEXT_INVALID: 'RLS_CONTEXT_INVALID' as ErrorCode,\n /** RLS policy evaluation threw an error */\n RLS_POLICY_EVALUATION_ERROR: 'RLS_POLICY_EVALUATION_ERROR' as ErrorCode\n} as const\n\n/**\n * Type for RLS error codes\n */\nexport type RLSErrorCode = (typeof RLSErrorCodes)[keyof typeof RLSErrorCodes]\n\n// ============================================================================\n// Base RLS Error\n// ============================================================================\n\n/**\n * Base class for all RLS-related errors\n *\n * Extends DatabaseError from @kysera/core for consistency with other Kysera packages.\n * Provides common error functionality including error codes and JSON serialization.\n *\n * @example\n * ```typescript\n * throw new RLSError('Something went wrong', RLSErrorCodes.RLS_POLICY_INVALID);\n * ```\n */\nexport class RLSError extends DatabaseError {\n /**\n * Creates a new RLS error\n *\n * @param message - Error message\n * @param code - RLS error code\n */\n constructor(message: string, code: RLSErrorCode) {\n super(message, code)\n this.name = 'RLSError'\n }\n}\n\n// ============================================================================\n// Context Errors\n// ============================================================================\n\n/**\n * Error thrown when RLS context is missing\n *\n * This error occurs when an operation requiring RLS context is executed\n * outside of a context scope (i.e., without calling withRLSContext()).\n *\n * @example\n * ```typescript\n * // This will throw RLSContextError\n * const result = await db.selectFrom('posts').execute();\n *\n * // Correct usage with context\n * await withRLSContext(rlsContext, async () => {\n * const result = await db.selectFrom('posts').execute();\n * });\n * ```\n */\nexport class RLSContextError extends RLSError {\n /**\n * Creates a new RLS context error\n *\n * @param message - Error message (defaults to standard message)\n */\n constructor(message = 'No RLS context found. Ensure code runs within withRLSContext()') {\n super(message, RLSErrorCodes.RLS_CONTEXT_MISSING)\n this.name = 'RLSContextError'\n }\n}\n\n/**\n * Error thrown when RLS context validation fails\n *\n * Extends RLSError as context validation failures are RLS-specific errors.\n *\n * This error occurs when the provided RLS context is invalid or missing\n * required fields.\n *\n * @example\n * ```typescript\n * // Missing required userId field\n * const invalidContext = {\n * auth: {\n * roles: ['user']\n * // userId is missing!\n * },\n * timestamp: new Date()\n * };\n *\n * // This will throw RLSContextValidationError\n * validateRLSContext(invalidContext);\n * ```\n */\nexport class RLSContextValidationError extends RLSError {\n public readonly field: string\n\n /**\n * Creates a new context validation error\n *\n * @param message - Error message\n * @param field - Field that failed validation\n */\n constructor(message: string, field: string) {\n super(message, RLSErrorCodes.RLS_CONTEXT_INVALID)\n this.name = 'RLSContextValidationError'\n this.field = field\n }\n\n override toJSON(): Record<string, unknown> {\n return {\n ...super.toJSON(),\n field: this.field\n }\n }\n}\n\n// ============================================================================\n// Policy Errors\n// ============================================================================\n\n/**\n * Error thrown when an RLS policy violation occurs\n *\n * This error is thrown when a database operation is denied by RLS policies.\n * It provides detailed information about the violation including the operation,\n * table, and reason for denial.\n *\n * @example\n * ```typescript\n * // User tries to update a post they don't own\n * throw new RLSPolicyViolation(\n * 'update',\n * 'posts',\n * 'User does not own this post',\n * 'ownership_policy'\n * );\n * ```\n */\nexport class RLSPolicyViolation extends RLSError {\n public readonly operation: string\n public readonly table: string\n public readonly reason: string\n public readonly policyName?: string\n\n /**\n * Creates a new policy violation error\n *\n * @param operation - Database operation that was denied (read, create, update, delete)\n * @param table - Table name where violation occurred\n * @param reason - Reason for the policy violation\n * @param policyName - Name of the policy that denied access (optional)\n */\n constructor(operation: string, table: string, reason: string, policyName?: string) {\n super(\n `RLS policy violation: ${operation} on ${table} - ${reason}`,\n RLSErrorCodes.RLS_POLICY_VIOLATION\n )\n this.name = 'RLSPolicyViolation'\n this.operation = operation\n this.table = table\n this.reason = reason\n if (policyName !== undefined) {\n this.policyName = policyName\n }\n }\n\n override toJSON(): Record<string, unknown> {\n const json: Record<string, unknown> = {\n ...super.toJSON(),\n operation: this.operation,\n table: this.table,\n reason: this.reason\n }\n if (this.policyName !== undefined) {\n json['policyName'] = this.policyName\n }\n return json\n }\n}\n\n// ============================================================================\n// Policy Evaluation Errors\n// ============================================================================\n\n/**\n * Error thrown when a policy condition throws an error during evaluation\n *\n * This error is distinct from RLSPolicyViolation - it indicates a bug in the\n * policy condition function itself, not a legitimate access denial.\n *\n * @example\n * ```typescript\n * // A policy with a bug\n * allow('read', ctx => {\n * return ctx.row.someField.value; // Throws if someField is undefined\n * });\n *\n * // This will throw RLSPolicyEvaluationError, not RLSPolicyViolation\n * ```\n */\nexport class RLSPolicyEvaluationError extends RLSError {\n public readonly operation: string\n public readonly table: string\n public readonly policyName?: string\n public readonly originalError?: Error\n\n /**\n * Creates a new policy evaluation error\n *\n * @param operation - Database operation being performed\n * @param table - Table name where error occurred\n * @param message - Error message from the policy\n * @param policyName - Name of the policy that threw\n * @param originalError - The original error thrown by the policy\n */\n constructor(\n operation: string,\n table: string,\n message: string,\n policyName?: string,\n originalError?: Error\n ) {\n super(\n `RLS policy evaluation error during ${operation} on ${table}: ${message}`,\n RLSErrorCodes.RLS_POLICY_EVALUATION_ERROR\n )\n this.name = 'RLSPolicyEvaluationError'\n this.operation = operation\n this.table = table\n if (policyName !== undefined) {\n this.policyName = policyName\n }\n if (originalError !== undefined) {\n this.originalError = originalError\n // Preserve the original stack trace for debugging\n if (originalError.stack) {\n this.stack = `${this.stack}\\n\\nCaused by:\\n${originalError.stack}`\n }\n }\n }\n\n override toJSON(): Record<string, unknown> {\n const json: Record<string, unknown> = {\n ...super.toJSON(),\n operation: this.operation,\n table: this.table\n }\n if (this.policyName !== undefined) {\n json['policyName'] = this.policyName\n }\n if (this.originalError !== undefined) {\n json['originalError'] = {\n name: this.originalError.name,\n message: this.originalError.message\n }\n }\n return json\n }\n}\n\n// ============================================================================\n// Schema Errors\n// ============================================================================\n\n/**\n * Error thrown when RLS schema validation fails\n *\n * Extends RLSError as schema validation failures are RLS-specific errors.\n *\n * This error occurs when the RLS schema definition is invalid or contains\n * configuration errors.\n *\n * @example\n * ```typescript\n * // Invalid policy definition\n * const invalidSchema = {\n * posts: {\n * policies: [\n * {\n * type: 'invalid-type', // Invalid policy type!\n * operation: 'read',\n * condition: (ctx) => true\n * }\n * ]\n * }\n * };\n *\n * // This will throw RLSSchemaError\n * validateRLSSchema(invalidSchema);\n * ```\n */\nexport class RLSSchemaError extends RLSError {\n public readonly details: Record<string, unknown>\n\n /**\n * Creates a new schema validation error\n *\n * @param message - Error message\n * @param details - Additional details about the validation failure\n */\n constructor(message: string, details: Record<string, unknown> = {}) {\n super(message, RLSErrorCodes.RLS_SCHEMA_INVALID)\n this.name = 'RLSSchemaError'\n this.details = details\n }\n\n override toJSON(): Record<string, unknown> {\n return {\n ...super.toJSON(),\n details: this.details\n }\n }\n}\n","import type { Kysely } from 'kysely'\nimport { sql } from 'kysely'\nimport type { RLSSchema, TableRLSConfig, PolicyDefinition, Operation } from '../policy/types.js'\nimport { RLSSchemaError } from '../errors.js'\n\n/**\n * Pattern for valid SQL identifiers (prevents injection in DDL statements)\n */\nconst SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\n/**\n * Validate and quote a SQL identifier to prevent injection.\n * Only allows alphanumeric + underscore characters.\n * @throws RLSSchemaError if identifier contains unsafe characters\n */\nfunction quoteIdent(name: string, context: string): string {\n if (!SAFE_IDENTIFIER.test(name)) {\n throw new RLSSchemaError(\n `Unsafe ${context}: \"${name}\". Only alphanumeric and underscore characters are allowed.`,\n { identifier: name, context }\n )\n }\n return '\"' + name.replace(/\"/g, '\"\"') + '\"'\n}\n\n/**\n * Quote a string literal for use in PL/pgSQL (single quotes, doubled for escaping)\n */\nfunction quoteLiteral(value: string, context: string): string {\n if (!SAFE_IDENTIFIER.test(value)) {\n throw new RLSSchemaError(\n `Unsafe ${context}: \"${value}\". Only alphanumeric and underscore characters are allowed.`,\n { identifier: value, context }\n )\n }\n return \"'\" + value.replace(/'/g, \"''\") + \"'\"\n}\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 const quotedSchema = quoteIdent(schemaName, 'schema name')\n\n for (const [table, config] of Object.entries(schema)) {\n if (!config) continue\n\n const quotedTable = quoteIdent(table, 'table name')\n const qualifiedTable = `${quotedSchema}.${quotedTable}`\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 rawPolicyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`\n const policySQL = this.generatePolicy(qualifiedTable, rawPolicyName, 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 quotedName = quoteIdent(name, 'policy name')\n const parts: string[] = [`CREATE POLICY ${quotedName}`, `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 — validate to prevent injection\n const role = policy.role ?? 'public'\n parts.push(`TO ${quoteIdent(role, 'role name')}`)\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 const quotedSchema = quoteIdent(schemaName, 'schema name')\n\n // Validate prefix (used in LIKE pattern)\n if (!SAFE_IDENTIFIER.test(policyPrefix)) {\n throw new RLSSchemaError(\n `Unsafe policy prefix: \"${policyPrefix}\". Only alphanumeric and underscore characters are allowed.`,\n { identifier: policyPrefix, context: 'policy prefix' }\n )\n }\n\n for (const table of Object.keys(schema)) {\n const quotedTable = quoteIdent(table, 'table name')\n const qualifiedTable = `${quotedSchema}.${quotedTable}`\n\n // Drop all policies with prefix — use dollar-quoting for safety\n // pg_policies.tablename/schemaname are system-provided, safe for comparison\n // policyPrefix is validated above\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 = ${quoteLiteral(table, 'table name')}\n AND schemaname = ${quoteLiteral(schemaName, 'schema name')}\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"]}
@@ -186,8 +186,9 @@ interface RLSContext<TUser = unknown, TMeta = unknown> {
186
186
  /**
187
187
  * Context creation timestamp
188
188
  * Used for temporal policies and audit trails
189
+ * @default new Date() (auto-set when not provided)
189
190
  */
190
- timestamp: Date;
191
+ timestamp?: Date;
191
192
  }
192
193
  /**
193
194
  * Context passed to policy evaluation functions
@@ -676,6 +677,15 @@ interface PolicyActivationContext {
676
677
  * Custom metadata for activation decisions
677
678
  */
678
679
  meta?: Record<string, unknown>;
680
+ /**
681
+ * Authentication context for auth-based policy activation
682
+ */
683
+ auth?: {
684
+ userId?: string;
685
+ roles?: string[];
686
+ isSystem?: boolean;
687
+ [key: string]: unknown;
688
+ };
679
689
  }
680
690
  /**
681
691
  * Condition function for policy activation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kysera/rls",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
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,10 +20,10 @@
20
20
  "src"
21
21
  ],
22
22
  "peerDependencies": {
23
- "kysely": ">=0.28.9",
23
+ "kysely": ">=0.28.14",
24
24
  "zod": "^4.3.6",
25
- "@kysera/executor": "0.8.7",
26
- "@kysera/repository": "0.8.7"
25
+ "@kysera/executor": "0.8.8",
26
+ "@kysera/repository": "0.8.8"
27
27
  },
28
28
  "peerDependenciesMeta": {
29
29
  "@kysera/executor": {
@@ -37,23 +37,23 @@
37
37
  }
38
38
  },
39
39
  "dependencies": {
40
- "@kysera/core": "0.8.7"
40
+ "@kysera/core": "0.8.8"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/better-sqlite3": "^7.6.13",
44
- "@types/node": "^25.5.0",
45
- "@types/pg": "^8.18.0",
46
- "@vitest/coverage-v8": "^4.1.0",
44
+ "@types/node": "^25.5.2",
45
+ "@types/pg": "^8.20.0",
46
+ "@vitest/coverage-v8": "^4.1.3",
47
47
  "better-sqlite3": "^12.8.0",
48
- "kysely": "^0.28.12",
48
+ "kysely": "^0.28.15",
49
49
  "mysql2": "^3.20.0",
50
50
  "pg": "^8.20.0",
51
51
  "tsup": "^8.5.1",
52
- "typescript": "^5.9.3",
53
- "vitest": "^4.1.0",
52
+ "typescript": "^6.0.2",
53
+ "vitest": "^4.1.3",
54
54
  "zod": "^4.3.6",
55
- "@kysera/executor": "0.8.7",
56
- "@kysera/repository": "0.8.7"
55
+ "@kysera/executor": "0.8.8",
56
+ "@kysera/repository": "0.8.8"
57
57
  },
58
58
  "keywords": [
59
59
  "kysely",
@@ -89,7 +89,7 @@
89
89
  "bun": ">=1.0.0"
90
90
  },
91
91
  "scripts": {
92
- "build": "tsup",
92
+ "build": "tsup && node ../../scripts/fix-node-imports.cjs ./dist async_hooks",
93
93
  "dev": "tsup --watch",
94
94
  "test": "vitest run",
95
95
  "test:watch": "vitest watch",
@@ -102,7 +102,7 @@
102
102
  "docker:up": "docker compose -f test/docker/docker-compose.test.yml up -d",
103
103
  "docker:down": "docker compose -f test/docker/docker-compose.test.yml down -v",
104
104
  "docker:logs": "docker compose -f test/docker/docker-compose.test.yml logs -f",
105
- "typecheck": "tsc --noEmit",
105
+ "typecheck": "tsc --noEmit -p tsconfig.build.json",
106
106
  "lint": "eslint ."
107
107
  }
108
108
  }
@@ -1,6 +1,40 @@
1
1
  import type { Kysely } from 'kysely'
2
2
  import { sql } from 'kysely'
3
3
  import type { RLSSchema, TableRLSConfig, PolicyDefinition, Operation } from '../policy/types.js'
4
+ import { RLSSchemaError } from '../errors.js'
5
+
6
+ /**
7
+ * Pattern for valid SQL identifiers (prevents injection in DDL statements)
8
+ */
9
+ const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/
10
+
11
+ /**
12
+ * Validate and quote a SQL identifier to prevent injection.
13
+ * Only allows alphanumeric + underscore characters.
14
+ * @throws RLSSchemaError if identifier contains unsafe characters
15
+ */
16
+ function quoteIdent(name: string, context: string): string {
17
+ if (!SAFE_IDENTIFIER.test(name)) {
18
+ throw new RLSSchemaError(
19
+ `Unsafe ${context}: "${name}". Only alphanumeric and underscore characters are allowed.`,
20
+ { identifier: name, context }
21
+ )
22
+ }
23
+ return '"' + name.replace(/"/g, '""') + '"'
24
+ }
25
+
26
+ /**
27
+ * Quote a string literal for use in PL/pgSQL (single quotes, doubled for escaping)
28
+ */
29
+ function quoteLiteral(value: string, context: string): string {
30
+ if (!SAFE_IDENTIFIER.test(value)) {
31
+ throw new RLSSchemaError(
32
+ `Unsafe ${context}: "${value}". Only alphanumeric and underscore characters are allowed.`,
33
+ { identifier: value, context }
34
+ )
35
+ }
36
+ return "'" + value.replace(/'/g, "''") + "'"
37
+ }
4
38
 
5
39
  /**
6
40
  * Options for PostgreSQL RLS generation
@@ -26,11 +60,13 @@ export class PostgresRLSGenerator {
26
60
  const { force = true, schemaName = 'public', policyPrefix = 'rls' } = options
27
61
 
28
62
  const statements: string[] = []
63
+ const quotedSchema = quoteIdent(schemaName, 'schema name')
29
64
 
30
65
  for (const [table, config] of Object.entries(schema)) {
31
66
  if (!config) continue
32
67
 
33
- const qualifiedTable = `${schemaName}.${table}`
68
+ const quotedTable = quoteIdent(table, 'table name')
69
+ const qualifiedTable = `${quotedSchema}.${quotedTable}`
34
70
  const tableConfig = config as TableRLSConfig
35
71
 
36
72
  // Enable RLS on table
@@ -43,8 +79,8 @@ export class PostgresRLSGenerator {
43
79
  // Generate policies
44
80
  let policyIndex = 0
45
81
  for (const policy of tableConfig.policies) {
46
- const policyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`
47
- const policySQL = this.generatePolicy(qualifiedTable, policyName, policy)
82
+ const rawPolicyName = policy.name ?? `${policyPrefix}_${table}_${policy.type}_${policyIndex++}`
83
+ const policySQL = this.generatePolicy(qualifiedTable, rawPolicyName, policy)
48
84
  if (policySQL) {
49
85
  statements.push(policySQL)
50
86
  }
@@ -68,7 +104,8 @@ export class PostgresRLSGenerator {
68
104
  return null
69
105
  }
70
106
 
71
- const parts: string[] = [`CREATE POLICY "${name}"`, `ON ${table}`]
107
+ const quotedName = quoteIdent(name, 'policy name')
108
+ const parts: string[] = [`CREATE POLICY ${quotedName}`, `ON ${table}`]
72
109
 
73
110
  // Policy type
74
111
  if (policy.type === 'deny') {
@@ -77,8 +114,9 @@ export class PostgresRLSGenerator {
77
114
  parts.push('AS PERMISSIVE')
78
115
  }
79
116
 
80
- // Target role
81
- parts.push(`TO ${policy.role ?? 'public'}`)
117
+ // Target role — validate to prevent injection
118
+ const role = policy.role ?? 'public'
119
+ parts.push(`TO ${quoteIdent(role, 'role name')}`)
82
120
 
83
121
  // Operation
84
122
  parts.push(`FOR ${this.mapOperation(policy.operation)}`)
@@ -186,18 +224,30 @@ AS $$ SELECT COALESCE(current_setting('app.is_system', true), 'false')::boolean
186
224
  generateDropStatements<DB>(schema: RLSSchema<DB>, options: PostgresRLSOptions = {}): string[] {
187
225
  const { schemaName = 'public', policyPrefix = 'rls' } = options
188
226
  const statements: string[] = []
227
+ const quotedSchema = quoteIdent(schemaName, 'schema name')
228
+
229
+ // Validate prefix (used in LIKE pattern)
230
+ if (!SAFE_IDENTIFIER.test(policyPrefix)) {
231
+ throw new RLSSchemaError(
232
+ `Unsafe policy prefix: "${policyPrefix}". Only alphanumeric and underscore characters are allowed.`,
233
+ { identifier: policyPrefix, context: 'policy prefix' }
234
+ )
235
+ }
189
236
 
190
237
  for (const table of Object.keys(schema)) {
191
- const qualifiedTable = `${schemaName}.${table}`
238
+ const quotedTable = quoteIdent(table, 'table name')
239
+ const qualifiedTable = `${quotedSchema}.${quotedTable}`
192
240
 
193
- // Drop all policies with prefix
241
+ // Drop all policies with prefix — use dollar-quoting for safety
242
+ // pg_policies.tablename/schemaname are system-provided, safe for comparison
243
+ // policyPrefix is validated above
194
244
  statements.push(
195
245
  `DO $$ BEGIN
196
246
  EXECUTE (
197
247
  SELECT string_agg('DROP POLICY IF EXISTS ' || quote_ident(policyname) || ' ON ${qualifiedTable};', E'\\n')
198
248
  FROM pg_policies
199
- WHERE tablename = '${table}'
200
- AND schemaname = '${schemaName}'
249
+ WHERE tablename = ${quoteLiteral(table, 'table name')}
250
+ AND schemaname = ${quoteLiteral(schemaName, 'schema name')}
201
251
  AND policyname LIKE '${policyPrefix}_%'
202
252
  );
203
253
  END $$;`
package/src/plugin.ts CHANGED
@@ -21,7 +21,7 @@ import { MutationGuard } from './transformer/mutation.js'
21
21
  import { rlsContext } from './context/manager.js'
22
22
  import { VERSION } from './version.js'
23
23
  import { RLSContextError, RLSPolicyViolation, RLSError, RLSErrorCodes } from './errors.js'
24
- import { silentLogger, type KyseraLogger } from '@kysera/core'
24
+ import { silentLogger, shouldApplyToTable, type KyseraLogger } from '@kysera/core'
25
25
  import {
26
26
  transformQueryBuilder,
27
27
  selectFromDynamicTable,
@@ -39,8 +39,15 @@ export interface RLSPluginOptions<DB = unknown> {
39
39
  schema: RLSSchema<DB>
40
40
 
41
41
  /**
42
- * Tables to exclude from RLS (always bypass policies)
43
- * @default []
42
+ * Whitelist of tables to apply RLS to.
43
+ * If provided, only these tables will have RLS enforced.
44
+ * Takes precedence over excludeTables when both are provided.
45
+ */
46
+ tables?: string[]
47
+
48
+ /**
49
+ * Tables to exclude from RLS (always bypass policies).
50
+ * Ignored if `tables` whitelist is provided.
44
51
  */
45
52
  excludeTables?: string[]
46
53
 
@@ -103,6 +110,7 @@ export interface RLSPluginOptions<DB = unknown> {
103
110
  * Note: 'schema' and 'onViolation' are not included as they are complex runtime objects.
104
111
  */
105
112
  export const RLSPluginOptionsSchema = z.object({
113
+ tables: z.array(z.string()).optional(),
106
114
  excludeTables: z.array(z.string()).optional(),
107
115
  bypassRoles: z.array(z.string()).optional(),
108
116
  requireContext: z.boolean().optional(),
@@ -169,7 +177,8 @@ type BaseRepository = BaseRepositoryLike<Record<string, unknown>>
169
177
  export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
170
178
  const {
171
179
  schema,
172
- excludeTables = [],
180
+ tables,
181
+ excludeTables,
173
182
  bypassRoles = [],
174
183
  logger = silentLogger,
175
184
  requireContext = true, // SECURITY: Changed to true for secure-by-default (CRIT-2 fix)
@@ -188,8 +197,8 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
188
197
  name: '@kysera/rls',
189
198
  version: VERSION,
190
199
 
191
- // Run after soft-delete (priority 0), before audit
192
- priority: 50,
200
+ // SECURITY plugin: must run FIRST to enforce access policies before other plugins
201
+ priority: 1000,
193
202
 
194
203
  // No dependencies by default
195
204
  dependencies: [],
@@ -200,7 +209,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
200
209
  onInit<TDB>(_executor: Kysely<TDB>): void {
201
210
  logger.info?.('[RLS] Initializing RLS plugin', {
202
211
  tables: Object.keys(schema).length,
203
- excludeTables: excludeTables.length,
212
+ excludeTables: excludeTables?.length ?? 0,
204
213
  bypassRoles: bypassRoles.length
205
214
  })
206
215
 
@@ -220,11 +229,9 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
220
229
  /**
221
230
  * Cleanup resources when executor is destroyed
222
231
  */
223
- onDestroy(): Promise<void> {
224
- // Clear registry to free up memory
232
+ onDestroy() {
225
233
  registry.clear()
226
234
  logger.info?.('[RLS] RLS plugin destroyed, cleared policy registry')
227
- return Promise.resolve()
228
235
  },
229
236
 
230
237
  /**
@@ -238,7 +245,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
238
245
  const { operation, table, metadata } = context
239
246
 
240
247
  // Skip if table is excluded
241
- if (excludeTables.includes(table)) {
248
+ if (!shouldApplyToTable(table, { tables, excludeTables })) {
242
249
  logger.debug?.(`[RLS] Skipping RLS for excluded table: ${table}`)
243
250
  return qb
244
251
  }
@@ -358,7 +365,7 @@ export function rlsPlugin<DB>(options: RLSPluginOptions<DB>): Plugin {
358
365
  const table = baseRepo.tableName
359
366
 
360
367
  // Skip excluded tables
361
- if (excludeTables.includes(table)) {
368
+ if (!shouldApplyToTable(table, { tables, excludeTables })) {
362
369
  logger.debug?.(`[RLS] Skipping repository extension for excluded table: ${table}`)
363
370
  return repo
364
371
  }
@@ -219,8 +219,9 @@ export interface RLSContext<TUser = unknown, TMeta = unknown> {
219
219
  /**
220
220
  * Context creation timestamp
221
221
  * Used for temporal policies and audit trails
222
+ * @default new Date() (auto-set when not provided)
222
223
  */
223
- timestamp: Date
224
+ timestamp?: Date
224
225
  }
225
226
 
226
227
  // ============================================================================
@@ -800,6 +801,16 @@ export interface PolicyActivationContext {
800
801
  * Custom metadata for activation decisions
801
802
  */
802
803
  meta?: Record<string, unknown>
804
+
805
+ /**
806
+ * Authentication context for auth-based policy activation
807
+ */
808
+ auth?: {
809
+ userId?: string
810
+ roles?: string[]
811
+ isSystem?: boolean
812
+ [key: string]: unknown
813
+ }
803
814
  }
804
815
 
805
816
  /**
@@ -257,10 +257,12 @@ export class MutationGuard<DB = unknown> {
257
257
  return
258
258
  }
259
259
 
260
+ // Create evaluation context once and reuse for all policies
261
+ const evalCtx = this.createEvalContext(ctx, table, operation, row, data)
262
+
260
263
  // Evaluate deny policies first (they override allows)
261
264
  const denies = this.registry.getDenies(table, operation)
262
265
  for (const deny of denies) {
263
- const evalCtx = this.createEvalContext(ctx, table, operation, row, data)
264
266
  const result = await this.evaluatePolicy(deny.evaluate, evalCtx, deny.name)
265
267
 
266
268
  if (result) {
@@ -272,7 +274,6 @@ export class MutationGuard<DB = unknown> {
272
274
  if ((operation === 'create' || operation === 'update') && data) {
273
275
  const validates = this.registry.getValidates(table, operation)
274
276
  for (const validate of validates) {
275
- const evalCtx = this.createEvalContext(ctx, table, operation, row, data)
276
277
  const result = await this.evaluatePolicy(validate.evaluate, evalCtx, validate.name)
277
278
 
278
279
  if (!result) {
@@ -290,11 +291,9 @@ export class MutationGuard<DB = unknown> {
290
291
  }
291
292
 
292
293
  if (allows.length > 0) {
293
- // At least one allow policy must pass
294
294
  let allowed = false
295
295
 
296
296
  for (const allow of allows) {
297
- const evalCtx = this.createEvalContext(ctx, table, operation, row, data)
298
297
  const result = await this.evaluatePolicy(allow.evaluate, evalCtx, allow.name)
299
298
 
300
299
  if (result) {