@basaltkit/audit 1.2.1 → 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 +21 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +29 -2
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://basaltkit-docs.pages.dev">
|
|
3
|
+
<img src="https://basaltkit-docs.pages.dev/social-card.png" alt="Basalt" width="440">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
1
7
|
# @basaltkit/audit
|
|
2
8
|
|
|
3
9
|
Audit trail for Basalt applications: automatically records, in an immutable history, who did what and when — from lifecycle hooks, domain events, and manual records.
|
|
@@ -176,7 +182,7 @@ Registers an `Audit` (singleton, token `AUDIT`), hooks into **all** hooks (`hook
|
|
|
176
182
|
| `tenantId` | `string` | No | all | Filters by tenant. |
|
|
177
183
|
| `actorId` | `string` | No | all | Filters by actor. |
|
|
178
184
|
| `since` | `number` | No | since forever | Only entries with `at >= since`. |
|
|
179
|
-
| `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. |
|
|
180
186
|
|
|
181
187
|
### `interface AuditStore`
|
|
182
188
|
|
|
@@ -185,10 +191,21 @@ Storage contract, **append-only by contract** (no update/delete):
|
|
|
185
191
|
- `append(entry: AuditEntry): Promise<void>`
|
|
186
192
|
- `query(query: AuditQuery): Promise<AuditEntry[]>` — must return most recent first and apply filters/limit.
|
|
187
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
|
+
|
|
188
199
|
### `class MemoryAuditStore`
|
|
189
200
|
|
|
190
201
|
In-memory implementation of `AuditStore` (freezes each entry; filters and reverses on query). Ideal for dev and tests; does not persist.
|
|
191
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
|
+
|
|
192
209
|
### `patternMatches(pattern: string, name: string): boolean`
|
|
193
210
|
|
|
194
211
|
Wildcard matcher over `:` and `.` segments — exported for reuse. `*` = one segment; `**` = one or more; `'**'` matches everything.
|
|
@@ -219,6 +236,9 @@ The defaults only cover `auth/billing/tenancy/permission`. Pass `hooks: [...]` w
|
|
|
219
236
|
**I lost the history after restarting.**
|
|
220
237
|
`MemoryAuditStore` is volatile. In production, implement `AuditStore` over a database.
|
|
221
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
|
+
|
|
222
242
|
**Can I edit or delete an entry?**
|
|
223
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.
|
|
224
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/core": "^1.
|
|
18
|
-
"@basaltkit/events": "^1.
|
|
21
|
+
"@basaltkit/core": "^1.3.1",
|
|
22
|
+
"@basaltkit/events": "^1.1.1"
|
|
19
23
|
},
|
|
20
24
|
"devDependencies": {
|
|
21
25
|
"@types/node": "^26.3.0",
|