@kysera/rls 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -312,6 +312,9 @@ function allow(operation, condition, options) {
312
312
  if (options?.hints !== void 0) {
313
313
  policy.hints = options.hints;
314
314
  }
315
+ if (options?.condition !== void 0) {
316
+ policy.activationCondition = options.condition;
317
+ }
315
318
  return policy;
316
319
  }
317
320
  function deny(operation, condition, options) {
@@ -328,6 +331,9 @@ function deny(operation, condition, options) {
328
331
  if (options?.hints !== void 0) {
329
332
  policy.hints = options.hints;
330
333
  }
334
+ if (options?.condition !== void 0) {
335
+ policy.activationCondition = options.condition;
336
+ }
331
337
  return policy;
332
338
  }
333
339
  function filter(operation, condition, options) {
@@ -343,6 +349,9 @@ function filter(operation, condition, options) {
343
349
  if (options?.hints !== void 0) {
344
350
  policy.hints = options.hints;
345
351
  }
352
+ if (options?.condition !== void 0) {
353
+ policy.activationCondition = options.condition;
354
+ }
346
355
  return policy;
347
356
  }
348
357
  function validate(operation, condition, options) {
@@ -359,6 +368,57 @@ function validate(operation, condition, options) {
359
368
  if (options?.hints !== void 0) {
360
369
  policy.hints = options.hints;
361
370
  }
371
+ if (options?.condition !== void 0) {
372
+ policy.activationCondition = options.condition;
373
+ }
374
+ return policy;
375
+ }
376
+ function whenEnvironment(environments, policyFn) {
377
+ const policy = policyFn();
378
+ const existingCondition = policy.activationCondition;
379
+ policy.activationCondition = (ctx) => {
380
+ const envMatch = environments.includes(ctx.environment ?? "");
381
+ if (!envMatch) return false;
382
+ return existingCondition ? existingCondition(ctx) : true;
383
+ };
384
+ return policy;
385
+ }
386
+ function whenFeature(feature, policyFn) {
387
+ const policy = policyFn();
388
+ const existingCondition = policy.activationCondition;
389
+ policy.activationCondition = (ctx) => {
390
+ let featureEnabled = false;
391
+ if (Array.isArray(ctx.features)) {
392
+ featureEnabled = ctx.features.includes(feature);
393
+ } else if (ctx.features && typeof ctx.features === "object" && "has" in ctx.features) {
394
+ featureEnabled = ctx.features.has(feature);
395
+ } else if (ctx.features && typeof ctx.features === "object") {
396
+ featureEnabled = !!ctx.features[feature];
397
+ }
398
+ if (!featureEnabled) return false;
399
+ return existingCondition ? existingCondition(ctx) : true;
400
+ };
401
+ return policy;
402
+ }
403
+ function whenTimeRange(startHour, endHour, policyFn) {
404
+ const policy = policyFn();
405
+ const existingCondition = policy.activationCondition;
406
+ policy.activationCondition = (ctx) => {
407
+ const hour = (ctx.timestamp ?? /* @__PURE__ */ new Date()).getHours();
408
+ let inRange;
409
+ if (startHour > endHour) {
410
+ inRange = hour >= startHour || hour < endHour;
411
+ } else {
412
+ inRange = hour >= startHour && hour < endHour;
413
+ }
414
+ if (!inRange) return false;
415
+ return existingCondition ? existingCondition(ctx) : true;
416
+ };
417
+ return policy;
418
+ }
419
+ function whenCondition(condition, policyFn) {
420
+ const policy = policyFn();
421
+ policy.activationCondition = condition;
362
422
  return policy;
363
423
  }
364
424
  var PolicyRegistry = class {
@@ -1640,6 +1700,2396 @@ function normalizeOperations(operation) {
1640
1700
  return [operation];
1641
1701
  }
1642
1702
 
1643
- export { PolicyRegistry, RLSContextError, RLSContextValidationError, RLSError, RLSErrorCodes, RLSPluginOptionsSchema, RLSPolicyEvaluationError, RLSPolicyViolation, RLSSchemaError, allow, createEvaluationContext, createRLSContext, deepMerge, defineRLSSchema, deny, filter, hashString, isAsyncFunction, mergeRLSSchemas, normalizeOperations, rlsContext, rlsPlugin, safeEvaluate, validate, withRLSContext, withRLSContextAsync };
1703
+ // src/resolvers/types.ts
1704
+ var InMemoryCacheProvider = class {
1705
+ cache = /* @__PURE__ */ new Map();
1706
+ get(key) {
1707
+ const entry = this.cache.get(key);
1708
+ if (!entry) return Promise.resolve(null);
1709
+ if (Date.now() > entry.expiresAt) {
1710
+ this.cache.delete(key);
1711
+ return Promise.resolve(null);
1712
+ }
1713
+ return Promise.resolve(entry.value);
1714
+ }
1715
+ set(key, value, ttlSeconds) {
1716
+ this.cache.set(key, {
1717
+ value,
1718
+ expiresAt: Date.now() + ttlSeconds * 1e3
1719
+ });
1720
+ return Promise.resolve();
1721
+ }
1722
+ delete(key) {
1723
+ this.cache.delete(key);
1724
+ return Promise.resolve();
1725
+ }
1726
+ deletePattern(pattern) {
1727
+ const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
1728
+ for (const key of this.cache.keys()) {
1729
+ if (key.startsWith(prefix)) {
1730
+ this.cache.delete(key);
1731
+ }
1732
+ }
1733
+ return Promise.resolve();
1734
+ }
1735
+ /**
1736
+ * Clear all cached entries
1737
+ */
1738
+ clear() {
1739
+ this.cache.clear();
1740
+ }
1741
+ /**
1742
+ * Get current cache size
1743
+ */
1744
+ get size() {
1745
+ return this.cache.size;
1746
+ }
1747
+ };
1748
+
1749
+ // src/resolvers/manager.ts
1750
+ var ResolverManager = class {
1751
+ resolvers = /* @__PURE__ */ new Map();
1752
+ cacheProvider;
1753
+ defaultCacheTtl;
1754
+ parallelResolution;
1755
+ resolverTimeout;
1756
+ logger;
1757
+ constructor(options = {}) {
1758
+ this.cacheProvider = options.cacheProvider ?? new InMemoryCacheProvider();
1759
+ this.defaultCacheTtl = options.defaultCacheTtl ?? 300;
1760
+ this.parallelResolution = options.parallelResolution ?? true;
1761
+ this.resolverTimeout = options.resolverTimeout ?? 5e3;
1762
+ this.logger = options.logger;
1763
+ }
1764
+ /**
1765
+ * Register a context resolver
1766
+ *
1767
+ * @param resolver - Resolver to register
1768
+ * @throws RLSError if resolver with same name already exists
1769
+ */
1770
+ register(resolver) {
1771
+ if (this.resolvers.has(resolver.name)) {
1772
+ throw new RLSError(
1773
+ `Resolver "${resolver.name}" is already registered`,
1774
+ RLSErrorCodes.RLS_SCHEMA_INVALID
1775
+ );
1776
+ }
1777
+ if (resolver.dependsOn) {
1778
+ for (const dep of resolver.dependsOn) {
1779
+ if (dep === resolver.name) {
1780
+ throw new RLSError(
1781
+ `Resolver "${resolver.name}" cannot depend on itself`,
1782
+ RLSErrorCodes.RLS_SCHEMA_INVALID
1783
+ );
1784
+ }
1785
+ }
1786
+ }
1787
+ this.resolvers.set(resolver.name, resolver);
1788
+ this.logger?.debug?.(`[ResolverManager] Registered resolver: ${resolver.name}`);
1789
+ }
1790
+ /**
1791
+ * Unregister a context resolver
1792
+ *
1793
+ * @param name - Name of resolver to unregister
1794
+ * @returns true if resolver was removed, false if it didn't exist
1795
+ */
1796
+ unregister(name) {
1797
+ const removed = this.resolvers.delete(name);
1798
+ if (removed) {
1799
+ this.logger?.debug?.(`[ResolverManager] Unregistered resolver: ${name}`);
1800
+ }
1801
+ return removed;
1802
+ }
1803
+ /**
1804
+ * Check if a resolver is registered
1805
+ *
1806
+ * @param name - Resolver name
1807
+ */
1808
+ hasResolver(name) {
1809
+ return this.resolvers.has(name);
1810
+ }
1811
+ /**
1812
+ * Get all registered resolver names
1813
+ */
1814
+ getResolverNames() {
1815
+ return Array.from(this.resolvers.keys());
1816
+ }
1817
+ /**
1818
+ * Resolve context data using all registered resolvers
1819
+ *
1820
+ * @param baseContext - Base context to resolve
1821
+ * @returns Enhanced context with resolved data
1822
+ *
1823
+ * @example
1824
+ * ```typescript
1825
+ * const baseCtx = {
1826
+ * auth: { userId: '123', roles: ['user'], tenantId: 'acme' },
1827
+ * timestamp: new Date()
1828
+ * };
1829
+ *
1830
+ * const enhancedCtx = await manager.resolve(baseCtx);
1831
+ * // enhancedCtx.auth.resolved contains all resolved data
1832
+ * ```
1833
+ */
1834
+ async resolve(baseContext) {
1835
+ const startTime = Date.now();
1836
+ const resolverOrder = this.getResolverOrder();
1837
+ this.logger?.debug?.(`[ResolverManager] Starting resolution for user ${baseContext.auth.userId}`, {
1838
+ resolverCount: resolverOrder.length,
1839
+ resolvers: resolverOrder.map((r) => r.name)
1840
+ });
1841
+ const results = /* @__PURE__ */ new Map();
1842
+ if (this.parallelResolution) {
1843
+ await this.resolveParallel(baseContext, resolverOrder, results);
1844
+ } else {
1845
+ await this.resolveSequential(baseContext, resolverOrder, results);
1846
+ }
1847
+ const mergedResolved = this.mergeResolvedData(results);
1848
+ const enhancedContext = {
1849
+ auth: {
1850
+ ...baseContext.auth,
1851
+ resolved: mergedResolved
1852
+ },
1853
+ timestamp: baseContext.timestamp
1854
+ };
1855
+ if (baseContext.meta !== void 0) {
1856
+ enhancedContext.meta = baseContext.meta;
1857
+ }
1858
+ const duration = Date.now() - startTime;
1859
+ this.logger?.info?.(`[ResolverManager] Resolution completed`, {
1860
+ userId: baseContext.auth.userId,
1861
+ durationMs: duration,
1862
+ resolverCount: results.size
1863
+ });
1864
+ return enhancedContext;
1865
+ }
1866
+ /**
1867
+ * Resolve a single resolver (useful for partial updates)
1868
+ *
1869
+ * @param name - Resolver name
1870
+ * @param baseContext - Base context
1871
+ * @returns Resolved data from the specific resolver
1872
+ */
1873
+ async resolveOne(name, baseContext) {
1874
+ const resolver = this.resolvers.get(name);
1875
+ if (!resolver) {
1876
+ this.logger?.warn?.(`[ResolverManager] Resolver not found: ${name}`);
1877
+ return null;
1878
+ }
1879
+ return await this.resolveWithCache(resolver, baseContext);
1880
+ }
1881
+ /**
1882
+ * Invalidate cached data for a user
1883
+ *
1884
+ * @param userId - User ID whose cache should be invalidated
1885
+ * @param resolverName - Optional specific resolver to invalidate
1886
+ */
1887
+ async invalidateCache(userId, resolverName) {
1888
+ if (resolverName) {
1889
+ const resolver = this.resolvers.get(resolverName);
1890
+ if (resolver?.cacheKey) {
1891
+ const key = resolver.cacheKey({ auth: { userId, roles: [] }, timestamp: /* @__PURE__ */ new Date() });
1892
+ if (key) {
1893
+ await this.cacheProvider.delete(key);
1894
+ this.logger?.debug?.(`[ResolverManager] Invalidated cache for ${resolverName}: ${key}`);
1895
+ }
1896
+ }
1897
+ } else {
1898
+ const pattern = `rls:*:${userId}:*`;
1899
+ if (this.cacheProvider.deletePattern) {
1900
+ await this.cacheProvider.deletePattern(pattern);
1901
+ }
1902
+ this.logger?.debug?.(`[ResolverManager] Invalidated all cache for user ${userId}`);
1903
+ }
1904
+ }
1905
+ /**
1906
+ * Clear all cached data
1907
+ */
1908
+ async clearCache() {
1909
+ if (this.cacheProvider instanceof InMemoryCacheProvider) {
1910
+ this.cacheProvider.clear();
1911
+ } else if (this.cacheProvider.deletePattern) {
1912
+ await this.cacheProvider.deletePattern("rls:*");
1913
+ }
1914
+ this.logger?.info?.("[ResolverManager] Cleared all cache");
1915
+ }
1916
+ // ============================================================================
1917
+ // Private Methods
1918
+ // ============================================================================
1919
+ /**
1920
+ * Get resolvers in dependency order (topological sort)
1921
+ */
1922
+ getResolverOrder() {
1923
+ const ordered = [];
1924
+ const visited = /* @__PURE__ */ new Set();
1925
+ const visiting = /* @__PURE__ */ new Set();
1926
+ const visit = (name) => {
1927
+ if (visited.has(name)) return;
1928
+ if (visiting.has(name)) {
1929
+ throw new RLSError(
1930
+ `Circular dependency detected in resolvers involving "${name}"`,
1931
+ RLSErrorCodes.RLS_SCHEMA_INVALID
1932
+ );
1933
+ }
1934
+ const resolver = this.resolvers.get(name);
1935
+ if (!resolver) return;
1936
+ visiting.add(name);
1937
+ if (resolver.dependsOn) {
1938
+ for (const dep of resolver.dependsOn) {
1939
+ visit(dep);
1940
+ }
1941
+ }
1942
+ visiting.delete(name);
1943
+ visited.add(name);
1944
+ ordered.push(resolver);
1945
+ };
1946
+ for (const name of this.resolvers.keys()) {
1947
+ visit(name);
1948
+ }
1949
+ return ordered.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
1950
+ }
1951
+ /**
1952
+ * Resolve resolvers sequentially
1953
+ */
1954
+ async resolveSequential(baseContext, resolvers, results) {
1955
+ for (const resolver of resolvers) {
1956
+ const data = await this.resolveWithCache(resolver, baseContext);
1957
+ if (data) {
1958
+ results.set(resolver.name, data);
1959
+ }
1960
+ }
1961
+ }
1962
+ /**
1963
+ * Resolve resolvers in parallel (respecting dependencies)
1964
+ */
1965
+ async resolveParallel(baseContext, resolvers, results) {
1966
+ const levels = [];
1967
+ const assigned = /* @__PURE__ */ new Set();
1968
+ const getLevel = (resolver) => {
1969
+ if (!resolver.dependsOn || resolver.dependsOn.length === 0) {
1970
+ return 0;
1971
+ }
1972
+ let maxDepLevel = 0;
1973
+ for (const dep of resolver.dependsOn) {
1974
+ const depResolver = this.resolvers.get(dep);
1975
+ if (depResolver) {
1976
+ maxDepLevel = Math.max(maxDepLevel, getLevel(depResolver) + 1);
1977
+ }
1978
+ }
1979
+ return maxDepLevel;
1980
+ };
1981
+ for (const resolver of resolvers) {
1982
+ const level = getLevel(resolver);
1983
+ while (levels.length <= level) {
1984
+ levels.push([]);
1985
+ }
1986
+ levels[level].push(resolver);
1987
+ assigned.add(resolver.name);
1988
+ }
1989
+ for (const level of levels) {
1990
+ await Promise.all(
1991
+ level.map(async (resolver) => {
1992
+ const data = await this.resolveWithCache(resolver, baseContext);
1993
+ if (data) {
1994
+ results.set(resolver.name, data);
1995
+ }
1996
+ })
1997
+ );
1998
+ }
1999
+ }
2000
+ /**
2001
+ * Resolve a single resolver with caching
2002
+ */
2003
+ async resolveWithCache(resolver, baseContext) {
2004
+ const startTime = Date.now();
2005
+ try {
2006
+ if (resolver.cacheKey) {
2007
+ const cacheKey = resolver.cacheKey(baseContext);
2008
+ if (cacheKey) {
2009
+ const cached = await this.cacheProvider.get(cacheKey);
2010
+ if (cached) {
2011
+ this.logger?.debug?.(`[ResolverManager] Cache hit for ${resolver.name}`, { cacheKey });
2012
+ return cached;
2013
+ }
2014
+ }
2015
+ }
2016
+ const data = await this.withTimeout(
2017
+ resolver.resolve(baseContext),
2018
+ this.resolverTimeout,
2019
+ `Resolver "${resolver.name}" timed out after ${this.resolverTimeout}ms`
2020
+ );
2021
+ if (resolver.cacheKey) {
2022
+ const cacheKey = resolver.cacheKey(baseContext);
2023
+ if (cacheKey) {
2024
+ const ttl = resolver.cacheTtl ?? this.defaultCacheTtl;
2025
+ await this.cacheProvider.set(cacheKey, data, ttl);
2026
+ this.logger?.debug?.(`[ResolverManager] Cached ${resolver.name}`, { cacheKey, ttl });
2027
+ }
2028
+ }
2029
+ const duration = Date.now() - startTime;
2030
+ this.logger?.debug?.(`[ResolverManager] Resolved ${resolver.name}`, { durationMs: duration });
2031
+ return data;
2032
+ } catch (error) {
2033
+ const duration = Date.now() - startTime;
2034
+ this.logger?.error?.(`[ResolverManager] Failed to resolve ${resolver.name}`, {
2035
+ error: error instanceof Error ? error.message : String(error),
2036
+ durationMs: duration
2037
+ });
2038
+ if (resolver.required !== false) {
2039
+ throw new RLSError(
2040
+ `Required resolver "${resolver.name}" failed: ${error instanceof Error ? error.message : String(error)}`,
2041
+ RLSErrorCodes.RLS_POLICY_EVALUATION_ERROR
2042
+ );
2043
+ }
2044
+ return null;
2045
+ }
2046
+ }
2047
+ /**
2048
+ * Execute a promise with timeout
2049
+ */
2050
+ async withTimeout(promise, timeoutMs, message) {
2051
+ return await Promise.race([
2052
+ promise,
2053
+ new Promise((_, reject) => setTimeout(() => {
2054
+ reject(new Error(message));
2055
+ }, timeoutMs))
2056
+ ]);
2057
+ }
2058
+ /**
2059
+ * Merge resolved data from multiple resolvers
2060
+ */
2061
+ mergeResolvedData(results) {
2062
+ const merged = {
2063
+ resolvedAt: /* @__PURE__ */ new Date()
2064
+ };
2065
+ for (const [name, data] of results) {
2066
+ merged[name] = data;
2067
+ for (const [key, value] of Object.entries(data)) {
2068
+ if (key !== "resolvedAt" && key !== "cacheKey") {
2069
+ if (merged[key] === void 0) {
2070
+ merged[key] = value;
2071
+ }
2072
+ }
2073
+ }
2074
+ }
2075
+ return merged;
2076
+ }
2077
+ };
2078
+ function createResolverManager(options) {
2079
+ return new ResolverManager(options);
2080
+ }
2081
+ function createResolver(config) {
2082
+ return {
2083
+ required: true,
2084
+ priority: 0,
2085
+ ...config
2086
+ };
2087
+ }
2088
+
2089
+ // src/rebac/types.ts
2090
+ function orgMembershipPath(resourceTable, organizationColumn = "organization_id") {
2091
+ return {
2092
+ name: `${resourceTable}_org_membership`,
2093
+ description: `Access ${resourceTable} through organization membership`,
2094
+ steps: [
2095
+ {
2096
+ from: resourceTable,
2097
+ to: "organizations",
2098
+ fromColumn: organizationColumn,
2099
+ toColumn: "id"
2100
+ },
2101
+ {
2102
+ from: "organizations",
2103
+ to: "employees",
2104
+ fromColumn: "id",
2105
+ toColumn: "organization_id"
2106
+ }
2107
+ ]
2108
+ };
2109
+ }
2110
+ function shopOrgMembershipPath(resourceTable, shopColumn = "shop_id") {
2111
+ return {
2112
+ name: `${resourceTable}_shop_org_membership`,
2113
+ description: `Access ${resourceTable} through shop's organization membership`,
2114
+ steps: [
2115
+ {
2116
+ from: resourceTable,
2117
+ to: "shops",
2118
+ fromColumn: shopColumn,
2119
+ toColumn: "id"
2120
+ },
2121
+ {
2122
+ from: "shops",
2123
+ to: "organizations",
2124
+ fromColumn: "organization_id",
2125
+ toColumn: "id"
2126
+ },
2127
+ {
2128
+ from: "organizations",
2129
+ to: "employees",
2130
+ fromColumn: "id",
2131
+ toColumn: "organization_id"
2132
+ }
2133
+ ]
2134
+ };
2135
+ }
2136
+ function teamHierarchyPath(resourceTable, teamColumn = "team_id") {
2137
+ return {
2138
+ name: `${resourceTable}_team_access`,
2139
+ description: `Access ${resourceTable} through team membership`,
2140
+ steps: [
2141
+ {
2142
+ from: resourceTable,
2143
+ to: "teams",
2144
+ fromColumn: teamColumn,
2145
+ toColumn: "id"
2146
+ },
2147
+ {
2148
+ from: "teams",
2149
+ to: "team_members",
2150
+ fromColumn: "id",
2151
+ toColumn: "team_id"
2152
+ }
2153
+ ]
2154
+ };
2155
+ }
2156
+ var ReBAcRegistry = class {
2157
+ tables = /* @__PURE__ */ new Map();
2158
+ globalRelationships = /* @__PURE__ */ new Map();
2159
+ logger;
2160
+ constructor(schema, options) {
2161
+ this.logger = options?.logger ?? silentLogger;
2162
+ if (schema) {
2163
+ this.loadSchema(schema);
2164
+ }
2165
+ }
2166
+ /**
2167
+ * Load ReBAC schema
2168
+ */
2169
+ loadSchema(schema) {
2170
+ for (const [table, config] of Object.entries(schema)) {
2171
+ if (!config) continue;
2172
+ this.registerTable(table, config);
2173
+ }
2174
+ }
2175
+ /**
2176
+ * Register ReBAC configuration for a single table
2177
+ */
2178
+ registerTable(table, config) {
2179
+ const compiled = {
2180
+ relationships: /* @__PURE__ */ new Map(),
2181
+ policies: []
2182
+ };
2183
+ for (const rel of config.relationships) {
2184
+ const compiledPath = this.compileRelationshipPath(rel, table);
2185
+ compiled.relationships.set(rel.name, compiledPath);
2186
+ this.globalRelationships.set(rel.name, compiledPath);
2187
+ }
2188
+ for (let i = 0; i < config.policies.length; i++) {
2189
+ const policy = config.policies[i];
2190
+ if (!policy) continue;
2191
+ const policyName = policy.name ?? `${table}_rebac_policy_${i}`;
2192
+ const compiledPolicy = this.compilePolicy(policy, policyName, table, compiled.relationships);
2193
+ compiled.policies.push(compiledPolicy);
2194
+ }
2195
+ compiled.policies.sort((a, b) => b.priority - a.priority);
2196
+ this.tables.set(table, compiled);
2197
+ this.logger.info?.(`[ReBAC] Registered table: ${table}`, {
2198
+ relationships: config.relationships.length,
2199
+ policies: config.policies.length
2200
+ });
2201
+ }
2202
+ /**
2203
+ * Register a global relationship path (available to all tables)
2204
+ */
2205
+ registerRelationship(path) {
2206
+ if (!path.steps.length) {
2207
+ throw new RLSSchemaError(`Relationship path "${path.name}" has no steps`, {
2208
+ path: path.name
2209
+ });
2210
+ }
2211
+ const compiled = this.compileRelationshipPath(path, path.steps[0].from);
2212
+ this.globalRelationships.set(path.name, compiled);
2213
+ }
2214
+ /**
2215
+ * Get ReBAC policies for a table and operation
2216
+ */
2217
+ getPolicies(table, operation) {
2218
+ const config = this.tables.get(table);
2219
+ if (!config) return [];
2220
+ return config.policies.filter((p) => p.operations.has(operation) || p.operations.has("all"));
2221
+ }
2222
+ /**
2223
+ * Get a specific relationship path
2224
+ */
2225
+ getRelationship(name, table) {
2226
+ if (table) {
2227
+ const tableConfig = this.tables.get(table);
2228
+ const tablePath = tableConfig?.relationships.get(name);
2229
+ if (tablePath) return tablePath;
2230
+ }
2231
+ return this.globalRelationships.get(name);
2232
+ }
2233
+ /**
2234
+ * Check if table has ReBAC configuration
2235
+ */
2236
+ hasTable(table) {
2237
+ return this.tables.has(table);
2238
+ }
2239
+ /**
2240
+ * Get all registered table names
2241
+ */
2242
+ getTables() {
2243
+ return Array.from(this.tables.keys());
2244
+ }
2245
+ /**
2246
+ * Clear all registrations
2247
+ */
2248
+ clear() {
2249
+ this.tables.clear();
2250
+ this.globalRelationships.clear();
2251
+ }
2252
+ // ============================================================================
2253
+ // Private Methods
2254
+ // ============================================================================
2255
+ /**
2256
+ * Compile a relationship path definition
2257
+ */
2258
+ compileRelationshipPath(path, sourceTable) {
2259
+ if (path.steps.length === 0) {
2260
+ throw new RLSSchemaError(`Relationship path "${path.name}" must have at least one step`, {
2261
+ path: path.name
2262
+ });
2263
+ }
2264
+ const compiledSteps = path.steps.map((step, index) => {
2265
+ if (!step.from || !step.to) {
2266
+ throw new RLSSchemaError(
2267
+ `Relationship step ${index} in "${path.name}" must have 'from' and 'to' tables`,
2268
+ { path: path.name, step: index }
2269
+ );
2270
+ }
2271
+ return {
2272
+ from: step.from,
2273
+ to: step.to,
2274
+ fromColumn: step.fromColumn ?? `${step.to}_id`,
2275
+ toColumn: step.toColumn ?? "id",
2276
+ alias: step.alias ?? step.to,
2277
+ joinType: step.joinType ?? "inner",
2278
+ additionalConditions: step.additionalConditions ?? {}
2279
+ };
2280
+ });
2281
+ for (let i = 1; i < compiledSteps.length; i++) {
2282
+ const prevStep = compiledSteps[i - 1];
2283
+ const currentStep = compiledSteps[i];
2284
+ if (currentStep.from !== prevStep.to && currentStep.from !== prevStep.alias) {
2285
+ throw new RLSSchemaError(
2286
+ `Relationship path "${path.name}" has broken chain at step ${i}: expected '${prevStep.to}' but got '${currentStep.from}'`,
2287
+ { path: path.name, step: i }
2288
+ );
2289
+ }
2290
+ }
2291
+ const lastStep = compiledSteps[compiledSteps.length - 1];
2292
+ return {
2293
+ name: path.name,
2294
+ steps: compiledSteps,
2295
+ sourceTable,
2296
+ targetTable: lastStep.to
2297
+ };
2298
+ }
2299
+ /**
2300
+ * Compile a ReBAC policy definition
2301
+ */
2302
+ compilePolicy(policy, name, table, tableRelationships) {
2303
+ const relationshipPath = tableRelationships.get(policy.relationshipPath) ?? this.globalRelationships.get(policy.relationshipPath);
2304
+ if (!relationshipPath) {
2305
+ throw new RLSSchemaError(
2306
+ `ReBAC policy "${name}" references unknown relationship path "${policy.relationshipPath}"`,
2307
+ { policy: name, table, relationshipPath: policy.relationshipPath }
2308
+ );
2309
+ }
2310
+ const ops = Array.isArray(policy.operation) ? policy.operation : [policy.operation];
2311
+ const expandedOps = ops.flatMap(
2312
+ (op) => op === "all" ? ["read", "create", "update", "delete"] : [op]
2313
+ );
2314
+ const getEndConditions = typeof policy.endCondition === "function" ? policy.endCondition : () => policy.endCondition;
2315
+ return {
2316
+ name,
2317
+ type: policy.policyType ?? "allow",
2318
+ operations: new Set(expandedOps),
2319
+ relationshipPath,
2320
+ getEndConditions,
2321
+ priority: policy.priority ?? 0
2322
+ };
2323
+ }
2324
+ };
2325
+ function createReBAcRegistry(schema, options) {
2326
+ return new ReBAcRegistry(schema, options);
2327
+ }
2328
+ var ReBAcTransformer = class {
2329
+ constructor(registry, options = {}) {
2330
+ this.registry = registry;
2331
+ this.options = options;
2332
+ this.options = {
2333
+ qualifyColumns: true,
2334
+ dialect: "postgres",
2335
+ ...options
2336
+ };
2337
+ }
2338
+ /**
2339
+ * Transform a SELECT query by applying ReBAC policies
2340
+ *
2341
+ * @param qb - Query builder to transform
2342
+ * @param table - Table being queried
2343
+ * @param operation - Operation being performed
2344
+ * @returns Transformed query builder
2345
+ */
2346
+ transform(qb, table, operation = "read") {
2347
+ const ctx = rlsContext.getContextOrNull();
2348
+ if (!ctx) {
2349
+ return qb;
2350
+ }
2351
+ if (ctx.auth.isSystem) {
2352
+ return qb;
2353
+ }
2354
+ const policies = this.registry.getPolicies(table, operation);
2355
+ if (policies.length === 0) {
2356
+ return qb;
2357
+ }
2358
+ let result = qb;
2359
+ for (const policy of policies) {
2360
+ result = this.applyPolicy(result, policy, ctx, table);
2361
+ }
2362
+ return result;
2363
+ }
2364
+ /**
2365
+ * Generate EXISTS condition SQL for a policy
2366
+ *
2367
+ * This method can be used to get the raw SQL for debugging or manual query building.
2368
+ *
2369
+ * @param policy - ReBAC policy to generate SQL for
2370
+ * @param ctx - RLS context
2371
+ * @param mainTable - Main query table
2372
+ * @param mainTableAlias - Alias for main table
2373
+ * @returns SQL string and parameters
2374
+ */
2375
+ generateExistsSql(policy, ctx, mainTable, mainTableAlias) {
2376
+ const { relationshipPath } = policy;
2377
+ const evalCtx = this.createEvalContext(ctx, mainTable);
2378
+ const endConditions = policy.getEndConditions(evalCtx);
2379
+ const alias = mainTableAlias ?? mainTable;
2380
+ const params = [];
2381
+ let paramIndex = 1;
2382
+ const steps = relationshipPath.steps;
2383
+ if (steps.length === 0) {
2384
+ return { sql: "TRUE", params: [] };
2385
+ }
2386
+ const firstStep = steps[0];
2387
+ let sql3 = `SELECT 1 FROM ${this.quote(firstStep.to)}`;
2388
+ if (firstStep.alias !== firstStep.to) {
2389
+ sql3 += ` AS ${this.quote(firstStep.alias)}`;
2390
+ }
2391
+ for (let i = 1; i < steps.length; i++) {
2392
+ const step = steps[i];
2393
+ const joinType = step.joinType === "left" ? "LEFT JOIN" : step.joinType === "right" ? "RIGHT JOIN" : "JOIN";
2394
+ sql3 += ` ${joinType} ${this.quote(step.to)}`;
2395
+ if (step.alias !== step.to) {
2396
+ sql3 += ` AS ${this.quote(step.alias)}`;
2397
+ }
2398
+ const prevStep = steps[i - 1];
2399
+ const prevAlias = prevStep.alias;
2400
+ sql3 += ` ON ${this.quote(prevAlias)}.${this.quote(step.fromColumn)} = ${this.quote(step.alias)}.${this.quote(step.toColumn)}`;
2401
+ if (Object.keys(step.additionalConditions).length > 0) {
2402
+ for (const [col, val] of Object.entries(step.additionalConditions)) {
2403
+ if (val === null) {
2404
+ sql3 += ` AND ${this.quote(step.alias)}.${this.quote(col)} IS NULL`;
2405
+ } else {
2406
+ sql3 += ` AND ${this.quote(step.alias)}.${this.quote(col)} = ${this.param(paramIndex++)}`;
2407
+ params.push(val);
2408
+ }
2409
+ }
2410
+ }
2411
+ }
2412
+ sql3 += ` WHERE ${this.quote(firstStep.alias)}.${this.quote(firstStep.toColumn)} = ${this.quote(alias)}.${this.quote(firstStep.fromColumn)}`;
2413
+ if (Object.keys(firstStep.additionalConditions).length > 0) {
2414
+ for (const [col, val] of Object.entries(firstStep.additionalConditions)) {
2415
+ if (val === null) {
2416
+ sql3 += ` AND ${this.quote(firstStep.alias)}.${this.quote(col)} IS NULL`;
2417
+ } else {
2418
+ sql3 += ` AND ${this.quote(firstStep.alias)}.${this.quote(col)} = ${this.param(paramIndex++)}`;
2419
+ params.push(val);
2420
+ }
2421
+ }
2422
+ }
2423
+ const lastStep = steps[steps.length - 1];
2424
+ for (const [col, val] of Object.entries(endConditions)) {
2425
+ if (val === null) {
2426
+ sql3 += ` AND ${this.quote(lastStep.alias)}.${this.quote(col)} IS NULL`;
2427
+ } else if (val === void 0) {
2428
+ continue;
2429
+ } else if (Array.isArray(val)) {
2430
+ if (val.length === 0) {
2431
+ sql3 += ` AND FALSE`;
2432
+ } else {
2433
+ const placeholders = val.map(() => this.param(paramIndex++)).join(", ");
2434
+ sql3 += ` AND ${this.quote(lastStep.alias)}.${this.quote(col)} IN (${placeholders})`;
2435
+ params.push(...val);
2436
+ }
2437
+ } else {
2438
+ sql3 += ` AND ${this.quote(lastStep.alias)}.${this.quote(col)} = ${this.param(paramIndex++)}`;
2439
+ params.push(val);
2440
+ }
2441
+ }
2442
+ const existsExpr = policy.type === "deny" ? `NOT EXISTS (${sql3})` : `EXISTS (${sql3})`;
2443
+ return { sql: existsExpr, params };
2444
+ }
2445
+ // ============================================================================
2446
+ // Private Methods
2447
+ // ============================================================================
2448
+ /**
2449
+ * Apply a single ReBAC policy to a query
2450
+ *
2451
+ * NOTE: Uses type casting for dynamic SQL because Kysely's type system
2452
+ * requires compile-time known types, but ReBAC policies work with
2453
+ * runtime-generated EXISTS clauses.
2454
+ */
2455
+ applyPolicy(qb, policy, ctx, table) {
2456
+ const { sql: existsSql, params } = this.generateExistsSql(policy, ctx, table, this.options.mainTableAlias);
2457
+ const sqlParts = existsSql.split(/\$\d+/);
2458
+ const rawBuilder = sql.join(
2459
+ sqlParts.map((part, i) => {
2460
+ if (i < params.length) {
2461
+ return sql`${sql.raw(part)}${params[i]}`;
2462
+ }
2463
+ return sql.raw(part);
2464
+ })
2465
+ );
2466
+ return qb.where(rawBuilder);
2467
+ }
2468
+ /**
2469
+ * Create evaluation context for policy conditions
2470
+ */
2471
+ createEvalContext(ctx, table) {
2472
+ return {
2473
+ auth: ctx.auth,
2474
+ table,
2475
+ operation: "read",
2476
+ ...ctx.meta !== void 0 && { meta: ctx.meta }
2477
+ };
2478
+ }
2479
+ /**
2480
+ * Quote an identifier for the target dialect
2481
+ */
2482
+ quote(identifier) {
2483
+ switch (this.options.dialect) {
2484
+ case "mysql":
2485
+ return `\`${identifier}\``;
2486
+ case "sqlite":
2487
+ return `"${identifier}"`;
2488
+ case "postgres":
2489
+ default:
2490
+ return `"${identifier}"`;
2491
+ }
2492
+ }
2493
+ /**
2494
+ * Generate parameter placeholder for the target dialect
2495
+ */
2496
+ param(index) {
2497
+ switch (this.options.dialect) {
2498
+ case "mysql":
2499
+ case "sqlite":
2500
+ return "?";
2501
+ case "postgres":
2502
+ default:
2503
+ return `$${index}`;
2504
+ }
2505
+ }
2506
+ };
2507
+ function allowRelation(operation, relationshipPath, endCondition, options) {
2508
+ const policy = {
2509
+ type: "filter",
2510
+ operation,
2511
+ relationshipPath,
2512
+ endCondition,
2513
+ policyType: "allow"
2514
+ };
2515
+ if (options?.name !== void 0) {
2516
+ policy.name = options.name;
2517
+ }
2518
+ if (options?.priority !== void 0) {
2519
+ policy.priority = options.priority;
2520
+ }
2521
+ return policy;
2522
+ }
2523
+ function denyRelation(operation, relationshipPath, endCondition, options) {
2524
+ const policy = {
2525
+ type: "filter",
2526
+ operation,
2527
+ relationshipPath,
2528
+ endCondition,
2529
+ policyType: "deny",
2530
+ priority: options?.priority ?? 100
2531
+ // Higher priority for deny
2532
+ };
2533
+ if (options?.name !== void 0) {
2534
+ policy.name = options.name;
2535
+ }
2536
+ return policy;
2537
+ }
2538
+ function createReBAcTransformer(registry, options) {
2539
+ return new ReBAcTransformer(registry, options);
2540
+ }
2541
+
2542
+ // src/field-access/types.ts
2543
+ function neverAccessible() {
2544
+ return {
2545
+ read: () => false,
2546
+ write: () => false,
2547
+ omitWhenHidden: true
2548
+ };
2549
+ }
2550
+ function ownerOnly(ownerField = "id") {
2551
+ return {
2552
+ read: (ctx) => {
2553
+ const rowValue = ctx.row?.[ownerField];
2554
+ return String(ctx.auth.userId) === String(rowValue);
2555
+ },
2556
+ write: (ctx) => {
2557
+ const rowValue = ctx.row?.[ownerField];
2558
+ return String(ctx.auth.userId) === String(rowValue);
2559
+ }
2560
+ };
2561
+ }
2562
+ function ownerOrRoles(roles, ownerField = "id") {
2563
+ return {
2564
+ read: (ctx) => {
2565
+ const rowValue = ctx.row?.[ownerField];
2566
+ return String(ctx.auth.userId) === String(rowValue) || roles.some((r) => ctx.auth.roles.includes(r));
2567
+ },
2568
+ write: (ctx) => {
2569
+ const rowValue = ctx.row?.[ownerField];
2570
+ return String(ctx.auth.userId) === String(rowValue) || roles.some((r) => ctx.auth.roles.includes(r));
2571
+ }
2572
+ };
2573
+ }
2574
+ function rolesOnly(roles) {
2575
+ return {
2576
+ read: (ctx) => roles.some((r) => ctx.auth.roles.includes(r)),
2577
+ write: (ctx) => roles.some((r) => ctx.auth.roles.includes(r))
2578
+ };
2579
+ }
2580
+ function readOnly(readCondition) {
2581
+ return {
2582
+ read: readCondition ?? (() => true),
2583
+ write: () => false
2584
+ };
2585
+ }
2586
+ function publicReadRestrictedWrite(writeCondition) {
2587
+ return {
2588
+ read: () => true,
2589
+ write: writeCondition
2590
+ };
2591
+ }
2592
+ function maskedField(maskFn, readCondition) {
2593
+ return {
2594
+ read: readCondition,
2595
+ write: readCondition,
2596
+ maskedValue: void 0,
2597
+ // Will be computed by maskFn
2598
+ maskFn
2599
+ };
2600
+ }
2601
+ var FieldAccessRegistry = class {
2602
+ tables = /* @__PURE__ */ new Map();
2603
+ logger;
2604
+ constructor(schema, options) {
2605
+ this.logger = options?.logger ?? silentLogger;
2606
+ if (schema) {
2607
+ this.loadSchema(schema);
2608
+ }
2609
+ }
2610
+ /**
2611
+ * Load field access schema
2612
+ */
2613
+ loadSchema(schema) {
2614
+ for (const [table, config] of Object.entries(schema)) {
2615
+ if (!config) continue;
2616
+ this.registerTable(table, config);
2617
+ }
2618
+ }
2619
+ /**
2620
+ * Register field access configuration for a table
2621
+ */
2622
+ registerTable(table, config) {
2623
+ const compiled = {
2624
+ table,
2625
+ defaultAccess: config.default ?? "allow",
2626
+ skipFor: config.skipFor ?? [],
2627
+ fields: /* @__PURE__ */ new Map()
2628
+ };
2629
+ for (const [field, fieldConfig] of Object.entries(config.fields)) {
2630
+ if (!fieldConfig) continue;
2631
+ const compiledField = this.compileFieldConfig(field, fieldConfig);
2632
+ compiled.fields.set(field, compiledField);
2633
+ }
2634
+ this.tables.set(table, compiled);
2635
+ this.logger.info?.(`[FieldAccess] Registered table: ${table}`, {
2636
+ fields: compiled.fields.size,
2637
+ defaultAccess: compiled.defaultAccess
2638
+ });
2639
+ }
2640
+ /**
2641
+ * Check if a field is readable in the current context
2642
+ *
2643
+ * @param table - Table name
2644
+ * @param field - Field name
2645
+ * @param ctx - Evaluation context
2646
+ * @returns True if field is readable
2647
+ */
2648
+ async canReadField(table, field, ctx) {
2649
+ const config = this.tables.get(table);
2650
+ if (!config) {
2651
+ return true;
2652
+ }
2653
+ if (config.skipFor.some((role) => ctx.auth.roles.includes(role))) {
2654
+ return true;
2655
+ }
2656
+ if (ctx.auth.isSystem) {
2657
+ return true;
2658
+ }
2659
+ const fieldConfig = config.fields.get(field);
2660
+ if (!fieldConfig) {
2661
+ return config.defaultAccess === "allow";
2662
+ }
2663
+ try {
2664
+ const result = fieldConfig.canRead(ctx);
2665
+ return result instanceof Promise ? await result : result;
2666
+ } catch (error) {
2667
+ this.logger.error?.(`[FieldAccess] Error checking read access for ${table}.${field}`, {
2668
+ error: error instanceof Error ? error.message : String(error)
2669
+ });
2670
+ return false;
2671
+ }
2672
+ }
2673
+ /**
2674
+ * Check if a field is writable in the current context
2675
+ *
2676
+ * @param table - Table name
2677
+ * @param field - Field name
2678
+ * @param ctx - Evaluation context
2679
+ * @returns True if field is writable
2680
+ */
2681
+ async canWriteField(table, field, ctx) {
2682
+ const config = this.tables.get(table);
2683
+ if (!config) {
2684
+ return true;
2685
+ }
2686
+ if (config.skipFor.some((role) => ctx.auth.roles.includes(role))) {
2687
+ return true;
2688
+ }
2689
+ if (ctx.auth.isSystem) {
2690
+ return true;
2691
+ }
2692
+ const fieldConfig = config.fields.get(field);
2693
+ if (!fieldConfig) {
2694
+ return config.defaultAccess === "allow";
2695
+ }
2696
+ try {
2697
+ const result = fieldConfig.canWrite(ctx);
2698
+ return result instanceof Promise ? await result : result;
2699
+ } catch (error) {
2700
+ this.logger.error?.(`[FieldAccess] Error checking write access for ${table}.${field}`, {
2701
+ error: error instanceof Error ? error.message : String(error)
2702
+ });
2703
+ return false;
2704
+ }
2705
+ }
2706
+ /**
2707
+ * Get field configuration
2708
+ *
2709
+ * @param table - Table name
2710
+ * @param field - Field name
2711
+ * @returns Compiled field access config or undefined
2712
+ */
2713
+ getFieldConfig(table, field) {
2714
+ return this.tables.get(table)?.fields.get(field);
2715
+ }
2716
+ /**
2717
+ * Get table configuration
2718
+ *
2719
+ * @param table - Table name
2720
+ * @returns Compiled table field access config or undefined
2721
+ */
2722
+ getTableConfig(table) {
2723
+ return this.tables.get(table);
2724
+ }
2725
+ /**
2726
+ * Check if table has field access configuration
2727
+ */
2728
+ hasTable(table) {
2729
+ return this.tables.has(table);
2730
+ }
2731
+ /**
2732
+ * Get all registered table names
2733
+ */
2734
+ getTables() {
2735
+ return Array.from(this.tables.keys());
2736
+ }
2737
+ /**
2738
+ * Get all fields with explicit configuration for a table
2739
+ *
2740
+ * @param table - Table name
2741
+ * @returns Array of field names
2742
+ */
2743
+ getConfiguredFields(table) {
2744
+ const config = this.tables.get(table);
2745
+ return config ? Array.from(config.fields.keys()) : [];
2746
+ }
2747
+ /**
2748
+ * Clear all configurations
2749
+ */
2750
+ clear() {
2751
+ this.tables.clear();
2752
+ }
2753
+ // ============================================================================
2754
+ // Private Methods
2755
+ // ============================================================================
2756
+ /**
2757
+ * Compile a field access configuration
2758
+ */
2759
+ compileFieldConfig(field, config) {
2760
+ return {
2761
+ field,
2762
+ canRead: config.read ?? (() => true),
2763
+ canWrite: config.write ?? (() => true),
2764
+ maskedValue: config.maskedValue ?? null,
2765
+ omitWhenHidden: config.omitWhenHidden ?? false
2766
+ };
2767
+ }
2768
+ };
2769
+ function createFieldAccessRegistry(schema, options) {
2770
+ return new FieldAccessRegistry(schema, options);
2771
+ }
2772
+
2773
+ // src/field-access/processor.ts
2774
+ var FieldAccessProcessor = class {
2775
+ constructor(registry, defaultMaskValue = null) {
2776
+ this.registry = registry;
2777
+ this.defaultMaskValue = defaultMaskValue;
2778
+ }
2779
+ /**
2780
+ * Apply field access control to a single row
2781
+ *
2782
+ * @param table - Table name
2783
+ * @param row - Row data
2784
+ * @param options - Processing options
2785
+ * @returns Masked row with metadata
2786
+ */
2787
+ async maskRow(table, row, options = {}) {
2788
+ const ctx = this.getContext();
2789
+ if (!ctx) {
2790
+ return {
2791
+ data: row,
2792
+ maskedFields: [],
2793
+ omittedFields: []
2794
+ };
2795
+ }
2796
+ if (ctx.auth.isSystem) {
2797
+ return {
2798
+ data: row,
2799
+ maskedFields: [],
2800
+ omittedFields: []
2801
+ };
2802
+ }
2803
+ const tableConfig = this.registry.getTableConfig(table);
2804
+ if (!tableConfig) {
2805
+ return {
2806
+ data: row,
2807
+ maskedFields: [],
2808
+ omittedFields: []
2809
+ };
2810
+ }
2811
+ if (tableConfig.skipFor.some((role) => ctx.auth.roles.includes(role))) {
2812
+ return {
2813
+ data: row,
2814
+ maskedFields: [],
2815
+ omittedFields: []
2816
+ };
2817
+ }
2818
+ const evalCtx = this.createEvalContext(ctx, row, table);
2819
+ const result = {};
2820
+ const maskedFields = [];
2821
+ const omittedFields = [];
2822
+ for (const [field, value] of Object.entries(row)) {
2823
+ if (options.excludeFields?.includes(field)) {
2824
+ continue;
2825
+ }
2826
+ if (options.includeFields && !options.includeFields.includes(field)) {
2827
+ continue;
2828
+ }
2829
+ const fieldResult = await this.evaluateFieldAccess(
2830
+ tableConfig,
2831
+ field,
2832
+ value,
2833
+ evalCtx,
2834
+ options
2835
+ );
2836
+ if (fieldResult.omit) {
2837
+ omittedFields.push(field);
2838
+ } else if (!fieldResult.accessible) {
2839
+ maskedFields.push(field);
2840
+ result[field] = fieldResult.value;
2841
+ } else {
2842
+ result[field] = value;
2843
+ }
2844
+ }
2845
+ return {
2846
+ data: result,
2847
+ maskedFields,
2848
+ omittedFields
2849
+ };
2850
+ }
2851
+ /**
2852
+ * Apply field access control to multiple rows
2853
+ *
2854
+ * @param table - Table name
2855
+ * @param rows - Array of rows
2856
+ * @param options - Processing options
2857
+ * @returns Array of masked rows
2858
+ */
2859
+ async maskRows(table, rows, options = {}) {
2860
+ return await Promise.all(rows.map((row) => this.maskRow(table, row, options)));
2861
+ }
2862
+ /**
2863
+ * Validate that all fields in mutation data are writable
2864
+ *
2865
+ * @param table - Table name
2866
+ * @param data - Mutation data
2867
+ * @param existingRow - Existing row (for update operations)
2868
+ * @throws RLSPolicyViolation if any field is not writable
2869
+ */
2870
+ async validateWrite(table, data, existingRow) {
2871
+ const ctx = this.getContext();
2872
+ if (!ctx) {
2873
+ return;
2874
+ }
2875
+ if (ctx.auth.isSystem) {
2876
+ return;
2877
+ }
2878
+ const tableConfig = this.registry.getTableConfig(table);
2879
+ if (!tableConfig) {
2880
+ return;
2881
+ }
2882
+ if (tableConfig.skipFor.some((role) => ctx.auth.roles.includes(role))) {
2883
+ return;
2884
+ }
2885
+ const evalCtx = this.createEvalContext(ctx, existingRow ?? {}, table, data);
2886
+ const unwritableFields = [];
2887
+ for (const field of Object.keys(data)) {
2888
+ const canWrite = await this.registry.canWriteField(table, field, evalCtx);
2889
+ if (!canWrite) {
2890
+ unwritableFields.push(field);
2891
+ }
2892
+ }
2893
+ if (unwritableFields.length > 0) {
2894
+ throw new RLSPolicyViolation(
2895
+ "write",
2896
+ table,
2897
+ `Cannot write to protected fields: ${unwritableFields.join(", ")}`
2898
+ );
2899
+ }
2900
+ }
2901
+ /**
2902
+ * Filter mutation data to only include writable fields
2903
+ *
2904
+ * @param table - Table name
2905
+ * @param data - Mutation data
2906
+ * @param existingRow - Existing row (for update operations)
2907
+ * @returns Filtered data with only writable fields
2908
+ */
2909
+ async filterWritableFields(table, data, existingRow) {
2910
+ const ctx = this.getContext();
2911
+ if (!ctx) {
2912
+ return { data, removedFields: [] };
2913
+ }
2914
+ if (ctx.auth.isSystem) {
2915
+ return { data, removedFields: [] };
2916
+ }
2917
+ const tableConfig = this.registry.getTableConfig(table);
2918
+ if (!tableConfig) {
2919
+ return { data, removedFields: [] };
2920
+ }
2921
+ if (tableConfig.skipFor.some((role) => ctx.auth.roles.includes(role))) {
2922
+ return { data, removedFields: [] };
2923
+ }
2924
+ const evalCtx = this.createEvalContext(ctx, existingRow ?? {}, table, data);
2925
+ const result = {};
2926
+ const removedFields = [];
2927
+ for (const [field, value] of Object.entries(data)) {
2928
+ const canWrite = await this.registry.canWriteField(table, field, evalCtx);
2929
+ if (canWrite) {
2930
+ result[field] = value;
2931
+ } else {
2932
+ removedFields.push(field);
2933
+ }
2934
+ }
2935
+ return { data: result, removedFields };
2936
+ }
2937
+ /**
2938
+ * Get list of readable fields for a table
2939
+ *
2940
+ * @param table - Table name
2941
+ * @param row - Row data (for context-dependent fields)
2942
+ * @returns Array of readable field names
2943
+ */
2944
+ async getReadableFields(table, row) {
2945
+ const ctx = this.getContext();
2946
+ if (!ctx || ctx.auth.isSystem) {
2947
+ return Object.keys(row);
2948
+ }
2949
+ const tableConfig = this.registry.getTableConfig(table);
2950
+ if (!tableConfig) {
2951
+ return Object.keys(row);
2952
+ }
2953
+ if (tableConfig.skipFor.some((role) => ctx.auth.roles.includes(role))) {
2954
+ return Object.keys(row);
2955
+ }
2956
+ const evalCtx = this.createEvalContext(ctx, row, table);
2957
+ const readable = [];
2958
+ for (const field of Object.keys(row)) {
2959
+ const canRead = await this.registry.canReadField(table, field, evalCtx);
2960
+ if (canRead) {
2961
+ readable.push(field);
2962
+ }
2963
+ }
2964
+ return readable;
2965
+ }
2966
+ /**
2967
+ * Get list of writable fields for a table
2968
+ *
2969
+ * @param table - Table name
2970
+ * @param row - Existing row data (for context-dependent fields)
2971
+ * @returns Array of writable field names
2972
+ */
2973
+ async getWritableFields(table, row) {
2974
+ const ctx = this.getContext();
2975
+ if (!ctx || ctx.auth.isSystem) {
2976
+ return Object.keys(row);
2977
+ }
2978
+ const tableConfig = this.registry.getTableConfig(table);
2979
+ if (!tableConfig) {
2980
+ return Object.keys(row);
2981
+ }
2982
+ if (tableConfig.skipFor.some((role) => ctx.auth.roles.includes(role))) {
2983
+ return Object.keys(row);
2984
+ }
2985
+ const evalCtx = this.createEvalContext(ctx, row, table);
2986
+ const writable = [];
2987
+ for (const field of Object.keys(row)) {
2988
+ const canWrite = await this.registry.canWriteField(table, field, evalCtx);
2989
+ if (canWrite) {
2990
+ writable.push(field);
2991
+ }
2992
+ }
2993
+ return writable;
2994
+ }
2995
+ // ============================================================================
2996
+ // Private Methods
2997
+ // ============================================================================
2998
+ /**
2999
+ * Get current RLS context
3000
+ */
3001
+ getContext() {
3002
+ return rlsContext.getContextOrNull();
3003
+ }
3004
+ /**
3005
+ * Create evaluation context
3006
+ */
3007
+ createEvalContext(ctx, row, table, data) {
3008
+ return {
3009
+ auth: ctx.auth,
3010
+ row,
3011
+ data,
3012
+ table,
3013
+ ...ctx.meta !== void 0 && { meta: ctx.meta }
3014
+ };
3015
+ }
3016
+ /**
3017
+ * Evaluate field access for a specific field
3018
+ */
3019
+ async evaluateFieldAccess(tableConfig, field, value, ctx, options) {
3020
+ const fieldConfig = tableConfig.fields.get(field);
3021
+ if (!fieldConfig) {
3022
+ const accessible = tableConfig.defaultAccess === "allow";
3023
+ return {
3024
+ accessible,
3025
+ value: accessible ? value : this.defaultMaskValue,
3026
+ omit: !accessible && options.throwOnDenied !== true
3027
+ };
3028
+ }
3029
+ try {
3030
+ const canRead = await fieldConfig.canRead(ctx);
3031
+ if (canRead) {
3032
+ return {
3033
+ accessible: true,
3034
+ value
3035
+ };
3036
+ }
3037
+ if (options.throwOnDenied) {
3038
+ throw new RLSPolicyViolation("read", ctx.table ?? "unknown", `Cannot read field: ${field}`);
3039
+ }
3040
+ const configWithMask = fieldConfig;
3041
+ const maskedValue = configWithMask.maskFn ? configWithMask.maskFn(value) : fieldConfig.maskedValue ?? this.defaultMaskValue;
3042
+ return {
3043
+ accessible: false,
3044
+ reason: `Field "${field}" is not accessible`,
3045
+ value: maskedValue,
3046
+ omit: fieldConfig.omitWhenHidden
3047
+ };
3048
+ } catch (error) {
3049
+ if (error instanceof RLSPolicyViolation) {
3050
+ throw error;
3051
+ }
3052
+ return {
3053
+ accessible: false,
3054
+ reason: `Error evaluating access: ${error instanceof Error ? error.message : String(error)}`,
3055
+ value: this.defaultMaskValue,
3056
+ omit: true
3057
+ };
3058
+ }
3059
+ }
3060
+ };
3061
+ function createFieldAccessProcessor(registry, defaultMaskValue) {
3062
+ return new FieldAccessProcessor(registry, defaultMaskValue);
3063
+ }
3064
+
3065
+ // src/composition/builder.ts
3066
+ function definePolicy(config, policies) {
3067
+ const result = {
3068
+ name: config.name,
3069
+ policies
3070
+ };
3071
+ if (config.description !== void 0) {
3072
+ result.description = config.description;
3073
+ }
3074
+ if (config.tags !== void 0) {
3075
+ result.tags = config.tags;
3076
+ }
3077
+ return result;
3078
+ }
3079
+ function defineFilterPolicy(name, filterFn, options) {
3080
+ return {
3081
+ name,
3082
+ policies: [
3083
+ filter("read", filterFn, {
3084
+ name: `${name}-filter`,
3085
+ ...options?.priority !== void 0 && { priority: options.priority }
3086
+ })
3087
+ ]
3088
+ };
3089
+ }
3090
+ function defineAllowPolicy(name, operation, condition, options) {
3091
+ return {
3092
+ name,
3093
+ policies: [
3094
+ allow(operation, condition, {
3095
+ name: `${name}-allow`,
3096
+ ...options?.priority !== void 0 && { priority: options.priority }
3097
+ })
3098
+ ]
3099
+ };
3100
+ }
3101
+ function defineDenyPolicy(name, operation, condition, options) {
3102
+ return {
3103
+ name,
3104
+ policies: [
3105
+ deny(operation, condition, {
3106
+ name: `${name}-deny`,
3107
+ priority: options?.priority ?? 100
3108
+ })
3109
+ ]
3110
+ };
3111
+ }
3112
+ function defineValidatePolicy(name, operation, condition, options) {
3113
+ return {
3114
+ name,
3115
+ policies: [
3116
+ validate(operation, condition, {
3117
+ name: `${name}-validate`,
3118
+ ...options?.priority !== void 0 && { priority: options.priority }
3119
+ })
3120
+ ]
3121
+ };
3122
+ }
3123
+ function defineCombinedPolicy(name, config) {
3124
+ const policies = [];
3125
+ if (config.filter) {
3126
+ policies.push(
3127
+ filter("read", config.filter, {
3128
+ name: `${name}-filter`
3129
+ })
3130
+ );
3131
+ }
3132
+ if (config.allow) {
3133
+ for (const [op, condition] of Object.entries(config.allow)) {
3134
+ if (condition) {
3135
+ policies.push(
3136
+ allow(op, condition, {
3137
+ name: `${name}-allow-${op}`
3138
+ })
3139
+ );
3140
+ }
3141
+ }
3142
+ }
3143
+ if (config.deny) {
3144
+ for (const [op, condition] of Object.entries(config.deny)) {
3145
+ if (condition) {
3146
+ policies.push(
3147
+ deny(op, condition, {
3148
+ name: `${name}-deny-${op}`,
3149
+ priority: 100
3150
+ })
3151
+ );
3152
+ }
3153
+ }
3154
+ }
3155
+ if (config.validate) {
3156
+ if (config.validate.create) {
3157
+ policies.push(
3158
+ validate("create", config.validate.create, {
3159
+ name: `${name}-validate-create`
3160
+ })
3161
+ );
3162
+ }
3163
+ if (config.validate.update) {
3164
+ policies.push(
3165
+ validate("update", config.validate.update, {
3166
+ name: `${name}-validate-update`
3167
+ })
3168
+ );
3169
+ }
3170
+ }
3171
+ return {
3172
+ name,
3173
+ policies
3174
+ };
3175
+ }
3176
+ function createTenantIsolationPolicy(config = {}) {
3177
+ const { tenantColumn = "tenant_id", validateOnMutation = true } = config;
3178
+ const policies = [
3179
+ // Filter reads by tenant
3180
+ filter("read", (ctx) => ({ [tenantColumn]: ctx.auth.tenantId }), {
3181
+ name: "tenant-isolation-filter",
3182
+ priority: 1e3
3183
+ })
3184
+ ];
3185
+ if (validateOnMutation) {
3186
+ policies.push(
3187
+ validate("create", (ctx) => {
3188
+ const data = ctx.data;
3189
+ return data?.[tenantColumn] === ctx.auth.tenantId;
3190
+ }, {
3191
+ name: "tenant-isolation-validate-create"
3192
+ }),
3193
+ validate("update", (ctx) => {
3194
+ const data = ctx.data;
3195
+ if (data?.[tenantColumn] !== void 0) {
3196
+ return data[tenantColumn] === ctx.auth.tenantId;
3197
+ }
3198
+ return true;
3199
+ }, {
3200
+ name: "tenant-isolation-validate-update"
3201
+ })
3202
+ );
3203
+ }
3204
+ return {
3205
+ name: "tenantIsolation",
3206
+ description: `Filter by ${tenantColumn} for multi-tenancy`,
3207
+ policies,
3208
+ tags: ["multi-tenant", "isolation"]
3209
+ };
3210
+ }
3211
+ function createOwnershipPolicy(config = {}) {
3212
+ const { ownerColumn = "owner_id", ownerOperations = ["read", "update", "delete"], canDelete = true } = config;
3213
+ const policies = [];
3214
+ const ops = ownerOperations.filter((op) => op !== "delete" || canDelete);
3215
+ if (ops.length > 0) {
3216
+ policies.push(
3217
+ allow(ops, (ctx) => {
3218
+ const row = ctx.row;
3219
+ return ctx.auth.userId === row?.[ownerColumn];
3220
+ }, {
3221
+ name: "ownership-allow"
3222
+ })
3223
+ );
3224
+ }
3225
+ if (!canDelete && ownerOperations.includes("delete")) {
3226
+ policies.push(
3227
+ deny("delete", () => true, {
3228
+ name: "ownership-no-delete",
3229
+ priority: 150
3230
+ })
3231
+ );
3232
+ }
3233
+ return {
3234
+ name: "ownership",
3235
+ description: `Owner access via ${ownerColumn}`,
3236
+ policies,
3237
+ tags: ["ownership"]
3238
+ };
3239
+ }
3240
+ function createSoftDeletePolicy(config = {}) {
3241
+ const { deletedColumn = "deleted_at", filterOnRead = true, preventHardDelete = true } = config;
3242
+ const policies = [];
3243
+ if (filterOnRead) {
3244
+ policies.push(
3245
+ filter("read", () => ({ [deletedColumn]: null }), {
3246
+ name: "soft-delete-filter",
3247
+ priority: 900
3248
+ })
3249
+ );
3250
+ }
3251
+ if (preventHardDelete) {
3252
+ policies.push(
3253
+ deny("delete", () => true, {
3254
+ name: "soft-delete-no-hard-delete",
3255
+ priority: 150
3256
+ })
3257
+ );
3258
+ }
3259
+ return {
3260
+ name: "softDelete",
3261
+ description: `Soft delete via ${deletedColumn}`,
3262
+ policies,
3263
+ tags: ["soft-delete"]
3264
+ };
3265
+ }
3266
+ function createStatusAccessPolicy(config) {
3267
+ const { statusColumn = "status", publicStatuses = [], editableStatuses = [], deletableStatuses = [] } = config;
3268
+ const policies = [];
3269
+ if (publicStatuses.length > 0) {
3270
+ policies.push(
3271
+ allow("read", (ctx) => {
3272
+ const row = ctx.row;
3273
+ return publicStatuses.includes(row?.[statusColumn]);
3274
+ }, {
3275
+ name: "status-public-read"
3276
+ })
3277
+ );
3278
+ }
3279
+ if (editableStatuses.length > 0) {
3280
+ policies.push(
3281
+ deny("update", (ctx) => {
3282
+ const row = ctx.row;
3283
+ return !editableStatuses.includes(row?.[statusColumn]);
3284
+ }, {
3285
+ name: "status-restrict-update",
3286
+ priority: 100
3287
+ })
3288
+ );
3289
+ }
3290
+ if (deletableStatuses.length > 0) {
3291
+ policies.push(
3292
+ deny("delete", (ctx) => {
3293
+ const row = ctx.row;
3294
+ return !deletableStatuses.includes(row?.[statusColumn]);
3295
+ }, {
3296
+ name: "status-restrict-delete",
3297
+ priority: 100
3298
+ })
3299
+ );
3300
+ }
3301
+ return {
3302
+ name: "statusAccess",
3303
+ description: `Status-based access via ${statusColumn}`,
3304
+ policies,
3305
+ tags: ["status"]
3306
+ };
3307
+ }
3308
+ function createAdminPolicy(roles) {
3309
+ return {
3310
+ name: "adminBypass",
3311
+ description: `Admin access for roles: ${roles.join(", ")}`,
3312
+ policies: [
3313
+ allow("all", (ctx) => roles.some((r) => ctx.auth.roles.includes(r)), {
3314
+ name: "admin-bypass",
3315
+ priority: 500
3316
+ })
3317
+ ],
3318
+ tags: ["admin"]
3319
+ };
3320
+ }
3321
+ function composePolicies(name, policies) {
3322
+ const allPolicies = [];
3323
+ const allTags = /* @__PURE__ */ new Set();
3324
+ for (const policy of policies) {
3325
+ allPolicies.push(...policy.policies);
3326
+ policy.tags?.forEach((tag) => allTags.add(tag));
3327
+ }
3328
+ return {
3329
+ name,
3330
+ description: `Composed from: ${policies.map((p) => p.name).join(", ")}`,
3331
+ policies: allPolicies,
3332
+ tags: Array.from(allTags)
3333
+ };
3334
+ }
3335
+ function extendPolicy(base, additional) {
3336
+ const result = {
3337
+ name: `${base.name}_extended`,
3338
+ policies: [...base.policies, ...additional]
3339
+ };
3340
+ if (base.description !== void 0) {
3341
+ result.description = base.description;
3342
+ }
3343
+ if (base.tags !== void 0) {
3344
+ result.tags = base.tags;
3345
+ }
3346
+ return result;
3347
+ }
3348
+ function overridePolicy(base, overrides) {
3349
+ const newPolicies = base.policies.map((policy) => {
3350
+ const override = policy.name ? overrides[policy.name] : void 0;
3351
+ return override ?? policy;
3352
+ });
3353
+ const result = {
3354
+ name: `${base.name}_overridden`,
3355
+ policies: newPolicies
3356
+ };
3357
+ if (base.description !== void 0) {
3358
+ result.description = base.description;
3359
+ }
3360
+ if (base.tags !== void 0) {
3361
+ result.tags = base.tags;
3362
+ }
3363
+ return result;
3364
+ }
3365
+
3366
+ // src/audit/types.ts
3367
+ var ConsoleAuditAdapter = class {
3368
+ options;
3369
+ constructor(options = {}) {
3370
+ this.options = {
3371
+ format: options.format ?? "text",
3372
+ colors: options.colors ?? true,
3373
+ includeTimestamp: options.includeTimestamp ?? true
3374
+ };
3375
+ }
3376
+ log(event) {
3377
+ if (this.options.format === "json") {
3378
+ console.log(JSON.stringify(event));
3379
+ } else {
3380
+ const prefix = this.getPrefix(event.decision);
3381
+ const timestamp = this.options.includeTimestamp ? `[${event.timestamp.toISOString()}] ` : "";
3382
+ console.log(
3383
+ `${timestamp}${prefix} RLS ${event.decision.toUpperCase()}: ${event.operation} on ${event.table}` + (event.policyName ? ` (policy: ${event.policyName})` : "") + (event.reason ? ` - ${event.reason}` : "") + (event.userId ? ` [user: ${event.userId}]` : "")
3384
+ );
3385
+ }
3386
+ return Promise.resolve();
3387
+ }
3388
+ async logBatch(events) {
3389
+ for (const event of events) {
3390
+ await this.log(event);
3391
+ }
3392
+ }
3393
+ getPrefix(decision) {
3394
+ if (!this.options.colors) {
3395
+ return decision === "allow" ? "\u2713" : decision === "deny" ? "\u2717" : "~";
3396
+ }
3397
+ switch (decision) {
3398
+ case "allow":
3399
+ return "\x1B[32m\u2713\x1B[0m";
3400
+ // Green
3401
+ case "deny":
3402
+ return "\x1B[31m\u2717\x1B[0m";
3403
+ // Red
3404
+ case "filter":
3405
+ return "\x1B[33m~\x1B[0m";
3406
+ // Yellow
3407
+ default:
3408
+ return "?";
3409
+ }
3410
+ }
3411
+ };
3412
+ var InMemoryAuditAdapter = class {
3413
+ events = [];
3414
+ maxSize;
3415
+ constructor(maxSize = 1e4) {
3416
+ this.maxSize = maxSize;
3417
+ }
3418
+ log(event) {
3419
+ this.events.push(event);
3420
+ if (this.events.length > this.maxSize) {
3421
+ this.events = this.events.slice(-this.maxSize);
3422
+ }
3423
+ return Promise.resolve();
3424
+ }
3425
+ logBatch(events) {
3426
+ this.events.push(...events);
3427
+ if (this.events.length > this.maxSize) {
3428
+ this.events = this.events.slice(-this.maxSize);
3429
+ }
3430
+ return Promise.resolve();
3431
+ }
3432
+ /**
3433
+ * Get all logged events
3434
+ */
3435
+ getEvents() {
3436
+ return [...this.events];
3437
+ }
3438
+ /**
3439
+ * Query events
3440
+ */
3441
+ query(params) {
3442
+ let results = [...this.events];
3443
+ if (params.userId !== void 0) {
3444
+ results = results.filter((e) => e.userId === params.userId);
3445
+ }
3446
+ if (params.tenantId !== void 0) {
3447
+ results = results.filter((e) => e.tenantId === params.tenantId);
3448
+ }
3449
+ if (params.table) {
3450
+ results = results.filter((e) => e.table === params.table);
3451
+ }
3452
+ if (params.operation) {
3453
+ results = results.filter((e) => e.operation === params.operation);
3454
+ }
3455
+ if (params.decision) {
3456
+ results = results.filter((e) => e.decision === params.decision);
3457
+ }
3458
+ if (params.startTime) {
3459
+ results = results.filter((e) => e.timestamp >= params.startTime);
3460
+ }
3461
+ if (params.endTime) {
3462
+ results = results.filter((e) => e.timestamp < params.endTime);
3463
+ }
3464
+ if (params.requestId) {
3465
+ results = results.filter((e) => e.requestId === params.requestId);
3466
+ }
3467
+ if (params.offset) {
3468
+ results = results.slice(params.offset);
3469
+ }
3470
+ if (params.limit) {
3471
+ results = results.slice(0, params.limit);
3472
+ }
3473
+ return results;
3474
+ }
3475
+ /**
3476
+ * Get statistics
3477
+ */
3478
+ getStats(params) {
3479
+ let events = this.events;
3480
+ if (params?.startTime) {
3481
+ events = events.filter((e) => e.timestamp >= params.startTime);
3482
+ }
3483
+ if (params?.endTime) {
3484
+ events = events.filter((e) => e.timestamp < params.endTime);
3485
+ }
3486
+ const byDecision = { allow: 0, deny: 0, filter: 0 };
3487
+ const byOperation = { read: 0, create: 0, update: 0, delete: 0, all: 0 };
3488
+ const byTable = {};
3489
+ for (const event of events) {
3490
+ byDecision[event.decision]++;
3491
+ byOperation[event.operation]++;
3492
+ byTable[event.table] = (byTable[event.table] ?? 0) + 1;
3493
+ }
3494
+ return {
3495
+ totalEvents: events.length,
3496
+ byDecision,
3497
+ byOperation,
3498
+ byTable,
3499
+ timeRange: {
3500
+ start: events[0]?.timestamp ?? /* @__PURE__ */ new Date(),
3501
+ end: events[events.length - 1]?.timestamp ?? /* @__PURE__ */ new Date()
3502
+ }
3503
+ };
3504
+ }
3505
+ /**
3506
+ * Clear all events
3507
+ */
3508
+ clear() {
3509
+ this.events = [];
3510
+ }
3511
+ /**
3512
+ * Get event count
3513
+ */
3514
+ get size() {
3515
+ return this.events.length;
3516
+ }
3517
+ };
3518
+
3519
+ // src/audit/logger.ts
3520
+ var AuditLogger = class {
3521
+ adapter;
3522
+ config;
3523
+ buffer = [];
3524
+ flushTimer = null;
3525
+ isShuttingDown = false;
3526
+ constructor(config) {
3527
+ this.adapter = config.adapter;
3528
+ const baseConfig = {
3529
+ enabled: config.enabled ?? true,
3530
+ defaults: config.defaults ?? {
3531
+ logAllowed: false,
3532
+ logDenied: true,
3533
+ logFilters: false
3534
+ },
3535
+ tables: config.tables ?? {},
3536
+ bufferSize: config.bufferSize ?? 100,
3537
+ flushInterval: config.flushInterval ?? 5e3,
3538
+ async: config.async ?? true,
3539
+ sampleRate: config.sampleRate ?? 1
3540
+ };
3541
+ this.config = config.onError !== void 0 ? { ...baseConfig, onError: config.onError } : baseConfig;
3542
+ if (this.config.flushInterval > 0) {
3543
+ this.startFlushTimer();
3544
+ }
3545
+ }
3546
+ /**
3547
+ * Log a policy decision
3548
+ *
3549
+ * @param operation - Database operation
3550
+ * @param table - Table name
3551
+ * @param decision - Decision result
3552
+ * @param policyName - Name of the policy
3553
+ * @param options - Additional options
3554
+ */
3555
+ async logDecision(operation, table, decision, policyName, options) {
3556
+ if (!this.config.enabled || this.isShuttingDown) {
3557
+ return;
3558
+ }
3559
+ if (this.config.sampleRate < 1 && Math.random() > this.config.sampleRate) {
3560
+ return;
3561
+ }
3562
+ const tableConfig = this.getTableConfig(table);
3563
+ if (!this.shouldLog(decision, tableConfig)) {
3564
+ return;
3565
+ }
3566
+ const ctx = rlsContext.getContextOrNull();
3567
+ const event = this.buildEvent(operation, table, decision, policyName, ctx, tableConfig, options);
3568
+ if (tableConfig.filter && !tableConfig.filter(event)) {
3569
+ event.filtered = true;
3570
+ return;
3571
+ }
3572
+ await this.logEvent(event);
3573
+ }
3574
+ /**
3575
+ * Log an allow decision
3576
+ */
3577
+ async logAllow(operation, table, policyName, options) {
3578
+ await this.logDecision(operation, table, "allow", policyName, options);
3579
+ }
3580
+ /**
3581
+ * Log a deny decision
3582
+ */
3583
+ async logDeny(operation, table, policyName, options) {
3584
+ await this.logDecision(operation, table, "deny", policyName, options);
3585
+ }
3586
+ /**
3587
+ * Log a filter application
3588
+ */
3589
+ async logFilter(table, policyName, options) {
3590
+ await this.logDecision("read", table, "filter", policyName, options);
3591
+ }
3592
+ /**
3593
+ * Flush buffered events
3594
+ */
3595
+ async flush() {
3596
+ if (this.buffer.length === 0) {
3597
+ return;
3598
+ }
3599
+ const eventsToFlush = [...this.buffer];
3600
+ this.buffer = [];
3601
+ try {
3602
+ if (this.adapter.logBatch) {
3603
+ await this.adapter.logBatch(eventsToFlush);
3604
+ } else {
3605
+ for (const event of eventsToFlush) {
3606
+ await this.adapter.log(event);
3607
+ }
3608
+ }
3609
+ } catch (error) {
3610
+ this.config.onError?.(error instanceof Error ? error : new Error(String(error)), eventsToFlush);
3611
+ }
3612
+ }
3613
+ /**
3614
+ * Close the logger
3615
+ */
3616
+ async close() {
3617
+ this.isShuttingDown = true;
3618
+ if (this.flushTimer) {
3619
+ clearInterval(this.flushTimer);
3620
+ this.flushTimer = null;
3621
+ }
3622
+ await this.flush();
3623
+ await this.adapter.flush?.();
3624
+ await this.adapter.close?.();
3625
+ }
3626
+ /**
3627
+ * Get buffer size
3628
+ */
3629
+ get bufferSize() {
3630
+ return this.buffer.length;
3631
+ }
3632
+ /**
3633
+ * Check if logger is enabled
3634
+ */
3635
+ get enabled() {
3636
+ return this.config.enabled;
3637
+ }
3638
+ /**
3639
+ * Enable or disable logging
3640
+ */
3641
+ setEnabled(enabled) {
3642
+ this.config.enabled = enabled;
3643
+ }
3644
+ // ============================================================================
3645
+ // Private Methods
3646
+ // ============================================================================
3647
+ /**
3648
+ * Get table-specific config with defaults
3649
+ */
3650
+ getTableConfig(table) {
3651
+ const tableOverride = this.config.tables[table];
3652
+ return {
3653
+ ...this.config.defaults,
3654
+ ...tableOverride,
3655
+ enabled: tableOverride?.enabled ?? true
3656
+ };
3657
+ }
3658
+ /**
3659
+ * Check if decision should be logged
3660
+ */
3661
+ shouldLog(decision, tableConfig) {
3662
+ if (!tableConfig.enabled) {
3663
+ return false;
3664
+ }
3665
+ switch (decision) {
3666
+ case "allow":
3667
+ return tableConfig.logAllowed ?? false;
3668
+ case "deny":
3669
+ return tableConfig.logDenied ?? true;
3670
+ case "filter":
3671
+ return tableConfig.logFilters ?? false;
3672
+ default:
3673
+ return false;
3674
+ }
3675
+ }
3676
+ /**
3677
+ * Build audit event
3678
+ */
3679
+ buildEvent(operation, table, decision, policyName, ctx, tableConfig, options) {
3680
+ const event = {
3681
+ timestamp: /* @__PURE__ */ new Date(),
3682
+ userId: ctx?.auth.userId ?? "anonymous",
3683
+ operation,
3684
+ table,
3685
+ decision
3686
+ };
3687
+ if (ctx?.auth.tenantId !== void 0) {
3688
+ event.tenantId = ctx.auth.tenantId;
3689
+ }
3690
+ if (policyName) {
3691
+ event.policyName = policyName;
3692
+ }
3693
+ if (options?.reason) {
3694
+ event.reason = options.reason;
3695
+ }
3696
+ if (options?.rowIds && options.rowIds.length > 0) {
3697
+ event.rowIds = options.rowIds;
3698
+ }
3699
+ if (options?.queryHash) {
3700
+ event.queryHash = options.queryHash;
3701
+ }
3702
+ if (options?.durationMs !== void 0) {
3703
+ event.durationMs = options.durationMs;
3704
+ }
3705
+ if (ctx?.request) {
3706
+ if (ctx.request.requestId) {
3707
+ event.requestId = ctx.request.requestId;
3708
+ }
3709
+ if (ctx.request.ipAddress) {
3710
+ event.ipAddress = ctx.request.ipAddress;
3711
+ }
3712
+ if (ctx.request.userAgent) {
3713
+ event.userAgent = ctx.request.userAgent;
3714
+ }
3715
+ }
3716
+ const context = this.buildContext(ctx, tableConfig, options?.context);
3717
+ if (context !== void 0) {
3718
+ event.context = context;
3719
+ }
3720
+ return event;
3721
+ }
3722
+ /**
3723
+ * Build context object with filtering
3724
+ */
3725
+ buildContext(ctx, tableConfig, additionalContext) {
3726
+ const context = {};
3727
+ if (ctx?.auth.roles && ctx.auth.roles.length > 0) {
3728
+ context["roles"] = ctx.auth.roles;
3729
+ }
3730
+ if (ctx?.auth.organizationIds && ctx.auth.organizationIds.length > 0) {
3731
+ context["organizationIds"] = ctx.auth.organizationIds;
3732
+ }
3733
+ if (ctx?.meta && typeof ctx.meta === "object") {
3734
+ Object.assign(context, ctx.meta);
3735
+ }
3736
+ if (additionalContext) {
3737
+ Object.assign(context, additionalContext);
3738
+ }
3739
+ let filteredContext = context;
3740
+ if (tableConfig.includeContext && tableConfig.includeContext.length > 0) {
3741
+ filteredContext = {};
3742
+ for (const key of tableConfig.includeContext) {
3743
+ if (key in context) {
3744
+ filteredContext[key] = context[key];
3745
+ }
3746
+ }
3747
+ }
3748
+ if (tableConfig.excludeContext && tableConfig.excludeContext.length > 0) {
3749
+ for (const key of tableConfig.excludeContext) {
3750
+ const { [key]: _, ...rest } = filteredContext;
3751
+ filteredContext = rest;
3752
+ }
3753
+ }
3754
+ return Object.keys(filteredContext).length > 0 ? filteredContext : void 0;
3755
+ }
3756
+ /**
3757
+ * Log event to buffer or directly
3758
+ */
3759
+ async logEvent(event) {
3760
+ if (this.config.async && this.config.bufferSize > 0) {
3761
+ this.buffer.push(event);
3762
+ if (this.buffer.length >= this.config.bufferSize) {
3763
+ await this.flush();
3764
+ }
3765
+ } else if (this.config.async) {
3766
+ this.adapter.log(event).catch((error) => {
3767
+ this.config.onError?.(error instanceof Error ? error : new Error(String(error)), [event]);
3768
+ });
3769
+ } else {
3770
+ try {
3771
+ await this.adapter.log(event);
3772
+ } catch (error) {
3773
+ this.config.onError?.(error instanceof Error ? error : new Error(String(error)), [event]);
3774
+ }
3775
+ }
3776
+ }
3777
+ /**
3778
+ * Start the flush timer
3779
+ */
3780
+ startFlushTimer() {
3781
+ this.flushTimer = setInterval(() => {
3782
+ this.flush().catch((error) => {
3783
+ this.config.onError?.(error instanceof Error ? error : new Error(String(error)), [...this.buffer]);
3784
+ });
3785
+ }, this.config.flushInterval);
3786
+ if (this.flushTimer.unref) {
3787
+ this.flushTimer.unref();
3788
+ }
3789
+ }
3790
+ };
3791
+ function createAuditLogger(config) {
3792
+ return new AuditLogger(config);
3793
+ }
3794
+
3795
+ // src/testing/index.ts
3796
+ var PolicyTester = class {
3797
+ registry;
3798
+ constructor(schema) {
3799
+ this.registry = new PolicyRegistry(schema);
3800
+ }
3801
+ /**
3802
+ * Evaluate policies for an operation
3803
+ *
3804
+ * @param table - Table name
3805
+ * @param operation - Operation to test
3806
+ * @param context - Test context
3807
+ * @returns Evaluation result
3808
+ */
3809
+ async evaluate(table, operation, context) {
3810
+ const evaluatedPolicies = [];
3811
+ if (!this.registry.hasTable(table)) {
3812
+ return {
3813
+ allowed: true,
3814
+ decisionType: "default",
3815
+ reason: "Table has no RLS policies",
3816
+ evaluatedPolicies
3817
+ };
3818
+ }
3819
+ if (context.auth.isSystem) {
3820
+ return {
3821
+ allowed: true,
3822
+ decisionType: "allow",
3823
+ reason: "System user bypasses RLS",
3824
+ evaluatedPolicies
3825
+ };
3826
+ }
3827
+ const skipFor = this.registry.getSkipFor(table);
3828
+ if (skipFor.some((role) => context.auth.roles.includes(role))) {
3829
+ return {
3830
+ allowed: true,
3831
+ decisionType: "allow",
3832
+ reason: `Role bypass: ${skipFor.find((r) => context.auth.roles.includes(r))}`,
3833
+ evaluatedPolicies
3834
+ };
3835
+ }
3836
+ const evalCtx = {
3837
+ auth: context.auth,
3838
+ row: context.row,
3839
+ data: context.data,
3840
+ table,
3841
+ operation,
3842
+ ...context.meta !== void 0 && { meta: context.meta }
3843
+ };
3844
+ const denies = this.registry.getDenies(table, operation);
3845
+ for (const deny2 of denies) {
3846
+ const result = await this.evaluatePolicy(deny2, evalCtx);
3847
+ evaluatedPolicies.push({
3848
+ name: deny2.name,
3849
+ type: "deny",
3850
+ result
3851
+ });
3852
+ if (result) {
3853
+ return {
3854
+ allowed: false,
3855
+ policyName: deny2.name,
3856
+ decisionType: "deny",
3857
+ reason: `Denied by policy: ${deny2.name}`,
3858
+ evaluatedPolicies
3859
+ };
3860
+ }
3861
+ }
3862
+ if ((operation === "create" || operation === "update") && context.data) {
3863
+ const validates = this.registry.getValidates(table, operation);
3864
+ for (const validate2 of validates) {
3865
+ const result = await this.evaluatePolicy(validate2, evalCtx);
3866
+ evaluatedPolicies.push({
3867
+ name: validate2.name,
3868
+ type: "validate",
3869
+ result
3870
+ });
3871
+ if (!result) {
3872
+ return {
3873
+ allowed: false,
3874
+ policyName: validate2.name,
3875
+ decisionType: "deny",
3876
+ reason: `Validation failed: ${validate2.name}`,
3877
+ evaluatedPolicies
3878
+ };
3879
+ }
3880
+ }
3881
+ }
3882
+ const allows = this.registry.getAllows(table, operation);
3883
+ const defaultDeny = this.registry.hasDefaultDeny(table);
3884
+ if (defaultDeny && allows.length === 0) {
3885
+ return {
3886
+ allowed: false,
3887
+ decisionType: "default",
3888
+ reason: "No allow policies defined (default deny)",
3889
+ evaluatedPolicies
3890
+ };
3891
+ }
3892
+ for (const allow2 of allows) {
3893
+ const result = await this.evaluatePolicy(allow2, evalCtx);
3894
+ evaluatedPolicies.push({
3895
+ name: allow2.name,
3896
+ type: "allow",
3897
+ result
3898
+ });
3899
+ if (result) {
3900
+ return {
3901
+ allowed: true,
3902
+ policyName: allow2.name,
3903
+ decisionType: "allow",
3904
+ reason: `Allowed by policy: ${allow2.name}`,
3905
+ evaluatedPolicies
3906
+ };
3907
+ }
3908
+ }
3909
+ if (defaultDeny) {
3910
+ return {
3911
+ allowed: false,
3912
+ decisionType: "default",
3913
+ reason: "No allow policies matched (default deny)",
3914
+ evaluatedPolicies
3915
+ };
3916
+ }
3917
+ return {
3918
+ allowed: true,
3919
+ decisionType: "default",
3920
+ reason: "No policies matched (default allow)",
3921
+ evaluatedPolicies
3922
+ };
3923
+ }
3924
+ /**
3925
+ * Get filter conditions for read operations
3926
+ *
3927
+ * @param table - Table name
3928
+ * @param operation - Must be 'read'
3929
+ * @param context - Test context
3930
+ * @returns Filter conditions
3931
+ */
3932
+ getFilters(table, _operation, context) {
3933
+ const conditions = {};
3934
+ const appliedFilters = [];
3935
+ if (!this.registry.hasTable(table)) {
3936
+ return { conditions, appliedFilters };
3937
+ }
3938
+ if (context.auth.isSystem) {
3939
+ return { conditions, appliedFilters };
3940
+ }
3941
+ const skipFor = this.registry.getSkipFor(table);
3942
+ if (skipFor.some((role) => context.auth.roles.includes(role))) {
3943
+ return { conditions, appliedFilters };
3944
+ }
3945
+ const filters = this.registry.getFilters(table);
3946
+ const evalCtx = {
3947
+ auth: context.auth,
3948
+ ...context.meta !== void 0 && { meta: context.meta }
3949
+ };
3950
+ for (const filter2 of filters) {
3951
+ const filterConditions = filter2.getConditions(evalCtx);
3952
+ Object.assign(conditions, filterConditions);
3953
+ appliedFilters.push(filter2.name);
3954
+ }
3955
+ return { conditions, appliedFilters };
3956
+ }
3957
+ /**
3958
+ * Test if a specific policy allows the operation
3959
+ *
3960
+ * @param table - Table name
3961
+ * @param policyName - Name of the policy to test
3962
+ * @param context - Test context
3963
+ * @returns True if policy allows
3964
+ */
3965
+ async testPolicy(table, policyName, context) {
3966
+ const operations = ["read", "create", "update", "delete"];
3967
+ for (const op of operations) {
3968
+ const evalCtx = {
3969
+ auth: context.auth,
3970
+ row: context.row,
3971
+ data: context.data,
3972
+ table,
3973
+ operation: op,
3974
+ ...context.meta !== void 0 && { meta: context.meta }
3975
+ };
3976
+ const allows = this.registry.getAllows(table, op);
3977
+ const allow2 = allows.find((p) => p.name === policyName);
3978
+ if (allow2) {
3979
+ const result = await this.evaluatePolicy(allow2, evalCtx);
3980
+ return { found: true, result };
3981
+ }
3982
+ const denies = this.registry.getDenies(table, op);
3983
+ const deny2 = denies.find((p) => p.name === policyName);
3984
+ if (deny2) {
3985
+ const result = await this.evaluatePolicy(deny2, evalCtx);
3986
+ return { found: true, result };
3987
+ }
3988
+ const validates = this.registry.getValidates(table, op);
3989
+ const validate2 = validates.find((p) => p.name === policyName);
3990
+ if (validate2) {
3991
+ const result = await this.evaluatePolicy(validate2, evalCtx);
3992
+ return { found: true, result };
3993
+ }
3994
+ }
3995
+ return { found: false };
3996
+ }
3997
+ /**
3998
+ * List all policies for a table
3999
+ */
4000
+ listPolicies(table) {
4001
+ const operations = ["read", "create", "update", "delete"];
4002
+ const allowSet = /* @__PURE__ */ new Set();
4003
+ const denySet = /* @__PURE__ */ new Set();
4004
+ const validateSet = /* @__PURE__ */ new Set();
4005
+ for (const op of operations) {
4006
+ this.registry.getAllows(table, op).forEach((p) => allowSet.add(p.name));
4007
+ this.registry.getDenies(table, op).forEach((p) => denySet.add(p.name));
4008
+ this.registry.getValidates(table, op).forEach((p) => validateSet.add(p.name));
4009
+ }
4010
+ return {
4011
+ allows: Array.from(allowSet),
4012
+ denies: Array.from(denySet),
4013
+ filters: this.registry.getFilters(table).map((f) => f.name),
4014
+ validates: Array.from(validateSet)
4015
+ };
4016
+ }
4017
+ /**
4018
+ * Get all registered tables
4019
+ */
4020
+ getTables() {
4021
+ return this.registry.getTables();
4022
+ }
4023
+ /**
4024
+ * Evaluate a single policy
4025
+ */
4026
+ async evaluatePolicy(policy, ctx) {
4027
+ try {
4028
+ const result = policy.evaluate(ctx);
4029
+ return result instanceof Promise ? await result : result;
4030
+ } catch {
4031
+ return false;
4032
+ }
4033
+ }
4034
+ };
4035
+ function createPolicyTester(schema) {
4036
+ return new PolicyTester(schema);
4037
+ }
4038
+ function createTestAuthContext(overrides) {
4039
+ return {
4040
+ roles: [],
4041
+ isSystem: false,
4042
+ ...overrides
4043
+ };
4044
+ }
4045
+ function createTestRow(data) {
4046
+ return { ...data };
4047
+ }
4048
+ var policyAssertions = {
4049
+ /**
4050
+ * Assert that the result is allowed
4051
+ */
4052
+ assertAllowed(result, message) {
4053
+ if (!result.allowed) {
4054
+ throw new Error(
4055
+ message ?? `Expected policy to allow, but was denied: ${result.reason}`
4056
+ );
4057
+ }
4058
+ },
4059
+ /**
4060
+ * Assert that the result is denied
4061
+ */
4062
+ assertDenied(result, message) {
4063
+ if (result.allowed) {
4064
+ throw new Error(
4065
+ message ?? `Expected policy to deny, but was allowed: ${result.reason}`
4066
+ );
4067
+ }
4068
+ },
4069
+ /**
4070
+ * Assert that a specific policy made the decision
4071
+ */
4072
+ assertPolicyUsed(result, policyName, message) {
4073
+ if (result.policyName !== policyName) {
4074
+ throw new Error(
4075
+ message ?? `Expected policy "${policyName}" but was "${result.policyName}"`
4076
+ );
4077
+ }
4078
+ },
4079
+ /**
4080
+ * Assert that filters include expected conditions
4081
+ */
4082
+ assertFiltersInclude(result, expected, message) {
4083
+ for (const [key, value] of Object.entries(expected)) {
4084
+ if (result.conditions[key] !== value) {
4085
+ throw new Error(
4086
+ message ?? `Expected filter condition ${key}=${JSON.stringify(value)} but got ${JSON.stringify(result.conditions[key])}`
4087
+ );
4088
+ }
4089
+ }
4090
+ }
4091
+ };
4092
+
4093
+ export { AuditLogger, ConsoleAuditAdapter, FieldAccessProcessor, FieldAccessRegistry, InMemoryAuditAdapter, InMemoryCacheProvider, PolicyRegistry, PolicyTester, RLSContextError, RLSContextValidationError, RLSError, RLSErrorCodes, RLSPluginOptionsSchema, RLSPolicyEvaluationError, RLSPolicyViolation, RLSSchemaError, ReBAcRegistry, ReBAcTransformer, ResolverManager, allow, allowRelation, composePolicies, createAdminPolicy, createAuditLogger, createEvaluationContext, createFieldAccessProcessor, createFieldAccessRegistry, createOwnershipPolicy, createPolicyTester, createRLSContext, createReBAcRegistry, createReBAcTransformer, createResolver, createResolverManager, createSoftDeletePolicy, createStatusAccessPolicy, createTenantIsolationPolicy, createTestAuthContext, createTestRow, deepMerge, defineAllowPolicy, defineCombinedPolicy, defineDenyPolicy, defineFilterPolicy, definePolicy, defineRLSSchema, defineValidatePolicy, deny, denyRelation, extendPolicy, filter, hashString, isAsyncFunction, maskedField, mergeRLSSchemas, neverAccessible, normalizeOperations, orgMembershipPath, overridePolicy, ownerOnly, ownerOrRoles, policyAssertions, publicReadRestrictedWrite, readOnly, rlsContext, rlsPlugin, rolesOnly, safeEvaluate, shopOrgMembershipPath, teamHierarchyPath, validate, whenCondition, whenEnvironment, whenFeature, whenTimeRange, withRLSContext, withRLSContextAsync };
1644
4094
  //# sourceMappingURL=index.js.map
1645
4095
  //# sourceMappingURL=index.js.map