@redaktyn/cli 0.0.0-stage → 1.1.0
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 +561 -2
- package/package.json +44 -4
- package/redaktyn-fingerprint.mjs +814 -0
- package/redaktyn-import.mjs +1652 -0
- package/redaktyn-scan.mjs +565 -0
- package/redaktyn-verify.mjs +557 -0
package/README.md
CHANGED
|
@@ -1,3 +1,562 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Redaktyn Fingerprint CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate HMAC-SHA256 secret fingerprints **locally on your machine** — nothing ever leaves your computer.
|
|
4
|
+
|
|
5
|
+
Use this when you want to register a fingerprint for a secret in Redaktyn **without pasting its raw value** into any browser tab or dashboard.
|
|
6
|
+
|
|
7
|
+
> Looking to **audit** Redaktyn rather than use it? See [`redaktyn-verify.mjs`](#redaktyn-verify--audit-the-claims)
|
|
8
|
+
> below, or the full write-up in [docs/05-security/VERIFIABLE-PROOF.md](../docs/05-security/VERIFIABLE-PROOF.md).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why this exists
|
|
13
|
+
|
|
14
|
+
The Redaktyn **extension enrolment page** (`extension/secrets.html`) computes fingerprints in-browser with the same zero-knowledge guarantee. The dashboard opens that page when you add a secret. This CLI is for when you prefer to:
|
|
15
|
+
|
|
16
|
+
- Generate fingerprints on an air-gapped machine
|
|
17
|
+
- Automate registration in CI/CD pipelines
|
|
18
|
+
- Audit the exact cryptography before trusting it
|
|
19
|
+
- Never open a browser tab with the raw secret
|
|
20
|
+
|
|
21
|
+
This CLI is for you.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- **Node.js ≥ 18**
|
|
28
|
+
- **`redaktyn-fingerprint.mjs`** — uses only Node.js built-in `crypto` (no npm packages required to run that script)
|
|
29
|
+
- **`redaktyn-import.mjs`** — imports the digest helpers from `redaktyn-fingerprint.mjs` and otherwise uses built-ins only, so it also needs no npm install. Keep the two files together.
|
|
30
|
+
- **`redaktyn-scan.mjs`** — loads `@redaktyn/shared` from the monorepo. From the repo root run `npm install` and ensure shared is built, e.g. `npm run build -w @redaktyn/shared`, before using the scanner
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
### Interactive wizard (recommended for first-timers)
|
|
37
|
+
|
|
38
|
+
From the **`cli`** directory (or invoke by path):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd cli
|
|
42
|
+
node redaktyn-fingerprint.mjs
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The wizard will guide you through:
|
|
46
|
+
1. Entering your Org ID (from Dashboard → Settings)
|
|
47
|
+
2. Entering your org passphrase (masked input)
|
|
48
|
+
3. Adding one or more secrets (label + value, both masked)
|
|
49
|
+
4. Showing you the exact crypto steps for transparency
|
|
50
|
+
5. Printing copy-ready JSON to paste into the dashboard
|
|
51
|
+
|
|
52
|
+
### Non-interactive (CI / scripting)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Preferred: passphrase via env var (won't appear in shell history)
|
|
56
|
+
export REDAKTYN_PASSPHRASE="your-org-passphrase"
|
|
57
|
+
|
|
58
|
+
cd cli && node redaktyn-fingerprint.mjs \
|
|
59
|
+
--org-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
|
|
60
|
+
--label "AWS_SECRET_ACCESS_KEY" \
|
|
61
|
+
--secret "AKIAIOSFODNN7EXAMPLE..."
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Batch mode (multiple secrets at once)
|
|
65
|
+
|
|
66
|
+
Create a plain-text file `secrets.txt`:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
# Lines starting with # are ignored
|
|
70
|
+
AWS_ACCESS_KEY:AKIAIOSFODNN7EXAMPLE
|
|
71
|
+
DATABASE_PASSWORD:s3cr3tPassw0rd!
|
|
72
|
+
STRIPE_SECRET_KEY:sk_live_abc123xyz
|
|
73
|
+
GITHUB_TOKEN:ghp_xxxxxxxxxxxx
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
export REDAKTYN_PASSPHRASE="your-org-passphrase"
|
|
80
|
+
|
|
81
|
+
cd cli && node redaktyn-fingerprint.mjs \
|
|
82
|
+
--org-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
|
|
83
|
+
--batch secrets.txt
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Outputs a JSON array — register each digest on Dashboard → **Secrets** (expand **"I already have a fingerprint from the CLI"** and paste, or POST `{ label, hmacHex, length }` via the API). Dashboard bulk import is removed; use this CLI batch mode plus the collapsible import panel or `POST /api/secrets`.
|
|
87
|
+
|
|
88
|
+
### JSON output (for piping / automation)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
node cli/redaktyn-fingerprint.mjs --org-id ... --label ... --secret ... --json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Output:
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"label": "AWS_SECRET_ACCESS_KEY",
|
|
98
|
+
"hmacHex": "64-lowercase-hex-chars...",
|
|
99
|
+
"length": 20
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## All options
|
|
106
|
+
|
|
107
|
+
| Flag | Description |
|
|
108
|
+
|------|-------------|
|
|
109
|
+
| `--org-id <uuid>` | Your organization UUID (Dashboard → Settings) |
|
|
110
|
+
| `--label <str>` | Human-readable name, e.g. `AWS_SECRET_KEY` |
|
|
111
|
+
| `--secret <str>` | The raw secret value to fingerprint |
|
|
112
|
+
| `--passphrase <str>` | Org passphrase (**prefer** `REDAKTYN_PASSPHRASE` env var) |
|
|
113
|
+
| `--batch <file>` | Path to a `LABEL:value` file for bulk generation |
|
|
114
|
+
| `--json` | Output pure JSON, no colors or headers |
|
|
115
|
+
| `--no-color` | Disable ANSI colors |
|
|
116
|
+
| `--help` | Show full help |
|
|
117
|
+
|
|
118
|
+
**Environment variables:**
|
|
119
|
+
|
|
120
|
+
| Variable | Description |
|
|
121
|
+
|----------|-------------|
|
|
122
|
+
| `REDAKTYN_PASSPHRASE` | Org passphrase — preferred over `--passphrase` flag |
|
|
123
|
+
| `NO_COLOR` | Disable ANSI colors (standard convention) |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## How the cryptography works
|
|
128
|
+
|
|
129
|
+
This tool implements the same algorithm as the Redaktyn extension enrolment page and browser extension detection path, so the digests it produces are interchangeable with theirs.
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Step 1 — Key derivation
|
|
133
|
+
Input: passphrase (UTF-8) + orgId (UTF-8)
|
|
134
|
+
Salt: SHA-256(orgId) ← org-scoped, no secret leakage
|
|
135
|
+
KDF: PBKDF2-SHA256, 600,000 iterations, 256-bit output
|
|
136
|
+
(KDF version pbkdf2-sha256-600k-v2, matching @redaktyn/shared)
|
|
137
|
+
Output: 32-byte HMAC key
|
|
138
|
+
|
|
139
|
+
Step 2 — Secret canonicalization
|
|
140
|
+
• Unicode NFC normalization
|
|
141
|
+
• CRLF / CR → LF
|
|
142
|
+
• Strip invisible / zero-width characters used in bypass tricks
|
|
143
|
+
• Trim leading & trailing whitespace
|
|
144
|
+
• UUID strings: lowercase (case-insensitive matching)
|
|
145
|
+
|
|
146
|
+
Step 3 — Fingerprint (three variants, all enrolled)
|
|
147
|
+
hmacHex HMAC-SHA256(canonical secret, key)
|
|
148
|
+
hmacHexStripped same, after removing every Unicode whitespace character
|
|
149
|
+
hmacHexLower same as stripped, then lowercased
|
|
150
|
+
|
|
151
|
+
Output: 64-character lowercase hex strings ← these are stored
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The two extra variants exist because a secret is trivially evaded otherwise:
|
|
155
|
+
`Amit@123` enrolled as a single digest is missed the moment someone types
|
|
156
|
+
`A mit@123` or `aMIt@123`. Enrolling only `hmacHex` leaves those gaps open.
|
|
157
|
+
|
|
158
|
+
**What the server stores:** only the labels, the three digests and their lengths — never the raw secret or the HMAC key.
|
|
159
|
+
|
|
160
|
+
You can verify this tool against extension-enrolled secrets by registering the same secret both ways and confirming the digests match. The pipeline itself is published as [`@redaktyn/shared`](../shared/README.md) if you would rather diff the algorithm than trust the description.
|
|
161
|
+
|
|
162
|
+
**LLM providers:** Neither `redaktyn-fingerprint.mjs` nor `redaktyn-scan.mjs` calls an LLM — all digest work is local crypto. Redaktyn deployments may optionally use providers such as **AWS Bedrock** as a fallback LLM on the server for separate AI-assisted features.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Finding your Org ID
|
|
167
|
+
|
|
168
|
+
1. Log in to the Redaktyn dashboard
|
|
169
|
+
2. Go to **Settings** (sidebar, bottom)
|
|
170
|
+
3. Scroll to **Organization Details**
|
|
171
|
+
4. Copy the UUID shown under "Organization ID"
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Importing the fingerprint into the dashboard
|
|
176
|
+
|
|
177
|
+
After generating the JSON:
|
|
178
|
+
|
|
179
|
+
1. Dashboard → **Secrets** page
|
|
180
|
+
2. Expand **"I already have a fingerprint from the CLI"** (collapsible panel — not a separate tab)
|
|
181
|
+
3. Paste the JSON output into the smart paste zone
|
|
182
|
+
4. Click **Register fingerprint**
|
|
183
|
+
5. Extensions pick up the new fingerprint on next sync (within ~5 minutes)
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Security notes
|
|
188
|
+
|
|
189
|
+
- **Never pass secrets via `--secret` flag in production** — the value may appear in process listings. Prefer interactive mode or `echo "value" | ...` piped from a password manager.
|
|
190
|
+
- `REDAKTYN_PASSPHRASE` via env is safer than `--passphrase` (not in shell history by default, but visible in `/proc`).
|
|
191
|
+
- For maximum safety: use the interactive wizard on a dedicated machine, then delete shell history.
|
|
192
|
+
- The fingerprint CLI is plain JavaScript. Read it — there are no obfuscated sections.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## `redaktyn-import` — bulk enrollment from files and vaults
|
|
197
|
+
|
|
198
|
+
Enrollment used to mean typing every secret into the dashboard by hand, so coverage was
|
|
199
|
+
whatever the admin remembered. The values are already sitting somewhere — a dotenv file,
|
|
200
|
+
Vault, AWS Secrets Manager, Doppler, 1Password — and every one of those can already print
|
|
201
|
+
JSON. `redaktyn-import` reads that, computes the digests **on your machine**, and sends
|
|
202
|
+
only digests.
|
|
203
|
+
|
|
204
|
+
Like `redaktyn-fingerprint`, it needs **no npm install** — Node 18+ and nothing else.
|
|
205
|
+
|
|
206
|
+
### Quick start
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
export REDAKTYN_SERVER_URL="https://your-api.example.com"
|
|
210
|
+
export REDAKTYN_PASSPHRASE="your-org-passphrase"
|
|
211
|
+
export REDAKTYN_TOKEN="eyJ…" # OWNER or ADMIN login token
|
|
212
|
+
|
|
213
|
+
# 1. See what it would do. This is the default; nothing is written.
|
|
214
|
+
node cli/redaktyn-import.mjs --env .env.production --source prod
|
|
215
|
+
|
|
216
|
+
# 2. Happy with the plan? Enroll it.
|
|
217
|
+
node cli/redaktyn-import.mjs --env .env.production --source prod --apply
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The dry run prints one line per key — the key name, the value's length, and the decision:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
KEY LEN ACTION WHY
|
|
224
|
+
AWS_SECRET_ACCESS_KEY 35 CREATE new
|
|
225
|
+
DATABASE_URL 52 CREATE new
|
|
226
|
+
SLACK_WEBHOOK 77 CREATE new
|
|
227
|
+
API_BASE_URL 26 rejected URL with no embedded credential or token
|
|
228
|
+
NODE_ENV 10 rejected only 10 chars (minimum 12)
|
|
229
|
+
PORT 4 rejected only 4 chars (minimum 12)
|
|
230
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
231
|
+
3 to create · 3 filtered out
|
|
232
|
+
Scan budget: every paste up to the extension's 5,242,880-char hard limit is scanned.
|
|
233
|
+
|
|
234
|
+
Dry run — nothing was sent. Re-run with --apply to register.
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Values are never printed** — not truncated, not masked, not even for a rejected key. A
|
|
238
|
+
preview is the thing that gets screenshotted or piped into a CI log.
|
|
239
|
+
|
|
240
|
+
### Vault recipes
|
|
241
|
+
|
|
242
|
+
There are no vendor SDKs here and nothing to configure. Every vault CLI can already emit
|
|
243
|
+
JSON, so `--stdin` covers all of them today:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# HashiCorp Vault (KV v1 and v2 envelopes are both unwrapped)
|
|
247
|
+
vault kv get -format=json secret/prod | node cli/redaktyn-import.mjs --stdin --source vault
|
|
248
|
+
|
|
249
|
+
# AWS Secrets Manager
|
|
250
|
+
aws secretsmanager get-secret-value --secret-id prod \
|
|
251
|
+
--query SecretString --output text | node cli/redaktyn-import.mjs --stdin --source aws
|
|
252
|
+
|
|
253
|
+
# …or pipe the whole envelope; SecretString is unwrapped for you
|
|
254
|
+
aws secretsmanager get-secret-value --secret-id prod \
|
|
255
|
+
| node cli/redaktyn-import.mjs --stdin --source aws
|
|
256
|
+
|
|
257
|
+
# Doppler
|
|
258
|
+
doppler secrets download --no-file --format json \
|
|
259
|
+
| node cli/redaktyn-import.mjs --stdin --source doppler
|
|
260
|
+
|
|
261
|
+
# 1Password
|
|
262
|
+
op item get prod --format json | node cli/redaktyn-import.mjs --stdin --source 1password
|
|
263
|
+
|
|
264
|
+
# GitHub Actions secrets cannot be read back by design — export from wherever they
|
|
265
|
+
# were set, or enroll those with redaktyn-fingerprint.
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Plus three file modes: `--env <file>` (dotenv, including `export ` prefixes, quotes,
|
|
269
|
+
comments and multi-line values such as PEM keys), `--json <file>`, and `--yaml <file>`.
|
|
270
|
+
YAML support is a **flat `key: value` subset only** — anything indented or listed is
|
|
271
|
+
refused rather than half-parsed, because a nested document read as flat enrolls digests
|
|
272
|
+
of values that are not the secrets.
|
|
273
|
+
|
|
274
|
+
### Why this stays zero-knowledge
|
|
275
|
+
|
|
276
|
+
The CLI runs on the admin's machine, so the vault credential and the plaintext both stay
|
|
277
|
+
there. Reading vaults from the browser instead would be worse on every axis: AWS wants
|
|
278
|
+
SigV4 and sends no CORS headers, Doppler's API cannot be called from a page, and the
|
|
279
|
+
secret would have to pass through a web origin that anything injected into it can script.
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
On your machine │ Sent to the server
|
|
283
|
+
─────────────────────────────────────────┼──────────────────────────────
|
|
284
|
+
vault / .env / Doppler output │
|
|
285
|
+
↓ │
|
|
286
|
+
canonicalize (NFC, CRLF→LF, trim, …) │
|
|
287
|
+
↓ │
|
|
288
|
+
PBKDF2-SHA256(passphrase, orgId) → key │
|
|
289
|
+
↓ │
|
|
290
|
+
HMAC-SHA256 × 3 variants │ { label, hmacHex, length,
|
|
291
|
+
+ keyed scan prefilter │ hmacHexStripped, lengthStripped,
|
|
292
|
+
│ hmacHexLower, lengthLower,
|
|
293
|
+
│ scanPrefilter,
|
|
294
|
+
│ tags: ["import:prod"] }
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The digest pipeline is imported from `redaktyn-fingerprint.mjs` rather than copied, so
|
|
298
|
+
there is exactly one canonical form in this directory. A guard test enforces that.
|
|
299
|
+
|
|
300
|
+
### What it decides, and what it refuses to decide
|
|
301
|
+
|
|
302
|
+
Identity is the **key name as the label**, scoped to the tag `import:<source>`. Two
|
|
303
|
+
imports from two vaults therefore never fight over the same row.
|
|
304
|
+
|
|
305
|
+
| Situation | Action |
|
|
306
|
+
|-----------|--------|
|
|
307
|
+
| Key is not enrolled yet | **CREATE** — `POST /api/secrets` with the `import:<source>` tag |
|
|
308
|
+
| Key is enrolled from this source, value changed | **ROTATE** — `PUT /api/secrets/:id/digest`, same row, same label, same detection history |
|
|
309
|
+
| Key is enrolled from this source, value unchanged | skip — or **ROTATE** once if the stored row lacks a bypass variant or the scan prefilter |
|
|
310
|
+
| This value is already enrolled under another label | skip, and says which label |
|
|
311
|
+
| A secret with this label exists **outside** this source | **CONFLICT** — reported, left alone |
|
|
312
|
+
| A secret from this source is missing from the input | reported, **never removed** |
|
|
313
|
+
|
|
314
|
+
The last two are deliberate. Rotating a same-label secret would silently take over one
|
|
315
|
+
that was enrolled by hand, and a vault read that fails or returns a subset must not be
|
|
316
|
+
able to cause org-wide loss of protection.
|
|
317
|
+
|
|
318
|
+
### The junk filter
|
|
319
|
+
|
|
320
|
+
Vaults contain `PORT=8080`, `NODE_ENV=production` and `DEBUG=true` alongside real secrets.
|
|
321
|
+
Enrolling `production` would end zero false positives in a single import — every paste
|
|
322
|
+
containing the word would be blocked. So values are filtered by default:
|
|
323
|
+
|
|
324
|
+
- shorter than 12 characters (`--min-length` to change)
|
|
325
|
+
- all digits, booleans, version numbers, email addresses, filesystem paths
|
|
326
|
+
- placeholders such as `changeme`
|
|
327
|
+
- fewer than 5 distinct characters, or very low character diversity
|
|
328
|
+
- URLs with **no** embedded credential and **no** token-shaped path segment
|
|
329
|
+
|
|
330
|
+
That last rule keeps the URLs that are themselves secrets: a Postgres DSN with an inline
|
|
331
|
+
password and a Slack webhook are both enrolled, while `https://api.example.com/v1` is not.
|
|
332
|
+
|
|
333
|
+
The filter advises; you decide. `--allow-weak` enrolls the rejects anyway and marks each
|
|
334
|
+
one as weak in the plan, so it cannot be used without seeing what it did.
|
|
335
|
+
|
|
336
|
+
### The scan budget
|
|
337
|
+
|
|
338
|
+
Detection slides one pass per distinct secret length, across three normalization passes,
|
|
339
|
+
and past `MAX_SCAN_WINDOWS` the extension **refuses** to scan rather than hang, which is
|
|
340
|
+
a hard block in sensitive contexts.
|
|
341
|
+
|
|
342
|
+
Every secret this command enrols also carries a **scan prefilter**: three keyed 16-bit
|
|
343
|
+
fingerprints that let the extension skip windows that cannot be that secret before
|
|
344
|
+
signing them. That is what makes a bulk import cheap. Each length still costs a pass, but
|
|
345
|
+
a pass over a fingerprinted length costs about 1/256 of what it used to. Largest paste
|
|
346
|
+
still scanned, with every secret a different length:
|
|
347
|
+
|
|
348
|
+
| Enrolled secrets | Window lengths | With prefilter (this CLI) | Without (enrolled before 2026-10-07) |
|
|
349
|
+
|------------------|----------------|---------------------------|--------------------------------------|
|
|
350
|
+
| 1 | 3 | 5,242,880 chars (the hard limit) | ~500,000 chars |
|
|
351
|
+
| 10 | 30 | 5,242,880 chars (the hard limit) | ~50,000 chars |
|
|
352
|
+
| 50 | 150 | ~2,550,000 chars | ~10,000 chars |
|
|
353
|
+
| 100 | 300 | ~1,275,000 chars | ~5,068 chars |
|
|
354
|
+
| 200 | 600 | ~637,000 chars | ~2,618 chars |
|
|
355
|
+
|
|
356
|
+
The figure printed is a **floor**: the size any text of any shape is guaranteed to be
|
|
357
|
+
scanned at, computed with the same arithmetic the extension's admission check uses and
|
|
358
|
+
pinned to it by tests. Ordinary prose usually scans well past it.
|
|
359
|
+
|
|
360
|
+
A secret enrolled before the prefilter existed has none, and every length it shares keeps
|
|
361
|
+
the old cost. The plan accounts for that. Secrets **in this import** that lack one are
|
|
362
|
+
re-sent once to add it (`rotate — value unchanged; adding missing variants`). Any left
|
|
363
|
+
outside the import are counted, with what re-enrolling them would raise the ceiling to:
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
Scan budget: 15 window lengths, pastes up to ~248,570 chars.
|
|
367
|
+
2 enrolled secrets have no scan prefilter (enrolled before 2026-10-07 or by an older client),
|
|
368
|
+
so every paste is checked against them the slow way. They are outside this import; re-enrolling them
|
|
369
|
+
from the dashboard, or importing them, raises this to ~5,242,880 chars.
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Against a server that predates the prefilter**, the CLI notices (its rows carry no
|
|
373
|
+
`scanPrefilter` field, or it refuses the field on the first write) and registers without
|
|
374
|
+
it, so the import still succeeds. Re-run it after the server updates and it adds the
|
|
375
|
+
prefilter to each secret. `--report-json` includes `scanBudget.legacySecrets`,
|
|
376
|
+
`maxPasteCharsIfReenrolled`, `atCharacterCap` and `serverStoresScanPrefilter`.
|
|
377
|
+
|
|
378
|
+
### Flags
|
|
379
|
+
|
|
380
|
+
| Flag | Description |
|
|
381
|
+
|------|-------------|
|
|
382
|
+
| `--env <file>` | dotenv file |
|
|
383
|
+
| `--json <file>` | JSON object of secrets |
|
|
384
|
+
| `--yaml <file>` | flat `key: value` YAML only |
|
|
385
|
+
| `--stdin` | JSON on stdin — how vault CLIs are consumed |
|
|
386
|
+
| `--source <name>` | Provenance tag, stored as `import:<name>` (default: file name, or `stdin`) |
|
|
387
|
+
| `--apply` | Actually enroll. Without it, dry run. |
|
|
388
|
+
| `--yes` | Skip the confirmation prompt (CI) |
|
|
389
|
+
| `--min-length <n>` | Minimum value length to accept (default 12) |
|
|
390
|
+
| `--allow-weak` | Enroll values the filter rejected |
|
|
391
|
+
| `--only <key\|glob>` | Only these keys (repeatable) |
|
|
392
|
+
| `--exclude <key\|glob>` | Skip these keys (repeatable) |
|
|
393
|
+
| `--server <url>` | Override `REDAKTYN_SERVER_URL` |
|
|
394
|
+
| `--org-id <uuid>` | Override org id (must match the token) |
|
|
395
|
+
| `--project <name\|id>` | Enrol into this project: hashed under the project's own key (HKDF of the org key and the project id) and compared only with that project's secrets. Without it, secrets go to the organisation's default project |
|
|
396
|
+
| `--report-json` | Machine-readable plan, no table |
|
|
397
|
+
| `--no-color` | Disable ANSI colors |
|
|
398
|
+
|
|
399
|
+
**Environment:** `REDAKTYN_SERVER_URL`, `REDAKTYN_PASSPHRASE`, `REDAKTYN_TOKEN`, and
|
|
400
|
+
optionally `REDAKTYN_ORG_ID`.
|
|
401
|
+
|
|
402
|
+
`REDAKTYN_TOKEN` must be an **OWNER or ADMIN login token**. `REDAKTYN_DEVICE_TOKEN` is
|
|
403
|
+
deliberately not accepted: an extension token can read the secret list, so a shell set up
|
|
404
|
+
for `redaktyn-scan` would produce a dry run that looks perfect and then fail on the first
|
|
405
|
+
write, after you had approved the plan. It is rejected up front instead.
|
|
406
|
+
|
|
407
|
+
**Getting that token.** There is no API-key screen; the login endpoint issues it:
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
export REDAKTYN_TOKEN=$(curl -s -X POST "$REDAKTYN_SERVER_URL/api/auth/login" \
|
|
411
|
+
-H 'Content-Type: application/json' \
|
|
412
|
+
-d '{"email":"you@example.com","password":"your-password"}' | jq -r .token)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Or copy it out of a logged-in dashboard: DevTools → Application → Local Storage →
|
|
416
|
+
`vaultx_token`. Tokens expire, so for scheduled CI runs mint one per run with the call
|
|
417
|
+
above rather than pasting a long-lived string into a secret store.
|
|
418
|
+
|
|
419
|
+
**Exit codes:** `0` plan printed or every write succeeded · `1` one or more writes failed ·
|
|
420
|
+
`2` usage, auth or parse error.
|
|
421
|
+
|
|
422
|
+
### CI example
|
|
423
|
+
|
|
424
|
+
```yaml
|
|
425
|
+
- name: Sync enrolled secrets from Doppler
|
|
426
|
+
env:
|
|
427
|
+
REDAKTYN_SERVER_URL: ${{ secrets.REDAKTYN_SERVER_URL }}
|
|
428
|
+
REDAKTYN_PASSPHRASE: ${{ secrets.REDAKTYN_PASSPHRASE }}
|
|
429
|
+
REDAKTYN_TOKEN: ${{ secrets.REDAKTYN_ADMIN_TOKEN }}
|
|
430
|
+
run: |
|
|
431
|
+
doppler secrets download --no-file --format json \
|
|
432
|
+
| node cli/redaktyn-import.mjs --stdin --source doppler --apply --yes
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## `redaktyn-scan` — Pre-Commit & CI/CD Scanner (Zero-Knowledge)
|
|
438
|
+
|
|
439
|
+
Protect your codebase **before** secrets reach an AI tool. `redaktyn-scan` runs the same
|
|
440
|
+
zero-knowledge HMAC detection as the browser extension, but inside your git workflow and CI pipelines.
|
|
441
|
+
|
|
442
|
+
### Quick start
|
|
443
|
+
|
|
444
|
+
Ensure shared is installed and built (`npm install` at repo root, then `npm run build -w @redaktyn/shared` if needed).
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
# API base URL — for an EC2 deployment with no custom domain, use the public IP:
|
|
448
|
+
# export REDAKTYN_SERVER_URL="http://<EC2-PUBLIC-IP>"
|
|
449
|
+
export REDAKTYN_SERVER_URL="https://your-api.example.com"
|
|
450
|
+
export REDAKTYN_PASSPHRASE="your-org-passphrase"
|
|
451
|
+
export REDAKTYN_DEVICE_TOKEN="eyJ…" # extension JWT from popup → Connect, or admin JWT for CI
|
|
452
|
+
|
|
453
|
+
# Scan staged changes before committing
|
|
454
|
+
node cli/redaktyn-scan.mjs --staged
|
|
455
|
+
|
|
456
|
+
# Install permanent git hook (runs automatically on every commit)
|
|
457
|
+
node cli/redaktyn-scan.mjs install-hooks
|
|
458
|
+
|
|
459
|
+
# Remove the hook
|
|
460
|
+
node cli/redaktyn-scan.mjs uninstall-hooks
|
|
461
|
+
|
|
462
|
+
# Scan a single file
|
|
463
|
+
node cli/redaktyn-scan.mjs --file ./src/config.ts
|
|
464
|
+
|
|
465
|
+
# Scan all text files under a directory
|
|
466
|
+
node cli/redaktyn-scan.mjs --dir ./src --exclude "*.min.js"
|
|
467
|
+
|
|
468
|
+
# Check connection and enrolled secret count
|
|
469
|
+
node cli/redaktyn-scan.mjs status --json
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Or from repo root via workspace bin: `npm run redaktyn-scan -- --staged` (passes flags after `--`).
|
|
473
|
+
|
|
474
|
+
### CI/CD integration (GitHub Actions example)
|
|
475
|
+
|
|
476
|
+
```yaml
|
|
477
|
+
- name: Redaktyn secret scan
|
|
478
|
+
env:
|
|
479
|
+
REDAKTYN_SERVER_URL: ${{ secrets.REDAKTYN_SERVER_URL }}
|
|
480
|
+
REDAKTYN_PASSPHRASE: ${{ secrets.REDAKTYN_PASSPHRASE }}
|
|
481
|
+
REDAKTYN_TOKEN: ${{ secrets.REDAKTYN_TOKEN }}
|
|
482
|
+
run: |
|
|
483
|
+
node cli/redaktyn-scan.mjs --staged --json
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Exit code `1` when enrolled digests are found in the staged diff — blocks the commit or fails the CI job.
|
|
487
|
+
|
|
488
|
+
### How it works (zero-knowledge)
|
|
489
|
+
|
|
490
|
+
1. Derives an HMAC key from your passphrase (PBKDF2-SHA256, 600K iterations, org ID as salt) by calling `@redaktyn/shared` directly, so its digests always match extension-enrolled secrets.
|
|
491
|
+
2. Slides windows of each enrolled secret's length over added lines from the git diff.
|
|
492
|
+
3. Sends **only 64-char hex HMAC digests** to `POST /api/cli/scan` — raw content never leaves your machine.
|
|
493
|
+
4. Server checks digests against enrolled rows and returns label matches.
|
|
494
|
+
|
|
495
|
+
**Scan flags:**
|
|
496
|
+
|
|
497
|
+
| Flag | Description |
|
|
498
|
+
|------|-------------|
|
|
499
|
+
| `--staged` | Scan git staged additions (`git diff --cached`) |
|
|
500
|
+
| `--file <path>` | Scan a single file |
|
|
501
|
+
| `--dir <path>` | Scan all text files under directory |
|
|
502
|
+
| `--server <url>` | Override `REDAKTYN_SERVER_URL` |
|
|
503
|
+
| `--org-id <uuid>` | Override org id (must match JWT) |
|
|
504
|
+
| `--json` | JSON output (machine-readable, CI-friendly) |
|
|
505
|
+
| `--verbose` | Extra diagnostic output |
|
|
506
|
+
| `--exclude <pat>` | Skip paths containing pattern (repeatable) |
|
|
507
|
+
| `install-hooks` | Write `.git/hooks/pre-commit` |
|
|
508
|
+
| `uninstall-hooks` | Remove Redaktyn pre-commit hook |
|
|
509
|
+
| `status` | Show connection + enrolled secret count |
|
|
510
|
+
|
|
511
|
+
**Note:** scans use the primary normalization path. Secrets enrolled with **only** a whitespace-collapsed
|
|
512
|
+
digest may require the browser extension for full bypass-variant parity.
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## `redaktyn-verify` — audit the claims
|
|
517
|
+
|
|
518
|
+
Where the other two tools *use* Redaktyn, this one **checks whether Redaktyn is
|
|
519
|
+
telling the truth**. It has no dependencies and no imports from this repo — it
|
|
520
|
+
reimplements the digest pipeline from the published spec on purpose, so that
|
|
521
|
+
agreeing with `@redaktyn/shared` means something. Read it before running it; it is
|
|
522
|
+
one file, and short.
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
curl -o redaktyn-verify.mjs https://your-api.example.com/api/verify/cli
|
|
526
|
+
node redaktyn-verify.mjs --help
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
From the repo: `npm run verify -- <command>`.
|
|
530
|
+
|
|
531
|
+
| Command | Checks |
|
|
532
|
+
|---------|--------|
|
|
533
|
+
| `recompute` | Derives the three digests from your own secret and compares them against what `GET /api/secrets` holds |
|
|
534
|
+
| `build` | Hashes the served dashboard bundle and compares it to the Ed25519-signed release manifest |
|
|
535
|
+
| `pin` | Records the current release-log head so a later rewrite is detectable |
|
|
536
|
+
| `check` | Re-fetches the log and verifies your pinned release still reproduces |
|
|
537
|
+
| `egress` | Asserts every field in a captured HAR against the published allowlist |
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
export REDAKTYN_SERVER="https://your-api.example.com"
|
|
541
|
+
|
|
542
|
+
node redaktyn-verify.mjs recompute --org-id <uuid> # prompts for passphrase + secret
|
|
543
|
+
node redaktyn-verify.mjs recompute --org-id <uuid> --token <jwt> # also compare vs server
|
|
544
|
+
node redaktyn-verify.mjs build
|
|
545
|
+
node redaktyn-verify.mjs pin
|
|
546
|
+
node redaktyn-verify.mjs check
|
|
547
|
+
node redaktyn-verify.mjs egress --har capture.har
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Every command exits non-zero on failure, so all of them work as CI gates.
|
|
551
|
+
`REDAKTYN_VERIFY_PIN` overrides where the pin is stored, which is what you want
|
|
552
|
+
when the pin should live beside the job doing the checking.
|
|
553
|
+
|
|
554
|
+
Each command prints what its result does **not** establish. For the reasoning
|
|
555
|
+
behind the split — and the things none of this proves — see
|
|
556
|
+
[docs/05-security/VERIFIABLE-PROOF.md](../docs/05-security/VERIFIABLE-PROOF.md).
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## License
|
|
561
|
+
|
|
562
|
+
MIT — free to use, inspect, fork, and redistribute.
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redaktyn/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Redaktyn CLI: bulk-enrol secrets from a .env or any vault (--project aware), local fingerprints, git/directory digest scans. Digests are computed on your machine; secret values never leave it.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"redaktyn-fingerprint": "./redaktyn-fingerprint.mjs",
|
|
8
|
+
"redaktyn-import": "./redaktyn-import.mjs",
|
|
9
|
+
"redaktyn-scan": "./redaktyn-scan.mjs",
|
|
10
|
+
"redaktyn-verify": "./redaktyn-verify.mjs"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@redaktyn/shared": "^1.0.0"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"keywords": [
|
|
20
|
+
"redaktyn",
|
|
21
|
+
"secret",
|
|
22
|
+
"secrets",
|
|
23
|
+
"dlp",
|
|
24
|
+
"scan",
|
|
25
|
+
"git",
|
|
26
|
+
"pre-commit",
|
|
27
|
+
"dotenv",
|
|
28
|
+
"vault",
|
|
29
|
+
"import"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/redaktyn/redaktyn.git",
|
|
34
|
+
"directory": "cli"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"redaktyn-fingerprint.mjs",
|
|
41
|
+
"redaktyn-import.mjs",
|
|
42
|
+
"redaktyn-scan.mjs",
|
|
43
|
+
"redaktyn-verify.mjs",
|
|
44
|
+
"README.md"
|
|
45
|
+
]
|
|
46
|
+
}
|