@ai-ecoverse/slicc-scrub

中文文档
1.1.0发布于 2026/10/23MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 1万

Zero-dependency secret scrubber (raw, fragments, JSON, percent, HTML, base64, hex, wrapped) shared by the vault, the kernel and slicc-agent

原始使用文档
文档目录7 节
  1. API
  2. The forms
  3. Matching
  4. Streaming
  5. Vectors
  6. Develop
  7. Release

slicc-scrub

One scrubber for every place a SLICC secret could slip into a model's context: the vault's forward responses and model requests, the kernel's exec children, and slicc-agent's tool output. It replaces a secret's value, in every encoding below, before the text moves on. The design is §3.15 of secrets-tokens-design.md (revision 4) in slicc-agent-plan; this README is the spec of what's built.

  • Zero runtime dependencies, no DOM and no Node APIs, no eval and no generated code. It runs as-is in a browser frame, a worker, Node and the kernel.
  • Deterministic: the same values and input always give the same output.
  • One set of vectors, vectors.json, which every consumer runs against its copy.
import { compile } from '@ai-ecoverse/slicc-scrub';

const scrubber = compile([{ name: 'GH_TOKEN', value: 'ghp_…', placeholder: 'fnox_proxy_…' }], { version: 3 });
scrubber.text('token: ghp_…');

API

compile(secrets, options?) → Scrubbersecrets is { name, value, placeholder }[]. A value shorter than 8 bytes of UTF-8 throws a RangeError: it can't be told apart from ordinary output. options are idleMs (default 250) and version (default 0), both only echoed back, and sameLength (default false, below)
scrubber.text(string) → { text, hits }scrubs UTF-8 text. A fragment match can cut a multi-byte character; its remains decode as U+FFFD
scrubber.bytes(Uint8Array) → { bytes, hits }scrubs bytes
scrubber.stream() → Streamscrubs a stream, below
scrubber.version, scrubber.idleMs, scrubber.sizethe options, and the number of distinct patterns
FRAGMENT, MIN_VALUE16 and 8

hits is [{ name, form, count }]: how often each secret was found in each form (raw, fragment, json, percent, html, base64, hex, wrapped). It never contains a value.

The forms

Each value v is matched, as UTF-8 bytes, in these forms. A form is added only when it differs from the ones before it.

formwhat is matched
rawv itself
fragmentany run of 16 bytes or more of v. Values shorter than 16 bytes have none
jsonthe inside of JSON.stringify(v); the same with / as \/; and with every control and non-ASCII UTF-16 unit as \uXXXX, in lower and in upper hex
percentencodeURIComponent(v); the same with lower-case hex; with %20 as +; and form encoding (everything but A–Z a–z 0–9 * - . _ escaped, space as +)
html& < > " as &amp; &lt; &gt; &quot;, and ' as &#39; or &#x27;
base64the standard and URL-safe alphabets: the whole encoding, padded and unpadded, and, for v at each byte offset 0, 1 and 2 inside a longer encoded stream, the run of characters that depends only on v's bytes (when it's at least 8 characters). That covers Basic user:pass with v on either side, JWT segments and base64'd JSON
hexlower and upper, contiguous
wrappedthe base64 and hex forms with CR and LF anywhere inside them (base64 -w 76, PEM, xxd -p)

Matching

  • One Aho–Corasick automaton over every pattern, on bytes. Fragments are found from their 16-byte windows and extended byte by byte along the value. A second automaton over the base64 and hex patterns runs on a view of the input with CR and LF removed, mapped back to the original bytes.
  • Leftmost, then longest: at each point the match that starts first wins, and among those the longest. Ties go to raw, then fragment, json, percent, html, base64, hex, then the earlier secret. Matches never overlap, and replaced text is never scanned again.
  • Replacements: a raw match (a fragment covering the whole value is one) becomes the secret's placeholder; anything else becomes [redacted:<name>].
  • sameLength: true replaces every match, in every form, with exactly as many bytes as it matched: each byte becomes * (0x2A), except CR and LF, which stay where they are. The filler is that one fixed byte, nothing taken from the value, the placeholder or the name, so the output has the input's length and line breaks, and length-prefixed framing around a match (an HTTP Content-Length, a chunk size, a WebSocket frame) still holds. Matching, the forms, hits and streaming are the same as without it; only the replacement differs.
  • Matching is case-sensitive; hex is matched in lower and upper case.

Streaming

stream() returns { push(Uint8Array) → Uint8Array, idle() → Uint8Array, end() → Uint8Array, hits }.

  • push returns every byte that can no longer be part of a match, scrubbed, and holds back the rest: from the earliest point where the input so far could still become a match (a proper prefix of a pattern, a fragment still growing along its value, or a wrapped pattern across CR and LF). A match split across any chunking is therefore found exactly as in one piece; for every input, any chunking followed by end() gives the same bytes as bytes().
  • end scrubs and returns everything held.
  • idle is for the caller to call after idleMs without input, so an interactive prompt can't stall. It releases the held bytes, scrubbed, only when fewer than 16 are held, and otherwise returns nothing. A held tail of 16 bytes or more could be the start of a longer match.
  • hits totals the stream so far.

Vectors

vectors.json (@ai-ecoverse/slicc-scrub/vectors.json) is { format: 1, fragment, minValue, idleMs, cases }. Each case is { name, secrets, input, output, hits }, or with inputHex and outputHex for bytes that aren't UTF-8. scripts/vectors.mjs writes it from how each input is built (Node's own base64, hex and URL encoders), never from the scrubber.

A consumer's test runs, for every case:

  1. bytes(input) (and text where the case is text) gives output and hits, in any order;
  2. for every split point, push(first), push(rest), end() gives output;
  3. one byte at a time gives output.

Develop

npm install
npm run build    # dist/index.js and dist/types
npm run lint
npm test         # vectors.json is fresh, the vectors at every split point, the API, a fuzz test, and CI
npm run vectors  # rewrite vectors.json

Unit tests stay in the gitignored test/unit/ (npm run test:unit), and the pre-commit hook requires them to cover every changed line of src/. The Biome, TypeScript, lefthook and Renovate configuration comes from slicc-shared-web.

Release

.github/workflows/ci.yml is the only workflow: one caller of slicc-shared-web's pipeline, at the same version as the @ai-ecoverse/slicc-shared-web devDependency. A pull request gets lint, npm test (test/integration/*.test.mjs, on one shard, as ci / integration) and the Claude and Codex reviews (ci / review). A push to main runs semantic-release, which tags a version from the conventional commits and publishes @ai-ecoverse/slicc-scrub to npm with provenance, through trusted publishing from ci.yml. There is no other way to release. test/integration/workflows.test.mjs keeps it so.

版本与发布记录

@ai-ecoverse/slicc-scrub 共发布 5 个版本,首次发布于 2026/10/13;过去 12 个月发布了 5 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.1.0latest2026/10/2315 天前
1.0.12026/10/2315 天前
1.0.02026/10/1325 天前
0.0.02026/10/1325 天前
0.0.0-stage2026/10/1326 天前

关于 @ai-ecoverse/slicc-scrub 的常见问题

@ai-ecoverse/slicc-scrub 是什么?

Zero-dependency secret scrubber (raw, fragments, JSON, percent, HTML, base64, hex, wrapped) shared by the vault, the kernel and slicc-agent

如何安装 @ai-ecoverse/slicc-scrub?

在项目目录运行 npm install @ai-ecoverse/slicc-scrub,或使用 pnpm add / yarn add / bun add。

@ai-ecoverse/slicc-scrub 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ai-ecoverse/slicc-scrub 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@ai-ecoverse/slicc-scrub 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ai-ecoverse/slicc-scrub 可以商用吗?

@ai-ecoverse/slicc-scrub 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ai-ecoverse/slicc-scrub 的下载量有多少?

根据 npm 官方下载统计,@ai-ecoverse/slicc-scrub 在 2026/10/05 – 2026/11/03 这 30 天内被下载 10,047 次,日均约 335 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ai-ecoverse/slicc-scrub 还在维护吗?

最新版本 1.1.0 发布于 2026/10/23(15 天前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/10/13。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。