/** * True when every ANSI control introducer in `text` belongs to a display-only * SGR color sequence (so stripping the ANSI removes only cosmetic styling, * nothing that could move the cursor, erase, or carry a payload). Recognizes * both the 7-bit `ESC[…m` and 8-bit C1 (`U+009B…m`) SGR encodings. * * Answered by the STRIPPER'S OWN tokenizer: scanAnsi emits one token per raw * introducer — 7-bit ESC and the whole C1 block, so a C1 cursor-move * (`U+009B 2J`), a C1-OSC string (`U+009D … BEL`), a C1-DCS/APC payload and a * lone or partial escape each yield a non-SGR token — and the predicate is * "every token is SGR". Because the same scan decides what Layer 1 splices, * this can no longer report "colour only" for bytes the stripper leaves behind. * @param {string} text * @returns {boolean} */ export function isSgrOnly(text: string): boolean; /** * Every maximal run of at least {@link LONG_RUN_THRESHOLD} consecutive * payload-capable invisible code points in `text`, in order: `index` is the * run's UTF-16 offset, `text` its verbatim slice, `charCount` its length in * code points. * * What {@link LONG_RUN_RE} means, in the form every scanner in this package * uses — because that regex cannot answer for a large document, and an 8 MB * paste of zero-widths (the exact payload the scan exists to catch) is what * took out the SessionStart scanner, the prompt gate and the tool-output tier * alike. Bounding the quantifier bounds the backtrack stack per `exec`; a run * that hits the bound is continued by {@link RUN_TAIL_RE} until it ends, so the * runs reported are maximal at any length. * @param {string} text * @returns {Generator<{ index: number, text: string, charCount: number }>} */ export function findLongRuns(text: string): Generator<{ index: number; text: string; charCount: number; }>; /** * True when `text` carries at least one {@link findLongRuns} run. * * The bounded pattern answers this on its own: a run long enough to be reported * is long enough to match, whether or not the match reaches the run's end — so * the yes/no costs one anchored scan and never measures the run. * @param {string} text * @returns {boolean} */ export function hasLongRun(text: string): boolean; /** * The agent-facing "Stripped: …" note for a Layer-1 strip: the removed category * labels, the LONG RUN marker when the de-ANSI'd text still holds a * payload-length invisible run, and a pointer to recover the bytes — a hex dump * is ASCII, so it passes through sanitization untouched. The single source of * this note, shared by the `sanitize` convenience entry and the tool-output * pipeline. * @param {string[]} invisFound CATEGORY codes applyLayer1 reported removing * @param {string} deAnsi ANSI-stripped text (invisible runs intact), for the LONG_RUN probe * @returns {string} */ export function describeStripped(invisFound: string[], deAnsi: string): string; /** * Count the PAYLOAD invisible code points in `text`: those the carve-out would * strip, excluding ZWNJ/ZWJ (and emoji VS16) that do real rendering work. * Consumers that gate on invisible density (e.g. the prompt classifier's scatter * threshold) use this so legitimate dense multilingual prose is not mistaken for * a hidden channel. * @param {string} text * @returns {number} */ export function countPayloadInvisible(text: string): number; /** * The `text` with every carve-out-PRESERVABLE invisible (joiners/selectors/tags/ * blank fillers doing real rendering work) replaced by a space, leaving only the * PAYLOAD invisibles in place. The LONG_RUN injection probe runs over this so a * legitimate emoji/flag/variation sequence never trips the "possible injection * payload" marker (alert fatigue), while a genuine hidden run still surfaces. * @param {string} text * @returns {string} */ export function payloadInvisibleView(text: string): string; /** * The first payload-invisible LONG RUN in `text`, or null when there is none. * * THE definition of "this text carries a hidden run", shared by every consumer * that has an opinion about one: the strip's `[LONG RUN — possible injection * payload]` marker, the prompt gate's block decision, and the tool-output * severity tier. They used to spell it twice, and differently — the marker * probed the PAYLOAD view while the prompt gate probed the raw text, so a * legitimate ten-emoji flag sequence (carve-out-preserved, never stripped) was * quietly enough to BLOCK a prompt while the strip that saw the same text * declined to even flag it. Masking the preserved invisibles is the right half * of that disagreement: a run the carve-out keeps is rendering work, not a * channel, and the joiners it does NOT keep are counted as payload anyway (see * {@link countEffectiveInvisible}). * * Because the view replaces only PRESERVED invisibles (and visible characters) * with spaces, a match consists solely of payload code points and is therefore * byte-identical to the corresponding span of `text` — so a caller may report * the sample verbatim. * @param {string} text * @returns {string | null} */ export function payloadLongRunSample(text: string): string | null; /** * How many invisible code points in `text` the strip layer treats as PAYLOAD: * the ones {@link countPayloadInvisible} counts, plus the joiners that sit in a * genuine linguistic context but exceed the carve-out's preservation budget. * * The surplus term closes the preserved-joiner covert channel (O3): * `countPayloadInvisible` excludes every ZWNJ/ZWJ doing real rendering work, so * an attacker who alternates `letter joiner letter joiner …` — every joiner * legitimately between two cursive letters — counts as ZERO there. The strip * layer already refuses that (it preserves joiners only up to * TOTAL_PRESERVED_JOINER_BUDGET / CONSECUTIVE_JOINER_CAP and strips the rest), * so the surplus is read back OFF the strip — the SSOT — rather than by * re-deriving the budget here, which is what would drift. * * A leading BOM is preserved by the strip but counted by * {@link countPayloadInvisible}, so the difference can go slightly negative; * hence the clamp. * @param {string} text ANSI-stripped text (an escape sequence can hide invisibles) * @returns {number} */ export function countEffectiveInvisible(text: string): number; /** * True when the invisible characters in `text` are INCIDENTAL: no hidden run, * and too few of them in total to carry an instruction. * * This is a severity line, not a strip line — the bytes are removed either way * (see ../src/severity.mjs). It exists because a single soft hyphen in a * pasted paragraph, or one variation selector a font demanded, raised the exact * `WARNING: Tool output sanitized` an encoded payload does, and a warning that * fires on a stray character in ordinary prose is one operators learn to skip. * * The bar is {@link LONG_RUN_THRESHOLD} — the count this module already calls * "payload length" — applied to the WHOLE text rather than to one run, so it is * strictly stronger than the run probe: fewer than ten payload-invisible code * points, however they are distributed, cannot spell a smuggled instruction (ten * tag characters are ten ASCII letters). Deliberately NOT the far looser * {@link SCATTERED_THRESHOLD} of 30, which is the prompt gate's BLOCK bar: 29 * tag characters is a short sentence, and staying quiet about a short sentence * hidden in a tool result is not a trade worth making. * @param {string} text ANSI-stripped text, invisible runs intact * @returns {boolean} */ export function isIncidentalInvisible(text: string): boolean; /** * Strip payload-capable invisible chars and report which categories were * removed. A single leading U+FEFF (BOM) is preserved as a legitimate marker; * interior BOMs and all soft hyphens (U+00AD) are stripped, since either can * encode hidden instructions. ZWNJ/ZWJ survive only in a linguistic context * (see the carve-out above). `found` names exactly the categories stripped, so * a caller never warns about a strip the carve-out skipped. * * `originalText` is the pre-processing text (before any ANSI strip) used ONLY to * decide whether a leading BOM is genuinely leading: an interior BOM that an * ANSI-strip left at index 0 of `text` (e.g. `ESC[m + interior U+FEFF`) must NOT be treated as a * legitimate leading marker. Defaults to `text` for the common single-arg call. * @param {string} text * @param {string} [originalText] * @returns {{ cleaned: string, found: string[] }} */ export function stripInvisibleWithReport(text: string, originalText?: string): { cleaned: string; found: string[]; }; /** * Strip payload-capable invisible chars (cleaned text only). See * stripInvisibleWithReport for the BOM and ZWNJ/ZWJ carve-out semantics. * @param {string} text * @returns {string} */ export function stripInvisible(text: string): string; export const VS: string; export const ZERO_WIDTH_MN: "\u034F\u17B4\u17B5"; export const BLANK_NON_CF: string; export const CATEGORY: Readonly<{ CF: "cf-format"; VARIATION_SELECTORS: "variation-selectors"; BLANK_FILLERS: "blank-fillers"; ANSI: "ansi"; LONE_SURROGATES: "lone-surrogates"; HTML_COMMENTS: "html-comments"; HIDDEN_HTML: "hidden-html"; EXFIL_URLS: "exfil-urls"; CONFUSABLE_HOST: "confusable-host"; }>; /** @type {Readonly>} */ export const CATEGORY_LABELS: Readonly>; /** @type {Array<[string, RegExp]>} Each entry pairs a CATEGORY code with its detector. */ export const CHECKS: Array<[string, RegExp]>; export const STRIP: RegExp; export { SGR_RE } from "./ansi.mjs"; export const LONG_RUN_THRESHOLD: 10; /** Total invisible-char count above which a file/prompt is treated as * payload-capable even without a long run (threshold-evasion catch). */ export const SCATTERED_THRESHOLD: 30; /** * The long-run pattern, declaratively: {@link LONG_RUN_THRESHOLD} or more * consecutive {@link STRIP} code points. * * Scan a document with {@link findLongRuns}, not with this: `exec`/`test` * throw `RangeError: Maximum call stack size exceeded` once a run passes * ~8.4 M code points, because V8 pushes one backtrack entry per iteration of * an unbounded quantifier onto a stack capped at 64 MB. This stays public as * the pattern itself, and as the independent oracle the scan is differenced * against (test/invisible-fast-path.test.mjs). */ export const LONG_RUN_RE: RegExp; export const CONSECUTIVE_JOINER_CAP: 8; export const CONSECUTIVE_SELECTOR_CAP: 8; export const TOTAL_PRESERVED_JOINER_BUDGET: 16; export const PRESERVED_JOINER_PER_VISIBLE: 8; export const PRESERVE_HARD_CAP: 64; export const TOTAL_PRESERVED_BLANK_BUDGET: 16; export const PRESERVED_BLANK_PER_ANCHOR: 2; export const LINGUISTIC_SCRIPTS: string[]; export { BRAHMIC_CONSONANT_RANGES } from "./joining-type.mjs";