@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.cjs +350 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -1
- package/dist/index.d.ts +83 -1
- package/dist/index.js +348 -31
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
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/
|
|
524
|
-
var
|
|
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
|
-
"
|
|
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
|
-
|
|
536
|
-
|
|
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
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|