@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 CHANGED
@@ -1,3 +1,562 @@
1
- # Temporary Holding Version
1
+ # Redaktyn Fingerprint CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }