@basaltkit/audit 1.2.2 → 1.3.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/README.md +15 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +29 -2
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ Registers an `Audit` (singleton, token `AUDIT`), hooks into **all** hooks (`hook
|
|
|
182
182
|
| `tenantId` | `string` | No | all | Filters by tenant. |
|
|
183
183
|
| `actorId` | `string` | No | all | Filters by actor. |
|
|
184
184
|
| `since` | `number` | No | since forever | Only entries with `at >= since`. |
|
|
185
|
-
| `limit` | `number` | No | no limit | Maximum number of results. |
|
|
185
|
+
| `limit` | `number` | No | no limit | Maximum number of results. The SQL-backed stores push it into the database, so a limited query never loads the whole trail. |
|
|
186
186
|
|
|
187
187
|
### `interface AuditStore`
|
|
188
188
|
|
|
@@ -191,10 +191,21 @@ Storage contract, **append-only by contract** (no update/delete):
|
|
|
191
191
|
- `append(entry: AuditEntry): Promise<void>`
|
|
192
192
|
- `query(query: AuditQuery): Promise<AuditEntry[]>` — must return most recent first and apply filters/limit.
|
|
193
193
|
|
|
194
|
+
Two helpers exist so a driver can push the limit down safely:
|
|
195
|
+
|
|
196
|
+
- `exactEventMatch(pattern?: string): string | undefined` — the event filter that may be pushed into SQL as an equality. Returns `undefined` for a pattern containing `*` (a wildcard) **or** `.` (because `patternMatches` treats `.` and `:` as interchangeable, so an equality would miss `a:b` for the pattern `a.b`); those must still be matched in code.
|
|
197
|
+
- `AUDIT_SCAN_PAGE: number` — rows a driver should read per round-trip when a wildcard forces a scan (500). Bounds peak memory.
|
|
198
|
+
|
|
194
199
|
### `class MemoryAuditStore`
|
|
195
200
|
|
|
196
201
|
In-memory implementation of `AuditStore` (freezes each entry; filters and reverses on query). Ideal for dev and tests; does not persist.
|
|
197
202
|
|
|
203
|
+
### Redaction
|
|
204
|
+
|
|
205
|
+
Payloads are scrubbed before they are persisted. `redactSensitive` masks values under secret-looking keys (`password`, `token`, `api_key`, `authorization`, …) as `'[redacted]'`; the opt-in `redactSensitiveAndPii` / `piiMinimizingRedactor` additionally replaces email/phone-shaped values with a stable `pii_<hash>` pseudonym.
|
|
206
|
+
|
|
207
|
+
Both walk **6 levels deep**. Anything deeper is replaced with `'[truncated]'` — not passed through. Payloads are arbitrary and the default subscription is `events: ['**']`, so returning the raw subtree meant a secret nested seven levels down reached the trail in cleartext. If your payloads are deeply nested, flatten them before recording rather than relying on depth.
|
|
208
|
+
|
|
198
209
|
### `patternMatches(pattern: string, name: string): boolean`
|
|
199
210
|
|
|
200
211
|
Wildcard matcher over `:` and `.` segments — exported for reuse. `*` = one segment; `**` = one or more; `'**'` matches everything.
|
|
@@ -225,6 +236,9 @@ The defaults only cover `auth/billing/tenancy/permission`. Pass `hooks: [...]` w
|
|
|
225
236
|
**I lost the history after restarting.**
|
|
226
237
|
`MemoryAuditStore` is volatile. In production, implement `AuditStore` over a database.
|
|
227
238
|
|
|
239
|
+
**A deeply-nested field comes back as `'[truncated]'`.**
|
|
240
|
+
The redactors stop at 6 levels and drop everything below, so a secret can never slip past the depth bound. Flatten the payload (or record the interesting fields explicitly) if you need that data in the trail.
|
|
241
|
+
|
|
228
242
|
**Can I edit or delete an entry?**
|
|
229
243
|
No — the contract is append-only and entries are frozen. This is a feature, not a limitation: it's what gives the trail evidentiary value.
|
|
230
244
|
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,18 @@ export declare class MemoryAuditStore implements AuditStore {
|
|
|
34
34
|
* 'order.*' matches 'order.created', '**' matches everything.
|
|
35
35
|
*/
|
|
36
36
|
export declare function patternMatches(pattern: string, name: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The event filter a driver may push into SQL as an equality. A pattern with a
|
|
39
|
+
* wildcard must still be matched in code; so must one containing `.`, because
|
|
40
|
+
* {@link patternMatches} treats `.` and `:` as interchangeable separators and an
|
|
41
|
+
* equality would miss `a:b` for the pattern `a.b`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function exactEventMatch(pattern: string | undefined): string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Rows a driver reads per round-trip when a wildcard pattern forces a scan.
|
|
46
|
+
* Bounds peak memory: a limited query no longer materialises the whole trail.
|
|
47
|
+
*/
|
|
48
|
+
export declare const AUDIT_SCAN_PAGE = 500;
|
|
37
49
|
/** Recursively masks sensitive fields so secrets/PII never reach the trail. */
|
|
38
50
|
export declare function redactSensitive(value: unknown, depth?: number): unknown;
|
|
39
51
|
export type AuditRedactor = (payload: unknown, event: string) => unknown;
|
package/dist/index.js
CHANGED
|
@@ -36,12 +36,37 @@ export function patternMatches(pattern, name) {
|
|
|
36
36
|
}
|
|
37
37
|
return patternSegments.length === nameSegments.length;
|
|
38
38
|
}
|
|
39
|
+
/** How deep the redactors walk a payload before dropping the rest. */
|
|
40
|
+
const MAX_REDACT_DEPTH = 6;
|
|
41
|
+
/** Stand-in for a subtree deeper than {@link MAX_REDACT_DEPTH}. */
|
|
42
|
+
const TRUNCATED = '[truncated]';
|
|
43
|
+
/**
|
|
44
|
+
* The event filter a driver may push into SQL as an equality. A pattern with a
|
|
45
|
+
* wildcard must still be matched in code; so must one containing `.`, because
|
|
46
|
+
* {@link patternMatches} treats `.` and `:` as interchangeable separators and an
|
|
47
|
+
* equality would miss `a:b` for the pattern `a.b`.
|
|
48
|
+
*/
|
|
49
|
+
export function exactEventMatch(pattern) {
|
|
50
|
+
if (pattern === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
return /[*.]/.test(pattern) ? undefined : pattern;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Rows a driver reads per round-trip when a wildcard pattern forces a scan.
|
|
56
|
+
* Bounds peak memory: a limited query no longer materialises the whole trail.
|
|
57
|
+
*/
|
|
58
|
+
export const AUDIT_SCAN_PAGE = 500;
|
|
39
59
|
/** Object keys whose values are masked before an entry is persisted. */
|
|
40
60
|
const SENSITIVE_KEY = /pass(word|wd)?|secret|token|authorization|api[-_]?key|credential|cookie|session|otp|mfa/i;
|
|
41
61
|
/** Recursively masks sensitive fields so secrets/PII never reach the trail. */
|
|
42
62
|
export function redactSensitive(value, depth = 0) {
|
|
43
|
-
|
|
63
|
+
// Past the depth bound the subtree is dropped, NOT passed through: event
|
|
64
|
+
// payloads are arbitrary, and returning the raw value here let a secret nested
|
|
65
|
+
// deeper than the limit reach the trail in cleartext.
|
|
66
|
+
if (value === null || typeof value !== 'object')
|
|
44
67
|
return value;
|
|
68
|
+
if (depth > MAX_REDACT_DEPTH)
|
|
69
|
+
return TRUNCATED;
|
|
45
70
|
if (Array.isArray(value))
|
|
46
71
|
return value.map((v) => redactSensitive(v, depth + 1));
|
|
47
72
|
const out = {};
|
|
@@ -70,7 +95,7 @@ export function pseudonymize(value) {
|
|
|
70
95
|
* entries correlatable.
|
|
71
96
|
*/
|
|
72
97
|
export function redactSensitiveAndPii(value, depth = 0) {
|
|
73
|
-
if (
|
|
98
|
+
if (value === null)
|
|
74
99
|
return value;
|
|
75
100
|
// Bound the length before the regex: a real email is <= 254 chars (RFC 5321),
|
|
76
101
|
// so only test plausibly-email-length strings — arbitrary logged values never
|
|
@@ -79,6 +104,8 @@ export function redactSensitiveAndPii(value, depth = 0) {
|
|
|
79
104
|
return value.length <= 320 && EMAIL_VALUE.test(value) ? pseudonymize(value) : value;
|
|
80
105
|
if (typeof value !== 'object')
|
|
81
106
|
return value;
|
|
107
|
+
if (depth > MAX_REDACT_DEPTH)
|
|
108
|
+
return TRUNCATED;
|
|
82
109
|
if (Array.isArray(value))
|
|
83
110
|
return value.map((v) => redactSensitiveAndPii(v, depth + 1));
|
|
84
111
|
const out = {};
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/audit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=22.5.0"
|
|
6
|
+
},
|
|
4
7
|
"description": "Append-only audit trail for Basalt: automatically records lifecycle hooks and domain events, enriched with actor/tenant/request from the context.",
|
|
5
8
|
"license": "MIT",
|
|
6
9
|
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
7
11
|
"exports": {
|
|
8
12
|
".": {
|
|
9
13
|
"types": "./dist/index.d.ts",
|
|
@@ -14,8 +18,8 @@
|
|
|
14
18
|
"dist"
|
|
15
19
|
],
|
|
16
20
|
"dependencies": {
|
|
17
|
-
"@basaltkit/
|
|
18
|
-
"@basaltkit/
|
|
21
|
+
"@basaltkit/core": "^1.3.1",
|
|
22
|
+
"@basaltkit/events": "^1.1.1"
|
|
19
23
|
},
|
|
20
24
|
"devDependencies": {
|
|
21
25
|
"@types/node": "^26.3.0",
|