agent-sanitizer 2.60.0 → 2.60.2
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 +12 -12
- package/THREAT-MODEL.md +21 -9
- package/claude-hooks/lib/layer-pipeline.mjs +87 -59
- package/claude-hooks/lib/placeholder-grammar.mjs +24 -46
- package/claude-hooks/lib/reveal.mjs +39 -12
- package/claude-hooks/lib/secret-annotate.mjs +23 -10
- package/claude-hooks/lib/secret-drop-guard.mjs +27 -9
- package/claude-hooks/pretooluse-sanitize.mjs +141 -108
- package/claude-hooks/sanitize-output.mjs +63 -37
- package/claude-hooks/sanitize-user-prompt.mjs +11 -13
- package/package.json +1 -1
- package/src/html.mjs +9 -52
- package/src/layer2-placeholder.mjs +60 -0
- package/types/claude-hooks/lib/layer-pipeline.d.mts +26 -45
- package/types/claude-hooks/lib/placeholder-grammar.d.mts +3 -23
- package/types/claude-hooks/lib/reveal.d.mts +15 -3
- package/types/claude-hooks/lib/secret-drop-guard.d.mts +9 -5
- package/types/claude-hooks/pretooluse-sanitize.d.mts +41 -28
- package/types/claude-hooks/sanitize-output.d.mts +13 -10
- package/types/claude-hooks/sanitize-user-prompt.d.mts +4 -5
- package/types/html.d.mts +4 -22
- package/types/layer2-placeholder.d.mts +21 -0
- package/types/src/layer2-placeholder.d.mts +21 -0
|
@@ -23,9 +23,12 @@ export function sweepStaleReveals(): void;
|
|
|
23
23
|
* can precompute it and pre-plant a symlink there to redirect this write onto a
|
|
24
24
|
* victim file — writeFileNoFollow refuses that instead of following it.
|
|
25
25
|
* @param {string} content
|
|
26
|
+
* @param {boolean} [secretsRedacted] whether the caller redacted `content`
|
|
27
|
+
* before this call; only then does the hint claim it (with the secret layer
|
|
28
|
+
* off the bytes are stored verbatim)
|
|
26
29
|
* @returns {string | null}
|
|
27
30
|
*/
|
|
28
|
-
export function persistReveal(content: string): string | null;
|
|
31
|
+
export function persistReveal(content: string, secretsRedacted?: boolean): string | null;
|
|
29
32
|
/**
|
|
30
33
|
* The store path for one Layer-2 splice's original bytes, keyed by the
|
|
31
34
|
* placeholder key. Throws on a malformed key (fail loud: every caller extracts
|
|
@@ -42,7 +45,14 @@ export function spanPath(key: string): string;
|
|
|
42
45
|
* symlink-refusingly (O_EXCL) because the path is content-addressed — an
|
|
43
46
|
* attacker who chose the page bytes can precompute it and pre-plant a symlink.
|
|
44
47
|
* Content-addressed dedupe: an existing entry (same key = same raw original)
|
|
45
|
-
* is left in place and counts as success
|
|
48
|
+
* is left in place and counts as success — EXCEPT when `secretsRedacted` is
|
|
49
|
+
* set and the entry differs from `content`. The key is minted from the RAW
|
|
50
|
+
* original, so a span first stored with the secret opt-in off holds the raw
|
|
51
|
+
* bytes under the very key a later opted-in run computes; keeping it would
|
|
52
|
+
* restore a raw secret into a write whose notice says secrets were redacted
|
|
53
|
+
* before storage. The opted-in write therefore replaces a differing entry. The
|
|
54
|
+
* reverse (opt-in off meeting a redacted entry) keeps the entry: redacted is
|
|
55
|
+
* the safe direction. Returns true when the span is on
|
|
46
56
|
* disk (written now or already there); false on any failure — non-fatal by
|
|
47
57
|
* contract, exactly like a failed reveal write (the splice already protected
|
|
48
58
|
* the output; a later rehydration of this key fails CLOSED with a deny).
|
|
@@ -53,9 +63,11 @@ export function spanPath(key: string): string;
|
|
|
53
63
|
* NAME, not an integrity check.
|
|
54
64
|
* @param {string} key
|
|
55
65
|
* @param {string} content the splice's original, redacted BEFORE this call
|
|
66
|
+
* @param {boolean} [secretsRedacted] whether `content` went through the secret
|
|
67
|
+
* redactor (the run's opt-in); only then may it replace a differing entry
|
|
56
68
|
* @returns {boolean}
|
|
57
69
|
*/
|
|
58
|
-
export function persistSpan(key: string, content: string): boolean;
|
|
70
|
+
export function persistSpan(key: string, content: string, secretsRedacted?: boolean): boolean;
|
|
59
71
|
/**
|
|
60
72
|
* The stored original for one Layer-2 placeholder key, or null when no span is
|
|
61
73
|
* stored (or the store is unusable). The open refuses symlinks (O_NOFOLLOW):
|
|
@@ -30,11 +30,15 @@ export function confirmMarkerPath(fingerprint: string): string;
|
|
|
30
30
|
*/
|
|
31
31
|
export function sweepStaleConfirms(): void;
|
|
32
32
|
/**
|
|
33
|
-
* Whether git tracks `filePath
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* Whether git tracks `filePath`, with every probe failure on the skip side the
|
|
34
|
+
* module doc declares. Exit 0 is tracked and exit 1 untracked. Exit 128 is
|
|
35
|
+
* either "not a repository" — no git recovery exists, so the guard applies — or
|
|
36
|
+
* a repository git could not read (a corrupt index, a permission error), which
|
|
37
|
+
* says nothing about the file; a second probe asks git which, reading its
|
|
38
|
+
* printed answer (`false` inside a bare repository or a `.git` directory is
|
|
39
|
+
* also exit 0, and also has no recovery path). A spawn-level
|
|
40
|
+
* failure (no git binary) or any other status also means we cannot tell —
|
|
41
|
+
* report tracked so the guard skips rather than denying on broken tooling.
|
|
38
42
|
* @param {string} filePath
|
|
39
43
|
* @param {typeof spawnSync} [spawn]
|
|
40
44
|
* @returns {boolean}
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* removed #<key>]` placeholders (copied from sanitized tool output) has each
|
|
5
5
|
* one restored to the stored original bytes from the reveal store's span files.
|
|
6
6
|
*
|
|
7
|
-
* SECURITY invariant: the stored span content was
|
|
8
|
-
* (sanitize-output runs strict web-ingress
|
|
9
|
-
* before persistSpan), so this rehydration
|
|
10
|
-
* worst it can restore is `[REDACTED…]`
|
|
11
|
-
*
|
|
7
|
+
* SECURITY invariant, with the secret layer on: the stored span content was
|
|
8
|
+
* REDACTED before persistence (sanitize-output runs strict web-ingress
|
|
9
|
+
* redaction on each splice original before persistSpan), so this rehydration
|
|
10
|
+
* can never write a raw secret — the worst it can restore is `[REDACTED…]`
|
|
11
|
+
* placeholder text, which the on-disk tripwire then flags on later Reads. With
|
|
12
|
+
* the layer off, spans are stored verbatim and the context claims nothing.
|
|
12
13
|
*
|
|
13
14
|
* Composition with the secret rehydrator: this runs AFTER it (a later terminal
|
|
14
15
|
* layer), and the two grammars are disjoint — a Layer-2 placeholder never
|
|
@@ -25,51 +26,62 @@
|
|
|
25
26
|
* edits / notebook JSON cannot be rehydrated).
|
|
26
27
|
* @param {string} tool
|
|
27
28
|
* @param {any} toolInput
|
|
29
|
+
* @param {boolean} [secretsOn] the run's secret opt-in (decided once by
|
|
30
|
+
* buildPreToolUseResponse; a direct caller reads the env); only with it on
|
|
31
|
+
* does the context claim the restored bytes were redacted
|
|
28
32
|
* @returns {{ updatedInput: any, context: string } | { deny: string } | null}
|
|
29
33
|
*/
|
|
30
|
-
export function rehydrateLayer2(tool: string, toolInput: any): {
|
|
34
|
+
export function rehydrateLayer2(tool: string, toolInput: any, secretsOn?: boolean): {
|
|
31
35
|
updatedInput: any;
|
|
32
36
|
context: string;
|
|
33
37
|
} | {
|
|
34
38
|
deny: string;
|
|
35
39
|
} | null;
|
|
36
40
|
/**
|
|
37
|
-
* The declared
|
|
41
|
+
* The declared stage chain after the gate. Every entry states the two properties
|
|
38
42
|
* the driver reasons about — whether it ERASES code points another layer reads,
|
|
39
43
|
* and whether its own decision is SKIP-BASED and therefore invalidated by such
|
|
40
44
|
* an erasure — so the confusable fold's ordering precondition is enforced by the
|
|
41
45
|
* table instead of restated as a comment.
|
|
42
46
|
*
|
|
43
|
-
*
|
|
47
|
+
* The authored-content stage is dropped from the table (not merely skipped) when
|
|
44
48
|
* AGENT_SANITIZER_OUTPUT_DISABLED=1, so the driver sees the chain that will
|
|
45
49
|
* actually run: with no erasing layer left after the fold there is no fixed
|
|
46
|
-
* point to reach and no extra pass to pay for.
|
|
47
|
-
*
|
|
50
|
+
* point to reach and no extra pass to pay for. A null `rehydrate` drops the
|
|
51
|
+
* secret rehydrator the same way.
|
|
52
|
+
* @param {((tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>) | null} rehydrate
|
|
48
53
|
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
|
|
54
|
+
* @param {boolean} [secretsOn] the run's secret opt-in (see
|
|
55
|
+
* buildPreToolUseResponse, which decides it once and passes it here)
|
|
49
56
|
* @returns {import("./lib/layer-pipeline.mjs").Layer[]}
|
|
50
57
|
*/
|
|
51
|
-
export function preToolUseLayers(rehydrate: (tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted
|
|
58
|
+
export function preToolUseLayers(rehydrate: ((tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>) | null, env?: NodeJS.ProcessEnv | Record<string, string | undefined>, secretsOn?: boolean): import("./lib/layer-pipeline.mjs").Layer[];
|
|
52
59
|
/**
|
|
53
|
-
* Compose the
|
|
54
|
-
*
|
|
55
|
-
*
|
|
60
|
+
* Compose the five stages. Returns the `hookSpecificOutput` fields to emit, or
|
|
61
|
+
* null for a clean no-op. Throws only if a stage's engine throws; the caller
|
|
62
|
+
* then applies this hook's fault posture (open by default — see
|
|
63
|
+
* hookFailureFields). Every exit routes through emitTraced.
|
|
56
64
|
* @param {any} input parsed PreToolUse event
|
|
57
65
|
* @param {(tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>} [rehydrate]
|
|
58
|
-
* injectable for tests
|
|
59
|
-
*
|
|
66
|
+
* injectable for tests, and then run whatever the secret opt-in says; the
|
|
67
|
+
* default binds the real redactor-daemon io (the layer reads the target file
|
|
68
|
+
* and maps secrets through the daemon) and runs only with the opt-in on
|
|
60
69
|
* @param {import("./lib/trace.mjs").TraceFn} [sink] where engagement is
|
|
61
70
|
* announced; a host with its own trace channel passes its sink (see lib/trace.mjs)
|
|
71
|
+
* @param {import("./lib/layer-pipeline.mjs").Layer[]} [layers] the stage table,
|
|
72
|
+
* injectable so a test can drive one the shipped stages never produce (an
|
|
73
|
+
* oscillating stage); defaults to {@link preToolUseLayers} for this call
|
|
62
74
|
* @returns {Promise<Record<string, unknown> | null>}
|
|
63
75
|
*/
|
|
64
|
-
export function buildPreToolUseResponse(input: any, rehydrate?: (tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>, sink?: import("./lib/trace.mjs").TraceFn): Promise<Record<string, unknown> | null>;
|
|
76
|
+
export function buildPreToolUseResponse(input: any, rehydrate?: (tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>, sink?: import("./lib/trace.mjs").TraceFn, layers?: import("./lib/layer-pipeline.mjs").Layer[]): Promise<Record<string, unknown> | null>;
|
|
65
77
|
/**
|
|
66
|
-
* Agent-agnostic judge over the
|
|
78
|
+
* Agent-agnostic judge over the five stages: consumes a control-plane
|
|
67
79
|
* ToolCallEvent and returns a Verdict, so a non-Claude host can run the same
|
|
68
80
|
* sanitization pipeline through its own adapter. The wired Claude CLI below
|
|
69
81
|
* routes through this judge and renders the Verdict with the Claude adapter; on
|
|
70
82
|
* any throw (a control-plane package-load failure included) it falls back to
|
|
71
|
-
*
|
|
72
|
-
*
|
|
83
|
+
* hookFailureFields — a native response that needs no package — so the
|
|
84
|
+
* caller's fault posture holds even when the adapter never loaded.
|
|
73
85
|
* @param {import("agent-control-plane-core").ToolCallEvent} event
|
|
74
86
|
* @param {(tool: string, toolInput: any) => ReturnType<typeof rehydrateRedacted>} [rehydrate]
|
|
75
87
|
* @param {{
|
|
@@ -135,14 +147,15 @@ export function failClosedFields(parsedOk: boolean, err: unknown, opts?: {
|
|
|
135
147
|
}): Record<string, unknown>;
|
|
136
148
|
/**
|
|
137
149
|
* True when a faulting PreToolUse call is the one case the OPEN posture must
|
|
138
|
-
* still not pass: a write-shaped tool whose input carries
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
150
|
+
* still not pass: a write-shaped tool whose input carries a redaction
|
|
151
|
+
* placeholder, matched by the real grammar (containsPlaceholder) so
|
|
152
|
+
* hint-prefixed prose such as `[REDACTED…]` is not mistaken for one. Such a
|
|
153
|
+
* placeholder stands for a secret the sanitizer redacted out of the model's
|
|
154
|
+
* view; with the sanitizer down, rehydration cannot translate it back, so
|
|
155
|
+
* letting the call through would persist the literal placeholder text over the
|
|
156
|
+
* real secret on disk — a destructive clobber, not a missed scan.
|
|
157
|
+
* Package-free by construction (a Set lookup and a walk of the already-parsed
|
|
158
|
+
* payload), so it holds when the failure IS the missing package.
|
|
146
159
|
* @param {unknown} input raw parsed PreToolUse payload (undefined if unparsed)
|
|
147
160
|
* @returns {boolean}
|
|
148
161
|
*/
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* never WHETHER their result is applied.
|
|
8
8
|
*
|
|
9
9
|
* A callback that throws is not caught here. That is deliberate: the throw lands
|
|
10
|
-
* in the CLI's
|
|
11
|
-
*
|
|
10
|
+
* in the CLI's catch like any other sanitizer fault, so a broken extension gets
|
|
11
|
+
* exactly the posture the operator chose (emitHookFailure) and no quieter one.
|
|
12
12
|
*
|
|
13
13
|
* These are NOT the Layer-5 injection-filter seam and do not inherit its
|
|
14
14
|
* delete-only, closed-enum restriction. (Its identifier is deliberately unspoken
|
|
@@ -58,11 +58,13 @@
|
|
|
58
58
|
* @param {{remainingMs: () => number}} [deadline] shared wall-clock budget across
|
|
59
59
|
* all leaves of one hook run; a direct caller gets a fresh full budget
|
|
60
60
|
* @param {SanitizeExtensions} [ext]
|
|
61
|
+
* @param {boolean} [secretsOn] the secret opt-in, decided ONCE per hook run by
|
|
62
|
+
* {@link evaluateToolOutput} and threaded down; a direct caller reads the env
|
|
61
63
|
* @returns {Promise<{ cleaned: string, warnings: string[], notes: string[], modified: boolean, sgrNote: boolean, reveal?: string, splices?: Array<{ placeholder: string, original: string }> }>}
|
|
62
64
|
*/
|
|
63
65
|
export function sanitizeText(text: string, toolName: string, deadline?: {
|
|
64
66
|
remainingMs: () => number;
|
|
65
|
-
}, ext?: SanitizeExtensions): Promise<{
|
|
67
|
+
}, ext?: SanitizeExtensions, secretsOn?: boolean): Promise<{
|
|
66
68
|
cleaned: string;
|
|
67
69
|
warnings: string[];
|
|
68
70
|
notes: string[];
|
|
@@ -104,6 +106,7 @@ export function sanitizeText(text: string, toolName: string, deadline?: {
|
|
|
104
106
|
* after `notes` for the same positional-compatibility reason
|
|
105
107
|
* @param {string} [path] dotted location of `value` within the tool output,
|
|
106
108
|
* used only to name a key collision's location in its warning
|
|
109
|
+
* @param {boolean} [secretsOn] the secret opt-in (see {@link sanitizeText})
|
|
107
110
|
* @returns {Promise<{ value: any, modified: boolean, sgrNote: boolean }>}
|
|
108
111
|
*/
|
|
109
112
|
export function sanitizeValue(value: any, toolName: string, warnings: string[], reveals?: string[], deadline?: {
|
|
@@ -111,7 +114,7 @@ export function sanitizeValue(value: any, toolName: string, warnings: string[],
|
|
|
111
114
|
}, ext?: SanitizeExtensions, notes?: string[], splices?: Array<{
|
|
112
115
|
placeholder: string;
|
|
113
116
|
original: string;
|
|
114
|
-
}>, path?: string): Promise<{
|
|
117
|
+
}>, path?: string, secretsOn?: boolean): Promise<{
|
|
115
118
|
value: any;
|
|
116
119
|
modified: boolean;
|
|
117
120
|
sgrNote: boolean;
|
|
@@ -235,7 +238,7 @@ export function evaluateToolOutput(input: any, ext?: SanitizeExtensions): Promis
|
|
|
235
238
|
* {@link evaluateToolOutput} already returns those contract fields (or null),
|
|
236
239
|
* so the judge only stamps the `allow` decision onto them — no native-envelope
|
|
237
240
|
* translation. Throws only if a layer engine throws (or on an UNKNOWN event);
|
|
238
|
-
* the CLI
|
|
241
|
+
* the CLI applies this hook's fault posture to any throw (emitHookFailure).
|
|
239
242
|
* @param {import("agent-control-plane-core").ToolCallEvent} event
|
|
240
243
|
* @param {SanitizeExtensions} [ext]
|
|
241
244
|
* @returns {Promise<import("agent-control-plane-core").Verdict>}
|
|
@@ -247,9 +250,9 @@ export function judgeSanitizeOutput(event: import("agent-control-plane-core").To
|
|
|
247
250
|
* omits the field is a PostToolUse call by construction. The claude adapter
|
|
248
251
|
* extracts `tool_response` (this hook's actual input) ONLY for a PostToolUse
|
|
249
252
|
* event; without this default a field-less but legitimate payload would parse as
|
|
250
|
-
* UNKNOWN, {@link judgeSanitizeOutput} would throw, and the CLI would
|
|
251
|
-
*
|
|
252
|
-
* left untouched, so the judge's UNKNOWN guard still
|
|
253
|
+
* UNKNOWN, {@link judgeSanitizeOutput} would throw, and the CLI would take the
|
|
254
|
+
* fault posture on real tool output. A payload carrying a DIFFERENT event name
|
|
255
|
+
* is left untouched, so the judge's UNKNOWN guard still fires on a genuinely
|
|
253
256
|
* unrecognized event.
|
|
254
257
|
* @param {unknown} input the raw stdin payload
|
|
255
258
|
* @returns {unknown}
|
|
@@ -285,8 +288,8 @@ export const ON_DISK_PLACEHOLDER_WARNING: string;
|
|
|
285
288
|
* never WHETHER their result is applied.
|
|
286
289
|
*
|
|
287
290
|
* A callback that throws is not caught here. That is deliberate: the throw lands
|
|
288
|
-
* in the CLI's
|
|
289
|
-
*
|
|
291
|
+
* in the CLI's catch like any other sanitizer fault, so a broken extension gets
|
|
292
|
+
* exactly the posture the operator chose (emitHookFailure) and no quieter one.
|
|
290
293
|
*
|
|
291
294
|
* These are NOT the Layer-5 injection-filter seam and do not inherit its
|
|
292
295
|
* delete-only, closed-enum restriction. (Its identifier is deliberately unspoken
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* Judge a normalized prompt-submit event. Agent-agnostic: consumes the
|
|
3
3
|
* control-plane ToolCallEvent and returns a Verdict, so the same prompt gate
|
|
4
4
|
* renders through any agent adapter, not just Claude's. Throws (into the
|
|
5
|
-
* calling hook's catch) when the sanitizer package never loaded
|
|
6
|
-
*
|
|
7
|
-
* never pass through.
|
|
5
|
+
* calling hook's catch) when the sanitizer package never loaded, so a prompt it
|
|
6
|
+
* cannot classify takes the hook's fault posture rather than a silent ALLOW.
|
|
8
7
|
* @param {import("agent-control-plane-core").ToolCallEvent} event
|
|
9
8
|
* @param {((s: string) => string) | null} [strip] the ANSI stripper (defaults
|
|
10
|
-
* to the package's stripAnsiFully; injectable so the
|
|
9
|
+
* to the package's stripAnsiFully; injectable so the fault path is testable)
|
|
11
10
|
* @param {Partial<typeof USER_PROMPT_MESSAGES>} [overrides] reason overrides,
|
|
12
11
|
* merged over the defaults so a partial table can never leave a field unset
|
|
13
12
|
* @returns {import("agent-control-plane-core").Verdict}
|
|
@@ -26,7 +25,7 @@ export function judgeSanitizeUserPrompt(event: import("agent-control-plane-core"
|
|
|
26
25
|
* env?: NodeJS.ProcessEnv | Record<string, string | undefined>,
|
|
27
26
|
* }} [opts]
|
|
28
27
|
* `strip` is the ANSI stripper (defaults to the package's stripAnsiFully;
|
|
29
|
-
* injectable so the
|
|
28
|
+
* injectable so the fault path is testable); `overrides` are reason
|
|
30
29
|
* overrides, merged over the defaults so a partial table can never leave a field
|
|
31
30
|
* unset; `trace` is where engagement is announced, for a host with its own trace
|
|
32
31
|
* channel (see lib/trace.mjs); `env` is the failure-posture source (see
|
package/types/html.d.mts
CHANGED
|
@@ -20,19 +20,6 @@ export function isHiddenOpen(htmlValue: string): string | null;
|
|
|
20
20
|
* @returns {string | null}
|
|
21
21
|
*/
|
|
22
22
|
export function closingTagName(htmlValue: string): string | null;
|
|
23
|
-
/**
|
|
24
|
-
* The keyed, content-addressed placeholder for one Layer-2 splice:
|
|
25
|
-
* `[hidden HTML removed #<key>]` / `[HTML comment removed #<key>]`, where
|
|
26
|
-
* `<key>` is the first 12 lowercase-hex chars of sha256 over the UTF-8
|
|
27
|
-
* encoding of the ORIGINAL spliced text. Content-addressed on purpose:
|
|
28
|
-
* identical spliced content yields the identical placeholder, so a rehydrator
|
|
29
|
-
* can match placeholder → original by key alone, and duplicated content never
|
|
30
|
-
* produces conflicting keys.
|
|
31
|
-
* @param {SpliceKind} kind
|
|
32
|
-
* @param {string} original the exact text the splice removed
|
|
33
|
-
* @returns {string}
|
|
34
|
-
*/
|
|
35
|
-
export function layer2Placeholder(kind: SpliceKind, original: string): string;
|
|
36
23
|
/**
|
|
37
24
|
* Replace each range of `text` with its kind's keyed placeholder, preserving
|
|
38
25
|
* every byte outside the ranges verbatim. Overlapping/nested ranges are merged
|
|
@@ -169,16 +156,8 @@ export function detectConfusableHosts(text: string): Array<{
|
|
|
169
156
|
description: string;
|
|
170
157
|
}> | null;
|
|
171
158
|
export const REPORTED_TAGS: Set<string>;
|
|
172
|
-
/**
|
|
173
|
-
* The single grammar definition for keyed Layer-2 placeholders — the exact
|
|
174
|
-
* output of {@link layer2Placeholder}, capture group 1 = the key. Global so
|
|
175
|
-
* callers can scan a document for every placeholder; reset `lastIndex` (or
|
|
176
|
-
* use `matchAll`) between uses.
|
|
177
|
-
*/
|
|
178
|
-
export const LAYER2_PLACEHOLDER_RE: RegExp;
|
|
179
159
|
export const HIDDEN_PLACEHOLDER: "[hidden HTML removed";
|
|
180
160
|
export const COMMENT_PLACEHOLDER: "[HTML comment removed";
|
|
181
|
-
export const UNPARSEABLE_PLACEHOLDER: "[HTML unparseable \u2014 withheld]";
|
|
182
161
|
export const DATA_URI_LENGTH_THRESHOLD: 4096;
|
|
183
162
|
export type CollectedUrl = {
|
|
184
163
|
url: string;
|
|
@@ -213,4 +192,7 @@ import { MD_LINK_HINT } from "./gates.mjs";
|
|
|
213
192
|
import { SECRET_HINT } from "./gates.mjs";
|
|
214
193
|
import { SECRET_HINT_EXT } from "./gates.mjs";
|
|
215
194
|
import { matchesSecretHint } from "./gates.mjs";
|
|
216
|
-
|
|
195
|
+
import { layer2Placeholder } from "./layer2-placeholder.mjs";
|
|
196
|
+
import { LAYER2_PLACEHOLDER_RE } from "./layer2-placeholder.mjs";
|
|
197
|
+
import { UNPARSEABLE_PLACEHOLDER } from "./layer2-placeholder.mjs";
|
|
198
|
+
export { HTML_TAG_PRESENT, MD_LINK_HINT, SECRET_HINT, SECRET_HINT_EXT, matchesSecretHint, layer2Placeholder, LAYER2_PLACEHOLDER_RE, UNPARSEABLE_PLACEHOLDER };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keyed, content-addressed placeholder for one Layer-2 splice:
|
|
3
|
+
* `[hidden HTML removed #<key>]` / `[HTML comment removed #<key>]`, where
|
|
4
|
+
* `<key>` is the first 12 lowercase-hex chars of sha256 over the UTF-8
|
|
5
|
+
* encoding of the ORIGINAL spliced text. Content-addressed on purpose:
|
|
6
|
+
* identical spliced content yields the identical placeholder, so a rehydrator
|
|
7
|
+
* can match placeholder → original by key alone, and duplicated content never
|
|
8
|
+
* produces conflicting keys.
|
|
9
|
+
* @param {"comment" | "hidden"} kind
|
|
10
|
+
* @param {string} original the exact text the splice removed
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function layer2Placeholder(kind: "comment" | "hidden", original: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* The single grammar definition for keyed Layer-2 placeholders — the exact
|
|
16
|
+
* output of {@link layer2Placeholder}, capture group 1 = the key. Global so
|
|
17
|
+
* callers can scan a document for every placeholder; reset `lastIndex` (or
|
|
18
|
+
* use `matchAll`) between uses.
|
|
19
|
+
*/
|
|
20
|
+
export const LAYER2_PLACEHOLDER_RE: RegExp;
|
|
21
|
+
export const UNPARSEABLE_PLACEHOLDER: "[HTML unparseable \u2014 withheld]";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keyed, content-addressed placeholder for one Layer-2 splice:
|
|
3
|
+
* `[hidden HTML removed #<key>]` / `[HTML comment removed #<key>]`, where
|
|
4
|
+
* `<key>` is the first 12 lowercase-hex chars of sha256 over the UTF-8
|
|
5
|
+
* encoding of the ORIGINAL spliced text. Content-addressed on purpose:
|
|
6
|
+
* identical spliced content yields the identical placeholder, so a rehydrator
|
|
7
|
+
* can match placeholder → original by key alone, and duplicated content never
|
|
8
|
+
* produces conflicting keys.
|
|
9
|
+
* @param {"comment" | "hidden"} kind
|
|
10
|
+
* @param {string} original the exact text the splice removed
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function layer2Placeholder(kind: "comment" | "hidden", original: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* The single grammar definition for keyed Layer-2 placeholders — the exact
|
|
16
|
+
* output of {@link layer2Placeholder}, capture group 1 = the key. Global so
|
|
17
|
+
* callers can scan a document for every placeholder; reset `lastIndex` (or
|
|
18
|
+
* use `matchAll`) between uses.
|
|
19
|
+
*/
|
|
20
|
+
export const LAYER2_PLACEHOLDER_RE: RegExp;
|
|
21
|
+
export const UNPARSEABLE_PLACEHOLDER: "[HTML unparseable \u2014 withheld]";
|