@descryy/runtime-evidence-store 0.0.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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Redaction (plan §28): "Raw runtime evidence must not automatically
3
+ * become unrestricted AI context." This runs at write time, unconditionally
4
+ * — `EvidenceStore.write` has no code path that skips it — rather than at
5
+ * read time, because a read-time pass is one forgotten call site away from
6
+ * failing (peer's framing, kept verbatim since it's exactly right).
7
+ *
8
+ * Extends the original key-name-only pass with the two capabilities §28
9
+ * names that a pure key-name walk structurally cannot reach:
10
+ *
11
+ * - **Body filtering.** `NETWORK_REQUEST`/`NETWORK_RESPONSE` evidence
12
+ * (`packages/browser`) carries `postData`/`body` as a JSON- or
13
+ * text-*encoded string*, not a parsed object -- the original redact()
14
+ * walks object structure, so a secret sitting inside that string (a
15
+ * `{"password": "hunter2"}` request body, say) was invisible to it
16
+ * regardless of how complete `SENSITIVE_KEY_PATTERN` was. Body-bearing
17
+ * keys now get parsed (if JSON) and recursed into with the same rules,
18
+ * or pattern-scanned as text otherwise.
19
+ * - **PII-aware field filtering.** A separate, independently toggleable
20
+ * pattern from credentials -- these are different concerns with
21
+ * different owners in a real system (security vs. privacy), and
22
+ * collapsing them into one pattern would make either impossible to
23
+ * reason about or configure on its own.
24
+ *
25
+ * **Configurable by whom, refused how (the two questions this row asked to
26
+ * have decided rather than defaulted):** by whoever constructs the
27
+ * `EvidenceStore` (`RedactionPolicy`, plumbed through
28
+ * `EvidenceStoreOptions` -- see `evidence-store.ts`), and every filter
29
+ * fails closed, disclosed, not silent: a value the body/PII filter cannot
30
+ * classify at all (not JSON, not text that looks safe to pattern-scan) is
31
+ * withheld wholesale rather than passed through on the assumption that
32
+ * "couldn't parse it" means "couldn't be sensitive." A marker replaces the
33
+ * value in place -- the key survives, so a reader can see *that* something
34
+ * was withheld, never just find it missing (same discipline the original
35
+ * key-based pass already established with `[REDACTED]`).
36
+ *
37
+ * **Not a claim of completeness.** Pattern-based text scanning is
38
+ * inherently partial -- a sensitive string shaped unlike every pattern
39
+ * here passes through unredacted, honestly, not silently promised as
40
+ * covered. What the "fail closed" guarantee actually claims: content this
41
+ * module cannot even classify never leaks by default; content it can
42
+ * classify gets the patterns below applied, not more.
43
+ */
44
+ import type { RedactionStatus } from "@descryy/runtime-contracts";
45
+ export interface RedactionPolicy {
46
+ /** Parse and recurse into JSON-shaped body/postData strings, applying the same key rules inside them. Default true -- restricted by default, not opt-in. */
47
+ readonly bodyFiltering?: boolean;
48
+ /** Also treat PII_KEY_PATTERN field names as sensitive, not just credentials. Default true. */
49
+ readonly piiFiltering?: boolean;
50
+ }
51
+ export interface RedactionResult {
52
+ readonly payload: unknown;
53
+ readonly redactionStatus: RedactionStatus;
54
+ /**
55
+ * The **key paths** this call redacted by name — `"sessionId"`,
56
+ * `"headers.authorization"`, `"items[0].email"`. Key names only; no value
57
+ * ever appears here, which is what makes it safe to log, assert on, and
58
+ * hand to an operator auditing a policy.
59
+ *
60
+ * **Added because `redactionStatus` alone made a real bug invisible for
61
+ * the life of a package (RT-063).** `SENSITIVE_KEY_PATTERN`'s
62
+ * `session(id)?` alternative matches the literal key `sessionId`, and all
63
+ * three browser collectors carried `sessionId: session.sessionId` — an
64
+ * internal `BrowserSession` handle, not a credential. Every
65
+ * `NAVIGATION` / `CLICK` / `INPUT` / `NETWORK_*` / `CONSOLE_MESSAGE` that
66
+ * package ever produced had it silently replaced with a marker.
67
+ *
68
+ * The status flag could not reveal it. `redacted` is what a real payload
69
+ * is *expected* to report, so the one signal that existed said exactly
70
+ * what it would have said if nothing were wrong — the silent-degradation
71
+ * shape this repo keeps meeting, in a module whose whole job is to be
72
+ * trustworthy about what it touched.
73
+ *
74
+ * A collector author can now assert `redactedKeys` is empty for a benign
75
+ * payload, which is a claim the boolean cannot express.
76
+ *
77
+ * **Text-scan hits are not listed here.** A secret found inside a body
78
+ * string by pattern has no key path — it was matched in free text — and
79
+ * inventing one (`"body[chars 40-64]"`) would put a fragment of the
80
+ * surrounding value into a field documented as name-only. Those still
81
+ * flip `redactionStatus`; `redactedKeys` answers "which field did the
82
+ * policy hit by name", not "was anything redacted at all".
83
+ */
84
+ readonly redactedKeys: readonly string[];
85
+ }
86
+ export declare function redact(payload: unknown, policy?: RedactionPolicy): RedactionResult;
87
+ /**
88
+ * Which keys the default policy would redact, without redacting anything.
89
+ *
90
+ * The tool a collector author needs *before* shipping a payload shape: hand
91
+ * it the key names, get back the ones that collide and would be wiped. The
92
+ * `sessionId` collision (RT-063) was a name nobody thought to check against
93
+ * a pattern list nobody thought to read, and the check costs one call.
94
+ */
95
+ export declare function collidingKeys(keys: readonly string[], policy?: RedactionPolicy): readonly string[];
96
+ /**
97
+ * Redact credential values out of a command line, keeping the shape.
98
+ *
99
+ * Two forms, because a command line has two:
100
+ *
101
+ * - **Attached** -- `--api-key=SEKRET`, `TOKEN:abc`, `DB_PASSWORD=hunter2`.
102
+ * The name and the separator survive; only the value is replaced.
103
+ * - **Detached** -- `["--api-key", "SEKRET"]` or `--api-key SEKRET`, where the
104
+ * secret is the *next* token. This is the form a naive per-token scanner
105
+ * misses entirely: `SEKRET` on its own carries nothing that marks it as a
106
+ * secret, and the only thing that does is the token before it.
107
+ *
108
+ * A detached flag at the very end of a command consumes nothing -- there is
109
+ * no value to redact, and inventing one would corrupt the command.
110
+ *
111
+ * The name and separator are kept on purpose. A command redacted to
112
+ * `[REDACTED]` wholesale is useless to the reader it was persisted for; a
113
+ * command redacted to `node server.mjs --api-key=[REDACTED]` still answers
114
+ * "what was run, and with what shape of argument".
115
+ */
116
+ export declare function redactCommand(value: string | readonly string[]): {
117
+ value: string | string[];
118
+ redacted: boolean;
119
+ };
120
+ //# sourceMappingURL=redaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AA2GlE,MAAM,WAAW,eAAe;IAC9B,4JAA4J;IAC5J,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,+FAA+F;IAC/F,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC;AAID,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,wBAAgB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,GAAE,eAAoB,GAAG,eAAe,CAStF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,GAAE,eAAoB,GAAG,SAAS,MAAM,EAAE,CAQtG;AA2HD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAahH"}
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Redaction (plan §28): "Raw runtime evidence must not automatically
3
+ * become unrestricted AI context." This runs at write time, unconditionally
4
+ * — `EvidenceStore.write` has no code path that skips it — rather than at
5
+ * read time, because a read-time pass is one forgotten call site away from
6
+ * failing (peer's framing, kept verbatim since it's exactly right).
7
+ *
8
+ * Extends the original key-name-only pass with the two capabilities §28
9
+ * names that a pure key-name walk structurally cannot reach:
10
+ *
11
+ * - **Body filtering.** `NETWORK_REQUEST`/`NETWORK_RESPONSE` evidence
12
+ * (`packages/browser`) carries `postData`/`body` as a JSON- or
13
+ * text-*encoded string*, not a parsed object -- the original redact()
14
+ * walks object structure, so a secret sitting inside that string (a
15
+ * `{"password": "hunter2"}` request body, say) was invisible to it
16
+ * regardless of how complete `SENSITIVE_KEY_PATTERN` was. Body-bearing
17
+ * keys now get parsed (if JSON) and recursed into with the same rules,
18
+ * or pattern-scanned as text otherwise.
19
+ * - **PII-aware field filtering.** A separate, independently toggleable
20
+ * pattern from credentials -- these are different concerns with
21
+ * different owners in a real system (security vs. privacy), and
22
+ * collapsing them into one pattern would make either impossible to
23
+ * reason about or configure on its own.
24
+ *
25
+ * **Configurable by whom, refused how (the two questions this row asked to
26
+ * have decided rather than defaulted):** by whoever constructs the
27
+ * `EvidenceStore` (`RedactionPolicy`, plumbed through
28
+ * `EvidenceStoreOptions` -- see `evidence-store.ts`), and every filter
29
+ * fails closed, disclosed, not silent: a value the body/PII filter cannot
30
+ * classify at all (not JSON, not text that looks safe to pattern-scan) is
31
+ * withheld wholesale rather than passed through on the assumption that
32
+ * "couldn't parse it" means "couldn't be sensitive." A marker replaces the
33
+ * value in place -- the key survives, so a reader can see *that* something
34
+ * was withheld, never just find it missing (same discipline the original
35
+ * key-based pass already established with `[REDACTED]`).
36
+ *
37
+ * **Not a claim of completeness.** Pattern-based text scanning is
38
+ * inherently partial -- a sensitive string shaped unlike every pattern
39
+ * here passes through unredacted, honestly, not silently promised as
40
+ * covered. What the "fail closed" guarantee actually claims: content this
41
+ * module cannot even classify never leaks by default; content it can
42
+ * classify gets the patterns below applied, not more.
43
+ */
44
+ const SENSITIVE_KEY_PATTERN = /^(authorization|cookie|set-cookie|token|secret|password|passwd|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|private[-_]?key|session(id)?|bearer)$/i;
45
+ const PII_KEY_PATTERN = /^(email|e-?mail(?:[-_]?address)?|phone(?:[-_]?number)?|mobile|ssn|social[-_]?security(?:[-_]?number)?|dob|date[-_]?of[-_]?birth|address|street[-_]?address|first[-_]?name|last[-_]?name|full[-_]?name|credit[-_]?card(?:[-_]?number)?|card[-_]?number|cvv|national[-_]?id|passport(?:[-_]?number)?)$/i;
46
+ /**
47
+ * Free-text-bearing keys: request/response bodies AND §28's separate
48
+ * "sensitive log filtering" item -- `raw` is `LogCollector`'s payload key
49
+ * for a `BACKEND_LOG`/`EXCEPTION`/`STACK_TRACE`'s captured text
50
+ * (`backend-observation/collector.ts`), the same "an app can print a
51
+ * secret as plain text, not just carry one as a structured field" problem
52
+ * body filtering exists for, just from a different producer.
53
+ */
54
+ const BODY_BEARING_KEY_PATTERN = /^(body|post[-_]?data|request[-_]?body|response[-_]?body|raw)$/i;
55
+ /**
56
+ * Keys whose value is a **command line** -- `command`, `args`, `argv`, `cmd`.
57
+ *
58
+ * **Added because a claim was written before it was true (RT-069).** The
59
+ * execution record persisted for replay carries
60
+ * `configuration.services[x].command`, and the note justifying it said a
61
+ * command line is one of the classic places a credential travels. It is --
62
+ * and nothing here caught one. Measured: `node server.mjs --api-key=SEKRET`
63
+ * went through `redact()` untouched, `redactedKeys: []`. `command` is not
64
+ * body-bearing, so it was never scanned; and even body scanning would have
65
+ * missed it, since the text patterns look for emails, card numbers and long
66
+ * tokens, and a short flag value is none of those.
67
+ *
68
+ * A command line needs its own scanner because its secrets are **positional
69
+ * rather than lexical**: what marks `SEKRET` as sensitive is not the value's
70
+ * shape but the *flag in front of it*.
71
+ */
72
+ const COMMAND_BEARING_KEY_PATTERN = /^(command|args|argv|cmd)$/i;
73
+ /**
74
+ * A flag or environment assignment whose NAME says the value after it is a
75
+ * credential: `--api-key=x`, `-token x`, `DATABASE_PASSWORD=x`,
76
+ * `--client-secret=x`.
77
+ *
78
+ * Deliberately name-driven and deliberately narrow. The alternative --
79
+ * redacting anything that looks high-entropy -- destroys ordinary arguments
80
+ * (a commit sha, a port, a base64 fixture path) and would make a redacted
81
+ * command unreadable, which costs the reader the thing a command line is
82
+ * kept for.
83
+ */
84
+ const SENSITIVE_ARG_NAME = /(?:key|token|secret|password|passwd|auth|credential|bearer|cookie)/i;
85
+ /**
86
+ * Names that contain a sensitive word but whose value is a **location, not a
87
+ * credential**: `--keys-dir`, `--secret-file`, `--token-path`, `--key-store`.
88
+ *
89
+ * Measured while writing this: `--keys-dir keys/dev.json` was redacted, which
90
+ * is a false positive that costs a reader a real path and protects nothing --
91
+ * the secret is the file's *contents*, which never appear on the command
92
+ * line. Excluded so the scanner keeps the information it has no reason to
93
+ * destroy.
94
+ */
95
+ const LOCATION_ARG_NAME = /(?:[-_](?:dir|directory|path|file|store|folder)|file|path)$/i;
96
+ const ARG_ASSIGNMENT = /^(-{0,2}[A-Za-z0-9_.-]*?)([=:])(.+)$/;
97
+ const REDACTED_MARKER = "[REDACTED]";
98
+ const REDACTED_BODY_MARKER = "[REDACTED-BODY]";
99
+ // Deliberately ordered narrowest-to-broadest so an email or card number
100
+ // inside what would also match the generic token pattern gets the more
101
+ // specific marker.
102
+ /**
103
+ * **Every quantifier here is bounded, and that is a fix rather than a style
104
+ * choice (RT-069).** The previous form was
105
+ * `/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g`, whose unbounded
106
+ * leading `+` backtracks catastrophically on text containing no `@`: the
107
+ * class consumes the whole input, fails to find `@`, gives back one
108
+ * character, and retries — O(n²).
109
+ *
110
+ * Measured, on a string of `x` with no `@` in it:
111
+ *
112
+ * | input | old | new |
113
+ * | --- | --- | --- |
114
+ * | 16 KB | 98 ms | 2.8 ms |
115
+ * | 64 KB | 2,073 ms | 10.7 ms |
116
+ * | 2 MB | ~35 min (extrapolated) | 218 ms |
117
+ *
118
+ * **This was a denial of service on our own collection path, reachable by
119
+ * ordinary application output.** Any backend log line of a few tens of
120
+ * kilobytes without an `@` — a serialised object, a stack dump, a base64
121
+ * blob — would wedge `redact()`, and `redact()` sits on the only write path
122
+ * into the store. Found by a §19 "large payload" test that hung instead of
123
+ * failing, which is the one symptom easy to mistake for a slow test.
124
+ *
125
+ * The bounds are RFC-shaped rather than invented: 64 characters for the local
126
+ * part (RFC 5321's limit), 63 per domain label, up to eight labels, 2–24 for
127
+ * the TLD. Verified behaviourally identical to the old pattern on every
128
+ * address form in this file's tests — the change costs no match and removes
129
+ * the quadratic.
130
+ */
131
+ const EMAIL_PATTERN = /[a-zA-Z0-9._%+-]{1,64}@[a-zA-Z0-9-]{1,63}(?:\.[a-zA-Z0-9-]{1,63}){0,8}\.[a-zA-Z]{2,24}/g;
132
+ // Anchored to always end on a digit -- (?:\d[ -]?){13,16} over-consumes a
133
+ // trailing space/dash that follows the number but isn't part of it
134
+ // (caught by a real test: "card 4111 1111 1111 1111 successfully" matched
135
+ // through the space before "successfully").
136
+ const CREDIT_CARD_PATTERN = /\b\d(?:[ -]?\d){12,15}\b/g;
137
+ const GENERIC_TOKEN_PATTERN = /\b[A-Za-z0-9_-]{24,}\b/g;
138
+ const TEXT_SCAN_PATTERNS = [EMAIL_PATTERN, CREDIT_CARD_PATTERN, GENERIC_TOKEN_PATTERN];
139
+ const DEFAULT_POLICY = { bodyFiltering: true, piiFiltering: true };
140
+ export function redact(payload, policy = {}) {
141
+ const resolved = { ...DEFAULT_POLICY, ...policy };
142
+ const redactedKeys = [];
143
+ const { value, redacted } = redactValue(payload, resolved, "", redactedKeys);
144
+ return {
145
+ payload: value,
146
+ redactionStatus: redacted ? "redacted" : "not-required",
147
+ redactedKeys,
148
+ };
149
+ }
150
+ /**
151
+ * Which keys the default policy would redact, without redacting anything.
152
+ *
153
+ * The tool a collector author needs *before* shipping a payload shape: hand
154
+ * it the key names, get back the ones that collide and would be wiped. The
155
+ * `sessionId` collision (RT-063) was a name nobody thought to check against
156
+ * a pattern list nobody thought to read, and the check costs one call.
157
+ */
158
+ export function collidingKeys(keys, policy = {}) {
159
+ const resolved = { ...DEFAULT_POLICY, ...policy };
160
+ return keys.filter((key) => SENSITIVE_KEY_PATTERN.test(key) ||
161
+ (resolved.piiFiltering && PII_KEY_PATTERN.test(key)) ||
162
+ (resolved.bodyFiltering && BODY_BEARING_KEY_PATTERN.test(key)));
163
+ }
164
+ function join(prefix, key) {
165
+ return prefix === "" ? key : `${prefix}.${key}`;
166
+ }
167
+ function redactValue(value, policy, path, redactedKeys) {
168
+ if (Array.isArray(value)) {
169
+ let redacted = false;
170
+ const result = value.map((item, index) => {
171
+ const inner = redactValue(item, policy, `${path}[${index}]`, redactedKeys);
172
+ redacted = redacted || inner.redacted;
173
+ return inner.value;
174
+ });
175
+ return { value: result, redacted };
176
+ }
177
+ if (value !== null && typeof value === "object") {
178
+ let redacted = false;
179
+ const result = {};
180
+ for (const [key, val] of Object.entries(value)) {
181
+ if (SENSITIVE_KEY_PATTERN.test(key) || (policy.piiFiltering && PII_KEY_PATTERN.test(key))) {
182
+ result[key] = REDACTED_MARKER;
183
+ redactedKeys.push(join(path, key));
184
+ redacted = true;
185
+ }
186
+ else if (COMMAND_BEARING_KEY_PATTERN.test(key) && (typeof val === "string" || Array.isArray(val))) {
187
+ const inner = redactCommand(val);
188
+ result[key] = inner.value;
189
+ if (inner.redacted)
190
+ redactedKeys.push(join(path, key));
191
+ redacted = redacted || inner.redacted;
192
+ }
193
+ else if (policy.bodyFiltering && typeof val === "string" && BODY_BEARING_KEY_PATTERN.test(key)) {
194
+ const inner = redactBodyString(val, policy, join(path, key), redactedKeys);
195
+ result[key] = inner.value;
196
+ redacted = redacted || inner.redacted;
197
+ }
198
+ else {
199
+ const inner = redactValue(val, policy, join(path, key), redactedKeys);
200
+ result[key] = inner.value;
201
+ redacted = redacted || inner.redacted;
202
+ }
203
+ }
204
+ return { value: result, redacted };
205
+ }
206
+ // A body-bearing key's value that isn't a string (null, or a non-string
207
+ // primitive some future producer emits) has nothing this module can
208
+ // scan -- left as-is, since a bare number or boolean can't itself carry
209
+ // a secret the way an encoded string can.
210
+ return { value, redacted: false };
211
+ }
212
+ /** JSON-shaped body content is parsed and recursed into; anything else is scanned as text, or withheld wholesale if it doesn't even look like text. */
213
+ function redactBodyString(text, policy, path, redactedKeys) {
214
+ const parsed = tryParseJson(text);
215
+ if (parsed !== undefined) {
216
+ // The body's own keys are reported under the body's path, so an operator
217
+ // can tell `headers.authorization` from `body.authorization` — different
218
+ // producers, different fixes.
219
+ const inner = redactValue(parsed, policy, path, redactedKeys);
220
+ return { value: JSON.stringify(inner.value), redacted: inner.redacted };
221
+ }
222
+ if (!looksLikeText(text)) {
223
+ // Fails closed: content this module cannot even classify as text is
224
+ // withheld wholesale rather than passed through on the assumption
225
+ // that "couldn't parse it" means "couldn't be sensitive."
226
+ redactedKeys.push(path);
227
+ return { value: REDACTED_BODY_MARKER, redacted: true };
228
+ }
229
+ return redactPlainText(text);
230
+ }
231
+ function tryParseJson(text) {
232
+ const trimmed = text.trim();
233
+ if (trimmed.length === 0 || (trimmed[0] !== "{" && trimmed[0] !== "[")) {
234
+ // A bare string/number/boolean JSON body ("true", "42") is technically
235
+ // valid JSON but never carries nested keys to redact -- treated as
236
+ // plain text instead, so the patterns below still get a chance at it.
237
+ return undefined;
238
+ }
239
+ try {
240
+ return JSON.parse(trimmed);
241
+ }
242
+ catch {
243
+ return undefined;
244
+ }
245
+ }
246
+ /** A crude but deliberate bar: mostly printable text, not a binary blob wearing a string's clothes. */
247
+ function looksLikeText(text) {
248
+ if (text.length === 0)
249
+ return true;
250
+ let controlChars = 0;
251
+ for (let i = 0; i < text.length; i++) {
252
+ const code = text.charCodeAt(i);
253
+ const isCommonWhitespace = code === 9 || code === 10 || code === 13;
254
+ if (!isCommonWhitespace && code < 0x20) {
255
+ controlChars++;
256
+ }
257
+ }
258
+ return controlChars / text.length < 0.05;
259
+ }
260
+ function redactPlainText(text) {
261
+ let redacted = false;
262
+ let result = text;
263
+ for (const pattern of TEXT_SCAN_PATTERNS) {
264
+ result = result.replace(pattern, () => {
265
+ redacted = true;
266
+ return REDACTED_MARKER;
267
+ });
268
+ }
269
+ return { value: result, redacted };
270
+ }
271
+ /**
272
+ * Redact credential values out of a command line, keeping the shape.
273
+ *
274
+ * Two forms, because a command line has two:
275
+ *
276
+ * - **Attached** -- `--api-key=SEKRET`, `TOKEN:abc`, `DB_PASSWORD=hunter2`.
277
+ * The name and the separator survive; only the value is replaced.
278
+ * - **Detached** -- `["--api-key", "SEKRET"]` or `--api-key SEKRET`, where the
279
+ * secret is the *next* token. This is the form a naive per-token scanner
280
+ * misses entirely: `SEKRET` on its own carries nothing that marks it as a
281
+ * secret, and the only thing that does is the token before it.
282
+ *
283
+ * A detached flag at the very end of a command consumes nothing -- there is
284
+ * no value to redact, and inventing one would corrupt the command.
285
+ *
286
+ * The name and separator are kept on purpose. A command redacted to
287
+ * `[REDACTED]` wholesale is useless to the reader it was persisted for; a
288
+ * command redacted to `node server.mjs --api-key=[REDACTED]` still answers
289
+ * "what was run, and with what shape of argument".
290
+ */
291
+ export function redactCommand(value) {
292
+ if (Array.isArray(value)) {
293
+ const out = redactTokens(value);
294
+ return { value: out.tokens, redacted: out.redacted };
295
+ }
296
+ // Split on whitespace, redact, rejoin. This does not attempt to parse
297
+ // shell quoting -- `tokenizeCommand` (controller) already refuses shell
298
+ // metacharacters, so a command reaching here is a plain argv-shaped
299
+ // string, and a quoting parser would be machinery for a case this repo
300
+ // deliberately does not accept.
301
+ const tokens = String(value).split(/\s+/);
302
+ const out = redactTokens(tokens);
303
+ return { value: out.tokens.join(" "), redacted: out.redacted };
304
+ }
305
+ function redactTokens(tokens) {
306
+ const result = [];
307
+ let redacted = false;
308
+ let consumeNext = false;
309
+ for (const token of tokens) {
310
+ if (consumeNext) {
311
+ result.push(REDACTED_MARKER);
312
+ redacted = true;
313
+ consumeNext = false;
314
+ continue;
315
+ }
316
+ const assignment = ARG_ASSIGNMENT.exec(token);
317
+ if (assignment !== null && isSensitiveArgName(assignment[1] ?? "")) {
318
+ result.push(`${assignment[1]}${assignment[2]}${REDACTED_MARKER}`);
319
+ redacted = true;
320
+ continue;
321
+ }
322
+ // A bare sensitive-looking flag arms the next token. Only a flag --
323
+ // a bare word containing "key" (a path like `keys/dev.json`) must not
324
+ // swallow whatever follows it.
325
+ if (token.startsWith("-") && isSensitiveArgName(token)) {
326
+ result.push(token);
327
+ consumeNext = true;
328
+ continue;
329
+ }
330
+ result.push(token);
331
+ }
332
+ return { tokens: result, redacted };
333
+ }
334
+ function isSensitiveArgName(name) {
335
+ return SENSITIVE_ARG_NAME.test(name) && !LOCATION_ARG_NAME.test(name);
336
+ }
337
+ //# sourceMappingURL=redaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redaction.js","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Replay (plan §24).
3
+ *
4
+ * ## What this replays, and what it does not
5
+ *
6
+ * **It replays evidence, not the application.** Given an `executionId`, it
7
+ * returns the recorded execution, the environment it was recorded in, and its
8
+ * evidence in recorded order — enough to re-derive correlation, findings and
9
+ * an evidence package without booting anything. It does not spawn a process,
10
+ * drive a browser, or reissue a request, and it never will from here: those
11
+ * are `descry-runtime`'s live path, and a "replay" that quietly re-executed
12
+ * would produce a *new* observation while claiming to reproduce an old one.
13
+ *
14
+ * That distinction is the whole reason this file names its own limits at the
15
+ * top. §24's row was `[ ]` with the note *"having the data is not having the
16
+ * capability"* — the opposite error is equally available, and it is shipping
17
+ * something called replay that reproduces less than the name promises.
18
+ *
19
+ * ## The gap this had to close first
20
+ *
21
+ * Evidence survived a process restart. **The execution that produced it did
22
+ * not.** `Execution` lived in `ExecutionController`'s memory and nowhere on
23
+ * disk, so after a restart the store held a pile of rows keyed by an
24
+ * `executionId` that no longer resolved to an application, a commit, or a
25
+ * configuration. §24 marked "execution configuration — persisted with the
26
+ * execution" as done, and it was true of the object and false of the disk.
27
+ *
28
+ * `recordExecution` is that missing half. It is a **new table**, never a new
29
+ * column on `evidence` — an added table appears under `CREATE TABLE IF NOT
30
+ * EXISTS` on an existing store file, where an added column would need a
31
+ * migration this package has no mechanism for.
32
+ *
33
+ * ## Redaction applies here too, and that is not obvious
34
+ *
35
+ * §20's rows are all about payloads, and every one of them is about evidence.
36
+ * But a service command is `configuration.services[x].command`, and a command
37
+ * line is one of the classic places a credential is passed —
38
+ * `node server.mjs --api-key=…`, `DATABASE_URL=… node app.js`. The execution
39
+ * record goes through the same `redact()` the evidence write path uses, for
40
+ * the same reason and on the same terms. `redactedKeys` (RT-063) reports what
41
+ * it caught.
42
+ *
43
+ * ## The environment fingerprint is deliberately narrow
44
+ *
45
+ * Four fields: node version, platform, arch, and the declared service
46
+ * commands. **`process.env` is not captured, and its absence is the design.**
47
+ * §20 records that the injected process environment is not captured or
48
+ * filtered — *"so there is nothing to leak yet, which is not the same as
49
+ * filtering."* Capturing it here to improve reproducibility would create
50
+ * exactly the leak that row says does not exist, inside the one module whose
51
+ * output is meant to be safe to hand to a model. A narrower fingerprint that
52
+ * can be trusted beats a fuller one that cannot.
53
+ */
54
+ import type { Evidence } from "@descryy/runtime-contracts";
55
+ /** What was true of the machine when an execution was recorded. Narrow on purpose — see this file's header. */
56
+ export interface EnvironmentFingerprint {
57
+ /** `process.version`, e.g. `v22.23.1`. */
58
+ readonly nodeVersion: string;
59
+ readonly platform: string;
60
+ readonly arch: string;
61
+ /**
62
+ * `serviceName -> command`, from the execution's own configuration.
63
+ * Redacted on the way in: a command line is a classic credential carrier.
64
+ */
65
+ readonly serviceCommands: Readonly<Record<string, string>>;
66
+ }
67
+ /** The recorded half of an execution — what `ExecutionController` held only in memory. */
68
+ export interface RecordedExecution {
69
+ readonly executionId: string;
70
+ readonly application: string;
71
+ readonly repository: string;
72
+ readonly commit: string;
73
+ readonly recordedAt: string;
74
+ readonly configuration: unknown;
75
+ readonly environment: EnvironmentFingerprint;
76
+ /** Key paths redaction removed from the configuration on the way in. Empty is a claim, not an absence (RT-063). */
77
+ readonly redactedKeys: readonly string[];
78
+ }
79
+ export interface ReplayWarning {
80
+ readonly kind: "environment-changed" | "no-evidence" | "unordered-timestamps";
81
+ readonly detail: string;
82
+ }
83
+ export interface Replay {
84
+ readonly execution: RecordedExecution;
85
+ /** In recorded order, oldest first. */
86
+ readonly evidence: readonly Evidence[];
87
+ /** The environment replay is happening *in*, for comparison against the recorded one. */
88
+ readonly currentEnvironment: EnvironmentFingerprint;
89
+ /**
90
+ * Never empty for a degraded replay, and each one names what differs
91
+ * rather than asserting the replay is invalid — whether a node-version
92
+ * change matters is the reader's call, not this module's.
93
+ */
94
+ readonly warnings: readonly ReplayWarning[];
95
+ }
96
+ /**
97
+ * The current machine's fingerprint.
98
+ *
99
+ * `serviceCommands` comes from the caller's configuration rather than being
100
+ * discovered, because there is nothing on a machine to discover it from — an
101
+ * execution's services are a property of the run, not of the host.
102
+ */
103
+ export declare function captureEnvironment(serviceCommands?: Readonly<Record<string, string>>, runtime?: {
104
+ version: string;
105
+ platform: string;
106
+ arch: string;
107
+ }): EnvironmentFingerprint;
108
+ /**
109
+ * Differences between the environment an execution was recorded in and the
110
+ * one it is being replayed in.
111
+ *
112
+ * **Reported, never enforced.** A replay in a different node version is not
113
+ * wrong; it is a fact a reader needs when a re-derived finding disagrees with
114
+ * the original. Refusing to replay would be this module deciding a question
115
+ * it has no information about.
116
+ */
117
+ export declare function environmentDifferences(recorded: EnvironmentFingerprint, current: EnvironmentFingerprint): readonly string[];
118
+ //# sourceMappingURL=replay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAE3D,+GAA+G;AAC/G,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC5D;AAED,0FAA0F;AAC1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAC;IAC7C,mHAAmH;IACnH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,qBAAqB,GAAG,aAAa,GAAG,sBAAsB,CAAC;IAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;IACtC,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,CAAC;IACvC,yFAAyF;IACzF,QAAQ,CAAC,kBAAkB,EAAE,sBAAsB,CAAC;IACpD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,eAAe,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,EACtD,OAAO,GAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAY,GACrE,sBAAsB,CAOxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,sBAAsB,EAChC,OAAO,EAAE,sBAAsB,GAC9B,SAAS,MAAM,EAAE,CAsBnB"}