redac---n 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +323 -2
- package/THIRD_PARTY_NOTICES.md +59 -0
- package/dist/aho.d.ts +25 -0
- package/dist/aho.js +116 -0
- package/dist/base64.d.ts +10 -0
- package/dist/base64.js +29 -0
- package/dist/bip39-data.d.ts +2 -0
- package/dist/bip39-data.js +2 -0
- package/dist/cache.d.ts +14 -0
- package/dist/cache.js +40 -0
- package/dist/calls.d.ts +64 -0
- package/dist/calls.js +269 -0
- package/dist/charlm-data.d.ts +2 -0
- package/dist/charlm-data.js +3 -0
- package/dist/charlm.d.ts +7 -0
- package/dist/charlm.js +65 -0
- package/dist/checksums.d.ts +10 -0
- package/dist/checksums.js +98 -0
- package/dist/commands.d.ts +72 -0
- package/dist/commands.js +787 -0
- package/dist/context.d.ts +98 -0
- package/dist/context.js +469 -0
- package/dist/encoded.d.ts +11 -0
- package/dist/encoded.js +336 -0
- package/dist/environment.d.ts +10 -0
- package/dist/environment.js +62 -0
- package/dist/formats-data.d.ts +2 -0
- package/dist/formats-data.js +405 -0
- package/dist/formats.d.ts +5 -0
- package/dist/formats.js +470 -0
- package/dist/hmac.d.ts +3 -0
- package/dist/hmac.js +87 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +488 -0
- package/dist/json.d.ts +82 -0
- package/dist/json.js +392 -0
- package/dist/known.d.ts +34 -0
- package/dist/known.js +340 -0
- package/dist/lines.d.ts +28 -0
- package/dist/lines.js +69 -0
- package/dist/lookup.d.ts +5 -0
- package/dist/lookup.js +7 -0
- package/dist/model-data.d.ts +3 -0
- package/dist/model-data.js +4 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +19 -0
- package/dist/named.d.ts +20 -0
- package/dist/named.js +3257 -0
- package/dist/names.d.ts +106 -0
- package/dist/names.js +1517 -0
- package/dist/normalize.d.ts +25 -0
- package/dist/normalize.js +529 -0
- package/dist/pem.d.ts +32 -0
- package/dist/pem.js +269 -0
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +470 -0
- package/dist/public.d.ts +44 -0
- package/dist/public.js +362 -0
- package/dist/publickeys.d.ts +41 -0
- package/dist/publickeys.js +232 -0
- package/dist/random.d.ts +29 -0
- package/dist/random.js +165 -0
- package/dist/repeats.d.ts +21 -0
- package/dist/repeats.js +80 -0
- package/dist/rules.d.ts +42 -0
- package/dist/rules.js +58 -0
- package/dist/runs.d.ts +10 -0
- package/dist/runs.js +36 -0
- package/dist/secretfile.d.ts +52 -0
- package/dist/secretfile.js +722 -0
- package/dist/shell.d.ts +127 -0
- package/dist/shell.js +703 -0
- package/dist/spans.d.ts +61 -0
- package/dist/spans.js +287 -0
- package/dist/sql.d.ts +20 -0
- package/dist/sql.js +66 -0
- package/dist/stream.d.ts +46 -0
- package/dist/stream.js +150 -0
- package/dist/structure.d.ts +24 -0
- package/dist/structure.js +1625 -0
- package/dist/syntax.d.ts +156 -0
- package/dist/syntax.js +446 -0
- package/dist/transcript.d.ts +30 -0
- package/dist/transcript.js +713 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +1 -0
- package/dist/unnamed.d.ts +59 -0
- package/dist/unnamed.js +939 -0
- package/dist/urls.d.ts +70 -0
- package/dist/urls.js +303 -0
- package/dist/values.d.ts +178 -0
- package/dist/values.js +1621 -0
- package/dist/yaml.d.ts +40 -0
- package/dist/yaml.js +174 -0
- package/docs/creddata.svg +318 -0
- package/docs/guessing.md +139 -0
- package/docs/holdout.svg +85 -0
- package/docs/layers.md +80 -0
- package/docs/models.svg +81 -0
- package/docs/real-text.svg +112 -0
- package/package.json +57 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KTibow
|
|
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
CHANGED
|
@@ -1,3 +1,324 @@
|
|
|
1
|
-
#
|
|
1
|
+
# redac---n
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Hide secrets in what your coding agent sends to a model provider.
|
|
4
|
+
|
|
5
|
+
Agents read `.env` files, run `kubectl get secret`, paste CI logs and run curl with auth headers, and all of it ends
|
|
6
|
+
up in the context sent to the provider, any router in between, and your logs. redac---n finds the secrets in that
|
|
7
|
+
text and replaces them with placeholders, and leaves alone the commit SHAs, digests, IDs and code the agent needs to
|
|
8
|
+
keep working.
|
|
9
|
+
|
|
10
|
+
TypeScript, no dependencies, no Node-only APIs: it runs on Node 20+, Bun and in the browser.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install redac---n
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Redactor } from "redac---n";
|
|
18
|
+
|
|
19
|
+
const redactor = new Redactor();
|
|
20
|
+
|
|
21
|
+
redactor.redact("DATABASE_URL=postgres://app:Hx7mQ2pL9vRt@db:5432/app\nSALT=k2Lm9Qx", { hint: { command: "cat .env" } }).text;
|
|
22
|
+
// DATABASE_URL=postgres://app:[REDACTED password #a794f2]@db:5432/app
|
|
23
|
+
// SALT=[REDACTED secret #48712e] ← the hint says this is a .env file; without it, a 7-character SALT would stay
|
|
24
|
+
|
|
25
|
+
redactor.redactValue(messages).value; // a whole Anthropic or OpenAI message list, before you send it
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**[Try it in your browser](https://kendell.dev/redac---n/):** paste a tool's output, give the command or file it
|
|
29
|
+
came from, and see what would be sent. The library runs in the page, so nothing you paste leaves it.
|
|
30
|
+
|
|
31
|
+
## How well it works
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
On a held-out set of realistic agent output (1,112 cases, never used for tuning), redac---n catches **93.0%** of
|
|
36
|
+
secrets and destroys **3.0%** of the values that look like secrets but aren't. The best combination of existing
|
|
37
|
+
scanners we found catches 70.6% and destroys 2.2%; all seven together catch 84.2% but destroy 28.6% of the
|
|
38
|
+
look-alikes.
|
|
39
|
+
|
|
40
|
+
| | secrets caught | look-alikes destroyed |
|
|
41
|
+
|---|---:|---:|
|
|
42
|
+
| **redac---n** (default) | **93.0%** | **3.0%** |
|
|
43
|
+
| redac---n, `guessing: "off"` | 90.9% | 1.1% |
|
|
44
|
+
| gitleaks + Betterleaks + Kingfisher | 70.6% | 2.2% |
|
|
45
|
+
| all seven scanners combined | 84.2% | 28.6% |
|
|
46
|
+
| CredSweeper, the best single scanner | 70.1% | 15.2% |
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
On ordinary text with almost no secrets in it (agent trajectories and repositories it never saw), it redacts less
|
|
51
|
+
than Betterleaks, CredSweeper, Nosey Parker and detect-secrets; the scanners that redact less catch 39–55% of
|
|
52
|
+
secrets. Each bar is split by what was redacted, every tool's redactions sorted by one rule
|
|
53
|
+
([eval/shapes.ts](eval/shapes.ts)): most of what Betterleaks and Nosey Parker hide is ordinary words, most of what
|
|
54
|
+
CredSweeper hides is hex (hashes and IDs), most of what detect-secrets hides is other values such as base64 digests,
|
|
55
|
+
and about half of redac---n's is credential-shaped. It can also
|
|
56
|
+
do something they can't: told which command or file the text came from, it reads what `cat .env` printed as a secret
|
|
57
|
+
file, and knows that `cat .env | wc -l` printed only a number.
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
[CredData](https://github.com/Samsung/CredData) is labelled lines from public repositories, sorted here into the same
|
|
62
|
+
kinds. The top row is its dev split, the repositories the fixes were made on; the bottom row is its test split, which
|
|
63
|
+
none of them saw. On the test split redac---n catches more than any other scanner but CredSweeper in every kind, and
|
|
64
|
+
hits fewer false values than the other six combined: 88% of credential-shaped secrets for 6.6% of the false values
|
|
65
|
+
there, against 75% and 14%. CredSweeper is drawn apart because CredData's lines come partly from it and its model is
|
|
66
|
+
trained on CredData's labels.
|
|
67
|
+
|
|
68
|
+
It isn't a guarantee: about one secret in fourteen still gets through, mostly short values with no name or format
|
|
69
|
+
next to them. See [what it misses](#what-it-misses).
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary>Every test set, and how they're scored</summary>
|
|
73
|
+
|
|
74
|
+
**Scoring.** A secret counts as caught only if no run of half its length (at least 8 characters) survives. A
|
|
75
|
+
look-alike counts as destroyed if it doesn't survive exactly. Ordinary text has almost no secrets, so every
|
|
76
|
+
redaction there counts as a false one.
|
|
77
|
+
|
|
78
|
+
**Held-out sets** were written blind, by agents that never saw the code, and scored before anyone looked at them.
|
|
79
|
+
Held-out 1 is scored only as totals, and its failures are never looked at. Each later set was scored once, blind,
|
|
80
|
+
and then became development data. Secrets caught / look-alikes destroyed at that blind scoring:
|
|
81
|
+
|
|
82
|
+
| set | redac---n | `guessing: "off"` | gitleaks + Betterleaks + Kingfisher | best single other scanner |
|
|
83
|
+
|---|---:|---:|---:|---:|
|
|
84
|
+
| Held-out 1: 1,112 cases over 104 kinds of output | 93.0 / 3.0 | 90.9 / 1.1 | 70.6 / 2.2 | CredSweeper 70.1 / 15.2 |
|
|
85
|
+
| Held-out 2: 848 cases from blind template writers | 86.8 / 5.7 | 78.3 / 3.7 | 60.3 / 5.1 | CredSweeper 65.7 / 20.1 |
|
|
86
|
+
| Held-out 3: 450 cases, mobile, ML, IoT, enterprise stacks, other CI vendors | 85.5 / 7.4 | 78.5 / 3.2 | 64.0 / 4.8 | CredSweeper 69.4 / 21.0 |
|
|
87
|
+
| Held-out 4: 478 everyday agent sessions, issue and PR threads | 90.4 / 1.9 | 86.7 / 0.7 | 66.2 / 3.6 | CredSweeper 74.1 / 20.4 |
|
|
88
|
+
| Held-out 5: 352 session logs as stored or sent (Claude Code and Codex JSONL, API request bodies) | 95.3 / 1.7 | 90.7 / 0.1 | 69.5 / 1.1 | CredSweeper 84.4 / 28.7 |
|
|
89
|
+
| Held-out 6: 112 cases from regional clouds and payment APIs | 93.2 / 9.3 | 78.9 / 5.8 | 61.7 / 3.1 | CredSweeper 73.7 / 18.1 |
|
|
90
|
+
| Held-out 7: 305 cases from databases, queues, observability, build tooling | 93.4 / 8.5 | 85.1 / 3.8 | 69.6 / 3.1 | CredSweeper 67.5 / 19.8 |
|
|
91
|
+
| Held-out 8: 261 cases from data and analytics tooling | 89.0 / 1.5 | 83.7 / 0.8 | 67.9 / 1.8 | CredSweeper 72.4 / 16.7 |
|
|
92
|
+
| [SlotGuard](https://github.com/illinoisdata/SlotGuard): 200 agent sessions, 852 planted values (652 credentials, and 200 email addresses this library leaves alone) | 62.6 / 0.0 | 53.1 / 0.0 | 29.2 / 0.0 | CredSweeper 46.9 / 0.0 |
|
|
93
|
+
| [ProwlBench](https://github.com/Lercas/prowlbench): 7,491 secrets in code, tickets, logs and chat in eight languages; 7,375 "secret-free" snippets, some with values under credential names that this library hides by design | 89.4 / 12.5 | 78.6 / 7.8 | 59.1 / 8.7 | detect-secrets 63.8 / 19.1 |
|
|
94
|
+
|
|
95
|
+
Other measurements; where a set was first scored blind, that score comes first:
|
|
96
|
+
|
|
97
|
+
| set | result |
|
|
98
|
+
|---|---|
|
|
99
|
+
| Development sets (4,250 cases, 6,424 secrets, 18,402 look-alikes; held-out 2–8 among them) | 95.4% caught, 1.4% destroyed; 95.4% and 1.4% when each case is wrapped in an Anthropic or OpenAI transcript and passed to `redactValue` |
|
|
100
|
+
| [CredData](https://github.com/Samsung/CredData) test split (real lines from public repositories) | 82.8% caught and 3.2% of non-secret lines hit, blind; now 83.5% and 3.5%. On the dev split it was tuned on, 91.1% and 1.8%: the gap shows how much of the tuning doesn't carry over to new repositories |
|
|
101
|
+
| Five more ordinary-text sets (new repositories, trajectory shards, donated agent sessions from [Trace Commons](https://huggingface.co/datasets/trace-commons/agent-traces), agent pull requests from [AIDev](https://huggingface.co/datasets/hao-li/AIDev), repositories that handle credentials) | 5.5, 4.1, 5.6, 22.5 and 17.0 redactions per million characters, blind (the last two hold AWS signatures in presigned URLs and recorded OAuth test runs with real tokens) |
|
|
102
|
+
| [GitLab's secret-detection rules](https://gitlab.com/gitlab-org/security-products/secret-detection/secret-detection-rules), each rule's examples | 81.4% of 2,038 caught and 23% of 168 negatives hit, blind; now 88.0% and 15% |
|
|
103
|
+
| Credential-handling code with no credentials ([ICSME 2026](https://doi.org/10.5281/zenodo.22942243), 5,903 snippets) | 4.6% of snippets hit, blind; now 2.0%, mostly passwords written into the code |
|
|
104
|
+
| [levainbot/fp-corpus](https://github.com/levainbot/fp-corpus): log and build output, 85 planted values | 79 caught and 3 of 116 clean sections hit, blind; now 81 (the four missed: an email address, two card numbers and one key) and 4 of 116 |
|
|
105
|
+
| Issue-report secrets ([Zenodo](https://doi.org/10.5281/zenodo.17430335)) | 76.6% caught, 3.1% destroyed |
|
|
106
|
+
| Speed | about 2 million characters a second on one core, synchronous (a 1 MB first send blocks for about half a second: use a worker for big bodies) |
|
|
107
|
+
|
|
108
|
+
**Other scanners** ran with their defaults and no network verification, on the same files: gitleaks, Betterleaks,
|
|
109
|
+
Kingfisher, TruffleHog, Nosey Parker, detect-secrets and CredSweeper (`eval/baselines.py`). Each case was a file,
|
|
110
|
+
named whichever way scored better for that scanner: all `.txt`, or an extension from the case's context (`.env`,
|
|
111
|
+
`.json`, `.py`). redac---n got the case's command or path as a hint, as an agent harness would give it. Part of the
|
|
112
|
+
measured loss is labels that disagree with this library's policy (see
|
|
113
|
+
[what it keeps on purpose](#what-it-keeps-on-purpose)).
|
|
114
|
+
|
|
115
|
+
`eval/HELDOUT.md` has every blind score and what each set led to. [Why each layer is there](docs/layers.md)
|
|
116
|
+
measures what each detector adds and costs.
|
|
117
|
+
|
|
118
|
+
</details>
|
|
119
|
+
|
|
120
|
+
## Use
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const redactor = new Redactor({
|
|
124
|
+
known: [{ value: stripeKey, label: "Stripe key" }], // values you hold: hidden wherever they turn up
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
redactor.redact(text, { hint }); // { text, findings }
|
|
128
|
+
redactor.redactValue(messages); // { value, findings }: every string in a JSON value
|
|
129
|
+
redactor.stream({ hint }); // { write(chunk), end() }: each returns redacted text ready to pass on
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Give it a hint whenever you can.** A hint says where the text came from, which changes what it means:
|
|
133
|
+
|
|
134
|
+
- `{ command: "cat .env" }`: the shell command that printed it. It's read with a real shell parser, so
|
|
135
|
+
`cat .env | nl` and `python3 -c "print(open('.env').read())"` print the file, while `cat .env | wc -l`,
|
|
136
|
+
`cut -d= -f1 .env` and `sed -i … .env` don't. `gh auth token`, `op read` and `openssl rand -hex 32` print a
|
|
137
|
+
credential.
|
|
138
|
+
- `{ path: "config/credentials.json" }`: the file it was read from.
|
|
139
|
+
- `{ secretFile: true }`: your own rules say it's a secret file (from a configured secrets directory, say).
|
|
140
|
+
|
|
141
|
+
In a secret file, every value under a credential's name and every bare token is hidden, whatever it looks like;
|
|
142
|
+
paths, references, URLs without passwords, sentences, booleans and numbers stay.
|
|
143
|
+
|
|
144
|
+
**Guessing.** Every rule always runs; `guessing` only sets how hard a model guesses at random-looking tokens with no
|
|
145
|
+
name or format next to them. `"confident"` (the default) takes the ones it's sure of; `"off"` makes no guesses, for
|
|
146
|
+
fewer false hits and lower recall; `"eager"` takes less certain ones too. Inside a secret file the same model always
|
|
147
|
+
runs, whatever `guessing` says, but there it isn't guessing whether the text holds secrets (the hint already says
|
|
148
|
+
it does): it sorts the file's values under neutral names into generated ones, which are hidden (`SALT=k2Lm9Qx`), and
|
|
149
|
+
settings, which stay (`PORT=8080`). [How it guesses](docs/guessing.md) explains the model and how it was measured.
|
|
150
|
+
|
|
151
|
+
**Placeholders** look like `[REDACTED Stripe key #3f9a1c]`. The tag is a keyed hash of the value, so the model can
|
|
152
|
+
tell when two placeholders hide the same secret, but nobody without the key can test guesses against it. One value
|
|
153
|
+
written different ways (URL-encoded, shell-escaped, JSON-escaped) gets the same tag and kind everywhere it appears.
|
|
154
|
+
JSON stays valid, and redacting already-redacted text changes nothing. The key is random for each Redactor: pass
|
|
155
|
+
`fingerprintKey` to keep tags stable across restarts (for a prompt cache, say), and keep it as secret as the
|
|
156
|
+
values. The kind can depend on the text (`password` in one place, `secret` in a secret file). Pass `placeholder` to
|
|
157
|
+
use your own format.
|
|
158
|
+
|
|
159
|
+
**More:**
|
|
160
|
+
|
|
161
|
+
- `detect(text, { hint })` returns the findings without changing the text (for an audit log, say). The findings
|
|
162
|
+
are yours to change.
|
|
163
|
+
- `fromEnvironment(process.env)` collects an environment's secrets as known values: variables with credential
|
|
164
|
+
names, vendor tokens under any name, and passwords inside URLs.
|
|
165
|
+
- `learn(text, { hint })` remembers what it finds, so it's hidden wherever it shows up later. It returns how many
|
|
166
|
+
values were new.
|
|
167
|
+
- `addKnown(value, label)` adds one value. Values too ordinary to hide everywhere are refused (it returns false):
|
|
168
|
+
under 6 characters, numbers of up to 8 digits, and letters-only values under 14 characters.
|
|
169
|
+
- `hashes: "hide"` hides password hashes too (they're kept by default).
|
|
170
|
+
- The standalone `redact(text, options)` export uses a throwaway Redactor: nothing is remembered, and tags are
|
|
171
|
+
random.
|
|
172
|
+
|
|
173
|
+
### Transcripts
|
|
174
|
+
|
|
175
|
+
`redactValue` takes a whole request body. Each tool result is judged with the command or path of the tool call that
|
|
176
|
+
produced it, matched by call ID, and content a tool writes to a file is judged as that file. IDs, roles, model
|
|
177
|
+
names, signatures and images are left alone. Base64 documents with a text media type are decoded and scanned.
|
|
178
|
+
Values are redacted as `JSON.stringify` would send them. Pass the parsed value, not its JSON text: `redact` on a
|
|
179
|
+
serialized conversation reads the strings in it, but can't match a tool's result to its call, so what `cat .env`
|
|
180
|
+
printed is judged without its command.
|
|
181
|
+
|
|
182
|
+
It learns before it redacts: a secret found once (`API_KEY=…` in a `cat .env`) is hidden everywhere in the
|
|
183
|
+
conversation, including earlier turns and wherever the model repeats it, and in whatever this Redactor redacts
|
|
184
|
+
later. Pass `{ learn: false }` to keep that to this call. A Redactor remembers what it found in each text, so
|
|
185
|
+
re-sending a growing conversation only costs the new turn, unless that turn holds a secret it hadn't seen: then the
|
|
186
|
+
history is scanned again to hide it there too. (This memory is capped at 16 M characters of text, plus the values
|
|
187
|
+
it holds.)
|
|
188
|
+
|
|
189
|
+
Anthropic signs `thinking` blocks. One that quotes a secret the model saw is redacted like any other text, and its
|
|
190
|
+
signature then no longer verifies, so a request continuing that turn is rejected: a failed request instead of a
|
|
191
|
+
leaked secret. Blocks with nothing to hide are left as they are. Pass `{ thinking: "keep" }` if you'd trust the
|
|
192
|
+
provider with those secrets anyway.
|
|
193
|
+
|
|
194
|
+
`stream()` releases text in batches once it's at least 32 K characters behind the newest chunk (`holdback`, at
|
|
195
|
+
least 8 K), so a secret split across chunks is never released half-redacted. Nothing comes out until about 64 K
|
|
196
|
+
characters have arrived, so it suits long tool output better than a live view; `end()` flushes the rest. An
|
|
197
|
+
unfinished line is held until it ends, up to 1 M characters (minified output, a one-line base64 blob). Its output
|
|
198
|
+
matches `redact()` on the whole text, except for a single value longer than the holdback, evidence more than 8 K
|
|
199
|
+
characters back, and a value named only a holdback's length after it.
|
|
200
|
+
|
|
201
|
+
## Security model
|
|
202
|
+
|
|
203
|
+
- **What the provider still sees:** all text that isn't a secret, each placeholder's kind (a format's name,
|
|
204
|
+
`password`, or your label: keep labels free of anything sensitive), and tags that link repeats of one value.
|
|
205
|
+
- **One Redactor per user or tenant.** A Redactor holds every value it was given or learned, in plain text, and
|
|
206
|
+
hides them in everything it redacts afterwards. Shared across customers, one customer's secrets and labels would
|
|
207
|
+
turn up in another's redacted text.
|
|
208
|
+
- **Learning works backwards.** A secret found in turn 5 is hidden in turns 1–4 the next time the history is
|
|
209
|
+
redacted. The provider already saw what was sent before, and the earlier turns now read differently, which
|
|
210
|
+
invalidates a prompt cache from the first changed turn.
|
|
211
|
+
- **Hostile text** (a repository or web page the agent reads) can't unhide a value you gave the Redactor or one it
|
|
212
|
+
learned: those are hidden whatever surrounds them. It can teach it a value to hide (`API_TOKEN=<a commit SHA>`
|
|
213
|
+
hides that SHA from then on), and next to a value it doesn't know, it can add context that reads as public, such
|
|
214
|
+
as an `EXAMPLE` marker or a hash's name.
|
|
215
|
+
- **Failures close.** If a detector throws, the whole text becomes one placeholder. `redact` throws on input that
|
|
216
|
+
isn't a string: treat a throw as "don't send".
|
|
217
|
+
- **Findings carry the secret** (`value`, the secret as a program would read it). Strip it before you log them.
|
|
218
|
+
- **Don't put secrets back into the model's tool calls.** A compromised router or an injected instruction could
|
|
219
|
+
send a restored secret anywhere. Let the executor or an egress proxy fill in credentials at run time
|
|
220
|
+
(`$STRIPE_KEY`). An agent that edits files it read through redaction needs its executor to restore that file's
|
|
221
|
+
values when writing it.
|
|
222
|
+
|
|
223
|
+
## What it finds
|
|
224
|
+
|
|
225
|
+
Seven detectors propose spans, and one decision stage (`src/policy.ts`) rules on what they propose, the same way
|
|
226
|
+
whichever detector found it. Values you gave it and a secret file's contents skip that stage: you, or the file,
|
|
227
|
+
already said what they are. Each detector catches secrets the others miss; [why each layer is there](docs/layers.md)
|
|
228
|
+
measures what each adds and what each costs.
|
|
229
|
+
|
|
230
|
+
1. **Values you gave it**, in every encoding output turns them into: JSON-, URL-, shell-, SQL- and HTML-escaped,
|
|
231
|
+
hex, base64 at any offset, and cut short.
|
|
232
|
+
2. **536 vendor token formats**: 137 maintained here and 399 from
|
|
233
|
+
[Betterleaks](https://github.com/betterleaks/betterleaks)' catalog (some vendors are in both).
|
|
234
|
+
3. **Structure:** private keys (by their DER, under any PEM label), URL passwords, Authorization headers, cookies,
|
|
235
|
+
connection strings, signed URLs, Kubernetes Secrets, CI masks, sign-in links, `.netrc`, and passwords given to
|
|
236
|
+
command-line tools, tool by tool (`mysql -p`, `curl -u`, `docker login -p`; but `redis-cli -p 6379` keeps its
|
|
237
|
+
port).
|
|
238
|
+
4. **Names:** values under credential names in every syntax tools print: env files, YAML, JSON, INI, code
|
|
239
|
+
assignments and declarations, flags, XML, setters, calls that take a secret (`jwt.sign(payload, secret)`),
|
|
240
|
+
commands that set one (`setx`, `npm config set`), tables and SQL rows, labels in prose and other languages, a
|
|
241
|
+
name on the line above, a column's header, a list's name, "here's your key:". A name is judged by its words:
|
|
242
|
+
`DB_PASSWORD_PROD` is a credential, `PASSWORD_MIN_LENGTH` and `TOKEN_URL` aren't. References, placeholders,
|
|
243
|
+
types and code stay, and so does a password that's just the user's or the database's name next to it.
|
|
244
|
+
5. **Where the text came from:** the output of `cat .env` and other secret files, commands that print a credential
|
|
245
|
+
(`gh auth token`), and secret files written inside the text (a heredoc, a diff's hunks).
|
|
246
|
+
6. **Encoded:** base64 and hex (including `xxd` and `hexdump` output), decoded and judged as the text they hold.
|
|
247
|
+
7. **Random tokens with no name or format:** a LightGBM model scores each token on how random it is for its length
|
|
248
|
+
and alphabet and on its context, and fixed rules keep what's never a key (hashes, UUIDs, public keys, encrypted
|
|
249
|
+
values, IDs).
|
|
250
|
+
|
|
251
|
+
Before any of that, text is read the way a terminal shows it (ANSI colours, line continuations, HTML entities,
|
|
252
|
+
invisible characters and a viewer's line numbers removed, from `cat -n` or an agent's Read tool), and JSON strings,
|
|
253
|
+
Python reprs, percent-encoded text and string literals split in pieces (`"sk-" + "…"`) are decoded or joined and
|
|
254
|
+
scanned as what they hold.
|
|
255
|
+
|
|
256
|
+
## What it misses
|
|
257
|
+
|
|
258
|
+
- **Short secrets with no name, format or context**, alone in ordinary text, look like any other ID. Random tokens
|
|
259
|
+
longer than 256 characters aren't judged on randomness alone either (they're far more often images and
|
|
260
|
+
certificates): those need a name, a format or a header.
|
|
261
|
+
- **Passwords in conversation** with no name next to them ("use Lamp-otter-91Qx"). In issue reports overall, about
|
|
262
|
+
three secrets in four are caught.
|
|
263
|
+
- **Shortened values** with `...` in the middle (`sk-abc...xyz`) are taken as a display, not a secret.
|
|
264
|
+
- **Keys in forms no rule reads:** a key written as a list of byte values (`22, 53, 50, …`), and formats with no
|
|
265
|
+
rule yet (PASERK keys).
|
|
266
|
+
- **Personal data.** It doesn't detect names, email addresses, SSNs, card numbers or postal addresses. Pass the ones
|
|
267
|
+
you know as `known` values: they're matched as written (and escaped), not in other formats (`4111 1111…` doesn't
|
|
268
|
+
match `4111-1111…`), and a one-word name under 14 letters is too ordinary to accept.
|
|
269
|
+
|
|
270
|
+
### What it keeps on purpose
|
|
271
|
+
|
|
272
|
+
These are fixed policy, not settings; `hashes: "hide"` is the only switch. To hide one of these values anyway, pass
|
|
273
|
+
it as a `known` value: known values are hidden whatever they look like, as long as they aren't too ordinary to accept
|
|
274
|
+
(`changeme` is refused; see `addKnown`).
|
|
275
|
+
|
|
276
|
+
- The stand-ins every common-password list starts with, under a password's name (`password`, `changeme`, `hunter2`,
|
|
277
|
+
`postgres`, `test123`), and a device's default PIN where the text says so. Docs use these far more than configs
|
|
278
|
+
do, and hiding a password an attacker tries first protects little.
|
|
279
|
+
- Password hashes, unless you pass `hashes: "hide"`.
|
|
280
|
+
- Hex and UUIDs under names that don't say they're credentials: far more often a commit, a digest or an ID than a
|
|
281
|
+
key.
|
|
282
|
+
- Encrypted values: SOPS, Ansible Vault, Sealed Secrets, Spring `{cipher}`, JWE, DPAPI. Jenkins' and Maven's `{…}`
|
|
283
|
+
are hidden, since their key sits on the same machine.
|
|
284
|
+
- Keys that are public by design: publishable keys (`pk_live_`), Sentry DSNs, Supabase anon keys, Google `AIza`
|
|
285
|
+
keys in client configuration (but not in a server's environment), keys passed to browser and mobile SDKs, Bitcoin
|
|
286
|
+
and NATS public keys.
|
|
287
|
+
- Authentication challenges a server sends (`WWW-Authenticate: NTLM …`).
|
|
288
|
+
- Test vectors under generic names in files that say they are test vectors (Wycheproof, CAVP).
|
|
289
|
+
|
|
290
|
+
## Development
|
|
291
|
+
|
|
292
|
+
`pnpm test` runs the behaviour tests (Node 22.18 or later), and `pnpm check` runs the quality gate.
|
|
293
|
+
|
|
294
|
+
<details>
|
|
295
|
+
<summary>Tests, the quality gate and the evaluations</summary>
|
|
296
|
+
|
|
297
|
+
- **Behaviour tests** (`test/`, by topic): a few cases per rule or decision, cross-products of credential names,
|
|
298
|
+
syntaxes and value shapes, and checks that long and repetitive inputs take linear time.
|
|
299
|
+
- **The quality gate** (`pnpm check`) scores every development set and the 132 regression sets
|
|
300
|
+
(`eval/corpora/regress/README.md`: probes of each rule's edges) against a committed baseline
|
|
301
|
+
(`eval/baseline.json.gz`), and fails on any case that newly leaks, is newly destroyed or is newly slow.
|
|
302
|
+
`scripts/snap.ts` does the same with a sample of ordinary text from the external corpora added.
|
|
303
|
+
- **Fuzzers:** `eval/fuzz-secrets.ts` hides random passwords and keys of ten shapes in twelve syntaxes, with values
|
|
304
|
+
up to 5,000 characters; `scripts/fuzz-perf.ts` puts every rule's trigger after long runs of filler and fails on
|
|
305
|
+
any input that takes more than linear time.
|
|
306
|
+
- **Evaluations:** `node eval/all.ts` runs the development checks. `node eval/layers.ts` scores each detector alone
|
|
307
|
+
and left out. `node eval/report.ts` scores the held-out set (totals only) and draws the first two charts above;
|
|
308
|
+
`node eval/creddata-chart.ts` draws the CredData charts (its header gives the steps before it).
|
|
309
|
+
- **The demo** (`site/`): `pnpm site` builds it into `_site/`, and every push to `main` deploys it to GitHub Pages
|
|
310
|
+
(`.github/workflows/pages.yml`).
|
|
311
|
+
- **Corpora:** the third-party corpora live outside the repository (`$REDACT_CORPORA`, by default
|
|
312
|
+
`~/Downloads/redact-corpora`); on a fresh clone, the scripts score `eval/corpora` and skip the rest. The corpora in
|
|
313
|
+
the repository are gzipped, because the fake credentials in them would otherwise look like real ones to secret
|
|
314
|
+
scanners, GitHub's push protection among them.
|
|
315
|
+
- **The model** is trained on public text and synthetic keys, never on the evaluation sets. That corpus isn't
|
|
316
|
+
published; the training scripts are, and train a model of the same design on yours
|
|
317
|
+
([How it guesses](docs/guessing.md#rebuilding-the-model)).
|
|
318
|
+
|
|
319
|
+
</details>
|
|
320
|
+
|
|
321
|
+
## License
|
|
322
|
+
|
|
323
|
+
MIT. The imported formats come from Betterleaks and the BIP-39 wordlist from Bitcoin's BIPs, both MIT; see
|
|
324
|
+
`THIRD_PARTY_NOTICES.md`.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## Betterleaks
|
|
4
|
+
|
|
5
|
+
`src/formats-data.ts` (shipped as `dist/formats-data.js`) is generated from the rule catalog of Betterleaks (https://github.com/betterleaks/betterleaks).
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Zachary Rice
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## BIP-39 English wordlist
|
|
32
|
+
|
|
33
|
+
`src/bip39-data.ts` (shipped as `dist/bip39-data.js`) holds the English wordlist of BIP-39, "Mnemonic code for
|
|
34
|
+
generating deterministic keys" (https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki), which states
|
|
35
|
+
"This BIP falls under the MIT License."
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
MIT License
|
|
39
|
+
|
|
40
|
+
Copyright (c) Marek Palatinus, Pavol Rusnak, Aaron Voisine, Sean Bowe (the authors of BIP-39)
|
|
41
|
+
|
|
42
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
43
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
44
|
+
in the Software without restriction, including without limitation the rights
|
|
45
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
46
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
47
|
+
furnished to do so, subject to the following conditions:
|
|
48
|
+
|
|
49
|
+
The above copyright notice and this permission notice shall be included in all
|
|
50
|
+
copies or substantial portions of the Software.
|
|
51
|
+
|
|
52
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
53
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
54
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
55
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
56
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
57
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
58
|
+
SOFTWARE.
|
|
59
|
+
```
|
package/dist/aho.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class KeywordSet {
|
|
2
|
+
private next;
|
|
3
|
+
private fail;
|
|
4
|
+
private out;
|
|
5
|
+
readonly keywords: string[];
|
|
6
|
+
private fold;
|
|
7
|
+
/** The character classes: 0 for a character in no keyword, else one per distinct character of the keywords. */
|
|
8
|
+
private classOf;
|
|
9
|
+
private width;
|
|
10
|
+
/** The next state for each state and class, `state * width + class`. */
|
|
11
|
+
private table;
|
|
12
|
+
private hasOut;
|
|
13
|
+
/** An empty keyword occurs everywhere, and indices are the callers' (a keyword can't be dropped), so it is refused. */
|
|
14
|
+
constructor(keywords: string[], caseSensitive?: boolean);
|
|
15
|
+
/**
|
|
16
|
+
* The automaton as a table, one read per character: for each state and each character that occurs in a keyword, the
|
|
17
|
+
* state after it (the failure links followed ahead of time); a character that occurs in none leads to the start. `order`
|
|
18
|
+
* is the states below the start, shallowest first, so a state's failure state is tabulated before it.
|
|
19
|
+
*/
|
|
20
|
+
private tabulate;
|
|
21
|
+
/** Indices of the keywords that occur in `text`. */
|
|
22
|
+
find(text: string): Set<number>;
|
|
23
|
+
/** Every occurrence of every keyword: [start, keyword index]. */
|
|
24
|
+
matches(text: string): Generator<[number, number]>;
|
|
25
|
+
}
|
package/dist/aho.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aho-Corasick: which of many keywords occur in a text, in one linear pass. Used to decide which format
|
|
3
|
+
* rules can match before running any of them (ASCII case folded, in the keywords as in the text), and to find
|
|
4
|
+
* short known values (case-sensitive, every occurrence).
|
|
5
|
+
*/
|
|
6
|
+
/** The number of UTF-16 code units, which a character class table is indexed by. */
|
|
7
|
+
const CHAR_CODES = 0x10000;
|
|
8
|
+
/** A character code with ASCII upper case folded to lower case. */
|
|
9
|
+
const foldAscii = (c) => (c >= 65 && c <= 90 ? c + 32 : c);
|
|
10
|
+
export class KeywordSet {
|
|
11
|
+
next = [new Map()];
|
|
12
|
+
fail = [0];
|
|
13
|
+
out = [[]];
|
|
14
|
+
keywords;
|
|
15
|
+
fold;
|
|
16
|
+
/** The character classes: 0 for a character in no keyword, else one per distinct character of the keywords. */
|
|
17
|
+
classOf;
|
|
18
|
+
width = 0;
|
|
19
|
+
/** The next state for each state and class, `state * width + class`. */
|
|
20
|
+
table;
|
|
21
|
+
hasOut;
|
|
22
|
+
/** An empty keyword occurs everywhere, and indices are the callers' (a keyword can't be dropped), so it is refused. */
|
|
23
|
+
constructor(keywords, caseSensitive = false) {
|
|
24
|
+
if (keywords.includes(""))
|
|
25
|
+
throw new RangeError("KeywordSet: empty keyword");
|
|
26
|
+
this.keywords = keywords;
|
|
27
|
+
this.fold = !caseSensitive;
|
|
28
|
+
keywords.forEach((k, i) => {
|
|
29
|
+
let s = 0;
|
|
30
|
+
for (let j = 0; j < k.length; j++) {
|
|
31
|
+
const c = this.fold ? foldAscii(k.charCodeAt(j)) : k.charCodeAt(j);
|
|
32
|
+
let t = this.next[s].get(c);
|
|
33
|
+
if (t === undefined) {
|
|
34
|
+
t = this.next.length;
|
|
35
|
+
this.next.push(new Map());
|
|
36
|
+
this.fail.push(0);
|
|
37
|
+
this.out.push([]);
|
|
38
|
+
this.next[s].set(c, t);
|
|
39
|
+
}
|
|
40
|
+
s = t;
|
|
41
|
+
}
|
|
42
|
+
this.out[s].push(i);
|
|
43
|
+
});
|
|
44
|
+
// Breadth-first failure links.
|
|
45
|
+
const queue = [];
|
|
46
|
+
for (const t of this.next[0].values())
|
|
47
|
+
queue.push(t);
|
|
48
|
+
for (let qi = 0; qi < queue.length; qi++) {
|
|
49
|
+
const s = queue[qi];
|
|
50
|
+
for (const [c, t] of this.next[s]) {
|
|
51
|
+
queue.push(t);
|
|
52
|
+
let f = this.fail[s];
|
|
53
|
+
while (f && !this.next[f].has(c))
|
|
54
|
+
f = this.fail[f];
|
|
55
|
+
// (`f` is shallower than `s`, so its child is never `t` itself.)
|
|
56
|
+
this.fail[t] = this.next[f].get(c) ?? 0;
|
|
57
|
+
this.out[t] = this.out[t].concat(this.out[this.fail[t]]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.tabulate(queue);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The automaton as a table, one read per character: for each state and each character that occurs in a keyword, the
|
|
64
|
+
* state after it (the failure links followed ahead of time); a character that occurs in none leads to the start. `order`
|
|
65
|
+
* is the states below the start, shallowest first, so a state's failure state is tabulated before it.
|
|
66
|
+
*/
|
|
67
|
+
tabulate(order) {
|
|
68
|
+
const classes = new Map();
|
|
69
|
+
for (const edges of this.next)
|
|
70
|
+
for (const c of edges.keys())
|
|
71
|
+
if (!classes.has(c))
|
|
72
|
+
classes.set(c, classes.size + 1);
|
|
73
|
+
this.width = classes.size + 1;
|
|
74
|
+
this.classOf = new Uint16Array(CHAR_CODES);
|
|
75
|
+
for (const [c, k] of classes)
|
|
76
|
+
this.classOf[c] = k;
|
|
77
|
+
// (Upper case reads as its lower case when folding: its class is the lower case letter's.)
|
|
78
|
+
if (this.fold)
|
|
79
|
+
for (let c = 65; c <= 90; c++)
|
|
80
|
+
this.classOf[c] = this.classOf[c + 32];
|
|
81
|
+
this.table = new Int32Array(this.next.length * this.width);
|
|
82
|
+
const fill = (s) => {
|
|
83
|
+
const base = s * this.width;
|
|
84
|
+
if (s)
|
|
85
|
+
this.table.copyWithin(base, this.fail[s] * this.width, this.fail[s] * this.width + this.width);
|
|
86
|
+
for (const [c, t] of this.next[s])
|
|
87
|
+
this.table[base + classes.get(c)] = t;
|
|
88
|
+
};
|
|
89
|
+
fill(0);
|
|
90
|
+
for (const s of order)
|
|
91
|
+
fill(s);
|
|
92
|
+
this.hasOut = Uint8Array.from(this.out, (o) => (o.length ? 1 : 0));
|
|
93
|
+
}
|
|
94
|
+
/** Indices of the keywords that occur in `text`. */
|
|
95
|
+
find(text) {
|
|
96
|
+
const found = new Set();
|
|
97
|
+
const { table, classOf, width, hasOut } = this;
|
|
98
|
+
for (let i = 0, s = 0; i < text.length; i++) {
|
|
99
|
+
s = table[s * width + classOf[text.charCodeAt(i)]];
|
|
100
|
+
if (hasOut[s])
|
|
101
|
+
for (const k of this.out[s])
|
|
102
|
+
found.add(k);
|
|
103
|
+
}
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
/** Every occurrence of every keyword: [start, keyword index]. */
|
|
107
|
+
*matches(text) {
|
|
108
|
+
const { table, classOf, width, hasOut } = this;
|
|
109
|
+
for (let i = 0, s = 0; i < text.length; i++) {
|
|
110
|
+
s = table[s * width + classOf[text.charCodeAt(i)]];
|
|
111
|
+
if (hasOut[s])
|
|
112
|
+
for (const k of this.out[s])
|
|
113
|
+
yield [i + 1 - this.keywords[k].length, k];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/base64.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Base64 and base64url (padded or not) decoding, without throwing. */
|
|
2
|
+
/** The bytes base64 or base64url encodes, or undefined if it isn't base64. */
|
|
3
|
+
export declare function base64Bytes(b64: string): Uint8Array | undefined;
|
|
4
|
+
/** The bytes base64 or base64url encodes, one character each (Latin-1), or undefined if it isn't base64. */
|
|
5
|
+
export declare function base64Binary(b64: string): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* The bytes base64 text decodes to, for reading what it holds: as base64Bytes, but text cut one character past a whole
|
|
8
|
+
* group (tool output cut short, `head -c`) has that character left off rather than failing whole.
|
|
9
|
+
*/
|
|
10
|
+
export declare function base64Prefix(b64: string): Uint8Array | undefined;
|
package/dist/base64.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Base64 and base64url (padded or not) decoding, without throwing. */
|
|
2
|
+
/** The bytes base64 or base64url encodes, or undefined if it isn't base64. */
|
|
3
|
+
export function base64Bytes(b64) {
|
|
4
|
+
const binary = base64Binary(b64);
|
|
5
|
+
return binary === undefined ? undefined : Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
6
|
+
}
|
|
7
|
+
/** `b64` without its trailing `=` padding (a backward scan: `/=+$/` retries from every `=` of a long run). */
|
|
8
|
+
function withoutPadding(b64) {
|
|
9
|
+
let end = b64.length;
|
|
10
|
+
while (end > 0 && b64[end - 1] === "=")
|
|
11
|
+
end--;
|
|
12
|
+
return b64.slice(0, end);
|
|
13
|
+
}
|
|
14
|
+
/** The bytes base64 or base64url encodes, one character each (Latin-1), or undefined if it isn't base64. */
|
|
15
|
+
export function base64Binary(b64) {
|
|
16
|
+
const t = withoutPadding(b64.replace(/-/g, "+").replace(/_/g, "/"));
|
|
17
|
+
// (atob accepts the rest: it ignores blanks, which the test refuses, and nonzero bits left over at the end.)
|
|
18
|
+
if (t.length % 4 === 1 || !/^[A-Za-z0-9+/]*$/.test(t))
|
|
19
|
+
return undefined;
|
|
20
|
+
return atob(t.padEnd(Math.ceil(t.length / 4) * 4, "="));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The bytes base64 text decodes to, for reading what it holds: as base64Bytes, but text cut one character past a whole
|
|
24
|
+
* group (tool output cut short, `head -c`) has that character left off rather than failing whole.
|
|
25
|
+
*/
|
|
26
|
+
export function base64Prefix(b64) {
|
|
27
|
+
const t = withoutPadding(b64);
|
|
28
|
+
return base64Bytes(t.length % 4 === 1 ? t.slice(0, -1) : t);
|
|
29
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** The BIP-39 English wordlist (bitcoin/bips, bip-0039/english.txt, MIT; sha256 2f5eed53…24dbda), in order. */
|
|
2
|
+
export declare const BIP39_WORDS = "abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual adapt add addict address adjust admit adult advance advice aerobic affair afford afraid again age agent agree ahead aim air airport aisle alarm album alcohol alert alien all alley allow almost alone alpha already also alter always amateur amazing among amount amused analyst anchor ancient anger angle angry animal ankle announce annual another answer antenna antique anxiety any apart apology appear apple approve april arch arctic area arena argue arm armed armor army around arrange arrest arrive arrow art artefact artist artwork ask aspect assault asset assist assume asthma athlete atom attack attend attitude attract auction audit august aunt author auto autumn average avocado avoid awake aware away awesome awful awkward axis baby bachelor bacon badge bag balance balcony ball bamboo banana banner bar barely bargain barrel base basic basket battle beach bean beauty because become beef before begin behave behind believe below belt bench benefit best betray better between beyond bicycle bid bike bind biology bird birth bitter black blade blame blanket blast bleak bless blind blood blossom blouse blue blur blush board boat body boil bomb bone bonus book boost border boring borrow boss bottom bounce box boy bracket brain brand brass brave bread breeze brick bridge brief bright bring brisk broccoli broken bronze broom brother brown brush bubble buddy budget buffalo build bulb bulk bullet bundle bunker burden burger burst bus business busy butter buyer buzz cabbage cabin cable cactus cage cake call calm camera camp can canal cancel candy cannon canoe canvas canyon capable capital captain car carbon card cargo carpet carry cart case cash casino castle casual cat catalog catch category cattle caught cause caution cave ceiling celery cement census century cereal certain chair chalk champion change chaos chapter charge chase chat cheap check cheese chef cherry chest chicken chief child chimney choice choose chronic chuckle chunk churn cigar cinnamon circle citizen city civil claim clap clarify claw clay clean clerk clever click client cliff climb clinic clip clock clog close cloth cloud clown club clump cluster clutch coach coast coconut code coffee coil coin collect color column combine come comfort comic common company concert conduct confirm congress connect consider control convince cook cool copper copy coral core corn correct cost cotton couch country couple course cousin cover coyote crack cradle craft cram crane crash crater crawl crazy cream credit creek crew cricket crime crisp critic crop cross crouch crowd crucial cruel cruise crumble crunch crush cry crystal cube culture cup cupboard curious current curtain curve cushion custom cute cycle dad damage damp dance danger daring dash daughter dawn day deal debate debris decade december decide decline decorate decrease deer defense define defy degree delay deliver demand demise denial dentist deny depart depend deposit depth deputy derive describe desert design desk despair destroy detail detect develop device devote diagram dial diamond diary dice diesel diet differ digital dignity dilemma dinner dinosaur direct dirt disagree discover disease dish dismiss disorder display distance divert divide divorce dizzy doctor document dog doll dolphin domain donate donkey donor door dose double dove draft dragon drama drastic draw dream dress drift drill drink drip drive drop drum dry duck dumb dune during dust dutch duty dwarf dynamic eager eagle early earn earth easily east easy echo ecology economy edge edit educate effort egg eight either elbow elder electric elegant element elephant elevator elite else embark embody embrace emerge emotion employ empower empty enable enact end endless endorse enemy energy enforce engage engine enhance enjoy enlist enough enrich enroll ensure enter entire entry envelope episode equal equip era erase erode erosion error erupt escape essay essence estate eternal ethics evidence evil evoke evolve exact example excess exchange excite exclude excuse execute exercise exhaust exhibit exile exist exit exotic expand expect expire explain expose express extend extra eye eyebrow fabric face faculty fade faint faith fall false fame family famous fan fancy fantasy farm fashion fat fatal father fatigue fault favorite feature february federal fee feed feel female fence festival fetch fever few fiber fiction field figure file film filter final find fine finger finish fire firm first fiscal fish fit fitness fix flag flame flash flat flavor flee flight flip float flock floor flower fluid flush fly foam focus fog foil fold follow food foot force forest forget fork fortune forum forward fossil foster found fox fragile frame frequent fresh friend fringe frog front frost frown frozen fruit fuel fun funny furnace fury future gadget gain galaxy gallery game gap garage garbage garden garlic garment gas gasp gate gather gauge gaze general genius genre gentle genuine gesture ghost giant gift giggle ginger giraffe girl give glad glance glare glass glide glimpse globe gloom glory glove glow glue goat goddess gold good goose gorilla gospel gossip govern gown grab grace grain grant grape grass gravity great green grid grief grit grocery group grow grunt guard guess guide guilt guitar gun gym habit hair half hammer hamster hand happy harbor hard harsh harvest hat have hawk hazard head health heart heavy hedgehog height hello helmet help hen hero hidden high hill hint hip hire history hobby hockey hold hole holiday hollow home honey hood hope horn horror horse hospital host hotel hour hover hub huge human humble humor hundred hungry hunt hurdle hurry hurt husband hybrid ice icon idea identify idle ignore ill illegal illness image imitate immense immune impact impose improve impulse inch include income increase index indicate indoor industry infant inflict inform inhale inherit initial inject injury inmate inner innocent input inquiry insane insect inside inspire install intact interest into invest invite involve iron island isolate issue item ivory jacket jaguar jar jazz jealous jeans jelly jewel job join joke journey joy judge juice jump jungle junior junk just kangaroo keen keep ketchup key kick kid kidney kind kingdom kiss kit kitchen kite kitten kiwi knee knife knock know lab label labor ladder lady lake lamp language laptop large later latin laugh laundry lava law lawn lawsuit layer lazy leader leaf learn leave lecture left leg legal legend leisure lemon lend length lens leopard lesson letter level liar liberty library license life lift light like limb limit link lion liquid list little live lizard load loan lobster local lock logic lonely long loop lottery loud lounge love loyal lucky luggage lumber lunar lunch luxury lyrics machine mad magic magnet maid mail main major make mammal man manage mandate mango mansion manual maple marble march margin marine market marriage mask mass master match material math matrix matter maximum maze meadow mean measure meat mechanic medal media melody melt member memory mention menu mercy merge merit merry mesh message metal method middle midnight milk million mimic mind minimum minor minute miracle mirror misery miss mistake mix mixed mixture mobile model modify mom moment monitor monkey monster month moon moral more morning mosquito mother motion motor mountain mouse move movie much muffin mule multiply muscle museum mushroom music must mutual myself mystery myth naive name napkin narrow nasty nation nature near neck need negative neglect neither nephew nerve nest net network neutral never news next nice night noble noise nominee noodle normal north nose notable note nothing notice novel now nuclear number nurse nut oak obey object oblige obscure observe obtain obvious occur ocean october odor off offer office often oil okay old olive olympic omit once one onion online only open opera opinion oppose option orange orbit orchard order ordinary organ orient original orphan ostrich other outdoor outer output outside oval oven over own owner oxygen oyster ozone pact paddle page pair palace palm panda panel panic panther paper parade parent park parrot party pass patch path patient patrol pattern pause pave payment peace peanut pear peasant pelican pen penalty pencil people pepper perfect permit person pet phone photo phrase physical piano picnic picture piece pig pigeon pill pilot pink pioneer pipe pistol pitch pizza place planet plastic plate play please pledge pluck plug plunge poem poet point polar pole police pond pony pool popular portion position possible post potato pottery poverty powder power practice praise predict prefer prepare present pretty prevent price pride primary print priority prison private prize problem process produce profit program project promote proof property prosper protect proud provide public pudding pull pulp pulse pumpkin punch pupil puppy purchase purity purpose purse push put puzzle pyramid quality quantum quarter question quick quit quiz quote rabbit raccoon race rack radar radio rail rain raise rally ramp ranch random range rapid rare rate rather raven raw razor ready real reason rebel rebuild recall receive recipe record recycle reduce reflect reform refuse region regret regular reject relax release relief rely remain remember remind remove render renew rent reopen repair repeat replace report require rescue resemble resist resource response result retire retreat return reunion reveal review reward rhythm rib ribbon rice rich ride ridge rifle right rigid ring riot ripple risk ritual rival river road roast robot robust rocket romance roof rookie room rose rotate rough round route royal rubber rude rug rule run runway rural sad saddle sadness safe sail salad salmon salon salt salute same sample sand satisfy satoshi sauce sausage save say scale scan scare scatter scene scheme school science scissors scorpion scout scrap screen script scrub sea search season seat second secret section security seed seek segment select sell seminar senior sense sentence series service session settle setup seven shadow shaft shallow share shed shell sheriff shield shift shine ship shiver shock shoe shoot shop short shoulder shove shrimp shrug shuffle shy sibling sick side siege sight sign silent silk silly silver similar simple since sing siren sister situate six size skate sketch ski skill skin skirt skull slab slam sleep slender slice slide slight slim slogan slot slow slush small smart smile smoke smooth snack snake snap sniff snow soap soccer social sock soda soft solar soldier solid solution solve someone song soon sorry sort soul sound soup source south space spare spatial spawn speak special speed spell spend sphere spice spider spike spin spirit split spoil sponsor spoon sport spot spray spread spring spy square squeeze squirrel stable stadium staff stage stairs stamp stand start state stay steak steel stem step stereo stick still sting stock stomach stone stool story stove strategy street strike strong struggle student stuff stumble style subject submit subway success such sudden suffer sugar suggest suit summer sun sunny sunset super supply supreme sure surface surge surprise surround survey suspect sustain swallow swamp swap swarm swear sweet swift swim swing switch sword symbol symptom syrup system table tackle tag tail talent talk tank tape target task taste tattoo taxi teach team tell ten tenant tennis tent term test text thank that theme then theory there they thing this thought three thrive throw thumb thunder ticket tide tiger tilt timber time tiny tip tired tissue title toast tobacco today toddler toe together toilet token tomato tomorrow tone tongue tonight tool tooth top topic topple torch tornado tortoise toss total tourist toward tower town toy track trade traffic tragic train transfer trap trash travel tray treat tree trend trial tribe trick trigger trim trip trophy trouble truck true truly trumpet trust truth try tube tuition tumble tuna tunnel turkey turn turtle twelve twenty twice twin twist two type typical ugly umbrella unable unaware uncle uncover under undo unfair unfold unhappy uniform unique unit universe unknown unlock until unusual unveil update upgrade uphold upon upper upset urban urge usage use used useful useless usual utility vacant vacuum vague valid valley valve van vanish vapor various vast vault vehicle velvet vendor venture venue verb verify version very vessel veteran viable vibrant vicious victory video view village vintage violin virtual virus visa visit visual vital vivid vocal voice void volcano volume vote voyage wage wagon wait walk wall walnut want warfare warm warrior wash wasp waste water wave way wealth weapon wear weasel weather web wedding weekend weird welcome west wet whale what wheat wheel when where whip whisper wide width wife wild will win window wine wing wink winner winter wire wisdom wise wish witness wolf woman wonder wood wool word work world worry worth wrap wreck wrestle wrist write wrong yard year yellow you young youth zebra zero zone zoo";
|