@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 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
- if (depth > 6 || value === null || typeof value !== 'object')
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 (depth > 6 || value === null)
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.2.2",
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/events": "^1.0.1",
18
- "@basaltkit/core": "^1.1.2"
21
+ "@basaltkit/core": "^1.3.1",
22
+ "@basaltkit/events": "^1.1.1"
19
23
  },
20
24
  "devDependencies": {
21
25
  "@types/node": "^26.3.0",