@kysera/rls 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -780,7 +780,7 @@ interface RLSPluginOptions<DB = unknown> {
780
780
  * Require RLS context (throws if missing)
781
781
  * @default true - SECURE BY DEFAULT
782
782
  *
783
- * SECURITY: Changed to true in v0.7.3+ for secure-by-default.
783
+ * SECURITY: Changed to true in v0.8.0+ for secure-by-default.
784
784
  * When true, missing context throws RLSContextError.
785
785
  * Only set to false if you have other security controls.
786
786
  */
@@ -836,9 +836,9 @@ const plugin = rlsPlugin({
836
836
  const orm = await createORM(db, [plugin])
837
837
  ```
838
838
 
839
- ### Security Configuration (v0.7.3+)
839
+ ### Security Configuration (v0.8.0+)
840
840
 
841
- **BREAKING CHANGE**: Starting in v0.7.3, `requireContext` defaults to `true` for secure-by-default behavior.
841
+ **BREAKING CHANGE**: Starting in v0.8.0, `requireContext` defaults to `true` for secure-by-default behavior.
842
842
 
843
843
  #### Secure Defaults (Recommended)
844
844
 
@@ -1316,6 +1316,822 @@ export {
1316
1316
 
1317
1317
  ---
1318
1318
 
1319
+ ## Advanced Features
1320
+
1321
+ The following advanced features provide enterprise-grade capabilities for complex authorization scenarios.
1322
+
1323
+ ### Context Resolvers (Pre-resolved Async Data)
1324
+
1325
+ Context resolvers allow you to pre-fetch and cache async data (like organization memberships, permissions from external services) before policy evaluation. This keeps policies synchronous and fast.
1326
+
1327
+ ```typescript
1328
+ import {
1329
+ ResolverManager,
1330
+ createResolverManager,
1331
+ createResolver,
1332
+ type EnhancedRLSContext
1333
+ } from '@kysera/rls'
1334
+
1335
+ // Define a resolver for organization memberships
1336
+ const orgResolver = createResolver({
1337
+ name: 'org-memberships',
1338
+ resolve: async (ctx) => {
1339
+ // Fetch from database or external service
1340
+ const memberships = await db
1341
+ .selectFrom('organization_members')
1342
+ .where('user_id', '=', ctx.auth.userId)
1343
+ .select(['organization_id', 'role'])
1344
+ .execute()
1345
+
1346
+ return {
1347
+ resolvedAt: new Date(),
1348
+ organizationIds: memberships.map(m => m.organization_id),
1349
+ orgRoles: memberships.reduce((acc, m) => {
1350
+ acc[m.organization_id] = m.role
1351
+ return acc
1352
+ }, {} as Record<string, string>)
1353
+ }
1354
+ },
1355
+ cacheKey: (ctx) => `rls:org:${ctx.auth.userId}`,
1356
+ cacheTtl: 300 // 5 minutes
1357
+ })
1358
+
1359
+ // Create manager and register resolvers
1360
+ const manager = createResolverManager({
1361
+ defaultCacheTtl: 300,
1362
+ parallelResolution: true,
1363
+ resolverTimeout: 5000
1364
+ })
1365
+
1366
+ manager.register(orgResolver)
1367
+
1368
+ // Resolve context before using RLS
1369
+ const baseCtx = {
1370
+ auth: { userId: '123', roles: ['user'] },
1371
+ timestamp: new Date()
1372
+ }
1373
+
1374
+ const enhancedCtx = await manager.resolve(baseCtx)
1375
+ // enhancedCtx.auth.resolved contains: { organizationIds, orgRoles, resolvedAt }
1376
+
1377
+ // Use in RLS context - policies can access resolved data synchronously
1378
+ await rlsContext.runAsync(enhancedCtx, async () => {
1379
+ const posts = await orm.posts.findAll()
1380
+ })
1381
+ ```
1382
+
1383
+ **Resolver Dependencies:**
1384
+
1385
+ ```typescript
1386
+ const permissionResolver = createResolver({
1387
+ name: 'permissions',
1388
+ dependsOn: ['org-memberships'], // Runs after org-memberships
1389
+ resolve: async (ctx) => {
1390
+ const orgIds = ctx.auth.resolved?.organizationIds ?? []
1391
+ const permissions = await fetchPermissions(ctx.auth.userId, orgIds)
1392
+ return { resolvedAt: new Date(), permissions }
1393
+ }
1394
+ })
1395
+ ```
1396
+
1397
+ **Cache Invalidation:**
1398
+
1399
+ ```typescript
1400
+ // Invalidate specific resolver cache
1401
+ await manager.invalidateCache(userId, 'org-memberships')
1402
+
1403
+ // Invalidate all caches for a user
1404
+ await manager.invalidateCache(userId)
1405
+
1406
+ // Clear all caches
1407
+ await manager.clearCache()
1408
+ ```
1409
+
1410
+ ---
1411
+
1412
+ ### Field-Level Access Control
1413
+
1414
+ Control access to individual columns within rows based on user context. Supports reading, writing, and masking.
1415
+
1416
+ ```typescript
1417
+ import {
1418
+ createFieldAccessRegistry,
1419
+ createFieldAccessProcessor,
1420
+ ownerOnly,
1421
+ rolesOnly,
1422
+ maskedField,
1423
+ publicReadRestrictedWrite,
1424
+ type FieldAccessSchema
1425
+ } from '@kysera/rls'
1426
+
1427
+ // Define field access rules
1428
+ const fieldSchema: FieldAccessSchema = {
1429
+ users: {
1430
+ fields: {
1431
+ // Email only visible to owner or admin
1432
+ email: ownerOnly('id'),
1433
+
1434
+ // Salary only visible to HR
1435
+ salary: rolesOnly(['hr', 'admin']),
1436
+
1437
+ // SSN always masked except for owner
1438
+ ssn: maskedField('***-**-****', ownerOnly('id')),
1439
+
1440
+ // Phone public read, restricted write
1441
+ phone: publicReadRestrictedWrite(['admin']),
1442
+
1443
+ // Custom rule
1444
+ notes: {
1445
+ read: ctx => ctx.auth.roles.includes('manager'),
1446
+ write: ctx => ctx.auth.roles.includes('admin')
1447
+ }
1448
+ },
1449
+ defaultMask: '[REDACTED]'
1450
+ }
1451
+ }
1452
+
1453
+ // Create registry and processor
1454
+ const registry = createFieldAccessRegistry(fieldSchema)
1455
+ const processor = createFieldAccessProcessor(registry)
1456
+
1457
+ // Check field access
1458
+ const canReadEmail = registry.canReadField('users', 'email', {
1459
+ auth: { userId: '1', roles: ['user'] },
1460
+ row: { id: '1', email: 'user@example.com' }
1461
+ })
1462
+
1463
+ // Mask sensitive fields in rows
1464
+ const rows = await db.selectFrom('users').selectAll().execute()
1465
+ const maskedRows = processor.maskRows('users', rows, {
1466
+ auth: { userId: '1', roles: ['user'] }
1467
+ })
1468
+ // Results have sensitive fields masked based on rules
1469
+ ```
1470
+
1471
+ **Predefined Access Patterns:**
1472
+
1473
+ | Pattern | Description |
1474
+ |---------|-------------|
1475
+ | `ownerOnly(field)` | Only row owner can read/write |
1476
+ | `rolesOnly(roles)` | Only specified roles can access |
1477
+ | `readOnly()` | Anyone can read, no one can write |
1478
+ | `neverAccessible()` | Always hidden |
1479
+ | `publicReadRestrictedWrite(roles)` | Anyone reads, roles write |
1480
+ | `maskedField(mask, condition)` | Shows mask unless condition passes |
1481
+ | `ownerOrRoles(roles, field)` | Owner or specified roles |
1482
+
1483
+ ---
1484
+
1485
+ ### Relationship-Based Access Control (ReBAC)
1486
+
1487
+ Define access based on relationships between entities using EXISTS subqueries. Ideal for complex organizational hierarchies.
1488
+
1489
+ ```typescript
1490
+ import {
1491
+ ReBAcRegistry,
1492
+ ReBAcTransformer,
1493
+ createReBAcRegistry,
1494
+ createReBAcTransformer,
1495
+ orgMembershipPath,
1496
+ shopOrgMembershipPath,
1497
+ allowRelation,
1498
+ denyRelation,
1499
+ type ReBAcSchema
1500
+ } from '@kysera/rls'
1501
+
1502
+ // Define relationship paths
1503
+ const rebacSchema: ReBAcSchema<Database> = {
1504
+ products: {
1505
+ relationships: [
1506
+ // products -> shops -> organizations -> org_members
1507
+ shopOrgMembershipPath('products', 'shop_id')
1508
+ ],
1509
+ policies: [
1510
+ {
1511
+ name: 'org-member-access',
1512
+ policyType: 'allow',
1513
+ operation: ['read', 'update'],
1514
+ relationshipPath: 'products_shop_org_membership',
1515
+ endCondition: ctx => ({
1516
+ user_id: ctx.auth.userId,
1517
+ status: 'active'
1518
+ })
1519
+ }
1520
+ ]
1521
+ },
1522
+
1523
+ documents: {
1524
+ relationships: [
1525
+ // Custom path: documents -> projects -> teams -> team_members
1526
+ {
1527
+ name: 'documents_team_membership',
1528
+ steps: [
1529
+ { from: 'documents', to: 'projects', fromColumn: 'project_id', toColumn: 'id' },
1530
+ { from: 'projects', to: 'teams', fromColumn: 'team_id', toColumn: 'id' },
1531
+ { from: 'teams', to: 'team_members', fromColumn: 'id', toColumn: 'team_id' }
1532
+ ]
1533
+ }
1534
+ ],
1535
+ policies: [
1536
+ allowRelation('read', 'documents_team_membership', ctx => ({
1537
+ user_id: ctx.auth.userId
1538
+ }))
1539
+ ]
1540
+ }
1541
+ }
1542
+
1543
+ // Create registry and transformer
1544
+ const registry = createReBAcRegistry(rebacSchema)
1545
+ const transformer = createReBAcTransformer(registry)
1546
+
1547
+ // Transform queries to add EXISTS subqueries
1548
+ await rlsContext.runAsync(ctx, async () => {
1549
+ // Original: SELECT * FROM products
1550
+ // Transformed: SELECT * FROM products WHERE EXISTS (
1551
+ // SELECT 1 FROM shops
1552
+ // JOIN organizations ON ...
1553
+ // JOIN org_members ON ...
1554
+ // WHERE org_members.user_id = $1 AND org_members.status = 'active'
1555
+ // )
1556
+ const products = await transformer.transformSelect(
1557
+ db.selectFrom('products').selectAll(),
1558
+ 'products',
1559
+ 'read'
1560
+ ).execute()
1561
+ })
1562
+ ```
1563
+
1564
+ **Predefined Relationship Patterns:**
1565
+
1566
+ ```typescript
1567
+ // Organization membership: table -> organizations -> org_members
1568
+ orgMembershipPath('documents', 'organization_id')
1569
+
1570
+ // Shop-based: table -> shops -> organizations -> org_members
1571
+ shopOrgMembershipPath('products', 'shop_id')
1572
+
1573
+ // Team hierarchy: table -> teams (recursive) -> team_members
1574
+ teamHierarchyPath('tasks', 'team_id')
1575
+ ```
1576
+
1577
+ ---
1578
+
1579
+ ### Policy Composition & Reusable Policies
1580
+
1581
+ Create reusable policy templates that can be composed and extended across tables.
1582
+
1583
+ ```typescript
1584
+ import {
1585
+ createTenantIsolationPolicy,
1586
+ createOwnershipPolicy,
1587
+ createSoftDeletePolicy,
1588
+ createStatusAccessPolicy,
1589
+ createAdminPolicy,
1590
+ composePolicies,
1591
+ extendPolicy,
1592
+ defineFilterPolicy,
1593
+ defineAllowPolicy,
1594
+ defineDenyPolicy,
1595
+ defineValidatePolicy
1596
+ } from '@kysera/rls'
1597
+
1598
+ // Use predefined policy templates
1599
+ const tenantPolicy = createTenantIsolationPolicy({
1600
+ tenantColumn: 'tenant_id',
1601
+ validateOnCreate: true
1602
+ })
1603
+
1604
+ const ownerPolicy = createOwnershipPolicy({
1605
+ ownerColumn: 'user_id',
1606
+ allowedOperations: ['update', 'delete']
1607
+ })
1608
+
1609
+ const softDeletePolicy = createSoftDeletePolicy({
1610
+ deletedAtColumn: 'deleted_at',
1611
+ includeDeleted: false
1612
+ })
1613
+
1614
+ const statusPolicy = createStatusAccessPolicy({
1615
+ statusColumn: 'status',
1616
+ publicStatuses: ['published'],
1617
+ draftStatuses: ['draft'],
1618
+ archivedStatuses: ['archived']
1619
+ })
1620
+
1621
+ const adminPolicy = createAdminPolicy({
1622
+ adminRoles: ['admin', 'superadmin']
1623
+ })
1624
+
1625
+ // Compose policies together
1626
+ const combinedPolicy = composePolicies(
1627
+ tenantPolicy,
1628
+ ownerPolicy,
1629
+ softDeletePolicy,
1630
+ adminPolicy
1631
+ )
1632
+
1633
+ // Use in schema
1634
+ const schema = defineRLSSchema<Database>({
1635
+ posts: {
1636
+ policies: combinedPolicy.policies,
1637
+ defaultDeny: true
1638
+ }
1639
+ })
1640
+ ```
1641
+
1642
+ **Custom Reusable Policies:**
1643
+
1644
+ ```typescript
1645
+ // Define reusable filter
1646
+ const publicPostsFilter = defineFilterPolicy(
1647
+ 'public-posts',
1648
+ ctx => ({ is_public: true, deleted_at: null })
1649
+ )
1650
+
1651
+ // Define reusable allow policy
1652
+ const ownerEditPolicy = defineAllowPolicy(
1653
+ 'owner-edit',
1654
+ ['update', 'delete'],
1655
+ ctx => ctx.auth.userId === ctx.row?.author_id
1656
+ )
1657
+
1658
+ // Define reusable deny policy
1659
+ const preventDeletePublished = defineDenyPolicy(
1660
+ 'no-delete-published',
1661
+ 'delete',
1662
+ ctx => ctx.row?.status === 'published'
1663
+ )
1664
+
1665
+ // Define reusable validation
1666
+ const tenantValidation = defineValidatePolicy(
1667
+ 'tenant-validation',
1668
+ ['create', 'update'],
1669
+ ctx => !ctx.data?.tenant_id || ctx.data.tenant_id === ctx.auth.tenantId
1670
+ )
1671
+
1672
+ // Extend existing policy
1673
+ const extendedPolicy = extendPolicy(tenantPolicy, {
1674
+ additionalPolicies: [ownerEditPolicy.policies[0]!]
1675
+ })
1676
+ ```
1677
+
1678
+ ---
1679
+
1680
+ ### Audit Trail Integration
1681
+
1682
+ Log all RLS policy decisions with buffering, sampling, and filtering capabilities.
1683
+
1684
+ ```typescript
1685
+ import {
1686
+ AuditLogger,
1687
+ createAuditLogger,
1688
+ ConsoleAuditAdapter,
1689
+ InMemoryAuditAdapter,
1690
+ type AuditConfig,
1691
+ type RLSAuditAdapter
1692
+ } from '@kysera/rls'
1693
+
1694
+ // Custom database adapter
1695
+ class DatabaseAuditAdapter implements RLSAuditAdapter {
1696
+ constructor(private db: Kysely<AuditDB>) {}
1697
+
1698
+ async log(event: RLSAuditEvent): Promise<void> {
1699
+ await this.db.insertInto('rls_audit_log')
1700
+ .values({
1701
+ user_id: String(event.userId),
1702
+ operation: event.operation,
1703
+ table_name: event.table,
1704
+ decision: event.decision,
1705
+ policy_name: event.policyName,
1706
+ reason: event.reason,
1707
+ context: JSON.stringify(event.context),
1708
+ created_at: event.timestamp
1709
+ })
1710
+ .execute()
1711
+ }
1712
+
1713
+ async logBatch(events: RLSAuditEvent[]): Promise<void> {
1714
+ await this.db.insertInto('rls_audit_log')
1715
+ .values(events.map(e => ({
1716
+ user_id: String(e.userId),
1717
+ operation: e.operation,
1718
+ table_name: e.table,
1719
+ decision: e.decision,
1720
+ policy_name: e.policyName,
1721
+ context: JSON.stringify(e.context),
1722
+ created_at: e.timestamp
1723
+ })))
1724
+ .execute()
1725
+ }
1726
+ }
1727
+
1728
+ // Create audit logger
1729
+ const auditLogger = createAuditLogger({
1730
+ adapter: new DatabaseAuditAdapter(auditDb),
1731
+ enabled: true,
1732
+ bufferSize: 100, // Buffer up to 100 events
1733
+ flushInterval: 5000, // Flush every 5 seconds
1734
+ sampleRate: 1.0, // Log 100% of events (use 0.1 for 10%)
1735
+ async: true, // Fire-and-forget logging
1736
+
1737
+ // Default settings for all tables
1738
+ defaults: {
1739
+ logAllowed: false, // Don't log allow decisions
1740
+ logDenied: true, // Log all denials
1741
+ logFilters: false // Don't log filter applications
1742
+ },
1743
+
1744
+ // Table-specific settings
1745
+ tables: {
1746
+ sensitive_data: {
1747
+ logAllowed: true, // Log all access to sensitive tables
1748
+ includeContext: ['requestId', 'ipAddress']
1749
+ },
1750
+ public_content: {
1751
+ enabled: false // Don't audit public content
1752
+ }
1753
+ },
1754
+
1755
+ // Error handler
1756
+ onError: (error, events) => {
1757
+ console.error('Audit logging failed:', error, events.length, 'events lost')
1758
+ }
1759
+ })
1760
+
1761
+ // Log decisions
1762
+ await auditLogger.logAllow('read', 'posts', 'ownership-allow')
1763
+ await auditLogger.logDeny('delete', 'posts', 'status-check', {
1764
+ reason: 'Cannot delete published posts',
1765
+ rowIds: [123]
1766
+ })
1767
+ await auditLogger.logFilter('posts', 'tenant-filter')
1768
+
1769
+ // Ensure all events are flushed
1770
+ await auditLogger.flush()
1771
+
1772
+ // Graceful shutdown
1773
+ await auditLogger.close()
1774
+ ```
1775
+
1776
+ **Built-in Adapters:**
1777
+
1778
+ ```typescript
1779
+ // Console adapter (development)
1780
+ const consoleAdapter = new ConsoleAuditAdapter({
1781
+ format: 'text', // or 'json'
1782
+ colors: true,
1783
+ includeTimestamp: true
1784
+ })
1785
+
1786
+ // In-memory adapter (testing)
1787
+ const memoryAdapter = new InMemoryAuditAdapter(10000) // max 10k events
1788
+ const events = memoryAdapter.getEvents()
1789
+ const stats = memoryAdapter.getStats()
1790
+ const filtered = memoryAdapter.query({
1791
+ userId: '123',
1792
+ decision: 'deny',
1793
+ startTime: new Date('2024-01-01')
1794
+ })
1795
+ ```
1796
+
1797
+ ---
1798
+
1799
+ ### Policy Testing Utilities
1800
+
1801
+ Unit test your RLS policies without a database connection.
1802
+
1803
+ ```typescript
1804
+ import {
1805
+ PolicyTester,
1806
+ createPolicyTester,
1807
+ createTestAuthContext,
1808
+ createTestRow,
1809
+ policyAssertions
1810
+ } from '@kysera/rls'
1811
+
1812
+ const tester = createPolicyTester(rlsSchema)
1813
+
1814
+ describe('Post RLS Policies', () => {
1815
+ it('should allow owner to update their post', async () => {
1816
+ const result = await tester.evaluate('posts', 'update', {
1817
+ auth: createTestAuthContext({
1818
+ userId: 'user-1',
1819
+ roles: ['user'],
1820
+ tenantId: 'tenant-1'
1821
+ }),
1822
+ row: createTestRow({
1823
+ id: 'post-1',
1824
+ author_id: 'user-1',
1825
+ tenant_id: 'tenant-1',
1826
+ status: 'draft'
1827
+ })
1828
+ })
1829
+
1830
+ expect(result.allowed).toBe(true)
1831
+ expect(result.policyName).toBe('ownership-allow')
1832
+ expect(result.decisionType).toBe('allow')
1833
+ })
1834
+
1835
+ it('should deny non-owner update', async () => {
1836
+ const result = await tester.evaluate('posts', 'update', {
1837
+ auth: createTestAuthContext({
1838
+ userId: 'user-2',
1839
+ roles: ['user']
1840
+ }),
1841
+ row: createTestRow({
1842
+ id: 'post-1',
1843
+ author_id: 'user-1'
1844
+ })
1845
+ })
1846
+
1847
+ policyAssertions.assertDenied(result)
1848
+ expect(result.reason).toContain('not owner')
1849
+ })
1850
+
1851
+ it('should apply tenant filter correctly', () => {
1852
+ const filters = tester.getFilters('posts', 'read', {
1853
+ auth: createTestAuthContext({
1854
+ userId: 'user-1',
1855
+ tenantId: 'tenant-1'
1856
+ })
1857
+ })
1858
+
1859
+ policyAssertions.assertFiltersInclude(filters, {
1860
+ tenant_id: 'tenant-1'
1861
+ })
1862
+ expect(filters.appliedFilters).toContain('tenant-isolation')
1863
+ })
1864
+
1865
+ it('should bypass for admin role', async () => {
1866
+ const result = await tester.evaluate('posts', 'delete', {
1867
+ auth: createTestAuthContext({
1868
+ userId: 'admin-1',
1869
+ roles: ['admin']
1870
+ }),
1871
+ row: createTestRow({ id: 'post-1' })
1872
+ })
1873
+
1874
+ expect(result.allowed).toBe(true)
1875
+ expect(result.reason).toContain('Role bypass')
1876
+ })
1877
+
1878
+ it('should test specific policy', async () => {
1879
+ const { found, result } = await tester.testPolicy(
1880
+ 'posts',
1881
+ 'ownership-allow',
1882
+ {
1883
+ auth: createTestAuthContext({ userId: 'user-1' }),
1884
+ row: createTestRow({ author_id: 'user-1' })
1885
+ }
1886
+ )
1887
+
1888
+ expect(found).toBe(true)
1889
+ expect(result).toBe(true)
1890
+ })
1891
+ })
1892
+
1893
+ // List all policies for debugging
1894
+ const policies = tester.listPolicies('posts')
1895
+ console.log('Allows:', policies.allows)
1896
+ console.log('Denies:', policies.denies)
1897
+ console.log('Filters:', policies.filters)
1898
+ console.log('Validates:', policies.validates)
1899
+ ```
1900
+
1901
+ **Assertion Helpers:**
1902
+
1903
+ ```typescript
1904
+ import { policyAssertions } from '@kysera/rls'
1905
+
1906
+ // Assert allowed
1907
+ policyAssertions.assertAllowed(result, 'Custom error message')
1908
+
1909
+ // Assert denied
1910
+ policyAssertions.assertDenied(result)
1911
+
1912
+ // Assert specific policy made decision
1913
+ policyAssertions.assertPolicyUsed(result, 'ownership-allow')
1914
+
1915
+ // Assert filter conditions
1916
+ policyAssertions.assertFiltersInclude(filterResult, {
1917
+ tenant_id: 'expected-tenant',
1918
+ deleted_at: null
1919
+ })
1920
+ ```
1921
+
1922
+ ---
1923
+
1924
+ ### Conditional Policy Activation
1925
+
1926
+ Activate policies based on environment, feature flags, or time-based conditions.
1927
+
1928
+ ```typescript
1929
+ import {
1930
+ whenEnvironment,
1931
+ whenFeature,
1932
+ whenTimeRange,
1933
+ whenCondition,
1934
+ defineRLSSchema,
1935
+ filter,
1936
+ allow,
1937
+ deny
1938
+ } from '@kysera/rls'
1939
+
1940
+ const schema = defineRLSSchema<Database>({
1941
+ posts: {
1942
+ policies: [
1943
+ // Only active in production
1944
+ whenEnvironment('production', () =>
1945
+ filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }))
1946
+ ),
1947
+
1948
+ // Only when feature flag is enabled
1949
+ whenFeature('strict-rls', () =>
1950
+ deny('delete', ctx => ctx.row?.status === 'published')
1951
+ ),
1952
+
1953
+ // Business hours only (9 AM - 6 PM)
1954
+ whenTimeRange(9, 18, () =>
1955
+ allow('create', ctx => ctx.auth.roles.includes('user'))
1956
+ ),
1957
+
1958
+ // Overnight maintenance (10 PM - 6 AM, crosses midnight)
1959
+ whenTimeRange(22, 6, () =>
1960
+ deny('all', () => true, { name: 'maintenance-mode' })
1961
+ ),
1962
+
1963
+ // Custom condition
1964
+ whenCondition(
1965
+ ctx => ctx.meta?.featureFlags?.includes('beta') ?? false,
1966
+ () => allow('read', ctx => ctx.auth.attributes?.betaTester === true)
1967
+ ),
1968
+
1969
+ // Nested conditions
1970
+ whenEnvironment('production', () =>
1971
+ whenFeature('audit-mode', () =>
1972
+ filter('read', ctx => ({
1973
+ ...ctx.auth.tenantId && { tenant_id: ctx.auth.tenantId },
1974
+ audit_enabled: true
1975
+ }))
1976
+ )
1977
+ )
1978
+ ]
1979
+ }
1980
+ })
1981
+ ```
1982
+
1983
+ **Setting Activation Context:**
1984
+
1985
+ ```typescript
1986
+ await rlsContext.runAsync({
1987
+ auth: {
1988
+ userId: user.id,
1989
+ roles: user.roles,
1990
+ tenantId: user.tenantId
1991
+ },
1992
+ timestamp: new Date(),
1993
+ meta: {
1994
+ environment: process.env.NODE_ENV,
1995
+ features: new Set(['strict-rls', 'audit-mode']),
1996
+ featureFlags: ['beta', 'new-ui']
1997
+ }
1998
+ }, async () => {
1999
+ // Policies will check activation conditions
2000
+ const posts = await orm.posts.findAll()
2001
+ })
2002
+ ```
2003
+
2004
+ **Feature Flag Integration:**
2005
+
2006
+ ```typescript
2007
+ // With object-style features
2008
+ meta: {
2009
+ features: {
2010
+ 'strict-rls': true,
2011
+ 'audit-mode': false,
2012
+ 'beta-features': user.isBetaTester
2013
+ }
2014
+ }
2015
+
2016
+ // With Set
2017
+ meta: {
2018
+ features: new Set(['strict-rls', 'audit-mode'])
2019
+ }
2020
+
2021
+ // With array
2022
+ meta: {
2023
+ features: ['strict-rls', 'audit-mode']
2024
+ }
2025
+ ```
2026
+
2027
+ ---
2028
+
2029
+ ## Complete API Exports
2030
+
2031
+ ```typescript
2032
+ // Core
2033
+ export { defineRLSSchema, mergeRLSSchemas } from '@kysera/rls'
2034
+ export { allow, deny, filter, validate } from '@kysera/rls'
2035
+ export { whenEnvironment, whenFeature, whenTimeRange, whenCondition } from '@kysera/rls'
2036
+ export { rlsPlugin } from '@kysera/rls'
2037
+ export { rlsContext, createRLSContext, withRLSContext, withRLSContextAsync } from '@kysera/rls'
2038
+
2039
+ // Context Resolvers
2040
+ export {
2041
+ ResolverManager,
2042
+ createResolverManager,
2043
+ createResolver,
2044
+ InMemoryCacheProvider,
2045
+ type ContextResolver,
2046
+ type EnhancedRLSContext,
2047
+ type ResolvedData
2048
+ } from '@kysera/rls'
2049
+
2050
+ // ReBAC
2051
+ export {
2052
+ ReBAcRegistry,
2053
+ ReBAcTransformer,
2054
+ createReBAcRegistry,
2055
+ createReBAcTransformer,
2056
+ orgMembershipPath,
2057
+ shopOrgMembershipPath,
2058
+ teamHierarchyPath,
2059
+ allowRelation,
2060
+ denyRelation,
2061
+ type RelationshipPath,
2062
+ type ReBAcSchema
2063
+ } from '@kysera/rls'
2064
+
2065
+ // Field Access
2066
+ export {
2067
+ FieldAccessRegistry,
2068
+ FieldAccessProcessor,
2069
+ createFieldAccessRegistry,
2070
+ createFieldAccessProcessor,
2071
+ ownerOnly,
2072
+ rolesOnly,
2073
+ readOnly,
2074
+ neverAccessible,
2075
+ publicReadRestrictedWrite,
2076
+ maskedField,
2077
+ ownerOrRoles,
2078
+ type FieldAccessSchema
2079
+ } from '@kysera/rls'
2080
+
2081
+ // Policy Composition
2082
+ export {
2083
+ createTenantIsolationPolicy,
2084
+ createOwnershipPolicy,
2085
+ createSoftDeletePolicy,
2086
+ createStatusAccessPolicy,
2087
+ createAdminPolicy,
2088
+ composePolicies,
2089
+ extendPolicy,
2090
+ defineFilterPolicy,
2091
+ defineAllowPolicy,
2092
+ defineDenyPolicy,
2093
+ defineValidatePolicy,
2094
+ defineCombinedPolicy,
2095
+ type ReusablePolicy
2096
+ } from '@kysera/rls'
2097
+
2098
+ // Audit Trail
2099
+ export {
2100
+ AuditLogger,
2101
+ createAuditLogger,
2102
+ ConsoleAuditAdapter,
2103
+ InMemoryAuditAdapter,
2104
+ type RLSAuditEvent,
2105
+ type RLSAuditAdapter,
2106
+ type AuditConfig
2107
+ } from '@kysera/rls'
2108
+
2109
+ // Testing
2110
+ export {
2111
+ PolicyTester,
2112
+ createPolicyTester,
2113
+ createTestAuthContext,
2114
+ createTestRow,
2115
+ policyAssertions,
2116
+ type PolicyEvaluationResult,
2117
+ type FilterEvaluationResult,
2118
+ type TestContext
2119
+ } from '@kysera/rls'
2120
+
2121
+ // Errors
2122
+ export {
2123
+ RLSError,
2124
+ RLSContextError,
2125
+ RLSPolicyViolation,
2126
+ RLSPolicyEvaluationError,
2127
+ RLSSchemaError,
2128
+ RLSContextValidationError,
2129
+ RLSErrorCodes
2130
+ } from '@kysera/rls'
2131
+ ```
2132
+
2133
+ ---
2134
+
1319
2135
  ## License
1320
2136
 
1321
2137
  MIT