@redaktyn/cli 1.5.8 → 1.5.9
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 +26 -36
- package/package.json +1 -1
- package/redaktyn-fingerprint.mjs +7 -7
package/README.md
CHANGED
|
@@ -35,11 +35,10 @@ This CLI is for you.
|
|
|
35
35
|
|
|
36
36
|
### Interactive wizard (recommended for first-timers)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Run it (after `npm install -g @redaktyn/cli`, or as `npx -p @redaktyn/cli redaktyn-fingerprint`):
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
|
|
42
|
-
node redaktyn-fingerprint.mjs
|
|
41
|
+
redaktyn-fingerprint
|
|
43
42
|
```
|
|
44
43
|
|
|
45
44
|
The wizard will guide you through:
|
|
@@ -56,7 +55,7 @@ The wizard will guide you through:
|
|
|
56
55
|
# Preferred: passphrase via env var (won't appear in shell history)
|
|
57
56
|
export REDAKTYN_PASSPHRASE="your-org-passphrase"
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
redaktyn-fingerprint \
|
|
60
59
|
--org-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
|
|
61
60
|
--label "AWS_SECRET_ACCESS_KEY" \
|
|
62
61
|
--secret "AKIAIOSFODNN7EXAMPLE..."
|
|
@@ -79,7 +78,7 @@ Run:
|
|
|
79
78
|
```bash
|
|
80
79
|
export REDAKTYN_PASSPHRASE="your-org-passphrase"
|
|
81
80
|
|
|
82
|
-
|
|
81
|
+
redaktyn-fingerprint \
|
|
83
82
|
--org-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
|
|
84
83
|
--batch secrets.txt
|
|
85
84
|
```
|
|
@@ -89,7 +88,7 @@ Outputs a JSON array — register each digest on Dashboard → **Secrets** (expa
|
|
|
89
88
|
### JSON output (for piping / automation)
|
|
90
89
|
|
|
91
90
|
```bash
|
|
92
|
-
|
|
91
|
+
redaktyn-fingerprint --org-id ... --label ... --secret ... --json
|
|
93
92
|
```
|
|
94
93
|
|
|
95
94
|
Output:
|
|
@@ -242,10 +241,6 @@ with `ps` while it runs. The token renews itself; if the session cannot be renew
|
|
|
242
241
|
device, the command says to ask for a new link. An admin's own `REDAKTYN_TOKEN` in the environment always takes
|
|
243
242
|
precedence over a saved session.
|
|
244
243
|
|
|
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
244
|
## `redaktyn-import` — bulk enrollment from files and vaults
|
|
250
245
|
|
|
251
246
|
Enrollment used to mean typing every secret into the dashboard by hand, so coverage was
|
|
@@ -262,10 +257,10 @@ Like `redaktyn-fingerprint`, it needs **no npm install** — Node 18+ and nothin
|
|
|
262
257
|
redaktyn-login # once: your OWNER/ADMIN email and password
|
|
263
258
|
|
|
264
259
|
# 1. See what it would do. It asks for the workspace passphrase (or read REDAKTYN_PASSPHRASE). This is the default; nothing is written.
|
|
265
|
-
|
|
260
|
+
redaktyn-import --env .env.production --source prod
|
|
266
261
|
|
|
267
262
|
# 2. Happy with the plan? Enroll it.
|
|
268
|
-
|
|
263
|
+
redaktyn-import --env .env.production --source prod --apply
|
|
269
264
|
```
|
|
270
265
|
|
|
271
266
|
The dry run prints one line per key — the key name, the value's length, and the decision:
|
|
@@ -295,22 +290,22 @@ JSON, so `--stdin` covers all of them today:
|
|
|
295
290
|
|
|
296
291
|
```bash
|
|
297
292
|
# HashiCorp Vault (KV v1 and v2 envelopes are both unwrapped)
|
|
298
|
-
vault kv get -format=json secret/prod |
|
|
293
|
+
vault kv get -format=json secret/prod | redaktyn-import --stdin --source vault
|
|
299
294
|
|
|
300
295
|
# AWS Secrets Manager
|
|
301
296
|
aws secretsmanager get-secret-value --secret-id prod \
|
|
302
|
-
--query SecretString --output text |
|
|
297
|
+
--query SecretString --output text | redaktyn-import --stdin --source aws
|
|
303
298
|
|
|
304
299
|
# …or pipe the whole envelope; SecretString is unwrapped for you
|
|
305
300
|
aws secretsmanager get-secret-value --secret-id prod \
|
|
306
|
-
|
|
|
301
|
+
| redaktyn-import --stdin --source aws
|
|
307
302
|
|
|
308
303
|
# Doppler
|
|
309
304
|
doppler secrets download --no-file --format json \
|
|
310
|
-
|
|
|
305
|
+
| redaktyn-import --stdin --source doppler
|
|
311
306
|
|
|
312
307
|
# 1Password
|
|
313
|
-
op item get prod --format json |
|
|
308
|
+
op item get prod --format json | redaktyn-import --stdin --source 1password
|
|
314
309
|
|
|
315
310
|
# GitHub Actions secrets cannot be read back by design — export from wherever they
|
|
316
311
|
# were set, or enroll those with redaktyn-fingerprint.
|
|
@@ -498,7 +493,7 @@ export REDAKTYN_TOKEN=$(curl -s -X POST "https://redaktyn.com/api/auth/login" \
|
|
|
498
493
|
REDAKTYN_TOKEN: ${{ secrets.REDAKTYN_ADMIN_TOKEN }}
|
|
499
494
|
run: |
|
|
500
495
|
doppler secrets download --no-file --format json \
|
|
501
|
-
|
|
|
496
|
+
| redaktyn-import --stdin --source doppler --apply --yes
|
|
502
497
|
```
|
|
503
498
|
|
|
504
499
|
---
|
|
@@ -510,36 +505,33 @@ zero-knowledge HMAC detection as the browser extension, but inside your git work
|
|
|
510
505
|
|
|
511
506
|
### Quick start
|
|
512
507
|
|
|
513
|
-
|
|
508
|
+
On your own machine, run `redaktyn-connect` once with your project's invite link and these commands need no environment variables. In CI, or anywhere there is no saved connection, set the variables below.
|
|
514
509
|
|
|
515
510
|
```bash
|
|
516
|
-
#
|
|
517
|
-
|
|
518
|
-
export REDAKTYN_SERVER_URL="https://your-api.example.com"
|
|
511
|
+
# Only when there is no saved connection (CI, scripts). A self-hosted install sets its own address.
|
|
512
|
+
export REDAKTYN_SERVER_URL="https://redaktyn.com"
|
|
519
513
|
export REDAKTYN_PASSPHRASE="your-org-passphrase"
|
|
520
|
-
export REDAKTYN_DEVICE_TOKEN="eyJ…" #
|
|
514
|
+
export REDAKTYN_DEVICE_TOKEN="eyJ…" # an OWNER/ADMIN token, for CI
|
|
521
515
|
|
|
522
516
|
# Scan staged changes before committing
|
|
523
|
-
|
|
517
|
+
redaktyn-scan --staged
|
|
524
518
|
|
|
525
519
|
# Install permanent git hook (runs automatically on every commit)
|
|
526
|
-
|
|
520
|
+
redaktyn-scan install-hooks
|
|
527
521
|
|
|
528
522
|
# Remove the hook
|
|
529
|
-
|
|
523
|
+
redaktyn-scan uninstall-hooks
|
|
530
524
|
|
|
531
525
|
# Scan a single file
|
|
532
|
-
|
|
526
|
+
redaktyn-scan --file ./src/config.ts
|
|
533
527
|
|
|
534
528
|
# Scan all text files under a directory
|
|
535
|
-
|
|
529
|
+
redaktyn-scan --dir ./src --exclude "*.min.js"
|
|
536
530
|
|
|
537
531
|
# Check connection and enrolled secret count
|
|
538
|
-
|
|
532
|
+
redaktyn-scan status --json
|
|
539
533
|
```
|
|
540
534
|
|
|
541
|
-
Or from repo root via workspace bin: `npm run redaktyn-scan -- --staged` (passes flags after `--`).
|
|
542
|
-
|
|
543
535
|
### CI/CD integration (GitHub Actions example)
|
|
544
536
|
|
|
545
537
|
```yaml
|
|
@@ -549,7 +541,7 @@ Or from repo root via workspace bin: `npm run redaktyn-scan -- --staged` (passes
|
|
|
549
541
|
REDAKTYN_PASSPHRASE: ${{ secrets.REDAKTYN_PASSPHRASE }}
|
|
550
542
|
REDAKTYN_TOKEN: ${{ secrets.REDAKTYN_TOKEN }}
|
|
551
543
|
run: |
|
|
552
|
-
|
|
544
|
+
redaktyn-scan --staged --json
|
|
553
545
|
```
|
|
554
546
|
|
|
555
547
|
Exit code `1` when enrolled digests are found in the staged diff — blocks the commit or fails the CI job.
|
|
@@ -590,12 +582,10 @@ reimplements the digest pipeline on its own, on purpose, so that agreeing with t
|
|
|
590
582
|
one file, and short.
|
|
591
583
|
|
|
592
584
|
```bash
|
|
593
|
-
curl -o redaktyn-verify.mjs https://
|
|
585
|
+
curl -o redaktyn-verify.mjs https://redaktyn.com/api/verify/cli
|
|
594
586
|
node redaktyn-verify.mjs --help
|
|
595
587
|
```
|
|
596
588
|
|
|
597
|
-
From the repo: `npm run verify -- <command>`.
|
|
598
|
-
|
|
599
589
|
| Command | Checks |
|
|
600
590
|
|---------|--------|
|
|
601
591
|
| `recompute` | Derives the three digests from your own secret and compares them against what `GET /api/secrets` holds |
|
|
@@ -605,7 +595,7 @@ From the repo: `npm run verify -- <command>`.
|
|
|
605
595
|
| `egress` | Asserts every field in a captured HAR against the published allowlist |
|
|
606
596
|
|
|
607
597
|
```bash
|
|
608
|
-
export REDAKTYN_SERVER="https://
|
|
598
|
+
export REDAKTYN_SERVER="https://redaktyn.com"
|
|
609
599
|
|
|
610
600
|
node redaktyn-verify.mjs recompute --org-id <uuid> # prompts for passphrase + secret
|
|
611
601
|
node redaktyn-verify.mjs recompute --org-id <uuid> --token <jwt> # also compare vs server
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redaktyn/cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.9",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/redaktyn-fingerprint.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{createHmac as A,pbkdf2 as L,createHash as O,hkdfSync as _}from"node:crypto";import{promisify as k}from"node:util";import*as E from"node:readline";import{readFileSync as C,existsSync as Y,realpathSync as F}from"node:fs";import{pathToFileURL as K}from"node:url";const U=k(L),a=process.argv.includes("--no-color")||process.env.NO_COLOR,e={reset:a?"":"\x1B[0m",bold:a?"":"\x1B[1m",dim:a?"":"\x1B[2m",red:a?"":"\x1B[31m",green:a?"":"\x1B[32m",yellow:a?"":"\x1B[33m",blue:a?"":"\x1B[34m",magenta:a?"":"\x1B[35m",cyan:a?"":"\x1B[36m",white:a?"":"\x1B[37m",gray:a?"":"\x1B[90m",bgBlue:a?"":"\x1B[44m",underline:a?"":"\x1B[4m"},b=6e5,v=32;function M(o){let r=o.normalize("NFC");return r=r.replace(/\r\n/g,`
|
|
3
3
|
`).replace(/[\r\u2028\u2029]/g,`
|
|
4
|
-
`),r=r.replace(/[\u00ad\u200b-\u200f\u2060-\u2064\ufeff]/g,""),r=r.trim(),P(r)}function P(o){return o.length===36&&o[8]==="-"&&o[13]==="-"&&o[18]==="-"&&o[23]==="-"&&/^[0-9a-fA-F]{32}$/.test(o.replace(/-/g,""))?o.toLowerCase():o}async function H(o,r){const t=O("sha256").update(r,"utf8").digest();return U(o,t,b,v,"sha256")}const
|
|
4
|
+
`),r=r.replace(/[\u00ad\u200b-\u200f\u2060-\u2064\ufeff]/g,""),r=r.trim(),P(r)}function P(o){return o.length===36&&o[8]==="-"&&o[13]==="-"&&o[18]==="-"&&o[23]==="-"&&/^[0-9a-fA-F]{32}$/.test(o.replace(/-/g,""))?o.toLowerCase():o}async function H(o,r){const t=O("sha256").update(r,"utf8").digest();return U(o,t,b,v,"sha256")}const z="redaktyn-project-key-v1";function $e(o,r){if(!r)throw new Error("deriveProjectKey: projectId is required");return Buffer.from(_("sha256",o,Buffer.from(r,"utf8"),Buffer.from(z,"utf8"),v))}function R(o){const r=o.normalize("NFC").replace(/\r\n/g,`
|
|
5
5
|
`).replace(/[\r\u2028\u2029]/g,`
|
|
6
|
-
`).replace(/[\u00ad\u200b-\u200f\u2060-\u2064\ufeff]/g,"").trim().replace(/\s/g,"");return P(r)}function
|
|
6
|
+
`).replace(/[\u00ad\u200b-\u200f\u2060-\u2064\ufeff]/g,"").trim().replace(/\s/g,"");return P(r)}function B(o){return R(o).toLowerCase()}function y(o,r){return A("sha256",r).update(o,"utf8").digest("hex")}const j="redaktyn-scan-prefilter-v1";function W(o,r){let t=0;for(let s=0;s<r.length;s++){const n=r.charCodeAt(s);t=Math.imul(t,o)+(n>=65&&n<=90?n+32:n)+1|0}return t>>>16}function J(o,r){const s=A("sha256",r).update(j,"utf8").digest().readInt32BE(0)|1,n=l=>W(s,l).toString(16).padStart(4,"0");return`v1.${n(o.exact)}${n(o.collapsed)}${n(o.lower)}`}function S(o,r,t,s){const n=q(r,t);return{label:o,orgId:s,hmacHex:n.digest,length:n.length,hmacHexStripped:n.hmacHexStripped,lengthStripped:n.lengthStripped,hmacHexLower:n.hmacHexLower,lengthLower:n.lengthLower,scanPrefilter:n.scanPrefilter,canonical:n.canonical}}function q(o,r){const t=M(o),s=R(o),n=B(o);return{digest:y(t,r),canonical:t,length:t.length,hmacHexStripped:y(s,r),lengthStripped:s.length,hmacHexLower:y(n,r),lengthLower:n.length,scanPrefilter:J({exact:t,collapsed:s,lower:n},r)}}function G(o){const r={help:!1,json:!1,noColor:!1,orgId:null,passphrase:null,secret:null,label:null,batch:null,allowSecretArgv:!1,allowPassphraseArgv:!1,secretFromArgv:!1,passphraseFromArgv:!1};for(let t=2;t<o.length;t++){const s=o[t];s==="--help"||s==="-h"?r.help=!0:s==="--json"?r.json=!0:s==="--no-color"?r.noColor=!0:s==="--org-id"?r.orgId=o[++t]??null:s==="--passphrase"?(r.passphrase=o[++t]??null,r.passphraseFromArgv=!0):s==="--secret"?(r.secret=o[++t]??null,r.secretFromArgv=!0):s==="--label"?r.label=o[++t]??null:s==="--batch"?r.batch=o[++t]??null:s==="--i-know-this-leaks-to-history"&&(r.allowSecretArgv=!0,r.allowPassphraseArgv=!0)}return r}function Z(o){const r=[];o.secretFromArgv&&!o.allowSecretArgv&&r.push("--secret"),o.passphraseFromArgv&&!o.allowPassphraseArgv&&r.push("--passphrase"),r.length!==0&&(console.error(`${e.red}${e.bold}Refusing to read sensitive material from argv: ${r.join(", ")}${e.reset}`),console.error(""),console.error(" argv is recorded in shell history and is visible to other processes."),console.error(" Use one of the safer paths instead:"),console.error(""),console.error(` ${e.green}# 1) env var (passphrase)${e.reset}`),console.error(" REDAKTYN_PASSPHRASE='\u2026' redaktyn-fingerprint \\"),console.error(" --org-id <uuid> --label MY_KEY"),console.error(""),console.error(` ${e.green}# 2) stdin pipe (secret)${e.reset}`),console.error(` printf '%s' "$SECRET" | redaktyn-fingerprint \\`),console.error(" --org-id <uuid> --label MY_KEY"),console.error(""),console.error(` ${e.green}# 3) interactive wizard (no flags) \u2014 hidden TTY prompts${e.reset}`),console.error(" redaktyn-fingerprint"),console.error(""),console.error(` ${e.dim}If you absolutely must keep using argv (e.g. one-off in an ephemeral`),console.error(` shell where history is disabled), pass --i-know-this-leaks-to-history.${e.reset}`),process.exit(2))}const X=Math.min(process.stdout.columns||80,80);function c(o="\u2500"){return e.gray+o.repeat(X)+e.reset}function he(o,r){return o+" ".repeat(Math.max(0,r-Q(o).length))}function Q(o){return o.replace(/\x1b\[[0-9;]*m/g,"")}function V(){const o=`${e.bold}${e.cyan} Redaktyn${e.reset}${e.bold} Fingerprint CLI${e.reset}`,r=`${e.gray} Zero-knowledge local secret digest \u2014 nothing leaves your machine${e.reset}`;console.log(),console.log(c("\u2550")),console.log(` ${o}`),console.log(` ${r}`),console.log(c("\u2550")),console.log()}function ue(o,r,t){console.log(` ${e.blue}${e.bold}[${o}]${e.reset} ${e.bold}${r}${e.reset}`),t&&console.log(` ${e.gray}${t}${e.reset}`)}function I(o,r){console.log(` ${e.green}\u2713${e.reset} ${e.bold}${o}${e.reset} ${e.gray}${r}${e.reset}`)}function u(o){console.error(` ${e.yellow}\u26A0${e.reset} ${e.yellow}${o}${e.reset}`)}function fe(o){console.log(` ${e.cyan}\u2139${e.reset} ${e.gray}${o}${e.reset}`)}function ee(o){if(!process.stdout.isTTY||a)return{stop:n=>n&&console.log(` ${e.green}\u2713${e.reset} ${n}`)};const r=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];let t=0;const s=setInterval(()=>{process.stdout.write(`\r ${e.cyan}${r[t++%r.length]}${e.reset} ${o} `)},80);return{stop:n=>{clearInterval(s),process.stdout.write(`\r ${e.green}\u2713${e.reset} ${n??o}`+" ".repeat(20)+`
|
|
7
7
|
`)}}}function x(o){return new Promise(r=>{if(process.stdout.write(` ${e.bold}${o}${e.reset} `),!process.stdin.isTTY){let i="";process.stdin.setEncoding("utf8"),process.stdin.once("data",d=>{process.stdin.pause(),r(d.trim())});return}const t=E.createInterface({input:process.stdin,output:process.stdout,terminal:!0});let s="";const n=process.stdin.setRawMode;typeof n=="function"&&process.stdin.setRawMode(!0),process.stdin.resume(),process.stdin.setEncoding("utf8");const l=i=>{i===""&&process.exit(),i==="\r"||i===`
|
|
8
8
|
`?(process.stdin.removeListener("data",l),typeof n=="function"&&process.stdin.setRawMode(!1),t.close(),process.stdout.write(`
|
|
9
9
|
`),r(s)):i==="\x7F"||i==="\b"?s.length>0&&(s=s.slice(0,-1),process.stdout.write("\b \b")):(s+=i,process.stdout.write("\u2022"))};process.stdin.on("data",l)})}function f(o,r){return new Promise(t=>{const s=E.createInterface({input:process.stdin,output:process.stdout}),n=r?` ${e.bold}${o}${e.reset} ${e.gray}[${r}]${e.reset} `:` ${e.bold}${o}${e.reset} `;s.question(n,l=>{s.close(),t(l.trim()||r||"")})})}async function oe(o){const r=await f(`${o} ${e.gray}(y/N)${e.reset}:`);return r.toLowerCase()==="y"||r.toLowerCase()==="yes"}function T(o,r){if(r){console.log(JSON.stringify({...m(o),orgId:o.orgId},null,2));return}console.log(),console.log(c()),console.log(` ${e.green}${e.bold}\u2705 Fingerprint generated successfully${e.reset}`),console.log(c()),console.log(),console.log(` ${e.bold}Label ${e.reset}${e.cyan}${o.label}${e.reset}`),console.log(` ${e.bold}HMAC Hex ${e.reset}${e.yellow}${o.hmacHex}${e.reset}`),console.log(` ${e.bold} stripped ${e.reset}${e.gray}${o.hmacHexStripped}${e.reset}`),console.log(` ${e.bold} lower ${e.reset}${e.gray}${o.hmacHexLower}${e.reset}`),console.log(` ${e.bold}Length ${e.reset}${o.length} chars ${e.gray}(stripped ${o.lengthStripped}, lower ${o.lengthLower})${e.reset}`),console.log(` ${e.bold}Org ID ${e.reset}${e.gray}${o.orgId}${e.reset}`),console.log(` ${e.bold}Algorithm ${e.reset}PBKDF2-SHA256 \xB7 ${b.toLocaleString("en-US")} iters \xB7 HMAC-SHA256`),console.log(),console.log(` ${e.dim}\u2500\u2500 Ready to paste into dashboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${e.reset}`),console.log(),console.log(` ${e.bold}${e.blue}Copy this JSON and paste it using the "Import Digest" option:${e.reset}`),console.log(),JSON.stringify(m(o),null,4).split(`
|
|
@@ -14,16 +14,16 @@ ${e.bold}${e.cyan}Redaktyn Fingerprint CLI${e.reset} \u2014 Zero-knowledge loc
|
|
|
14
14
|
${e.bold}USAGE${e.reset}
|
|
15
15
|
|
|
16
16
|
${e.green}Interactive wizard (recommended):${e.reset}
|
|
17
|
-
|
|
17
|
+
redaktyn-fingerprint
|
|
18
18
|
|
|
19
19
|
${e.green}Non-interactive (scripting / CI):${e.reset}
|
|
20
|
-
REDAKTYN_PASSPHRASE="..."
|
|
20
|
+
REDAKTYN_PASSPHRASE="..." redaktyn-fingerprint \\
|
|
21
21
|
--org-id "your-org-uuid" \\
|
|
22
22
|
--label "MY_SECRET_KEY" \\
|
|
23
23
|
--secret "raw-secret-value"
|
|
24
24
|
|
|
25
25
|
${e.green}Batch mode (file with label:secret lines):${e.reset}
|
|
26
|
-
REDAKTYN_PASSPHRASE="..."
|
|
26
|
+
REDAKTYN_PASSPHRASE="..." redaktyn-fingerprint \\
|
|
27
27
|
--batch secrets.txt --org-id "your-org-uuid"
|
|
28
28
|
|
|
29
29
|
${e.bold}OPTIONS${e.reset}
|
|
@@ -74,10 +74,10 @@ ${e.bold}SECURITY NOTE${e.reset}
|
|
|
74
74
|
(argv is recorded in shell history and visible to other processes).
|
|
75
75
|
\u2500 Preferred: REDAKTYN_PASSPHRASE env var + stdin pipe for the secret:
|
|
76
76
|
printf '%s' "$SECRET" | REDAKTYN_PASSPHRASE='\u2026' \\
|
|
77
|
-
|
|
77
|
+
redaktyn-fingerprint --org-id <uuid> --label MY_KEY
|
|
78
78
|
\u2500 Override with --i-know-this-leaks-to-history only in disposable shells.
|
|
79
79
|
\u2500 This tool is intentionally dependency-free. Audit it: it's ~300 lines of Node.js.${e.reset}
|
|
80
80
|
|
|
81
81
|
`)}function te(o){Y(o)||(console.error(`${e.red}Error: batch file not found: ${o}${e.reset}`),process.exit(1));const r=C(o,"utf8").split(`
|
|
82
82
|
`),t=[];for(const[s,n]of r.entries()){const l=n.trim();if(!l||l.startsWith("#"))continue;const i=l.indexOf(":");if(i<1){u(`Skipping malformed line ${s+1} (expected LABEL:value).`);continue}t.push({label:l.slice(0,i).trim(),secret:l.slice(i+1)})}return t}const w=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function se(o){V(),console.log(` ${e.bold}Welcome!${e.reset} This wizard generates a zero-knowledge fingerprint of`),console.log(` your secret locally. Nothing is sent anywhere.
|
|
83
|
-
`),console.log(c("\xB7")),console.log(` ${e.bold}${e.magenta}STEP 1 \u2014 Organization ID${e.reset}`),console.log(` ${e.gray}Find this in: Dashboard \u2192 Settings \u2192 Organization Details${e.reset}`),console.log();let r=o.orgId?.trim()??"";if(!r||!w.test(r))for(r=await f("Org ID (UUID format, e.g. a1b2c3d4-\u2026):");!w.test(r.trim());)u("That doesn't look like a valid UUID. Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"),r=await f("Org ID:");else I("Org ID",r);r=r.trim(),console.log(),console.log(c("\xB7")),console.log(` ${e.bold}${e.magenta}STEP 2 \u2014 Organization Passphrase${e.reset}`),console.log(` ${e.gray}The same passphrase your team uses when connecting the extension.${e.reset}`),console.log(` ${e.gray}Tip: set REDAKTYN_PASSPHRASE in your shell to avoid retyping.${e.reset}`),console.log();let t=(process.env.REDAKTYN_PASSPHRASE&&String(process.env.REDAKTYN_PASSPHRASE))??"";if(t)I("Passphrase","\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (loaded from REDAKTYN_PASSPHRASE)");else for(t=await x("Passphrase (hidden):");!t||t.length<8;)u("Passphrase must be at least 8 characters."),t=await x("Passphrase:");const s=[];let n=!0,l=0;for(;n;){l++,console.log(),console.log(c("\xB7")),console.log(` ${e.bold}${e.magenta}STEP ${l===1?"3":l+2} \u2014 Secret #${l}${e.reset}`),console.log();const d=await f(`Label (human-readable name, e.g. AWS_SECRET_KEY${l>1,""}):`);if(!d){u("Label cannot be empty \u2014 skipping.");continue}const $=await x("Secret value (hidden):");if(!$){u("Secret cannot be empty \u2014 skipping.");continue}console.log(),console.log(` ${e.dim}\u2500\u2500 Computing fingerprint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${e.reset}`);const p=ee("Deriving HMAC key via PBKDF2-SHA256 (600,000 iterations)\u2026");let h;try{h=await H(t,r),p.stop("HMAC key derived "+e.gray+"(256-bit, browser-identical)"+e.reset)}catch(N){p.stop(),console.error(`${e.red}Key derivation failed: ${N.message}${e.reset}`),process.exit(1)}const g=S(d,$,h,r);console.log(),console.log(` ${e.dim}Step 1${e.reset} ${e.gray}NFC normalize + trim + UUID case-fold${e.reset}`),console.log(` ${e.gray}canonical form computed (${g.canonical.length} chars) \u2014 never printed${e.reset}`),console.log(` ${e.dim}Step 2${e.reset} ${e.gray}HMAC-SHA256(canonical, derivedKey)${e.reset}`),console.log(` ${e.yellow}${g.hmacHex.slice(0,16)}\u2026${g.hmacHex.slice(-8)}${e.reset} ${e.gray}(64-char hex, not reversible)${e.reset}`),console.log(` ${e.dim}Step 3${e.reset} ${e.gray}Two bypass variants: whitespace-collapsed and lowercased${e.reset}`),console.log(` ${e.gray}${g.hmacHexStripped.slice(0,16)}\u2026 \xB7 ${g.hmacHexLower.slice(0,16)}\u2026${e.reset}`),console.log(` ${e.dim}Step 4${e.reset} ${e.gray}Length recorded: ${g.length} chars${e.reset}`),s.push(g),console.log(),n=await oe("Add another secret with the same passphrase?")}const i=o.json;s.length===1?T(s[0],i):D(s,i),i||(console.log(` ${e.bold}${e.cyan}Next steps:${e.reset}`),console.log(` ${e.gray}1. Copy the JSON above${e.reset}`),console.log(` ${e.gray}2. Dashboard \u2192 Secrets \u2192 "Import Digest" button${e.reset}`),console.log(` ${e.gray}3. Paste the JSON and click Add${e.reset}`),console.log(` ${e.gray}4. Extensions pick it up on next sync (\u22645 min)${e.reset}`),console.log(),console.log(` ${e.dim}Your secret value was used only in-memory and is now gone.${e.reset}`),console.log())}async function ne(o){const r=o.orgId?.trim();(!r||!w.test(r))&&(console.error(`${e.red}Error: --org-id must be a valid UUID.${e.reset}`),console.error(" Find it in: Dashboard \u2192 Settings \u2192 Organization Details"),process.exit(1));const t=process.env.REDAKTYN_PASSPHRASE&&String(process.env.REDAKTYN_PASSPHRASE)||o.passphrase?.trim()||"";t||(console.error(`${e.red}Error: set REDAKTYN_PASSPHRASE env var or use --passphrase.${e.reset}`),process.exit(1));const s=await H(t,r);if(o.batch){const d=te(o.batch);d.length===0&&(console.error(`${e.red}Error: batch file is empty or has no valid entries.${e.reset}`),process.exit(1));const $=d.map(({label:p,secret:h})=>S(p,h,s,r));D($,o.json);return}const n=o.label?.trim(),l=o.secret;n||(console.error(`${e.red}Error: --label is required in non-interactive mode.${e.reset}`),process.exit(1));let i=l;i==null&&(process.stdin.isTTY||(i=await new Promise((d,$)=>{let p="";process.stdin.setEncoding("utf8"),process.stdin.on("data",h=>p+=h),process.stdin.on("end",()=>d(p.trim())),process.stdin.on("error",$)}))),i||(console.error(`${e.red}Error: --secret is required (or pipe on stdin).${e.reset}`),process.exit(1)),T(S(n,i,s,r),o.json)}async function le(){const o=G(process.argv);o.help&&(re(),process.exit(0)),Z(o),process.stdin.isTTY&&!o.orgId&&!o.secret&&!o.batch&&!o.label&&!process.env.REDAKTYN_PASSPHRASE?await se(o):await ne(o)}function ie(){try{return F(process.argv[1])}catch{return process.argv[1]}}const ae=!!process.argv[1]&&import.meta.url===K(ie()).href;ae&&le().catch(o=>{console.error(`${e.red}Fatal: ${o instanceof Error?o.message:o}${e.reset}`),process.exit(1)});export{b as PBKDF2_ITERATIONS,
|
|
83
|
+
`),console.log(c("\xB7")),console.log(` ${e.bold}${e.magenta}STEP 1 \u2014 Organization ID${e.reset}`),console.log(` ${e.gray}Find this in: Dashboard \u2192 Settings \u2192 Organization Details${e.reset}`),console.log();let r=o.orgId?.trim()??"";if(!r||!w.test(r))for(r=await f("Org ID (UUID format, e.g. a1b2c3d4-\u2026):");!w.test(r.trim());)u("That doesn't look like a valid UUID. Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"),r=await f("Org ID:");else I("Org ID",r);r=r.trim(),console.log(),console.log(c("\xB7")),console.log(` ${e.bold}${e.magenta}STEP 2 \u2014 Organization Passphrase${e.reset}`),console.log(` ${e.gray}The same passphrase your team uses when connecting the extension.${e.reset}`),console.log(` ${e.gray}Tip: set REDAKTYN_PASSPHRASE in your shell to avoid retyping.${e.reset}`),console.log();let t=(process.env.REDAKTYN_PASSPHRASE&&String(process.env.REDAKTYN_PASSPHRASE))??"";if(t)I("Passphrase","\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (loaded from REDAKTYN_PASSPHRASE)");else for(t=await x("Passphrase (hidden):");!t||t.length<8;)u("Passphrase must be at least 8 characters."),t=await x("Passphrase:");const s=[];let n=!0,l=0;for(;n;){l++,console.log(),console.log(c("\xB7")),console.log(` ${e.bold}${e.magenta}STEP ${l===1?"3":l+2} \u2014 Secret #${l}${e.reset}`),console.log();const d=await f(`Label (human-readable name, e.g. AWS_SECRET_KEY${l>1,""}):`);if(!d){u("Label cannot be empty \u2014 skipping.");continue}const $=await x("Secret value (hidden):");if(!$){u("Secret cannot be empty \u2014 skipping.");continue}console.log(),console.log(` ${e.dim}\u2500\u2500 Computing fingerprint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${e.reset}`);const p=ee("Deriving HMAC key via PBKDF2-SHA256 (600,000 iterations)\u2026");let h;try{h=await H(t,r),p.stop("HMAC key derived "+e.gray+"(256-bit, browser-identical)"+e.reset)}catch(N){p.stop(),console.error(`${e.red}Key derivation failed: ${N.message}${e.reset}`),process.exit(1)}const g=S(d,$,h,r);console.log(),console.log(` ${e.dim}Step 1${e.reset} ${e.gray}NFC normalize + trim + UUID case-fold${e.reset}`),console.log(` ${e.gray}canonical form computed (${g.canonical.length} chars) \u2014 never printed${e.reset}`),console.log(` ${e.dim}Step 2${e.reset} ${e.gray}HMAC-SHA256(canonical, derivedKey)${e.reset}`),console.log(` ${e.yellow}${g.hmacHex.slice(0,16)}\u2026${g.hmacHex.slice(-8)}${e.reset} ${e.gray}(64-char hex, not reversible)${e.reset}`),console.log(` ${e.dim}Step 3${e.reset} ${e.gray}Two bypass variants: whitespace-collapsed and lowercased${e.reset}`),console.log(` ${e.gray}${g.hmacHexStripped.slice(0,16)}\u2026 \xB7 ${g.hmacHexLower.slice(0,16)}\u2026${e.reset}`),console.log(` ${e.dim}Step 4${e.reset} ${e.gray}Length recorded: ${g.length} chars${e.reset}`),s.push(g),console.log(),n=await oe("Add another secret with the same passphrase?")}const i=o.json;s.length===1?T(s[0],i):D(s,i),i||(console.log(` ${e.bold}${e.cyan}Next steps:${e.reset}`),console.log(` ${e.gray}1. Copy the JSON above${e.reset}`),console.log(` ${e.gray}2. Dashboard \u2192 Secrets \u2192 "Import Digest" button${e.reset}`),console.log(` ${e.gray}3. Paste the JSON and click Add${e.reset}`),console.log(` ${e.gray}4. Extensions pick it up on next sync (\u22645 min)${e.reset}`),console.log(),console.log(` ${e.dim}Your secret value was used only in-memory and is now gone.${e.reset}`),console.log())}async function ne(o){const r=o.orgId?.trim();(!r||!w.test(r))&&(console.error(`${e.red}Error: --org-id must be a valid UUID.${e.reset}`),console.error(" Find it in: Dashboard \u2192 Settings \u2192 Organization Details"),process.exit(1));const t=process.env.REDAKTYN_PASSPHRASE&&String(process.env.REDAKTYN_PASSPHRASE)||o.passphrase?.trim()||"";t||(console.error(`${e.red}Error: set REDAKTYN_PASSPHRASE env var or use --passphrase.${e.reset}`),process.exit(1));const s=await H(t,r);if(o.batch){const d=te(o.batch);d.length===0&&(console.error(`${e.red}Error: batch file is empty or has no valid entries.${e.reset}`),process.exit(1));const $=d.map(({label:p,secret:h})=>S(p,h,s,r));D($,o.json);return}const n=o.label?.trim(),l=o.secret;n||(console.error(`${e.red}Error: --label is required in non-interactive mode.${e.reset}`),process.exit(1));let i=l;i==null&&(process.stdin.isTTY||(i=await new Promise((d,$)=>{let p="";process.stdin.setEncoding("utf8"),process.stdin.on("data",h=>p+=h),process.stdin.on("end",()=>d(p.trim())),process.stdin.on("error",$)}))),i||(console.error(`${e.red}Error: --secret is required (or pipe on stdin).${e.reset}`),process.exit(1)),T(S(n,i,s,r),o.json)}async function le(){const o=G(process.argv);o.help&&(re(),process.exit(0)),Z(o),process.stdin.isTTY&&!o.orgId&&!o.secret&&!o.batch&&!o.label&&!process.env.REDAKTYN_PASSPHRASE?await se(o):await ne(o)}function ie(){try{return F(process.argv[1])}catch{return process.argv[1]}}const ae=!!process.argv[1]&&import.meta.url===K(ie()).href;ae&&le().catch(o=>{console.error(`${e.red}Fatal: ${o instanceof Error?o.message:o}${e.reset}`),process.exit(1)});export{b as PBKDF2_ITERATIONS,z as PROJECT_KDF_LABEL,M as canonicalize,q as computeHmac,H as deriveKey,$e as deriveProjectKey,m as enrollPayload,S as fingerprint,J as scanPrefilter};
|