@usebetterdev/audit-core 0.4.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 +1267 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +524 -0
- package/dist/index.d.ts +524 -0
- package/dist/index.js +1227 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AUDIT_LOG_SCHEMA: () => AUDIT_LOG_SCHEMA,
|
|
24
|
+
AuditQueryBuilder: () => AuditQueryBuilder,
|
|
25
|
+
betterAudit: () => betterAudit,
|
|
26
|
+
createAuditApi: () => createAuditApi,
|
|
27
|
+
createAuditConsoleEndpoints: () => createAuditConsoleEndpoints,
|
|
28
|
+
fromBearerToken: () => fromBearerToken,
|
|
29
|
+
fromCookie: () => fromCookie,
|
|
30
|
+
fromHeader: () => fromHeader,
|
|
31
|
+
getAuditContext: () => getAuditContext,
|
|
32
|
+
handleMiddleware: () => handleMiddleware,
|
|
33
|
+
mergeAuditContext: () => mergeAuditContext,
|
|
34
|
+
normalizeInput: () => normalizeInput,
|
|
35
|
+
parseDuration: () => parseDuration,
|
|
36
|
+
runWithAuditContext: () => runWithAuditContext
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/context.ts
|
|
41
|
+
var import_node_async_hooks = require("async_hooks");
|
|
42
|
+
var storage = new import_node_async_hooks.AsyncLocalStorage();
|
|
43
|
+
function getAuditContext() {
|
|
44
|
+
return storage.getStore();
|
|
45
|
+
}
|
|
46
|
+
function runWithAuditContext(context, fn) {
|
|
47
|
+
return Promise.resolve(storage.run(context, () => fn()));
|
|
48
|
+
}
|
|
49
|
+
function mergeAuditContext(override, fn) {
|
|
50
|
+
const current = storage.getStore() ?? {};
|
|
51
|
+
const merged = { ...current, ...override };
|
|
52
|
+
return Promise.resolve(storage.run(merged, () => fn()));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/diff.ts
|
|
56
|
+
function computeDiff(before, after) {
|
|
57
|
+
const b = before ?? {};
|
|
58
|
+
const a = after ?? {};
|
|
59
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(a)]);
|
|
60
|
+
const changedFields = [];
|
|
61
|
+
for (const key of allKeys) {
|
|
62
|
+
const inBefore = Object.prototype.hasOwnProperty.call(b, key);
|
|
63
|
+
const inAfter = Object.prototype.hasOwnProperty.call(a, key);
|
|
64
|
+
if (!inBefore || !inAfter) {
|
|
65
|
+
changedFields.push(key);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
if (JSON.stringify(b[key]) !== JSON.stringify(a[key])) {
|
|
70
|
+
changedFields.push(key);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
changedFields.push(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { changedFields };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/enrichment-registry.ts
|
|
80
|
+
function makeKey(table, operation) {
|
|
81
|
+
return `${table}:${operation.toUpperCase()}`;
|
|
82
|
+
}
|
|
83
|
+
function validateRedactInclude(config, key) {
|
|
84
|
+
if (config.redact !== void 0 && config.redact.length > 0 && config.include !== void 0 && config.include.length > 0) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Enrichment for "${key}" cannot specify both "redact" and "include". Use one or the other.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var EnrichmentRegistry = class {
|
|
91
|
+
entries = /* @__PURE__ */ new Map();
|
|
92
|
+
register(table, operation, config) {
|
|
93
|
+
const key = makeKey(table, operation);
|
|
94
|
+
validateRedactInclude(config, key);
|
|
95
|
+
const existing = this.entries.get(key);
|
|
96
|
+
if (existing !== void 0) {
|
|
97
|
+
existing.push(config);
|
|
98
|
+
} else {
|
|
99
|
+
this.entries.set(key, [config]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
getEntries() {
|
|
103
|
+
const result = [];
|
|
104
|
+
for (const [key, configs] of this.entries) {
|
|
105
|
+
const separatorIndex = key.indexOf(":");
|
|
106
|
+
const table = key.slice(0, separatorIndex);
|
|
107
|
+
const operation = key.slice(separatorIndex + 1);
|
|
108
|
+
result.push({ table, operation, configs });
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
resolve(table, operation) {
|
|
113
|
+
const normalizedOp = operation.toUpperCase();
|
|
114
|
+
const keysToCheck = [
|
|
115
|
+
makeKey("*", "*"),
|
|
116
|
+
makeKey("*", normalizedOp),
|
|
117
|
+
makeKey(table, "*"),
|
|
118
|
+
makeKey(table, normalizedOp)
|
|
119
|
+
];
|
|
120
|
+
const allConfigs = [];
|
|
121
|
+
for (const key of keysToCheck) {
|
|
122
|
+
const configs = this.entries.get(key);
|
|
123
|
+
if (configs !== void 0) {
|
|
124
|
+
for (const config of configs) {
|
|
125
|
+
allConfigs.push(config);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (allConfigs.length === 0) {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
return mergeEnrichmentConfigs(allConfigs, `${table}:${normalizedOp}`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
function mergeEnrichmentConfigs(configs, contextKey) {
|
|
136
|
+
const result = {};
|
|
137
|
+
for (const config of configs) {
|
|
138
|
+
if (config.label !== void 0) {
|
|
139
|
+
result.label = config.label;
|
|
140
|
+
}
|
|
141
|
+
if (config.description !== void 0) {
|
|
142
|
+
result.description = config.description;
|
|
143
|
+
}
|
|
144
|
+
if (config.severity !== void 0) {
|
|
145
|
+
result.severity = config.severity;
|
|
146
|
+
}
|
|
147
|
+
if (config.notify !== void 0) {
|
|
148
|
+
result.notify = config.notify;
|
|
149
|
+
}
|
|
150
|
+
if (config.compliance !== void 0) {
|
|
151
|
+
if (result.compliance !== void 0) {
|
|
152
|
+
const merged = [...result.compliance, ...config.compliance];
|
|
153
|
+
result.compliance = [...new Set(merged)];
|
|
154
|
+
} else {
|
|
155
|
+
result.compliance = [...config.compliance];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (config.redact !== void 0) {
|
|
159
|
+
if (result.redact !== void 0) {
|
|
160
|
+
const merged = [...result.redact, ...config.redact];
|
|
161
|
+
result.redact = [...new Set(merged)];
|
|
162
|
+
} else {
|
|
163
|
+
result.redact = [...config.redact];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (config.include !== void 0) {
|
|
167
|
+
if (result.include !== void 0) {
|
|
168
|
+
const merged = [...result.include, ...config.include];
|
|
169
|
+
result.include = [...new Set(merged)];
|
|
170
|
+
} else {
|
|
171
|
+
result.include = [...config.include];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (result.redact !== void 0 && result.redact.length > 0 && result.include !== void 0 && result.include.length > 0) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Enrichment merge for "${contextKey}" produced both "redact" and "include". These are mutually exclusive \u2014 fix the conflicting registrations.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
function applyFieldRedaction(log, resolved) {
|
|
183
|
+
const { redact, include } = resolved;
|
|
184
|
+
const removedFields = /* @__PURE__ */ new Set();
|
|
185
|
+
if (redact !== void 0 && redact.length > 0) {
|
|
186
|
+
const redactSet = new Set(redact);
|
|
187
|
+
if (log.beforeData !== void 0) {
|
|
188
|
+
for (const key of Object.keys(log.beforeData)) {
|
|
189
|
+
if (redactSet.has(key)) {
|
|
190
|
+
removedFields.add(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
log.beforeData = filterOutKeys(log.beforeData, redactSet);
|
|
194
|
+
}
|
|
195
|
+
if (log.afterData !== void 0) {
|
|
196
|
+
for (const key of Object.keys(log.afterData)) {
|
|
197
|
+
if (redactSet.has(key)) {
|
|
198
|
+
removedFields.add(key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
log.afterData = filterOutKeys(log.afterData, redactSet);
|
|
202
|
+
}
|
|
203
|
+
if (log.diff !== void 0) {
|
|
204
|
+
log.diff = {
|
|
205
|
+
changedFields: log.diff.changedFields.filter(
|
|
206
|
+
(field) => !redactSet.has(field)
|
|
207
|
+
)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
} else if (include !== void 0 && include.length > 0) {
|
|
211
|
+
const includeSet = new Set(include);
|
|
212
|
+
if (log.beforeData !== void 0) {
|
|
213
|
+
for (const key of Object.keys(log.beforeData)) {
|
|
214
|
+
if (!includeSet.has(key)) {
|
|
215
|
+
removedFields.add(key);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
log.beforeData = keepOnlyKeys(log.beforeData, includeSet);
|
|
219
|
+
}
|
|
220
|
+
if (log.afterData !== void 0) {
|
|
221
|
+
for (const key of Object.keys(log.afterData)) {
|
|
222
|
+
if (!includeSet.has(key)) {
|
|
223
|
+
removedFields.add(key);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
log.afterData = keepOnlyKeys(log.afterData, includeSet);
|
|
227
|
+
}
|
|
228
|
+
if (log.diff !== void 0) {
|
|
229
|
+
log.diff = {
|
|
230
|
+
changedFields: log.diff.changedFields.filter(
|
|
231
|
+
(field) => includeSet.has(field)
|
|
232
|
+
)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (removedFields.size > 0) {
|
|
237
|
+
log.redactedFields = [...removedFields].sort();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function applyEnrichment(log, resolved) {
|
|
241
|
+
applyFieldRedaction(log, resolved);
|
|
242
|
+
if (resolved.description !== void 0 && log.description === void 0) {
|
|
243
|
+
try {
|
|
244
|
+
const descriptionContext = {
|
|
245
|
+
before: log.beforeData !== void 0 ? structuredClone(log.beforeData) : void 0,
|
|
246
|
+
after: log.afterData !== void 0 ? structuredClone(log.afterData) : void 0,
|
|
247
|
+
diff: log.diff !== void 0 ? structuredClone(log.diff) : void 0,
|
|
248
|
+
actorId: log.actorId,
|
|
249
|
+
metadata: log.metadata !== void 0 ? structuredClone(log.metadata) : void 0
|
|
250
|
+
};
|
|
251
|
+
log.description = resolved.description(descriptionContext);
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (resolved.label !== void 0 && log.label === void 0) {
|
|
256
|
+
log.label = resolved.label;
|
|
257
|
+
}
|
|
258
|
+
if (resolved.severity !== void 0 && log.severity === void 0) {
|
|
259
|
+
log.severity = resolved.severity;
|
|
260
|
+
}
|
|
261
|
+
if (resolved.notify !== void 0 && log.notify === void 0) {
|
|
262
|
+
log.notify = resolved.notify;
|
|
263
|
+
}
|
|
264
|
+
if (resolved.compliance !== void 0) {
|
|
265
|
+
if (log.compliance !== void 0) {
|
|
266
|
+
const merged = [...log.compliance, ...resolved.compliance];
|
|
267
|
+
log.compliance = [...new Set(merged)];
|
|
268
|
+
} else {
|
|
269
|
+
log.compliance = [...resolved.compliance];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function filterOutKeys(data, keysToRemove) {
|
|
274
|
+
const result = {};
|
|
275
|
+
for (const [key, value] of Object.entries(data)) {
|
|
276
|
+
if (!keysToRemove.has(key)) {
|
|
277
|
+
result[key] = value;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
function keepOnlyKeys(data, keysToKeep) {
|
|
283
|
+
const result = {};
|
|
284
|
+
for (const [key, value] of Object.entries(data)) {
|
|
285
|
+
if (keysToKeep.has(key)) {
|
|
286
|
+
result[key] = value;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/normalize.ts
|
|
293
|
+
function normalizeInput(operation, before, after) {
|
|
294
|
+
switch (operation) {
|
|
295
|
+
case "INSERT": {
|
|
296
|
+
return { before: void 0, after };
|
|
297
|
+
}
|
|
298
|
+
case "DELETE": {
|
|
299
|
+
return { before, after: void 0 };
|
|
300
|
+
}
|
|
301
|
+
case "UPDATE": {
|
|
302
|
+
return { before, after };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/duration.ts
|
|
308
|
+
var DURATION_PATTERN = /^(\d+)([hdwmy])$/;
|
|
309
|
+
function isValidUnit(raw) {
|
|
310
|
+
return raw === "h" || raw === "d" || raw === "w" || raw === "m" || raw === "y";
|
|
311
|
+
}
|
|
312
|
+
function parseDuration(input, referenceDate) {
|
|
313
|
+
const match = DURATION_PATTERN.exec(input);
|
|
314
|
+
if (match === null) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Invalid duration "${input}". Expected format: <number><unit> where unit is h, d, w, m, or y (e.g. "4h", "30d", "2w", "3m", "1y").`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const value = Number(match[1]);
|
|
320
|
+
const rawUnit = match[2];
|
|
321
|
+
if (value === 0) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Invalid duration "${input}". Value must be greater than zero.`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (!isValidUnit(rawUnit)) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`Invalid duration unit "${rawUnit}". Expected h, d, w, m, or y.`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const result = referenceDate !== void 0 ? new Date(referenceDate) : /* @__PURE__ */ new Date();
|
|
332
|
+
if (rawUnit === "h") {
|
|
333
|
+
result.setHours(result.getHours() - value);
|
|
334
|
+
} else if (rawUnit === "d") {
|
|
335
|
+
result.setDate(result.getDate() - value);
|
|
336
|
+
} else if (rawUnit === "w") {
|
|
337
|
+
result.setDate(result.getDate() - value * 7);
|
|
338
|
+
} else if (rawUnit === "m") {
|
|
339
|
+
result.setMonth(result.getMonth() - value);
|
|
340
|
+
} else {
|
|
341
|
+
result.setFullYear(result.getFullYear() - value);
|
|
342
|
+
}
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/query-builder.ts
|
|
347
|
+
var DEFAULT_MAX_QUERY_LIMIT = 1e3;
|
|
348
|
+
var MAX_SEARCH_TEXT_LENGTH = 500;
|
|
349
|
+
var AuditQueryBuilder = class _AuditQueryBuilder {
|
|
350
|
+
#executor;
|
|
351
|
+
#filters;
|
|
352
|
+
#limit;
|
|
353
|
+
#cursor;
|
|
354
|
+
#maxLimit;
|
|
355
|
+
#sortOrder;
|
|
356
|
+
constructor(executor, filters, limit, cursor, maxLimit, sortOrder) {
|
|
357
|
+
this.#executor = executor;
|
|
358
|
+
this.#filters = filters ?? {};
|
|
359
|
+
this.#limit = limit;
|
|
360
|
+
this.#cursor = cursor;
|
|
361
|
+
this.#maxLimit = maxLimit ?? DEFAULT_MAX_QUERY_LIMIT;
|
|
362
|
+
this.#sortOrder = sortOrder;
|
|
363
|
+
}
|
|
364
|
+
/** Filter by table name and optional record ID. Last-write-wins. */
|
|
365
|
+
resource(tableName, recordId) {
|
|
366
|
+
return new _AuditQueryBuilder(
|
|
367
|
+
this.#executor,
|
|
368
|
+
{
|
|
369
|
+
...this.#filters,
|
|
370
|
+
resource: recordId !== void 0 ? { tableName, recordId } : { tableName }
|
|
371
|
+
},
|
|
372
|
+
this.#limit,
|
|
373
|
+
this.#cursor,
|
|
374
|
+
this.#maxLimit,
|
|
375
|
+
this.#sortOrder
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
/** Filter by actor IDs (OR semantics, deduplicates). */
|
|
379
|
+
actor(...ids) {
|
|
380
|
+
const existing = this.#filters.actorIds ?? [];
|
|
381
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...ids])];
|
|
382
|
+
return new _AuditQueryBuilder(
|
|
383
|
+
this.#executor,
|
|
384
|
+
{ ...this.#filters, actorIds: merged },
|
|
385
|
+
this.#limit,
|
|
386
|
+
this.#cursor,
|
|
387
|
+
this.#maxLimit,
|
|
388
|
+
this.#sortOrder
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
/** Add severity levels to the filter (OR semantics, deduplicates). */
|
|
392
|
+
severity(...levels) {
|
|
393
|
+
const existing = this.#filters.severities ?? [];
|
|
394
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...levels])];
|
|
395
|
+
return new _AuditQueryBuilder(
|
|
396
|
+
this.#executor,
|
|
397
|
+
{ ...this.#filters, severities: merged },
|
|
398
|
+
this.#limit,
|
|
399
|
+
this.#cursor,
|
|
400
|
+
this.#maxLimit,
|
|
401
|
+
this.#sortOrder
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
/** Add compliance tags to the filter (AND semantics, deduplicates). */
|
|
405
|
+
compliance(...tags) {
|
|
406
|
+
const existing = this.#filters.compliance ?? [];
|
|
407
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...tags])];
|
|
408
|
+
return new _AuditQueryBuilder(
|
|
409
|
+
this.#executor,
|
|
410
|
+
{ ...this.#filters, compliance: merged },
|
|
411
|
+
this.#limit,
|
|
412
|
+
this.#cursor,
|
|
413
|
+
this.#maxLimit,
|
|
414
|
+
this.#sortOrder
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Filter entries created after a point in time.
|
|
419
|
+
*
|
|
420
|
+
* Accepts a `Date` or a duration string (e.g. "4h", "30d", "2w", "3m", "1y").
|
|
421
|
+
* Duration strings are eagerly validated but resolved at query time for a fresh "now".
|
|
422
|
+
* Last-write-wins.
|
|
423
|
+
*/
|
|
424
|
+
since(value) {
|
|
425
|
+
const filter = this.#parseTimeFilter(value);
|
|
426
|
+
return new _AuditQueryBuilder(
|
|
427
|
+
this.#executor,
|
|
428
|
+
{ ...this.#filters, since: filter },
|
|
429
|
+
this.#limit,
|
|
430
|
+
this.#cursor,
|
|
431
|
+
this.#maxLimit,
|
|
432
|
+
this.#sortOrder
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Filter entries created before a point in time.
|
|
437
|
+
*
|
|
438
|
+
* Accepts a `Date` or a duration string (e.g. "4h", "30d", "2w", "3m", "1y").
|
|
439
|
+
* Duration strings are eagerly validated but resolved at query time for a fresh "now".
|
|
440
|
+
* Last-write-wins.
|
|
441
|
+
*/
|
|
442
|
+
until(value) {
|
|
443
|
+
const filter = this.#parseTimeFilter(value);
|
|
444
|
+
return new _AuditQueryBuilder(
|
|
445
|
+
this.#executor,
|
|
446
|
+
{ ...this.#filters, until: filter },
|
|
447
|
+
this.#limit,
|
|
448
|
+
this.#cursor,
|
|
449
|
+
this.#maxLimit,
|
|
450
|
+
this.#sortOrder
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
/** Full-text search filter. Last-write-wins. Max 500 characters. */
|
|
454
|
+
search(text) {
|
|
455
|
+
if (text.length > MAX_SEARCH_TEXT_LENGTH) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`searchText must be at most ${MAX_SEARCH_TEXT_LENGTH} characters, got ${text.length}`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
return new _AuditQueryBuilder(
|
|
461
|
+
this.#executor,
|
|
462
|
+
{ ...this.#filters, searchText: text },
|
|
463
|
+
this.#limit,
|
|
464
|
+
this.#cursor,
|
|
465
|
+
this.#maxLimit,
|
|
466
|
+
this.#sortOrder
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
/** Add operation types to the filter (OR semantics, deduplicates). */
|
|
470
|
+
operation(...ops) {
|
|
471
|
+
const existing = this.#filters.operations ?? [];
|
|
472
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...ops])];
|
|
473
|
+
return new _AuditQueryBuilder(
|
|
474
|
+
this.#executor,
|
|
475
|
+
{ ...this.#filters, operations: merged },
|
|
476
|
+
this.#limit,
|
|
477
|
+
this.#cursor,
|
|
478
|
+
this.#maxLimit,
|
|
479
|
+
this.#sortOrder
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
/** Set maximum number of entries to return. Must be > 0 and <= maxLimit. */
|
|
483
|
+
limit(n) {
|
|
484
|
+
if (n <= 0) {
|
|
485
|
+
throw new Error(`limit must be greater than 0, got ${n}`);
|
|
486
|
+
}
|
|
487
|
+
if (n > this.#maxLimit) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`limit ${n} exceeds maximum allowed limit of ${this.#maxLimit}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return new _AuditQueryBuilder(
|
|
493
|
+
this.#executor,
|
|
494
|
+
{ ...this.#filters },
|
|
495
|
+
n,
|
|
496
|
+
this.#cursor,
|
|
497
|
+
this.#maxLimit,
|
|
498
|
+
this.#sortOrder
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
/** Set the pagination cursor for fetching the next page. */
|
|
502
|
+
after(cursor) {
|
|
503
|
+
return new _AuditQueryBuilder(
|
|
504
|
+
this.#executor,
|
|
505
|
+
{ ...this.#filters },
|
|
506
|
+
this.#limit,
|
|
507
|
+
cursor,
|
|
508
|
+
this.#maxLimit,
|
|
509
|
+
this.#sortOrder
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
/** Set the sort direction for results. */
|
|
513
|
+
order(direction) {
|
|
514
|
+
return new _AuditQueryBuilder(
|
|
515
|
+
this.#executor,
|
|
516
|
+
{ ...this.#filters },
|
|
517
|
+
this.#limit,
|
|
518
|
+
this.#cursor,
|
|
519
|
+
this.#maxLimit,
|
|
520
|
+
direction
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
/** Returns the query specification without executing. Useful for tests and debugging. */
|
|
524
|
+
toSpec() {
|
|
525
|
+
const filters = { ...this.#filters };
|
|
526
|
+
if (filters.severities !== void 0) {
|
|
527
|
+
filters.severities = [...filters.severities];
|
|
528
|
+
}
|
|
529
|
+
if (filters.operations !== void 0) {
|
|
530
|
+
filters.operations = [...filters.operations];
|
|
531
|
+
}
|
|
532
|
+
if (filters.compliance !== void 0) {
|
|
533
|
+
filters.compliance = [...filters.compliance];
|
|
534
|
+
}
|
|
535
|
+
if (filters.actorIds !== void 0) {
|
|
536
|
+
filters.actorIds = [...filters.actorIds];
|
|
537
|
+
}
|
|
538
|
+
const spec = { filters };
|
|
539
|
+
const effectiveLimit = this.#limit ?? this.#maxLimit;
|
|
540
|
+
spec.limit = effectiveLimit;
|
|
541
|
+
if (this.#cursor !== void 0) {
|
|
542
|
+
spec.cursor = this.#cursor;
|
|
543
|
+
}
|
|
544
|
+
if (this.#sortOrder !== void 0) {
|
|
545
|
+
spec.sortOrder = this.#sortOrder;
|
|
546
|
+
}
|
|
547
|
+
return spec;
|
|
548
|
+
}
|
|
549
|
+
/** Execute the query against the adapter. */
|
|
550
|
+
list() {
|
|
551
|
+
return this.#executor(this.toSpec());
|
|
552
|
+
}
|
|
553
|
+
#parseTimeFilter(value) {
|
|
554
|
+
if (value instanceof Date) {
|
|
555
|
+
return { date: value };
|
|
556
|
+
}
|
|
557
|
+
parseDuration(value);
|
|
558
|
+
return { duration: value };
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// src/audit-api.ts
|
|
563
|
+
var CSV_HEADERS = [
|
|
564
|
+
"id",
|
|
565
|
+
"timestamp",
|
|
566
|
+
"tableName",
|
|
567
|
+
"operation",
|
|
568
|
+
"recordId",
|
|
569
|
+
"actorId",
|
|
570
|
+
"severity",
|
|
571
|
+
"label",
|
|
572
|
+
"description"
|
|
573
|
+
];
|
|
574
|
+
function escapeCsvField(value) {
|
|
575
|
+
if (value.includes('"') || value.includes(",") || value.includes("\n") || value.includes("\r")) {
|
|
576
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
577
|
+
}
|
|
578
|
+
return value;
|
|
579
|
+
}
|
|
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(",");
|
|
593
|
+
}
|
|
594
|
+
function toTimeFilter(date) {
|
|
595
|
+
return { date };
|
|
596
|
+
}
|
|
597
|
+
function buildQuerySpec(filters, effectiveLimit) {
|
|
598
|
+
const limit = Math.min(filters.limit ?? effectiveLimit, effectiveLimit);
|
|
599
|
+
const spec = {
|
|
600
|
+
filters: {},
|
|
601
|
+
limit
|
|
602
|
+
};
|
|
603
|
+
if (filters.tableName !== void 0) {
|
|
604
|
+
spec.filters.resource = { tableName: filters.tableName };
|
|
605
|
+
}
|
|
606
|
+
if (filters.actorId !== void 0) {
|
|
607
|
+
spec.filters.actorIds = [filters.actorId];
|
|
608
|
+
}
|
|
609
|
+
if (filters.severity !== void 0) {
|
|
610
|
+
spec.filters.severities = [filters.severity];
|
|
611
|
+
}
|
|
612
|
+
if (filters.compliance !== void 0) {
|
|
613
|
+
spec.filters.compliance = [filters.compliance];
|
|
614
|
+
}
|
|
615
|
+
if (filters.operation !== void 0) {
|
|
616
|
+
spec.filters.operations = [filters.operation.toUpperCase()];
|
|
617
|
+
}
|
|
618
|
+
if (filters.since !== void 0) {
|
|
619
|
+
spec.filters.since = toTimeFilter(filters.since);
|
|
620
|
+
}
|
|
621
|
+
if (filters.until !== void 0) {
|
|
622
|
+
spec.filters.until = toTimeFilter(filters.until);
|
|
623
|
+
}
|
|
624
|
+
if (filters.search !== void 0) {
|
|
625
|
+
spec.filters.searchText = filters.search;
|
|
626
|
+
}
|
|
627
|
+
if (filters.cursor !== void 0) {
|
|
628
|
+
spec.cursor = filters.cursor;
|
|
629
|
+
}
|
|
630
|
+
return spec;
|
|
631
|
+
}
|
|
632
|
+
function createAuditApi(adapter, registry, maxQueryLimit) {
|
|
633
|
+
const effectiveLimit = maxQueryLimit ?? 1e3;
|
|
634
|
+
function requireQueryLogs() {
|
|
635
|
+
if (adapter.queryLogs === void 0) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
"Console API requires a database adapter that implements queryLogs(). Check that your ORM adapter supports querying."
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return adapter.queryLogs;
|
|
641
|
+
}
|
|
642
|
+
function requireGetLogById() {
|
|
643
|
+
if (adapter.getLogById === void 0) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
"Console API requires a database adapter that implements getLogById(). Check that your ORM adapter supports querying."
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
return adapter.getLogById;
|
|
649
|
+
}
|
|
650
|
+
function requireGetStats() {
|
|
651
|
+
if (adapter.getStats === void 0) {
|
|
652
|
+
throw new Error(
|
|
653
|
+
"Console API requires a database adapter that implements getStats(). Check that your ORM adapter supports statistics."
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return adapter.getStats;
|
|
657
|
+
}
|
|
658
|
+
function requirePurgeLogs() {
|
|
659
|
+
if (adapter.purgeLogs === void 0) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
"Console API requires a database adapter that implements purgeLogs(). Check that your ORM adapter supports log purging."
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return adapter.purgeLogs;
|
|
665
|
+
}
|
|
666
|
+
async function queryLogs(filters) {
|
|
667
|
+
const queryFn = requireQueryLogs();
|
|
668
|
+
const spec = buildQuerySpec(filters ?? {}, effectiveLimit);
|
|
669
|
+
const result = await queryFn(spec);
|
|
670
|
+
return {
|
|
671
|
+
entries: result.entries,
|
|
672
|
+
...result.nextCursor !== void 0 && { nextCursor: result.nextCursor },
|
|
673
|
+
hasNextPage: result.nextCursor !== void 0
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async function getLog(id) {
|
|
677
|
+
const getLogFn = requireGetLogById();
|
|
678
|
+
return getLogFn(id);
|
|
679
|
+
}
|
|
680
|
+
async function getStats(options) {
|
|
681
|
+
const getStatsFn = requireGetStats();
|
|
682
|
+
return getStatsFn(options);
|
|
683
|
+
}
|
|
684
|
+
function getEnrichments() {
|
|
685
|
+
const entries = registry.getEntries();
|
|
686
|
+
const summaries = [];
|
|
687
|
+
for (const entry of entries) {
|
|
688
|
+
for (const config of entry.configs) {
|
|
689
|
+
const summary = {
|
|
690
|
+
table: entry.table,
|
|
691
|
+
operation: entry.operation
|
|
692
|
+
};
|
|
693
|
+
if (config.label !== void 0) {
|
|
694
|
+
summary.label = config.label;
|
|
695
|
+
}
|
|
696
|
+
if (config.severity !== void 0) {
|
|
697
|
+
summary.severity = config.severity;
|
|
698
|
+
}
|
|
699
|
+
if (config.compliance !== void 0) {
|
|
700
|
+
summary.compliance = config.compliance;
|
|
701
|
+
}
|
|
702
|
+
if (config.notify !== void 0) {
|
|
703
|
+
summary.notify = config.notify;
|
|
704
|
+
}
|
|
705
|
+
if (config.redact !== void 0) {
|
|
706
|
+
summary.redact = config.redact;
|
|
707
|
+
}
|
|
708
|
+
if (config.include !== void 0) {
|
|
709
|
+
summary.include = config.include;
|
|
710
|
+
}
|
|
711
|
+
summaries.push(summary);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return summaries;
|
|
715
|
+
}
|
|
716
|
+
async function exportLogs(filters, format) {
|
|
717
|
+
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");
|
|
728
|
+
}
|
|
729
|
+
async function purgeLogs(options) {
|
|
730
|
+
const purgeFn = requirePurgeLogs();
|
|
731
|
+
return purgeFn(options);
|
|
732
|
+
}
|
|
733
|
+
return { queryLogs, getLog, getStats, getEnrichments, exportLogs, purgeLogs };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/console-endpoints.ts
|
|
737
|
+
var MAX_PARAM_LENGTH = 1e3;
|
|
738
|
+
function exceedsMaxLength(value) {
|
|
739
|
+
return value !== void 0 && value.length > MAX_PARAM_LENGTH;
|
|
740
|
+
}
|
|
741
|
+
function parsePositiveInt(value, max) {
|
|
742
|
+
if (value === void 0) {
|
|
743
|
+
return void 0;
|
|
744
|
+
}
|
|
745
|
+
const parsed = Number(value);
|
|
746
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
747
|
+
return void 0;
|
|
748
|
+
}
|
|
749
|
+
return Math.min(Math.floor(parsed), max);
|
|
750
|
+
}
|
|
751
|
+
function parseIsoDate(value) {
|
|
752
|
+
if (value === void 0) {
|
|
753
|
+
return void 0;
|
|
754
|
+
}
|
|
755
|
+
const date = new Date(value);
|
|
756
|
+
if (Number.isNaN(date.getTime())) {
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
return date;
|
|
760
|
+
}
|
|
761
|
+
function parseConsoleQueryFilters(query) {
|
|
762
|
+
const filters = {};
|
|
763
|
+
const limit = parsePositiveInt(query.limit, 1e3);
|
|
764
|
+
if (limit !== void 0) {
|
|
765
|
+
filters.limit = limit;
|
|
766
|
+
}
|
|
767
|
+
if (query.tableName !== void 0) {
|
|
768
|
+
filters.tableName = query.tableName;
|
|
769
|
+
}
|
|
770
|
+
if (query.operation !== void 0) {
|
|
771
|
+
filters.operation = query.operation;
|
|
772
|
+
}
|
|
773
|
+
if (query.actorId !== void 0) {
|
|
774
|
+
filters.actorId = query.actorId;
|
|
775
|
+
}
|
|
776
|
+
if (query.severity !== void 0) {
|
|
777
|
+
filters.severity = query.severity;
|
|
778
|
+
}
|
|
779
|
+
if (query.compliance !== void 0) {
|
|
780
|
+
filters.compliance = query.compliance;
|
|
781
|
+
}
|
|
782
|
+
if (query.search !== void 0) {
|
|
783
|
+
filters.search = query.search;
|
|
784
|
+
}
|
|
785
|
+
if (query.cursor !== void 0) {
|
|
786
|
+
filters.cursor = query.cursor;
|
|
787
|
+
}
|
|
788
|
+
const since = parseIsoDate(query.since);
|
|
789
|
+
if (since !== void 0) {
|
|
790
|
+
filters.since = since;
|
|
791
|
+
}
|
|
792
|
+
const until = parseIsoDate(query.until);
|
|
793
|
+
if (until !== void 0) {
|
|
794
|
+
filters.until = until;
|
|
795
|
+
}
|
|
796
|
+
return filters;
|
|
797
|
+
}
|
|
798
|
+
function hasLongQueryParam(query) {
|
|
799
|
+
return exceedsMaxLength(query.tableName) || exceedsMaxLength(query.actorId) || exceedsMaxLength(query.cursor) || exceedsMaxLength(query.operation) || exceedsMaxLength(query.severity) || exceedsMaxLength(query.compliance) || exceedsMaxLength(query.search);
|
|
800
|
+
}
|
|
801
|
+
function createAuditConsoleEndpoints(api) {
|
|
802
|
+
return [
|
|
803
|
+
{
|
|
804
|
+
method: "GET",
|
|
805
|
+
path: "/logs",
|
|
806
|
+
requiredPermission: "read",
|
|
807
|
+
async handler(request) {
|
|
808
|
+
try {
|
|
809
|
+
if (hasLongQueryParam(request.query)) {
|
|
810
|
+
return { status: 400, body: { error: "Query parameter exceeds maximum length" } };
|
|
811
|
+
}
|
|
812
|
+
const filters = parseConsoleQueryFilters(request.query);
|
|
813
|
+
const result = await api.queryLogs(filters);
|
|
814
|
+
return { status: 200, body: result };
|
|
815
|
+
} catch {
|
|
816
|
+
return { status: 500, body: { error: "Internal server error" } };
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
method: "GET",
|
|
822
|
+
path: "/logs/:id",
|
|
823
|
+
requiredPermission: "read",
|
|
824
|
+
async handler(request) {
|
|
825
|
+
try {
|
|
826
|
+
const id = request.params.id?.trim();
|
|
827
|
+
if (!id) {
|
|
828
|
+
return { status: 400, body: { error: "Missing log id" } };
|
|
829
|
+
}
|
|
830
|
+
const log = await api.getLog(id);
|
|
831
|
+
if (!log) {
|
|
832
|
+
return { status: 404, body: { error: "Audit log not found" } };
|
|
833
|
+
}
|
|
834
|
+
return { status: 200, body: log };
|
|
835
|
+
} catch {
|
|
836
|
+
return { status: 500, body: { error: "Internal server error" } };
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
method: "GET",
|
|
842
|
+
path: "/stats",
|
|
843
|
+
requiredPermission: "read",
|
|
844
|
+
async handler(request) {
|
|
845
|
+
try {
|
|
846
|
+
const options = {};
|
|
847
|
+
const since = parseIsoDate(request.query.since);
|
|
848
|
+
if (since !== void 0) {
|
|
849
|
+
options.since = since;
|
|
850
|
+
}
|
|
851
|
+
if (request.query.tenantId !== void 0) {
|
|
852
|
+
options.tenantId = request.query.tenantId;
|
|
853
|
+
}
|
|
854
|
+
const stats = await api.getStats(options);
|
|
855
|
+
return { status: 200, body: stats };
|
|
856
|
+
} catch {
|
|
857
|
+
return { status: 500, body: { error: "Internal server error" } };
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
method: "GET",
|
|
863
|
+
path: "/enrichments",
|
|
864
|
+
requiredPermission: "read",
|
|
865
|
+
async handler() {
|
|
866
|
+
try {
|
|
867
|
+
const enrichments = api.getEnrichments();
|
|
868
|
+
return { status: 200, body: enrichments };
|
|
869
|
+
} catch {
|
|
870
|
+
return { status: 500, body: { error: "Internal server error" } };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
method: "GET",
|
|
876
|
+
path: "/export",
|
|
877
|
+
requiredPermission: "read",
|
|
878
|
+
async handler(request) {
|
|
879
|
+
try {
|
|
880
|
+
const format = request.query.format;
|
|
881
|
+
if (format !== void 0 && format !== "csv" && format !== "json") {
|
|
882
|
+
return { status: 400, body: { error: "Invalid format. Must be 'csv' or 'json'" } };
|
|
883
|
+
}
|
|
884
|
+
if (hasLongQueryParam(request.query)) {
|
|
885
|
+
return { status: 400, body: { error: "Query parameter exceeds maximum length" } };
|
|
886
|
+
}
|
|
887
|
+
const filters = parseConsoleQueryFilters(request.query);
|
|
888
|
+
const data = await api.exportLogs(filters, format);
|
|
889
|
+
return { status: 200, body: data };
|
|
890
|
+
} catch {
|
|
891
|
+
return { status: 500, body: { error: "Internal server error" } };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
method: "DELETE",
|
|
897
|
+
path: "/logs",
|
|
898
|
+
requiredPermission: "admin",
|
|
899
|
+
async handler(request) {
|
|
900
|
+
try {
|
|
901
|
+
const body = request.body;
|
|
902
|
+
const beforeValue = body?.before;
|
|
903
|
+
if (beforeValue === void 0 || beforeValue === null) {
|
|
904
|
+
return { status: 400, body: { error: "Missing required field: before" } };
|
|
905
|
+
}
|
|
906
|
+
const before = new Date(beforeValue);
|
|
907
|
+
if (Number.isNaN(before.getTime())) {
|
|
908
|
+
return { status: 400, body: { error: "Invalid date for 'before' field" } };
|
|
909
|
+
}
|
|
910
|
+
const options = { before };
|
|
911
|
+
if (typeof body?.tableName === "string" && body.tableName.length > 0) {
|
|
912
|
+
options.tableName = body.tableName;
|
|
913
|
+
}
|
|
914
|
+
const result = await api.purgeLogs(options);
|
|
915
|
+
return { status: 200, body: result };
|
|
916
|
+
} catch {
|
|
917
|
+
return { status: 500, body: { error: "Internal server error" } };
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/better-audit.ts
|
|
925
|
+
function withContext(context, fn) {
|
|
926
|
+
return runWithAuditContext(context, fn);
|
|
927
|
+
}
|
|
928
|
+
function betterAudit(config) {
|
|
929
|
+
const { database } = config;
|
|
930
|
+
const auditTables = new Set(config.auditTables);
|
|
931
|
+
const registry = new EnrichmentRegistry();
|
|
932
|
+
const beforeLogHooks = config.beforeLog !== void 0 ? [...config.beforeLog] : [];
|
|
933
|
+
const afterLogHooks = config.afterLog !== void 0 ? [...config.afterLog] : [];
|
|
934
|
+
function enrich(table, operation, enrichmentConfig) {
|
|
935
|
+
if (table !== "*" && !auditTables.has(table)) {
|
|
936
|
+
throw new Error(
|
|
937
|
+
`Cannot register enrichment for table "${table}": it is not in auditTables. Registered tables: ${[...auditTables].join(", ")}`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
registry.register(table, operation, enrichmentConfig);
|
|
941
|
+
}
|
|
942
|
+
function onBeforeLog(hook) {
|
|
943
|
+
beforeLogHooks.push(hook);
|
|
944
|
+
return () => {
|
|
945
|
+
const index = beforeLogHooks.indexOf(hook);
|
|
946
|
+
if (index !== -1) {
|
|
947
|
+
beforeLogHooks.splice(index, 1);
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function onAfterLog(hook) {
|
|
952
|
+
afterLogHooks.push(hook);
|
|
953
|
+
return () => {
|
|
954
|
+
const index = afterLogHooks.indexOf(hook);
|
|
955
|
+
if (index !== -1) {
|
|
956
|
+
afterLogHooks.splice(index, 1);
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
async function writeAndRunAfterHooks(log) {
|
|
961
|
+
await database.writeLog(log);
|
|
962
|
+
for (const hook of afterLogHooks) {
|
|
963
|
+
await hook(log);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
async function captureLog(input) {
|
|
967
|
+
if (!auditTables.has(input.tableName)) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (input.recordId === "") {
|
|
971
|
+
throw new Error("captureLog requires a non-empty recordId");
|
|
972
|
+
}
|
|
973
|
+
const normalized = normalizeInput(input.operation, input.before, input.after);
|
|
974
|
+
const context = getAuditContext();
|
|
975
|
+
const actorId = input.actorId ?? context?.actorId;
|
|
976
|
+
const label = input.label ?? context?.label;
|
|
977
|
+
const reason = input.reason ?? context?.reason;
|
|
978
|
+
const compliance = input.compliance ?? context?.compliance;
|
|
979
|
+
const metadata = input.metadata ?? context?.metadata;
|
|
980
|
+
const log = {
|
|
981
|
+
id: crypto.randomUUID(),
|
|
982
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
983
|
+
tableName: input.tableName,
|
|
984
|
+
operation: input.operation,
|
|
985
|
+
recordId: input.recordId,
|
|
986
|
+
...actorId !== void 0 && { actorId },
|
|
987
|
+
...label !== void 0 && { label },
|
|
988
|
+
...reason !== void 0 && { reason },
|
|
989
|
+
...compliance !== void 0 && { compliance },
|
|
990
|
+
...metadata !== void 0 && { metadata },
|
|
991
|
+
...input.description !== void 0 && { description: input.description },
|
|
992
|
+
...input.severity !== void 0 && { severity: input.severity },
|
|
993
|
+
...input.notify !== void 0 && { notify: input.notify },
|
|
994
|
+
...normalized.before !== void 0 && { beforeData: { ...normalized.before } },
|
|
995
|
+
...normalized.after !== void 0 && { afterData: { ...normalized.after } }
|
|
996
|
+
};
|
|
997
|
+
if (input.operation === "UPDATE") {
|
|
998
|
+
const diff = computeDiff(normalized.before, normalized.after);
|
|
999
|
+
if (diff.changedFields.length > 0) {
|
|
1000
|
+
log.diff = diff;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const resolved = registry.resolve(input.tableName, input.operation);
|
|
1004
|
+
if (resolved !== void 0) {
|
|
1005
|
+
applyEnrichment(log, resolved);
|
|
1006
|
+
}
|
|
1007
|
+
for (const hook of beforeLogHooks) {
|
|
1008
|
+
await hook(log);
|
|
1009
|
+
}
|
|
1010
|
+
const isAsync = input.asyncWrite ?? config.asyncWrite ?? false;
|
|
1011
|
+
if (isAsync) {
|
|
1012
|
+
void writeAndRunAfterHooks(log).catch((error) => {
|
|
1013
|
+
if (config.onError !== void 0) {
|
|
1014
|
+
config.onError(error);
|
|
1015
|
+
} else {
|
|
1016
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1017
|
+
console.error(`audit: async write failed for ${log.tableName}/${log.id} \u2014 ${message}`);
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
} else {
|
|
1021
|
+
await writeAndRunAfterHooks(log);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function query() {
|
|
1025
|
+
if (database.queryLogs === void 0) {
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
"audit.query() requires a database adapter that implements queryLogs(). Check that your ORM adapter supports querying."
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
const queryLogs = database.queryLogs;
|
|
1031
|
+
return new AuditQueryBuilder(
|
|
1032
|
+
(spec) => queryLogs(spec),
|
|
1033
|
+
void 0,
|
|
1034
|
+
void 0,
|
|
1035
|
+
void 0,
|
|
1036
|
+
config.maxQueryLimit
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
if (config.console) {
|
|
1040
|
+
const api = createAuditApi(database, registry, config.maxQueryLimit);
|
|
1041
|
+
const endpoints = createAuditConsoleEndpoints(api);
|
|
1042
|
+
config.console.registerProduct({
|
|
1043
|
+
id: "audit",
|
|
1044
|
+
name: "Better Audit",
|
|
1045
|
+
endpoints
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
return { captureLog, query, withContext, enrich, onBeforeLog, onAfterLog };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/audit-log-schema.ts
|
|
1052
|
+
var AUDIT_LOG_SCHEMA = {
|
|
1053
|
+
tableName: "audit_logs",
|
|
1054
|
+
columns: {
|
|
1055
|
+
id: {
|
|
1056
|
+
type: "uuid",
|
|
1057
|
+
nullable: false,
|
|
1058
|
+
primaryKey: true,
|
|
1059
|
+
defaultExpression: "gen_random_uuid()",
|
|
1060
|
+
description: "Unique identifier for the audit log entry"
|
|
1061
|
+
},
|
|
1062
|
+
timestamp: {
|
|
1063
|
+
type: "timestamptz",
|
|
1064
|
+
nullable: false,
|
|
1065
|
+
defaultExpression: "now()",
|
|
1066
|
+
indexed: true,
|
|
1067
|
+
description: "When the audited event occurred"
|
|
1068
|
+
},
|
|
1069
|
+
table_name: {
|
|
1070
|
+
type: "text",
|
|
1071
|
+
nullable: false,
|
|
1072
|
+
indexed: true,
|
|
1073
|
+
description: "Name of the table that was modified"
|
|
1074
|
+
},
|
|
1075
|
+
operation: {
|
|
1076
|
+
type: "text",
|
|
1077
|
+
nullable: false,
|
|
1078
|
+
indexed: true,
|
|
1079
|
+
description: "Type of operation: INSERT, UPDATE, or DELETE"
|
|
1080
|
+
},
|
|
1081
|
+
record_id: {
|
|
1082
|
+
type: "text",
|
|
1083
|
+
nullable: false,
|
|
1084
|
+
indexed: true,
|
|
1085
|
+
description: "Primary key of the affected record"
|
|
1086
|
+
},
|
|
1087
|
+
actor_id: {
|
|
1088
|
+
type: "text",
|
|
1089
|
+
nullable: true,
|
|
1090
|
+
indexed: true,
|
|
1091
|
+
description: "Identifier of the user or system that performed the action"
|
|
1092
|
+
},
|
|
1093
|
+
before_data: {
|
|
1094
|
+
type: "jsonb",
|
|
1095
|
+
nullable: true,
|
|
1096
|
+
description: "Row snapshot before the mutation (DELETE and UPDATE only)"
|
|
1097
|
+
},
|
|
1098
|
+
after_data: {
|
|
1099
|
+
type: "jsonb",
|
|
1100
|
+
nullable: true,
|
|
1101
|
+
description: "Row snapshot after the mutation (INSERT and UPDATE only)"
|
|
1102
|
+
},
|
|
1103
|
+
diff: {
|
|
1104
|
+
type: "jsonb",
|
|
1105
|
+
nullable: true,
|
|
1106
|
+
description: "Changed field names for UPDATE operations"
|
|
1107
|
+
},
|
|
1108
|
+
label: {
|
|
1109
|
+
type: "text",
|
|
1110
|
+
nullable: true,
|
|
1111
|
+
description: "Human-readable label for the event"
|
|
1112
|
+
},
|
|
1113
|
+
description: {
|
|
1114
|
+
type: "text",
|
|
1115
|
+
nullable: true,
|
|
1116
|
+
description: "Detailed description of what happened"
|
|
1117
|
+
},
|
|
1118
|
+
severity: {
|
|
1119
|
+
type: "text",
|
|
1120
|
+
nullable: true,
|
|
1121
|
+
description: "Severity level: low, medium, high, or critical"
|
|
1122
|
+
},
|
|
1123
|
+
compliance: {
|
|
1124
|
+
type: "jsonb",
|
|
1125
|
+
nullable: true,
|
|
1126
|
+
description: "Compliance framework tags (e.g. soc2, gdpr, hipaa)"
|
|
1127
|
+
},
|
|
1128
|
+
notify: {
|
|
1129
|
+
type: "boolean",
|
|
1130
|
+
nullable: true,
|
|
1131
|
+
description: "Whether this event should trigger notifications"
|
|
1132
|
+
},
|
|
1133
|
+
reason: {
|
|
1134
|
+
type: "text",
|
|
1135
|
+
nullable: true,
|
|
1136
|
+
description: "Justification or reason for the action"
|
|
1137
|
+
},
|
|
1138
|
+
metadata: {
|
|
1139
|
+
type: "jsonb",
|
|
1140
|
+
nullable: true,
|
|
1141
|
+
description: "Arbitrary key-value metadata attached to the event"
|
|
1142
|
+
},
|
|
1143
|
+
redacted_fields: {
|
|
1144
|
+
type: "jsonb",
|
|
1145
|
+
nullable: true,
|
|
1146
|
+
description: "Field names that were removed by redaction rules"
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
// src/context-extractor.ts
|
|
1152
|
+
function decodeJwtPayload(token) {
|
|
1153
|
+
try {
|
|
1154
|
+
const parts = token.split(".");
|
|
1155
|
+
if (parts.length < 2) {
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
const payload = parts[1];
|
|
1159
|
+
if (!payload) {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
let base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
1163
|
+
while (base64.length % 4 !== 0) {
|
|
1164
|
+
base64 += "=";
|
|
1165
|
+
}
|
|
1166
|
+
const decoded = atob(base64);
|
|
1167
|
+
const parsed = JSON.parse(decoded);
|
|
1168
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
return parsed;
|
|
1172
|
+
} catch {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function fromBearerToken(claim) {
|
|
1177
|
+
return (request) => {
|
|
1178
|
+
const authorization = request.headers.get("authorization");
|
|
1179
|
+
if (!authorization) {
|
|
1180
|
+
return void 0;
|
|
1181
|
+
}
|
|
1182
|
+
const parts = authorization.split(" ");
|
|
1183
|
+
if (parts.length !== 2 || parts[0]?.toLowerCase() !== "bearer") {
|
|
1184
|
+
return void 0;
|
|
1185
|
+
}
|
|
1186
|
+
const token = parts[1];
|
|
1187
|
+
if (!token) {
|
|
1188
|
+
return void 0;
|
|
1189
|
+
}
|
|
1190
|
+
const payload = decodeJwtPayload(token);
|
|
1191
|
+
if (!payload) {
|
|
1192
|
+
return void 0;
|
|
1193
|
+
}
|
|
1194
|
+
const value = payload[claim];
|
|
1195
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function fromCookie(cookieName) {
|
|
1199
|
+
return (request) => {
|
|
1200
|
+
const cookieHeader = request.headers.get("cookie");
|
|
1201
|
+
if (!cookieHeader) {
|
|
1202
|
+
return void 0;
|
|
1203
|
+
}
|
|
1204
|
+
const cookies = cookieHeader.split(";");
|
|
1205
|
+
for (const cookie of cookies) {
|
|
1206
|
+
const separatorIndex = cookie.indexOf("=");
|
|
1207
|
+
if (separatorIndex === -1) {
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
const name = cookie.slice(0, separatorIndex).trim();
|
|
1211
|
+
if (name === cookieName) {
|
|
1212
|
+
const value = cookie.slice(separatorIndex + 1).trim();
|
|
1213
|
+
return value.length > 0 ? value : void 0;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return void 0;
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function fromHeader(headerName) {
|
|
1220
|
+
return (request) => {
|
|
1221
|
+
const value = request.headers.get(headerName);
|
|
1222
|
+
if (!value || value.trim().length === 0) {
|
|
1223
|
+
return void 0;
|
|
1224
|
+
}
|
|
1225
|
+
return value.trim();
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
async function safeExtract(extractor, request, onError) {
|
|
1229
|
+
if (!extractor) {
|
|
1230
|
+
return void 0;
|
|
1231
|
+
}
|
|
1232
|
+
try {
|
|
1233
|
+
return await extractor(request);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
if (onError) {
|
|
1236
|
+
onError(error);
|
|
1237
|
+
}
|
|
1238
|
+
return void 0;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async function handleMiddleware(extractor, request, next, options = {}) {
|
|
1242
|
+
const actorId = await safeExtract(extractor.actor, request, options.onError);
|
|
1243
|
+
if (actorId === void 0) {
|
|
1244
|
+
await next();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const auditContext = { actorId };
|
|
1248
|
+
await runWithAuditContext(auditContext, () => next());
|
|
1249
|
+
}
|
|
1250
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1251
|
+
0 && (module.exports = {
|
|
1252
|
+
AUDIT_LOG_SCHEMA,
|
|
1253
|
+
AuditQueryBuilder,
|
|
1254
|
+
betterAudit,
|
|
1255
|
+
createAuditApi,
|
|
1256
|
+
createAuditConsoleEndpoints,
|
|
1257
|
+
fromBearerToken,
|
|
1258
|
+
fromCookie,
|
|
1259
|
+
fromHeader,
|
|
1260
|
+
getAuditContext,
|
|
1261
|
+
handleMiddleware,
|
|
1262
|
+
mergeAuditContext,
|
|
1263
|
+
normalizeInput,
|
|
1264
|
+
parseDuration,
|
|
1265
|
+
runWithAuditContext
|
|
1266
|
+
});
|
|
1267
|
+
//# sourceMappingURL=index.cjs.map
|