@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/README.md +819 -3
- package/dist/index.d.ts +2841 -11
- package/dist/index.js +2451 -1
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/{types-Dowjd6zG.d.ts → types-CyqksFKU.d.ts} +72 -1
- package/package.json +11 -6
- package/src/audit/index.ts +25 -0
- package/src/audit/logger.ts +465 -0
- package/src/audit/types.ts +625 -0
- package/src/composition/builder.ts +556 -0
- package/src/composition/index.ts +43 -0
- package/src/composition/types.ts +415 -0
- package/src/field-access/index.ts +38 -0
- package/src/field-access/processor.ts +442 -0
- package/src/field-access/registry.ts +259 -0
- package/src/field-access/types.ts +453 -0
- package/src/index.ts +180 -2
- package/src/policy/builder.ts +187 -10
- package/src/policy/types.ts +84 -0
- package/src/rebac/index.ts +30 -0
- package/src/rebac/registry.ts +303 -0
- package/src/rebac/transformer.ts +391 -0
- package/src/rebac/types.ts +412 -0
- package/src/resolvers/index.ts +30 -0
- package/src/resolvers/manager.ts +507 -0
- package/src/resolvers/types.ts +447 -0
- package/src/testing/index.ts +554 -0
package/dist/native/index.d.ts
CHANGED
|
@@ -638,6 +638,77 @@ interface PolicyHints {
|
|
|
638
638
|
* Stable policies can be cached during a query execution
|
|
639
639
|
*/
|
|
640
640
|
stable?: boolean;
|
|
641
|
+
/**
|
|
642
|
+
* Whether the policy condition can be async
|
|
643
|
+
* @internal Used by validation and allow/deny policies
|
|
644
|
+
*/
|
|
645
|
+
async?: boolean;
|
|
646
|
+
/**
|
|
647
|
+
* Whether the policy result is cacheable
|
|
648
|
+
*/
|
|
649
|
+
cacheable?: boolean;
|
|
650
|
+
/**
|
|
651
|
+
* Cache TTL in seconds if cacheable
|
|
652
|
+
*/
|
|
653
|
+
cacheTTL?: number;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Context for evaluating policy activation conditions
|
|
657
|
+
*
|
|
658
|
+
* Contains metadata about the current environment that determines
|
|
659
|
+
* whether a policy should be active.
|
|
660
|
+
*/
|
|
661
|
+
interface PolicyActivationContext {
|
|
662
|
+
/**
|
|
663
|
+
* Current environment (development, staging, production)
|
|
664
|
+
*/
|
|
665
|
+
environment?: string;
|
|
666
|
+
/**
|
|
667
|
+
* Feature flags that are enabled
|
|
668
|
+
* Can be a Set, array, or object with boolean/truthy values
|
|
669
|
+
*/
|
|
670
|
+
features?: Set<string> | string[] | Record<string, unknown>;
|
|
671
|
+
/**
|
|
672
|
+
* Current timestamp (for time-based policies)
|
|
673
|
+
*/
|
|
674
|
+
timestamp?: Date;
|
|
675
|
+
/**
|
|
676
|
+
* Custom metadata for activation decisions
|
|
677
|
+
*/
|
|
678
|
+
meta?: Record<string, unknown>;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Condition function for policy activation
|
|
682
|
+
*
|
|
683
|
+
* Returns true if the policy should be active, false otherwise.
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```typescript
|
|
687
|
+
* // Only active in production
|
|
688
|
+
* const productionOnly: PolicyActivationCondition = ctx =>
|
|
689
|
+
* ctx.environment === 'production';
|
|
690
|
+
*
|
|
691
|
+
* // Only active when feature flag is enabled
|
|
692
|
+
* const featureGated: PolicyActivationCondition = ctx =>
|
|
693
|
+
* ctx.features?.includes('new_security_policy') ?? false;
|
|
694
|
+
*
|
|
695
|
+
* // Time-based activation (active during business hours)
|
|
696
|
+
* const businessHours: PolicyActivationCondition = ctx => {
|
|
697
|
+
* const hour = (ctx.timestamp ?? new Date()).getHours();
|
|
698
|
+
* return hour >= 9 && hour < 17;
|
|
699
|
+
* };
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
type PolicyActivationCondition = (ctx: PolicyActivationContext) => boolean;
|
|
703
|
+
/**
|
|
704
|
+
* Extended policy definition with activation condition
|
|
705
|
+
*/
|
|
706
|
+
interface ConditionalPolicyDefinition extends PolicyDefinition {
|
|
707
|
+
/**
|
|
708
|
+
* Condition that determines if this policy is active
|
|
709
|
+
* If undefined, the policy is always active
|
|
710
|
+
*/
|
|
711
|
+
activationCondition?: PolicyActivationCondition;
|
|
641
712
|
}
|
|
642
713
|
|
|
643
|
-
export type {
|
|
714
|
+
export type { ConditionalPolicyDefinition as C, FilterCondition as F, Operation as O, PolicyCondition as P, RLSSchema as R, TableRLSConfig as T, PolicyHints as a, PolicyActivationCondition as b, PolicyDefinition as c, CompiledPolicy as d, CompiledFilterPolicy as e, RLSContext as f, RLSAuthContext as g, RLSRequestContext as h, PolicyEvaluationContext as i, PolicyType as j, PolicyActivationContext as k };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kysera/rls",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Row-Level Security plugin for Kysely - declarative policies, query transformation, native RLS support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
],
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"kysely": ">=0.28.8",
|
|
24
|
-
"
|
|
25
|
-
"@kysera/
|
|
24
|
+
"zod": ">=3.0.0",
|
|
25
|
+
"@kysera/executor": "0.8.3",
|
|
26
|
+
"@kysera/repository": "0.8.3"
|
|
26
27
|
},
|
|
27
28
|
"peerDependenciesMeta": {
|
|
28
29
|
"@kysera/executor": {
|
|
@@ -30,10 +31,13 @@
|
|
|
30
31
|
},
|
|
31
32
|
"@kysera/repository": {
|
|
32
33
|
"optional": true
|
|
34
|
+
},
|
|
35
|
+
"zod": {
|
|
36
|
+
"optional": false
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
"dependencies": {
|
|
36
|
-
"@kysera/core": "0.8.
|
|
40
|
+
"@kysera/core": "0.8.3"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
39
43
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -47,8 +51,9 @@
|
|
|
47
51
|
"tsup": "^8.5.1",
|
|
48
52
|
"typescript": "^5.9.3",
|
|
49
53
|
"vitest": "^4.0.16",
|
|
50
|
-
"
|
|
51
|
-
"@kysera/
|
|
54
|
+
"zod": "^3.24.2",
|
|
55
|
+
"@kysera/executor": "0.8.3",
|
|
56
|
+
"@kysera/repository": "0.8.3"
|
|
52
57
|
},
|
|
53
58
|
"keywords": [
|
|
54
59
|
"kysely",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Trail Module
|
|
3
|
+
*
|
|
4
|
+
* Provides audit logging for RLS policy decisions.
|
|
5
|
+
*
|
|
6
|
+
* @module @kysera/rls/audit
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export type {
|
|
11
|
+
AuditDecision,
|
|
12
|
+
RLSAuditEvent,
|
|
13
|
+
RLSAuditAdapter,
|
|
14
|
+
TableAuditConfig,
|
|
15
|
+
AuditConfig,
|
|
16
|
+
AuditQueryParams,
|
|
17
|
+
AuditStats,
|
|
18
|
+
ConsoleAuditAdapterOptions
|
|
19
|
+
} from './types.js'
|
|
20
|
+
|
|
21
|
+
// Adapters
|
|
22
|
+
export { ConsoleAuditAdapter, InMemoryAuditAdapter } from './types.js'
|
|
23
|
+
|
|
24
|
+
// Logger
|
|
25
|
+
export { AuditLogger, createAuditLogger } from './logger.js'
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Logger
|
|
3
|
+
*
|
|
4
|
+
* Manages audit event logging with buffering and filtering.
|
|
5
|
+
*
|
|
6
|
+
* @module @kysera/rls/audit/logger
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
RLSAuditEvent,
|
|
11
|
+
RLSAuditAdapter,
|
|
12
|
+
AuditConfig,
|
|
13
|
+
TableAuditConfig,
|
|
14
|
+
AuditDecision
|
|
15
|
+
} from './types.js'
|
|
16
|
+
import type { Operation, RLSContext } from '../policy/types.js'
|
|
17
|
+
import { rlsContext } from '../context/manager.js'
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Audit Logger
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Audit Logger
|
|
25
|
+
*
|
|
26
|
+
* Manages RLS audit event logging with buffering, filtering, and sampling.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const logger = new AuditLogger({
|
|
31
|
+
* adapter: new DatabaseAuditAdapter(db),
|
|
32
|
+
* bufferSize: 50,
|
|
33
|
+
* flushInterval: 5000,
|
|
34
|
+
* defaults: {
|
|
35
|
+
* logAllowed: false,
|
|
36
|
+
* logDenied: true,
|
|
37
|
+
* logFilters: false
|
|
38
|
+
* },
|
|
39
|
+
* tables: {
|
|
40
|
+
* sensitive_data: {
|
|
41
|
+
* logAllowed: true,
|
|
42
|
+
* includeContext: ['requestId', 'ipAddress']
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // Log an event
|
|
48
|
+
* await logger.logDecision('update', 'posts', 'allow', 'ownership-allow');
|
|
49
|
+
*
|
|
50
|
+
* // Ensure all events are flushed
|
|
51
|
+
* await logger.flush();
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export class AuditLogger {
|
|
55
|
+
private adapter: RLSAuditAdapter
|
|
56
|
+
private config: Required<Omit<AuditConfig, 'adapter' | 'tables' | 'onError'>> & {
|
|
57
|
+
tables: Record<string, TableAuditConfig>
|
|
58
|
+
onError?: (error: Error, events: RLSAuditEvent[]) => void
|
|
59
|
+
}
|
|
60
|
+
private buffer: RLSAuditEvent[] = []
|
|
61
|
+
private flushTimer: NodeJS.Timeout | null = null
|
|
62
|
+
private isShuttingDown = false
|
|
63
|
+
|
|
64
|
+
constructor(config: AuditConfig) {
|
|
65
|
+
this.adapter = config.adapter
|
|
66
|
+
const baseConfig = {
|
|
67
|
+
enabled: config.enabled ?? true,
|
|
68
|
+
defaults: config.defaults ?? {
|
|
69
|
+
logAllowed: false,
|
|
70
|
+
logDenied: true,
|
|
71
|
+
logFilters: false
|
|
72
|
+
},
|
|
73
|
+
tables: config.tables ?? {},
|
|
74
|
+
bufferSize: config.bufferSize ?? 100,
|
|
75
|
+
flushInterval: config.flushInterval ?? 5000,
|
|
76
|
+
async: config.async ?? true,
|
|
77
|
+
sampleRate: config.sampleRate ?? 1.0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.config = config.onError !== undefined
|
|
81
|
+
? { ...baseConfig, onError: config.onError }
|
|
82
|
+
: baseConfig
|
|
83
|
+
|
|
84
|
+
// Start flush timer
|
|
85
|
+
if (this.config.flushInterval > 0) {
|
|
86
|
+
this.startFlushTimer()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Log a policy decision
|
|
92
|
+
*
|
|
93
|
+
* @param operation - Database operation
|
|
94
|
+
* @param table - Table name
|
|
95
|
+
* @param decision - Decision result
|
|
96
|
+
* @param policyName - Name of the policy
|
|
97
|
+
* @param options - Additional options
|
|
98
|
+
*/
|
|
99
|
+
async logDecision(
|
|
100
|
+
operation: Operation,
|
|
101
|
+
table: string,
|
|
102
|
+
decision: AuditDecision,
|
|
103
|
+
policyName?: string,
|
|
104
|
+
options?: {
|
|
105
|
+
reason?: string
|
|
106
|
+
rowIds?: (string | number)[]
|
|
107
|
+
queryHash?: string
|
|
108
|
+
durationMs?: number
|
|
109
|
+
context?: Record<string, unknown>
|
|
110
|
+
}
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
if (!this.config.enabled || this.isShuttingDown) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check sampling
|
|
117
|
+
if (this.config.sampleRate < 1.0 && Math.random() > this.config.sampleRate) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Get table config
|
|
122
|
+
const tableConfig = this.getTableConfig(table)
|
|
123
|
+
|
|
124
|
+
// Check if this decision type should be logged
|
|
125
|
+
if (!this.shouldLog(decision, tableConfig)) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Get current RLS context
|
|
130
|
+
const ctx = rlsContext.getContextOrNull()
|
|
131
|
+
|
|
132
|
+
// Build event
|
|
133
|
+
const event = this.buildEvent(operation, table, decision, policyName, ctx, tableConfig, options)
|
|
134
|
+
|
|
135
|
+
// Apply custom filter if present
|
|
136
|
+
if (tableConfig.filter && !tableConfig.filter(event)) {
|
|
137
|
+
event.filtered = true
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Log the event
|
|
142
|
+
await this.logEvent(event)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Log an allow decision
|
|
147
|
+
*/
|
|
148
|
+
async logAllow(
|
|
149
|
+
operation: Operation,
|
|
150
|
+
table: string,
|
|
151
|
+
policyName?: string,
|
|
152
|
+
options?: {
|
|
153
|
+
reason?: string
|
|
154
|
+
rowIds?: (string | number)[]
|
|
155
|
+
context?: Record<string, unknown>
|
|
156
|
+
}
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
await this.logDecision(operation, table, 'allow', policyName, options)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Log a deny decision
|
|
163
|
+
*/
|
|
164
|
+
async logDeny(
|
|
165
|
+
operation: Operation,
|
|
166
|
+
table: string,
|
|
167
|
+
policyName?: string,
|
|
168
|
+
options?: {
|
|
169
|
+
reason?: string
|
|
170
|
+
rowIds?: (string | number)[]
|
|
171
|
+
context?: Record<string, unknown>
|
|
172
|
+
}
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
await this.logDecision(operation, table, 'deny', policyName, options)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Log a filter application
|
|
179
|
+
*/
|
|
180
|
+
async logFilter(
|
|
181
|
+
table: string,
|
|
182
|
+
policyName?: string,
|
|
183
|
+
options?: {
|
|
184
|
+
context?: Record<string, unknown>
|
|
185
|
+
}
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
await this.logDecision('read', table, 'filter', policyName, options)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Flush buffered events
|
|
192
|
+
*/
|
|
193
|
+
async flush(): Promise<void> {
|
|
194
|
+
if (this.buffer.length === 0) {
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const eventsToFlush = [...this.buffer]
|
|
199
|
+
this.buffer = []
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if (this.adapter.logBatch) {
|
|
203
|
+
await this.adapter.logBatch(eventsToFlush)
|
|
204
|
+
} else {
|
|
205
|
+
for (const event of eventsToFlush) {
|
|
206
|
+
await this.adapter.log(event)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.config.onError?.(error instanceof Error ? error : new Error(String(error)), eventsToFlush)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Close the logger
|
|
216
|
+
*/
|
|
217
|
+
async close(): Promise<void> {
|
|
218
|
+
this.isShuttingDown = true
|
|
219
|
+
|
|
220
|
+
if (this.flushTimer) {
|
|
221
|
+
clearInterval(this.flushTimer)
|
|
222
|
+
this.flushTimer = null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await this.flush()
|
|
226
|
+
await this.adapter.flush?.()
|
|
227
|
+
await this.adapter.close?.()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get buffer size
|
|
232
|
+
*/
|
|
233
|
+
get bufferSize(): number {
|
|
234
|
+
return this.buffer.length
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Check if logger is enabled
|
|
239
|
+
*/
|
|
240
|
+
get enabled(): boolean {
|
|
241
|
+
return this.config.enabled
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Enable or disable logging
|
|
246
|
+
*/
|
|
247
|
+
setEnabled(enabled: boolean): void {
|
|
248
|
+
this.config.enabled = enabled
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// Private Methods
|
|
253
|
+
// ============================================================================
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get table-specific config with defaults
|
|
257
|
+
*/
|
|
258
|
+
private getTableConfig(table: string): TableAuditConfig {
|
|
259
|
+
const tableOverride = this.config.tables[table]
|
|
260
|
+
return {
|
|
261
|
+
...this.config.defaults,
|
|
262
|
+
...tableOverride,
|
|
263
|
+
enabled: tableOverride?.enabled ?? true
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check if decision should be logged
|
|
269
|
+
*/
|
|
270
|
+
private shouldLog(decision: AuditDecision, tableConfig: TableAuditConfig): boolean {
|
|
271
|
+
if (!tableConfig.enabled) {
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
switch (decision) {
|
|
276
|
+
case 'allow':
|
|
277
|
+
return tableConfig.logAllowed ?? false
|
|
278
|
+
case 'deny':
|
|
279
|
+
return tableConfig.logDenied ?? true
|
|
280
|
+
case 'filter':
|
|
281
|
+
return tableConfig.logFilters ?? false
|
|
282
|
+
default:
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Build audit event
|
|
289
|
+
*/
|
|
290
|
+
private buildEvent(
|
|
291
|
+
operation: Operation,
|
|
292
|
+
table: string,
|
|
293
|
+
decision: AuditDecision,
|
|
294
|
+
policyName: string | undefined,
|
|
295
|
+
ctx: RLSContext | null,
|
|
296
|
+
tableConfig: TableAuditConfig,
|
|
297
|
+
options?: {
|
|
298
|
+
reason?: string
|
|
299
|
+
rowIds?: (string | number)[]
|
|
300
|
+
queryHash?: string
|
|
301
|
+
durationMs?: number
|
|
302
|
+
context?: Record<string, unknown>
|
|
303
|
+
}
|
|
304
|
+
): RLSAuditEvent {
|
|
305
|
+
const event: RLSAuditEvent = {
|
|
306
|
+
timestamp: new Date(),
|
|
307
|
+
userId: ctx?.auth.userId ?? 'anonymous',
|
|
308
|
+
operation,
|
|
309
|
+
table,
|
|
310
|
+
decision
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Add tenant ID if present
|
|
314
|
+
if (ctx?.auth.tenantId !== undefined) {
|
|
315
|
+
event.tenantId = ctx.auth.tenantId
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Add policy name
|
|
319
|
+
if (policyName) {
|
|
320
|
+
event.policyName = policyName
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Add options
|
|
324
|
+
if (options?.reason) {
|
|
325
|
+
event.reason = options.reason
|
|
326
|
+
}
|
|
327
|
+
if (options?.rowIds && options.rowIds.length > 0) {
|
|
328
|
+
event.rowIds = options.rowIds
|
|
329
|
+
}
|
|
330
|
+
if (options?.queryHash) {
|
|
331
|
+
event.queryHash = options.queryHash
|
|
332
|
+
}
|
|
333
|
+
if (options?.durationMs !== undefined) {
|
|
334
|
+
event.durationMs = options.durationMs
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Add request context
|
|
338
|
+
if (ctx?.request) {
|
|
339
|
+
if (ctx.request.requestId) {
|
|
340
|
+
event.requestId = ctx.request.requestId
|
|
341
|
+
}
|
|
342
|
+
if (ctx.request.ipAddress) {
|
|
343
|
+
event.ipAddress = ctx.request.ipAddress
|
|
344
|
+
}
|
|
345
|
+
if (ctx.request.userAgent) {
|
|
346
|
+
event.userAgent = ctx.request.userAgent
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Build context
|
|
351
|
+
const context = this.buildContext(ctx, tableConfig, options?.context)
|
|
352
|
+
if (context !== undefined) {
|
|
353
|
+
event.context = context
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return event
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Build context object with filtering
|
|
361
|
+
*/
|
|
362
|
+
private buildContext(
|
|
363
|
+
ctx: RLSContext | null,
|
|
364
|
+
tableConfig: TableAuditConfig,
|
|
365
|
+
additionalContext?: Record<string, unknown>
|
|
366
|
+
): Record<string, unknown> | undefined {
|
|
367
|
+
const context: Record<string, unknown> = {}
|
|
368
|
+
|
|
369
|
+
// Add roles
|
|
370
|
+
if (ctx?.auth.roles && ctx.auth.roles.length > 0) {
|
|
371
|
+
context['roles'] = ctx.auth.roles
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Add organization IDs if present
|
|
375
|
+
if (ctx?.auth.organizationIds && ctx.auth.organizationIds.length > 0) {
|
|
376
|
+
context['organizationIds'] = ctx.auth.organizationIds
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Add meta if present
|
|
380
|
+
if (ctx?.meta && typeof ctx.meta === 'object') {
|
|
381
|
+
Object.assign(context, ctx.meta)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Add additional context
|
|
385
|
+
if (additionalContext) {
|
|
386
|
+
Object.assign(context, additionalContext)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Apply include/exclude filters
|
|
390
|
+
let filteredContext = context
|
|
391
|
+
|
|
392
|
+
if (tableConfig.includeContext && tableConfig.includeContext.length > 0) {
|
|
393
|
+
filteredContext = {}
|
|
394
|
+
for (const key of tableConfig.includeContext) {
|
|
395
|
+
if (key in context) {
|
|
396
|
+
filteredContext[key] = context[key]
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (tableConfig.excludeContext && tableConfig.excludeContext.length > 0) {
|
|
402
|
+
for (const key of tableConfig.excludeContext) {
|
|
403
|
+
// Use destructuring to avoid dynamic delete
|
|
404
|
+
const { [key]: _, ...rest } = filteredContext
|
|
405
|
+
filteredContext = rest
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return Object.keys(filteredContext).length > 0 ? filteredContext : undefined
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Log event to buffer or directly
|
|
414
|
+
*/
|
|
415
|
+
private async logEvent(event: RLSAuditEvent): Promise<void> {
|
|
416
|
+
if (this.config.async && this.config.bufferSize > 0) {
|
|
417
|
+
// Buffered async logging
|
|
418
|
+
this.buffer.push(event)
|
|
419
|
+
|
|
420
|
+
if (this.buffer.length >= this.config.bufferSize) {
|
|
421
|
+
// Buffer full, flush now
|
|
422
|
+
await this.flush()
|
|
423
|
+
}
|
|
424
|
+
} else if (this.config.async) {
|
|
425
|
+
// Async fire-and-forget
|
|
426
|
+
this.adapter.log(event).catch((error: unknown) => {
|
|
427
|
+
this.config.onError?.(error instanceof Error ? error : new Error(String(error)), [event])
|
|
428
|
+
})
|
|
429
|
+
} else {
|
|
430
|
+
// Synchronous logging
|
|
431
|
+
try {
|
|
432
|
+
await this.adapter.log(event)
|
|
433
|
+
} catch (error) {
|
|
434
|
+
this.config.onError?.(error instanceof Error ? error : new Error(String(error)), [event])
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Start the flush timer
|
|
441
|
+
*/
|
|
442
|
+
private startFlushTimer(): void {
|
|
443
|
+
this.flushTimer = setInterval(() => {
|
|
444
|
+
this.flush().catch((error: unknown) => {
|
|
445
|
+
this.config.onError?.(error instanceof Error ? error : new Error(String(error)), [...this.buffer])
|
|
446
|
+
})
|
|
447
|
+
}, this.config.flushInterval)
|
|
448
|
+
|
|
449
|
+
// Don't block process exit
|
|
450
|
+
if (this.flushTimer.unref) {
|
|
451
|
+
this.flushTimer.unref()
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ============================================================================
|
|
457
|
+
// Factory Function
|
|
458
|
+
// ============================================================================
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Create an audit logger
|
|
462
|
+
*/
|
|
463
|
+
export function createAuditLogger(config: AuditConfig): AuditLogger {
|
|
464
|
+
return new AuditLogger(config)
|
|
465
|
+
}
|