@redaktyn/cli 1.6.1 → 1.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redaktyn/cli",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
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,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import{createHmac as A,pbkdf2 as k,createHash as N,hkdfSync as L}from"node:crypto";import{promisify as O}from"node:util";import*as _ from"node:readline";import{readFileSync as C,existsSync as F,realpathSync as Y}from"node:fs";import{pathToFileURL as K}from"node:url";const U=O(k),i=process.argv.includes("--no-color")||process.env.NO_COLOR,e={reset:i?"":"\x1B[0m",bold:i?"":"\x1B[1m",dim:i?"":"\x1B[2m",red:i?"":"\x1B[31m",green:i?"":"\x1B[32m",yellow:i?"":"\x1B[33m",blue:i?"":"\x1B[34m",magenta:i?"":"\x1B[35m",cyan:i?"":"\x1B[36m",white:i?"":"\x1B[37m",gray:i?"":"\x1B[90m",bgBlue:i?"":"\x1B[44m",underline:i?"":"\x1B[4m"},y=6e5,v=32;function M(o){let r=o.normalize("NFC");return r=r.replace(/\r\n/g,`
2
+ import{createHmac as A,pbkdf2 as k,createHash as N,hkdfSync as L}from"node:crypto";import{promisify as O}from"node:util";import*as _ from"node:readline";import{readFileSync as C,existsSync as F,realpathSync as Y}from"node:fs";import{pathToFileURL as K}from"node:url";const U=O(k),i=process.argv.includes("--no-color")||process.env.NO_COLOR,e={reset:i?"":"\x1B[0m",bold:i?"":"\x1B[1m",dim:i?"":"\x1B[2m",red:i?"":"\x1B[31m",green:i?"":"\x1B[32m",yellow:i?"":"\x1B[33m",blue:i?"":"\x1B[34m",magenta:i?"":"\x1B[35m",cyan:i?"":"\x1B[36m",white:i?"":"\x1B[37m",gray:i?"":"\x1B[90m",bgBlue:i?"":"\x1B[44m",underline:i?"":"\x1B[4m"},b=6e5,v=32;function M(o){let r=o.includes("\x1B")?o.replace(/\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007\u001b]{0,512}(?:\u0007|\u001b\\))/g,""):o;return r=r.normalize("NFC"),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(),E(r)}function E(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 P(o,r){const s=N("sha256").update(r,"utf8").digest();return U(o,s,y,v,"sha256")}const z="redaktyn-project-key-v1";function $e(o,r){if(!r)throw new Error("deriveProjectKey: projectId is required");return Buffer.from(L("sha256",o,Buffer.from(r,"utf8"),Buffer.from(z,"utf8"),v))}function H(o){const r=o.normalize("NFC").replace(/\r\n/g,`
4
+ `),r=r.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u0084\u0086-\u009f\u00ad\u034f\u061c\u115f\u1160\u17b4\u17b5\u180b-\u180f\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff9-\ufffb]/g,""),r=r.trim(),E(r)}function E(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 P(o,r){const s=N("sha256").update(r,"utf8").digest();return U(o,s,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(L("sha256",o,Buffer.from(r,"utf8"),Buffer.from(z,"utf8"),v))}function H(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 E(r)}function B(o){return H(o).toLowerCase()}function b(o,r){return A("sha256",r).update(o,"utf8").digest("hex")}const j="redaktyn-scan-prefilter-v1";function W(o,r){let s=0;for(let t=0;t<r.length;t++){const n=r.charCodeAt(t);s=Math.imul(s,o)+(n>=65&&n<=90?n+32:n)+1|0}return s>>>16}function J(o,r){const t=A("sha256",r).update(j,"utf8").digest().readInt32BE(0)|1,n=a=>W(t,a).toString(16).padStart(4,"0");return`v1.${n(o.exact)}${n(o.collapsed)}${n(o.lower)}`}function S(o,r,s,t){const n=q(r,s);return{label:o,orgId:t,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 s=M(o),t=H(o),n=B(o);return{digest:b(s,r),canonical:s,length:s.length,hmacHexStripped:b(t,r),lengthStripped:t.length,hmacHexLower:b(n,r),lengthLower:n.length,scanPrefilter:J({exact:s,collapsed:t,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 s=2;s<o.length;s++){const t=o[s];t==="--help"||t==="-h"?r.help=!0:t==="--json"?r.json=!0:t==="--no-color"?r.noColor=!0:t==="--org-id"?r.orgId=o[++s]??null:t==="--passphrase"?(r.passphrase=o[++s]??null,r.passphraseFromArgv=!0):t==="--secret"?(r.secret=o[++s]??null,r.secretFromArgv=!0):t==="--label"?r.label=o[++s]??null:t==="--batch"?r.batch=o[++s]??null:t==="--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,s){console.log(` ${e.blue}${e.bold}[${o}]${e.reset} ${e.bold}${r}${e.reset}`),s&&console.log(` ${e.gray}${s}${e.reset}`)}function R(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||i)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 s=0;const t=setInterval(()=>{process.stdout.write(`\r ${e.cyan}${r[s++%r.length]}${e.reset} ${o} `)},80);return{stop:n=>{clearInterval(t),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 E(r)}function B(o){return H(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 s=0;for(let t=0;t<r.length;t++){const n=r.charCodeAt(t);s=Math.imul(s,o)+(n>=65&&n<=90?n+32:n)+1|0}return s>>>16}function J(o,r){const t=A("sha256",r).update(j,"utf8").digest().readInt32BE(0)|1,n=a=>W(t,a).toString(16).padStart(4,"0");return`v1.${n(o.exact)}${n(o.collapsed)}${n(o.lower)}`}function S(o,r,s,t){const n=q(r,s);return{label:o,orgId:t,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 s=M(o),t=H(o),n=B(o);return{digest:y(s,r),canonical:s,length:s.length,hmacHexStripped:y(t,r),lengthStripped:t.length,hmacHexLower:y(n,r),lengthLower:n.length,scanPrefilter:J({exact:s,collapsed:t,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 s=2;s<o.length;s++){const t=o[s];t==="--help"||t==="-h"?r.help=!0:t==="--json"?r.json=!0:t==="--no-color"?r.noColor=!0:t==="--org-id"?r.orgId=o[++s]??null:t==="--passphrase"?(r.passphrase=o[++s]??null,r.passphraseFromArgv=!0):t==="--secret"?(r.secret=o[++s]??null,r.secretFromArgv=!0):t==="--label"?r.label=o[++s]??null:t==="--batch"?r.batch=o[++s]??null:t==="--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 ue(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 he(o,r,s){console.log(` ${e.blue}${e.bold}[${o}]${e.reset} ${e.bold}${r}${e.reset}`),s&&console.log(` ${e.gray}${s}${e.reset}`)}function R(o,r){console.log(` ${e.green}\u2713${e.reset} ${e.bold}${o}${e.reset} ${e.gray}${r}${e.reset}`)}function h(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||i)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 s=0;const t=setInterval(()=>{process.stdout.write(`\r ${e.cyan}${r[s++%r.length]}${e.reset} ${o} `)},80);return{stop:n=>{clearInterval(t),process.stdout.write(`\r ${e.green}\u2713${e.reset} ${n??o}`+" ".repeat(20)+`
7
7
  `)}}}function w(o){return new Promise(r=>{if(process.stdout.write(` ${e.bold}${o}${e.reset} `),!process.stdin.isTTY){let a="";process.stdin.setEncoding("utf8"),process.stdin.once("data",l=>{process.stdin.pause(),r(l.trim())});return}let s="";const t=process.stdin.setRawMode;typeof t=="function"&&process.stdin.setRawMode(!0),process.stdin.resume(),process.stdin.setEncoding("utf8");const n=a=>{for(const l of a){if(l===""&&process.exit(),l==="\r"||l===`
8
8
  `){process.stdin.removeListener("data",n),typeof t=="function"&&process.stdin.setRawMode(!1),process.stdin.pause(),process.stdout.write(`
9
- `),r(s);return}l==="\x7F"||l==="\b"?s.length>0&&(s=s.slice(0,-1),process.stdout.write("\b \b")):(s+=l,process.stdout.write("\u2022"))}};process.stdin.on("data",n)})}function f(o,r){return new Promise(s=>{const t=_.createInterface({input:process.stdin,output:process.stdout}),n=r?` ${e.bold}${o}${e.reset} ${e.gray}[${r}]${e.reset} `:` ${e.bold}${o}${e.reset} `;t.question(n,a=>{t.close(),s(a.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 I(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 ${y.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(`
9
+ `),r(s);return}l==="\x7F"||l==="\b"?s.length>0&&(s=s.slice(0,-1),process.stdout.write("\b \b")):(s+=l,process.stdout.write("\u2022"))}};process.stdin.on("data",n)})}function f(o,r){return new Promise(s=>{const t=_.createInterface({input:process.stdin,output:process.stdout}),n=r?` ${e.bold}${o}${e.reset} ${e.gray}[${r}]${e.reset} `:` ${e.bold}${o}${e.reset} `;t.question(n,a=>{t.close(),s(a.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 I(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(`
10
10
  `).forEach(s=>console.log(` ${e.green}${s}${e.reset}`)),console.log(),console.log(c())}function m(o){return{label:o.label,hmacHex:o.hmacHex,length:o.length,hmacHexStripped:o.hmacHexStripped,lengthStripped:o.lengthStripped,hmacHexLower:o.hmacHexLower,lengthLower:o.lengthLower,...o.scanPrefilter?{scanPrefilter:o.scanPrefilter}:{}}}function D(o,r){if(r){console.log(JSON.stringify(o.map(m),null,2));return}console.log(),console.log(c()),console.log(` ${e.green}${e.bold}\u2705 ${o.length} fingerprint(s) generated${e.reset}`),console.log(c()),console.log(),o.forEach((s,t)=>{console.log(` ${e.bold}${e.cyan}[${t+1}] ${s.label}${e.reset}`),console.log(` hmacHex ${e.yellow}${s.hmacHex.slice(0,16)}\u2026${s.hmacHex.slice(-8)}${e.reset}`),console.log(` length ${s.length}`),console.log()}),console.log(` ${e.bold}${e.blue}Full JSON (paste into API /api/secrets or Import Digest):${e.reset}`),console.log(),JSON.stringify(o.map(m),null,4).split(`
11
11
  `).forEach(s=>console.log(` ${e.green}${s}${e.reset}`)),console.log(),console.log(c())}function re(){console.log(`
12
12
  ${e.bold}${e.cyan}Redaktyn Fingerprint CLI${e.reset} \u2014 Zero-knowledge local secret digest generator
@@ -39,7 +39,7 @@ ${e.bold}OPTIONS${e.reset}
39
39
 
40
40
  ${e.bold}HOW IT WORKS${e.reset}
41
41
 
42
- 1. Your passphrase + org ID \u2192 PBKDF2-SHA256 (${y.toLocaleString("en-US")} iters) \u2192 256-bit HMAC key
42
+ 1. Your passphrase + org ID \u2192 PBKDF2-SHA256 (${b.toLocaleString("en-US")} iters) \u2192 256-bit HMAC key
43
43
  2. Secret \u2192 NFC normalize + strip invisible Unicode + trim + UUID case-fold
44
44
  3. HMAC-SHA256 over that, plus two bypass variants:
45
45
  ${e.bold}hmacHex${e.reset} exact canonical form
@@ -79,5 +79,5 @@ ${e.bold}SECURITY NOTE${e.reset}
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 se(o){F(o)||(console.error(`${e.red}Error: batch file not found: ${o}${e.reset}`),process.exit(1));const r=C(o,"utf8").split(`
82
- `),s=[];for(const[t,n]of r.entries()){const a=n.trim();if(!a||a.startsWith("#"))continue;const l=a.indexOf(":");if(l<1){u(`Skipping malformed line ${t+1} (expected LABEL:value).`);continue}s.push({label:a.slice(0,l).trim(),secret:a.slice(l+1)})}return s}const x=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function te(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||!x.test(r))for(r=await f("Org ID (UUID format, e.g. a1b2c3d4-\u2026):");!x.test(r.trim());)u("That doesn't look like a valid UUID. Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"),r=await f("Org ID:");else R("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(` ${e.yellow}Derives the key from the passphrase alone. For a workspace whose key is locked with the server${e.reset}`),console.log(` ${e.yellow}(Redaktyn 1.6.0+), use redaktyn-import instead.${e.reset}`),console.log();let s=(process.env.REDAKTYN_PASSPHRASE&&String(process.env.REDAKTYN_PASSPHRASE))??"";if(s)R("Passphrase","\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (loaded from REDAKTYN_PASSPHRASE)");else for(s=await w("Passphrase (hidden):");!s||s.length<8;)u("Passphrase must be at least 8 characters."),s=await w("Passphrase:");const t=[];let n=!0,a=0;for(;n;){a++,console.log(),console.log(c("\xB7")),console.log(` ${e.bold}${e.magenta}STEP ${a===1?"3":a+2} \u2014 Secret #${a}${e.reset}`),console.log();const d=await f(`Label (human-readable name, e.g. AWS_SECRET_KEY${a>1,""}):`);if(!d){u("Label cannot be empty \u2014 skipping.");continue}const $=await w("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 P(s,r),p.stop("HMAC key derived "+e.gray+"(256-bit, browser-identical)"+e.reset)}catch(T){p.stop(),console.error(`${e.red}Key derivation failed: ${T.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}`),t.push(g),console.log(),n=await oe("Add another secret with the same passphrase?")}const l=o.json;t.length===1?I(t[0],l):D(t,l),l||(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||!x.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 s=process.env.REDAKTYN_PASSPHRASE&&String(process.env.REDAKTYN_PASSPHRASE)||o.passphrase?.trim()||"";s||(console.error(`${e.red}Error: set REDAKTYN_PASSPHRASE env var or use --passphrase.${e.reset}`),process.exit(1)),console.error(`${e.dim}Derives the key from the passphrase alone. For a workspace whose key is locked with the server (Redaktyn 1.6.0+), use redaktyn-import instead.${e.reset}`);const t=await P(s,r);if(o.batch){const d=se(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,t,r));D($,o.json);return}const n=o.label?.trim(),a=o.secret;n||(console.error(`${e.red}Error: --label is required in non-interactive mode.${e.reset}`),process.exit(1));let l=a;l==null&&(process.stdin.isTTY||(l=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",$)}))),l||(console.error(`${e.red}Error: --secret is required (or pipe on stdin).${e.reset}`),process.exit(1)),I(S(n,l,t,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 te(o):await ne(o)}function ae(){try{return Y(process.argv[1])}catch{return process.argv[1]}}const ie=!!process.argv[1]&&import.meta.url===K(ae()).href;ie&&le().catch(o=>{console.error(`${e.red}Fatal: ${o instanceof Error?o.message:o}${e.reset}`),process.exit(1)});export{y as PBKDF2_ITERATIONS,z as PROJECT_KDF_LABEL,M as canonicalize,q as computeHmac,P as deriveKey,$e as deriveProjectKey,m as enrollPayload,S as fingerprint,J as scanPrefilter};
82
+ `),s=[];for(const[t,n]of r.entries()){const a=n.trim();if(!a||a.startsWith("#"))continue;const l=a.indexOf(":");if(l<1){h(`Skipping malformed line ${t+1} (expected LABEL:value).`);continue}s.push({label:a.slice(0,l).trim(),secret:a.slice(l+1)})}return s}const x=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function te(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||!x.test(r))for(r=await f("Org ID (UUID format, e.g. a1b2c3d4-\u2026):");!x.test(r.trim());)h("That doesn't look like a valid UUID. Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"),r=await f("Org ID:");else R("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(` ${e.yellow}Derives the key from the passphrase alone. For a workspace whose key is locked with the server${e.reset}`),console.log(` ${e.yellow}(Redaktyn 1.6.0+), use redaktyn-import instead.${e.reset}`),console.log();let s=(process.env.REDAKTYN_PASSPHRASE&&String(process.env.REDAKTYN_PASSPHRASE))??"";if(s)R("Passphrase","\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (loaded from REDAKTYN_PASSPHRASE)");else for(s=await w("Passphrase (hidden):");!s||s.length<8;)h("Passphrase must be at least 8 characters."),s=await w("Passphrase:");const t=[];let n=!0,a=0;for(;n;){a++,console.log(),console.log(c("\xB7")),console.log(` ${e.bold}${e.magenta}STEP ${a===1?"3":a+2} \u2014 Secret #${a}${e.reset}`),console.log();const d=await f(`Label (human-readable name, e.g. AWS_SECRET_KEY${a>1,""}):`);if(!d){h("Label cannot be empty \u2014 skipping.");continue}const $=await w("Secret value (hidden):");if(!$){h("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 u;try{u=await P(s,r),p.stop("HMAC key derived "+e.gray+"(256-bit, browser-identical)"+e.reset)}catch(T){p.stop(),console.error(`${e.red}Key derivation failed: ${T.message}${e.reset}`),process.exit(1)}const g=S(d,$,u,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}`),t.push(g),console.log(),n=await oe("Add another secret with the same passphrase?")}const l=o.json;t.length===1?I(t[0],l):D(t,l),l||(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||!x.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 s=process.env.REDAKTYN_PASSPHRASE&&String(process.env.REDAKTYN_PASSPHRASE)||o.passphrase?.trim()||"";s||(console.error(`${e.red}Error: set REDAKTYN_PASSPHRASE env var or use --passphrase.${e.reset}`),process.exit(1)),console.error(`${e.dim}Derives the key from the passphrase alone. For a workspace whose key is locked with the server (Redaktyn 1.6.0+), use redaktyn-import instead.${e.reset}`);const t=await P(s,r);if(o.batch){const d=se(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:u})=>S(p,u,t,r));D($,o.json);return}const n=o.label?.trim(),a=o.secret;n||(console.error(`${e.red}Error: --label is required in non-interactive mode.${e.reset}`),process.exit(1));let l=a;l==null&&(process.stdin.isTTY||(l=await new Promise((d,$)=>{let p="";process.stdin.setEncoding("utf8"),process.stdin.on("data",u=>p+=u),process.stdin.on("end",()=>d(p.trim())),process.stdin.on("error",$)}))),l||(console.error(`${e.red}Error: --secret is required (or pipe on stdin).${e.reset}`),process.exit(1)),I(S(n,l,t,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 te(o):await ne(o)}function ae(){try{return Y(process.argv[1])}catch{return process.argv[1]}}const ie=!!process.argv[1]&&import.meta.url===K(ae()).href;ie&&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,P as deriveKey,$e as deriveProjectKey,m as enrollPayload,S as fingerprint,J as scanPrefilter};
@@ -1,26 +1,26 @@
1
1
  #!/usr/bin/env node
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,deriveProjectKey as be,enrollPayload as Se}from"./redaktyn-fingerprint.mjs";import{freshAdminSession as je,freshSession as Re,loadAdminSession as B,loadSession as Ee,resolveServerUrl as Ne}from"./redaktyn-session.mjs";import{askWorkspaceKey as Ae}from"./redaktyn-passphrase.mjs";import{banner as xe,bar as Te,box as U,g as C,humanBytes as z,k as g,rich as R,reveal as Oe,rows as Y,say as k,step as G}from"./redaktyn-ui.mjs";import{createRequire as Ie}from"node:module";const Le=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 y extends Error{}const V=15e5,Z=5e4,_e=1e4,D=5*1024*1024,Pe=256,Ce=65536,De=/^v1\.[0-9a-f]{12}$/,X=e=>typeof e.scanPrefilter=="string"&&De.test(e.scanPrefilter);function Q(e,t=!1){const o=[new Map,new Map,new Map],r=(n,i,u)=>{if(!(i>0))return;const l=o[n].get(i);o[n].set(i,l?{windowLen:i,prefiltered:l.prefiltered&&u,entries:l.entries+1}:{windowLen:i,prefiltered:u,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/Pe)+Math.ceil(n*r.entries/Ce):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 He(e){return e.length===0||e.some(t=>t&&Object.prototype.hasOwnProperty.call(t,"scanPrefilter"))}function We(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 Ke(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 y(`${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 y(`Unknown flag: ${n}`)}if(t.minLength!==null&&(!Number.isInteger(t.minLength)||t.minLength<1))throw new y("--min-length must be a positive integer");return t}const Ye=/^[A-Za-z_][A-Za-z0-9_.-]*$/;function Je(e){const t=e.replace(/\r\n/g,`
2
+ import{readFileSync as ke,realpathSync as be}from"node:fs";import{basename as Y}from"node:path";import*as ve from"node:readline";import{pathToFileURL as Se}from"node:url";import{canonicalize as je,computeHmac as Re,deriveProjectKey as Ee,enrollPayload as Ne}from"./redaktyn-fingerprint.mjs";import{freshAdminSession as Ae,freshSession as xe,loadAdminSession as G,loadSession as Te,resolveServerUrl as Oe}from"./redaktyn-session.mjs";import{askWorkspaceKey as Ie}from"./redaktyn-passphrase.mjs";import{banner as Le,bar as _e,box as z,g as D,humanBytes as V,k as g,rich as E,reveal as Pe,rows as J,say as w,step as Z}from"./redaktyn-ui.mjs";import{createRequire as Ce}from"node:module";const X=Ce(import.meta.url),De=X("./package.json").version;function Me(){try{const e=X("./redaktyn-shared.cjs");return typeof e.assessSecretGuessability=="function"?e.assessSecretGuessability:null}catch{return null}}const A=process.argv.includes("--no-color")||!!process.env.NO_COLOR,s={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",cyan:A?"":"\x1B[36m",gray:A?"":"\x1B[90m"};class y extends Error{}const Q=15e5,ee=5e4,He=1e4,M=5*1024*1024,We=256,Ke=65536,Ye=/^v1\.[0-9a-f]{12}$/,te=e=>typeof e.scanPrefilter=="string"&&Ye.test(e.scanPrefilter);function ne(e,t=!1){const r=[new Map,new Map,new Map],o=(n,i,p)=>{if(!(i>0))return;const l=r[n].get(i);r[n].set(i,l?{windowLen:i,prefiltered:l.prefiltered&&p,entries:l.entries+1}:{windowLen:i,prefiltered:p,entries:1})};for(const n of e){const i=t||te(n);n.hmacHex&&o(0,n.length,i),typeof n.hmacHexStripped=="string"&&o(1,n.lengthStripped,i),typeof n.hmacHexLower=="string"&&o(2,n.lengthLower,i)}return r.flatMap(n=>[...n.values()])}function oe(e,t){let r=0;for(const o of t){const n=Math.max(0,e-o.windowLen+1);r+=o.prefiltered?Math.ceil(n/We)+Math.ceil(n*o.entries/Ke):n}return r}function re(e){if(e.length===0)return 1/0;if(oe(M,e)<=Q)return M;let t=0,r=M;for(;t<r;){const o=Math.ceil((t+r)/2);oe(o,e)<=Q?t=o:r=o-1}return t}function Je(e){const t=ne(e),r=re(t);return{windowLengths:t.length,ceiling:r,legacySecrets:e.filter(o=>o.hmacHex&&!te(o)).length,ceilingIfReenrolled:re(ne(e,!0)),atCharacterCap:r===M}}function qe(e){return e.length===0||e.some(t=>t&&Object.prototype.hasOwnProperty.call(t,"scanPrefilter"))}function Fe(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 Be(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},r=(o,n)=>{const i=e[o];if(i===void 0||i.startsWith("--"))throw new y(`${n} needs a value`);return i};for(let o=2;o<e.length;o++){const n=e[o];if(n==="--help"||n==="-h")t.help=!0;else if(n==="--env")t.env=r(++o,"--env");else if(n==="--json")t.jsonFile=r(++o,"--json");else if(n==="--yaml")t.yaml=r(++o,"--yaml");else if(n==="--stdin")t.stdin=!0;else if(n==="--source")t.source=r(++o,"--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(r(++o,"--min-length"));else if(n==="--allow-weak")t.allowWeak=!0;else if(n==="--only")t.only.push(r(++o,"--only"));else if(n==="--exclude")t.exclude.push(r(++o,"--exclude"));else if(n==="--server")t.server=r(++o,"--server");else if(n==="--org-id")t.orgId=r(++o,"--org-id");else if(n==="--project")t.project=r(++o,"--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 y(`Unknown flag: ${n}`)}if(t.minLength!==null&&(!Number.isInteger(t.minLength)||t.minLength<1))throw new y("--min-length must be a positive integer");return t}const Ue=/^[A-Za-z_][A-Za-z0-9_.-]*$/;function Ge(e){const t=e.replace(/\r\n/g,`
3
3
  `).replace(/\r/g,`
4
- `),o=[],r=[];let n=0,i=1;const u=()=>{const d=t.indexOf(`
5
- `,n);n=d===-1?t.length:d+1},l=(d,a)=>{let p=0;for(let m=d;m<a;m++)t[m]===`
6
- `&&p++;return p};for(;n<t.length;){const d=t[n];if(d===`
7
- `){i++,n++;continue}if(d===" "||d===" "){n++;continue}if(d==="#"){u(),i++;continue}const a=i;let p=n;for(;p<t.length&&t[p]!=="="&&t[p]!==`
8
- `;)p++;if(p>=t.length||t[p]===`
9
- `){r.push({line:a,message:"no '=' on this line \u2014 skipped"}),n=p+1,i++;continue}let m=t.slice(n,p).trim();for(/^export\s/.test(m)&&(m=m.replace(/^export\s+/,"").trim()),n=p+1;n<t.length&&(t[n]===" "||t[n]===" ");)n++;let $="",c=!0;const f=t[n];if(f==='"'||f==="'"||f==="`"){const v=++n,w=t.indexOf(`
10
- `,n);let S=!1;for(;n<t.length;){const b=t[n];if(b==="\\"&&f==='"'&&n+1<t.length){const j=t[n+1];$+=j==="n"?`
11
- `:j==="t"?" ":j==="r"?"\r":j==='"'||j==="\\"?j:`\\${j}`,n+=2;continue}if(b===f){n++,S=!0;break}$+=b,n++}S?(i+=l(v,n),u(),i++):(r.push({line:a,message:"unterminated quote \u2014 skipped this entry"}),c=!1,n=w===-1?t.length:w+1,i++)}else{const v=t.indexOf(`
12
- `,n),w=v===-1?t.length:v;let S=t.slice(n,w);const b=S.search(/\s#/);b!==-1&&(S=S.slice(0,b)),$=S.trim(),n=w===t.length?w:w+1,i++}if(c){if(!Ye.test(m)){r.push({line:a,message:`unusable key name ${JSON.stringify(m)} \u2014 skipped`});continue}o.push({key:m,value:$,line:a})}}return{entries:o,problems:r}}const Fe=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 y("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 y("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=>Fe.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 qe(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(u=>typeof n[u]=="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(u){throw new y(`Input is not valid JSON: ${u.message}`)}const{shape:o,doc:r}=ne(t),{entries:n,problems:i}=qe(r);return{entries:n,problems:i,shape:o}}function Be(e){const t=e[0];for(let o=1;o<e.length;o++){if(t==='"'&&e[o]==="\\"){o++;continue}if(e[o]===t){if(t==="'"&&e[o+1]==="'"){o++;continue}return o}}return-1}function Ue(e){const t=[],o=[];return e.replace(/\r\n/g,`
4
+ `),r=[],o=[];let n=0,i=1;const p=()=>{const d=t.indexOf(`
5
+ `,n);n=d===-1?t.length:d+1},l=(d,a)=>{let u=0;for(let m=d;m<a;m++)t[m]===`
6
+ `&&u++;return u};for(;n<t.length;){const d=t[n];if(d===`
7
+ `){i++,n++;continue}if(d===" "||d===" "){n++;continue}if(d==="#"){p(),i++;continue}const a=i;let u=n;for(;u<t.length&&t[u]!=="="&&t[u]!==`
8
+ `;)u++;if(u>=t.length||t[u]===`
9
+ `){o.push({line:a,message:"no '=' on this line \u2014 skipped"}),n=u+1,i++;continue}let m=t.slice(n,u).trim();for(/^export\s/.test(m)&&(m=m.replace(/^export\s+/,"").trim()),n=u+1;n<t.length&&(t[n]===" "||t[n]===" ");)n++;let $="",c=!0;const f=t[n];if(f==='"'||f==="'"||f==="`"){const k=++n,v=t.indexOf(`
10
+ `,n);let b=!1;for(;n<t.length;){const S=t[n];if(S==="\\"&&f==='"'&&n+1<t.length){const j=t[n+1];$+=j==="n"?`
11
+ `:j==="t"?" ":j==="r"?"\r":j==='"'||j==="\\"?j:`\\${j}`,n+=2;continue}if(S===f){n++,b=!0;break}$+=S,n++}b?(i+=l(k,n),p(),i++):(o.push({line:a,message:"unterminated quote \u2014 skipped this entry"}),c=!1,n=v===-1?t.length:v+1,i++)}else{const k=t.indexOf(`
12
+ `,n),v=k===-1?t.length:k;let b=t.slice(n,v);const S=b.search(/\s#/);S!==-1&&(b=b.slice(0,S)),$=b.trim(),n=v===t.length?v:v+1,i++}if(c){if(!Ue.test(m)){o.push({line:a,message:`unusable key name ${JSON.stringify(m)} \u2014 skipped`});continue}r.push({key:m,value:$,line:a})}}return{entries:r,problems:o}}const ze=new Set(["data","metadata","lease_id","lease_duration","lease_renewable","renewable","request_id","warnings","auth","wrap_info","mount_type"]),_=e=>typeof e=="object"&&e!==null&&!Array.isArray(e);function se(e){if(!_(e))throw new y("Expected a JSON object of secrets, got "+ie(e));if(typeof e.SecretString=="string"){let o;try{o=JSON.parse(e.SecretString)}catch{throw new y("SecretString is not JSON. For a single-value secret pass it to redaktyn-fingerprint instead.")}return se(o)}if(Array.isArray(e.fields)){const o={};for(const n of e.fields){if(!_(n))continue;const i=n.label??n.id;typeof i!="string"||typeof n.value!="string"||(o[i]=n.value)}return{shape:"1password-item",doc:o}}const t=Object.keys(e);if(t.length>0&&t.every(o=>ze.has(o))&&_(e.data)){const o=e.data;return _(o.data)&&"metadata"in o?{shape:"vault-kv2",doc:o.data}:{shape:"vault-kv1",doc:o}}return{shape:"flat",doc:e}}function ie(e){return e===null?"null":Array.isArray(e)?"an array":typeof e}function Ve(e){const t=[],r=[];for(const[o,n]of Object.entries(e)){if(typeof n=="string"){t.push({key:o,value:n});continue}if(typeof n=="number"||typeof n=="boolean"){t.push({key:o,value:String(n)});continue}if(_(n)){const i=["value","raw","computed","computedValue"].find(p=>typeof n[p]=="string");if(i){t.push({key:o,value:n[i]});continue}r.push({key:o,message:"nested object with no value/raw/computed field \u2014 skipped"});continue}r.push({key:o,message:`${ie(n)} is not a secret value \u2014 skipped`})}return{entries:t,problems:r}}function ae(e){let t;try{t=JSON.parse(e)}catch(p){throw new y(`Input is not valid JSON: ${p.message}`)}const{shape:r,doc:o}=se(t),{entries:n,problems:i}=Ve(o);return{entries:n,problems:i,shape:r}}function Ze(e){const t=e[0];for(let r=1;r<e.length;r++){if(t==='"'&&e[r]==="\\"){r++;continue}if(e[r]===t){if(t==="'"&&e[r+1]==="'"){r++;continue}return r}}return-1}function Xe(e){const t=[],r=[];return e.replace(/\r\n/g,`
13
13
  `).replace(/\r/g,`
14
14
  `).split(`
15
- `).forEach((n,i)=>{const u=i+1;if(n.trim()===""||n.trim().startsWith("#")||n.trim()==="---"||n.trim()==="...")return;if(/^\s/.test(n))throw new y(`${u}: 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 y(`${u}: YAML lists are not supported. Use --json.`);if(/^[|>]/.test(n.trim()))throw new y(`${u}: YAML block scalars are not supported. Use --json.`);const l=/^([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s?(.*)$/.exec(n);if(!l){o.push({line:u,message:"not a `key: value` line \u2014 skipped"});return}const d=l[1];let a=l[2]??"";if(a.trim()===""||a.trim()==="|"||a.trim()===">")throw new y(`${u}: "${d}" has no inline value, which in YAML means a nested block or null. Refusing rather than registering an empty fingerprint.`);const p=a.trim(),m=p[0]==='"'||p[0]==="'"?Be(p):-1;if(m!==-1&&/^(\s+#.*)?$/.test(p.slice(m+1))){const $=p.slice(1,m);a=p[0]==='"'?$.replace(/\\(.)/g,(c,f)=>f==="n"?`
16
- `:f==="t"?" ":f==="r"?"\r":f):$.replace(/''/g,"'")}else if(p.startsWith('"')&&p.endsWith('"')&&p.length>1||p.startsWith("'")&&p.endsWith("'")&&p.length>1)a=p.slice(1,-1),p[0]==='"'&&(a=a.replace(/\\n/g,`
17
- `).replace(/\\t/g," ").replace(/\\(.)/g,"$1"));else{const $=p.search(/\s#/);a=($===-1?p:p.slice(0,$)).trim()}t.push({key:d,value:a,line:u})}),{entries:t,problems:o}}const J=12,ze=new Set(["DOPPLER_PROJECT","DOPPLER_ENVIRONMENT","DOPPLER_CONFIG"]),Ge=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 Ve(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 Ze(e,{minLength:t=J}={}){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(Ge.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=Ve(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 Xe(){if(process.stdin.isTTY)throw new y("--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 y(`${t} ${e}: ${o.code==="ENOENT"?"no such file":o.message}`)}}function O(e){return`import:${e}`}function Qe(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(et).join(".*")+"$","i").test(e):e.toLowerCase()===o.toLowerCase())}const et=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function tt({candidates:e,existing:t,source:o}){const r=O(o),n=new Map;for(const c of t)c.hmacHex&&n.set(c.hmacHex,c);const i=t.filter(c=>Array.isArray(c.tags)&&c.tags.includes(r)),u=new Map(i.map(c=>[c.label,c])),l=new Map;for(const c of t)i.includes(c)||l.has(c.label)||l.set(c.label,c);const d=[],a=new Set,p=new Map;for(const c of e){const{key:f,digests:v}=c;a.add(f);const w=p.get(v.digest);if(w){d.push({key:f,action:u.has(f)?"conflict":"skip",reason:`same value as "${w}" in this import`,...u.has(f)?{id:u.get(f).id}:{},length:v.length});continue}const S=d.length;m(c);const b=d[S];b&&(b.action==="create"||b.action==="rotate")&&p.set(v.digest,f)}function m({key:c,digests:f}){const v=u.get(c);if(v){if(v.hmacHex===f.digest&&nt(v,f))d.push({key:c,action:"skip",reason:"already registered and unchanged",id:v.id,length:f.length});else{const b=n.get(f.digest);if(b&&b.id!==v.id)d.push({key:c,action:"conflict",reason:`new value is already registered as "${b.label}"`,id:v.id,length:f.length});else{const j=v.hmacHex===f.digest?"value unchanged; adding missing variants":"value changed";d.push({key:c,action:"rotate",reason:j,id:v.id,length:f.length})}}return}const w=n.get(f.digest);if(w){d.push({key:c,action:"skip",reason:`already registered as "${w.label}"`,id:w.id,length:f.length});return}const S=l.get(c);if(S){d.push({key:c,action:"conflict",reason:"a secret with this label exists outside this import source",id:S.id,length:f.length});return}d.push({key:c,action:"create",reason:"new",length:f.length})}const $=i.filter(c=>!a.has(c.label)).map(c=>({label:c.label,id:c.id,status:c.status}));return{plan:d,missing:$}}function nt(e,t){return e.hmacHexStripped===t.hmacHexStripped&&e.hmacHexLower===t.hmacHexLower&&e.scanPrefilter===t.scanPrefilter}const ae=e=>e.replace(/\/+$/,"");async function M(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(d){throw new y(`Cannot reach ${ae(e)}: ${d.message}`)}const u=await i.text();let l=null;if(u)try{l=JSON.parse(u)}catch{l=null}return{status:i.status,ok:i.ok,data:l,text:u}}async function rt(e,t,o){const r=await M(e,t,"GET","/api/projects");if(!r.ok||!Array.isArray(r.data))throw new y(`GET /api/projects failed: ${r.status} ${String(r.text).slice(0,200)}`);const n=o.trim().toLowerCase(),i=r.data.filter(l=>l.id===o.trim()),u=i.length?i:r.data.filter(l=>String(l.name).toLowerCase()===n);if(u.length===0)throw new y(`No project named or numbered "${o}". Known: ${r.data.map(l=>l.name).join(", ")||"none"}.`);if(u.length>1)throw new y(`"${o}" matches ${u.length} projects. Pass the project id instead.`);if(u[0].status==="CLOSED")throw new y(`Project "${u[0].name}" is closed. Reopen it before enrolling into it.`);return u[0]}async function ot({plan:e,existing:t,serverUrl:o,token:r}){const n=e.filter(l=>l.action==="rotate"),i=l=>t.find(d=>d.id===l.id)?.projectId??null;if(!n.some(i))return;const u=await M(o,r,"GET","/api/projects");if(!(!u.ok||!Array.isArray(u.data)))for(const l of n){const d=i(l);if(!d)continue;const a=u.data.find(p=>p.id===d);a&&(a.usesOrgKey?l.sendProjectId=d:(l.action="conflict",l.reason=`lives in project "${a.name}", which has its own key; re-run with --project "${a.name}"`))}}async function st(e,t,o=!1){const r=await M(e,t,"GET","/api/secrets");if(o&&(r.status===401||r.status===403))throw new y("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 y("Server rejected the token (401). REDAKTYN_TOKEN must be a current OWNER or ADMIN login token.");if(r.status===403)throw new y("This token is not allowed to manage secrets (403). Importing needs an OWNER or ADMIN token.");if(!r.ok)throw new y(`GET /api/secrets failed: ${r.status} ${r.text.slice(0,200)}`);if(!Array.isArray(r.data))throw new y("GET /api/secrets did not return a list.");return r.data}function it(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 at(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 p=i+1;if(n.trim()===""||n.trim().startsWith("#")||n.trim()==="---"||n.trim()==="...")return;if(/^\s/.test(n))throw new y(`${p}: 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 y(`${p}: YAML lists are not supported. Use --json.`);if(/^[|>]/.test(n.trim()))throw new y(`${p}: YAML block scalars are not supported. Use --json.`);const l=/^([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s?(.*)$/.exec(n);if(!l){r.push({line:p,message:"not a `key: value` line \u2014 skipped"});return}const d=l[1];let a=l[2]??"";if(a.trim()===""||a.trim()==="|"||a.trim()===">")throw new y(`${p}: "${d}" has no inline value, which in YAML means a nested block or null. Refusing rather than registering an empty fingerprint.`);const u=a.trim(),m=u[0]==='"'||u[0]==="'"?Ze(u):-1;if(m!==-1&&/^(\s+#.*)?$/.test(u.slice(m+1))){const $=u.slice(1,m);a=u[0]==='"'?$.replace(/\\(.)/g,(c,f)=>f==="n"?`
16
+ `:f==="t"?" ":f==="r"?"\r":f):$.replace(/''/g,"'")}else if(u.startsWith('"')&&u.endsWith('"')&&u.length>1||u.startsWith("'")&&u.endsWith("'")&&u.length>1)a=u.slice(1,-1),u[0]==='"'&&(a=a.replace(/\\n/g,`
17
+ `).replace(/\\t/g," ").replace(/\\(.)/g,"$1"));else{const $=u.search(/\s#/);a=($===-1?u:u.slice(0,$)).trim()}t.push({key:d,value:a,line:p})}),{entries:t,problems:r}}const q=12,Qe=new Set(["DOPPLER_PROJECT","DOPPLER_ENVIRONMENT","DOPPLER_CONFIG"]),et=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 le(e){if(e.length===0)return 0;const t=new Map;for(const o of e)t.set(o,(t.get(o)??0)+1);let r=0;for(const o of t.values()){const n=o/e.length;r-=n*Math.log2(n)}return r}function tt(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(o=>o.length>=16&&le(o)>=2.5)?{ok:!0}:{ok:!1,reason:"URL with no embedded credential or token"}}function nt(e,{minLength:t=q}={}){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(et.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 r=tt(e);if(r&&!r.ok)return{ok:!1,reason:r.reason};if(!r){const o=new Set(e).size;if(o<5)return{ok:!1,reason:`only ${o} distinct characters`};if(le(e)<2)return{ok:!1,reason:"very low character diversity"}}return{ok:!0}}async function ot(){if(process.stdin.isTTY)throw new y("--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 ke(e,"utf8")}catch(r){throw new y(`${t} ${e}: ${r.code==="ENOENT"?"no such file":r.message}`)}}function O(e){return`import:${e}`}function rt(e){return String(e).replace(/^[./]+/,"").replace(/[^A-Za-z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60)||"import"}function ce(e,t){return t.some(r=>r.includes("*")?new RegExp("^"+r.split("*").map(st).join(".*")+"$","i").test(e):e.toLowerCase()===r.toLowerCase())}const st=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function it({candidates:e,existing:t,source:r}){const o=O(r),n=new Map;for(const c of t)c.hmacHex&&n.set(c.hmacHex,c);const i=t.filter(c=>Array.isArray(c.tags)&&c.tags.includes(o)),p=new Map(i.map(c=>[c.label,c])),l=new Map;for(const c of t)i.includes(c)||l.has(c.label)||l.set(c.label,c);const d=[],a=new Set,u=new Map;for(const c of e){const{key:f,digests:k}=c;a.add(f);const v=u.get(k.digest);if(v){d.push({key:f,action:p.has(f)?"conflict":"skip",reason:`same value as "${v}" in this import`,...p.has(f)?{id:p.get(f).id}:{},length:k.length});continue}const b=d.length;m(c);const S=d[b];S&&(S.action==="create"||S.action==="rotate")&&u.set(k.digest,f)}function m({key:c,digests:f}){const k=p.get(c);if(k){if(k.hmacHex===f.digest&&at(k,f))d.push({key:c,action:"skip",reason:"already registered and unchanged",id:k.id,length:f.length});else{const S=n.get(f.digest);if(S&&S.id!==k.id)d.push({key:c,action:"conflict",reason:`new value is already registered as "${S.label}"`,id:k.id,length:f.length});else{const j=k.hmacHex===f.digest?"value unchanged; adding missing variants":"value changed";d.push({key:c,action:"rotate",reason:j,id:k.id,length:f.length})}}return}const v=n.get(f.digest);if(v){d.push({key:c,action:"skip",reason:`already registered as "${v.label}"`,id:v.id,length:f.length});return}const b=l.get(c);if(b){d.push({key:c,action:"conflict",reason:"a secret with this label exists outside this import source",id:b.id,length:f.length});return}d.push({key:c,action:"create",reason:"new",length:f.length})}const $=i.filter(c=>!a.has(c.label)).map(c=>({label:c.label,id:c.id,status:c.status}));return{plan:d,missing:$}}function at(e,t){return e.hmacHexStripped===t.hmacHexStripped&&e.hmacHexLower===t.hmacHexLower&&e.scanPrefilter===t.scanPrefilter}const de=e=>e.replace(/\/+$/,"");async function H(e,t,r,o,n){let i;try{i=await fetch(`${de(e)}${o}`,{method:r,headers:{Authorization:`Bearer ${t}`,...n?{"Content-Type":"application/json"}:{}},...n?{body:JSON.stringify(n)}:{}})}catch(d){throw new y(`Cannot reach ${de(e)}: ${d.message}`)}const p=await i.text();let l=null;if(p)try{l=JSON.parse(p)}catch{l=null}return{status:i.status,ok:i.ok,data:l,text:p}}async function lt(e,t,r){const o=await H(e,t,"GET","/api/projects");if(!o.ok||!Array.isArray(o.data))throw new y(`GET /api/projects failed: ${o.status} ${String(o.text).slice(0,200)}`);const n=r.trim().toLowerCase(),i=o.data.filter(l=>l.id===r.trim()),p=i.length?i:o.data.filter(l=>String(l.name).toLowerCase()===n);if(p.length===0)throw new y(`No project named or numbered "${r}". Known: ${o.data.map(l=>l.name).join(", ")||"none"}.`);if(p.length>1)throw new y(`"${r}" matches ${p.length} projects. Pass the project id instead.`);if(p[0].status==="CLOSED")throw new y(`Project "${p[0].name}" is closed. Reopen it before enrolling into it.`);return p[0]}async function ct({plan:e,existing:t,serverUrl:r,token:o}){const n=e.filter(l=>l.action==="rotate"),i=l=>t.find(d=>d.id===l.id)?.projectId??null;if(!n.some(i))return;const p=await H(r,o,"GET","/api/projects");if(!(!p.ok||!Array.isArray(p.data)))for(const l of n){const d=i(l);if(!d)continue;const a=p.data.find(u=>u.id===d);a&&(a.usesOrgKey?l.sendProjectId=d:(l.action="conflict",l.reason=`lives in project "${a.name}", which has its own key; re-run with --project "${a.name}"`))}}async function dt(e,t,r=!1){const o=await H(e,t,"GET","/api/secrets");if(r&&(o.status===401||o.status===403))throw new y("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(o.status===401)throw new y("Server rejected the token (401). REDAKTYN_TOKEN must be a current OWNER or ADMIN login token.");if(o.status===403)throw new y("This token is not allowed to manage secrets (403). Importing needs an OWNER or ADMIN token.");if(!o.ok)throw new y(`GET /api/secrets failed: ${o.status} ${o.text.slice(0,200)}`);if(!Array.isArray(o.data))throw new y("GET /api/secrets did not return a list.");return o.data}function ut(e){try{const t=e.split(".")[1];if(!t)return null;const r=t+"=".repeat((4-t.length%4)%4),o=Buffer.from(r.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8");return JSON.parse(o)}catch{return null}}function pt(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.
18
18
  Get an OWNER or ADMIN login token instead:
19
19
  curl -s -X POST "$REDAKTYN_SERVER_URL/api/auth/login" \\
20
20
  -H 'Content-Type: application/json' \\
21
- -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 lt={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 ct({rows:e,missing:t,source:o,dryRun:r,budget:n,projectName:i=null}){const u=Math.min(process.stdout.columns||80,100),l=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(u)+s.reset),console.log(`${s.dim}${"KEY".padEnd(l)} ${"LEN".padStart(5)} ${"ACTION".padEnd(9)} WHY${s.reset}`);for(const p of e){const m=lt[p.action]??{label:p.action,color:""};console.log(`${le(p.key,l)} ${String(p.length??"-").padStart(5)} ${m.color}${m.label.padEnd(9)}${s.reset} ${s.dim}${p.reason??""}${s.reset}`)}console.log(s.dim+"\u2500".repeat(u)+s.reset);const d=H(e),a=[d.create?`${s.green}${d.create} to create${s.reset}`:null,d.rotate?`${s.yellow}${d.rotate} to rotate${s.reset}`:null,d.skip?`${d.skip} unchanged`:null,d.rejected?`${d.rejected} filtered out`:null,d.conflict?`${s.red}${d.conflict} conflicting${s.reset}`:null].filter(Boolean);if(console.log(" "+(a.length?a.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 p of t.slice(0,20))console.log(`${s.dim} \xB7 ${p.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 dt={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 pt({rows:e,missing:t,source:o,dryRun:r,budget:n,projectName:i=null,byKey:u}){const l=Math.min(process.stderr.columns||90,100),d=30;k(""),k(` ${g.bold("Plan")} ${g.dim(`source tag ${O(o)}${i?` \xB7 project ${i}`:""}`)}`),k(` ${g.gray("\u2500".repeat(l-4))}`),k(` ${g.gray(`${"KEY".padEnd(d)} ${"LEN".padStart(4)} ${"ACTION".padEnd(8)} ${"FINGERPRINT".padEnd(11)} WHY`)}`);for(const m of e){const $=dt[m.action]??{label:m.action,paint:v=>v},c=u.get(m.key),f=c&&(m.action==="create"||m.action==="rotate")?de(c.digests.digest):"";k(` ${le(m.key,d)} ${String(m.length??"-").padStart(4)} ${$.paint($.label.padEnd(8))} ${g.amber(f.padEnd(11))} ${g.dim(m.reason??"")}`)}k(` ${g.gray("\u2500".repeat(l-4))}`);const a=H(e),p=[a.create?g.green(`${a.create} to create`):null,a.rotate?g.amber(`${a.rotate} to rotate`):null,a.skip?`${a.skip} unchanged`:null,a.rejected?`${a.rejected} filtered out`:null,a.conflict?g.red(`${a.conflict} conflicting`):null].filter(Boolean);if(k(` ${p.length?p.join(g.dim(" \xB7 ")):"nothing to do"}`),t.length>0){k(""),k(` ${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 m of t.slice(0,10))k(` ${g.dim(`\xB7 ${m.label}`)}`);t.length>10&&k(` ${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<_e){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 ut(e){if(!process.stdin.isTTY)throw new y("--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 Se({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,...u}=t.digests,l=ht(e.key,o?{...u,scanPrefilter:i}:u),{label:d,...a}=l,p=e.sendProjectId??r,m=p?{projectId:p}:{};return e.action==="create"?{method:"POST",path:"/api/secrets",body:{...l,...m,tags:[n]}}:{method:"PUT",path:`/api/secrets/${e.id}/digest`,body:{...a,...m}}}const de=e=>`${e.slice(0,8)}\u2026`;function ft(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 gt({rows:e,byKey:t,serverUrl:o,token:r,source:n,projectId:i=null}){const u=[],l=O(n);let d=!0,a=0,p=0;const m=e.filter(c=>c.action==="create"||c.action==="rotate").length;let $=0;for(const c of e){if(c.action!=="create"&&c.action!=="rotate")continue;const f=t.get(c.key),v=j=>{const A=ce(c,f,{withPrefilter:j,projectId:i,tag:l});return a+=Buffer.byteLength(JSON.stringify(A.body)),p+=1,M(o,r,A.method,A.path,A.body)};let w=await v(d);d&&f.digests.scanPrefilter&&We(w)&&(d=!1,pe(),w=await v(!1)),$+=1;const S=R?` ${g.dim(de(f.digests.digest))}`:"";if(w.ok){u.push({key:c.key,action:c.action,ok:!0}),console.log(` ${s.green}\u2713${s.reset} ${c.action==="create"?"created":"rotated"} ${c.key}${S}`),R&&k(` ${Te($,m)}`);continue}const b=wt(w);u.push({key:c.key,action:c.action,ok:!1,status:w.status,reason:b}),console.log(` ${s.red}\u2717${s.reset} ${c.key}: ${b}`)}return u.sentBytes=a,u.requests=p,u}const mt=e=>e.replace(/[0-9a-f]{40,}/g,t=>`${t.slice(0,12)}\u2026(${t.length} hex)`);function yt({writes:e,byKey:t,serverUrl:o,projectId:r,source:n,dryRun:i,showRequest:u,viaSession:l=!1}){if(e.length===0)return;const d=O(n),a=e.map(c=>{const f=t.get(c.key);return ce(c,f,{withPrefilter:!!f.digests.scanPrefilter,projectId:r,tag:d})}),p=a.reduce((c,f)=>c+Buffer.byteLength(JSON.stringify(f.body)),0),m=[...new Set(a.flatMap(c=>Object.keys(c.body)))];let $=o;try{$=new URL(o).host}catch{}if(R&&(k(""),U(i?"What WOULD leave this machine":"What leaves this machine",["",...Y([["Sent to",`${$}: ${a.length} request${a.length===1?"":"s"}, ${z(p)} in total`],["Each request","label, 3 fingerprints, 3 lengths"],...ft(m.join(", "),44).map(c=>["",g.dim(c)]),["Secret values",g.green("0 B")]],{indent:0,labelWidth:18}),"",g.dim("Never leaves this machine:"),...Y([["The values",`${e.length} read into memory, fingerprinted, then dropped`],...l?[["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})),u){const c=a[0];k(""),k(` ${g.dim(`The first request, exactly as it is built (${c.method} ${c.path}), digests shortened:`)}`),k(mt(JSON.stringify(c.body,null,2)).split(`
21
+ -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 ht={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 ue(e,t){return e.length<=t?e.padEnd(t):e.slice(0,t-1)+"\u2026"}function ft({rows:e,missing:t,source:r,dryRun:o,budget:n,projectName:i=null}){const p=Math.min(process.stdout.columns||80,100),l=34;console.log(""),console.log(`${s.bold}Redaktyn import${s.reset} ${s.dim}\xB7 source tag ${O(r)}${i?` \xB7 project ${i}`:""}${s.reset}`),console.log(s.dim+"\u2500".repeat(p)+s.reset),console.log(`${s.dim}${"KEY".padEnd(l)} ${"LEN".padStart(5)} ${"ACTION".padEnd(9)} WHY${s.reset}`);for(const u of e){const m=ht[u.action]??{label:u.action,color:""};console.log(`${ue(u.key,l)} ${String(u.length??"-").padStart(5)} ${m.color}${m.label.padEnd(9)}${s.reset} ${s.dim}${u.reason??""}${s.reset}`)}console.log(s.dim+"\u2500".repeat(p)+s.reset);const d=W(e),a=[d.create?`${s.green}${d.create} to create${s.reset}`:null,d.rotate?`${s.yellow}${d.rotate} to rotate${s.reset}`:null,d.skip?`${d.skip} unchanged`:null,d.rejected?`${d.rejected} filtered out`:null,d.conflict?`${s.red}${d.conflict} conflicting${s.reset}`:null].filter(Boolean);if(console.log(" "+(a.length?a.join(s.dim+" \xB7 "+s.reset):"nothing to do")),t.length>0){console.log(""),console.log(`${s.yellow}${t.length} secret(s) tagged ${O(r)} 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 u of t.slice(0,20))console.log(`${s.dim} \xB7 ${u.label}${s.reset}`);t.length>20&&console.log(`${s.dim} \xB7 \u2026 ${t.length-20} more${s.reset}`)}B(n),o&&(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 gt={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 mt({rows:e,missing:t,source:r,dryRun:o,budget:n,projectName:i=null,byKey:p}){const l=Math.min(process.stderr.columns||90,100),d=30;w(""),w(` ${g.bold("Plan")} ${g.dim(`source tag ${O(r)}${i?` \xB7 project ${i}`:""}`)}`),w(` ${g.gray("\u2500".repeat(l-4))}`),w(` ${g.gray(`${"KEY".padEnd(d)} ${"LEN".padStart(4)} ${"ACTION".padEnd(8)} ${"FINGERPRINT".padEnd(11)} WHY`)}`);for(const m of e){const $=gt[m.action]??{label:m.action,paint:k=>k},c=p.get(m.key),f=c&&(m.action==="create"||m.action==="rotate")?he(c.digests.digest):"";w(` ${ue(m.key,d)} ${String(m.length??"-").padStart(4)} ${$.paint($.label.padEnd(8))} ${g.amber(f.padEnd(11))} ${g.dim(m.reason??"")}`)}w(` ${g.gray("\u2500".repeat(l-4))}`);const a=W(e),u=[a.create?g.green(`${a.create} to create`):null,a.rotate?g.amber(`${a.rotate} to rotate`):null,a.skip?`${a.skip} unchanged`:null,a.rejected?`${a.rejected} filtered out`:null,a.conflict?g.red(`${a.conflict} conflicting`):null].filter(Boolean);if(w(` ${u.length?u.join(g.dim(" \xB7 ")):"nothing to do"}`),t.length>0){w(""),w(` ${g.amber("\u25B2")} ${t.length} secret(s) tagged ${O(r)} are not in this input. ${g.dim("Reported only: import never removes anything.")}`);for(const m of t.slice(0,10))w(` ${g.dim(`\xB7 ${m.label}`)}`);t.length>10&&w(` ${g.dim(`\xB7 \u2026 ${t.length-10} more`)}`)}B(n)}function B(e){if(!e||e.ceiling===1/0)return;const t=e.ceiling.toLocaleString("en-US"),r=o=>{if(!(e.legacySecrets>0)||!(e.ceilingIfReenrolled>e.ceiling))return;const n=e.legacySecrets;console.log(`${o} ${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(`${o} 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(`${o} from the dashboard, or importing ${n===1?"it":"them"}, raises this to ~${e.ceilingIfReenrolled.toLocaleString("en-US")} chars.${s.reset}`)};if(e.ceiling<He){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."),r(""),console.log(`${s.dim} Register the high-value secrets only, or split them across workspaces.${s.reset}`);return}if(e.ceiling<ee){console.log(""),console.log(`${s.yellow}Scan budget:${s.reset} ${e.windowLengths} window lengths after this import; pastes above ~${t} chars are refused.`),r("");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}`),r(s.dim)}function W(e){const t={create:0,rotate:0,skip:0,conflict:0,rejected:0};for(const r of e)t[r.action]=(t[r.action]??0)+1;return t}async function yt(e){if(!process.stdin.isTTY)throw new y("--apply needs a terminal to confirm on, or --yes to skip the prompt.");const t=ve.createInterface({input:process.stdin,output:process.stdout});try{const r=await new Promise(o=>t.question(`${e} `,o));return/^y(es)?$/i.test(r.trim())}finally{t.close()}}function $t(e,t){return Ne({label:e,hmacHex:t.digest,length:t.length,hmacHexStripped:t.hmacHexStripped,lengthStripped:t.lengthStripped,hmacHexLower:t.hmacHexLower,lengthLower:t.lengthLower,scanPrefilter:t.scanPrefilter})}function pe(e,t,{withPrefilter:r,projectId:o,tag:n}){const{scanPrefilter:i,...p}=t.digests,l=$t(e.key,r?{...p,scanPrefilter:i}:p),{label:d,...a}=l,u=e.sendProjectId??o,m=u?{projectId:u}:{};return e.action==="create"?{method:"POST",path:"/api/secrets",body:{...l,...m,tags:[n]}}:{method:"PUT",path:`/api/secrets/${e.id}/digest`,body:{...a,...m}}}const he=e=>`${e.slice(0,8)}\u2026`;function wt(e,t){const r=[];let o="";for(const n of e.split(" "))o&&(o+" "+n).length>t?(r.push(o),o=n):o=o?`${o} ${n}`:n;return o&&r.push(o),r}async function kt({rows:e,byKey:t,serverUrl:r,token:o,source:n,projectId:i=null}){const p=[],l=O(n);let d=!0,a=0,u=0;const m=e.filter(c=>c.action==="create"||c.action==="rotate").length;let $=0;for(const c of e){if(c.action!=="create"&&c.action!=="rotate")continue;const f=t.get(c.key),k=j=>{const x=pe(c,f,{withPrefilter:j,projectId:i,tag:l});return a+=Buffer.byteLength(JSON.stringify(x.body)),u+=1,H(r,o,x.method,x.path,x.body)};let v=await k(d);d&&f.digests.scanPrefilter&&Fe(v)&&(d=!1,fe(),v=await k(!1)),$+=1;const b=E?` ${g.dim(he(f.digests.digest))}`:"";if(v.ok){p.push({key:c.key,action:c.action,ok:!0}),console.log(` ${s.green}\u2713${s.reset} ${c.action==="create"?"created":"rotated"} ${c.key}${b}`),E&&w(` ${_e($,m)}`);continue}const S=jt(v);p.push({key:c.key,action:c.action,ok:!1,status:v.status,reason:S}),console.log(` ${s.red}\u2717${s.reset} ${c.key}: ${S}`)}return p.sentBytes=a,p.requests=u,p}const bt=e=>e.replace(/[0-9a-f]{40,}/g,t=>`${t.slice(0,12)}\u2026(${t.length} hex)`);function vt({writes:e,byKey:t,serverUrl:r,projectId:o,source:n,dryRun:i,showRequest:p,viaSession:l=!1}){if(e.length===0)return;const d=O(n),a=e.map(c=>{const f=t.get(c.key);return pe(c,f,{withPrefilter:!!f.digests.scanPrefilter,projectId:o,tag:d})}),u=a.reduce((c,f)=>c+Buffer.byteLength(JSON.stringify(f.body)),0),m=[...new Set(a.flatMap(c=>Object.keys(c.body)))];let $=r;try{$=new URL(r).host}catch{}if(E&&(w(""),z(i?"What WOULD leave this machine":"What leaves this machine",["",...J([["Sent to",`${$}: ${a.length} request${a.length===1?"":"s"}, ${V(u)} in total`],["Each request","label, 3 fingerprints, 3 lengths"],...wt(m.join(", "),44).map(c=>["",g.dim(c)]),["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`],...l?[["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})),p){const c=a[0];w(""),w(` ${g.dim(`The first request, exactly as it is built (${c.method} ${c.path}), digests shortened:`)}`),w(bt(JSON.stringify(c.body,null,2)).split(`
22
22
  `).map(f=>` ${f}`).join(`
23
- `))}else R&&k(` ${g.dim("See the exact request body: --show-request")}`)}async function $t({label:e,cand:t,viaSession:o=!1}){const r=t.digests.length;k(""),k(` ${g.bold(e)} ${g.gray("\u2022".repeat(Math.min(r,22)))} ${g.dim(`${r} characters, read here and never sent`)}`),k(` ${g.gray(C.bar)} ${g.dim("normalise ")} trim, unicode NFC, strip invisible characters`),k(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 Oe(` ${g.gray(C.down)} ${g.dim("fingerprint ")} `,t.digests.digest,{show:16}),k(` ${g.dim("HMAC-SHA256, plus whitespace-stripped and lower-case variants, so a disguised paste still matches")}`),k("")}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 wt(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 kt(){console.log(`
23
+ `))}else E&&w(` ${g.dim("See the exact request body: --show-request")}`)}async function St({label:e,cand:t,viaSession:r=!1}){const o=t.digests.length;w(""),w(` ${g.bold(e)} ${g.gray("\u2022".repeat(Math.min(o,22)))} ${g.dim(`${o} characters, read here and never sent`)}`),w(` ${g.gray(D.bar)} ${g.dim("normalise ")} trim, unicode NFC, strip invisible characters`),w(r?` ${g.gray(D.bar)} ${g.dim("project key ")} the key from your invite, already on this machine (no passphrase needed)`:` ${g.gray(D.bar)} ${g.dim("derive key ")} PBKDF2-SHA256, 600,000 rounds, from your passphrase, on this machine`),await Pe(` ${g.gray(D.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 fe(){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 jt(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 Rt(){console.log(`
24
24
  ${s.bold}redaktyn-import${s.reset} \u2014 register fingerprints in bulk from files or any vault CLI
25
25
 
26
26
  ${s.bold}Digests are computed on this machine.${s.reset} Values are never sent, never
@@ -44,7 +44,7 @@ ${s.bold}Options${s.reset}
44
44
  (default: input file name, or "stdin")
45
45
  --apply Actually register. Without it this is a dry run.
46
46
  --yes Skip the confirmation prompt (for CI)
47
- --min-length <n> Minimum value length to accept (default ${J})
47
+ --min-length <n> Minimum value length to accept (default ${q})
48
48
  --allow-weak Register values the junk filter rejected
49
49
  --only <key|glob> Only these keys (repeatable)
50
50
  --exclude <key|glob> Skip these keys (repeatable)
@@ -67,4 +67,4 @@ ${s.bold}Exit codes${s.reset}
67
67
  0 plan printed, or every write succeeded
68
68
  1 one or more writes failed
69
69
  2 usage, auth or parse error
70
- `)}async function vt(e){const t=[e.env&&"env",e.jsonFile&&"json",e.yaml&&"yaml",e.stdin&&"stdin"].filter(Boolean);if(t.length===0)throw new y("Nothing to import. Pass one of --env, --json, --yaml or --stdin (see --help).");if(t.length>1)throw new y(`Pick one input mode, got: ${t.map(i=>"--"+i).join(", ")}`);const o=t[0];if(o==="env")return{...Je(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{...Ue(F(e.yaml,"--yaml")),mode:o,defaultSource:K(e.yaml)};const r=await Xe();if(r.trim()==="")throw new y("Nothing arrived on stdin.");return{...oe(r),mode:o,defaultSource:"stdin"}}async function bt(e){const t=process.env.REDAKTYN_TOKEN||process.env.REDAKTYN_JWT;let o=null;if(!t){const p=B();if(p)try{o=await je(p)}catch(m){throw new y(m.message)}}const r=e.server||!t&&o?.serverUrl||Ne(),n=t||o?.token;if(!n)throw new y("Run `redaktyn-login` to sign in as an OWNER or ADMIN (or set REDAKTYN_TOKEN for a script).");const i=it(n),u=at(i);if(u)throw new y(u);const l=e.orgId||i?.orgId||i?.org_id||process.env.REDAKTYN_ORG_ID;if(!l)throw new y("Could not determine the org id from the token. Pass --org-id.");const d=e.orgId||process.env.REDAKTYN_ORG_ID;if(d&&i?.orgId&&d!==i.orgId)throw new y("The org id given does not match the token's org id.");let a;try{({orgKey:a}=await Ae({server:r,token:n,orgId:l,who:o?`Signed in as ${o.email}${o.orgName?` (${o.orgName})`:""}.`:null,label:"Org passphrase",record:!!e.apply}))}catch(p){throw new y(p.message)}return{serverUrl:r,token:n,orgId:l,orgKey:a}}async function St(e){const o=process.env.REDAKTYN_TOKEN||process.env.REDAKTYN_JWT||B()?null:Ee();if(o){let a;try{a=await Re(o)}catch(p){throw new y(p.message)}if(e.project){const p=e.project.trim().toLowerCase();if(p!==String(a.projectId).toLowerCase()&&p!==String(a.projectName||"").toLowerCase())throw new y(`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 y("This session is not tied to a project. Ask for a new invite link.");if(e.apply&&!a.canEnrol)throw new y("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,orgKey:u}=await bt(e),l=e.project?await rt(r,n,e.project):null,d=l&&!l.usesOrgKey?be(u,l.id):u;return{serverUrl:r,token:n,key:d,project:l,viaSession:!1}}async function jt(){const e=Ke(process.argv);if(e.help)return kt(),0;e.reportJson||xe("import secrets",Le);const t=await vt(e),o=Qe(e.source??t.defaultSource);R&&!e.reportJson&&k(` ${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 h of t.entries)r.set(h.key,h);const n=t.entries.length-r.size;let i=[...r.values()];if(e.only.length>0&&(i=i.filter(h=>ie(h.key,e.only))),e.exclude.length>0&&(i=i.filter(h=>!ie(h.key,e.exclude))),i.length===0)throw new y(`No usable keys found in the input (${t.mode}). `+(t.problems?.length?`${t.problems.length} line(s) were skipped \u2014 see above.`:""));const{serverUrl:u,token:l,key:d,project:a,viaSession:p}=await St(e),m=e.minLength??J,$=[],c=[],f=R&&!e.reportJson?G("Fingerprinting on this machine"):null;for(const h of i){if(h.key.length>200){c.push({key:h.key,action:"rejected",reason:"key longer than 200 characters"});continue}if(ze.has(h.key)){c.push({key:h.key,action:"rejected",reason:"Doppler's own project, environment or config name, not a secret"});continue}const N=ke(h.value),P=Ze(N,{minLength:m});if(!P.ok&&!e.allowWeak){c.push({key:h.key,action:"rejected",reason:P.reason,length:N.length});continue}const ge=ve(h.value,d);$.push({key:h.key,digests:ge,weak:!P.ok,weakReason:P.reason})}f?.succeed(`${$.length} fingerprint${$.length===1?"":"s"} computed here ${g.dim("(three variants each)")}`),R&&!e.reportJson&&$.length>0&&await $t({label:$[0].key,cand:$[0],viaSession:p});const v=R&&!e.reportJson?G("Comparing with what your workspace already holds"):null;let w=await st(u,l,p);a&&(w=w.filter(h=>h&&h.projectId===a.id)),v?.succeed(`${w.length} existing secret${w.length===1?"":"s"} compared`);const S=He(w);if(!S)for(const h of $)delete h.digests.scanPrefilter;const{plan:b,missing:j}=tt({candidates:$,existing:w,source:o});a||await ot({plan:b,existing:w,serverUrl:u,token:l});const A=new Map($.map(h=>[h.key,h]));for(const h of b){const N=A.get(h.key);N?.weak&&(h.action==="create"||h.action==="rotate")&&(h.reason=`${h.reason} (weak: ${N.weakReason}, allowed by --allow-weak)`)}const _=[...b,...c].sort((h,N)=>he(h.action)-he(N.action)||h.key.localeCompare(N.key)),fe=Rt({existing:w,plan:b,byKey:A}),E=Me(fe),W=b.filter(h=>h.action==="create"||h.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:m,allowWeak:e.allowWeak,duplicateKeys:n,inputProblems:t.problems??[],counts:H(_),plan:_.map(h=>({key:h.key,action:h.action,reason:h.reason??null,length:h.length??null})),missing:j,scanBudget:{windowLengths:E.windowLengths,maxPasteChars:E.ceiling===1/0?null:E.ceiling,warn:E.ceiling<Z,atCharacterCap:E.atCharacterCap,legacySecrets:E.legacySecrets,maxPasteCharsIfReenrolled:E.ceilingIfReenrolled===1/0?null:E.ceilingIfReenrolled,serverStoresScanPrefilter:S}},null,2)),!e.apply)return 0}else{for(const h of t.problems??[]){const N=h.line!==void 0?`line ${h.line}`:h.key;console.log(`${s.yellow}!${s.reset} ${N}: ${h.message}`)}n>0&&console.log(`${s.dim}${n} duplicate key(s) in the input; the last value of each was used.${s.reset}`),S||pe(),R?pt({rows:_,missing:j,source:o,dryRun:!e.apply,budget:E,projectName:a?a.name:null,byKey:A}):ct({rows:_,missing:j,source:o,dryRun:!e.apply,budget:E,projectName:a?a.name:null}),yt({writes:W,byKey:A,serverUrl:u,projectId:a?a.id:null,source:o,dryRun:!e.apply,showRequest:e.showRequest,viaSession:p}),R&&!e.apply&&(k(""),k(` ${g.bold("Dry run. Nothing was sent.")} ${g.dim("Re-run with --apply to register.")}`)),R&&k("")}if(!e.apply)return 0;if(W.length===0)return console.log(`${s.dim}Nothing to write.${s.reset}`),0;if(!e.yes){const h=H(b);if(!await ut(`Register ${h.create} new and rotate ${h.rotate} existing fingerprint(s) for this org? [y/N]`))return console.log("Aborted."),0}console.log("");const T=await gt({rows:W,byKey:A,serverUrl:u,token:l,source:o,projectId:a?a.id:null}),I=T.filter(h=>!h.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(E),R&&(k(""),U(I.length===0?"Done":"Finished with errors",["",...Y([["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 ue=["create","rotate","conflict","skip","rejected"],he=e=>{const t=ue.indexOf(e);return t===-1?ue.length:t};function Rt({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 u=o.get(i.key);if(!u)continue;const l=u.digests;if(i.action==="create"){r.push({id:`pending:${i.key}`,label:i.key,hmacHex:l.digest,length:l.length,hmacHexStripped:l.hmacHexStripped,lengthStripped:l.lengthStripped,hmacHexLower:l.hmacHexLower,lengthLower:l.lengthLower,scanPrefilter:l.scanPrefilter,tags:[]});continue}if(i.action==="rotate"){const d=n.get(i.id);if(!d)continue;d.hmacHex=l.digest,d.length=l.length,d.hmacHexStripped=l.hmacHexStripped,d.lengthStripped=l.lengthStripped,d.hmacHexLower=l.hmacHexLower,d.lengthLower=l.lengthLower,d.scanPrefilter=l.scanPrefilter}}return r}function Et(){try{return ye(process.argv[1])}catch{return process.argv[1]}}const Nt=!!process.argv[1]&&import.meta.url===we(Et()).href;Nt&&jt().then(e=>process.exit(e)).catch(e=>{e instanceof y&&(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{J as DEFAULT_MIN_LENGTH,it as decodeJwtPayload,tt as diffAgainstServer,qe as entriesFromJsonDocument,Ze as junkVerdict,Qe as normalizeSource,Je as parseDotenv,Ue as parseFlatYaml,oe as parseJsonSecrets,Me as scanBudgetForSecrets,O as sourceTag,at as tokenRoleProblem,ne as unwrapSecretsDocument};
70
+ `)}async function Et(e){const t=[e.env&&"env",e.jsonFile&&"json",e.yaml&&"yaml",e.stdin&&"stdin"].filter(Boolean);if(t.length===0)throw new y("Nothing to import. Pass one of --env, --json, --yaml or --stdin (see --help).");if(t.length>1)throw new y(`Pick one input mode, got: ${t.map(i=>"--"+i).join(", ")}`);const r=t[0];if(r==="env")return{...Ge(F(e.env,"--env")),mode:r,defaultSource:Y(e.env)};if(r==="json")return{...ae(F(e.jsonFile,"--json")),mode:r,defaultSource:Y(e.jsonFile)};if(r==="yaml")return{...Xe(F(e.yaml,"--yaml")),mode:r,defaultSource:Y(e.yaml)};const o=await ot();if(o.trim()==="")throw new y("Nothing arrived on stdin.");return{...ae(o),mode:r,defaultSource:"stdin"}}async function Nt(e){const t=process.env.REDAKTYN_TOKEN||process.env.REDAKTYN_JWT;let r=null;if(!t){const u=G();if(u)try{r=await Ae(u)}catch(m){throw new y(m.message)}}const o=e.server||!t&&r?.serverUrl||Oe(),n=t||r?.token;if(!n)throw new y("Run `redaktyn-login` to sign in as an OWNER or ADMIN (or set REDAKTYN_TOKEN for a script).");const i=ut(n),p=pt(i);if(p)throw new y(p);const l=e.orgId||i?.orgId||i?.org_id||process.env.REDAKTYN_ORG_ID;if(!l)throw new y("Could not determine the org id from the token. Pass --org-id.");const d=e.orgId||process.env.REDAKTYN_ORG_ID;if(d&&i?.orgId&&d!==i.orgId)throw new y("The org id given does not match the token's org id.");let a;try{({orgKey:a}=await Ie({server:o,token:n,orgId:l,who:r?`Signed in as ${r.email}${r.orgName?` (${r.orgName})`:""}.`:null,label:"Org passphrase",record:!!e.apply}))}catch(u){throw new y(u.message)}return{serverUrl:o,token:n,orgId:l,orgKey:a}}async function At(e){const r=process.env.REDAKTYN_TOKEN||process.env.REDAKTYN_JWT||G()?null:Te();if(r){let a;try{a=await xe(r)}catch(u){throw new y(u.message)}if(e.project){const u=e.project.trim().toLowerCase();if(u!==String(a.projectId).toLowerCase()&&u!==String(a.projectName||"").toLowerCase())throw new y(`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 y("This session is not tied to a project. Ask for a new invite link.");if(e.apply&&!a.canEnrol)throw new y("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:o,token:n,orgId:i,orgKey:p}=await Nt(e),l=e.project?await lt(o,n,e.project):null,d=l&&!l.usesOrgKey?Ee(p,l.id):p;return{serverUrl:o,token:n,key:d,project:l,viaSession:!1}}async function xt(){const e=Be(process.argv);if(e.help)return Rt(),0;e.reportJson||Le("import secrets",De);const t=await Et(e),r=rt(e.source??t.defaultSource);E&&!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 o=new Map;for(const h of t.entries)o.set(h.key,h);const n=t.entries.length-o.size;let i=[...o.values()];if(e.only.length>0&&(i=i.filter(h=>ce(h.key,e.only))),e.exclude.length>0&&(i=i.filter(h=>!ce(h.key,e.exclude))),i.length===0)throw new y(`No usable keys found in the input (${t.mode}). `+(t.problems?.length?`${t.problems.length} line(s) were skipped \u2014 see above.`:""));const{serverUrl:p,token:l,key:d,project:a,viaSession:u}=await At(e),m=e.minLength??q,$=[],c=[],f=Me(),k=E&&!e.reportJson?Z("Fingerprinting on this machine"):null;for(const h of i){if(h.key.length>200){c.push({key:h.key,action:"rejected",reason:"key longer than 200 characters"});continue}if(Qe.has(h.key)){c.push({key:h.key,action:"rejected",reason:"Doppler's own project, environment or config name, not a secret"});continue}const R=je(h.value),C=nt(R,{minLength:m});if(!C.ok&&!e.allowWeak){c.push({key:h.key,action:"rejected",reason:C.reason,length:R.length});continue}const we=Re(h.value,d),U=f?f(h.value):null;$.push({key:h.key,digests:we,weak:!C.ok,weakReason:C.reason,guessable:U?.guessable?U:null})}k?.succeed(`${$.length} fingerprint${$.length===1?"":"s"} computed here ${g.dim("(three variants each)")}`),E&&!e.reportJson&&$.length>0&&await St({label:$[0].key,cand:$[0],viaSession:u});const v=E&&!e.reportJson?Z("Comparing with what your workspace already holds"):null;let b=await dt(p,l,u);a&&(b=b.filter(h=>h&&h.projectId===a.id)),v?.succeed(`${b.length} existing secret${b.length===1?"":"s"} compared`);const S=qe(b);if(!S)for(const h of $)delete h.digests.scanPrefilter;const{plan:j,missing:x}=it({candidates:$,existing:b,source:r});a||await ct({plan:j,existing:b,serverUrl:p,token:l});const I=new Map($.map(h=>[h.key,h]));for(const h of j){const R=I.get(h.key);R?.weak&&(h.action==="create"||h.action==="rotate")&&(h.reason=`${h.reason} (weak: ${R.weakReason}, allowed by --allow-weak)`),R?.guessable&&(h.action==="create"||h.action==="rotate")&&(h.reason=`${h.reason} (could be guessed: ${Tt(R.guessable)})`)}const ye=j.filter(h=>(h.action==="create"||h.action==="rotate")&&I.get(h.key)?.guessable).map(h=>h.key),P=[...j,...c].sort((h,R)=>me(h.action)-me(R.action)||h.key.localeCompare(R.key)),$e=It({existing:b,plan:j,byKey:I}),N=Je($e),K=j.filter(h=>h.action==="create"||h.action==="rotate");if(e.reportJson){if(console.log(JSON.stringify({source:r,sourceTag:O(r),mode:t.mode,shape:t.shape??null,dryRun:!e.apply,minLength:m,allowWeak:e.allowWeak,duplicateKeys:n,inputProblems:t.problems??[],counts:W(P),plan:P.map(h=>({key:h.key,action:h.action,reason:h.reason??null,length:h.length??null,guessable:f?!!I.get(h.key)?.guessable:null})),missing:x,scanBudget:{windowLengths:N.windowLengths,maxPasteChars:N.ceiling===1/0?null:N.ceiling,warn:N.ceiling<ee,atCharacterCap:N.atCharacterCap,legacySecrets:N.legacySecrets,maxPasteCharsIfReenrolled:N.ceilingIfReenrolled===1/0?null:N.ceilingIfReenrolled,serverStoresScanPrefilter:S}},null,2)),!e.apply)return 0}else{for(const h of t.problems??[]){const R=h.line!==void 0?`line ${h.line}`:h.key;console.log(`${s.yellow}!${s.reset} ${R}: ${h.message}`)}n>0&&console.log(`${s.dim}${n} duplicate key(s) in the input; the last value of each was used.${s.reset}`),S||fe(),E?mt({rows:P,missing:x,source:r,dryRun:!e.apply,budget:N,projectName:a?a.name:null,byKey:I}):ft({rows:P,missing:x,source:r,dryRun:!e.apply,budget:N,projectName:a?a.name:null}),vt({writes:K,byKey:I,serverUrl:p,projectId:a?a.id:null,source:r,dryRun:!e.apply,showRequest:e.showRequest,viaSession:u}),Ot(ye,f!==null),E&&!e.apply&&(w(""),w(` ${g.bold("Dry run. Nothing was sent.")} ${g.dim("Re-run with --apply to register.")}`)),E&&w("")}if(!e.apply)return 0;if(K.length===0)return console.log(`${s.dim}Nothing to write.${s.reset}`),0;if(!e.yes){const h=W(j);if(!await yt(`Register ${h.create} new and rotate ${h.rotate} existing fingerprint(s) for this org? [y/N]`))return console.log("Aborted."),0}console.log("");const T=await kt({rows:K,byKey:I,serverUrl:p,token:l,source:r,projectId:a?a.id:null}),L=T.filter(h=>!h.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}`),B(N),E&&(w(""),z(L.length===0?"Done":"Finished with errors",["",...J([["Registered",`${T.length-L.length} of ${T.length}`],["Sent",`${V(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}function Tt(e){return e.patterns.length>0?`built from ${e.patterns.slice(0,3).join(", ")}`:"short"}function Ot(e,t){if(!t){console.log(`${s.dim}Could not load the check for values that could be guessed; nothing else changed.${s.reset}`);return}if(e.length===0)return;const r=e.length===1;console.log(""),console.log(`${s.yellow}!${s.reset} ${e.length} value${r?"":"s"} could be found by guessing: ${e.join(", ")}.`),console.log(` Anyone who copies the workspace key off one of your team's devices can test guesses against ${r?"its":"their"} fingerprint very fast.`),console.log(` Registering still stops ${r?"it":"them"} being pasted. Change ${r?"it":"them"} to a long random value when you can, then import again.`)}const ge=["create","rotate","conflict","skip","rejected"],me=e=>{const t=ge.indexOf(e);return t===-1?ge.length:t};function It({existing:e,plan:t,byKey:r}){const o=e.map(i=>({...i})),n=new Map(o.map(i=>[i.id,i]));for(const i of t){const p=r.get(i.key);if(!p)continue;const l=p.digests;if(i.action==="create"){o.push({id:`pending:${i.key}`,label:i.key,hmacHex:l.digest,length:l.length,hmacHexStripped:l.hmacHexStripped,lengthStripped:l.lengthStripped,hmacHexLower:l.hmacHexLower,lengthLower:l.lengthLower,scanPrefilter:l.scanPrefilter,tags:[]});continue}if(i.action==="rotate"){const d=n.get(i.id);if(!d)continue;d.hmacHex=l.digest,d.length=l.length,d.hmacHexStripped=l.hmacHexStripped,d.lengthStripped=l.lengthStripped,d.hmacHexLower=l.hmacHexLower,d.lengthLower=l.lengthLower,d.scanPrefilter=l.scanPrefilter}}return o}function Lt(){try{return be(process.argv[1])}catch{return process.argv[1]}}const _t=!!process.argv[1]&&import.meta.url===Se(Lt()).href;_t&&xt().then(e=>process.exit(e)).catch(e=>{e instanceof y&&(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{q as DEFAULT_MIN_LENGTH,ut as decodeJwtPayload,it as diffAgainstServer,Ve as entriesFromJsonDocument,nt as junkVerdict,rt as normalizeSource,Ge as parseDotenv,Xe as parseFlatYaml,ae as parseJsonSecrets,Je as scanBudgetForSecrets,O as sourceTag,pt as tokenRoleProblem,se as unwrapSecretsDocument};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{pathToFileURL as _}from"node:url";import{existsSync as R,realpathSync as G,readFileSync as H,statSync as q,writeFileSync as z,chmodSync as M}from"node:fs";import{createInterface as J}from"node:readline";import{deriveProjectKey as O}from"./redaktyn-fingerprint.mjs";import{SessionError as a,apiJson as A,freshAdminSession as V,loadAdminSession as X,resolveServerUrl as Q,sealInviteKey as P}from"./redaktyn-session.mjs";import{askWorkspaceKey as U}from"./redaktyn-passphrase.mjs";import{copyToClipboard as Z}from"./redaktyn-clipboard.mjs";import{banner as ee,box as te,k as d,rich as D,rows as ne,say as k,step as b}from"./redaktyn-ui.mjs";import{createRequire as oe}from"node:module";const F=oe(import.meta.url),se=F("./package.json").version;function re(){console.log(`redaktyn-invite: mint a single-use invite link for one project
2
+ import{pathToFileURL as H}from"node:url";import{existsSync as D,realpathSync as G,readFileSync as q,statSync as z,writeFileSync as M,chmodSync as J}from"node:fs";import{createInterface as V}from"node:readline";import{deriveProjectKey as O}from"./redaktyn-fingerprint.mjs";import{SessionError as a,apiJson as A,freshAdminSession as X,loadAdminSession as Q,resolveServerUrl as Z,sealInviteKey as P}from"./redaktyn-session.mjs";import{askWorkspaceKey as U}from"./redaktyn-passphrase.mjs";import{copyToClipboard as ee}from"./redaktyn-clipboard.mjs";import{banner as te,box as ne,k as c,rich as K,rows as oe,say as v,step as x}from"./redaktyn-ui.mjs";import{createRequire as se}from"node:module";const _=se(import.meta.url),re=_("./package.json").version;function ie(){console.log(`redaktyn-invite: mint a single-use invite link for one project
3
3
 
4
4
  --project <name|id> The project the link is for (required)
5
5
  --can-enrol Let the redeeming CLI register secrets on that project
@@ -16,12 +16,10 @@ import{pathToFileURL as _}from"node:url";import{existsSync as R,realpathSync as
16
16
  --app-url <url> Address to put in the link (default: the server you are signed in to)
17
17
  --server <url> API address (default https://redaktyn.com; self-hosted only)
18
18
 
19
- Needs: redaktyn-login first (or REDAKTYN_TOKEN), and the workspace passphrase (REDAKTYN_PASSPHRASE or a prompt)`)}function ie(n){const e={project:null,canEnrol:!1,days:7,name:null,email:null,appUrl:null,server:null,help:!1,copy:null,csv:null,apply:!1,out:null,force:!1};for(let r=2;r<n.length;r++){const t=n[r],i=()=>{const c=n[++r];if(c===void 0||c.startsWith("--"))throw new a(`${t} needs a value`);return c};if(t==="--help"||t==="-h")e.help=!0;else if(t==="--project")e.project=i();else if(t==="--can-enrol"||t==="--can-enroll")e.canEnrol=!0;else if(t==="--expires-days")e.days=Number(i());else if(t==="--name")e.name=i();else if(t==="--email")e.email=i();else if(t==="--copy")e.copy=!0;else if(t==="--no-copy")e.copy=!1;else if(t==="--csv")e.csv=i();else if(t==="--apply")e.apply=!0;else if(t==="--out")e.out=i();else if(t==="--force")e.force=!0;else if(t==="--app-url")e.appUrl=i();else if(t==="--server")e.server=i();else throw new a(`Unknown flag: ${t}`)}if(!Number.isInteger(e.days)||e.days<1||e.days>365)throw new a("--expires-days must be 1 to 365");if(e.csv&&(e.name||e.email||e.copy!==null))throw new a("--csv takes the names and emails from the file: drop --name, --email and --copy");if(!e.csv&&(e.apply||e.out||e.force))throw new a("--apply, --out and --force go with --csv");return e}function ae(n){return new Promise(e=>{const r=J({input:process.stdin,output:process.stderr});r.question(n,t=>{r.close(),e(!/^\s*n/i.test(t))})})}async function W(n,e){let r=e;if(r===null&&(r=process.stdin.isTTY&&D?await ae(` Copy the link to your clipboard? ${d.dim("[Y/n]")} `):!1),!r)return;const t=Z(n),i=t.ok?"Copied. Paste it where you are sending it; it works once.":"Could not copy (no clipboard tool found). The link is printed above.";D?k(` ${t.ok?d.green("\u2714"):d.amber("\u25B2")} ${i}`):console.log(i)}function le(n){try{return JSON.parse(Buffer.from(n.split(".")[1],"base64url").toString()).orgId}catch{return null}}function ce(){try{return F("./redaktyn-shared.cjs")}catch{throw new a("Cannot load the shared code. From a checkout, run `npm install` then `npm run build -w @redaktyn/shared`; otherwise reinstall the CLI.")}}const de=n=>String(n).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"project";async function pe({o:n,server:e,token:r,orgId:t,who:i,project:c}){const u=ce(),T=u.checkRosterFile({name:n.csv,size:R(n.csv)?q(n.csv).size:0});if(!R(n.csv))throw new a(`No such file: ${n.csv}`);if(!T.ok)throw new a(T.reason);const $=H(n.csv,"utf8"),h=await A(e,"GET","/api/invites",{token:r}),f=Date.now(),S=new Set((Array.isArray(h.data)?h.data:[]).filter(o=>o.projectId===c.id&&o.assignedEmail&&o.currentUses<o.maxUses&&(!o.expiresAt||new Date(o.expiresAt).getTime()>f)).map(o=>String(o.assignedEmail).toLowerCase())),s=u.parseRoster($,{existingEmails:S}),w=s.rows.filter(o=>o.issues.length>0);console.log(`
20
- Read ${s.rows.length} row${s.rows.length===1?"":"s"} from ${n.csv}${s.hadHeader?" (column titles found)":""}: ${s.people.length} to invite, ${w.length} set aside.`);for(const o of w.slice(0,20))console.log(` row ${o.line}: ${o.email||"(no address)"} ${o.issues.map(v=>u.ISSUE_LABEL[v]).join(", ")}`);w.length>20&&console.log(` and ${w.length-20} more`),s.skipped>0&&console.log(` ${s.skipped} rows past the first ${u.ROSTER_MAX_ROWS} were not read: invite in smaller batches.`);const x=s.rows.filter(o=>o.issues.length===0&&o.nameDerived).length;if(x>0&&console.log(` ${x} name${x===1?" was":"s were"} made from the email address because the list had none. It is locked into that person's link.`),s.people.length===0)throw new a("Nothing to invite in that file.");const m=await A(e,"GET","/api/plans/current",{token:r});if(m.ok&&m.data?.invites&&Number.isFinite(m.data.invites.max)&&m.data.invites.used+s.people.length>m.data.invites.max)throw new a(`That would make ${m.data.invites.used+s.people.length} unused invite links, but your plan allows ${m.data.invites.max}. Revoke unused links, move up a size, or invite fewer people.`);const y=n.out||`redaktyn-invites-${de(c.name)}.csv`;if(R(y)&&!n.force)throw new a(`${y} already exists. Choose another --out, or add --force to overwrite it.`);if(!n.apply)return console.log(`
21
- Dry run: nothing was created. Re-run with --apply to make ${s.people.length} link${s.people.length===1?"":"s"} and write them to ${y}.`),0;const{orgKey:N}=await U({server:e,token:r,orgId:t,who:i}),j=b("Deriving the project key on this machine"),C=c.usesOrgKey?N:O(N,c.id);j.succeed(`Project key derived here ${d.dim("(PBKDF2-SHA256, 600,000 rounds)")}`);const I=b(`Creating ${s.people.length} link${s.people.length===1?"":"s"}`),l=[];let g=null;for(const o of s.people){if(g){l.push({...o,link:null,error:g});continue}try{const{w:v,code:B,blob:Y}=await P(C,c.usesOrgKey?1:2),E=await A(e,"POST","/api/invites",{token:r,body:{projectId:c.id,keyBlob:Y,maxUses:1,expiresInDays:n.days,assignedName:o.name,assignedEmail:o.email,...n.canEnrol?{canEnrol:!0}:{}}});if(!E.ok){const L=E.data?.error||`HTTP ${E.status}`;l.push({...o,link:null,error:L}),(E.status===401||E.status===403)&&(g=`not attempted: ${L}`);continue}l.push({...o,link:`${n.appUrl.replace(/\/+$/,"")}/connect?code=${E.data.code}#w2=${v}`,inviteCode:B,error:null})}catch(v){l.push({...o,link:null,error:v instanceof Error?v.message:"failed"})}}const p=l.filter(o=>o.link).length,K=l.length-p;K===0?I.succeed(`${p} link${p===1?"":"s"} created ${d.dim("(the server stored ciphertext it cannot open)")}`):I.fail(`${p} of ${l.length} links created`),z(y,u.inviteLinksCsv(l)+`
22
- `,{mode:384});try{M(y,384)}catch{}console.log(`
23
- Links written to ${y} (readable only by you). They are not printed here: each is a credential that works once.`),console.log("Send each person their own link over a channel you trust, then delete the file.");for(const o of l.filter(v=>!v.link).slice(0,20))console.log(` not created for ${o.email}: ${o.error}`);return K===0?0:1}async function he(n){const e=ie(n);if(e.help)return re(),0;if(!e.project)throw new a("Say which project the link is for: --project <name|id>");const r=process.env.REDAKTYN_TOKEN,t=r?null:await V(X()),i=e.server||(t&&!r?t.serverUrl:null)||Q(),c=r||t.token;e.appUrl||(e.appUrl=i);const u=le(c);if(!u)throw new a("That token carries no org id.");const T=t?`Signed in as ${t.email}${t.orgName?` (${t.orgName})`:""}.`:null;ee("invite link",se);const $=b("Looking up the project"),h=await A(i,"GET","/api/projects",{token:c});if(!h.ok||!Array.isArray(h.data))throw $.fail("Could not read your projects"),new a(`GET /api/projects failed (${h.status}). Is the token an OWNER/ADMIN token?`);const f=e.project.trim(),S=h.data.filter(p=>p.id===f).length?h.data.filter(p=>p.id===f):h.data.filter(p=>String(p.name).toLowerCase()===f.toLowerCase());if(S.length!==1)throw $.fail(S.length?`"${f}" matches several projects`:`No project "${f}"`),new a(S.length?`"${f}" matches several projects; pass the id.`:`No project "${f}". Known: ${h.data.map(p=>p.name).join(", ")}.`);const s=S[0];if(s.status==="CLOSED")throw $.fail("That project is closed"),new a(`Project ${s.name} is closed.`);if($.succeed(`Project ${s.name}`),e.csv)return pe({o:e,server:i,token:c,orgId:u,who:T,project:s});const{orgKey:w}=await U({server:i,token:c,orgId:u,who:T}),x=b("Deriving the project key on this machine"),m=s.usesOrgKey?w:O(w,s.id);x.succeed(`Project key derived here ${d.dim("(PBKDF2-SHA256, 600,000 rounds)")}`);const y=b("Sealing the key into the link"),{w:N,code:j,blob:C}=await P(m,s.usesOrgKey?1:2);y.succeed(`Key sealed ${d.dim("(AES-GCM; the opener lives only in the link's #fragment)")}`);const I=b("Registering the link with the server"),l=await A(i,"POST","/api/invites",{token:c,body:{projectId:s.id,keyBlob:C,maxUses:1,expiresInDays:e.days,...e.canEnrol?{canEnrol:!0}:{},...e.name?{assignedName:e.name}:{},...e.email?{assignedEmail:e.email}:{}}});if(!l.ok)throw I.fail("The server refused the invite"),new a(`The server refused the invite (${l.status}): ${l.data?.error||l.text.slice(0,200)}`);I.succeed(`Registered ${d.dim("(the server stored ciphertext it cannot open)")}`);const g=`${e.appUrl.replace(/\/+$/,"")}/connect?code=${l.data.code}#w2=${N}`;return D?(k(""),te("Invite ready",["",...ne([["Project",s.name],["Access",e.canEnrol?d.amber("may enrol secrets on this project only"):"scan only"],["Lifetime",`single use ${d.dim("\xB7")} expires in ${e.days} day${e.days===1?"":"s"}`],["Opens with","the part after # in the link. It never reaches the server"],["Revoke",`DELETE /api/invites/${l.data.id}`]],{indent:0}),""],{tone:"green",width:78}),k(""),k(` ${d.bold("Send this link over a channel you trust.")} ${d.dim("Whoever opens it first gets this project's key.")}`),k(` ${d.dim("They run")} ${d.bold("redaktyn-connect")} ${d.dim("and paste it.")}`),k(""),console.log(g),console.log(`Security code: ${j}`),k(""),await W(`${g}
24
- Security code: ${j}`,e.copy),0):(console.log(`Invite for project ${s.name}${e.canEnrol?" (may enrol secrets)":" (scan only)"}, single use, expires in ${e.days} day(s):
25
- `),console.log(g),console.log(`Security code: ${j}`),console.log(`
26
- Send it over a channel you trust: whoever opens it first gets this project's key.`),console.log(`They run: redaktyn-connect and paste it. Revoke before use: DELETE /api/invites/${l.data.id}`),await W(`${g}
27
- Security code: ${j}`,e.copy),0)}function ue(){try{return G(process.argv[1])}catch{return process.argv[1]}}process.argv[1]&&import.meta.url===_(ue()).href&&he(process.argv).then(n=>process.exit(n),n=>{console.error(n.message),process.exit(n instanceof a?1:2)});export{he as main};
19
+ Needs: redaktyn-login first (or REDAKTYN_TOKEN), and the workspace passphrase (REDAKTYN_PASSPHRASE or a prompt)`)}function ae(o){const e={project:null,canEnrol:!1,days:7,name:null,email:null,appUrl:null,server:null,help:!1,copy:null,csv:null,apply:!1,out:null,force:!1};for(let r=2;r<o.length;r++){const n=o[r],i=()=>{const l=o[++r];if(l===void 0||l.startsWith("--"))throw new a(`${n} needs a value`);return l};if(n==="--help"||n==="-h")e.help=!0;else if(n==="--project")e.project=i();else if(n==="--can-enrol"||n==="--can-enroll")e.canEnrol=!0;else if(n==="--expires-days")e.days=Number(i());else if(n==="--name")e.name=i();else if(n==="--email")e.email=i();else if(n==="--copy")e.copy=!0;else if(n==="--no-copy")e.copy=!1;else if(n==="--csv")e.csv=i();else if(n==="--apply")e.apply=!0;else if(n==="--out")e.out=i();else if(n==="--force")e.force=!0;else if(n==="--app-url")e.appUrl=i();else if(n==="--server")e.server=i();else throw new a(`Unknown flag: ${n}`)}if(!Number.isInteger(e.days)||e.days<1||e.days>365)throw new a("--expires-days must be 1 to 365");if(e.csv&&(e.name||e.email||e.copy!==null))throw new a("--csv takes the names and emails from the file: drop --name, --email and --copy");if(!e.csv&&(e.apply||e.out||e.force))throw new a("--apply, --out and --force go with --csv");return e}function le(o){return new Promise(e=>{const r=V({input:process.stdin,output:process.stderr});r.question(o,n=>{r.close(),e(!/^\s*n/i.test(n))})})}async function F(o,e){let r=e;if(r===null&&(r=process.stdin.isTTY&&K?await le(` Copy the link to your clipboard? ${c.dim("[Y/n]")} `):!1),!r)return;const n=ee(o),i=n.ok?"Copied. Paste it where you are sending it; it works once.":"Could not copy (no clipboard tool found). The link is printed above.";K?v(` ${n.ok?c.green("\u2714"):c.amber("\u25B2")} ${i}`):console.log(i)}function ce(o){try{return JSON.parse(Buffer.from(o.split(".")[1],"base64url").toString()).orgId}catch{return null}}function W(){try{return _("./redaktyn-shared.cjs")}catch{throw new a("Cannot load the shared code. From a checkout, run `npm install` then `npm run build -w @redaktyn/shared`; otherwise reinstall the CLI.")}}const de=o=>String(o).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"project";async function pe({o,server:e,token:r,orgId:n,who:i,project:l}){const f=W(),I=f.checkRosterFile({name:o.csv,size:D(o.csv)?z(o.csv).size:0});if(!D(o.csv))throw new a(`No such file: ${o.csv}`);if(!I.ok)throw new a(I.reason);const E=q(o.csv,"utf8"),h=await A(e,"GET","/api/invites",{token:r}),m=Date.now(),S=new Set((Array.isArray(h.data)?h.data:[]).filter(t=>t.projectId===l.id&&t.assignedEmail&&t.currentUses<t.maxUses&&(!t.expiresAt||new Date(t.expiresAt).getTime()>m)).map(t=>String(t.assignedEmail).toLowerCase())),s=f.parseRoster(E,{existingEmails:S}),w=s.rows.filter(t=>t.issues.length>0);console.log(`
20
+ Read ${s.rows.length} row${s.rows.length===1?"":"s"} from ${o.csv}${s.hadHeader?" (column titles found)":""}: ${s.people.length} to invite, ${w.length} set aside.`);for(const t of w.slice(0,20))console.log(` row ${t.line}: ${t.email||"(no address)"} ${t.issues.map(g=>f.ISSUE_LABEL[g]).join(", ")}`);w.length>20&&console.log(` and ${w.length-20} more`),s.skipped>0&&console.log(` ${s.skipped} rows past the first ${f.ROSTER_MAX_ROWS} were not read: invite in smaller batches.`);const j=s.rows.filter(t=>t.issues.length===0&&t.nameDerived).length;if(j>0&&console.log(` ${j} name${j===1?" was":"s were"} made from the email address because the list had none. It is locked into that person's link.`),s.people.length===0)throw new a("Nothing to invite in that file.");const u=await A(e,"GET","/api/plans/current",{token:r});if(u.ok&&u.data?.invites&&Number.isFinite(u.data.invites.max)&&u.data.invites.used+s.people.length>u.data.invites.max)throw new a(`That would make ${u.data.invites.used+s.people.length} unused invite links, but your plan allows ${u.data.invites.max}. Revoke unused links, move up a size, or invite fewer people.`);const y=o.out||`redaktyn-invites-${de(l.name)}.csv`;if(D(y)&&!o.force)throw new a(`${y} already exists. Choose another --out, or add --force to overwrite it.`);if(!o.apply)return console.log(`
21
+ Dry run: nothing was created. Re-run with --apply to make ${s.people.length} link${s.people.length===1?"":"s"} and write them to ${y}.`),0;const{orgKey:C}=await U({server:e,token:r,orgId:n,who:i}),N=x("Deriving the project key on this machine"),R=l.usesOrgKey?C:O(C,l.id);N.succeed(`Project key derived here ${c.dim("(PBKDF2-SHA256, 600,000 rounds)")}`);const k=x(`Creating ${s.people.length} link${s.people.length===1?"":"s"}`),p=[];let T=null;for(const t of s.people){if(T){p.push({...t,link:null,error:T});continue}try{const{w:g,code:Y,blob:B}=await P(R,l.usesOrgKey?1:2),b=await A(e,"POST","/api/invites",{token:r,body:{projectId:l.id,keyBlob:B,maxUses:1,expiresInDays:o.days,assignedName:t.name,assignedEmail:t.email,...o.canEnrol?{canEnrol:!0}:{}}});if(!b.ok){const L=b.data?.error||`HTTP ${b.status}`;p.push({...t,link:null,error:L}),(b.status===401||b.status===403)&&(T=`not attempted: ${L}`);continue}p.push({...t,link:`${o.appUrl.replace(/\/+$/,"")}/connect?code=${b.data.code}#w2=${g}`,inviteCode:Y,error:null})}catch(g){p.push({...t,link:null,error:g instanceof Error?g.message:"failed"})}}const d=p.filter(t=>t.link).length,$=p.length-d;$===0?k.succeed(`${d} link${d===1?"":"s"} created ${c.dim("(the server stored ciphertext it cannot open)")}`):k.fail(`${d} of ${p.length} links created`),M(y,f.inviteLinksCsv(p)+`
22
+ `,{mode:384});try{J(y,384)}catch{}console.log(`
23
+ Links written to ${y} (readable only by you). They are not printed here: each is a credential that works once.`),console.log("Send each person their own link over a channel you trust, then delete the file.");for(const t of p.filter(g=>!g.link).slice(0,20))console.log(` not created for ${t.email}: ${t.error}`);return $===0?0:1}async function he(o){const e=ae(o);if(e.help)return ie(),0;if(!e.project)throw new a("Say which project the link is for: --project <name|id>");const r=process.env.REDAKTYN_TOKEN,n=r?null:await X(Q()),i=e.server||(n&&!r?n.serverUrl:null)||Z(),l=r||n.token;e.appUrl||(e.appUrl=i);const f=ce(l);if(!f)throw new a("That token carries no org id.");const I=n?`Signed in as ${n.email}${n.orgName?` (${n.orgName})`:""}.`:null;te("invite link",re);const E=x("Looking up the project"),h=await A(i,"GET","/api/projects",{token:l});if(!h.ok||!Array.isArray(h.data))throw E.fail("Could not read your projects"),new a(`GET /api/projects failed (${h.status}). Is the token an OWNER/ADMIN token?`);const m=e.project.trim(),S=h.data.filter(t=>t.id===m).length?h.data.filter(t=>t.id===m):h.data.filter(t=>String(t.name).toLowerCase()===m.toLowerCase());if(S.length!==1)throw E.fail(S.length?`"${m}" matches several projects`:`No project "${m}"`),new a(S.length?`"${m}" matches several projects; pass the id.`:`No project "${m}". Known: ${h.data.map(t=>t.name).join(", ")}.`);const s=S[0];if(s.status==="CLOSED")throw E.fail("That project is closed"),new a(`Project ${s.name} is closed.`);if(E.succeed(`Project ${s.name}`),e.csv)return pe({o:e,server:i,token:l,orgId:f,who:I,project:s});const{SECURITY_CODE_WHERE_TO_TYPE:w,inviteShareText:j}=W(),{orgKey:u}=await U({server:i,token:l,orgId:f,who:I}),y=x("Deriving the project key on this machine"),C=s.usesOrgKey?u:O(u,s.id);y.succeed(`Project key derived here ${c.dim("(PBKDF2-SHA256, 600,000 rounds)")}`);const N=x("Sealing the key into the link"),{w:R,code:k,blob:p}=await P(C,s.usesOrgKey?1:2);N.succeed(`Key sealed ${c.dim("(AES-GCM; the opener lives only in the link's #fragment)")}`);const T=x("Registering the link with the server"),d=await A(i,"POST","/api/invites",{token:l,body:{projectId:s.id,keyBlob:p,maxUses:1,expiresInDays:e.days,...e.canEnrol?{canEnrol:!0}:{},...e.name?{assignedName:e.name}:{},...e.email?{assignedEmail:e.email}:{}}});if(!d.ok)throw T.fail("The server refused the invite"),new a(`The server refused the invite (${d.status}): ${d.data?.error||d.text.slice(0,200)}`);T.succeed(`Registered ${c.dim("(the server stored ciphertext it cannot open)")}`);const $=`${e.appUrl.replace(/\/+$/,"")}/connect?code=${d.data.code}#w2=${R}`;return K?(v(""),ne("Invite ready",["",...oe([["Project",s.name],["Access",e.canEnrol?c.amber("may enrol secrets on this project only"):"scan only"],["Lifetime",`single use ${c.dim("\xB7")} expires in ${e.days} day${e.days===1?"":"s"}`],["Opens with","the part after # in the link. It never reaches the server"],["Revoke",`DELETE /api/invites/${d.data.id}`]],{indent:0}),""],{tone:"green",width:78}),v(""),v(` ${c.bold("Send this link over a channel you trust.")} ${c.dim("Whoever opens it first gets this project's key.")}`),v(` ${c.dim("They run")} ${c.bold("redaktyn-connect")} ${c.dim("and paste it.")}`),v(""),console.log($),console.log(`Security code: ${k}`),v(` ${c.dim(w)}`),v(""),await F(j($,k),e.copy),0):(console.log(`Invite for project ${s.name}${e.canEnrol?" (may enrol secrets)":" (scan only)"}, single use, expires in ${e.days} day(s):
24
+ `),console.log($),console.log(`Security code: ${k}`),console.log(w),console.log(`
25
+ Send it over a channel you trust: whoever opens it first gets this project's key.`),console.log(`They run: redaktyn-connect and paste it. Revoke before use: DELETE /api/invites/${d.data.id}`),await F(j($,k),e.copy),0)}function ue(){try{return G(process.argv[1])}catch{return process.argv[1]}}process.argv[1]&&import.meta.url===H(ue()).href&&he(process.argv).then(o=>process.exit(o),o=>{console.error(o.message),process.exit(o instanceof a?1:2)});export{he as main};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{pathToFileURL as N}from"node:url";import{realpathSync as A}from"node:fs";import{spawn as x}from"node:child_process";import{createRequire as R}from"node:module";import{banner as I,box as y,k as o,rich as g,rows as P,say as d,step as T}from"./redaktyn-ui.mjs";const O=R(import.meta.url)("./package.json").version;import*as W from"node:readline";import{SessionError as v,deleteAdminSession as E,loadAdminSession as k,loginWithBrowser as j,loginWithPassword as D,readHidden as Y,resolveServerUrl as C,sessionDir as $}from"./redaktyn-session.mjs";function U(e){if(process.env.REDAKTYN_NO_BROWSER)return!1;try{const[n,t]=process.platform==="darwin"?["open",[e]]:process.platform==="win32"?["cmd",["/c","start","",e]]:["xdg-open",[e]],a=x(n,t,{stdio:"ignore",detached:!0});return a.on("error",()=>{}),a.unref(),!0}catch{return!1}}function B(){console.log(`redaktyn-login: sign in as a workspace admin
2
+ import{pathToFileURL as N}from"node:url";import{realpathSync as x}from"node:fs";import{spawn as A}from"node:child_process";import{createRequire as R}from"node:module";import{banner as T,box as y,k as o,rich as g,rows as P,say as d,step as I}from"./redaktyn-ui.mjs";const O=R(import.meta.url)("./package.json").version;import*as W from"node:readline";import{SessionError as v,apiJson as E,deleteAdminSession as j,loadAdminSession as k,loginWithBrowser as D,loginWithPassword as U,readHidden as Y,resolveServerUrl as C,sessionDir as $}from"./redaktyn-session.mjs";function B(r){if(process.env.REDAKTYN_NO_BROWSER)return!1;try{const[n,i]=process.platform==="darwin"?["open",[r]]:process.platform==="win32"?["cmd",["/c","start","",r]]:["xdg-open",[r]],a=A(n,i,{stdio:"ignore",detached:!0});return a.on("error",()=>{}),a.unref(),!0}catch{return!1}}function L(){console.log(`redaktyn-login: sign in as a workspace admin
3
3
 
4
4
  redaktyn-login open the browser and approve the code there (nothing typed here)
5
5
  redaktyn-login --no-browser print the link instead of opening it (SSH, containers)
@@ -11,8 +11,8 @@ import{pathToFileURL as N}from"node:url";import{realpathSync as A}from"node:fs";
11
11
 
12
12
  --server <url> API address (default ${C()}; only a self-hosted install changes it)
13
13
 
14
- The login is stored in ${$()} (readable only by you). Your password is never saved.`)}function M(e){return new Promise(n=>{const t=W.createInterface({input:process.stdin,output:process.stderr});t.question(e,a=>{t.close(),n(a.trim())})})}function u(e){if(g){d(""),y("Signed in",[...P([["Account",`${e.email} ${o.dim(`(${e.role})`)}`],["Workspace",e.orgName??e.orgId??"your workspace"],["Server",e.serverUrl],["Session",`${o.dim("kept in")} ${$()}${o.dim("/admin.json, readable only by you")}`],["Password",o.green("never typed here, never stored")]],{indent:0})],{tone:"green"});return}console.log(`Logged in as ${e.email} (${e.role}) of ${e.orgName??e.orgId??"your workspace"}.`),console.log(` server ${e.serverUrl}`)}async function q(e){const n=e.slice(2);if(n.includes("--help")||n.includes("-h"))return B(),0;if(n.includes("--logout")){const r=!!k();return E(),console.log(r?"Forgot this machine's Redaktyn admin login.":"There was no admin login to forget."),0}if(n.includes("--status")){const r=k();return r?(u(r),0):(console.log("Not logged in. Run: redaktyn-login"),1)}const t=r=>{const i=n.indexOf(r);return i>=0?n[i+1]:void 0},a=t("--server"),l=n.includes("--password-stdin");if(!(l||n.includes("--password")||t("--email")!==void 0)){let r=null,i=null;try{I("sign in",O);const s=await j({serverUrl:a,onCode:({userCode:h,verificationUrl:m,expiresIn:b})=>{const f=!n.includes("--no-browser")&&U(m);if(g){y("Approve in your browser",["",` ${o.gray("Your code")} ${o.bold(o.clay(h))}`,"",` ${f?"A browser tab is opening on this link.":"Open this link in a browser where you are signed in:"}`,` ${o.blue(m)}`,"",` ${o.dim("Check the page shows the same code, then press Approve.")}`,` ${o.dim("Nothing is typed in this terminal, and no password is sent from it.")}`,""],{tone:"clay",width:70}),d("");const S=Date.now()+(b||600)*1e3;r=T("Waiting for you to approve in the browser"),i=setInterval(()=>{const w=Math.max(0,Math.round((S-Date.now())/1e3));r.update(`Waiting for you to approve in the browser ${o.dim(`${Math.floor(w/60)}:${String(w%60).padStart(2,"0")} left`)}`)},1e3);return}console.error(f?"Opening your browser to sign in.":"Open this page in a browser where you are signed in:"),console.error(`
15
- ${m}
16
- `),console.error(`Check that it shows the code ${h} and approve it. Waiting\u2026 (Ctrl+C to cancel)`)}});return i&&clearInterval(i),r&&r.succeed(`Approved by ${s.email}`),g||console.log(""),u(s),g?(d(""),d(` ${o.dim("Next")} ${o.bold("redaktyn-import --env .env --project <name>")} ${o.dim("(add --apply to register)")}`),d("")):console.log(`
17
- Next: redaktyn-import --env .env --project <name> (add --apply to register)`),0}catch(s){if(i&&clearInterval(i),r&&r.fail(s instanceof Error?s.message:String(s)),s instanceof v)return r||console.error(s.message),1;throw s}}let c=t("--email");if(!c){if(!process.stdin.isTTY)return console.error("Pass --email, or run this in a terminal."),2;c=await M("Email: ")}if(!c)return console.error("No email given."),2;if(!l&&!process.stdin.isTTY)return console.error("Run this in a terminal, or use --password-stdin."),2;const p=await Y(l?"":"Password: ");if(!p.trim())return console.error("No password given."),2;try{const r=await D(c,p,{serverUrl:a});return u(r),console.log(`
18
- Next: redaktyn-import --env .env --project <name> (add --apply to register)`),0}catch(r){if(r instanceof v)return console.error(r.message),l&&p!==p.trim()&&console.error(`The password read from standard input starts or ends with a space. If that is not part of it, send it with: printf '%s' "$PASSWORD"`),1;throw r}}function F(){try{return A(process.argv[1])}catch{return process.argv[1]}}process.argv[1]&&import.meta.url===N(F()).href&&q(process.argv).then(e=>process.exit(e),e=>{console.error(e.message),process.exit(1)});export{q as main};
14
+ The login is stored in ${$()} (readable only by you). Your password is never saved.`)}function M(r){return new Promise(n=>{const i=W.createInterface({input:process.stdin,output:process.stderr});i.question(r,a=>{i.close(),n(a.trim())})})}function m(r){if(g){d(""),y("Signed in",[...P([["Account",`${r.email} ${o.dim(`(${r.role})`)}`],["Workspace",r.orgName??r.orgId??"your workspace"],["Server",r.serverUrl],["Session",`${o.dim("kept in")} ${$()}${o.dim("/admin.json, readable only by you")}`],["Password",o.green("never typed here, never stored")]],{indent:0})],{tone:"green"});return}console.log(`Logged in as ${r.email} (${r.role}) of ${r.orgName??r.orgId??"your workspace"}.`),console.log(` server ${r.serverUrl}`)}async function q(r){const n=r.slice(2);if(n.includes("--help")||n.includes("-h"))return L(),0;if(n.includes("--logout")){const e=k();let s=!1;if(e)try{const t=await E(e.serverUrl,"POST","/api/auth/logout",{token:e.token});s=t.ok&&t.data?.ended===!0}catch{}return j(),console.log(e?s?"Logged out: this machine's Redaktyn admin login is forgotten and ended on the server.":"Forgot this machine's Redaktyn admin login. The server could not be told, so its token runs until it expires (within 24 hours).":"There was no admin login to forget."),0}if(n.includes("--status")){const e=k();return e?(m(e),0):(console.log("Not logged in. Run: redaktyn-login"),1)}const i=e=>{const s=n.indexOf(e);return s>=0?n[s+1]:void 0},a=i("--server"),l=n.includes("--password-stdin");if(!(l||n.includes("--password")||i("--email")!==void 0)){let e=null,s=null;try{T("sign in",O);const t=await D({serverUrl:a,onCode:({userCode:h,verificationUrl:u,expiresIn:b})=>{const f=!n.includes("--no-browser")&&B(u);if(g){y("Approve in your browser",["",` ${o.gray("Your code")} ${o.bold(o.clay(h))}`,"",` ${f?"A browser tab is opening on this link.":"Open this link in a browser where you are signed in:"}`,` ${o.blue(u)}`,"",` ${o.dim("Check the page shows the same code, then press Approve.")}`,` ${o.dim("Nothing is typed in this terminal, and no password is sent from it.")}`,""],{tone:"clay",width:70}),d("");const S=Date.now()+(b||600)*1e3;e=I("Waiting for you to approve in the browser"),s=setInterval(()=>{const w=Math.max(0,Math.round((S-Date.now())/1e3));e.update(`Waiting for you to approve in the browser ${o.dim(`${Math.floor(w/60)}:${String(w%60).padStart(2,"0")} left`)}`)},1e3);return}console.error(f?"Opening your browser to sign in.":"Open this page in a browser where you are signed in:"),console.error(`
15
+ ${u}
16
+ `),console.error(`Check that it shows the code ${h} and approve it. Waiting\u2026 (Ctrl+C to cancel)`)}});return s&&clearInterval(s),e&&e.succeed(`Approved by ${t.email}`),g||console.log(""),m(t),g?(d(""),d(` ${o.dim("Next")} ${o.bold("redaktyn-import --env .env --project <name>")} ${o.dim("(add --apply to register)")}`),d("")):console.log(`
17
+ Next: redaktyn-import --env .env --project <name> (add --apply to register)`),0}catch(t){if(s&&clearInterval(s),e&&e.fail(t instanceof Error?t.message:String(t)),t instanceof v)return e||console.error(t.message),1;throw t}}let c=i("--email");if(!c){if(!process.stdin.isTTY)return console.error("Pass --email, or run this in a terminal."),2;c=await M("Email: ")}if(!c)return console.error("No email given."),2;if(!l&&!process.stdin.isTTY)return console.error("Run this in a terminal, or use --password-stdin."),2;const p=await Y(l?"":"Password: ");if(!p.trim())return console.error("No password given."),2;try{const e=await U(c,p,{serverUrl:a});return m(e),console.log(`
18
+ Next: redaktyn-import --env .env --project <name> (add --apply to register)`),0}catch(e){if(e instanceof v)return console.error(e.message),l&&p!==p.trim()&&console.error(`The password read from standard input starts or ends with a space. If that is not part of it, send it with: printf '%s' "$PASSWORD"`),1;throw e}}function F(){try{return x(process.argv[1])}catch{return process.argv[1]}}process.argv[1]&&import.meta.url===N(F()).href&&q(process.argv).then(r=>process.exit(r),r=>{console.error(r.message),process.exit(1)});export{q as main};