@mossbear/eval-core 0.1.0-alpha.100
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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/adapter.d.ts +31 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +10 -0
- package/dist/adapter.js.map +1 -0
- package/dist/credential-patterns.d.ts +9 -0
- package/dist/credential-patterns.d.ts.map +1 -0
- package/dist/credential-patterns.js +9 -0
- package/dist/credential-patterns.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/redact-prose.d.ts +77 -0
- package/dist/redact-prose.d.ts.map +1 -0
- package/dist/redact-prose.js +339 -0
- package/dist/redact-prose.js.map +1 -0
- package/dist/redact-prose.test.d.ts +2 -0
- package/dist/redact-prose.test.d.ts.map +1 -0
- package/dist/redact-prose.test.js +429 -0
- package/dist/redact-prose.test.js.map +1 -0
- package/dist/redaction.d.ts +23 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +23 -0
- package/dist/redaction.js.map +1 -0
- package/dist/sanitize-command.d.ts +21 -0
- package/dist/sanitize-command.d.ts.map +1 -0
- package/dist/sanitize-command.js +908 -0
- package/dist/sanitize-command.js.map +1 -0
- package/dist/sanitize-command.test.d.ts +2 -0
- package/dist/sanitize-command.test.d.ts.map +1 -0
- package/dist/sanitize-command.test.js +867 -0
- package/dist/sanitize-command.test.js.map +1 -0
- package/dist/steering-classification.d.ts +19 -0
- package/dist/steering-classification.d.ts.map +1 -0
- package/dist/steering-classification.js +55 -0
- package/dist/steering-classification.js.map +1 -0
- package/dist/strip-code-blocks.d.ts +68 -0
- package/dist/strip-code-blocks.d.ts.map +1 -0
- package/dist/strip-code-blocks.js +280 -0
- package/dist/strip-code-blocks.js.map +1 -0
- package/dist/strip-code-blocks.test.d.ts +2 -0
- package/dist/strip-code-blocks.test.d.ts.map +1 -0
- package/dist/strip-code-blocks.test.js +250 -0
- package/dist/strip-code-blocks.test.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mossbear
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @mossbear/eval-core
|
|
2
|
+
|
|
3
|
+
Portable pieces of Mossbear's evaluation path — command redaction, the `EvalAdapter` LLM seam, and on-demand guide normalization. Grading itself runs on the [Mossbear](https://app.mossbear.io) dashboard; this package holds the parts that must stay importable outside it.
|
|
4
|
+
|
|
5
|
+
`sanitizeCommand` is used by the [`mossbear` CLI](https://www.npmjs.com/package/mossbear) to redact shell command text before an action is ever recorded; the adapter and normalization prompt are used server-side.
|
|
6
|
+
|
|
7
|
+
You normally don't install this directly; it ships as a dependency of `mossbear`.
|
|
8
|
+
|
|
9
|
+
Source: https://github.com/mossbear-io/mossbear (`packages/eval-core`)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The LLM completion seam.
|
|
3
|
+
*
|
|
4
|
+
* eval-core never imports fetch or any HTTP library directly — a caller
|
|
5
|
+
* supplies the transport and this package stays dependency-free and testable.
|
|
6
|
+
* The dashboard implements it in `src/lib/eval/serverEvalAdapter.ts`; tests
|
|
7
|
+
* pass mocks.
|
|
8
|
+
*/
|
|
9
|
+
/** Token counts for one completion call, when an adapter can report them. */
|
|
10
|
+
export type EvalUsage = {
|
|
11
|
+
inputTokens: number;
|
|
12
|
+
outputTokens: number;
|
|
13
|
+
cacheReadTokens?: number;
|
|
14
|
+
cacheWriteTokens?: number;
|
|
15
|
+
};
|
|
16
|
+
/** Adapter interface for LLM completion. */
|
|
17
|
+
export interface EvalAdapter {
|
|
18
|
+
complete(system: string, user: string): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Optional: adapters that can report token usage alongside a completion's
|
|
21
|
+
* text implement this instead of (or in addition to) `complete`. Callers
|
|
22
|
+
* that care about usage/cost attribution
|
|
23
|
+
* call this when present; an adapter without it is still a valid
|
|
24
|
+
* `EvalAdapter`.
|
|
25
|
+
*/
|
|
26
|
+
completeWithUsage?(system: string, user: string): Promise<{
|
|
27
|
+
text: string;
|
|
28
|
+
usage: EvalUsage | null;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6EAA6E;AAC7E,MAAM,MAAM,SAAS,GAAG;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACvD;;;;;;OAMG;IACH,iBAAiB,CAAC,CAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC,CAAA;CACtD"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The LLM completion seam.
|
|
3
|
+
*
|
|
4
|
+
* eval-core never imports fetch or any HTTP library directly — a caller
|
|
5
|
+
* supplies the transport and this package stays dependency-free and testable.
|
|
6
|
+
* The dashboard implements it in `src/lib/eval/serverEvalAdapter.ts`; tests
|
|
7
|
+
* pass mocks.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Syntax shared by the command and prose credential scanners. */
|
|
2
|
+
export declare const SENSITIVE_WORD_SOURCE = "(?:api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|authorization|token|secret|passw(?:or)?d|credentials?)";
|
|
3
|
+
export declare const SENSITIVE_KEY_SOURCE = "[\\w.-]*(?:api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|authorization|token|secret|passw(?:or)?d|credentials?)[\\w.-]*";
|
|
4
|
+
export declare const SHELL_WORD_SOURCE: string;
|
|
5
|
+
/** Whole segments in prose, avoiding ordinary words such as secretary. */
|
|
6
|
+
export declare const PROSE_SENSITIVE_KEY_SOURCE = "(?:[A-Za-z0-9]+[._-])*(?:api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|authorization|token|secret|passw(?:or)?d|credentials?)(?:[._-][A-Za-z0-9]+)*";
|
|
7
|
+
/** Cookie pairs, bounded by header quoting or the next prose/shell word. */
|
|
8
|
+
export declare const COOKIE_HEADER_SOURCE: string;
|
|
9
|
+
//# sourceMappingURL=credential-patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-patterns.d.ts","sourceRoot":"","sources":["../src/credential-patterns.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,eAAO,MAAM,qBAAqB,0IACuG,CAAA;AACzI,eAAO,MAAM,oBAAoB,0JAA6C,CAAA;AAC9E,eAAO,MAAM,iBAAiB,QAAgG,CAAA;AAE9H,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,sLAAyE,CAAA;AAEhH,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,QAAuN,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Syntax shared by the command and prose credential scanners. */
|
|
2
|
+
export const SENSITIVE_WORD_SOURCE = '(?:api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|authorization|token|secret|passw(?:or)?d|credentials?)';
|
|
3
|
+
export const SENSITIVE_KEY_SOURCE = `[\\w.-]*${SENSITIVE_WORD_SOURCE}[\\w.-]*`;
|
|
4
|
+
export const SHELL_WORD_SOURCE = String.raw `(?:\$'(?:\\[\s\S]|[^'\\])*'|'[^']*'|"(?:\\.|[^"\\])*"|\\[\s\S]|[^\s;&|()<>'"\\])+`;
|
|
5
|
+
/** Whole segments in prose, avoiding ordinary words such as secretary. */
|
|
6
|
+
export const PROSE_SENSITIVE_KEY_SOURCE = `(?:[A-Za-z0-9]+[._-])*${SENSITIVE_WORD_SOURCE}(?:[._-][A-Za-z0-9]+)*`;
|
|
7
|
+
/** Cookie pairs, bounded by header quoting or the next prose/shell word. */
|
|
8
|
+
export const COOKIE_HEADER_SOURCE = String.raw `\b((?:set-cookie|cookie)[ \t]*:[ \t]*)(?:[\w.!#$%&*+^\x60|~-]+[ \t]*=[ \t]*(?:"(?:\\.|[^"\\\r\n])*"|[^\s;"',]*))(?:[ \t]*;[ \t]*[\w.!#$%&*+^\x60|~-]+[ \t]*=[ \t]*(?:"(?:\\.|[^"\\\r\n])*"|[^\s;"',]*))*`;
|
|
9
|
+
//# sourceMappingURL=credential-patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-patterns.js","sourceRoot":"","sources":["../src/credential-patterns.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,MAAM,CAAC,MAAM,qBAAqB,GAChC,uIAAuI,CAAA;AACzI,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,qBAAqB,UAAU,CAAA;AAC9E,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAA,mFAAmF,CAAA;AAE9H,0EAA0E;AAC1E,MAAM,CAAC,MAAM,0BAA0B,GAAG,yBAAyB,qBAAqB,wBAAwB,CAAA;AAEhH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAA,0MAA0M,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mossbear/eval-core
|
|
3
|
+
*
|
|
4
|
+
* Pure evaluation logic with no filesystem, network, or secret-loading side effects.
|
|
5
|
+
* This package must remain importable outside the CLI package.
|
|
6
|
+
*/
|
|
7
|
+
export { type EvalAdapter, type EvalUsage } from './adapter.js';
|
|
8
|
+
export { sanitizeCommand } from './sanitize-command.js';
|
|
9
|
+
export { stripCodeBlocks, type CodeBlockStripResult } from './strip-code-blocks.js';
|
|
10
|
+
export { STEERING_LABELS, STEERING_CLASSIFIER_VERSION, classifySteeringTurn, classifySteeringTurnV1, scoreSteeringBaseline, type SteeringBaselineScore, type SteeringLabel, } from './steering-classification.js';
|
|
11
|
+
export { MAX_PROSE_CHARS, PROSE_REDACTION_CLASSES, redactProse, type ProseRedactionClass, type ProseRedactionReport, } from './redact-prose.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEvD,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAEnF,OAAO,EACL,eAAe,EACf,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,GACnB,MAAM,8BAA8B,CAAA;AAErC,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mossbear/eval-core
|
|
3
|
+
*
|
|
4
|
+
* Pure evaluation logic with no filesystem, network, or secret-loading side effects.
|
|
5
|
+
* This package must remain importable outside the CLI package.
|
|
6
|
+
*/
|
|
7
|
+
export { sanitizeCommand } from './sanitize-command.js';
|
|
8
|
+
export { stripCodeBlocks } from './strip-code-blocks.js';
|
|
9
|
+
export { STEERING_LABELS, STEERING_CLASSIFIER_VERSION, classifySteeringTurn, classifySteeringTurnV1, scoreSteeringBaseline, } from './steering-classification.js';
|
|
10
|
+
export { MAX_PROSE_CHARS, PROSE_REDACTION_CLASSES, redactProse, } from './redact-prose.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEvD,OAAO,EAAE,eAAe,EAA6B,MAAM,wBAAwB,CAAA;AAEnF,OAAO,EACL,eAAe,EACf,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,GAGtB,MAAM,8BAA8B,CAAA;AAErC,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,WAAW,GAGZ,MAAM,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret redaction and length-capping for **prose** — transcript message text,
|
|
3
|
+
* as opposed to the shell command text `sanitizeCommand` handles.
|
|
4
|
+
*
|
|
5
|
+
* Why a second redactor rather than reusing the first: `sanitizeCommand` is
|
|
6
|
+
* pattern-matched against *shell syntax* — heredocs, `KEY=value` anchored at a
|
|
7
|
+
* shell token boundary, `Authorization:` headers
|
|
8
|
+
* (`./sanitize-command.ts:31-49`). `transcript-content-classes-plan.md:20`
|
|
9
|
+
* records that it "catches none of it in prose" and names a prose redactor the
|
|
10
|
+
* load-bearing deliverable rather than a checkbox on one. This is that gate.
|
|
11
|
+
*
|
|
12
|
+
* It lives here beside `sanitizeCommand`, and not in the CLI, because the plan
|
|
13
|
+
* asks for exactly that (`transcript-content-classes-plan.md:43`): shared, so
|
|
14
|
+
* the CLI and any future server-side path cannot drift into different
|
|
15
|
+
* strengths of the same protection.
|
|
16
|
+
*
|
|
17
|
+
* **What this is not.** This is the redactor and nothing else: it does not
|
|
18
|
+
* extract transcript messages and does not strip code blocks. It is no longer
|
|
19
|
+
* true, as this header said through Slice 1, that "no caller uploads its
|
|
20
|
+
* output" — transcript narration ships, and `redactProse` runs on it
|
|
21
|
+
* (`../../cli/src/transcript/narration.ts:133`) before it is attached to an
|
|
22
|
+
* action at the `action-queue.ts` choke point and uploaded by `mossbear sync`.
|
|
23
|
+
* This function is now the last thing between prose and the wire, which is why
|
|
24
|
+
* a gap in the pattern list below is a live disclosure rather than a latent
|
|
25
|
+
* one. Two consequences of that are worth saying out loud rather than
|
|
26
|
+
* assuming:
|
|
27
|
+
*
|
|
28
|
+
* - **Code-block stripping is structural and happens before this runs.**
|
|
29
|
+
* Fenced and indented blocks come out by the Markdown structure of the
|
|
30
|
+
* message (Slice 2). A regex over prose must never be what stands between
|
|
31
|
+
* a code block and the wire, so nothing here attempts it.
|
|
32
|
+
* - **Redaction is best-effort; the length cap is the backstop.** The same
|
|
33
|
+
* admission `sanitizeCommand` makes (`./sanitize-command.ts:26-29`). A
|
|
34
|
+
* pattern list cannot enumerate every
|
|
35
|
+
* secret shape, which is exactly why the class table strips whole classes
|
|
36
|
+
* rather than trusting a scrubber to find secrets inside them.
|
|
37
|
+
*
|
|
38
|
+
* Reports **what it redacted, by class and count**, so a disclosure surface can
|
|
39
|
+
* show the user what was removed rather than asserting safety at them.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Maximum characters of a single prose message that survive redaction.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately far larger than `sanitizeCommand`'s 200
|
|
45
|
+
* (`./sanitize-command.ts:20`): a command is a tool
|
|
46
|
+
* invocation, where anything long is bulk payload, whereas prose is the
|
|
47
|
+
* content — "I'm going to X because Y" is the whole reason this class syncs at
|
|
48
|
+
* all, and truncating it to a couple of sentences would leave the grader with
|
|
49
|
+
* the same intent-free evidence it has today. Long enough for a real
|
|
50
|
+
* explanation, short enough that a pasted file body cannot ride along whole.
|
|
51
|
+
*/
|
|
52
|
+
export declare const MAX_PROSE_CHARS = 2000;
|
|
53
|
+
/**
|
|
54
|
+
* The classes this reports on. Named rather than free-form strings so a
|
|
55
|
+
* disclosure surface can label each count without re-deriving the taxonomy —
|
|
56
|
+
* and so adding a pattern forces a decision about which class it belongs to.
|
|
57
|
+
*/
|
|
58
|
+
export declare const PROSE_REDACTION_CLASSES: readonly ["private_key", "url_credentials", "assignment", "token", "email", "long_random_string"];
|
|
59
|
+
export type ProseRedactionClass = (typeof PROSE_REDACTION_CLASSES)[number];
|
|
60
|
+
export type ProseRedactionReport = {
|
|
61
|
+
/** The redacted text, length-capped. */
|
|
62
|
+
text: string;
|
|
63
|
+
/** How many spans were replaced, per class. Absent classes redacted nothing. */
|
|
64
|
+
counts: Partial<Record<ProseRedactionClass, number>>;
|
|
65
|
+
/** Total spans replaced across all classes. */
|
|
66
|
+
redactedCount: number;
|
|
67
|
+
/** The text hit `MAX_PROSE_CHARS` and lost its tail. */
|
|
68
|
+
wasTruncated: boolean;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Redact secrets from prose and cap its length, reporting what was removed.
|
|
72
|
+
*
|
|
73
|
+
* Runs on-device, before upload, always — the server never receives the
|
|
74
|
+
* unredacted form and therefore cannot leak it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function redactProse(input: string | null | undefined): ProseRedactionReport;
|
|
77
|
+
//# sourceMappingURL=redact-prose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact-prose.d.ts","sourceRoot":"","sources":["../src/redact-prose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAUH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,OAAO,CAAA;AAwBnC;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,mGAO1B,CAAA;AAEV,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE1E,MAAM,MAAM,oBAAoB,GAAG;IACjC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAA;IACpD,+CAA+C;IAC/C,aAAa,EAAE,MAAM,CAAA;IACrB,wDAAwD;IACxD,YAAY,EAAE,OAAO,CAAA;CACtB,CAAA;AA8ID;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,oBAAoB,CAgIlF"}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret redaction and length-capping for **prose** — transcript message text,
|
|
3
|
+
* as opposed to the shell command text `sanitizeCommand` handles.
|
|
4
|
+
*
|
|
5
|
+
* Why a second redactor rather than reusing the first: `sanitizeCommand` is
|
|
6
|
+
* pattern-matched against *shell syntax* — heredocs, `KEY=value` anchored at a
|
|
7
|
+
* shell token boundary, `Authorization:` headers
|
|
8
|
+
* (`./sanitize-command.ts:31-49`). `transcript-content-classes-plan.md:20`
|
|
9
|
+
* records that it "catches none of it in prose" and names a prose redactor the
|
|
10
|
+
* load-bearing deliverable rather than a checkbox on one. This is that gate.
|
|
11
|
+
*
|
|
12
|
+
* It lives here beside `sanitizeCommand`, and not in the CLI, because the plan
|
|
13
|
+
* asks for exactly that (`transcript-content-classes-plan.md:43`): shared, so
|
|
14
|
+
* the CLI and any future server-side path cannot drift into different
|
|
15
|
+
* strengths of the same protection.
|
|
16
|
+
*
|
|
17
|
+
* **What this is not.** This is the redactor and nothing else: it does not
|
|
18
|
+
* extract transcript messages and does not strip code blocks. It is no longer
|
|
19
|
+
* true, as this header said through Slice 1, that "no caller uploads its
|
|
20
|
+
* output" — transcript narration ships, and `redactProse` runs on it
|
|
21
|
+
* (`../../cli/src/transcript/narration.ts:133`) before it is attached to an
|
|
22
|
+
* action at the `action-queue.ts` choke point and uploaded by `mossbear sync`.
|
|
23
|
+
* This function is now the last thing between prose and the wire, which is why
|
|
24
|
+
* a gap in the pattern list below is a live disclosure rather than a latent
|
|
25
|
+
* one. Two consequences of that are worth saying out loud rather than
|
|
26
|
+
* assuming:
|
|
27
|
+
*
|
|
28
|
+
* - **Code-block stripping is structural and happens before this runs.**
|
|
29
|
+
* Fenced and indented blocks come out by the Markdown structure of the
|
|
30
|
+
* message (Slice 2). A regex over prose must never be what stands between
|
|
31
|
+
* a code block and the wire, so nothing here attempts it.
|
|
32
|
+
* - **Redaction is best-effort; the length cap is the backstop.** The same
|
|
33
|
+
* admission `sanitizeCommand` makes (`./sanitize-command.ts:26-29`). A
|
|
34
|
+
* pattern list cannot enumerate every
|
|
35
|
+
* secret shape, which is exactly why the class table strips whole classes
|
|
36
|
+
* rather than trusting a scrubber to find secrets inside them.
|
|
37
|
+
*
|
|
38
|
+
* Reports **what it redacted, by class and count**, so a disclosure surface can
|
|
39
|
+
* show the user what was removed rather than asserting safety at them.
|
|
40
|
+
*/
|
|
41
|
+
import { COOKIE_HEADER_SOURCE, PROSE_SENSITIVE_KEY_SOURCE, SENSITIVE_KEY_SOURCE, SHELL_WORD_SOURCE, } from './credential-patterns.js';
|
|
42
|
+
import { PRIVATE_KEY_BLOCK_SOURCE, REDACTED } from './redaction.js';
|
|
43
|
+
/**
|
|
44
|
+
* Maximum characters of a single prose message that survive redaction.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately far larger than `sanitizeCommand`'s 200
|
|
47
|
+
* (`./sanitize-command.ts:20`): a command is a tool
|
|
48
|
+
* invocation, where anything long is bulk payload, whereas prose is the
|
|
49
|
+
* content — "I'm going to X because Y" is the whole reason this class syncs at
|
|
50
|
+
* all, and truncating it to a couple of sentences would leave the grader with
|
|
51
|
+
* the same intent-free evidence it has today. Long enough for a real
|
|
52
|
+
* explanation, short enough that a pasted file body cannot ride along whole.
|
|
53
|
+
*/
|
|
54
|
+
export const MAX_PROSE_CHARS = 2000;
|
|
55
|
+
/**
|
|
56
|
+
* Characters actually scanned by the patterns below.
|
|
57
|
+
*
|
|
58
|
+
* The cap on the *output* is not a bound on the *work*: without this, every
|
|
59
|
+
* pattern scans an arbitrarily long untrusted message before a single
|
|
60
|
+
* character is discarded, and the assignment matcher backtracks badly on
|
|
61
|
+
* adversarial input (a long run of `SECRET` with no `=` after it). A pasted
|
|
62
|
+
* megabyte could then stall CLI sync to produce 2,000 characters.
|
|
63
|
+
*
|
|
64
|
+
* Generous rather than equal to `MAX_PROSE_CHARS` because redaction *shrinks*
|
|
65
|
+
* text — a secret replaced by `[redacted]` pulls later content into the
|
|
66
|
+
* surviving window — so the scan has to see more than it will keep. Anything
|
|
67
|
+
* past this is dropped before scanning and counts as truncation.
|
|
68
|
+
*/
|
|
69
|
+
const MAX_SCAN_CHARS = MAX_PROSE_CHARS * 8;
|
|
70
|
+
const QUOTED_VALUE_SOURCE = String.raw `(?:"(?:[^"\\]|\\[\s\S])*(?:"|$)|'(?:[^'\\]|\\[\s\S])*(?:'|$))`;
|
|
71
|
+
// Python concatenates adjacent quoted literals. Do not consume a following
|
|
72
|
+
// YAML key as another fragment of the value.
|
|
73
|
+
const MAPPING_VALUE_SOURCE = String.raw `${QUOTED_VALUE_SOURCE}(?:\s*${QUOTED_VALUE_SOURCE}(?!\s*:))*|[^,}\s]+`;
|
|
74
|
+
const QUOTED_COMMAND_WORD_SOURCE = String.raw `(?:\$'(?:[^'\\]|\\[\s\S])*(?:'|$)(?:${SHELL_WORD_SOURCE})?|${SHELL_WORD_SOURCE})`;
|
|
75
|
+
/**
|
|
76
|
+
* The classes this reports on. Named rather than free-form strings so a
|
|
77
|
+
* disclosure surface can label each count without re-deriving the taxonomy —
|
|
78
|
+
* and so adding a pattern forces a decision about which class it belongs to.
|
|
79
|
+
*/
|
|
80
|
+
export const PROSE_REDACTION_CLASSES = [
|
|
81
|
+
'private_key',
|
|
82
|
+
'url_credentials',
|
|
83
|
+
'assignment',
|
|
84
|
+
'token',
|
|
85
|
+
'email',
|
|
86
|
+
'long_random_string',
|
|
87
|
+
];
|
|
88
|
+
/**
|
|
89
|
+
* Ordered redactions. Order matters the same way it does in `sanitizeCommand`:
|
|
90
|
+
* the widest structural shapes run first, so a token *inside* a PEM block or a
|
|
91
|
+
* URL is already gone before the narrower patterns scan for it.
|
|
92
|
+
*/
|
|
93
|
+
const PROSE_REDACTIONS = [
|
|
94
|
+
// Whole private-key blocks, including an unterminated one — a truncated key
|
|
95
|
+
// is still a key. First, because everything else would otherwise match the
|
|
96
|
+
// base64 inside it.
|
|
97
|
+
['private_key', new RegExp(PRIVATE_KEY_BLOCK_SOURCE, 'g'), REDACTED],
|
|
98
|
+
// `Authorization: Bearer <token>` / `Basic <blob>`, as pasted from a request
|
|
99
|
+
// or a curl transcript. These values are routinely short-lived and short —
|
|
100
|
+
// too short for the prefix and long-run rules below — and `Authorization` is
|
|
101
|
+
// a header name rather than a sensitive assignment key, so without this rule
|
|
102
|
+
// a pasted header uploads a live credential verbatim. `sanitizeCommand`
|
|
103
|
+
// carries the same pattern for command text (`./sanitize-command.ts:45`).
|
|
104
|
+
[
|
|
105
|
+
'token',
|
|
106
|
+
/\b(authorization\s*:\s*)(?:bearer\s+|basic\s+|token\s+)?\S+/gi,
|
|
107
|
+
`$1${REDACTED}`,
|
|
108
|
+
],
|
|
109
|
+
['token', new RegExp(COOKIE_HEADER_SOURCE, 'gi'), `$1${REDACTED}`],
|
|
110
|
+
[
|
|
111
|
+
'assignment',
|
|
112
|
+
new RegExp(String.raw `("${SENSITIVE_KEY_SOURCE}"\s*:\s*)(?:${MAPPING_VALUE_SOURCE})`, 'gi'),
|
|
113
|
+
`$1"${REDACTED}"`,
|
|
114
|
+
],
|
|
115
|
+
// Quoted keys in Python/YAML mappings carry the same secrets as JSON.
|
|
116
|
+
[
|
|
117
|
+
'assignment',
|
|
118
|
+
new RegExp(String.raw `('${SENSITIVE_KEY_SOURCE}'\s*:\s*)(?:${MAPPING_VALUE_SOURCE})`, 'gi'),
|
|
119
|
+
`$1${REDACTED}`,
|
|
120
|
+
],
|
|
121
|
+
// URL userinfo. Both shapes: `scheme://user:secret@host` and the
|
|
122
|
+
// token-as-sole-userinfo form `scheme://token@host`, which a colon-requiring
|
|
123
|
+
// pattern misses entirely and which no other rule here catches when the token
|
|
124
|
+
// is short. The host always stays — "it failed against api.example.com" is
|
|
125
|
+
// the part worth grading.
|
|
126
|
+
[
|
|
127
|
+
'url_credentials',
|
|
128
|
+
/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+(?::[^@\s]*)?@/gi,
|
|
129
|
+
`$1${REDACTED}@`,
|
|
130
|
+
],
|
|
131
|
+
// A sensitive key followed by `=` or `:` and a value. Unlike the command
|
|
132
|
+
// version this is *not* anchored at a shell-token boundary — in prose the
|
|
133
|
+
// pair is embedded in a sentence ("I set OPENAI_API_KEY=sk-… and reran"), so
|
|
134
|
+
// anchoring on shell syntax is precisely what misses it.
|
|
135
|
+
//
|
|
136
|
+
// The sensitive word must be a **whole `_`/`-` separated segment** of the
|
|
137
|
+
// key, not a substring of it. As an arbitrary substring, `SECRET` matches
|
|
138
|
+
// `secretary` and `TOKEN` matches `tokenization`, so ordinary prose like
|
|
139
|
+
// "secretary: Alice" lost its value — and prose scrubbed into nonsense is
|
|
140
|
+
// useless to a grader and alarming to a reader, which makes over-redaction a
|
|
141
|
+
// defect here rather than a safe default.
|
|
142
|
+
//
|
|
143
|
+
// The quoted-value arms consume escape sequences (`\\.`) rather than stopping
|
|
144
|
+
// at the first inner quote: a value like `"correct horse \\"battery staple"`
|
|
145
|
+
// otherwise terminates early and leaves the tail of the password in place.
|
|
146
|
+
[
|
|
147
|
+
'assignment',
|
|
148
|
+
new RegExp(String.raw `\b(${PROSE_SENSITIVE_KEY_SOURCE})(\s*[=:]\s*)(?:\$'(?:[^'\\]|\\[\s\S])*(?:'|$)(?:${SHELL_WORD_SOURCE})?|(?:"(?:[^"\\]|\\[\s\S])*(?:"|$)|'(?:[^'\\]|\\[\s\S])*(?:'|$))(?:[^\s"']+|"(?:[^"\\]|\\[\s\S])*(?:"|$)|'(?:[^'\\]|\\[\s\S])*(?:'|$))*|[^\s]+)`, 'gi'),
|
|
149
|
+
`$1$2${REDACTED}`,
|
|
150
|
+
],
|
|
151
|
+
// Well-known bare token shapes, by their published prefixes. Prefix-anchored
|
|
152
|
+
// rather than entropy-guessed, so an ordinary long word is never mistaken for
|
|
153
|
+
// a credential.
|
|
154
|
+
//
|
|
155
|
+
// **Our own CLI token leads the list** (`API_KEY_PREFIX`, the prefix the
|
|
156
|
+
// dashboard mints CLI tokens under), for the same reason `sanitizeCommand`
|
|
157
|
+
// carries it (`./sanitize-command.ts:59-68`): the product's
|
|
158
|
+
// own secret shape is the one most likely to appear. In prose the `assignment`
|
|
159
|
+
// rule above only catches the pair form — `token: mossbear_…` — while the
|
|
160
|
+
// sentence form a user actually writes ("I set my token to mossbear_… and it
|
|
161
|
+
// worked") and a bare mention have no `key=value` to anchor on. The
|
|
162
|
+
// `long_random_string` backstop does not reach them either: a `mossbear_` key
|
|
163
|
+
// is under its 40-character floor. Without this arm, a user who mentions
|
|
164
|
+
// their token in a Claude Code message uploads a live dashboard credential in
|
|
165
|
+
// plaintext. Any future re-mint under a new prefix has to be added here in the
|
|
166
|
+
// same change, and to `sanitizeCommand` — a token neither redactor matches is
|
|
167
|
+
// one that ships verbatim.
|
|
168
|
+
//
|
|
169
|
+
// `auden_` is the pre-rename prefix. Every key issued under it is deleted by
|
|
170
|
+
// migration `0056-revoke_legacy_prefix_cli_tokens.ts`, so it matches nothing
|
|
171
|
+
// that can still authenticate; it is kept because a CLI redacting on the
|
|
172
|
+
// user's machine can be older than the dashboard it syncs to, and that window
|
|
173
|
+
// is exactly the one where dropping the rule uploads a live key.
|
|
174
|
+
//
|
|
175
|
+
// The `{8,}` floor matches `sanitizeCommand`'s, deliberately: the two
|
|
176
|
+
// redactors guard the same secret against the same upload, and a prose-side
|
|
177
|
+
// floor set higher would be a difference in strength with nothing behind it.
|
|
178
|
+
[
|
|
179
|
+
'token',
|
|
180
|
+
/\b(?:mossbear_[A-Za-z0-9_-]{8,}|auden_[A-Za-z0-9_-]{8,}|sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[A-Za-z0-9_-]{20,})/g,
|
|
181
|
+
REDACTED,
|
|
182
|
+
],
|
|
183
|
+
// Quoted command values in narration must be consumed as complete shell
|
|
184
|
+
// words. A bare word after a flag may be prose ("--token to authenticate").
|
|
185
|
+
[
|
|
186
|
+
'assignment',
|
|
187
|
+
new RegExp(String.raw `(--${PROSE_SENSITIVE_KEY_SOURCE}\s+)(?=\$?["'])${QUOTED_COMMAND_WORD_SOURCE}`, 'gi'),
|
|
188
|
+
`$1${REDACTED}`,
|
|
189
|
+
],
|
|
190
|
+
// Email addresses, listed among the plan's targets
|
|
191
|
+
// (`transcript-content-classes-plan.md:43`).
|
|
192
|
+
['email', /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, REDACTED],
|
|
193
|
+
// Long unbroken base64/hex runs: a credential shape this list has no prefix
|
|
194
|
+
// for. Runs last, so anything a named pattern would have caught is gone.
|
|
195
|
+
//
|
|
196
|
+
// Two constraints keep it from eating ordinary text, and both are load-bearing:
|
|
197
|
+
//
|
|
198
|
+
// - **Delimiter lookarounds, not `\b`.** `+` and `/` are in the base64
|
|
199
|
+
// alphabet but are not word characters, so a blob beginning with one has
|
|
200
|
+
// no word boundary to match at — `\b` would start a character late and
|
|
201
|
+
// leave a 40-character secret one short of the floor, passing it through
|
|
202
|
+
// whole.
|
|
203
|
+
// - **It must contain a digit.** Length alone is not a random-string
|
|
204
|
+
// signal: `createAuthenticatedDashboardRequestContext` is 42 characters
|
|
205
|
+
// of ordinary identifier, and redacting it removes the exact detail a
|
|
206
|
+
// grader needs. Digests, ids and encoded blobs carry digits; long
|
|
207
|
+
// camel-case names do not. An all-alphabetic secret is the accepted
|
|
208
|
+
// miss, and it is what the named patterns above and the cap exist for.
|
|
209
|
+
[
|
|
210
|
+
'long_random_string',
|
|
211
|
+
/(?<![A-Za-z0-9+/=_-])(?=[A-Za-z0-9+/_-]*\d)[A-Za-z0-9+/_-]{40,}={0,2}(?![A-Za-z0-9+/=_-])/g,
|
|
212
|
+
REDACTED,
|
|
213
|
+
],
|
|
214
|
+
];
|
|
215
|
+
/**
|
|
216
|
+
* Redact secrets from prose and cap its length, reporting what was removed.
|
|
217
|
+
*
|
|
218
|
+
* Runs on-device, before upload, always — the server never receives the
|
|
219
|
+
* unredacted form and therefore cannot leak it.
|
|
220
|
+
*/
|
|
221
|
+
export function redactProse(input) {
|
|
222
|
+
if (typeof input !== 'string' || input === '') {
|
|
223
|
+
return { text: '', counts: {}, redactedCount: 0, wasTruncated: false };
|
|
224
|
+
}
|
|
225
|
+
// Bounded before any pattern runs — see `MAX_SCAN_CHARS`.
|
|
226
|
+
const wasScanTruncated = input.length > MAX_SCAN_CHARS;
|
|
227
|
+
let text = wasScanTruncated ? input.slice(0, MAX_SCAN_CHARS) : input;
|
|
228
|
+
// Decode only credential key spellings. JSON escapes must not disguise a
|
|
229
|
+
// sensitive key, while unrelated prose retains its original spelling.
|
|
230
|
+
const sensitiveKey = new RegExp(`^${SENSITIVE_KEY_SOURCE}$`, 'i');
|
|
231
|
+
text = text.replace(/("(?:[^"\\]|\\.)*")(\s*:)/g, (match, key, separator) => {
|
|
232
|
+
try {
|
|
233
|
+
const decoded = JSON.parse(key);
|
|
234
|
+
return typeof decoded === 'string' && sensitiveKey.test(decoded)
|
|
235
|
+
? `${JSON.stringify(decoded)}${separator}`
|
|
236
|
+
: match;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return match;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
const counts = {};
|
|
243
|
+
let redactedCount = 0;
|
|
244
|
+
const recordAssignment = () => {
|
|
245
|
+
counts.assignment = (counts.assignment ?? 0) + 1;
|
|
246
|
+
redactedCount += 1;
|
|
247
|
+
};
|
|
248
|
+
// A YAML block scalar's value is the indented lines after its marker.
|
|
249
|
+
// Redacting only `|` or `>` leaves the complete credential behind.
|
|
250
|
+
const lines = text.split('\n');
|
|
251
|
+
const scalarKey = new RegExp(String.raw `^([ \t]*)(-[ \t]+)?(["']?${SENSITIVE_KEY_SOURCE}["']?[ \t]*:[ \t]*)[|>](?:[+-][1-9]?|[1-9][+-]?)?[ \t]*(?:#[^\r]*)?\r?$`, 'i');
|
|
252
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
253
|
+
const match = scalarKey.exec(lines[index]);
|
|
254
|
+
if (!match)
|
|
255
|
+
continue;
|
|
256
|
+
const mappingIndent = match[1].length + (match[2]?.length ?? 0);
|
|
257
|
+
let end = index + 1;
|
|
258
|
+
while (end < lines.length) {
|
|
259
|
+
const line = lines[end];
|
|
260
|
+
if (line.trim() && /^[ \t]*/.exec(line)[0].length <= mappingIndent)
|
|
261
|
+
break;
|
|
262
|
+
end += 1;
|
|
263
|
+
}
|
|
264
|
+
// These credential names are valid plain YAML keys. Removing their quotes
|
|
265
|
+
// prevents the later JSON-key rule from treating sibling YAML as a value.
|
|
266
|
+
const keyPrefix = match[3].replace(/^["']|["'](?=[ \t]*:)/g, '');
|
|
267
|
+
lines.splice(index, end - index, `${match[1]}${match[2] ?? ''}${keyPrefix}${REDACTED}`);
|
|
268
|
+
recordAssignment();
|
|
269
|
+
}
|
|
270
|
+
text = lines.join('\n');
|
|
271
|
+
// JSON strings can contain another serialized object. Scan quoted tokens
|
|
272
|
+
// structurally before the flat pair patterns, with a bounded recursion.
|
|
273
|
+
const redactEncoded = (value, depth) => {
|
|
274
|
+
if (depth > 20) {
|
|
275
|
+
recordAssignment();
|
|
276
|
+
return REDACTED;
|
|
277
|
+
}
|
|
278
|
+
if (typeof value === 'string' && /^\s*[[{]/.test(value)) {
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse(value);
|
|
281
|
+
const redacted = redactEncoded(parsed, depth + 1);
|
|
282
|
+
return JSON.stringify(redacted) === JSON.stringify(parsed)
|
|
283
|
+
? value
|
|
284
|
+
: JSON.stringify(redacted);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return sensitiveKey.test(value) ? REDACTED : value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (Array.isArray(value))
|
|
291
|
+
return value.map((item) => redactEncoded(item, depth + 1));
|
|
292
|
+
if (value === null || typeof value !== 'object')
|
|
293
|
+
return value;
|
|
294
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
295
|
+
if (sensitiveKey.test(key)) {
|
|
296
|
+
recordAssignment();
|
|
297
|
+
return [key, REDACTED];
|
|
298
|
+
}
|
|
299
|
+
return [key, redactEncoded(item, depth + 1)];
|
|
300
|
+
}));
|
|
301
|
+
};
|
|
302
|
+
text = text.replace(/"(?:[^"\\]|\\[\s\S])*(?:"|$)/g, (token) => {
|
|
303
|
+
try {
|
|
304
|
+
const value = JSON.parse(token);
|
|
305
|
+
const redacted = redactEncoded(value, 0);
|
|
306
|
+
return redacted === value ? token : JSON.stringify(redacted);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return token;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
for (const [redactionClass, pattern, replacement] of PROSE_REDACTIONS) {
|
|
313
|
+
// Counted before replacing, and against a fresh regex, so the shared
|
|
314
|
+
// literal's `lastIndex` cannot carry between calls — a stateful `g` regex
|
|
315
|
+
// silently skipping the first match of the next message is exactly the
|
|
316
|
+
// failure a redactor must not have.
|
|
317
|
+
const counter = new RegExp(pattern.source, pattern.flags);
|
|
318
|
+
const matches = text.match(counter);
|
|
319
|
+
if (!matches || matches.length === 0)
|
|
320
|
+
continue;
|
|
321
|
+
// Structural passes leave redaction markers behind. Re-matching one is
|
|
322
|
+
// not a second replacement of the credential that produced it.
|
|
323
|
+
const single = new RegExp(pattern.source, pattern.flags.replace('g', ''));
|
|
324
|
+
const changed = matches.filter((match) => match.replace(single, replacement) !== match).length;
|
|
325
|
+
if (changed === 0)
|
|
326
|
+
continue;
|
|
327
|
+
counts[redactionClass] = (counts[redactionClass] ?? 0) + changed;
|
|
328
|
+
redactedCount += changed;
|
|
329
|
+
text = text.replace(new RegExp(pattern.source, pattern.flags), replacement);
|
|
330
|
+
}
|
|
331
|
+
const wasTruncated = wasScanTruncated || text.length > MAX_PROSE_CHARS;
|
|
332
|
+
if (text.length > MAX_PROSE_CHARS) {
|
|
333
|
+
// Marked rather than silently cut, so a reader can tell a short message
|
|
334
|
+
// from a long one that lost its tail.
|
|
335
|
+
text = `${text.slice(0, MAX_PROSE_CHARS)}… [truncated]`;
|
|
336
|
+
}
|
|
337
|
+
return { text, counts, redactedCount, wasTruncated };
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=redact-prose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact-prose.js","sourceRoot":"","sources":["../src/redact-prose.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact-prose.test.d.ts","sourceRoot":"","sources":["../src/redact-prose.test.ts"],"names":[],"mappings":""}
|