@redaktyn/cli 0.0.0-stage → 1.5.7

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 ADDED
@@ -0,0 +1,26 @@
1
+ Redaktyn Software License
2
+
3
+ Copyright (c) 2026 Redaktyn. All rights reserved.
4
+
5
+ This software is made available to customers and evaluators of Redaktyn for one purpose: to be installed and run in
6
+ connection with a Redaktyn workspace. No other right is granted.
7
+
8
+ You may not, and may not allow anyone else to:
9
+
10
+ 1. copy, modify, translate or create derivative works of the software, except for the copying that running it
11
+ requires;
12
+ 2. distribute, sell, lease, sublicense, publish or otherwise make the software available to a third party;
13
+ 3. use the software, or anything learned from it, to build, train or improve a product or service that competes with
14
+ Redaktyn;
15
+ 4. decompile, disassemble, reverse engineer, or otherwise attempt to extract the source code, algorithms, rules or
16
+ models of the software, except to the extent this restriction is forbidden by applicable law.
17
+
18
+ Versions of this software published before this license was adopted remain available under the terms they were
19
+ published with. This license applies to this version and to later versions.
20
+
21
+ The software includes third-party components, each under its own license; for example, @noble/hashes is MIT licensed
22
+ and its notice is kept in the file that contains it.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING THE WARRANTIES OF
25
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
26
+ CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
package/README.md CHANGED
@@ -1,3 +1,630 @@
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 the Trust Center in your Redaktyn dashboard.
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`** — uses the digest code that ships inside this package; there is nothing else to install.
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
+ On a brand-new workspace that has no passphrase yet, one is generated for you, shown once, and recorded with the server once after you type `saved`. A passphrase you type yourself is refused for a new workspace, `REDAKTYN_PASSPHRASE` included (set it to the generated one for later runs). An `import` dry run records nothing and uses a throwaway passphrase. A workspace that already has a passphrase is unchanged: you type it.
49
+ 3. Adding one or more secrets (label + value, both masked)
50
+ 4. Showing you the exact crypto steps for transparency
51
+ 5. Printing copy-ready JSON to paste into the dashboard
52
+
53
+ ### Non-interactive (CI / scripting)
54
+
55
+ ```bash
56
+ # Preferred: passphrase via env var (won't appear in shell history)
57
+ export REDAKTYN_PASSPHRASE="your-org-passphrase"
58
+
59
+ cd cli && node redaktyn-fingerprint.mjs \
60
+ --org-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
61
+ --label "AWS_SECRET_ACCESS_KEY" \
62
+ --secret "AKIAIOSFODNN7EXAMPLE..."
63
+ ```
64
+
65
+ ### Batch mode (multiple secrets at once)
66
+
67
+ Create a plain-text file `secrets.txt`:
68
+
69
+ ```
70
+ # Lines starting with # are ignored
71
+ AWS_ACCESS_KEY:AKIAIOSFODNN7EXAMPLE
72
+ DATABASE_PASSWORD:s3cr3tPassw0rd!
73
+ STRIPE_SECRET_KEY:sk_live_abc123xyz
74
+ GITHUB_TOKEN:ghp_xxxxxxxxxxxx
75
+ ```
76
+
77
+ Run:
78
+
79
+ ```bash
80
+ export REDAKTYN_PASSPHRASE="your-org-passphrase"
81
+
82
+ cd cli && node redaktyn-fingerprint.mjs \
83
+ --org-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
84
+ --batch secrets.txt
85
+ ```
86
+
87
+ 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`.
88
+
89
+ ### JSON output (for piping / automation)
90
+
91
+ ```bash
92
+ node cli/redaktyn-fingerprint.mjs --org-id ... --label ... --secret ... --json
93
+ ```
94
+
95
+ Output:
96
+ ```json
97
+ {
98
+ "label": "AWS_SECRET_ACCESS_KEY",
99
+ "hmacHex": "64-lowercase-hex-chars...",
100
+ "length": 20
101
+ }
102
+ ```
103
+
104
+ ---
105
+
106
+ ## All options
107
+
108
+ | Flag | Description |
109
+ |------|-------------|
110
+ | `--org-id <uuid>` | Your organization UUID (Dashboard → Settings) |
111
+ | `--label <str>` | Human-readable name, e.g. `AWS_SECRET_KEY` |
112
+ | `--secret <str>` | The raw secret value to fingerprint |
113
+ | `--passphrase <str>` | Org passphrase (**prefer** `REDAKTYN_PASSPHRASE` env var) |
114
+ | `--batch <file>` | Path to a `LABEL:value` file for bulk generation |
115
+ | `--json` | Output pure JSON, no colors or headers |
116
+ | `--no-color` | Disable ANSI colors |
117
+ | `--help` | Show full help |
118
+
119
+ **Environment variables:**
120
+
121
+ | Variable | Description |
122
+ |----------|-------------|
123
+ | `REDAKTYN_PASSPHRASE` | Org passphrase — preferred over `--passphrase` flag |
124
+ | `NO_COLOR` | Disable ANSI colors (standard convention) |
125
+
126
+ ---
127
+
128
+ ## How the cryptography works
129
+
130
+ 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.
131
+
132
+ ```
133
+ Step 1 — Key derivation
134
+ Input: passphrase (UTF-8) + orgId (UTF-8)
135
+ Salt: SHA-256(orgId) ← org-scoped, no secret leakage
136
+ KDF: PBKDF2-SHA256, 600,000 iterations, 256-bit output
137
+ (KDF version pbkdf2-sha256-600k-v2, matching the browser extension)
138
+ Output: 32-byte HMAC key
139
+
140
+ Step 2 — Secret canonicalization
141
+ • Unicode NFC normalization
142
+ • CRLF / CR → LF
143
+ • Strip invisible / zero-width characters used in bypass tricks
144
+ • Trim leading & trailing whitespace
145
+ • UUID strings: lowercase (case-insensitive matching)
146
+
147
+ Step 3 — Fingerprint (three variants, all enrolled)
148
+ hmacHex HMAC-SHA256(canonical secret, key)
149
+ hmacHexStripped same, after removing every Unicode whitespace character
150
+ hmacHexLower same as stripped, then lowercased
151
+
152
+ Output: 64-character lowercase hex strings ← these are stored
153
+ ```
154
+
155
+ The two extra variants exist because a secret is trivially evaded otherwise:
156
+ `Amit@123` enrolled as a single digest is missed the moment someone types
157
+ `A mit@123` or `aMIt@123`. Enrolling only `hmacHex` leaves those gaps open.
158
+
159
+ **What the server stores:** only the labels, the three digests and their lengths — never the raw secret or the HMAC key.
160
+
161
+ You can verify this tool against extension-enrolled secrets by registering the same secret both ways and confirming the digests match. `redaktyn-verify recompute` computes the same digests again, independently of this tool.
162
+
163
+ **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.
164
+
165
+ ---
166
+
167
+ ## Finding your Org ID
168
+
169
+ 1. Log in to the Redaktyn dashboard
170
+ 2. Go to **Settings** (sidebar, bottom)
171
+ 3. Scroll to **Organization Details**
172
+ 4. Copy the UUID shown under "Organization ID"
173
+
174
+ ---
175
+
176
+ ## Importing the fingerprint into the dashboard
177
+
178
+ After generating the JSON:
179
+
180
+ 1. Dashboard → **Secrets** page
181
+ 2. Expand **"I already have a fingerprint from the CLI"** (collapsible panel — not a separate tab)
182
+ 3. Paste the JSON output into the smart paste zone
183
+ 4. Click **Register fingerprint**
184
+ 5. Extensions pick up the new fingerprint on next sync (within ~5 minutes)
185
+
186
+ ---
187
+
188
+ ## Security notes
189
+
190
+ - **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.
191
+ - `REDAKTYN_PASSPHRASE` via env is safer than `--passphrase` (not in shell history by default, but visible in `/proc`).
192
+ - For maximum safety: use the interactive wizard on a dedicated machine, then delete shell history.
193
+ - The fingerprint CLI is plain JavaScript. Read it — there are no obfuscated sections.
194
+
195
+ ---
196
+
197
+ ## `redaktyn-connect` and `redaktyn-invite` — a developer who never holds admin credentials
198
+
199
+ `redaktyn-import` and `redaktyn-scan` normally need an OWNER/ADMIN login token and the workspace
200
+ passphrase. That is the right thing for an admin, and the wrong thing to hand a developer who only has to
201
+ enrol one engagement's secrets. An invite gives them the narrow version instead.
202
+
203
+ **Admin: mint a single-use link for one project.**
204
+
205
+ ```bash
206
+ redaktyn-login # once: email + password, nothing to export or copy
207
+ export REDAKTYN_PASSPHRASE='...' # or leave it out and you are asked; it stays on this machine
208
+
209
+ redaktyn-invite --project acme-engagement --can-enrol --expires-days 3
210
+ # -> https://redaktyn.com/connect?code=ABCD1234#w=...
211
+ # in a terminal it then offers to copy the link; --copy always does, --no-copy never does
212
+ ```
213
+
214
+ The project's scanning key is derived locally and sealed under a one-time key that exists only in the link's
215
+ `#fragment`. The server stores ciphertext it cannot open, and the fragment is never sent to it. `--can-enrol`
216
+ is off unless you ask: without it the link is scan-only. A link carries a key for **one** device, so it works
217
+ once.
218
+
219
+ **Developer: connect once, then work.**
220
+
221
+ ```bash
222
+ redaktyn-connect # prompts for the link, so it stays out of shell history
223
+ redaktyn-import --env .env # dry run, no environment variables needed
224
+ redaktyn-import --env .env --apply
225
+ redaktyn-scan --staged # also uses the saved key
226
+ redaktyn-connect --status | --logout
227
+ ```
228
+
229
+ What the developer has, and does not have:
230
+
231
+ | | |
232
+ |---|---|
233
+ | Can | scan, and (with `--can-enrol`) register and re-fingerprint secrets **on that one project** |
234
+ | Cannot | enrol anywhere else, delete or edit a secret, see another project's digests, or reach any admin route |
235
+ | Holds | that project's key only (`HKDF(workspace key, project id)`), never the workspace key or passphrase |
236
+ | Revoked by | an admin removing the device (`DELETE /api/users/:id`): it stops working at once, not when a token expires |
237
+
238
+ The session lives in `~/.config/redaktyn/session.json` (directory `0700`, file `0600`), the same protection
239
+ `~/.aws/credentials` relies on. It is a file and not the OS keychain on purpose: macOS `security
240
+ add-generic-password` takes the secret as a command-line argument, which any process on the machine can read
241
+ with `ps` while it runs. The token renews itself; if the session cannot be renewed, or an admin removed the
242
+ device, the command says to ask for a new link. An admin's own `REDAKTYN_TOKEN` in the environment always takes
243
+ precedence over a saved session.
244
+
245
+ **Server note:** `--can-enrol` needs the `InviteEnrolGrant` table. Schema changes here are applied by hand
246
+ (`npx prisma db push`), and an invite minted before that is refused with `ENROL_GRANTS_UNAVAILABLE` rather
247
+ than quietly becoming a plain link.
248
+
249
+ ## `redaktyn-import` — bulk enrollment from files and vaults
250
+
251
+ Enrollment used to mean typing every secret into the dashboard by hand, so coverage was
252
+ whatever the admin remembered. The values are already sitting somewhere — a dotenv file,
253
+ Vault, AWS Secrets Manager, Doppler, 1Password — and every one of those can already print
254
+ JSON. `redaktyn-import` reads that, computes the digests **on your machine**, and sends
255
+ only digests.
256
+
257
+ Like `redaktyn-fingerprint`, it needs **no npm install** — Node 18+ and nothing else.
258
+
259
+ ### Quick start
260
+
261
+ ```bash
262
+ redaktyn-login # once: your OWNER/ADMIN email and password
263
+
264
+ # 1. See what it would do. It asks for the workspace passphrase (or read REDAKTYN_PASSPHRASE). This is the default; nothing is written.
265
+ node cli/redaktyn-import.mjs --env .env.production --source prod
266
+
267
+ # 2. Happy with the plan? Enroll it.
268
+ node cli/redaktyn-import.mjs --env .env.production --source prod --apply
269
+ ```
270
+
271
+ The dry run prints one line per key — the key name, the value's length, and the decision:
272
+
273
+ ```
274
+ KEY LEN ACTION WHY
275
+ AWS_SECRET_ACCESS_KEY 35 CREATE new
276
+ DATABASE_URL 52 CREATE new
277
+ SLACK_WEBHOOK 77 CREATE new
278
+ API_BASE_URL 26 rejected URL with no embedded credential or token
279
+ NODE_ENV 10 rejected only 10 chars (minimum 12)
280
+ PORT 4 rejected only 4 chars (minimum 12)
281
+ ────────────────────────────────────────────────────────────────────────────────
282
+ 3 to create · 3 filtered out
283
+ Scan budget: every paste up to the extension's 5,242,880-char hard limit is scanned.
284
+
285
+ Dry run — nothing was sent. Re-run with --apply to register.
286
+ ```
287
+
288
+ **Values are never printed** — not truncated, not masked, not even for a rejected key. A
289
+ preview is the thing that gets screenshotted or piped into a CI log.
290
+
291
+ ### Vault recipes
292
+
293
+ There are no vendor SDKs here and nothing to configure. Every vault CLI can already emit
294
+ JSON, so `--stdin` covers all of them today:
295
+
296
+ ```bash
297
+ # HashiCorp Vault (KV v1 and v2 envelopes are both unwrapped)
298
+ vault kv get -format=json secret/prod | node cli/redaktyn-import.mjs --stdin --source vault
299
+
300
+ # AWS Secrets Manager
301
+ aws secretsmanager get-secret-value --secret-id prod \
302
+ --query SecretString --output text | node cli/redaktyn-import.mjs --stdin --source aws
303
+
304
+ # …or pipe the whole envelope; SecretString is unwrapped for you
305
+ aws secretsmanager get-secret-value --secret-id prod \
306
+ | node cli/redaktyn-import.mjs --stdin --source aws
307
+
308
+ # Doppler
309
+ doppler secrets download --no-file --format json \
310
+ | node cli/redaktyn-import.mjs --stdin --source doppler
311
+
312
+ # 1Password
313
+ op item get prod --format json | node cli/redaktyn-import.mjs --stdin --source 1password
314
+
315
+ # GitHub Actions secrets cannot be read back by design — export from wherever they
316
+ # were set, or enroll those with redaktyn-fingerprint.
317
+ ```
318
+
319
+ Plus three file modes: `--env <file>` (dotenv, including `export ` prefixes, quotes,
320
+ comments and multi-line values such as PEM keys), `--json <file>`, and `--yaml <file>`.
321
+ YAML support is a **flat `key: value` subset only** — anything indented or listed is
322
+ refused rather than half-parsed, because a nested document read as flat enrolls digests
323
+ of values that are not the secrets.
324
+
325
+ ### Why this stays zero-knowledge
326
+
327
+ The CLI runs on the admin's machine, so the vault credential and the plaintext both stay
328
+ there. Reading vaults from the browser instead would be worse on every axis: AWS wants
329
+ SigV4 and sends no CORS headers, Doppler's API cannot be called from a page, and the
330
+ secret would have to pass through a web origin that anything injected into it can script.
331
+
332
+ ```
333
+ On your machine │ Sent to the server
334
+ ─────────────────────────────────────────┼──────────────────────────────
335
+ vault / .env / Doppler output │
336
+ ↓ │
337
+ canonicalize (NFC, CRLF→LF, trim, …) │
338
+ ↓ │
339
+ PBKDF2-SHA256(passphrase, orgId) → key │
340
+ ↓ │
341
+ HMAC-SHA256 × 3 variants │ { label, hmacHex, length,
342
+ + keyed scan prefilter │ hmacHexStripped, lengthStripped,
343
+ │ hmacHexLower, lengthLower,
344
+ │ scanPrefilter,
345
+ │ tags: ["import:prod"] }
346
+ ```
347
+
348
+ The digest pipeline is imported from `redaktyn-fingerprint.mjs` rather than copied, so
349
+ there is exactly one canonical form in this directory. A guard test enforces that.
350
+
351
+ ### What it decides, and what it refuses to decide
352
+
353
+ Identity is the **key name as the label**, scoped to the tag `import:<source>`. Two
354
+ imports from two vaults therefore never fight over the same row.
355
+
356
+ | Situation | Action |
357
+ |-----------|--------|
358
+ | Key is not enrolled yet | **CREATE** — `POST /api/secrets` with the `import:<source>` tag |
359
+ | Key is enrolled from this source, value changed | **ROTATE** — `PUT /api/secrets/:id/digest`, same row, same label, same detection history |
360
+ | Key is enrolled from this source, value unchanged | skip — or **ROTATE** once if the stored row lacks a bypass variant or the scan prefilter |
361
+ | This value is already enrolled under another label | skip, and says which label |
362
+ | A secret with this label exists **outside** this source | **CONFLICT** — reported, left alone |
363
+ | A secret from this source is missing from the input | reported, **never removed** |
364
+
365
+ The last two are deliberate. Rotating a same-label secret would silently take over one
366
+ that was enrolled by hand, and a vault read that fails or returns a subset must not be
367
+ able to cause org-wide loss of protection.
368
+
369
+ ### The junk filter
370
+
371
+ Vaults contain `PORT=8080`, `NODE_ENV=production` and `DEBUG=true` alongside real secrets.
372
+ Enrolling `production` would end zero false positives in a single import — every paste
373
+ containing the word would be blocked. So values are filtered by default:
374
+
375
+ - shorter than 12 characters (`--min-length` to change)
376
+ - all digits, booleans, version numbers, email addresses, filesystem paths
377
+ - placeholders such as `changeme`
378
+ - fewer than 5 distinct characters, or very low character diversity
379
+ - URLs with **no** embedded credential and **no** token-shaped path segment
380
+
381
+ That last rule keeps the URLs that are themselves secrets: a Postgres DSN with an inline
382
+ password and a Slack webhook are both enrolled, while `https://api.example.com/v1` is not.
383
+
384
+ The filter advises; you decide. `--allow-weak` enrolls the rejects anyway and marks each
385
+ one as weak in the plan, so it cannot be used without seeing what it did.
386
+
387
+ ### The scan budget
388
+
389
+ Detection slides one pass per distinct secret length, across three normalization passes,
390
+ and past `MAX_SCAN_WINDOWS` the extension **refuses** to scan rather than hang, which is
391
+ a hard block in sensitive contexts.
392
+
393
+ Every secret this command enrols also carries a **scan prefilter**: three keyed 16-bit
394
+ fingerprints that let the extension skip windows that cannot be that secret before
395
+ signing them. That is what makes a bulk import cheap. Each length still costs a pass, but
396
+ a pass over a fingerprinted length costs about 1/256 of what it used to. Largest paste
397
+ still scanned, with every secret a different length:
398
+
399
+ | Enrolled secrets | Window lengths | With prefilter (this CLI) | Without (enrolled before 2026-10-07) |
400
+ |------------------|----------------|---------------------------|--------------------------------------|
401
+ | 1 | 3 | 5,242,880 chars (the hard limit) | ~500,000 chars |
402
+ | 10 | 30 | 5,242,880 chars (the hard limit) | ~50,000 chars |
403
+ | 50 | 150 | ~2,550,000 chars | ~10,000 chars |
404
+ | 100 | 300 | ~1,275,000 chars | ~5,068 chars |
405
+ | 200 | 600 | ~637,000 chars | ~2,618 chars |
406
+
407
+ The figure printed is a **floor**: the size any text of any shape is guaranteed to be
408
+ scanned at, computed with the same arithmetic the extension's admission check uses and
409
+ pinned to it by tests. Ordinary prose usually scans well past it.
410
+
411
+ A secret enrolled before the prefilter existed has none, and every length it shares keeps
412
+ the old cost. The plan accounts for that. Secrets **in this import** that lack one are
413
+ re-sent once to add it (`rotate — value unchanged; adding missing variants`). Any left
414
+ outside the import are counted, with what re-enrolling them would raise the ceiling to:
415
+
416
+ ```
417
+ Scan budget: 15 window lengths, pastes up to ~248,570 chars.
418
+ 2 enrolled secrets have no scan prefilter (enrolled before 2026-10-07 or by an older client),
419
+ so every paste is checked against them the slow way. They are outside this import; re-enrolling them
420
+ from the dashboard, or importing them, raises this to ~5,242,880 chars.
421
+ ```
422
+
423
+ **Against a server that predates the prefilter**, the CLI notices (its rows carry no
424
+ `scanPrefilter` field, or it refuses the field on the first write) and registers without
425
+ it, so the import still succeeds. Re-run it after the server updates and it adds the
426
+ prefilter to each secret. `--report-json` includes `scanBudget.legacySecrets`,
427
+ `maxPasteCharsIfReenrolled`, `atCharacterCap` and `serverStoresScanPrefilter`.
428
+
429
+ ### Flags
430
+
431
+ | Flag | Description |
432
+ |------|-------------|
433
+ | `--env <file>` | dotenv file |
434
+ | `--json <file>` | JSON object of secrets |
435
+ | `--yaml <file>` | flat `key: value` YAML only |
436
+ | `--stdin` | JSON on stdin — how vault CLIs are consumed |
437
+ | `--source <name>` | Provenance tag, stored as `import:<name>` (default: file name, or `stdin`) |
438
+ | `--apply` | Actually enroll. Without it, dry run. |
439
+ | `--yes` | Skip the confirmation prompt (CI) |
440
+ | `--min-length <n>` | Minimum value length to accept (default 12) |
441
+ | `--allow-weak` | Enroll values the filter rejected |
442
+ | `--only <key\|glob>` | Only these keys (repeatable) |
443
+ | `--exclude <key\|glob>` | Skip these keys (repeatable) |
444
+ | `--server <url>` | Override `REDAKTYN_SERVER_URL` |
445
+ | `--org-id <uuid>` | Override org id (must match the token) |
446
+ | `--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 |
447
+ | `--report-json` | Machine-readable plan, no table |
448
+ | `--no-color` | Disable ANSI colors |
449
+
450
+ **Signing in.** Run `redaktyn-login` once. It opens your browser on a code, like `gh auth login`: you are already signed in
451
+ to the dashboard, so you check that the code matches the one in the terminal and press Approve. Nothing is typed into the
452
+ terminal, and the token is never in a URL. Over SSH or in a container, `--no-browser` prints the link to open anywhere you are
453
+ signed in. A machine with no browser at all can use `--password` (email and password in the terminal) or `--password-stdin`.
454
+ The login token is kept in a file only you can read, so `redaktyn-import` and `redaktyn-invite` need no `export` for it; the
455
+ password is never stored. Only an OWNER or ADMIN can approve. The server address defaults to `https://redaktyn.com`; only a
456
+ self-hosted install sets `REDAKTYN_SERVER_URL` or `--server`. `redaktyn-login --status` shows who you are, `--logout` forgets
457
+ it. A login that has aged out says so and asks you to log in again.
458
+
459
+ **What the terminal shows.** On an interactive terminal every command uses one look: a banner, steps that end in a tick, boxed summaries, and for
460
+ secrets a view of what is happening to them. `redaktyn-demo` runs the real fingerprint pipeline offline on a made-up secret (no account needed). On
461
+ a real import the first secret is walked through normalise, key derivation and HMAC, and a box states what would leave the machine (requests, bytes,
462
+ fields) before anything is sent, on a dry run too; `--show-request` prints the first request body exactly as built, digests shortened. The byte count
463
+ is computed from the requests that are then sent, and a test fails if they ever differ. Piped output, CI and `TERM=dumb` get the plain lines scripts
464
+ rely on; `NO_COLOR`, `REDAKTYN_NO_ANIMATION` and `REDAKTYN_ASCII` are honoured. Results (a link, `--report-json`) go to stdout, the display to stderr.
465
+
466
+ **The passphrase prompt** says who is signed in, what it is asking for and that nothing shows while you type, then checks the answer against the workspace before anything is made from it, so a typo is refused with "try again (2 left)" instead of producing a link or digests that match nothing. Set `REDAKTYN_PASSPHRASE` to skip the prompt; it is checked the same way. A workspace with no stored verifier skips the check.
467
+
468
+ **Environment** (all optional now): `REDAKTYN_PASSPHRASE` (otherwise you are asked), `REDAKTYN_SERVER_URL`
469
+ (self-hosted only), `REDAKTYN_ORG_ID`, and `REDAKTYN_TOKEN` for scripts.
470
+
471
+ `REDAKTYN_TOKEN` must be an **OWNER or ADMIN login token**, and when it is set it wins over a saved login.
472
+ `REDAKTYN_DEVICE_TOKEN` is deliberately not accepted: an extension token can read the secret list, so a shell
473
+ set up for `redaktyn-scan` would produce a dry run that looks perfect and then fail on the first write, after
474
+ you had approved the plan. It is rejected up front instead.
475
+
476
+ **For CI**, where nobody can type a password, mint a token per run and pass it as `REDAKTYN_TOKEN`:
477
+
478
+ ```bash
479
+ export REDAKTYN_TOKEN=$(curl -s -X POST "https://redaktyn.com/api/auth/login" \
480
+ -H 'Content-Type: application/json' \
481
+ -d '{"email":"ci@example.com","password":"…"}' | jq -r .token)
482
+ ```
483
+
484
+ > **Correction (2026-10-10):** this section used to say the token could be copied out of a logged-in dashboard
485
+ > (Local Storage → `vaultx_token`). That has not been true since the dashboard session moved to an HttpOnly
486
+ > cookie, which script cannot read. Use `redaktyn-login`, or the call above.
487
+
488
+ **Exit codes:** `0` plan printed or every write succeeded · `1` one or more writes failed ·
489
+ `2` usage, auth or parse error.
490
+
491
+ ### CI example
492
+
493
+ ```yaml
494
+ - name: Sync enrolled secrets from Doppler
495
+ env:
496
+ REDAKTYN_SERVER_URL: ${{ secrets.REDAKTYN_SERVER_URL }}
497
+ REDAKTYN_PASSPHRASE: ${{ secrets.REDAKTYN_PASSPHRASE }}
498
+ REDAKTYN_TOKEN: ${{ secrets.REDAKTYN_ADMIN_TOKEN }}
499
+ run: |
500
+ doppler secrets download --no-file --format json \
501
+ | node cli/redaktyn-import.mjs --stdin --source doppler --apply --yes
502
+ ```
503
+
504
+ ---
505
+
506
+ ## `redaktyn-scan` — Pre-Commit & CI/CD Scanner (Zero-Knowledge)
507
+
508
+ Protect your codebase **before** secrets reach an AI tool. `redaktyn-scan` runs the same
509
+ zero-knowledge HMAC detection as the browser extension, but inside your git workflow and CI pipelines.
510
+
511
+ ### Quick start
512
+
513
+ Reinstall the package: `npm install -g @redaktyn/cli`, or run a command with `npx -p @redaktyn/cli`.
514
+
515
+ ```bash
516
+ # API base URL — for an EC2 deployment with no custom domain, use the public IP:
517
+ # export REDAKTYN_SERVER_URL="http://<EC2-PUBLIC-IP>"
518
+ export REDAKTYN_SERVER_URL="https://your-api.example.com"
519
+ export REDAKTYN_PASSPHRASE="your-org-passphrase"
520
+ export REDAKTYN_DEVICE_TOKEN="eyJ…" # extension JWT from popup → Connect, or admin JWT for CI
521
+
522
+ # Scan staged changes before committing
523
+ node cli/redaktyn-scan.mjs --staged
524
+
525
+ # Install permanent git hook (runs automatically on every commit)
526
+ node cli/redaktyn-scan.mjs install-hooks
527
+
528
+ # Remove the hook
529
+ node cli/redaktyn-scan.mjs uninstall-hooks
530
+
531
+ # Scan a single file
532
+ node cli/redaktyn-scan.mjs --file ./src/config.ts
533
+
534
+ # Scan all text files under a directory
535
+ node cli/redaktyn-scan.mjs --dir ./src --exclude "*.min.js"
536
+
537
+ # Check connection and enrolled secret count
538
+ node cli/redaktyn-scan.mjs status --json
539
+ ```
540
+
541
+ Or from repo root via workspace bin: `npm run redaktyn-scan -- --staged` (passes flags after `--`).
542
+
543
+ ### CI/CD integration (GitHub Actions example)
544
+
545
+ ```yaml
546
+ - name: Redaktyn secret scan
547
+ env:
548
+ REDAKTYN_SERVER_URL: ${{ secrets.REDAKTYN_SERVER_URL }}
549
+ REDAKTYN_PASSPHRASE: ${{ secrets.REDAKTYN_PASSPHRASE }}
550
+ REDAKTYN_TOKEN: ${{ secrets.REDAKTYN_TOKEN }}
551
+ run: |
552
+ node cli/redaktyn-scan.mjs --staged --json
553
+ ```
554
+
555
+ Exit code `1` when enrolled digests are found in the staged diff — blocks the commit or fails the CI job.
556
+
557
+ ### How it works (zero-knowledge)
558
+
559
+ 1. Derives an HMAC key from your passphrase (PBKDF2-SHA256, 600K iterations, org ID as salt) with the same digest code the browser extension uses, so its digests always match extension-enrolled secrets.
560
+ 2. Slides windows of each enrolled secret's length over added lines from the git diff.
561
+ 3. Sends **only 64-char hex HMAC digests** to `POST /api/cli/scan` — raw content never leaves your machine.
562
+ 4. Server checks digests against enrolled rows and returns label matches.
563
+
564
+ **Scan flags:**
565
+
566
+ | Flag | Description |
567
+ |------|-------------|
568
+ | `--staged` | Scan git staged additions (`git diff --cached`) |
569
+ | `--file <path>` | Scan a single file |
570
+ | `--dir <path>` | Scan all text files under directory |
571
+ | `--server <url>` | Override `REDAKTYN_SERVER_URL` |
572
+ | `--org-id <uuid>` | Override org id (must match JWT) |
573
+ | `--json` | JSON output (machine-readable, CI-friendly) |
574
+ | `--verbose` | Extra diagnostic output |
575
+ | `--exclude <pat>` | Skip paths containing pattern (repeatable) |
576
+ | `install-hooks` | Write `.git/hooks/pre-commit` |
577
+ | `uninstall-hooks` | Remove Redaktyn pre-commit hook |
578
+ | `status` | Show connection + enrolled secret count |
579
+
580
+ **Note:** scans use the primary normalization path. Secrets enrolled with **only** a whitespace-collapsed
581
+ digest may require the browser extension for full bypass-variant parity.
582
+
583
+ ---
584
+
585
+ ## `redaktyn-verify` — audit the claims
586
+
587
+ Where the other two tools *use* Redaktyn, this one **checks whether Redaktyn is
588
+ telling the truth**. It has no dependencies and no imports from this repo — it
589
+ reimplements the digest pipeline on its own, on purpose, so that agreeing with the extension and the CLI means something. Read it before running it; it is
590
+ one file, and short.
591
+
592
+ ```bash
593
+ curl -o redaktyn-verify.mjs https://your-api.example.com/api/verify/cli
594
+ node redaktyn-verify.mjs --help
595
+ ```
596
+
597
+ From the repo: `npm run verify -- <command>`.
598
+
599
+ | Command | Checks |
600
+ |---------|--------|
601
+ | `recompute` | Derives the three digests from your own secret and compares them against what `GET /api/secrets` holds |
602
+ | `build` | Hashes the served dashboard bundle and compares it to the Ed25519-signed release manifest |
603
+ | `pin` | Records the current release-log head so a later rewrite is detectable |
604
+ | `check` | Re-fetches the log and verifies your pinned release still reproduces |
605
+ | `egress` | Asserts every field in a captured HAR against the published allowlist |
606
+
607
+ ```bash
608
+ export REDAKTYN_SERVER="https://your-api.example.com"
609
+
610
+ node redaktyn-verify.mjs recompute --org-id <uuid> # prompts for passphrase + secret
611
+ node redaktyn-verify.mjs recompute --org-id <uuid> --token <jwt> # also compare vs server
612
+ node redaktyn-verify.mjs build
613
+ node redaktyn-verify.mjs pin
614
+ node redaktyn-verify.mjs check
615
+ node redaktyn-verify.mjs egress --har capture.har
616
+ ```
617
+
618
+ Every command exits non-zero on failure, so all of them work as CI gates.
619
+ `REDAKTYN_VERIFY_PIN` overrides where the pin is stored, which is what you want
620
+ when the pin should live beside the job doing the checking.
621
+
622
+ Each command prints what its result does **not** establish. For the reasoning
623
+ behind the split — and the things none of this proves — see
624
+ the Trust Center in your Redaktyn dashboard.
625
+
626
+ ---
627
+
628
+ ## License
629
+
630
+ MIT — free to use, inspect, fork, and redistribute.