@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 +280 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +279 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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/
|
|
563
|
-
var
|
|
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
|
-
"
|
|
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
|
-
|
|
575
|
-
|
|
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
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|