@vitrinka/redact 0.1.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/CHANGELOG.md +23 -0
- package/README.md +61 -0
- package/build/iife.d.ts +10 -0
- package/build/iife.js +8 -0
- package/build/index.d.ts +171 -0
- package/build/index.js +634 -0
- package/package.json +47 -0
- package/spec/REDACTION-SPEC.md +176 -0
- package/spec/vectors.json +294 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release: the shared recorder redaction engine.
|
|
6
|
+
|
|
7
|
+
- Policy-driven rule compilation (`compileRules`) with built-in safe defaults
|
|
8
|
+
mirroring the vitrinka server's ingest engine: auth-bearing header names
|
|
9
|
+
(plus the `…api-key`/`…token` suffix rule), sensitive body keys (normalized
|
|
10
|
+
matching, so `card_number` ≡ `cardNumber`), and URL parameter scrubbing for
|
|
11
|
+
query AND fragment, split on both `&` and `;`.
|
|
12
|
+
- Token-based key matching on top of the defaults (`X-Dev-Auth-Secret`,
|
|
13
|
+
`otpCode`, `user_password_hash` are caught without being listed).
|
|
14
|
+
- Surface transforms: `redactHeaders` (with value/budget caps), `redactBody`
|
|
15
|
+
(JSON structural with recursion guard, form-encoded, multipart/form-data,
|
|
16
|
+
truncated-JSON key fallback), `redactUrl`, `redactText`, `redactAndCap`
|
|
17
|
+
(shape-aware redact/cap ordering), `maskDirectives` (rrweb-style DOM
|
|
18
|
+
recorders), `pixelPolicy` (screenshot recorders).
|
|
19
|
+
- Workspace policy extensions: `extraHeaders`, `extraBodyKeys`, `patterns`
|
|
20
|
+
(each compiled in its own try/catch; a bad pattern is skipped, never fatal),
|
|
21
|
+
`maskAllText`, `fullFidelity` passthrough.
|
|
22
|
+
- Language-agnostic `spec/REDACTION-SPEC.md` + `spec/vectors.json`
|
|
23
|
+
conformance suite for ports to other recorder platforms.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @vitrinka/redact
|
|
2
|
+
|
|
3
|
+
The vitrinka recorder redaction engine: safe-by-default scrubbing of
|
|
4
|
+
auth-bearing headers, sensitive body keys, and URL query/fragment secrets in
|
|
5
|
+
recorded sessions — extensible per workspace via the redaction policy the
|
|
6
|
+
vitrinka server serves, and shared by every vitrinka capture client
|
|
7
|
+
([`@vitrinka/expo`](../expo) and the [browser extension](../../apps/extension)).
|
|
8
|
+
|
|
9
|
+
The vitrinka server applies the same policy again at ingest as the
|
|
10
|
+
load-bearing backstop. This engine is defense in depth: with it, secrets never
|
|
11
|
+
leave the device at all, and recorded payloads are smaller on the wire.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
compileRules,
|
|
18
|
+
redactHeaders,
|
|
19
|
+
redactBody,
|
|
20
|
+
redactUrl,
|
|
21
|
+
redactAndCap,
|
|
22
|
+
maskDirectives,
|
|
23
|
+
pixelPolicy,
|
|
24
|
+
} from '@vitrinka/redact';
|
|
25
|
+
|
|
26
|
+
// Fetch the workspace policy at session start (GET /api/v1/recorder/policy).
|
|
27
|
+
// FAIL CLOSED: on any failure, compile null — the built-in safe defaults.
|
|
28
|
+
const rules = compileRules(policyOrNull);
|
|
29
|
+
|
|
30
|
+
redactHeaders(rules, { Authorization: 'Bearer …' });
|
|
31
|
+
// → { Authorization: '[redacted]' }
|
|
32
|
+
|
|
33
|
+
redactBody(rules, '{"password":"hunter2"}', 'application/json');
|
|
34
|
+
// → '{"password":"[redacted]"}'
|
|
35
|
+
|
|
36
|
+
redactUrl(rules, 'https://app.example.com/cb#access_token=…');
|
|
37
|
+
// → 'https://app.example.com/cb#access_token=[redacted]'
|
|
38
|
+
|
|
39
|
+
redactAndCap(rules, body, 64 * 1024, contentType); // redact-then-cap, shape-aware
|
|
40
|
+
|
|
41
|
+
maskDirectives(rules); // rrweb-style DOM recorders: maskAllInputs/maskAllText
|
|
42
|
+
pixelPolicy(rules); // screenshot recorders: 'none' | 'blur'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`compileRules` caches by policy identity — call it per event for free.
|
|
46
|
+
`fullFidelity: true` in the policy (the self-host escape hatch, env-gated
|
|
47
|
+
server-side) turns every transform into a pass-through; clients only honor
|
|
48
|
+
the field, never default to it.
|
|
49
|
+
|
|
50
|
+
## Porting to other platforms
|
|
51
|
+
|
|
52
|
+
The engine is specified language-agnostically in
|
|
53
|
+
[`spec/REDACTION-SPEC.md`](spec/REDACTION-SPEC.md), with a portable
|
|
54
|
+
conformance suite in [`spec/vectors.json`](spec/vectors.json) (ported from the
|
|
55
|
+
server engine's table tests). A recorder for any UI technology — Flutter,
|
|
56
|
+
native iOS/Android, desktop — implements the spec's surface transforms and
|
|
57
|
+
must pass every vector. This TypeScript implementation is the reference.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
[Elastic License 2.0](../../LICENSE).
|
package/build/iife.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IIFE entry for build-free consumers (the browser extension's service worker
|
|
3
|
+
* loads the built file via importScripts). Attaches the whole engine as
|
|
4
|
+
* `globalThis.VitrinkaRedact`. Built with `bun run build:iife`; the extension
|
|
5
|
+
* checks the artifact in and CI diffs it against a fresh build.
|
|
6
|
+
*/
|
|
7
|
+
import * as engine from './index';
|
|
8
|
+
declare global {
|
|
9
|
+
var VitrinkaRedact: typeof engine;
|
|
10
|
+
}
|
package/build/iife.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IIFE entry for build-free consumers (the browser extension's service worker
|
|
3
|
+
* loads the built file via importScripts). Attaches the whole engine as
|
|
4
|
+
* `globalThis.VitrinkaRedact`. Built with `bun run build:iife`; the extension
|
|
5
|
+
* checks the artifact in and CI diffs it against a fresh build.
|
|
6
|
+
*/
|
|
7
|
+
import * as engine from './index';
|
|
8
|
+
globalThis.VitrinkaRedact = engine;
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitrinka/redact — the recorder redaction engine.
|
|
3
|
+
*
|
|
4
|
+
* Safe-by-default, capture-side scrubbing of auth-bearing headers, sensitive
|
|
5
|
+
* body keys, and URL query/fragment secrets, extensible per workspace via the
|
|
6
|
+
* redaction policy the vitrinka server serves at
|
|
7
|
+
* `GET /api/v1/recorder/policy`. The server re-applies the same policy at
|
|
8
|
+
* ingest (its `internal/redact` engine) as the load-bearing backstop — this
|
|
9
|
+
* engine is defense in depth: secrets never transit, stored payloads shrink.
|
|
10
|
+
*
|
|
11
|
+
* The engine is a PLATFORM, not a client: it owns no I/O and no capture. A
|
|
12
|
+
* recorder (browser extension, Expo, or a future Flutter/Swift/Kotlin port)
|
|
13
|
+
* compiles the policy into a {@link RuleSet} once, then routes each capture
|
|
14
|
+
* surface through the matching transform:
|
|
15
|
+
*
|
|
16
|
+
* headers → {@link redactHeaders}
|
|
17
|
+
* bodies → {@link redactBody} / {@link redactAndCap}
|
|
18
|
+
* URLs → {@link redactUrl}
|
|
19
|
+
* free text → {@link redactText}
|
|
20
|
+
* DOM capture → {@link maskDirectives} (rrweb-style recorders)
|
|
21
|
+
* pixels → {@link pixelPolicy} (screenshot recorders)
|
|
22
|
+
*
|
|
23
|
+
* Conformance is defined by `spec/REDACTION-SPEC.md` + `spec/vectors.json`
|
|
24
|
+
* (ported from the server engine's table tests): any implementation, in any
|
|
25
|
+
* language, must pass the vectors. This TS implementation is the reference.
|
|
26
|
+
*
|
|
27
|
+
* Matching is DELIBERATELY a superset of the server's: the server matches
|
|
28
|
+
* normalized names (lowercase, `-`/`_` stripped) against its default sets;
|
|
29
|
+
* this engine additionally token-matches key names (so `X-Dev-Auth-Secret`,
|
|
30
|
+
* `otpCode` and `user_password_hash` hit without being listed). Over-redacting
|
|
31
|
+
* a value the server would keep is acceptable; the reverse is not.
|
|
32
|
+
*
|
|
33
|
+
* FAIL CLOSED: `compileRules(null)` is the full default rule set. A client
|
|
34
|
+
* whose policy fetch fails must use it — never capture-everything. Only an
|
|
35
|
+
* explicit `fullFidelity: true` from the server (self-host escape hatch,
|
|
36
|
+
* env-gated there) disables scrubbing.
|
|
37
|
+
*/
|
|
38
|
+
/** Replaces every scrubbed value — a marker, not "", so a replayed session
|
|
39
|
+
* still shows that a header/field WAS present. */
|
|
40
|
+
export declare const REDACTED = "[redacted]";
|
|
41
|
+
/**
|
|
42
|
+
* The workspace-configurable redaction policy, exactly as the server serves
|
|
43
|
+
* it. The zero/absent policy is the safe default.
|
|
44
|
+
*/
|
|
45
|
+
export interface RedactionPolicy {
|
|
46
|
+
/** Additional header names whose values are scrubbed. */
|
|
47
|
+
extraHeaders?: string[];
|
|
48
|
+
/** Additional JSON/form body keys to scrub recursively. */
|
|
49
|
+
extraBodyKeys?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Extra regex patterns; every match in recorded string values is replaced.
|
|
52
|
+
* The server pre-filters these to a backtracking-safe subset before serving
|
|
53
|
+
* them to clients; each is still compiled in its own try/catch here.
|
|
54
|
+
*/
|
|
55
|
+
patterns?: string[];
|
|
56
|
+
/** Mask ALL text in DOM recordings; screenshot recorders degrade pixels. */
|
|
57
|
+
maskAllText?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Disables redaction entirely — the self-host escape hatch. Only ever
|
|
60
|
+
* honored when the server explicitly served it; clients never default to it.
|
|
61
|
+
*/
|
|
62
|
+
fullFidelity?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** A compiled policy — build once per policy via {@link compileRules}. */
|
|
65
|
+
export interface RuleSet {
|
|
66
|
+
readonly full: boolean;
|
|
67
|
+
readonly maskAllText: boolean;
|
|
68
|
+
/** Normalized header names (defaults + policy extras). */
|
|
69
|
+
readonly headers: ReadonlySet<string>;
|
|
70
|
+
/** Normalized body-key names (defaults + policy extras). */
|
|
71
|
+
readonly bodyKeys: ReadonlySet<string>;
|
|
72
|
+
readonly patterns: readonly RegExp[];
|
|
73
|
+
}
|
|
74
|
+
/** Canonicalize a header/body-key name: lowercase, `-` and `_` removed. */
|
|
75
|
+
export declare function norm(s: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Compile a policy (or null/undefined = the safe defaults) into a RuleSet.
|
|
78
|
+
* Cached by policy identity, so calling per event is free. Bad regexes are
|
|
79
|
+
* skipped individually — the server backstop still applies them at ingest.
|
|
80
|
+
*/
|
|
81
|
+
export declare function compileRules(policy?: RedactionPolicy | null): RuleSet;
|
|
82
|
+
/**
|
|
83
|
+
* Token-wise secret detection: TOKEN matching, not substring, so `author`,
|
|
84
|
+
* `authorId`, `shippingAddress` and `pinned` stay intact while
|
|
85
|
+
* `Authorization`, `accessToken` and `X-Dev-Auth-Secret` are caught.
|
|
86
|
+
*/
|
|
87
|
+
export declare function isSecretKey(key: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Is this header's value scrubbed? Listed (defaults + policy), an
|
|
90
|
+
* api-key/token-suffixed variant (the server's suffix rule), or token-secret.
|
|
91
|
+
*/
|
|
92
|
+
export declare function sensitiveHeader(rules: RuleSet, name: string): boolean;
|
|
93
|
+
/** Is this JSON/form key's value scrubbed? Listed (normalized) or token-secret. */
|
|
94
|
+
export declare function sensitiveBodyKey(rules: RuleSet, name: string): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Is this URL query/fragment parameter's value scrubbed? Every sensitive body
|
|
97
|
+
* key, plus the api-key/token suffix rule (?access_token=…, ?sas_token=…).
|
|
98
|
+
*/
|
|
99
|
+
export declare function sensitiveParam(rules: RuleSet, name: string): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Redact sensitive-keyed pairs in one raw query/fragment/form string,
|
|
102
|
+
* splitting on BOTH separators (`&` and `;`). Never URLSearchParams (it
|
|
103
|
+
* treats `;` as a literal, hiding a trailing `;access_token=…` inside the
|
|
104
|
+
* previous value) and never the platform URL parser (React Native's is
|
|
105
|
+
* incomplete). A pair with a benign name still loses its value when the
|
|
106
|
+
* DECODED value carries an embedded secret pair (`?next=%2Fcb%3Ftoken%3D…`).
|
|
107
|
+
* A fully benign string comes back byte-identical.
|
|
108
|
+
*/
|
|
109
|
+
export declare function scrubPairs(rules: RuleSet, sensitive: (rules: RuleSet, name: string) => boolean, raw: string): string;
|
|
110
|
+
/**
|
|
111
|
+
* Redact sensitive query/fragment parameter values in one URL — OAuth/OIDC
|
|
112
|
+
* callbacks (?access_token=…), magic links (?token=…), SAS URLs. The fragment
|
|
113
|
+
* scrubs too (implicit-grant OAuth returns tokens as `#access_token=…`).
|
|
114
|
+
* String-split rather than URL-parsed: React Native's URL is incomplete, and
|
|
115
|
+
* an unparseable URL must never be a free pass. Benign URLs pass through
|
|
116
|
+
* byte-identical; the extra patterns run over the result.
|
|
117
|
+
*/
|
|
118
|
+
export declare function redactUrl(rules: RuleSet, raw: string): string;
|
|
119
|
+
/**
|
|
120
|
+
* Scrub one request/response body. JSON parses + scrubs recursively (shape
|
|
121
|
+
* preserved so a timeline still shows which fields were sent); a JSON-looking
|
|
122
|
+
* body that fails to parse — or is too big to parse safely — gets the
|
|
123
|
+
* key-pair fallback; form-encoded and multipart bodies scrub key-wise;
|
|
124
|
+
* anything else gets the free-text scanner + patterns. Never throws — capture
|
|
125
|
+
* must not break the recorded app.
|
|
126
|
+
*/
|
|
127
|
+
export declare function redactBody(rules: RuleSet, body: string, contentType?: string): string;
|
|
128
|
+
/**
|
|
129
|
+
* Redact a free-text capture (console line, unknown-shape body, URL-bearing
|
|
130
|
+
* log). JSON is walked structurally; anything else gets the text scanner.
|
|
131
|
+
* Never throws.
|
|
132
|
+
*/
|
|
133
|
+
export declare function redactText(rules: RuleSet, text: string | undefined): string | undefined;
|
|
134
|
+
/**
|
|
135
|
+
* Redact, then cap to `cap` characters — with the ORDER chosen by shape:
|
|
136
|
+
* JSON redacts WHOLE then caps (capping first would slice it into invalid
|
|
137
|
+
* JSON, downgrading exactly the payloads most likely to carry credentials);
|
|
138
|
+
* anything else caps first (slicing text is harmless and bounds masking
|
|
139
|
+
* cost), then the truncation fallback still guards the sliced tail.
|
|
140
|
+
*/
|
|
141
|
+
export declare function redactAndCap(rules: RuleSet, body: string, cap: number, contentType?: string): string | undefined;
|
|
142
|
+
/** Size caps for a captured header map (one giant cookie must not bloat events). */
|
|
143
|
+
export interface HeaderCaps {
|
|
144
|
+
/** Max characters kept of one header value (default 1024). */
|
|
145
|
+
maxValueLen?: number;
|
|
146
|
+
/** Total budget in characters across the whole map (default 8192). */
|
|
147
|
+
budget?: number;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Scrub + cap a captured header map: sensitive names lose their value, the
|
|
151
|
+
* rest pass the extra patterns; values are bounded individually and by a
|
|
152
|
+
* total budget (the map gains a `…: (truncated)` marker when it overflows).
|
|
153
|
+
*/
|
|
154
|
+
export declare function redactHeaders(rules: RuleSet, headers: Record<string, unknown> | undefined | null, caps?: HeaderCaps): Record<string, string> | undefined;
|
|
155
|
+
/** rrweb-style DOM-capture masking options derived from the rules. */
|
|
156
|
+
export interface MaskDirectives {
|
|
157
|
+
/** Fail-closed default: input VALUES are always masked unless fullFidelity. */
|
|
158
|
+
maskAllInputs: boolean;
|
|
159
|
+
/** Mask every text node (policy `maskAllText`). */
|
|
160
|
+
maskAllText: boolean;
|
|
161
|
+
/** Older rrweb spelling of maskAllText; set to `*` when it applies. */
|
|
162
|
+
maskTextSelector?: string;
|
|
163
|
+
}
|
|
164
|
+
/** DOM-capture masking for rrweb-style recorders. */
|
|
165
|
+
export declare function maskDirectives(rules: RuleSet): MaskDirectives;
|
|
166
|
+
/**
|
|
167
|
+
* What a PIXEL capture surface (screenshots — real rendered text, no DOM to
|
|
168
|
+
* mask) must do under these rules: `blur` = degrade resolution until text is
|
|
169
|
+
* unreadable while layout survives; `none` = capture normally.
|
|
170
|
+
*/
|
|
171
|
+
export declare function pixelPolicy(rules: RuleSet): 'none' | 'blur';
|