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