@usebetterdev/audit-core 0.4.0-beta.3 → 0.5.0-beta.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.
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  mergeAuditContext: () => mergeAuditContext,
34
34
  normalizeInput: () => normalizeInput,
35
35
  parseDuration: () => parseDuration,
36
+ runExport: () => runExport,
36
37
  runWithAuditContext: () => runWithAuditContext
37
38
  });
38
39
  module.exports = __toCommonJS(index_exports);
@@ -559,38 +560,214 @@ var AuditQueryBuilder = class _AuditQueryBuilder {
559
560
  }
560
561
  };
561
562
 
562
- // src/audit-api.ts
563
- var CSV_HEADERS = [
563
+ // src/export.ts
564
+ var DEFAULT_BATCH_SIZE = 500;
565
+ var DEFAULT_CSV_DELIMITER = ",";
566
+ var CSV_COLUMNS = [
564
567
  "id",
565
568
  "timestamp",
566
569
  "tableName",
567
570
  "operation",
568
571
  "recordId",
569
572
  "actorId",
570
- "severity",
573
+ "beforeData",
574
+ "afterData",
575
+ "diff",
571
576
  "label",
572
- "description"
577
+ "description",
578
+ "severity",
579
+ "compliance",
580
+ "notify",
581
+ "reason",
582
+ "metadata",
583
+ "redactedFields"
573
584
  ];
574
- function escapeCsvField(value) {
575
- if (value.includes('"') || value.includes(",") || value.includes("\n") || value.includes("\r")) {
585
+ var JSON_FIELDS = /* @__PURE__ */ new Set([
586
+ "beforeData",
587
+ "afterData",
588
+ "diff",
589
+ "compliance",
590
+ "metadata",
591
+ "redactedFields"
592
+ ]);
593
+ function escapeCsvField(value, delimiter) {
594
+ if (value.includes('"') || value.includes(delimiter) || value.includes("\n") || value.includes("\r")) {
576
595
  return `"${value.replace(/"/g, '""')}"`;
577
596
  }
578
597
  return value;
579
598
  }
580
- function logToCsvRow(log) {
581
- const fields = [
582
- log.id,
583
- log.timestamp instanceof Date ? log.timestamp.toISOString() : String(log.timestamp),
584
- log.tableName,
585
- log.operation,
586
- log.recordId,
587
- log.actorId ?? "",
588
- log.severity ?? "",
589
- log.label ?? "",
590
- log.description ?? ""
591
- ];
592
- return fields.map(escapeCsvField).join(",");
599
+ function formatCsvValue(log, field, delimiter) {
600
+ const value = log[field];
601
+ if (value === void 0 || value === null) {
602
+ return "";
603
+ }
604
+ if (JSON_FIELDS.has(field)) {
605
+ return escapeCsvField(JSON.stringify(value), delimiter);
606
+ }
607
+ if (value instanceof Date) {
608
+ return escapeCsvField(value.toISOString(), delimiter);
609
+ }
610
+ if (typeof value === "boolean") {
611
+ return value ? "true" : "false";
612
+ }
613
+ return escapeCsvField(String(value), delimiter);
614
+ }
615
+ function createStringSink() {
616
+ const chunks = [];
617
+ return {
618
+ async write(chunk) {
619
+ chunks.push(chunk);
620
+ },
621
+ async finish() {
622
+ return chunks.join("");
623
+ },
624
+ async abort() {
625
+ }
626
+ };
627
+ }
628
+ function createStreamSink(stream) {
629
+ const writer = stream.getWriter();
630
+ return {
631
+ async write(chunk) {
632
+ await writer.write(chunk);
633
+ },
634
+ async finish() {
635
+ await writer.close();
636
+ return void 0;
637
+ },
638
+ async abort(error) {
639
+ await writer.abort(error);
640
+ }
641
+ };
593
642
  }
643
+ async function runExport(executor, options) {
644
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
645
+ if (batchSize <= 0) {
646
+ throw new Error(`batchSize must be greater than 0, got ${batchSize}`);
647
+ }
648
+ const delimiter = options.csvDelimiter ?? DEFAULT_CSV_DELIMITER;
649
+ if (delimiter.length !== 1) {
650
+ throw new Error("csvDelimiter must be exactly one character");
651
+ }
652
+ const jsonStyle = options.jsonStyle ?? "ndjson";
653
+ const sink = options.output === "string" ? createStringSink() : createStreamSink(options.output);
654
+ const isStringSink = options.output === "string";
655
+ const baseSpec = options.query !== void 0 ? options.query.toSpec() : { filters: {} };
656
+ const totalLimit = baseSpec.limit;
657
+ const spec = { ...baseSpec, limit: batchSize };
658
+ let rowCount = 0;
659
+ try {
660
+ if (options.format === "csv") {
661
+ const header = CSV_COLUMNS.map((col) => escapeCsvField(col, delimiter)).join(delimiter);
662
+ await sink.write(header + "\n");
663
+ let cursor;
664
+ for (; ; ) {
665
+ const currentSpec = cursor !== void 0 ? { ...spec, cursor } : spec;
666
+ const result = await executor(currentSpec);
667
+ if (isStringSink) {
668
+ const lines = [];
669
+ for (const entry of result.entries) {
670
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
671
+ break;
672
+ }
673
+ const row = CSV_COLUMNS.map((col) => formatCsvValue(entry, col, delimiter)).join(delimiter);
674
+ lines.push(row + "\n");
675
+ rowCount++;
676
+ }
677
+ if (lines.length > 0) {
678
+ await sink.write(lines.join(""));
679
+ }
680
+ } else {
681
+ for (const entry of result.entries) {
682
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
683
+ break;
684
+ }
685
+ const row = CSV_COLUMNS.map((col) => formatCsvValue(entry, col, delimiter)).join(delimiter);
686
+ await sink.write(row + "\n");
687
+ rowCount++;
688
+ }
689
+ }
690
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
691
+ break;
692
+ }
693
+ if (result.nextCursor === void 0) {
694
+ break;
695
+ }
696
+ cursor = result.nextCursor;
697
+ }
698
+ } else {
699
+ if (jsonStyle === "array") {
700
+ let cursor;
701
+ const entries = [];
702
+ for (; ; ) {
703
+ const currentSpec = cursor !== void 0 ? { ...spec, cursor } : spec;
704
+ const result = await executor(currentSpec);
705
+ for (const entry of result.entries) {
706
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
707
+ break;
708
+ }
709
+ entries.push(entry);
710
+ rowCount++;
711
+ }
712
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
713
+ break;
714
+ }
715
+ if (result.nextCursor === void 0) {
716
+ break;
717
+ }
718
+ cursor = result.nextCursor;
719
+ }
720
+ await sink.write(JSON.stringify(entries, null, 2) + "\n");
721
+ } else {
722
+ let cursor;
723
+ for (; ; ) {
724
+ const currentSpec = cursor !== void 0 ? { ...spec, cursor } : spec;
725
+ const result = await executor(currentSpec);
726
+ if (isStringSink) {
727
+ const lines = [];
728
+ for (const entry of result.entries) {
729
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
730
+ break;
731
+ }
732
+ lines.push(JSON.stringify(entry) + "\n");
733
+ rowCount++;
734
+ }
735
+ if (lines.length > 0) {
736
+ await sink.write(lines.join(""));
737
+ }
738
+ } else {
739
+ for (const entry of result.entries) {
740
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
741
+ break;
742
+ }
743
+ await sink.write(JSON.stringify(entry) + "\n");
744
+ rowCount++;
745
+ }
746
+ }
747
+ if (totalLimit !== void 0 && rowCount >= totalLimit) {
748
+ break;
749
+ }
750
+ if (result.nextCursor === void 0) {
751
+ break;
752
+ }
753
+ cursor = result.nextCursor;
754
+ }
755
+ }
756
+ }
757
+ } catch (error) {
758
+ await sink.abort(error);
759
+ throw error;
760
+ }
761
+ const data = await sink.finish();
762
+ if (data !== void 0) {
763
+ return { rowCount, data };
764
+ }
765
+ return { rowCount };
766
+ }
767
+
768
+ // src/audit-api.ts
769
+ var VALID_SEVERITIES = /* @__PURE__ */ new Set(["low", "medium", "high", "critical"]);
770
+ var VALID_OPERATIONS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE"]);
594
771
  function toTimeFilter(date) {
595
772
  return { date };
596
773
  }
@@ -607,13 +784,24 @@ function buildQuerySpec(filters, effectiveLimit) {
607
784
  spec.filters.actorIds = [filters.actorId];
608
785
  }
609
786
  if (filters.severity !== void 0) {
787
+ if (!VALID_SEVERITIES.has(filters.severity)) {
788
+ throw new Error(
789
+ `Invalid severity "${filters.severity}". Must be one of: low, medium, high, critical`
790
+ );
791
+ }
610
792
  spec.filters.severities = [filters.severity];
611
793
  }
612
794
  if (filters.compliance !== void 0) {
613
795
  spec.filters.compliance = [filters.compliance];
614
796
  }
615
797
  if (filters.operation !== void 0) {
616
- spec.filters.operations = [filters.operation.toUpperCase()];
798
+ const normalized = filters.operation.toUpperCase();
799
+ if (!VALID_OPERATIONS.has(normalized)) {
800
+ throw new Error(
801
+ `Invalid operation "${filters.operation}". Must be one of: INSERT, UPDATE, DELETE`
802
+ );
803
+ }
804
+ spec.filters.operations = [normalized];
617
805
  }
618
806
  if (filters.since !== void 0) {
619
807
  spec.filters.since = toTimeFilter(filters.since);
@@ -714,17 +902,28 @@ function createAuditApi(adapter, registry, maxQueryLimit) {
714
902
  return summaries;
715
903
  }
716
904
  async function exportLogs(filters, format) {
905
+ const queryFn = requireQueryLogs();
717
906
  const exportFormat = format ?? "json";
718
- const exportFilters = { ...filters, limit: effectiveLimit };
719
- const result = await queryLogs(exportFilters);
720
- if (exportFormat === "json") {
721
- return JSON.stringify(result.entries);
722
- }
723
- const rows = [CSV_HEADERS.join(",")];
724
- for (const log of result.entries) {
725
- rows.push(logToCsvRow(log));
726
- }
727
- return rows.join("\n");
907
+ const spec = buildQuerySpec(filters ?? {}, effectiveLimit);
908
+ const queryBuilder = new AuditQueryBuilder(
909
+ (s) => queryFn(s),
910
+ spec.filters,
911
+ spec.limit,
912
+ void 0,
913
+ effectiveLimit,
914
+ spec.sortOrder
915
+ );
916
+ const exportOptions = {
917
+ format: exportFormat,
918
+ query: queryBuilder,
919
+ output: "string",
920
+ ...exportFormat === "json" && { jsonStyle: "array" }
921
+ };
922
+ const result = await runExport(
923
+ (s) => queryFn(s),
924
+ exportOptions
925
+ );
926
+ return result.data ?? "";
728
927
  }
729
928
  async function purgeLogs(options) {
730
929
  const purgeFn = requirePurgeLogs();
@@ -960,14 +1159,54 @@ function createAuditConsoleEndpoints(api) {
960
1159
  ];
961
1160
  }
962
1161
 
1162
+ // src/retention.ts
1163
+ function validateRetentionPolicy(policy) {
1164
+ if (!Number.isInteger(policy.days) || !Number.isFinite(policy.days) || policy.days <= 0) {
1165
+ throw new Error(
1166
+ `retention: 'days' must be a positive integer, got ${String(policy.days)}`
1167
+ );
1168
+ }
1169
+ if (policy.tables !== void 0) {
1170
+ if (!Array.isArray(policy.tables) || policy.tables.length === 0 || policy.tables.some((t) => typeof t !== "string" || t === "")) {
1171
+ throw new Error(
1172
+ "retention: 'tables' must be a non-empty array of non-empty strings"
1173
+ );
1174
+ }
1175
+ }
1176
+ }
1177
+
963
1178
  // src/better-audit.ts
964
1179
  function withContext(context, fn) {
965
1180
  return runWithAuditContext(context, fn);
966
1181
  }
967
1182
  function betterAudit(config) {
1183
+ if (config.retention !== void 0) {
1184
+ validateRetentionPolicy(config.retention);
1185
+ }
968
1186
  const { database } = config;
969
1187
  const auditTables = new Set(config.auditTables);
1188
+ if (config.retention?.tables !== void 0) {
1189
+ const unknown = config.retention.tables.filter((t) => !auditTables.has(t));
1190
+ if (unknown.length > 0) {
1191
+ throw new Error(
1192
+ `retention: 'tables' contains table(s) not in auditTables: ${unknown.join(", ")}. Registered tables: ${[...auditTables].join(", ")}`
1193
+ );
1194
+ }
1195
+ }
970
1196
  const registry = new EnrichmentRegistry();
1197
+ const resolvedRetention = config.retention !== void 0 ? {
1198
+ ...config.retention,
1199
+ ...config.retention.tables !== void 0 && { tables: [...config.retention.tables] }
1200
+ } : void 0;
1201
+ function retentionPolicy() {
1202
+ if (resolvedRetention === void 0) {
1203
+ return void 0;
1204
+ }
1205
+ return {
1206
+ ...resolvedRetention,
1207
+ ...resolvedRetention.tables !== void 0 && { tables: [...resolvedRetention.tables] }
1208
+ };
1209
+ }
971
1210
  const beforeLogHooks = config.beforeLog !== void 0 ? [...config.beforeLog] : [];
972
1211
  const afterLogHooks = config.afterLog !== void 0 ? [...config.afterLog] : [];
973
1212
  function enrich(table, operation, enrichmentConfig) {
@@ -1075,6 +1314,15 @@ function betterAudit(config) {
1075
1314
  config.maxQueryLimit
1076
1315
  );
1077
1316
  }
1317
+ async function exportLogs(options) {
1318
+ if (database.queryLogs === void 0) {
1319
+ throw new Error(
1320
+ "audit.export() requires a database adapter that implements queryLogs(). Check that your ORM adapter supports querying."
1321
+ );
1322
+ }
1323
+ const queryLogs = database.queryLogs;
1324
+ return runExport((spec) => queryLogs(spec), options);
1325
+ }
1078
1326
  if (config.console) {
1079
1327
  const api = createAuditApi(database, registry, config.maxQueryLimit);
1080
1328
  const endpoints = createAuditConsoleEndpoints(api);
@@ -1084,7 +1332,7 @@ function betterAudit(config) {
1084
1332
  endpoints
1085
1333
  });
1086
1334
  }
1087
- return { captureLog, query, withContext, enrich, onBeforeLog, onAfterLog };
1335
+ return { captureLog, query, export: exportLogs, withContext, enrich, onBeforeLog, onAfterLog, retentionPolicy };
1088
1336
  }
1089
1337
 
1090
1338
  // src/audit-log-schema.ts
@@ -1301,6 +1549,7 @@ async function handleMiddleware(extractor, request, next, options = {}) {
1301
1549
  mergeAuditContext,
1302
1550
  normalizeInput,
1303
1551
  parseDuration,
1552
+ runExport,
1304
1553
  runWithAuditContext
1305
1554
  });
1306
1555
  //# sourceMappingURL=index.cjs.map