@geekmidas/audit 0.0.1

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.
Files changed (47) hide show
  1. package/TECHNICAL.md +937 -0
  2. package/dist/Auditor-CZ8lkASv.d.cts +260 -0
  3. package/dist/Auditor-ii62d_pF.d.mts +260 -0
  4. package/dist/Auditor.cjs +0 -0
  5. package/dist/Auditor.d.cts +2 -0
  6. package/dist/Auditor.d.mts +2 -0
  7. package/dist/Auditor.mjs +0 -0
  8. package/dist/DefaultAuditor-1HDUGMub.cjs +124 -0
  9. package/dist/DefaultAuditor-1HDUGMub.cjs.map +1 -0
  10. package/dist/DefaultAuditor-B9Unin1g.d.mts +59 -0
  11. package/dist/DefaultAuditor-BAVnNmRh.mjs +96 -0
  12. package/dist/DefaultAuditor-BAVnNmRh.mjs.map +1 -0
  13. package/dist/DefaultAuditor-Dqc4UZA1.d.cts +59 -0
  14. package/dist/DefaultAuditor.cjs +3 -0
  15. package/dist/DefaultAuditor.d.cts +4 -0
  16. package/dist/DefaultAuditor.d.mts +4 -0
  17. package/dist/DefaultAuditor.mjs +3 -0
  18. package/dist/index.cjs +3 -0
  19. package/dist/index.d.cts +4 -0
  20. package/dist/index.d.mts +4 -0
  21. package/dist/index.mjs +3 -0
  22. package/dist/kysely.cjs +193 -0
  23. package/dist/kysely.cjs.map +1 -0
  24. package/dist/kysely.d.cts +165 -0
  25. package/dist/kysely.d.mts +165 -0
  26. package/dist/kysely.mjs +191 -0
  27. package/dist/kysely.mjs.map +1 -0
  28. package/dist/storage-BtFY7Rha.d.cts +102 -0
  29. package/dist/storage-DnAMfOA7.d.mts +102 -0
  30. package/dist/storage.cjs +0 -0
  31. package/dist/storage.d.cts +3 -0
  32. package/dist/storage.d.mts +3 -0
  33. package/dist/storage.mjs +0 -0
  34. package/dist/types.cjs +0 -0
  35. package/dist/types.d.cts +2 -0
  36. package/dist/types.d.mts +2 -0
  37. package/dist/types.mjs +0 -0
  38. package/package.json +34 -0
  39. package/src/Auditor.ts +157 -0
  40. package/src/DefaultAuditor.ts +141 -0
  41. package/src/__tests__/DefaultAuditor.spec.ts +420 -0
  42. package/src/__tests__/KyselyAuditStorage.integration.spec.ts +517 -0
  43. package/src/__tests__/KyselyAuditStorage.spec.ts +359 -0
  44. package/src/index.ts +23 -0
  45. package/src/kysely.ts +391 -0
  46. package/src/storage.ts +101 -0
  47. package/src/types.ts +147 -0
package/src/kysely.ts ADDED
@@ -0,0 +1,391 @@
1
+ import type {
2
+ ControlledTransaction,
3
+ IsolationLevel,
4
+ Kysely,
5
+ Transaction,
6
+ } from 'kysely';
7
+ import type { AuditQueryOptions, AuditStorage } from './storage';
8
+ import type { AuditRecord } from './types';
9
+
10
+ /**
11
+ * Minimal interface for transaction-aware audit flushing.
12
+ * Use this when you need to flush audits within a database transaction.
13
+ *
14
+ * @template TTransaction - Transaction type (e.g., Kysely Transaction)
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { withAuditableTransaction } from '@geekmidas/audit/kysely';
19
+ * import type { TransactionAwareAuditor } from '@geekmidas/audit/kysely';
20
+ *
21
+ * const result = await withAuditableTransaction(
22
+ * db,
23
+ * auditor as TransactionAwareAuditor<Transaction<DB>>,
24
+ * async (trx) => {
25
+ * // Your transactional operations
26
+ * return result;
27
+ * },
28
+ * );
29
+ * ```
30
+ */
31
+ export interface TransactionAwareAuditor<TTransaction = unknown> {
32
+ /** Register the transaction with the auditor for use during flush */
33
+ setTransaction(trx: TTransaction): void;
34
+ /** Flush all pending audits, optionally within a transaction */
35
+ flush(trx?: TTransaction): Promise<void>;
36
+ }
37
+
38
+ export interface TransactionSettings {
39
+ isolationLevel?: IsolationLevel;
40
+ }
41
+
42
+ export type DatabaseConnection<T> =
43
+ | ControlledTransaction<T>
44
+ | Kysely<T>
45
+ | Transaction<T>;
46
+
47
+ /**
48
+ * Execute a callback within a database transaction with automatic audit handling.
49
+ *
50
+ * This wrapper ensures that:
51
+ * 1. The transaction is automatically registered with the auditor
52
+ * 2. Manual audits (via `auditor.audit()`) are flushed BEFORE the transaction commits
53
+ * 3. If audit flush fails, the entire transaction rolls back
54
+ * 4. If the callback fails, audits are NOT written (atomic consistency)
55
+ *
56
+ * **Note:** Declarative audits (defined via `.audit([...])` on the endpoint builder)
57
+ * are processed AFTER the handler returns, so they run outside this transaction.
58
+ * If you need all audits to be atomic with your database operations, use manual
59
+ * audits via `auditor.audit()` inside this wrapper.
60
+ *
61
+ * @param db - Database connection (Kysely, Transaction, or ControlledTransaction)
62
+ * @param auditor - Auditor instance that will receive the transaction
63
+ * @param cb - Callback to execute within the transaction
64
+ * @param settings - Optional transaction settings (isolation level)
65
+ * @returns The result of the callback
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * import { withAuditableTransaction } from '@geekmidas/audit/kysely';
70
+ *
71
+ * const result = await withAuditableTransaction(
72
+ * services.database,
73
+ * auditor,
74
+ * async (trx) => {
75
+ * const user = await trx
76
+ * .insertInto('users')
77
+ * .values(data)
78
+ * .returningAll()
79
+ * .executeTakeFirstOrThrow();
80
+ *
81
+ * // Manual audits are atomic with the transaction
82
+ * auditor.audit('user.created', { userId: user.id, email: user.email });
83
+ *
84
+ * return user;
85
+ * },
86
+ * );
87
+ * // Audits are automatically flushed inside the transaction before commit
88
+ * ```
89
+ */
90
+ export async function withAuditableTransaction<DB, T>(
91
+ db: DatabaseConnection<DB>,
92
+ auditor: TransactionAwareAuditor<Transaction<DB>>,
93
+ cb: (trx: Transaction<DB>) => Promise<T>,
94
+ settings?: TransactionSettings,
95
+ ): Promise<T> {
96
+ const execute = async (trx: Transaction<DB>): Promise<T> => {
97
+ // Register transaction with auditor
98
+ auditor.setTransaction(trx);
99
+
100
+ // Execute the callback
101
+ const result = await cb(trx);
102
+
103
+ // Flush audits BEFORE transaction commits
104
+ // If this fails, the transaction will roll back
105
+ await auditor.flush(trx);
106
+
107
+ return result;
108
+ };
109
+
110
+ // If already in a transaction, just run with it
111
+ if (db.isTransaction) {
112
+ return execute(db as Transaction<DB>);
113
+ }
114
+
115
+ const builder = db.transaction();
116
+
117
+ if (settings?.isolationLevel) {
118
+ return builder.setIsolationLevel(settings.isolationLevel).execute(execute);
119
+ }
120
+
121
+ return builder.execute(execute);
122
+ }
123
+
124
+ /**
125
+ * Database table interface for audit records.
126
+ * Use this to define your audit_logs table in your Kysely database schema.
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * interface Database {
131
+ * audit_logs: AuditLogTable;
132
+ * // ... other tables
133
+ * }
134
+ * ```
135
+ */
136
+ export interface AuditLogTable {
137
+ id: string;
138
+ type: string;
139
+ operation: string;
140
+ table: string | null;
141
+ entityId: string | null;
142
+ oldValues: string | null;
143
+ newValues: string | null;
144
+ payload: string | null;
145
+ timestamp: Date;
146
+ actorId: string | null;
147
+ actorType: string | null;
148
+ actorData: string | null;
149
+ metadata: string | null;
150
+ }
151
+
152
+ /**
153
+ * Configuration for KyselyAuditStorage.
154
+ */
155
+ export interface KyselyAuditStorageConfig<DB> {
156
+ /** Kysely database instance */
157
+ db: Kysely<DB>;
158
+ /** Table name for audit logs (must be a key in DB that extends AuditLogTable) */
159
+ tableName: keyof DB & string;
160
+ }
161
+
162
+ /**
163
+ * Kysely-based audit storage implementation.
164
+ * Stores audit records in a database table using Kysely.
165
+ *
166
+ * @template DB - Your Kysely database schema
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * interface Database {
171
+ * audit_logs: AuditLogTable;
172
+ * }
173
+ *
174
+ * const storage = new KyselyAuditStorage({
175
+ * db: kyselyDb,
176
+ * tableName: 'audit_logs',
177
+ * });
178
+ *
179
+ * const auditor = new DefaultAuditor({
180
+ * actor: { id: 'user-123', type: 'user' },
181
+ * storage,
182
+ * });
183
+ * ```
184
+ */
185
+ export class KyselyAuditStorage<DB> implements AuditStorage {
186
+ private readonly db: Kysely<DB>;
187
+ private readonly tableName: keyof DB & string;
188
+
189
+ constructor(config: KyselyAuditStorageConfig<DB>) {
190
+ this.db = config.db;
191
+ this.tableName = config.tableName;
192
+ }
193
+
194
+ async write(records: AuditRecord[], trx?: unknown): Promise<void> {
195
+ if (records.length === 0) {
196
+ return;
197
+ }
198
+
199
+ const db = (trx as Transaction<DB>) ?? this.db;
200
+ const rows = records.map((record) => this.toRow(record));
201
+
202
+ await (db as any)
203
+ .insertInto(this.tableName)
204
+ .values(rows)
205
+ .execute();
206
+ }
207
+
208
+ async query(options: AuditQueryOptions): Promise<AuditRecord[]> {
209
+ let query = (this.db as any)
210
+ .selectFrom(this.tableName)
211
+ .selectAll();
212
+
213
+ query = this.applyFilters(query, options);
214
+
215
+ // Ordering
216
+ const orderBy = options.orderBy ?? 'timestamp';
217
+ const orderDirection = options.orderDirection ?? 'desc';
218
+ query = query.orderBy(
219
+ orderBy === 'timestamp' ? 'timestamp' : 'type',
220
+ orderDirection,
221
+ );
222
+
223
+ // Pagination
224
+ if (options.limit !== undefined) {
225
+ query = query.limit(options.limit);
226
+ }
227
+ if (options.offset !== undefined) {
228
+ query = query.offset(options.offset);
229
+ }
230
+
231
+ const rows = await query.execute();
232
+ return rows.map((row: AuditLogTable) => this.fromRow(row));
233
+ }
234
+
235
+ async count(
236
+ options: Omit<AuditQueryOptions, 'limit' | 'offset'>,
237
+ ): Promise<number> {
238
+ let query = (this.db as any)
239
+ .selectFrom(this.tableName)
240
+ .select((eb: any) => eb.fn.count('id').as('count'));
241
+
242
+ query = this.applyFilters(query, options);
243
+
244
+ const result = await query.executeTakeFirst();
245
+ return Number(result?.count ?? 0);
246
+ }
247
+
248
+ /**
249
+ * Get the Kysely database instance for transactional operations.
250
+ * Used by endpoint adaptors to automatically wrap handlers in transactions.
251
+ */
252
+ getDatabase(): Kysely<DB> {
253
+ return this.db;
254
+ }
255
+
256
+ private applyFilters(query: any, options: AuditQueryOptions): any {
257
+ // Type filter
258
+ if (options.type !== undefined) {
259
+ if (Array.isArray(options.type)) {
260
+ query = query.where('type', 'in', options.type);
261
+ } else {
262
+ query = query.where('type', '=', options.type);
263
+ }
264
+ }
265
+
266
+ // Entity ID filter
267
+ if (options.entityId !== undefined) {
268
+ const entityId =
269
+ typeof options.entityId === 'string'
270
+ ? options.entityId
271
+ : JSON.stringify(options.entityId);
272
+ query = query.where('entityId', '=', entityId);
273
+ }
274
+
275
+ // Table filter
276
+ if (options.table !== undefined) {
277
+ query = query.where('table', '=', options.table);
278
+ }
279
+
280
+ // Actor ID filter
281
+ if (options.actorId !== undefined) {
282
+ query = query.where('actorId', '=', options.actorId);
283
+ }
284
+
285
+ // Date range filters
286
+ if (options.from !== undefined) {
287
+ query = query.where('timestamp', '>=', options.from);
288
+ }
289
+ if (options.to !== undefined) {
290
+ query = query.where('timestamp', '<=', options.to);
291
+ }
292
+
293
+ return query;
294
+ }
295
+
296
+ private toRow(record: AuditRecord): AuditLogTable {
297
+ return {
298
+ id: record.id,
299
+ type: record.type,
300
+ operation: record.operation,
301
+ table: record.table ?? null,
302
+ entityId:
303
+ record.entityId === undefined
304
+ ? null
305
+ : typeof record.entityId === 'string'
306
+ ? record.entityId
307
+ : JSON.stringify(record.entityId),
308
+ oldValues:
309
+ record.oldValues !== undefined
310
+ ? JSON.stringify(record.oldValues)
311
+ : null,
312
+ newValues:
313
+ record.newValues !== undefined
314
+ ? JSON.stringify(record.newValues)
315
+ : null,
316
+ payload:
317
+ record.payload !== undefined ? JSON.stringify(record.payload) : null,
318
+ timestamp: record.timestamp,
319
+ actorId: record.actor?.id ?? null,
320
+ actorType: record.actor?.type ?? null,
321
+ actorData:
322
+ record.actor !== undefined
323
+ ? JSON.stringify(this.getActorData(record.actor))
324
+ : null,
325
+ metadata:
326
+ record.metadata !== undefined ? JSON.stringify(record.metadata) : null,
327
+ };
328
+ }
329
+
330
+ private fromRow(row: AuditLogTable): AuditRecord {
331
+ const actor =
332
+ row.actorId !== null || row.actorType !== null
333
+ ? {
334
+ id: row.actorId ?? undefined,
335
+ type: row.actorType ?? undefined,
336
+ ...(row.actorData ? this.parseJson(row.actorData) : {}),
337
+ }
338
+ : undefined;
339
+
340
+ return {
341
+ id: row.id,
342
+ type: row.type,
343
+ operation: row.operation as AuditRecord['operation'],
344
+ table: row.table ?? undefined,
345
+ entityId: row.entityId
346
+ ? this.parseEntityId(row.entityId)
347
+ : undefined,
348
+ oldValues: row.oldValues
349
+ ? this.parseJson(row.oldValues)
350
+ : undefined,
351
+ newValues: row.newValues
352
+ ? this.parseJson(row.newValues)
353
+ : undefined,
354
+ payload: row.payload ? this.parseJson(row.payload) : undefined,
355
+ timestamp: row.timestamp,
356
+ actor,
357
+ metadata: row.metadata ? this.parseJson(row.metadata) : undefined,
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Parse a JSON value that may already be parsed (e.g., from jsonb columns).
363
+ */
364
+ private parseJson(value: string | object): Record<string, unknown> {
365
+ if (typeof value === 'object') {
366
+ return value as Record<string, unknown>;
367
+ }
368
+ return JSON.parse(value);
369
+ }
370
+
371
+ private getActorData(
372
+ actor: NonNullable<AuditRecord['actor']>,
373
+ ): Record<string, unknown> {
374
+ const { id, type, ...rest } = actor;
375
+ return rest;
376
+ }
377
+
378
+ private parseEntityId(
379
+ entityId: string,
380
+ ): string | Record<string, unknown> {
381
+ try {
382
+ const parsed = JSON.parse(entityId);
383
+ if (typeof parsed === 'object' && parsed !== null) {
384
+ return parsed;
385
+ }
386
+ return entityId;
387
+ } catch {
388
+ return entityId;
389
+ }
390
+ }
391
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,101 @@
1
+ import type { AuditRecord } from './types';
2
+
3
+ /**
4
+ * Options for querying audit records.
5
+ */
6
+ export interface AuditQueryOptions {
7
+ /** Filter by audit type */
8
+ type?: string | string[];
9
+ /** Filter by entity ID */
10
+ entityId?: string | Record<string, unknown>;
11
+ /** Filter by table name */
12
+ table?: string;
13
+ /** Filter by actor ID */
14
+ actorId?: string;
15
+ /** Filter by date range (start) */
16
+ from?: Date;
17
+ /** Filter by date range (end) */
18
+ to?: Date;
19
+ /** Limit number of results */
20
+ limit?: number;
21
+ /** Offset for pagination */
22
+ offset?: number;
23
+ /** Sort order */
24
+ orderBy?: 'timestamp' | 'type';
25
+ /** Sort direction */
26
+ orderDirection?: 'asc' | 'desc';
27
+ }
28
+
29
+ /**
30
+ * Interface for audit storage backends.
31
+ * Implement this to store audits in your preferred location.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * // Same database storage (recommended for transactions)
36
+ * const auditStorage: AuditStorage = {
37
+ * async write(records, trx) {
38
+ * const db = trx ?? getDatabase();
39
+ * await db.insertInto('audit_logs').values(records).execute();
40
+ * },
41
+ * };
42
+ *
43
+ * // External audit service
44
+ * const externalAuditStorage: AuditStorage = {
45
+ * async write(records) {
46
+ * await fetch('https://audit.example.com/api/audits', {
47
+ * method: 'POST',
48
+ * body: JSON.stringify({ records }),
49
+ * });
50
+ * },
51
+ * };
52
+ * ```
53
+ */
54
+ export interface AuditStorage {
55
+ /**
56
+ * Write audit records to storage.
57
+ * Called by Auditor.flush() to persist collected audits.
58
+ *
59
+ * @param records - The audit records to write
60
+ * @param trx - Optional transaction context (for same-DB storage)
61
+ */
62
+ write(records: AuditRecord[], trx?: unknown): Promise<void>;
63
+
64
+ /**
65
+ * Optional: Query audit records for retrieval.
66
+ * Implement this for audit log viewing/searching.
67
+ *
68
+ * @param options - Query filters and pagination
69
+ * @returns Matching audit records
70
+ */
71
+ query?(options: AuditQueryOptions): Promise<AuditRecord[]>;
72
+
73
+ /**
74
+ * Optional: Count audit records matching filters.
75
+ * Useful for pagination.
76
+ *
77
+ * @param options - Query filters (limit/offset ignored)
78
+ * @returns Count of matching records
79
+ */
80
+ count?(options: Omit<AuditQueryOptions, 'limit' | 'offset'>): Promise<number>;
81
+
82
+ /**
83
+ * Optional: Get the database connection for transactional audit writes.
84
+ * When implemented, the endpoint adaptor can automatically wrap handlers
85
+ * in a transaction, ensuring audits are atomic with other database operations.
86
+ *
87
+ * @returns Database connection (e.g., Kysely instance)
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * class KyselyAuditStorage implements AuditStorage {
92
+ * constructor(private db: Kysely<DB>) {}
93
+ *
94
+ * getDatabase() {
95
+ * return this.db;
96
+ * }
97
+ * }
98
+ * ```
99
+ */
100
+ getDatabase?(): unknown;
101
+ }
package/src/types.ts ADDED
@@ -0,0 +1,147 @@
1
+ import type { InferStandardSchema } from '@geekmidas/schema';
2
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
3
+
4
+ /**
5
+ * Represents an auditable action with a type and payload.
6
+ * Similar to PublishableMessage in @geekmidas/events.
7
+ *
8
+ * @template TType - The audit type/name (e.g., 'user.created')
9
+ * @template TPayload - The audit payload data
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * type AppAuditAction =
14
+ * | AuditableAction<'user.created', { userId: string; email: string }>
15
+ * | AuditableAction<'user.updated', { userId: string; changes: string[] }>
16
+ * | AuditableAction<'order.placed', { orderId: string; total: number }>;
17
+ * ```
18
+ */
19
+ export type AuditableAction<TType extends string, TPayload = unknown> = {
20
+ type: TType;
21
+ payload: TPayload;
22
+ };
23
+
24
+ /**
25
+ * Extract the type string from an AuditableAction union.
26
+ */
27
+ export type ExtractAuditType<T extends AuditableAction<string, unknown>> =
28
+ T extends AuditableAction<infer TType, unknown> ? TType : never;
29
+
30
+ /**
31
+ * Extract the payload for a specific audit type from an AuditableAction union.
32
+ */
33
+ export type ExtractAuditPayload<
34
+ T extends AuditableAction<string, unknown>,
35
+ TType extends ExtractAuditType<T>,
36
+ > = T extends AuditableAction<TType, infer TPayload> ? TPayload : never;
37
+
38
+ /**
39
+ * Audit operation types for database auditing.
40
+ */
41
+ export type AuditOperation = 'INSERT' | 'UPDATE' | 'DELETE' | 'CUSTOM';
42
+
43
+ /**
44
+ * Represents the actor who performed an audited action.
45
+ */
46
+ export interface AuditActor {
47
+ /** Unique identifier for the actor (user ID, service ID, etc.) */
48
+ id?: string;
49
+ /** Type of actor ('user', 'system', 'service', etc.) */
50
+ type?: string;
51
+ /** Additional actor properties */
52
+ [key: string]: unknown;
53
+ }
54
+
55
+ /**
56
+ * Metadata associated with an audit record.
57
+ */
58
+ export interface AuditMetadata {
59
+ /** Request correlation ID */
60
+ requestId?: string;
61
+ /** Which endpoint was called */
62
+ endpoint?: string;
63
+ /** HTTP method */
64
+ method?: string;
65
+ /** Client IP address */
66
+ ip?: string;
67
+ /** Client user agent */
68
+ userAgent?: string;
69
+ /** Additional metadata */
70
+ [key: string]: unknown;
71
+ }
72
+
73
+ /**
74
+ * A complete audit record representing a tracked action.
75
+ */
76
+ export interface AuditRecord<TPayload = unknown> {
77
+ /** Unique identifier for this audit record */
78
+ id: string;
79
+ /** Audit type (e.g., 'user.created', 'order.placed') */
80
+ type: string;
81
+ /** Operation type for database audits */
82
+ operation: AuditOperation;
83
+ /** Database table name (for database operations) */
84
+ table?: string;
85
+ /** Entity primary key(s) */
86
+ entityId?: string | Record<string, unknown>;
87
+ /** Previous state (for UPDATE/DELETE) */
88
+ oldValues?: Record<string, unknown>;
89
+ /** New state (for INSERT/UPDATE) */
90
+ newValues?: Record<string, unknown>;
91
+ /** Custom payload (for CUSTOM operations) */
92
+ payload?: TPayload;
93
+ /** When the audit was recorded */
94
+ timestamp: Date;
95
+ /** Who performed the action */
96
+ actor?: AuditActor;
97
+ /** Request context */
98
+ metadata?: AuditMetadata;
99
+ }
100
+
101
+ /**
102
+ * Options for manual audit calls.
103
+ */
104
+ export interface AuditOptions {
105
+ /** Entity primary key(s) for easier querying */
106
+ entityId?: string | Record<string, unknown>;
107
+ /** Database table name */
108
+ table?: string;
109
+ /** Operation type (defaults to 'CUSTOM') */
110
+ operation?: AuditOperation;
111
+ /** Previous state */
112
+ oldValues?: Record<string, unknown>;
113
+ /** New state */
114
+ newValues?: Record<string, unknown>;
115
+ }
116
+
117
+ /**
118
+ * Mapped audit definition for declarative auditing.
119
+ * Similar to MappedEvent in @geekmidas/events.
120
+ */
121
+ export interface MappedAudit<
122
+ TAuditAction extends AuditableAction<string, unknown>,
123
+ TOutput extends StandardSchemaV1 | undefined = undefined,
124
+ > {
125
+ /** The audit type - must be a valid type from the AuditableAction union */
126
+ type: ExtractAuditType<TAuditAction>;
127
+ /** Function to extract payload from the response */
128
+ payload: (
129
+ response: InferStandardSchema<TOutput>,
130
+ ) => ExtractAuditPayload<TAuditAction, ExtractAuditType<TAuditAction>>;
131
+ /** Optional condition - only audit if this returns true */
132
+ when?: (response: InferStandardSchema<TOutput>) => boolean;
133
+ /** Optional entity ID extractor for easier querying */
134
+ entityId?: (
135
+ response: InferStandardSchema<TOutput>,
136
+ ) => string | Record<string, unknown>;
137
+ /** Optional table name for database association */
138
+ table?: string;
139
+ }
140
+
141
+ /**
142
+ * Extract the AuditableAction type from an Auditor.
143
+ */
144
+ export type ExtractAuditorAction<T> = T extends Auditor<infer A> ? A : never;
145
+
146
+ // Forward declaration for Auditor type extraction
147
+ import type { Auditor } from './Auditor';