@usebetterdev/audit-core 0.4.0-beta.4 → 0.5.0

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.d.ts CHANGED
@@ -104,7 +104,39 @@ declare class AuditQueryBuilder {
104
104
  list(): Promise<AuditQueryResult>;
105
105
  }
106
106
 
107
+ /** Options for `createExportResponse()`. */
108
+ interface ExportResponseOptions {
109
+ /** Output format. Default: `"csv"`. */
110
+ format?: "csv" | "json";
111
+ /** JSON output style. Default: `"ndjson"`. */
112
+ jsonStyle?: "ndjson" | "array";
113
+ /** Rows per database round-trip. */
114
+ batchSize?: number;
115
+ /** CSV delimiter character. */
116
+ csvDelimiter?: string;
117
+ /** Optional query builder to filter exported entries. */
118
+ query?: AuditQueryBuilder;
119
+ /** Custom filename stem (without extension). Default: `"audit-export-YYYY-MM-DD"`. */
120
+ filename?: string;
121
+ }
122
+ /**
123
+ * Create a streaming `Response` from the audit export engine.
124
+ *
125
+ * Uses `TransformStream` + `TextEncoderStream` to bridge the string-based
126
+ * `runExport()` output to a binary `ReadableStream` suitable for `new Response()`.
127
+ * The export runs asynchronously — the Response is returned immediately so the
128
+ * first byte can arrive before all rows are read.
129
+ */
130
+ declare function createExportResponse(executor: QueryExecutor, options?: ExportResponseOptions): Response;
131
+
107
132
  type AuditOperation = "INSERT" | "UPDATE" | "DELETE";
133
+ /** Retention policy controlling automatic and CLI-driven purge of old audit logs. */
134
+ interface RetentionPolicy {
135
+ /** Purge entries older than this many days. Must be a positive integer. */
136
+ days: number;
137
+ /** When set, only purge logs for these specific tables. */
138
+ tables?: string[];
139
+ }
108
140
  type AuditSeverity = "low" | "medium" | "high" | "critical";
109
141
  /** A single audit log entry as stored in the database. */
110
142
  interface AuditLog {
@@ -225,6 +257,8 @@ interface BetterAuditConfig {
225
257
  onError?: (error: unknown) => void;
226
258
  /** Console integration. Pass a BetterConsoleInstance to register audit dashboard endpoints. */
227
259
  console?: ConsoleRegistration;
260
+ /** Optional retention policy. When set, enables purge CLI and auto-purge scheduler. */
261
+ retention?: RetentionPolicy;
228
262
  }
229
263
  /** Input to a manual captureLog call. Context fields override the current AsyncLocalStorage scope. */
230
264
  interface CaptureLogInput {
@@ -288,10 +322,50 @@ interface EnrichmentConfig {
288
322
  */
289
323
  include?: string[];
290
324
  }
325
+ /** Options for `audit.export()`. */
326
+ interface ExportOptions {
327
+ /** Output format. */
328
+ format: "csv" | "json";
329
+ /** Optional query builder to filter exported entries. */
330
+ query?: AuditQueryBuilder;
331
+ /**
332
+ * Output target.
333
+ * - `WritableStream<string>`: WHATWG Web Streams API (Node 18+, Bun, Deno)
334
+ * - `'string'`: buffer in memory and return the full output via `ExportResult`
335
+ */
336
+ output: WritableStream<string> | "string";
337
+ /** Rows per database round-trip. Default 500. */
338
+ batchSize?: number;
339
+ /** CSV delimiter character (must be exactly one character). Default `','`. */
340
+ csvDelimiter?: string;
341
+ /** JSON output style. `'ndjson'` (default) for streaming, `'array'` for pretty-printed. */
342
+ jsonStyle?: "ndjson" | "array";
343
+ }
344
+ /** Result returned when `audit.export()` completes. */
345
+ interface ExportResult {
346
+ rowCount: number;
347
+ /** Present only when `output` is `'string'`. */
348
+ data?: string;
349
+ }
291
350
  interface BetterAuditInstance {
292
351
  captureLog(input: CaptureLogInput): Promise<void>;
293
352
  /** Returns a fluent query builder. Requires `queryAdapter` in config. */
294
353
  query(): AuditQueryBuilder;
354
+ /**
355
+ * Export audit log entries as CSV or JSON.
356
+ *
357
+ * Streams rows in batches via cursor pagination so memory stays flat
358
+ * regardless of export size. Requires `queryLogs` on the database adapter.
359
+ */
360
+ export(options: ExportOptions): Promise<ExportResult>;
361
+ /**
362
+ * Create a streaming HTTP `Response` for downloading audit logs.
363
+ *
364
+ * Returns a standard `Response` with correct Content-Type, Content-Disposition,
365
+ * and Cache-Control headers. The body is a streaming `ReadableStream` backed
366
+ * by the export engine. Requires `queryLogs` on the database adapter.
367
+ */
368
+ exportResponse(options?: ExportResponseOptions): Response;
295
369
  withContext<T>(context: AuditContext, fn: () => Promise<T>): Promise<T>;
296
370
  /**
297
371
  * Register an enrichment config for a table/operation pair.
@@ -304,6 +378,8 @@ interface BetterAuditInstance {
304
378
  onBeforeLog(hook: BeforeLogHook): () => void;
305
379
  /** Register a hook that runs after each log is written. Returns a dispose function to unregister the hook. */
306
380
  onAfterLog(hook: AfterLogHook): () => void;
381
+ /** Returns the resolved retention policy, or undefined if none was configured. */
382
+ retentionPolicy(): RetentionPolicy | undefined;
307
383
  }
308
384
 
309
385
  declare function betterAudit(config: BetterAuditConfig): BetterAuditInstance;
@@ -380,6 +456,12 @@ declare const AUDIT_LOG_SCHEMA: AuditLogSchema;
380
456
  */
381
457
  declare function parseDuration(input: string, referenceDate?: Date): Date;
382
458
 
459
+ /**
460
+ * Core export engine. Fetches rows in cursor-paginated batches and writes
461
+ * them to the sink as CSV or JSON. Memory stays flat regardless of total rows.
462
+ */
463
+ declare function runExport(executor: QueryExecutor, options: ExportOptions): Promise<ExportResult>;
464
+
383
465
  /** Resolved enrichment config after merging all matching tiers. */
384
466
  interface ResolvedEnrichment {
385
467
  label?: string;
@@ -519,4 +601,4 @@ declare function fromHeader(headerName: string): ValueExtractor;
519
601
  */
520
602
  declare function handleMiddleware(extractor: ContextExtractor, request: Request, next: () => Promise<void>, options?: MiddlewareHandlerOptions): Promise<void>;
521
603
 
522
- export { AUDIT_LOG_SCHEMA, type AfterLogHook, type AuditApi, type AuditContext, type AuditDatabaseAdapter, type AuditLog, type AuditLogColumnName, type AuditLogSchema, type AuditOperation, AuditQueryBuilder, type AuditQueryFilters, type AuditQueryResult, type AuditQuerySpec, type AuditSeverity, type AuditStats, type BeforeLogHook, type BetterAuditConfig, type BetterAuditInstance, type CaptureLogInput, type ColumnDefinition, type ColumnType, type ConsoleQueryFilters, type ConsoleQueryResult, type ContextExtractor, type EnrichmentConfig, type EnrichmentDescriptionContext, type EnrichmentSummary, type MiddlewareHandlerOptions, type QueryExecutor, type ResourceFilter, type TimeFilter, type ValueExtractor, betterAudit, createAuditApi, createAuditConsoleEndpoints, fromBearerToken, fromCookie, fromHeader, getAuditContext, handleMiddleware, mergeAuditContext, normalizeInput, parseDuration, runWithAuditContext };
604
+ export { AUDIT_LOG_SCHEMA, type AfterLogHook, type AuditApi, type AuditContext, type AuditDatabaseAdapter, type AuditLog, type AuditLogColumnName, type AuditLogSchema, type AuditOperation, AuditQueryBuilder, type AuditQueryFilters, type AuditQueryResult, type AuditQuerySpec, type AuditSeverity, type AuditStats, type BeforeLogHook, type BetterAuditConfig, type BetterAuditInstance, type CaptureLogInput, type ColumnDefinition, type ColumnType, type ConsoleQueryFilters, type ConsoleQueryResult, type ContextExtractor, type EnrichmentConfig, type EnrichmentDescriptionContext, type EnrichmentSummary, type ExportOptions, type ExportResponseOptions, type ExportResult, type MiddlewareHandlerOptions, type QueryExecutor, type ResourceFilter, type RetentionPolicy, type TimeFilter, type ValueExtractor, betterAudit, createAuditApi, createAuditConsoleEndpoints, createExportResponse, fromBearerToken, fromCookie, fromHeader, getAuditContext, handleMiddleware, mergeAuditContext, normalizeInput, parseDuration, runExport, runWithAuditContext };
package/dist/index.js CHANGED
@@ -520,38 +520,214 @@ var AuditQueryBuilder = class _AuditQueryBuilder {
520
520
  }
521
521
  };
522
522
 
523
- // src/audit-api.ts
524
- var CSV_HEADERS = [
523
+ // src/export.ts
524
+ var DEFAULT_BATCH_SIZE = 500;
525
+ var DEFAULT_CSV_DELIMITER = ",";
526
+ var CSV_COLUMNS = [
525
527
  "id",
526
528
  "timestamp",
527
529
  "tableName",
528
530
  "operation",
529
531
  "recordId",
530
532
  "actorId",
531
- "severity",
533
+ "beforeData",
534
+ "afterData",
535
+ "diff",
532
536
  "label",
533
- "description"
537
+ "description",
538
+ "severity",
539
+ "compliance",
540
+ "notify",
541
+ "reason",
542
+ "metadata",
543
+ "redactedFields"
534
544
  ];
535
- function escapeCsvField(value) {
536
- if (value.includes('"') || value.includes(",") || value.includes("\n") || value.includes("\r")) {
545
+ var JSON_FIELDS = /* @__PURE__ */ new Set([
546
+ "beforeData",
547
+ "afterData",
548
+ "diff",
549
+ "compliance",
550
+ "metadata",
551
+ "redactedFields"
552
+ ]);
553
+ function escapeCsvField(value, delimiter) {
554
+ if (value.includes('"') || value.includes(delimiter) || value.includes("\n") || value.includes("\r")) {
537
555
  return `"${value.replace(/"/g, '""')}"`;
538
556
  }
539
557
  return value;
540
558
  }
541
- function logToCsvRow(log) {
542
- const fields = [
543
- log.id,
544
- log.timestamp instanceof Date ? log.timestamp.toISOString() : String(log.timestamp),
545
- log.tableName,
546
- log.operation,
547
- log.recordId,
548
- log.actorId ?? "",
549
- log.severity ?? "",
550
- log.label ?? "",
551
- log.description ?? ""
552
- ];
553
- return fields.map(escapeCsvField).join(",");
559
+ function formatCsvValue(log, field, delimiter) {
560
+ const value = log[field];
561
+ if (value === void 0 || value === null) {
562
+ return "";
563
+ }
564
+ if (JSON_FIELDS.has(field)) {
565
+ return escapeCsvField(JSON.stringify(value), delimiter);
566
+ }
567
+ if (value instanceof Date) {
568
+ return escapeCsvField(value.toISOString(), delimiter);
569
+ }
570
+ if (typeof value === "boolean") {
571
+ return value ? "true" : "false";
572
+ }
573
+ return escapeCsvField(String(value), delimiter);
574
+ }
575
+ function createStringSink() {
576
+ const chunks = [];
577
+ return {
578
+ async write(chunk) {
579
+ chunks.push(chunk);
580
+ },
581
+ async finish() {
582
+ return chunks.join("");
583
+ },
584
+ async abort() {
585
+ }
586
+ };
554
587
  }
588
+ function createStreamSink(stream) {
589
+ const writer = stream.getWriter();
590
+ return {
591
+ async write(chunk) {
592
+ await writer.write(chunk);
593
+ },
594
+ async finish() {
595
+ await writer.close();
596
+ return void 0;
597
+ },
598
+ async abort(error) {
599
+ await writer.abort(error);
600
+ }
601
+ };
602
+ }
603
+ async function runExport(executor, options) {
604
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
605
+ if (batchSize <= 0) {
606
+ throw new Error(`batchSize must be greater than 0, got ${batchSize}`);
607
+ }
608
+ const delimiter = options.csvDelimiter ?? DEFAULT_CSV_DELIMITER;
609
+ if (delimiter.length !== 1) {
610
+ throw new Error("csvDelimiter must be exactly one character");
611
+ }
612
+ const jsonStyle = options.jsonStyle ?? "ndjson";
613
+ const sink = options.output === "string" ? createStringSink() : createStreamSink(options.output);
614
+ const isStringSink = options.output === "string";
615
+ const baseSpec = options.query !== void 0 ? options.query.toSpec() : { filters: {} };
616
+ const totalLimit = baseSpec.limit;
617
+ const spec = { ...baseSpec, limit: batchSize };
618
+ let rowCount = 0;
619
+ try {
620
+ if (options.format === "csv") {
621
+ const header = CSV_COLUMNS.map((col) => escapeCsvField(col, delimiter)).join(delimiter);
622
+ await sink.write(header + "\n");
623
+ let cursor;
624
+ for (; ; ) {
625
+ const currentSpec = cursor !== void 0 ? { ...spec, cursor } : spec;
626
+ const result = await executor(currentSpec);
627
+ if (isStringSink) {
628
+ const lines = [];
629
+ for (const entry of result.entries) {
630
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
631
+ break;
632
+ }
633
+ const row = CSV_COLUMNS.map((col) => formatCsvValue(entry, col, delimiter)).join(delimiter);
634
+ lines.push(row + "\n");
635
+ rowCount++;
636
+ }
637
+ if (lines.length > 0) {
638
+ await sink.write(lines.join(""));
639
+ }
640
+ } else {
641
+ for (const entry of result.entries) {
642
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
643
+ break;
644
+ }
645
+ const row = CSV_COLUMNS.map((col) => formatCsvValue(entry, col, delimiter)).join(delimiter);
646
+ await sink.write(row + "\n");
647
+ rowCount++;
648
+ }
649
+ }
650
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
651
+ break;
652
+ }
653
+ if (result.nextCursor === void 0) {
654
+ break;
655
+ }
656
+ cursor = result.nextCursor;
657
+ }
658
+ } else {
659
+ if (jsonStyle === "array") {
660
+ let cursor;
661
+ const entries = [];
662
+ for (; ; ) {
663
+ const currentSpec = cursor !== void 0 ? { ...spec, cursor } : spec;
664
+ const result = await executor(currentSpec);
665
+ for (const entry of result.entries) {
666
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
667
+ break;
668
+ }
669
+ entries.push(entry);
670
+ rowCount++;
671
+ }
672
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
673
+ break;
674
+ }
675
+ if (result.nextCursor === void 0) {
676
+ break;
677
+ }
678
+ cursor = result.nextCursor;
679
+ }
680
+ await sink.write(JSON.stringify(entries, null, 2) + "\n");
681
+ } else {
682
+ let cursor;
683
+ for (; ; ) {
684
+ const currentSpec = cursor !== void 0 ? { ...spec, cursor } : spec;
685
+ const result = await executor(currentSpec);
686
+ if (isStringSink) {
687
+ const lines = [];
688
+ for (const entry of result.entries) {
689
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
690
+ break;
691
+ }
692
+ lines.push(JSON.stringify(entry) + "\n");
693
+ rowCount++;
694
+ }
695
+ if (lines.length > 0) {
696
+ await sink.write(lines.join(""));
697
+ }
698
+ } else {
699
+ for (const entry of result.entries) {
700
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
701
+ break;
702
+ }
703
+ await sink.write(JSON.stringify(entry) + "\n");
704
+ rowCount++;
705
+ }
706
+ }
707
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
708
+ break;
709
+ }
710
+ if (result.nextCursor === void 0) {
711
+ break;
712
+ }
713
+ cursor = result.nextCursor;
714
+ }
715
+ }
716
+ }
717
+ } catch (error) {
718
+ await sink.abort(error);
719
+ throw error;
720
+ }
721
+ const data = await sink.finish();
722
+ if (data !== void 0) {
723
+ return { rowCount, data };
724
+ }
725
+ return { rowCount };
726
+ }
727
+
728
+ // src/audit-api.ts
729
+ var VALID_SEVERITIES = /* @__PURE__ */ new Set(["low", "medium", "high", "critical"]);
730
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE"]);
555
731
  function toTimeFilter(date) {
556
732
  return { date };
557
733
  }
@@ -568,13 +744,24 @@ function buildQuerySpec(filters, effectiveLimit) {
568
744
  spec.filters.actorIds = [filters.actorId];
569
745
  }
570
746
  if (filters.severity !== void 0) {
747
+ if (!VALID_SEVERITIES.has(filters.severity)) {
748
+ throw new Error(
749
+ `Invalid severity "${filters.severity}". Must be one of: low, medium, high, critical`
750
+ );
751
+ }
571
752
  spec.filters.severities = [filters.severity];
572
753
  }
573
754
  if (filters.compliance !== void 0) {
574
755
  spec.filters.compliance = [filters.compliance];
575
756
  }
576
757
  if (filters.operation !== void 0) {
577
- spec.filters.operations = [filters.operation.toUpperCase()];
758
+ const normalized = filters.operation.toUpperCase();
759
+ if (!VALID_OPERATIONS.has(normalized)) {
760
+ throw new Error(
761
+ `Invalid operation "${filters.operation}". Must be one of: INSERT, UPDATE, DELETE`
762
+ );
763
+ }
764
+ spec.filters.operations = [normalized];
578
765
  }
579
766
  if (filters.since !== void 0) {
580
767
  spec.filters.since = toTimeFilter(filters.since);
@@ -675,17 +862,28 @@ function createAuditApi(adapter, registry, maxQueryLimit) {
675
862
  return summaries;
676
863
  }
677
864
  async function exportLogs(filters, format) {
865
+ const queryFn = requireQueryLogs();
678
866
  const exportFormat = format ?? "json";
679
- const exportFilters = { ...filters, limit: effectiveLimit };
680
- const result = await queryLogs(exportFilters);
681
- if (exportFormat === "json") {
682
- return JSON.stringify(result.entries);
683
- }
684
- const rows = [CSV_HEADERS.join(",")];
685
- for (const log of result.entries) {
686
- rows.push(logToCsvRow(log));
687
- }
688
- return rows.join("\n");
867
+ const spec = buildQuerySpec(filters ?? {}, effectiveLimit);
868
+ const queryBuilder = new AuditQueryBuilder(
869
+ (s) => queryFn(s),
870
+ spec.filters,
871
+ spec.limit,
872
+ void 0,
873
+ effectiveLimit,
874
+ spec.sortOrder
875
+ );
876
+ const exportOptions = {
877
+ format: exportFormat,
878
+ query: queryBuilder,
879
+ output: "string",
880
+ ...exportFormat === "json" && { jsonStyle: "array" }
881
+ };
882
+ const result = await runExport(
883
+ (s) => queryFn(s),
884
+ exportOptions
885
+ );
886
+ return result.data ?? "";
689
887
  }
690
888
  async function purgeLogs(options) {
691
889
  const purgeFn = requirePurgeLogs();
@@ -921,14 +1119,113 @@ function createAuditConsoleEndpoints(api) {
921
1119
  ];
922
1120
  }
923
1121
 
1122
+ // src/export-response.ts
1123
+ function contentTypeForFormat(format, jsonStyle) {
1124
+ if (format === "csv") {
1125
+ return "text/csv; charset=utf-8";
1126
+ }
1127
+ if (jsonStyle === "ndjson") {
1128
+ return "application/x-ndjson; charset=utf-8";
1129
+ }
1130
+ return "application/json; charset=utf-8";
1131
+ }
1132
+ function fileExtensionForFormat(format, jsonStyle) {
1133
+ if (format === "csv") {
1134
+ return ".csv";
1135
+ }
1136
+ if (jsonStyle === "ndjson") {
1137
+ return ".ndjson";
1138
+ }
1139
+ return ".json";
1140
+ }
1141
+ function formatDate(date) {
1142
+ const year = date.getFullYear();
1143
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1144
+ const day = String(date.getDate()).padStart(2, "0");
1145
+ return `${year}-${month}-${day}`;
1146
+ }
1147
+ function sanitiseFilename(name) {
1148
+ return name.replace(/["\\\r\n\x00-\x1f]/g, "");
1149
+ }
1150
+ function createExportResponse(executor, options = {}) {
1151
+ const format = options.format ?? "csv";
1152
+ const jsonStyle = options.jsonStyle ?? "ndjson";
1153
+ const stem = options.filename ?? `audit-export-${formatDate(/* @__PURE__ */ new Date())}`;
1154
+ const extension = fileExtensionForFormat(format, jsonStyle);
1155
+ const fullFilename = sanitiseFilename(`${stem}${extension}`);
1156
+ const contentType = contentTypeForFormat(format, jsonStyle);
1157
+ const transform = new TransformStream();
1158
+ const encoder = new TextEncoderStream();
1159
+ const readable = transform.readable.pipeThrough(encoder);
1160
+ runExport(executor, {
1161
+ format,
1162
+ jsonStyle,
1163
+ output: transform.writable,
1164
+ ...options.batchSize !== void 0 && { batchSize: options.batchSize },
1165
+ ...options.csvDelimiter !== void 0 && {
1166
+ csvDelimiter: options.csvDelimiter
1167
+ },
1168
+ ...options.query !== void 0 && { query: options.query }
1169
+ }).catch(() => {
1170
+ });
1171
+ return new Response(readable, {
1172
+ status: 200,
1173
+ headers: {
1174
+ "Content-Type": contentType,
1175
+ "Content-Disposition": `attachment; filename="${fullFilename}"`,
1176
+ "Cache-Control": "no-cache"
1177
+ }
1178
+ });
1179
+ }
1180
+
1181
+ // src/retention.ts
1182
+ function validateRetentionPolicy(policy) {
1183
+ if (!Number.isInteger(policy.days) || !Number.isFinite(policy.days) || policy.days <= 0) {
1184
+ throw new Error(
1185
+ `retention: 'days' must be a positive integer, got ${String(policy.days)}`
1186
+ );
1187
+ }
1188
+ if (policy.tables !== void 0) {
1189
+ if (!Array.isArray(policy.tables) || policy.tables.length === 0 || policy.tables.some((t) => typeof t !== "string" || t === "")) {
1190
+ throw new Error(
1191
+ "retention: 'tables' must be a non-empty array of non-empty strings"
1192
+ );
1193
+ }
1194
+ }
1195
+ }
1196
+
924
1197
  // src/better-audit.ts
925
1198
  function withContext(context, fn) {
926
1199
  return runWithAuditContext(context, fn);
927
1200
  }
928
1201
  function betterAudit(config) {
1202
+ if (config.retention !== void 0) {
1203
+ validateRetentionPolicy(config.retention);
1204
+ }
929
1205
  const { database } = config;
930
1206
  const auditTables = new Set(config.auditTables);
1207
+ if (config.retention?.tables !== void 0) {
1208
+ const unknown = config.retention.tables.filter((t) => !auditTables.has(t));
1209
+ if (unknown.length > 0) {
1210
+ throw new Error(
1211
+ `retention: 'tables' contains table(s) not in auditTables: ${unknown.join(", ")}. Registered tables: ${[...auditTables].join(", ")}`
1212
+ );
1213
+ }
1214
+ }
931
1215
  const registry = new EnrichmentRegistry();
1216
+ const resolvedRetention = config.retention !== void 0 ? {
1217
+ ...config.retention,
1218
+ ...config.retention.tables !== void 0 && { tables: [...config.retention.tables] }
1219
+ } : void 0;
1220
+ function retentionPolicy() {
1221
+ if (resolvedRetention === void 0) {
1222
+ return void 0;
1223
+ }
1224
+ return {
1225
+ ...resolvedRetention,
1226
+ ...resolvedRetention.tables !== void 0 && { tables: [...resolvedRetention.tables] }
1227
+ };
1228
+ }
932
1229
  const beforeLogHooks = config.beforeLog !== void 0 ? [...config.beforeLog] : [];
933
1230
  const afterLogHooks = config.afterLog !== void 0 ? [...config.afterLog] : [];
934
1231
  function enrich(table, operation, enrichmentConfig) {
@@ -1036,6 +1333,24 @@ function betterAudit(config) {
1036
1333
  config.maxQueryLimit
1037
1334
  );
1038
1335
  }
1336
+ async function exportLogs(options) {
1337
+ if (database.queryLogs === void 0) {
1338
+ throw new Error(
1339
+ "audit.export() requires a database adapter that implements queryLogs(). Check that your ORM adapter supports querying."
1340
+ );
1341
+ }
1342
+ const queryLogs = database.queryLogs;
1343
+ return runExport((spec) => queryLogs(spec), options);
1344
+ }
1345
+ function exportResponse(options) {
1346
+ if (database.queryLogs === void 0) {
1347
+ throw new Error(
1348
+ "audit.exportResponse() requires a database adapter that implements queryLogs(). Check that your ORM adapter supports querying."
1349
+ );
1350
+ }
1351
+ const queryLogs = database.queryLogs;
1352
+ return createExportResponse((spec) => queryLogs(spec), options);
1353
+ }
1039
1354
  if (config.console) {
1040
1355
  const api = createAuditApi(database, registry, config.maxQueryLimit);
1041
1356
  const endpoints = createAuditConsoleEndpoints(api);
@@ -1045,7 +1360,7 @@ function betterAudit(config) {
1045
1360
  endpoints
1046
1361
  });
1047
1362
  }
1048
- return { captureLog, query, withContext, enrich, onBeforeLog, onAfterLog };
1363
+ return { captureLog, query, export: exportLogs, exportResponse, withContext, enrich, onBeforeLog, onAfterLog, retentionPolicy };
1049
1364
  }
1050
1365
 
1051
1366
  // src/audit-log-schema.ts
@@ -1253,6 +1568,7 @@ export {
1253
1568
  betterAudit,
1254
1569
  createAuditApi,
1255
1570
  createAuditConsoleEndpoints,
1571
+ createExportResponse,
1256
1572
  fromBearerToken,
1257
1573
  fromCookie,
1258
1574
  fromHeader,
@@ -1261,6 +1577,7 @@ export {
1261
1577
  mergeAuditContext,
1262
1578
  normalizeInput,
1263
1579
  parseDuration,
1580
+ runExport,
1264
1581
  runWithAuditContext
1265
1582
  };
1266
1583
  //# sourceMappingURL=index.js.map