@redaktyn/cli 1.5.7 → 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 CHANGED
@@ -35,11 +35,10 @@ This CLI is for you.
35
35
 
36
36
  ### Interactive wizard (recommended for first-timers)
37
37
 
38
- From the **`cli`** directory (or invoke by path):
38
+ Run it (after `npm install -g @redaktyn/cli`, or as `npx -p @redaktyn/cli redaktyn-fingerprint`):
39
39
 
40
40
  ```bash
41
- cd cli
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
- cd cli && node redaktyn-fingerprint.mjs \
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
- cd cli && node redaktyn-fingerprint.mjs \
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
- node cli/redaktyn-fingerprint.mjs --org-id ... --label ... --secret ... --json
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
- node cli/redaktyn-import.mjs --env .env.production --source prod
260
+ redaktyn-import --env .env.production --source prod
266
261
 
267
262
  # 2. Happy with the plan? Enroll it.
268
- node cli/redaktyn-import.mjs --env .env.production --source prod --apply
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 | node cli/redaktyn-import.mjs --stdin --source vault
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 | node cli/redaktyn-import.mjs --stdin --source aws
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
- | node cli/redaktyn-import.mjs --stdin --source aws
301
+ | redaktyn-import --stdin --source aws
307
302
 
308
303
  # Doppler
309
304
  doppler secrets download --no-file --format json \
310
- | node cli/redaktyn-import.mjs --stdin --source doppler
305
+ | redaktyn-import --stdin --source doppler
311
306
 
312
307
  # 1Password
313
- op item get prod --format json | node cli/redaktyn-import.mjs --stdin --source 1password
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
- | node cli/redaktyn-import.mjs --stdin --source doppler --apply --yes
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
- Reinstall the package: `npm install -g @redaktyn/cli`, or run a command with `npx -p @redaktyn/cli`.
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
- # 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"
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…" # extension JWT from popup → Connect, or admin JWT for CI
514
+ export REDAKTYN_DEVICE_TOKEN="eyJ…" # an OWNER/ADMIN token, for CI
521
515
 
522
516
  # Scan staged changes before committing
523
- node cli/redaktyn-scan.mjs --staged
517
+ redaktyn-scan --staged
524
518
 
525
519
  # Install permanent git hook (runs automatically on every commit)
526
- node cli/redaktyn-scan.mjs install-hooks
520
+ redaktyn-scan install-hooks
527
521
 
528
522
  # Remove the hook
529
- node cli/redaktyn-scan.mjs uninstall-hooks
523
+ redaktyn-scan uninstall-hooks
530
524
 
531
525
  # Scan a single file
532
- node cli/redaktyn-scan.mjs --file ./src/config.ts
526
+ redaktyn-scan --file ./src/config.ts
533
527
 
534
528
  # Scan all text files under a directory
535
- node cli/redaktyn-scan.mjs --dir ./src --exclude "*.min.js"
529
+ redaktyn-scan --dir ./src --exclude "*.min.js"
536
530
 
537
531
  # Check connection and enrolled secret count
538
- node cli/redaktyn-scan.mjs status --json
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
- node cli/redaktyn-scan.mjs --staged --json
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://your-api.example.com/api/verify/cli
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://your-api.example.com"
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.7",
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": {
@@ -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 j="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(j,"utf8"),v))}function R(o){const r=o.normalize("NFC").replace(/\r\n/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 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 z(o){return R(o).toLowerCase()}function y(o,r){return A("sha256",r).update(o,"utf8").digest("hex")}const B="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(B,"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=z(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' node redaktyn-fingerprint.mjs \\"),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" | node redaktyn-fingerprint.mjs \\`),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(" node redaktyn-fingerprint.mjs"),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)+`
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
- node redaktyn-fingerprint.mjs
17
+ redaktyn-fingerprint
18
18
 
19
19
  ${e.green}Non-interactive (scripting / CI):${e.reset}
20
- REDAKTYN_PASSPHRASE="..." node redaktyn-fingerprint.mjs \\
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="..." node redaktyn-fingerprint.mjs \\
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
- node redaktyn-fingerprint.mjs --org-id <uuid> --label MY_KEY
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,j 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};
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};
@@ -1,25 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as me,realpathSync as ye}from"node:fs";import{basename as W}from"node:path";import*as $e from"node:readline";import{pathToFileURL as we}from"node:url";import{canonicalize as ke,computeHmac as ve,deriveKey as be,deriveProjectKey as Se,enrollPayload as Re}from"./redaktyn-fingerprint.mjs";import{freshAdminSession as Ne,freshSession as je,loadAdminSession as U,loadSession as Ae,resolveServerUrl as Ee}from"./redaktyn-session.mjs";import{askWorkspacePassphrase as xe}from"./redaktyn-passphrase.mjs";import{banner as Te,bar as Oe,box as B,g as C,humanBytes as z,k as g,rich as R,reveal as Le,rows as K,say as w,step as G}from"./redaktyn-ui.mjs";import{createRequire as Ie}from"node:module";const _e=Ie(import.meta.url)("./package.json").version,x=process.argv.includes("--no-color")||!!process.env.NO_COLOR,s={reset:x?"":"\x1B[0m",bold:x?"":"\x1B[1m",dim:x?"":"\x1B[2m",red:x?"":"\x1B[31m",green:x?"":"\x1B[32m",yellow:x?"":"\x1B[33m",blue:x?"":"\x1B[34m",cyan:x?"":"\x1B[36m",gray:x?"":"\x1B[90m"};class m extends Error{}const V=15e5,Z=5e4,Pe=1e4,D=5*1024*1024,Ce=256,De=65536,He=/^v1\.[0-9a-f]{12}$/,X=e=>typeof e.scanPrefilter=="string"&&He.test(e.scanPrefilter);function Q(e,t=!1){const o=[new Map,new Map,new Map],r=(n,i,h)=>{if(!(i>0))return;const d=o[n].get(i);o[n].set(i,d?{windowLen:i,prefiltered:d.prefiltered&&h,entries:d.entries+1}:{windowLen:i,prefiltered:h,entries:1})};for(const n of e){const i=t||X(n);n.hmacHex&&r(0,n.length,i),typeof n.hmacHexStripped=="string"&&r(1,n.lengthStripped,i),typeof n.hmacHexLower=="string"&&r(2,n.lengthLower,i)}return o.flatMap(n=>[...n.values()])}function ee(e,t){let o=0;for(const r of t){const n=Math.max(0,e-r.windowLen+1);o+=r.prefiltered?Math.ceil(n/Ce)+Math.ceil(n*r.entries/De):n}return o}function te(e){if(e.length===0)return 1/0;if(ee(D,e)<=V)return D;let t=0,o=D;for(;t<o;){const r=Math.ceil((t+o)/2);ee(r,e)<=V?t=r:o=r-1}return t}function Me(e){const t=Q(e),o=te(t);return{windowLengths:t.length,ceiling:o,legacySecrets:e.filter(r=>r.hmacHex&&!X(r)).length,ceilingIfReenrolled:te(Q(e,!0)),atCharacterCap:o===D}}function We(e){return e.length===0||e.some(t=>t&&Object.prototype.hasOwnProperty.call(t,"scanPrefilter"))}function Ke(e){const t=e.data&&Array.isArray(e.data.unknownFields)?e.data.unknownFields:null;return e.status===400&&e.data?.code==="UNKNOWN_FIELD"&&t!==null&&t.length===1&&t[0]==="scanPrefilter"}function Ye(e){const t={help:!1,env:null,jsonFile:null,yaml:null,stdin:!1,source:null,apply:!1,yes:!1,minLength:null,allowWeak:!1,only:[],exclude:[],server:null,orgId:null,project:null,reportJson:!1,showRequest:!1,noColor:!1},o=(r,n)=>{const i=e[r];if(i===void 0||i.startsWith("--"))throw new m(`${n} needs a value`);return i};for(let r=2;r<e.length;r++){const n=e[r];if(n==="--help"||n==="-h")t.help=!0;else if(n==="--env")t.env=o(++r,"--env");else if(n==="--json")t.jsonFile=o(++r,"--json");else if(n==="--yaml")t.yaml=o(++r,"--yaml");else if(n==="--stdin")t.stdin=!0;else if(n==="--source")t.source=o(++r,"--source");else if(n==="--apply")t.apply=!0;else if(n==="--yes"||n==="-y")t.yes=!0;else if(n==="--min-length")t.minLength=Number(o(++r,"--min-length"));else if(n==="--allow-weak")t.allowWeak=!0;else if(n==="--only")t.only.push(o(++r,"--only"));else if(n==="--exclude")t.exclude.push(o(++r,"--exclude"));else if(n==="--server")t.server=o(++r,"--server");else if(n==="--org-id")t.orgId=o(++r,"--org-id");else if(n==="--project")t.project=o(++r,"--project");else if(n==="--report-json")t.reportJson=!0;else if(n==="--show-request")t.showRequest=!0;else if(n==="--no-color")t.noColor=!0;else throw new m(`Unknown flag: ${n}`)}if(t.minLength!==null&&(!Number.isInteger(t.minLength)||t.minLength<1))throw new m("--min-length must be a positive integer");return t}const Je=/^[A-Za-z_][A-Za-z0-9_.-]*$/;function Fe(e){const t=e.replace(/\r\n/g,`
2
+ import{readFileSync as me,realpathSync as ye}from"node:fs";import{basename as K}from"node:path";import*as $e from"node:readline";import{pathToFileURL as we}from"node:url";import{canonicalize as ke,computeHmac as ve,deriveKey as be,deriveProjectKey as Se,enrollPayload as je}from"./redaktyn-fingerprint.mjs";import{freshAdminSession as Re,freshSession as Ae,loadAdminSession as U,loadSession as Ee,resolveServerUrl as Ne}from"./redaktyn-session.mjs";import{askWorkspacePassphrase as xe}from"./redaktyn-passphrase.mjs";import{banner as Te,bar as Oe,box as B,g as C,humanBytes as z,k as g,rich as j,reveal as Ie,rows as J,say as w,step as G}from"./redaktyn-ui.mjs";import{createRequire as Le}from"node:module";const _e=Le(import.meta.url)("./package.json").version,x=process.argv.includes("--no-color")||!!process.env.NO_COLOR,s={reset:x?"":"\x1B[0m",bold:x?"":"\x1B[1m",dim:x?"":"\x1B[2m",red:x?"":"\x1B[31m",green:x?"":"\x1B[32m",yellow:x?"":"\x1B[33m",blue:x?"":"\x1B[34m",cyan:x?"":"\x1B[36m",gray:x?"":"\x1B[90m"};class m extends Error{}const V=15e5,Z=5e4,Pe=1e4,D=5*1024*1024,Ce=256,De=65536,He=/^v1\.[0-9a-f]{12}$/,X=e=>typeof e.scanPrefilter=="string"&&He.test(e.scanPrefilter);function Q(e,t=!1){const o=[new Map,new Map,new Map],r=(n,i,h)=>{if(!(i>0))return;const a=o[n].get(i);o[n].set(i,a?{windowLen:i,prefiltered:a.prefiltered&&h,entries:a.entries+1}:{windowLen:i,prefiltered:h,entries:1})};for(const n of e){const i=t||X(n);n.hmacHex&&r(0,n.length,i),typeof n.hmacHexStripped=="string"&&r(1,n.lengthStripped,i),typeof n.hmacHexLower=="string"&&r(2,n.lengthLower,i)}return o.flatMap(n=>[...n.values()])}function ee(e,t){let o=0;for(const r of t){const n=Math.max(0,e-r.windowLen+1);o+=r.prefiltered?Math.ceil(n/Ce)+Math.ceil(n*r.entries/De):n}return o}function te(e){if(e.length===0)return 1/0;if(ee(D,e)<=V)return D;let t=0,o=D;for(;t<o;){const r=Math.ceil((t+o)/2);ee(r,e)<=V?t=r:o=r-1}return t}function Me(e){const t=Q(e),o=te(t);return{windowLengths:t.length,ceiling:o,legacySecrets:e.filter(r=>r.hmacHex&&!X(r)).length,ceilingIfReenrolled:te(Q(e,!0)),atCharacterCap:o===D}}function We(e){return e.length===0||e.some(t=>t&&Object.prototype.hasOwnProperty.call(t,"scanPrefilter"))}function Ke(e){const t=e.data&&Array.isArray(e.data.unknownFields)?e.data.unknownFields:null;return e.status===400&&e.data?.code==="UNKNOWN_FIELD"&&t!==null&&t.length===1&&t[0]==="scanPrefilter"}function Je(e){const t={help:!1,env:null,jsonFile:null,yaml:null,stdin:!1,source:null,apply:!1,yes:!1,minLength:null,allowWeak:!1,only:[],exclude:[],server:null,orgId:null,project:null,reportJson:!1,showRequest:!1,noColor:!1},o=(r,n)=>{const i=e[r];if(i===void 0||i.startsWith("--"))throw new m(`${n} needs a value`);return i};for(let r=2;r<e.length;r++){const n=e[r];if(n==="--help"||n==="-h")t.help=!0;else if(n==="--env")t.env=o(++r,"--env");else if(n==="--json")t.jsonFile=o(++r,"--json");else if(n==="--yaml")t.yaml=o(++r,"--yaml");else if(n==="--stdin")t.stdin=!0;else if(n==="--source")t.source=o(++r,"--source");else if(n==="--apply")t.apply=!0;else if(n==="--yes"||n==="-y")t.yes=!0;else if(n==="--min-length")t.minLength=Number(o(++r,"--min-length"));else if(n==="--allow-weak")t.allowWeak=!0;else if(n==="--only")t.only.push(o(++r,"--only"));else if(n==="--exclude")t.exclude.push(o(++r,"--exclude"));else if(n==="--server")t.server=o(++r,"--server");else if(n==="--org-id")t.orgId=o(++r,"--org-id");else if(n==="--project")t.project=o(++r,"--project");else if(n==="--report-json")t.reportJson=!0;else if(n==="--show-request")t.showRequest=!0;else if(n==="--no-color")t.noColor=!0;else throw new m(`Unknown flag: ${n}`)}if(t.minLength!==null&&(!Number.isInteger(t.minLength)||t.minLength<1))throw new m("--min-length must be a positive integer");return t}const Ye=/^[A-Za-z_][A-Za-z0-9_.-]*$/;function Fe(e){const t=e.replace(/\r\n/g,`
3
3
  `).replace(/\r/g,`
4
4
  `),o=[],r=[];let n=0,i=1;const h=()=>{const c=t.indexOf(`
5
- `,n);n=c===-1?t.length:c+1},d=(c,p)=>{let a=0;for(let l=c;l<p;l++)t[l]===`
6
- `&&a++;return a};for(;n<t.length;){const c=t[n];if(c===`
7
- `){i++,n++;continue}if(c===" "||c===" "){n++;continue}if(c==="#"){h(),i++;continue}const p=i;let a=n;for(;a<t.length&&t[a]!=="="&&t[a]!==`
8
- `;)a++;if(a>=t.length||t[a]===`
9
- `){r.push({line:p,message:"no '=' on this line \u2014 skipped"}),n=a+1,i++;continue}let l=t.slice(n,a).trim();for(/^export\s/.test(l)&&(l=l.replace(/^export\s+/,"").trim()),n=a+1;n<t.length&&(t[n]===" "||t[n]===" ");)n++;let y="",u=!0;const $=t[n];if($==='"'||$==="'"){const S=++n,k=t.indexOf(`
10
- `,n);let v=!1;for(;n<t.length;){const b=t[n];if(b==="\\"&&$==='"'&&n+1<t.length){const A=t[n+1];y+=A==="n"?`
11
- `:A==="t"?" ":A==="r"?"\r":A,n+=2;continue}if(b===$){n++,v=!0;break}y+=b,n++}v?(i+=d(S,n),h(),i++):(r.push({line:p,message:"unterminated quote \u2014 skipped this entry"}),u=!1,n=k===-1?t.length:k+1,i++)}else{const S=t.indexOf(`
12
- `,n),k=S===-1?t.length:S;let v=t.slice(n,k);const b=v.search(/\s#/);b!==-1&&(v=v.slice(0,b)),y=v.trim(),n=k===t.length?k:k+1,i++}if(u){if(!Je.test(l)){r.push({line:p,message:`unusable key name ${JSON.stringify(l)} \u2014 skipped`});continue}o.push({key:l,value:y,line:p})}}return{entries:o,problems:r}}const qe=new Set(["data","metadata","lease_id","lease_duration","lease_renewable","renewable","request_id","warnings","auth","wrap_info","mount_type"]),I=e=>typeof e=="object"&&e!==null&&!Array.isArray(e);function ne(e){if(!I(e))throw new m("Expected a JSON object of secrets, got "+re(e));if(typeof e.SecretString=="string"){let r;try{r=JSON.parse(e.SecretString)}catch{throw new m("SecretString is not JSON. For a single-value secret pass it to redaktyn-fingerprint instead.")}return ne(r)}if(Array.isArray(e.fields)){const r={};for(const n of e.fields){if(!I(n))continue;const i=n.label??n.id;typeof i!="string"||typeof n.value!="string"||(r[i]=n.value)}return{shape:"1password-item",doc:r}}const t=Object.keys(e);if(t.length>0&&t.every(r=>qe.has(r))&&I(e.data)){const r=e.data;return I(r.data)&&"metadata"in r?{shape:"vault-kv2",doc:r.data}:{shape:"vault-kv1",doc:r}}return{shape:"flat",doc:e}}function re(e){return e===null?"null":Array.isArray(e)?"an array":typeof e}function Ue(e){const t=[],o=[];for(const[r,n]of Object.entries(e)){if(typeof n=="string"){t.push({key:r,value:n});continue}if(typeof n=="number"||typeof n=="boolean"){t.push({key:r,value:String(n)});continue}if(I(n)){const i=["value","raw","computed","computedValue"].find(h=>typeof n[h]=="string");if(i){t.push({key:r,value:n[i]});continue}o.push({key:r,message:"nested object with no value/raw/computed field \u2014 skipped"});continue}o.push({key:r,message:`${re(n)} is not a secret value \u2014 skipped`})}return{entries:t,problems:o}}function oe(e){let t;try{t=JSON.parse(e)}catch(h){throw new m(`Input is not valid JSON: ${h.message}`)}const{shape:o,doc:r}=ne(t),{entries:n,problems:i}=Ue(r);return{entries:n,problems:i,shape:o}}function Be(e){const t=[],o=[];return e.replace(/\r\n/g,`
5
+ `,n);n=c===-1?t.length:c+1},a=(c,p)=>{let l=0;for(let d=c;d<p;d++)t[d]===`
6
+ `&&l++;return l};for(;n<t.length;){const c=t[n];if(c===`
7
+ `){i++,n++;continue}if(c===" "||c===" "){n++;continue}if(c==="#"){h(),i++;continue}const p=i;let l=n;for(;l<t.length&&t[l]!=="="&&t[l]!==`
8
+ `;)l++;if(l>=t.length||t[l]===`
9
+ `){r.push({line:p,message:"no '=' on this line \u2014 skipped"}),n=l+1,i++;continue}let d=t.slice(n,l).trim();for(/^export\s/.test(d)&&(d=d.replace(/^export\s+/,"").trim()),n=l+1;n<t.length&&(t[n]===" "||t[n]===" ");)n++;let y="",u=!0;const $=t[n];if($==='"'||$==="'"){const S=++n,k=t.indexOf(`
10
+ `,n);let b=!1;for(;n<t.length;){const v=t[n];if(v==="\\"&&$==='"'&&n+1<t.length){const E=t[n+1];y+=E==="n"?`
11
+ `:E==="t"?" ":E==="r"?"\r":E,n+=2;continue}if(v===$){n++,b=!0;break}y+=v,n++}b?(i+=a(S,n),h(),i++):(r.push({line:p,message:"unterminated quote \u2014 skipped this entry"}),u=!1,n=k===-1?t.length:k+1,i++)}else{const S=t.indexOf(`
12
+ `,n),k=S===-1?t.length:S;let b=t.slice(n,k);const v=b.search(/\s#/);v!==-1&&(b=b.slice(0,v)),y=b.trim(),n=k===t.length?k:k+1,i++}if(u){if(!Ye.test(d)){r.push({line:p,message:`unusable key name ${JSON.stringify(d)} \u2014 skipped`});continue}o.push({key:d,value:y,line:p})}}return{entries:o,problems:r}}const qe=new Set(["data","metadata","lease_id","lease_duration","lease_renewable","renewable","request_id","warnings","auth","wrap_info","mount_type"]),L=e=>typeof e=="object"&&e!==null&&!Array.isArray(e);function ne(e){if(!L(e))throw new m("Expected a JSON object of secrets, got "+re(e));if(typeof e.SecretString=="string"){let r;try{r=JSON.parse(e.SecretString)}catch{throw new m("SecretString is not JSON. For a single-value secret pass it to redaktyn-fingerprint instead.")}return ne(r)}if(Array.isArray(e.fields)){const r={};for(const n of e.fields){if(!L(n))continue;const i=n.label??n.id;typeof i!="string"||typeof n.value!="string"||(r[i]=n.value)}return{shape:"1password-item",doc:r}}const t=Object.keys(e);if(t.length>0&&t.every(r=>qe.has(r))&&L(e.data)){const r=e.data;return L(r.data)&&"metadata"in r?{shape:"vault-kv2",doc:r.data}:{shape:"vault-kv1",doc:r}}return{shape:"flat",doc:e}}function re(e){return e===null?"null":Array.isArray(e)?"an array":typeof e}function Ue(e){const t=[],o=[];for(const[r,n]of Object.entries(e)){if(typeof n=="string"){t.push({key:r,value:n});continue}if(typeof n=="number"||typeof n=="boolean"){t.push({key:r,value:String(n)});continue}if(L(n)){const i=["value","raw","computed","computedValue"].find(h=>typeof n[h]=="string");if(i){t.push({key:r,value:n[i]});continue}o.push({key:r,message:"nested object with no value/raw/computed field \u2014 skipped"});continue}o.push({key:r,message:`${re(n)} is not a secret value \u2014 skipped`})}return{entries:t,problems:o}}function oe(e){let t;try{t=JSON.parse(e)}catch(h){throw new m(`Input is not valid JSON: ${h.message}`)}const{shape:o,doc:r}=ne(t),{entries:n,problems:i}=Ue(r);return{entries:n,problems:i,shape:o}}function Be(e){const t=[],o=[];return e.replace(/\r\n/g,`
13
13
  `).replace(/\r/g,`
14
14
  `).split(`
15
- `).forEach((n,i)=>{const h=i+1;if(n.trim()===""||n.trim().startsWith("#")||n.trim()==="---"||n.trim()==="...")return;if(/^\s/.test(n))throw new m(`${h}: this YAML is indented, so it is not flat key/value. Refusing rather than guessing \u2014 convert it to JSON and use --json.`);if(/^-\s/.test(n.trim())||n.trim()==="-")throw new m(`${h}: YAML lists are not supported. Use --json.`);if(/^[|>]/.test(n.trim()))throw new m(`${h}: YAML block scalars are not supported. Use --json.`);const d=/^([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s?(.*)$/.exec(n);if(!d){o.push({line:h,message:"not a `key: value` line \u2014 skipped"});return}const c=d[1];let p=d[2]??"";if(p.trim()===""||p.trim()==="|"||p.trim()===">")throw new m(`${h}: "${c}" has no inline value, which in YAML means a nested block or null. Refusing rather than registering an empty fingerprint.`);const a=p.trim();if(a.startsWith('"')&&a.endsWith('"')&&a.length>1||a.startsWith("'")&&a.endsWith("'")&&a.length>1)p=a.slice(1,-1),a[0]==='"'&&(p=p.replace(/\\n/g,`
16
- `).replace(/\\t/g," ").replace(/\\(.)/g,"$1"));else{const l=a.search(/\s#/);p=(l===-1?a:a.slice(0,l)).trim()}t.push({key:c,value:p,line:h})}),{entries:t,problems:o}}const Y=12,ze=new Set(["changeme","change-me","change_me","replaceme","replace-me","your-secret-here","your_secret_here","yoursecrethere","development","production","staging","localhost","password","passw0rd","secret","topsecret","undefined","none","null","n/a","todo","tbd","example","test","dummy","placeholder"]);function se(e){if(e.length===0)return 0;const t=new Map;for(const r of e)t.set(r,(t.get(r)??0)+1);let o=0;for(const r of t.values()){const n=r/e.length;o-=n*Math.log2(n)}return o}function Ge(e){if(!/:\/\//.test(e))return null;let t;try{t=new URL(e)}catch{return null}return t.password?{ok:!0}:[...t.pathname.split("/"),...t.searchParams.values(),t.hash.replace(/^#/,"")].filter(Boolean).some(r=>r.length>=16&&se(r)>=2.5)?{ok:!0}:{ok:!1,reason:"URL with no embedded credential or token"}}function Ve(e,{minLength:t=Y}={}){if(e.length===0)return{ok:!1,reason:"empty value"};if(e.length<t)return{ok:!1,reason:`only ${e.length} chars (minimum ${t})`};if(/^\d+$/.test(e))return{ok:!1,reason:"all digits \u2014 port, id or timestamp"};if(/^(true|false|yes|no|on|off|null|nil|none|undefined|enabled|disabled)$/i.test(e))return{ok:!1,reason:"boolean or empty-ish flag"};if(/^v?\d+(\.\d+){1,3}([-+][0-9A-Za-z.-]+)?$/.test(e))return{ok:!1,reason:"version number"};if(ze.has(e.toLowerCase()))return{ok:!1,reason:"placeholder or environment name"};if(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(e))return{ok:!1,reason:"email address"};if(/^(\/[^\s]*|\.{1,2}\/[^\s]*|~\/[^\s]*|[A-Za-z]:[\\/][^\s]*)$/.test(e))return{ok:!1,reason:"filesystem path"};const o=Ge(e);if(o&&!o.ok)return{ok:!1,reason:o.reason};if(!o){const r=new Set(e).size;if(r<5)return{ok:!1,reason:`only ${r} distinct characters`};if(se(e)<2)return{ok:!1,reason:"very low character diversity"}}return{ok:!0}}async function Ze(){if(process.stdin.isTTY)throw new m("--stdin was passed but nothing is piped in.");const e=[];for await(const t of process.stdin)e.push(t);return Buffer.concat(e).toString("utf8")}function J(e,t){try{return me(e,"utf8")}catch(o){throw new m(`${t} ${e}: ${o.code==="ENOENT"?"no such file":o.message}`)}}function O(e){return`import:${e}`}function Xe(e){return String(e).replace(/^[./]+/,"").replace(/[^A-Za-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"import"}function ie(e,t){return t.some(o=>o.includes("*")?new RegExp("^"+o.split("*").map(Qe).join(".*")+"$","i").test(e):e.toLowerCase()===o.toLowerCase())}const Qe=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function et({candidates:e,existing:t,source:o}){const r=O(o),n=new Map;for(const l of t)l.hmacHex&&n.set(l.hmacHex,l);const i=t.filter(l=>Array.isArray(l.tags)&&l.tags.includes(r)),h=new Map(i.map(l=>[l.label,l])),d=new Map;for(const l of t)i.includes(l)||d.has(l.label)||d.set(l.label,l);const c=[],p=new Set;for(const l of e){const{key:y,digests:u}=l;p.add(y);const $=h.get(y);if($){if($.hmacHex===u.digest&&tt($,u))c.push({key:y,action:"skip",reason:"already registered and unchanged",id:$.id,length:u.length});else{const v=n.get(u.digest);if(v&&v.id!==$.id)c.push({key:y,action:"conflict",reason:`new value is already registered as "${v.label}"`,id:$.id,length:u.length});else{const b=$.hmacHex===u.digest?"value unchanged; adding missing variants":"value changed";c.push({key:y,action:"rotate",reason:b,id:$.id,length:u.length})}}continue}const S=n.get(u.digest);if(S){c.push({key:y,action:"skip",reason:`already registered as "${S.label}"`,id:S.id,length:u.length});continue}const k=d.get(y);if(k){c.push({key:y,action:"conflict",reason:"a secret with this label exists outside this import source",id:k.id,length:u.length});continue}c.push({key:y,action:"create",reason:"new",length:u.length})}const a=i.filter(l=>!p.has(l.label)).map(l=>({label:l.label,id:l.id,status:l.status}));return{plan:c,missing:a}}function tt(e,t){return e.hmacHexStripped===t.hmacHexStripped&&e.hmacHexLower===t.hmacHexLower&&e.scanPrefilter===t.scanPrefilter}const ae=e=>e.replace(/\/+$/,"");async function F(e,t,o,r,n){let i;try{i=await fetch(`${ae(e)}${r}`,{method:o,headers:{Authorization:`Bearer ${t}`,...n?{"Content-Type":"application/json"}:{}},...n?{body:JSON.stringify(n)}:{}})}catch(c){throw new m(`Cannot reach ${ae(e)}: ${c.message}`)}const h=await i.text();let d=null;if(h)try{d=JSON.parse(h)}catch{d=null}return{status:i.status,ok:i.ok,data:d,text:h}}async function nt(e,t,o){const r=await F(e,t,"GET","/api/projects");if(!r.ok||!Array.isArray(r.data))throw new m(`GET /api/projects failed: ${r.status} ${String(r.text).slice(0,200)}`);const n=o.trim().toLowerCase(),i=r.data.filter(d=>d.id===o.trim()),h=i.length?i:r.data.filter(d=>String(d.name).toLowerCase()===n);if(h.length===0)throw new m(`No project named or numbered "${o}". Known: ${r.data.map(d=>d.name).join(", ")||"none"}.`);if(h.length>1)throw new m(`"${o}" matches ${h.length} projects. Pass the project id instead.`);if(h[0].status==="CLOSED")throw new m(`Project "${h[0].name}" is closed. Reopen it before enrolling into it.`);return h[0]}async function rt(e,t,o=!1){const r=await F(e,t,"GET","/api/secrets");if(o&&(r.status===401||r.status===403))throw new m("This machine's Redaktyn session is no longer valid: the device was removed or its session ended. Ask your admin for a new invite link and run `redaktyn-connect` again.");if(r.status===401)throw new m("Server rejected the token (401). REDAKTYN_TOKEN must be a current OWNER or ADMIN login token.");if(r.status===403)throw new m("This token is not allowed to manage secrets (403). Importing needs an OWNER or ADMIN token.");if(!r.ok)throw new m(`GET /api/secrets failed: ${r.status} ${r.text.slice(0,200)}`);if(!Array.isArray(r.data))throw new m("GET /api/secrets did not return a list.");return r.data}function ot(e){try{const t=e.split(".")[1];if(!t)return null;const o=t+"=".repeat((4-t.length%4)%4),r=Buffer.from(o.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8");return JSON.parse(r)}catch{return null}}function st(e){if(!e)return null;if(e.type==="extension")return`REDAKTYN_TOKEN is an extension device token. Those can sync fingerprints but not register them.
15
+ `).forEach((n,i)=>{const h=i+1;if(n.trim()===""||n.trim().startsWith("#")||n.trim()==="---"||n.trim()==="...")return;if(/^\s/.test(n))throw new m(`${h}: this YAML is indented, so it is not flat key/value. Refusing rather than guessing \u2014 convert it to JSON and use --json.`);if(/^-\s/.test(n.trim())||n.trim()==="-")throw new m(`${h}: YAML lists are not supported. Use --json.`);if(/^[|>]/.test(n.trim()))throw new m(`${h}: YAML block scalars are not supported. Use --json.`);const a=/^([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s?(.*)$/.exec(n);if(!a){o.push({line:h,message:"not a `key: value` line \u2014 skipped"});return}const c=a[1];let p=a[2]??"";if(p.trim()===""||p.trim()==="|"||p.trim()===">")throw new m(`${h}: "${c}" has no inline value, which in YAML means a nested block or null. Refusing rather than registering an empty fingerprint.`);const l=p.trim();if(l.startsWith('"')&&l.endsWith('"')&&l.length>1||l.startsWith("'")&&l.endsWith("'")&&l.length>1)p=l.slice(1,-1),l[0]==='"'&&(p=p.replace(/\\n/g,`
16
+ `).replace(/\\t/g," ").replace(/\\(.)/g,"$1"));else{const d=l.search(/\s#/);p=(d===-1?l:l.slice(0,d)).trim()}t.push({key:c,value:p,line:h})}),{entries:t,problems:o}}const Y=12,ze=new Set(["changeme","change-me","change_me","replaceme","replace-me","your-secret-here","your_secret_here","yoursecrethere","development","production","staging","localhost","password","passw0rd","secret","topsecret","undefined","none","null","n/a","todo","tbd","example","test","dummy","placeholder"]);function se(e){if(e.length===0)return 0;const t=new Map;for(const r of e)t.set(r,(t.get(r)??0)+1);let o=0;for(const r of t.values()){const n=r/e.length;o-=n*Math.log2(n)}return o}function Ge(e){if(!/:\/\//.test(e))return null;let t;try{t=new URL(e)}catch{return null}return t.password?{ok:!0}:[...t.pathname.split("/"),...t.searchParams.values(),t.hash.replace(/^#/,"")].filter(Boolean).some(r=>r.length>=16&&se(r)>=2.5)?{ok:!0}:{ok:!1,reason:"URL with no embedded credential or token"}}function Ve(e,{minLength:t=Y}={}){if(e.length===0)return{ok:!1,reason:"empty value"};if(e.length<t)return{ok:!1,reason:`only ${e.length} chars (minimum ${t})`};if(/^\d+$/.test(e))return{ok:!1,reason:"all digits \u2014 port, id or timestamp"};if(/^(true|false|yes|no|on|off|null|nil|none|undefined|enabled|disabled)$/i.test(e))return{ok:!1,reason:"boolean or empty-ish flag"};if(/^v?\d+(\.\d+){1,3}([-+][0-9A-Za-z.-]+)?$/.test(e))return{ok:!1,reason:"version number"};if(ze.has(e.toLowerCase()))return{ok:!1,reason:"placeholder or environment name"};if(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(e))return{ok:!1,reason:"email address"};if(/^(\/[^\s]*|\.{1,2}\/[^\s]*|~\/[^\s]*|[A-Za-z]:[\\/][^\s]*)$/.test(e))return{ok:!1,reason:"filesystem path"};const o=Ge(e);if(o&&!o.ok)return{ok:!1,reason:o.reason};if(!o){const r=new Set(e).size;if(r<5)return{ok:!1,reason:`only ${r} distinct characters`};if(se(e)<2)return{ok:!1,reason:"very low character diversity"}}return{ok:!0}}async function Ze(){if(process.stdin.isTTY)throw new m("--stdin was passed but nothing is piped in.");const e=[];for await(const t of process.stdin)e.push(t);return Buffer.concat(e).toString("utf8")}function F(e,t){try{return me(e,"utf8")}catch(o){throw new m(`${t} ${e}: ${o.code==="ENOENT"?"no such file":o.message}`)}}function O(e){return`import:${e}`}function Xe(e){return String(e).replace(/^[./]+/,"").replace(/[^A-Za-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"import"}function ie(e,t){return t.some(o=>o.includes("*")?new RegExp("^"+o.split("*").map(Qe).join(".*")+"$","i").test(e):e.toLowerCase()===o.toLowerCase())}const Qe=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function et({candidates:e,existing:t,source:o}){const r=O(o),n=new Map;for(const d of t)d.hmacHex&&n.set(d.hmacHex,d);const i=t.filter(d=>Array.isArray(d.tags)&&d.tags.includes(r)),h=new Map(i.map(d=>[d.label,d])),a=new Map;for(const d of t)i.includes(d)||a.has(d.label)||a.set(d.label,d);const c=[],p=new Set;for(const d of e){const{key:y,digests:u}=d;p.add(y);const $=h.get(y);if($){if($.hmacHex===u.digest&&tt($,u))c.push({key:y,action:"skip",reason:"already registered and unchanged",id:$.id,length:u.length});else{const b=n.get(u.digest);if(b&&b.id!==$.id)c.push({key:y,action:"conflict",reason:`new value is already registered as "${b.label}"`,id:$.id,length:u.length});else{const v=$.hmacHex===u.digest?"value unchanged; adding missing variants":"value changed";c.push({key:y,action:"rotate",reason:v,id:$.id,length:u.length})}}continue}const S=n.get(u.digest);if(S){c.push({key:y,action:"skip",reason:`already registered as "${S.label}"`,id:S.id,length:u.length});continue}const k=a.get(y);if(k){c.push({key:y,action:"conflict",reason:"a secret with this label exists outside this import source",id:k.id,length:u.length});continue}c.push({key:y,action:"create",reason:"new",length:u.length})}const l=i.filter(d=>!p.has(d.label)).map(d=>({label:d.label,id:d.id,status:d.status}));return{plan:c,missing:l}}function tt(e,t){return e.hmacHexStripped===t.hmacHexStripped&&e.hmacHexLower===t.hmacHexLower&&e.scanPrefilter===t.scanPrefilter}const ae=e=>e.replace(/\/+$/,"");async function H(e,t,o,r,n){let i;try{i=await fetch(`${ae(e)}${r}`,{method:o,headers:{Authorization:`Bearer ${t}`,...n?{"Content-Type":"application/json"}:{}},...n?{body:JSON.stringify(n)}:{}})}catch(c){throw new m(`Cannot reach ${ae(e)}: ${c.message}`)}const h=await i.text();let a=null;if(h)try{a=JSON.parse(h)}catch{a=null}return{status:i.status,ok:i.ok,data:a,text:h}}async function nt(e,t,o){const r=await H(e,t,"GET","/api/projects");if(!r.ok||!Array.isArray(r.data))throw new m(`GET /api/projects failed: ${r.status} ${String(r.text).slice(0,200)}`);const n=o.trim().toLowerCase(),i=r.data.filter(a=>a.id===o.trim()),h=i.length?i:r.data.filter(a=>String(a.name).toLowerCase()===n);if(h.length===0)throw new m(`No project named or numbered "${o}". Known: ${r.data.map(a=>a.name).join(", ")||"none"}.`);if(h.length>1)throw new m(`"${o}" matches ${h.length} projects. Pass the project id instead.`);if(h[0].status==="CLOSED")throw new m(`Project "${h[0].name}" is closed. Reopen it before enrolling into it.`);return h[0]}async function rt({plan:e,existing:t,serverUrl:o,token:r}){const n=e.filter(a=>a.action==="rotate"),i=a=>t.find(c=>c.id===a.id)?.projectId??null;if(!n.some(i))return;const h=await H(o,r,"GET","/api/projects");if(!(!h.ok||!Array.isArray(h.data)))for(const a of n){const c=i(a);if(!c)continue;const p=h.data.find(l=>l.id===c);p&&(p.usesOrgKey?a.sendProjectId=c:(a.action="conflict",a.reason=`lives in project "${p.name}", which has its own key; re-run with --project "${p.name}"`))}}async function ot(e,t,o=!1){const r=await H(e,t,"GET","/api/secrets");if(o&&(r.status===401||r.status===403))throw new m("This machine's Redaktyn session is no longer valid: the device was removed or its session ended. Ask your admin for a new invite link and run `redaktyn-connect` again.");if(r.status===401)throw new m("Server rejected the token (401). REDAKTYN_TOKEN must be a current OWNER or ADMIN login token.");if(r.status===403)throw new m("This token is not allowed to manage secrets (403). Importing needs an OWNER or ADMIN token.");if(!r.ok)throw new m(`GET /api/secrets failed: ${r.status} ${r.text.slice(0,200)}`);if(!Array.isArray(r.data))throw new m("GET /api/secrets did not return a list.");return r.data}function st(e){try{const t=e.split(".")[1];if(!t)return null;const o=t+"=".repeat((4-t.length%4)%4),r=Buffer.from(o.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8");return JSON.parse(r)}catch{return null}}function it(e){if(!e)return null;if(e.type==="extension")return`REDAKTYN_TOKEN is an extension device token. Those can sync fingerprints but not register them.
17
17
  Get an OWNER or ADMIN login token instead:
18
18
  curl -s -X POST "$REDAKTYN_SERVER_URL/api/auth/login" \\
19
19
  -H 'Content-Type: application/json' \\
20
- -d '{"email":"you@example.com","password":"\u2026"}' | jq -r .token`;const t=e.role;return typeof t=="string"&&!["OWNER","ADMIN"].includes(t.toUpperCase())?`This token has role ${t}. Importing needs OWNER or ADMIN.`:null}const it={create:{label:"CREATE",color:s.green},rotate:{label:"ROTATE",color:s.yellow},skip:{label:"skip",color:s.gray},conflict:{label:"CONFLICT",color:s.red},rejected:{label:"rejected",color:s.gray}};function le(e,t){return e.length<=t?e.padEnd(t):e.slice(0,t-1)+"\u2026"}function at({rows:e,missing:t,source:o,dryRun:r,budget:n,projectName:i=null}){const h=Math.min(process.stdout.columns||80,100),d=34;console.log(""),console.log(`${s.bold}Redaktyn import${s.reset} ${s.dim}\xB7 source tag ${O(o)}${i?` \xB7 project ${i}`:""}${s.reset}`),console.log(s.dim+"\u2500".repeat(h)+s.reset),console.log(`${s.dim}${"KEY".padEnd(d)} ${"LEN".padStart(5)} ${"ACTION".padEnd(9)} WHY${s.reset}`);for(const a of e){const l=it[a.action]??{label:a.action,color:""};console.log(`${le(a.key,d)} ${String(a.length??"-").padStart(5)} ${l.color}${l.label.padEnd(9)}${s.reset} ${s.dim}${a.reason??""}${s.reset}`)}console.log(s.dim+"\u2500".repeat(h)+s.reset);const c=H(e),p=[c.create?`${s.green}${c.create} to create${s.reset}`:null,c.rotate?`${s.yellow}${c.rotate} to rotate${s.reset}`:null,c.skip?`${c.skip} unchanged`:null,c.rejected?`${c.rejected} filtered out`:null,c.conflict?`${s.red}${c.conflict} conflicting${s.reset}`:null].filter(Boolean);if(console.log(" "+(p.length?p.join(s.dim+" \xB7 "+s.reset):"nothing to do")),t.length>0){console.log(""),console.log(`${s.yellow}${t.length} secret(s) tagged ${O(o)} are not in this input.${s.reset}`),console.log(`${s.dim} Reported only \u2014 import never removes anything. Retire them in the dashboard if they are gone.${s.reset}`);for(const a of t.slice(0,20))console.log(`${s.dim} \xB7 ${a.label}${s.reset}`);t.length>20&&console.log(`${s.dim} \xB7 \u2026 ${t.length-20} more${s.reset}`)}q(n),r&&(console.log(""),console.log(`${s.bold}Dry run \u2014 nothing was sent.${s.reset} ${s.dim}Re-run with --apply to register.${s.reset}`)),console.log("")}const lt={create:{label:"CREATE",paint:g.green},rotate:{label:"ROTATE",paint:g.amber},skip:{label:"skip",paint:g.gray},conflict:{label:"CONFLICT",paint:g.red},rejected:{label:"rejected",paint:g.gray}};function ct({rows:e,missing:t,source:o,dryRun:r,budget:n,projectName:i=null,byKey:h}){const d=Math.min(process.stderr.columns||90,100),c=30;w(""),w(` ${g.bold("Plan")} ${g.dim(`source tag ${O(o)}${i?` \xB7 project ${i}`:""}`)}`),w(` ${g.gray("\u2500".repeat(d-4))}`),w(` ${g.gray(`${"KEY".padEnd(c)} ${"LEN".padStart(4)} ${"ACTION".padEnd(8)} ${"FINGERPRINT".padEnd(11)} WHY`)}`);for(const l of e){const y=lt[l.action]??{label:l.action,paint:S=>S},u=h.get(l.key),$=u&&(l.action==="create"||l.action==="rotate")?de(u.digests.digest):"";w(` ${le(l.key,c)} ${String(l.length??"-").padStart(4)} ${y.paint(y.label.padEnd(8))} ${g.amber($.padEnd(11))} ${g.dim(l.reason??"")}`)}w(` ${g.gray("\u2500".repeat(d-4))}`);const p=H(e),a=[p.create?g.green(`${p.create} to create`):null,p.rotate?g.amber(`${p.rotate} to rotate`):null,p.skip?`${p.skip} unchanged`:null,p.rejected?`${p.rejected} filtered out`:null,p.conflict?g.red(`${p.conflict} conflicting`):null].filter(Boolean);if(w(` ${a.length?a.join(g.dim(" \xB7 ")):"nothing to do"}`),t.length>0){w(""),w(` ${g.amber("\u25B2")} ${t.length} secret(s) tagged ${O(o)} are not in this input. ${g.dim("Reported only: import never removes anything.")}`);for(const l of t.slice(0,10))w(` ${g.dim(`\xB7 ${l.label}`)}`);t.length>10&&w(` ${g.dim(`\xB7 \u2026 ${t.length-10} more`)}`)}q(n)}function q(e){if(!e||e.ceiling===1/0)return;const t=e.ceiling.toLocaleString("en-US"),o=r=>{if(!(e.legacySecrets>0)||!(e.ceilingIfReenrolled>e.ceiling))return;const n=e.legacySecrets;console.log(`${r} ${n} enrolled secret${n===1?" has":"s have"} no scan prefilter (enrolled before 2026-10-07 or by an older client),${s.reset}`),console.log(`${r} so every paste is checked against ${n===1?"it":"them"} the slow way. They are outside this import; re-enrolling ${n===1?"it":"them"}${s.reset}`),console.log(`${r} from the dashboard, or importing ${n===1?"it":"them"}, raises this to ~${e.ceilingIfReenrolled.toLocaleString("en-US")} chars.${s.reset}`)};if(e.ceiling<Pe){console.log(""),console.log(`${s.red}${s.bold}Scan budget: pastes over ~${t} characters will be refused${s.reset}`),console.log(` ${e.windowLengths} distinct window lengths after this import. Detection slides one pass per`),console.log(` length, so past that size the extension ${s.bold}refuses to scan${s.reset} rather than hang \u2014`),console.log(" which is a hard block in sensitive contexts. A paste at the ceiling takes up to ~8s."),o(""),console.log(`${s.dim} Register the high-value secrets only, or split them across workspaces.${s.reset}`);return}if(e.ceiling<Z){console.log(""),console.log(`${s.yellow}Scan budget:${s.reset} ${e.windowLengths} window lengths after this import; pastes above ~${t} chars are refused.`),o("");return}console.log(e.atCharacterCap?`${s.dim} Scan budget: every paste up to the extension's ${t}-char hard limit is scanned.${s.reset}`:`${s.dim} Scan budget: ${e.windowLengths} window lengths, pastes up to ~${t} chars.${s.reset}`),o(s.dim)}function H(e){const t={create:0,rotate:0,skip:0,conflict:0,rejected:0};for(const o of e)t[o.action]=(t[o.action]??0)+1;return t}async function dt(e){if(!process.stdin.isTTY)throw new m("--apply needs a terminal to confirm on, or --yes to skip the prompt.");const t=$e.createInterface({input:process.stdin,output:process.stdout});try{const o=await new Promise(r=>t.question(`${e} `,r));return/^y(es)?$/i.test(o.trim())}finally{t.close()}}function pt(e,t){return Re({label:e,hmacHex:t.digest,length:t.length,hmacHexStripped:t.hmacHexStripped,lengthStripped:t.lengthStripped,hmacHexLower:t.hmacHexLower,lengthLower:t.lengthLower,scanPrefilter:t.scanPrefilter})}function ce(e,t,{withPrefilter:o,projectId:r,tag:n}){const{scanPrefilter:i,...h}=t.digests,d=pt(e.key,o?{...h,scanPrefilter:i}:h),{label:c,...p}=d,a=r?{projectId:r}:{};return e.action==="create"?{method:"POST",path:"/api/secrets",body:{...d,...a,tags:[n]}}:{method:"PUT",path:`/api/secrets/${e.id}/digest`,body:{...p,...a}}}const de=e=>`${e.slice(0,8)}\u2026`;function ht(e,t){const o=[];let r="";for(const n of e.split(" "))r&&(r+" "+n).length>t?(o.push(r),r=n):r=r?`${r} ${n}`:n;return r&&o.push(r),o}async function ut({rows:e,byKey:t,serverUrl:o,token:r,source:n,projectId:i=null}){const h=[],d=O(n);let c=!0,p=0,a=0;const l=e.filter(u=>u.action==="create"||u.action==="rotate").length;let y=0;for(const u of e){if(u.action!=="create"&&u.action!=="rotate")continue;const $=t.get(u.key),S=A=>{const E=ce(u,$,{withPrefilter:A,projectId:i,tag:d});return p+=Buffer.byteLength(JSON.stringify(E.body)),a+=1,F(o,r,E.method,E.path,E.body)};let k=await S(c);c&&$.digests.scanPrefilter&&Ke(k)&&(c=!1,pe(),k=await S(!1)),y+=1;const v=R?` ${g.dim(de($.digests.digest))}`:"";if(k.ok){h.push({key:u.key,action:u.action,ok:!0}),console.log(` ${s.green}\u2713${s.reset} ${u.action==="create"?"created":"rotated"} ${u.key}${v}`),R&&w(` ${Oe(y,l)}`);continue}const b=yt(k);h.push({key:u.key,action:u.action,ok:!1,status:k.status,reason:b}),console.log(` ${s.red}\u2717${s.reset} ${u.key}: ${b}`)}return h.sentBytes=p,h.requests=a,h}const ft=e=>e.replace(/[0-9a-f]{40,}/g,t=>`${t.slice(0,12)}\u2026(${t.length} hex)`);function gt({writes:e,byKey:t,serverUrl:o,projectId:r,source:n,dryRun:i,showRequest:h,viaSession:d=!1}){if(e.length===0)return;const c=O(n),p=e.map(u=>{const $=t.get(u.key);return ce(u,$,{withPrefilter:!!$.digests.scanPrefilter,projectId:r,tag:c})}),a=p.reduce((u,$)=>u+Buffer.byteLength(JSON.stringify($.body)),0),l=[...new Set(p.flatMap(u=>Object.keys(u.body)))];let y=o;try{y=new URL(o).host}catch{}if(R&&(w(""),B(i?"What WOULD leave this machine":"What leaves this machine",["",...K([["Sent to",`${y}: ${p.length} request${p.length===1?"":"s"}, ${z(a)} in total`],["Each request","label, 3 fingerprints, 3 lengths"],...ht(l.join(", "),44).map(u=>["",g.dim(u)]),["Secret values",g.green("0 B")]],{indent:0,labelWidth:18}),"",g.dim("Never leaves this machine:"),...K([["The values",`${e.length} read into memory, fingerprinted, then dropped`],...d?[["Your passphrase","not used here. This machine holds the project's key from its invite"],["The project key","in the session file only you can read; never sent to the server"]]:[["Your passphrase","asked for, used to derive the key, then forgotten"],["The workspace key","derived here, held in memory, never written or sent"]]],{indent:0,labelWidth:18}),""],{tone:"green",width:100})),h){const u=p[0];w(""),w(` ${g.dim(`The first request, exactly as it is built (${u.method} ${u.path}), digests shortened:`)}`),w(ft(JSON.stringify(u.body,null,2)).split(`
20
+ -d '{"email":"you@example.com","password":"\u2026"}' | jq -r .token`;const t=e.role;return typeof t=="string"&&!["OWNER","ADMIN"].includes(t.toUpperCase())?`This token has role ${t}. Importing needs OWNER or ADMIN.`:null}const at={create:{label:"CREATE",color:s.green},rotate:{label:"ROTATE",color:s.yellow},skip:{label:"skip",color:s.gray},conflict:{label:"CONFLICT",color:s.red},rejected:{label:"rejected",color:s.gray}};function le(e,t){return e.length<=t?e.padEnd(t):e.slice(0,t-1)+"\u2026"}function lt({rows:e,missing:t,source:o,dryRun:r,budget:n,projectName:i=null}){const h=Math.min(process.stdout.columns||80,100),a=34;console.log(""),console.log(`${s.bold}Redaktyn import${s.reset} ${s.dim}\xB7 source tag ${O(o)}${i?` \xB7 project ${i}`:""}${s.reset}`),console.log(s.dim+"\u2500".repeat(h)+s.reset),console.log(`${s.dim}${"KEY".padEnd(a)} ${"LEN".padStart(5)} ${"ACTION".padEnd(9)} WHY${s.reset}`);for(const l of e){const d=at[l.action]??{label:l.action,color:""};console.log(`${le(l.key,a)} ${String(l.length??"-").padStart(5)} ${d.color}${d.label.padEnd(9)}${s.reset} ${s.dim}${l.reason??""}${s.reset}`)}console.log(s.dim+"\u2500".repeat(h)+s.reset);const c=M(e),p=[c.create?`${s.green}${c.create} to create${s.reset}`:null,c.rotate?`${s.yellow}${c.rotate} to rotate${s.reset}`:null,c.skip?`${c.skip} unchanged`:null,c.rejected?`${c.rejected} filtered out`:null,c.conflict?`${s.red}${c.conflict} conflicting${s.reset}`:null].filter(Boolean);if(console.log(" "+(p.length?p.join(s.dim+" \xB7 "+s.reset):"nothing to do")),t.length>0){console.log(""),console.log(`${s.yellow}${t.length} secret(s) tagged ${O(o)} are not in this input.${s.reset}`),console.log(`${s.dim} Reported only \u2014 import never removes anything. Retire them in the dashboard if they are gone.${s.reset}`);for(const l of t.slice(0,20))console.log(`${s.dim} \xB7 ${l.label}${s.reset}`);t.length>20&&console.log(`${s.dim} \xB7 \u2026 ${t.length-20} more${s.reset}`)}q(n),r&&(console.log(""),console.log(`${s.bold}Dry run \u2014 nothing was sent.${s.reset} ${s.dim}Re-run with --apply to register.${s.reset}`)),console.log("")}const ct={create:{label:"CREATE",paint:g.green},rotate:{label:"ROTATE",paint:g.amber},skip:{label:"skip",paint:g.gray},conflict:{label:"CONFLICT",paint:g.red},rejected:{label:"rejected",paint:g.gray}};function dt({rows:e,missing:t,source:o,dryRun:r,budget:n,projectName:i=null,byKey:h}){const a=Math.min(process.stderr.columns||90,100),c=30;w(""),w(` ${g.bold("Plan")} ${g.dim(`source tag ${O(o)}${i?` \xB7 project ${i}`:""}`)}`),w(` ${g.gray("\u2500".repeat(a-4))}`),w(` ${g.gray(`${"KEY".padEnd(c)} ${"LEN".padStart(4)} ${"ACTION".padEnd(8)} ${"FINGERPRINT".padEnd(11)} WHY`)}`);for(const d of e){const y=ct[d.action]??{label:d.action,paint:S=>S},u=h.get(d.key),$=u&&(d.action==="create"||d.action==="rotate")?de(u.digests.digest):"";w(` ${le(d.key,c)} ${String(d.length??"-").padStart(4)} ${y.paint(y.label.padEnd(8))} ${g.amber($.padEnd(11))} ${g.dim(d.reason??"")}`)}w(` ${g.gray("\u2500".repeat(a-4))}`);const p=M(e),l=[p.create?g.green(`${p.create} to create`):null,p.rotate?g.amber(`${p.rotate} to rotate`):null,p.skip?`${p.skip} unchanged`:null,p.rejected?`${p.rejected} filtered out`:null,p.conflict?g.red(`${p.conflict} conflicting`):null].filter(Boolean);if(w(` ${l.length?l.join(g.dim(" \xB7 ")):"nothing to do"}`),t.length>0){w(""),w(` ${g.amber("\u25B2")} ${t.length} secret(s) tagged ${O(o)} are not in this input. ${g.dim("Reported only: import never removes anything.")}`);for(const d of t.slice(0,10))w(` ${g.dim(`\xB7 ${d.label}`)}`);t.length>10&&w(` ${g.dim(`\xB7 \u2026 ${t.length-10} more`)}`)}q(n)}function q(e){if(!e||e.ceiling===1/0)return;const t=e.ceiling.toLocaleString("en-US"),o=r=>{if(!(e.legacySecrets>0)||!(e.ceilingIfReenrolled>e.ceiling))return;const n=e.legacySecrets;console.log(`${r} ${n} enrolled secret${n===1?" has":"s have"} no scan prefilter (enrolled before 2026-10-07 or by an older client),${s.reset}`),console.log(`${r} so every paste is checked against ${n===1?"it":"them"} the slow way. They are outside this import; re-enrolling ${n===1?"it":"them"}${s.reset}`),console.log(`${r} from the dashboard, or importing ${n===1?"it":"them"}, raises this to ~${e.ceilingIfReenrolled.toLocaleString("en-US")} chars.${s.reset}`)};if(e.ceiling<Pe){console.log(""),console.log(`${s.red}${s.bold}Scan budget: pastes over ~${t} characters will be refused${s.reset}`),console.log(` ${e.windowLengths} distinct window lengths after this import. Detection slides one pass per`),console.log(` length, so past that size the extension ${s.bold}refuses to scan${s.reset} rather than hang \u2014`),console.log(" which is a hard block in sensitive contexts. A paste at the ceiling takes up to ~8s."),o(""),console.log(`${s.dim} Register the high-value secrets only, or split them across workspaces.${s.reset}`);return}if(e.ceiling<Z){console.log(""),console.log(`${s.yellow}Scan budget:${s.reset} ${e.windowLengths} window lengths after this import; pastes above ~${t} chars are refused.`),o("");return}console.log(e.atCharacterCap?`${s.dim} Scan budget: every paste up to the extension's ${t}-char hard limit is scanned.${s.reset}`:`${s.dim} Scan budget: ${e.windowLengths} window lengths, pastes up to ~${t} chars.${s.reset}`),o(s.dim)}function M(e){const t={create:0,rotate:0,skip:0,conflict:0,rejected:0};for(const o of e)t[o.action]=(t[o.action]??0)+1;return t}async function pt(e){if(!process.stdin.isTTY)throw new m("--apply needs a terminal to confirm on, or --yes to skip the prompt.");const t=$e.createInterface({input:process.stdin,output:process.stdout});try{const o=await new Promise(r=>t.question(`${e} `,r));return/^y(es)?$/i.test(o.trim())}finally{t.close()}}function ht(e,t){return je({label:e,hmacHex:t.digest,length:t.length,hmacHexStripped:t.hmacHexStripped,lengthStripped:t.lengthStripped,hmacHexLower:t.hmacHexLower,lengthLower:t.lengthLower,scanPrefilter:t.scanPrefilter})}function ce(e,t,{withPrefilter:o,projectId:r,tag:n}){const{scanPrefilter:i,...h}=t.digests,a=ht(e.key,o?{...h,scanPrefilter:i}:h),{label:c,...p}=a,l=e.sendProjectId??r,d=l?{projectId:l}:{};return e.action==="create"?{method:"POST",path:"/api/secrets",body:{...a,...d,tags:[n]}}:{method:"PUT",path:`/api/secrets/${e.id}/digest`,body:{...p,...d}}}const de=e=>`${e.slice(0,8)}\u2026`;function ut(e,t){const o=[];let r="";for(const n of e.split(" "))r&&(r+" "+n).length>t?(o.push(r),r=n):r=r?`${r} ${n}`:n;return r&&o.push(r),o}async function ft({rows:e,byKey:t,serverUrl:o,token:r,source:n,projectId:i=null}){const h=[],a=O(n);let c=!0,p=0,l=0;const d=e.filter(u=>u.action==="create"||u.action==="rotate").length;let y=0;for(const u of e){if(u.action!=="create"&&u.action!=="rotate")continue;const $=t.get(u.key),S=E=>{const N=ce(u,$,{withPrefilter:E,projectId:i,tag:a});return p+=Buffer.byteLength(JSON.stringify(N.body)),l+=1,H(o,r,N.method,N.path,N.body)};let k=await S(c);c&&$.digests.scanPrefilter&&Ke(k)&&(c=!1,pe(),k=await S(!1)),y+=1;const b=j?` ${g.dim(de($.digests.digest))}`:"";if(k.ok){h.push({key:u.key,action:u.action,ok:!0}),console.log(` ${s.green}\u2713${s.reset} ${u.action==="create"?"created":"rotated"} ${u.key}${b}`),j&&w(` ${Oe(y,d)}`);continue}const v=$t(k);h.push({key:u.key,action:u.action,ok:!1,status:k.status,reason:v}),console.log(` ${s.red}\u2717${s.reset} ${u.key}: ${v}`)}return h.sentBytes=p,h.requests=l,h}const gt=e=>e.replace(/[0-9a-f]{40,}/g,t=>`${t.slice(0,12)}\u2026(${t.length} hex)`);function mt({writes:e,byKey:t,serverUrl:o,projectId:r,source:n,dryRun:i,showRequest:h,viaSession:a=!1}){if(e.length===0)return;const c=O(n),p=e.map(u=>{const $=t.get(u.key);return ce(u,$,{withPrefilter:!!$.digests.scanPrefilter,projectId:r,tag:c})}),l=p.reduce((u,$)=>u+Buffer.byteLength(JSON.stringify($.body)),0),d=[...new Set(p.flatMap(u=>Object.keys(u.body)))];let y=o;try{y=new URL(o).host}catch{}if(j&&(w(""),B(i?"What WOULD leave this machine":"What leaves this machine",["",...J([["Sent to",`${y}: ${p.length} request${p.length===1?"":"s"}, ${z(l)} in total`],["Each request","label, 3 fingerprints, 3 lengths"],...ut(d.join(", "),44).map(u=>["",g.dim(u)]),["Secret values",g.green("0 B")]],{indent:0,labelWidth:18}),"",g.dim("Never leaves this machine:"),...J([["The values",`${e.length} read into memory, fingerprinted, then dropped`],...a?[["Your passphrase","not used here. This machine holds the project's key from its invite"],["The project key","in the session file only you can read; never sent to the server"]]:[["Your passphrase","asked for, used to derive the key, then forgotten"],["The workspace key","derived here, held in memory, never written or sent"]]],{indent:0,labelWidth:18}),""],{tone:"green",width:100})),h){const u=p[0];w(""),w(` ${g.dim(`The first request, exactly as it is built (${u.method} ${u.path}), digests shortened:`)}`),w(gt(JSON.stringify(u.body,null,2)).split(`
21
21
  `).map($=>` ${$}`).join(`
22
- `))}else R&&w(` ${g.dim("See the exact request body: --show-request")}`)}async function mt({label:e,cand:t,viaSession:o=!1}){const r=t.digests.length;w(""),w(` ${g.bold(e)} ${g.gray("\u2022".repeat(Math.min(r,22)))} ${g.dim(`${r} characters, read here and never sent`)}`),w(` ${g.gray(C.bar)} ${g.dim("normalise ")} trim, unicode NFC, strip invisible characters`),w(o?` ${g.gray(C.bar)} ${g.dim("project key ")} the key from your invite, already on this machine (no passphrase needed)`:` ${g.gray(C.bar)} ${g.dim("derive key ")} PBKDF2-SHA256, 600,000 rounds, from your passphrase, on this machine`),await Le(` ${g.gray(C.down)} ${g.dim("fingerprint ")} `,t.digests.digest,{show:16}),w(` ${g.dim("HMAC-SHA256, plus whitespace-stripped and lower-case variants, so a disguised paste still matches")}`),w("")}function pe(){console.log(`${s.yellow}!${s.reset} This server does not store scan prefilters yet, so secrets are registered without one.`),console.log(`${s.dim} They are protected exactly the same, but every paste checks them the slow way. Re-run this${s.reset}`),console.log(`${s.dim} import once the server is updated; it adds the prefilter to each of them.${s.reset}`)}function yt(e){if(e.status===409)return"another secret in this workspace already has this fingerprint";if(e.status===403)return"token is not OWNER or ADMIN";if(e.status===401)return"token expired or invalid";if(e.status===404)return"secret no longer exists \u2014 re-run to re-plan";const t=e.data&&typeof e.data.error=="string"?e.data.error:e.text.slice(0,160);return`${e.status} ${t}`}function $t(){console.log(`
22
+ `))}else j&&w(` ${g.dim("See the exact request body: --show-request")}`)}async function yt({label:e,cand:t,viaSession:o=!1}){const r=t.digests.length;w(""),w(` ${g.bold(e)} ${g.gray("\u2022".repeat(Math.min(r,22)))} ${g.dim(`${r} characters, read here and never sent`)}`),w(` ${g.gray(C.bar)} ${g.dim("normalise ")} trim, unicode NFC, strip invisible characters`),w(o?` ${g.gray(C.bar)} ${g.dim("project key ")} the key from your invite, already on this machine (no passphrase needed)`:` ${g.gray(C.bar)} ${g.dim("derive key ")} PBKDF2-SHA256, 600,000 rounds, from your passphrase, on this machine`),await Ie(` ${g.gray(C.down)} ${g.dim("fingerprint ")} `,t.digests.digest,{show:16}),w(` ${g.dim("HMAC-SHA256, plus whitespace-stripped and lower-case variants, so a disguised paste still matches")}`),w("")}function pe(){console.log(`${s.yellow}!${s.reset} This server does not store scan prefilters yet, so secrets are registered without one.`),console.log(`${s.dim} They are protected exactly the same, but every paste checks them the slow way. Re-run this${s.reset}`),console.log(`${s.dim} import once the server is updated; it adds the prefilter to each of them.${s.reset}`)}function $t(e){if(e.status===409&&e.data?.code==="PROJECT_MISMATCH"&&typeof e.data.error=="string")return e.data.error;if(e.status===409)return"another secret in this workspace already has this fingerprint";if(e.status===403)return"token is not OWNER or ADMIN";if(e.status===401)return"token expired or invalid";if(e.status===404)return"secret no longer exists \u2014 re-run to re-plan";const t=e.data&&typeof e.data.error=="string"?e.data.error:e.text.slice(0,160);return`${e.status} ${t}`}function wt(){console.log(`
23
23
  ${s.bold}redaktyn-import${s.reset} \u2014 register fingerprints in bulk from files or any vault CLI
24
24
 
25
25
  ${s.bold}Digests are computed on this machine.${s.reset} Values are never sent, never
@@ -66,4 +66,4 @@ ${s.bold}Exit codes${s.reset}
66
66
  0 plan printed, or every write succeeded
67
67
  1 one or more writes failed
68
68
  2 usage, auth or parse error
69
- `)}async function wt(e){const t=[e.env&&"env",e.jsonFile&&"json",e.yaml&&"yaml",e.stdin&&"stdin"].filter(Boolean);if(t.length===0)throw new m("Nothing to import. Pass one of --env, --json, --yaml or --stdin (see --help).");if(t.length>1)throw new m(`Pick one input mode, got: ${t.map(i=>"--"+i).join(", ")}`);const o=t[0];if(o==="env")return{...Fe(J(e.env,"--env")),mode:o,defaultSource:W(e.env)};if(o==="json")return{...oe(J(e.jsonFile,"--json")),mode:o,defaultSource:W(e.jsonFile)};if(o==="yaml")return{...Be(J(e.yaml,"--yaml")),mode:o,defaultSource:W(e.yaml)};const r=await Ze();if(r.trim()==="")throw new m("Nothing arrived on stdin.");return{...oe(r),mode:o,defaultSource:"stdin"}}async function kt(e){const t=process.env.REDAKTYN_TOKEN||process.env.REDAKTYN_JWT;let o=null;if(!t){const a=U();if(a)try{o=await Ne(a)}catch(l){throw new m(l.message)}}const r=e.server||!t&&o?.serverUrl||Ee(),n=t||o?.token;if(!n)throw new m("Run `redaktyn-login` to sign in as an OWNER or ADMIN (or set REDAKTYN_TOKEN for a script).");const i=ot(n),h=st(i);if(h)throw new m(h);const d=e.orgId||i?.orgId||i?.org_id||process.env.REDAKTYN_ORG_ID;if(!d)throw new m("Could not determine the org id from the token. Pass --org-id.");const c=e.orgId||process.env.REDAKTYN_ORG_ID;if(c&&i?.orgId&&c!==i.orgId)throw new m("The org id given does not match the token's org id.");let p;try{p=await xe({server:r,token:n,orgId:d,who:o?`Signed in as ${o.email}${o.orgName?` (${o.orgName})`:""}.`:null,label:"Org passphrase",record:!!e.apply})}catch(a){throw new m(a.message)}return{serverUrl:r,token:n,orgId:d,passphrase:p}}async function vt(e){const o=process.env.REDAKTYN_TOKEN||process.env.REDAKTYN_JWT||U()?null:Ae();if(o){let a;try{a=await je(o)}catch(l){throw new m(l.message)}if(e.project){const l=e.project.trim().toLowerCase();if(l!==String(a.projectId).toLowerCase()&&l!==String(a.projectName||"").toLowerCase())throw new m(`This machine is connected to project "${a.projectName??a.projectId}" only, not "${e.project}". Drop --project, or ask for an invite to the other project.`)}if(!a.projectId)throw new m("This session is not tied to a project. Ask for a new invite link.");if(e.apply&&!a.canEnrol)throw new m("This invite lets you scan but not enrol secrets. Ask your admin for a link made with: redaktyn-invite --can-enrol");return{serverUrl:e.server||a.serverUrl,token:a.token,key:Buffer.from(a.key,"hex"),project:{id:a.projectId,name:a.projectName??a.projectId,usesOrgKey:a.projectUsesOrgKey===!0},viaSession:!0}}const{serverUrl:r,token:n,orgId:i,passphrase:h}=await kt(e),d=await be(h,i),c=e.project?await nt(r,n,e.project):null,p=c&&!c.usesOrgKey?Se(d,c.id):d;return{serverUrl:r,token:n,key:p,project:c,viaSession:!1}}async function bt(){const e=Ye(process.argv);if(e.help)return $t(),0;e.reportJson||Te("import secrets",_e);const t=await wt(e),o=Xe(e.source??t.defaultSource);R&&!e.reportJson&&w(` ${g.green("\u2714")} Read ${t.entries.length} key${t.entries.length===1?"":"s"} ${g.dim(`(${t.mode}); the values stay in memory`)}`);const r=new Map;for(const f of t.entries)r.set(f.key,f);const n=t.entries.length-r.size;let i=[...r.values()];if(e.only.length>0&&(i=i.filter(f=>ie(f.key,e.only))),e.exclude.length>0&&(i=i.filter(f=>!ie(f.key,e.exclude))),i.length===0)throw new m(`No usable keys found in the input (${t.mode}). `+(t.problems?.length?`${t.problems.length} line(s) were skipped \u2014 see above.`:""));const{serverUrl:h,token:d,key:c,project:p,viaSession:a}=await vt(e),l=e.minLength??Y,y=[],u=[],$=R&&!e.reportJson?G("Fingerprinting on this machine"):null;for(const f of i){if(f.key.length>200){u.push({key:f.key,action:"rejected",reason:"key longer than 200 characters"});continue}const j=ke(f.value),P=Ve(j,{minLength:l});if(!P.ok&&!e.allowWeak){u.push({key:f.key,action:"rejected",reason:P.reason,length:j.length});continue}const ge=ve(f.value,c);y.push({key:f.key,digests:ge,weak:!P.ok,weakReason:P.reason})}$?.succeed(`${y.length} fingerprint${y.length===1?"":"s"} computed here ${g.dim("(three variants each)")}`),R&&!e.reportJson&&y.length>0&&await mt({label:y[0].key,cand:y[0],viaSession:a});const S=R&&!e.reportJson?G("Comparing with what your workspace already holds"):null;let k=await rt(h,d,a);p&&(k=k.filter(f=>f&&f.projectId===p.id)),S?.succeed(`${k.length} existing secret${k.length===1?"":"s"} compared`);const v=We(k);if(!v)for(const f of y)delete f.digests.scanPrefilter;const{plan:b,missing:A}=et({candidates:y,existing:k,source:o}),E=new Map(y.map(f=>[f.key,f]));for(const f of b){const j=E.get(f.key);j?.weak&&(f.action==="create"||f.action==="rotate")&&(f.reason=`${f.reason} (weak: ${j.weakReason}, allowed by --allow-weak)`)}const _=[...b,...u].sort((f,j)=>ue(f.action)-ue(j.action)||f.key.localeCompare(j.key)),fe=St({existing:k,plan:b,byKey:E}),N=Me(fe),M=b.filter(f=>f.action==="create"||f.action==="rotate");if(e.reportJson){if(console.log(JSON.stringify({source:o,sourceTag:O(o),mode:t.mode,shape:t.shape??null,dryRun:!e.apply,minLength:l,allowWeak:e.allowWeak,duplicateKeys:n,inputProblems:t.problems??[],counts:H(_),plan:_.map(f=>({key:f.key,action:f.action,reason:f.reason??null,length:f.length??null})),missing:A,scanBudget:{windowLengths:N.windowLengths,maxPasteChars:N.ceiling===1/0?null:N.ceiling,warn:N.ceiling<Z,atCharacterCap:N.atCharacterCap,legacySecrets:N.legacySecrets,maxPasteCharsIfReenrolled:N.ceilingIfReenrolled===1/0?null:N.ceilingIfReenrolled,serverStoresScanPrefilter:v}},null,2)),!e.apply)return 0}else{for(const f of t.problems??[]){const j=f.line!==void 0?`line ${f.line}`:f.key;console.log(`${s.yellow}!${s.reset} ${j}: ${f.message}`)}n>0&&console.log(`${s.dim}${n} duplicate key(s) in the input; the last value of each was used.${s.reset}`),v||pe(),R?ct({rows:_,missing:A,source:o,dryRun:!e.apply,budget:N,projectName:p?p.name:null,byKey:E}):at({rows:_,missing:A,source:o,dryRun:!e.apply,budget:N,projectName:p?p.name:null}),gt({writes:M,byKey:E,serverUrl:h,projectId:p?p.id:null,source:o,dryRun:!e.apply,showRequest:e.showRequest,viaSession:a}),R&&!e.apply&&(w(""),w(` ${g.bold("Dry run. Nothing was sent.")} ${g.dim("Re-run with --apply to register.")}`)),R&&w("")}if(!e.apply)return 0;if(M.length===0)return console.log(`${s.dim}Nothing to write.${s.reset}`),0;if(!e.yes){const f=H(b);if(!await dt(`Register ${f.create} new and rotate ${f.rotate} existing fingerprint(s) for this org? [y/N]`))return console.log("Aborted."),0}console.log("");const T=await ut({rows:M,byKey:E,serverUrl:h,token:d,source:o,projectId:p?p.id:null}),L=T.filter(f=>!f.ok);return console.log(""),console.log(L.length===0?`${s.green}${T.length} fingerprint(s) registered.${s.reset}`:`${s.red}${L.length} of ${T.length} failed.${s.reset}`),q(N),R&&(w(""),B(L.length===0?"Done":"Finished with errors",["",...K([["Registered",`${T.length-L.length} of ${T.length}`],["Sent",`${z(T.sentBytes??0)} in ${T.requests??0} request${T.requests===1?"":"s"}`],["Secret values sent",g.green("0 B")],["Takes effect","in every connected browser within about a minute"]],{indent:0}),""],{tone:L.length===0?"green":"red",width:78})),console.log(""),L.length===0?0:1}const he=["create","rotate","conflict","skip","rejected"],ue=e=>{const t=he.indexOf(e);return t===-1?he.length:t};function St({existing:e,plan:t,byKey:o}){const r=e.map(i=>({...i})),n=new Map(r.map(i=>[i.id,i]));for(const i of t){const h=o.get(i.key);if(!h)continue;const d=h.digests;if(i.action==="create"){r.push({id:`pending:${i.key}`,label:i.key,hmacHex:d.digest,length:d.length,hmacHexStripped:d.hmacHexStripped,lengthStripped:d.lengthStripped,hmacHexLower:d.hmacHexLower,lengthLower:d.lengthLower,scanPrefilter:d.scanPrefilter,tags:[]});continue}if(i.action==="rotate"){const c=n.get(i.id);if(!c)continue;c.hmacHex=d.digest,c.length=d.length,c.hmacHexStripped=d.hmacHexStripped,c.lengthStripped=d.lengthStripped,c.hmacHexLower=d.hmacHexLower,c.lengthLower=d.lengthLower,c.scanPrefilter=d.scanPrefilter}}return r}function Rt(){try{return ye(process.argv[1])}catch{return process.argv[1]}}const Nt=!!process.argv[1]&&import.meta.url===we(Rt()).href;Nt&&bt().then(e=>process.exit(e)).catch(e=>{e instanceof m&&(console.error(`${s.red}${e.message}${s.reset}`),process.exit(2)),console.error(`${s.red}Fatal: ${e instanceof Error?e.message:e}${s.reset}`),process.exit(1)});export{Y as DEFAULT_MIN_LENGTH,ot as decodeJwtPayload,et as diffAgainstServer,Ue as entriesFromJsonDocument,Ve as junkVerdict,Xe as normalizeSource,Fe as parseDotenv,Be as parseFlatYaml,oe as parseJsonSecrets,Me as scanBudgetForSecrets,O as sourceTag,st as tokenRoleProblem,ne as unwrapSecretsDocument};
69
+ `)}async function kt(e){const t=[e.env&&"env",e.jsonFile&&"json",e.yaml&&"yaml",e.stdin&&"stdin"].filter(Boolean);if(t.length===0)throw new m("Nothing to import. Pass one of --env, --json, --yaml or --stdin (see --help).");if(t.length>1)throw new m(`Pick one input mode, got: ${t.map(i=>"--"+i).join(", ")}`);const o=t[0];if(o==="env")return{...Fe(F(e.env,"--env")),mode:o,defaultSource:K(e.env)};if(o==="json")return{...oe(F(e.jsonFile,"--json")),mode:o,defaultSource:K(e.jsonFile)};if(o==="yaml")return{...Be(F(e.yaml,"--yaml")),mode:o,defaultSource:K(e.yaml)};const r=await Ze();if(r.trim()==="")throw new m("Nothing arrived on stdin.");return{...oe(r),mode:o,defaultSource:"stdin"}}async function vt(e){const t=process.env.REDAKTYN_TOKEN||process.env.REDAKTYN_JWT;let o=null;if(!t){const l=U();if(l)try{o=await Re(l)}catch(d){throw new m(d.message)}}const r=e.server||!t&&o?.serverUrl||Ne(),n=t||o?.token;if(!n)throw new m("Run `redaktyn-login` to sign in as an OWNER or ADMIN (or set REDAKTYN_TOKEN for a script).");const i=st(n),h=it(i);if(h)throw new m(h);const a=e.orgId||i?.orgId||i?.org_id||process.env.REDAKTYN_ORG_ID;if(!a)throw new m("Could not determine the org id from the token. Pass --org-id.");const c=e.orgId||process.env.REDAKTYN_ORG_ID;if(c&&i?.orgId&&c!==i.orgId)throw new m("The org id given does not match the token's org id.");let p;try{p=await xe({server:r,token:n,orgId:a,who:o?`Signed in as ${o.email}${o.orgName?` (${o.orgName})`:""}.`:null,label:"Org passphrase",record:!!e.apply})}catch(l){throw new m(l.message)}return{serverUrl:r,token:n,orgId:a,passphrase:p}}async function bt(e){const o=process.env.REDAKTYN_TOKEN||process.env.REDAKTYN_JWT||U()?null:Ee();if(o){let l;try{l=await Ae(o)}catch(d){throw new m(d.message)}if(e.project){const d=e.project.trim().toLowerCase();if(d!==String(l.projectId).toLowerCase()&&d!==String(l.projectName||"").toLowerCase())throw new m(`This machine is connected to project "${l.projectName??l.projectId}" only, not "${e.project}". Drop --project, or ask for an invite to the other project.`)}if(!l.projectId)throw new m("This session is not tied to a project. Ask for a new invite link.");if(e.apply&&!l.canEnrol)throw new m("This invite lets you scan but not enrol secrets. Ask your admin for a link made with: redaktyn-invite --can-enrol");return{serverUrl:e.server||l.serverUrl,token:l.token,key:Buffer.from(l.key,"hex"),project:{id:l.projectId,name:l.projectName??l.projectId,usesOrgKey:l.projectUsesOrgKey===!0},viaSession:!0}}const{serverUrl:r,token:n,orgId:i,passphrase:h}=await vt(e),a=await be(h,i),c=e.project?await nt(r,n,e.project):null,p=c&&!c.usesOrgKey?Se(a,c.id):a;return{serverUrl:r,token:n,key:p,project:c,viaSession:!1}}async function St(){const e=Je(process.argv);if(e.help)return wt(),0;e.reportJson||Te("import secrets",_e);const t=await kt(e),o=Xe(e.source??t.defaultSource);j&&!e.reportJson&&w(` ${g.green("\u2714")} Read ${t.entries.length} key${t.entries.length===1?"":"s"} ${g.dim(`(${t.mode}); the values stay in memory`)}`);const r=new Map;for(const f of t.entries)r.set(f.key,f);const n=t.entries.length-r.size;let i=[...r.values()];if(e.only.length>0&&(i=i.filter(f=>ie(f.key,e.only))),e.exclude.length>0&&(i=i.filter(f=>!ie(f.key,e.exclude))),i.length===0)throw new m(`No usable keys found in the input (${t.mode}). `+(t.problems?.length?`${t.problems.length} line(s) were skipped \u2014 see above.`:""));const{serverUrl:h,token:a,key:c,project:p,viaSession:l}=await bt(e),d=e.minLength??Y,y=[],u=[],$=j&&!e.reportJson?G("Fingerprinting on this machine"):null;for(const f of i){if(f.key.length>200){u.push({key:f.key,action:"rejected",reason:"key longer than 200 characters"});continue}const A=ke(f.value),P=Ve(A,{minLength:d});if(!P.ok&&!e.allowWeak){u.push({key:f.key,action:"rejected",reason:P.reason,length:A.length});continue}const ge=ve(f.value,c);y.push({key:f.key,digests:ge,weak:!P.ok,weakReason:P.reason})}$?.succeed(`${y.length} fingerprint${y.length===1?"":"s"} computed here ${g.dim("(three variants each)")}`),j&&!e.reportJson&&y.length>0&&await yt({label:y[0].key,cand:y[0],viaSession:l});const S=j&&!e.reportJson?G("Comparing with what your workspace already holds"):null;let k=await ot(h,a,l);p&&(k=k.filter(f=>f&&f.projectId===p.id)),S?.succeed(`${k.length} existing secret${k.length===1?"":"s"} compared`);const b=We(k);if(!b)for(const f of y)delete f.digests.scanPrefilter;const{plan:v,missing:E}=et({candidates:y,existing:k,source:o});p||await rt({plan:v,existing:k,serverUrl:h,token:a});const N=new Map(y.map(f=>[f.key,f]));for(const f of v){const A=N.get(f.key);A?.weak&&(f.action==="create"||f.action==="rotate")&&(f.reason=`${f.reason} (weak: ${A.weakReason}, allowed by --allow-weak)`)}const _=[...v,...u].sort((f,A)=>ue(f.action)-ue(A.action)||f.key.localeCompare(A.key)),fe=jt({existing:k,plan:v,byKey:N}),R=Me(fe),W=v.filter(f=>f.action==="create"||f.action==="rotate");if(e.reportJson){if(console.log(JSON.stringify({source:o,sourceTag:O(o),mode:t.mode,shape:t.shape??null,dryRun:!e.apply,minLength:d,allowWeak:e.allowWeak,duplicateKeys:n,inputProblems:t.problems??[],counts:M(_),plan:_.map(f=>({key:f.key,action:f.action,reason:f.reason??null,length:f.length??null})),missing:E,scanBudget:{windowLengths:R.windowLengths,maxPasteChars:R.ceiling===1/0?null:R.ceiling,warn:R.ceiling<Z,atCharacterCap:R.atCharacterCap,legacySecrets:R.legacySecrets,maxPasteCharsIfReenrolled:R.ceilingIfReenrolled===1/0?null:R.ceilingIfReenrolled,serverStoresScanPrefilter:b}},null,2)),!e.apply)return 0}else{for(const f of t.problems??[]){const A=f.line!==void 0?`line ${f.line}`:f.key;console.log(`${s.yellow}!${s.reset} ${A}: ${f.message}`)}n>0&&console.log(`${s.dim}${n} duplicate key(s) in the input; the last value of each was used.${s.reset}`),b||pe(),j?dt({rows:_,missing:E,source:o,dryRun:!e.apply,budget:R,projectName:p?p.name:null,byKey:N}):lt({rows:_,missing:E,source:o,dryRun:!e.apply,budget:R,projectName:p?p.name:null}),mt({writes:W,byKey:N,serverUrl:h,projectId:p?p.id:null,source:o,dryRun:!e.apply,showRequest:e.showRequest,viaSession:l}),j&&!e.apply&&(w(""),w(` ${g.bold("Dry run. Nothing was sent.")} ${g.dim("Re-run with --apply to register.")}`)),j&&w("")}if(!e.apply)return 0;if(W.length===0)return console.log(`${s.dim}Nothing to write.${s.reset}`),0;if(!e.yes){const f=M(v);if(!await pt(`Register ${f.create} new and rotate ${f.rotate} existing fingerprint(s) for this org? [y/N]`))return console.log("Aborted."),0}console.log("");const T=await ft({rows:W,byKey:N,serverUrl:h,token:a,source:o,projectId:p?p.id:null}),I=T.filter(f=>!f.ok);return console.log(""),console.log(I.length===0?`${s.green}${T.length} fingerprint(s) registered.${s.reset}`:`${s.red}${I.length} of ${T.length} failed.${s.reset}`),q(R),j&&(w(""),B(I.length===0?"Done":"Finished with errors",["",...J([["Registered",`${T.length-I.length} of ${T.length}`],["Sent",`${z(T.sentBytes??0)} in ${T.requests??0} request${T.requests===1?"":"s"}`],["Secret values sent",g.green("0 B")],["Takes effect","in every connected browser within about a minute"]],{indent:0}),""],{tone:I.length===0?"green":"red",width:78})),console.log(""),I.length===0?0:1}const he=["create","rotate","conflict","skip","rejected"],ue=e=>{const t=he.indexOf(e);return t===-1?he.length:t};function jt({existing:e,plan:t,byKey:o}){const r=e.map(i=>({...i})),n=new Map(r.map(i=>[i.id,i]));for(const i of t){const h=o.get(i.key);if(!h)continue;const a=h.digests;if(i.action==="create"){r.push({id:`pending:${i.key}`,label:i.key,hmacHex:a.digest,length:a.length,hmacHexStripped:a.hmacHexStripped,lengthStripped:a.lengthStripped,hmacHexLower:a.hmacHexLower,lengthLower:a.lengthLower,scanPrefilter:a.scanPrefilter,tags:[]});continue}if(i.action==="rotate"){const c=n.get(i.id);if(!c)continue;c.hmacHex=a.digest,c.length=a.length,c.hmacHexStripped=a.hmacHexStripped,c.lengthStripped=a.lengthStripped,c.hmacHexLower=a.hmacHexLower,c.lengthLower=a.lengthLower,c.scanPrefilter=a.scanPrefilter}}return r}function Rt(){try{return ye(process.argv[1])}catch{return process.argv[1]}}const At=!!process.argv[1]&&import.meta.url===we(Rt()).href;At&&St().then(e=>process.exit(e)).catch(e=>{e instanceof m&&(console.error(`${s.red}${e.message}${s.reset}`),process.exit(2)),console.error(`${s.red}Fatal: ${e instanceof Error?e.message:e}${s.reset}`),process.exit(1)});export{Y as DEFAULT_MIN_LENGTH,st as decodeJwtPayload,et as diffAgainstServer,Ue as entriesFromJsonDocument,Ve as junkVerdict,Xe as normalizeSource,Fe as parseDotenv,Be as parseFlatYaml,oe as parseJsonSecrets,Me as scanBudgetForSecrets,O as sourceTag,it as tokenRoleProblem,ne as unwrapSecretsDocument};