@redaktyn/cli 1.6.6 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/redaktyn-scan.mjs +13 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redaktyn/cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.7",
|
|
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-scan.mjs
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{resolveServerUrl as ke}from"./redaktyn-session.mjs";import{LOCK_HEADERS as
|
|
3
|
-
`)}function
|
|
4
|
-
`)}async function Ut(e,n,t,o){const s=e.replace(/\/+$/,""),r={Authorization:`Bearer ${n}`},
|
|
5
|
-
`)+1),cut:!0};if(t.error)throw t.error;if(t.status!==0)throw new Error(t.stderr);return{patch:t.stdout,cut:!1}}const le=["diff","--cached","--unified=0","--no-color","--no-ext-diff","--no-textconv"];function de(e){let n=null,t=0;if(e.length>=2&&e[0]===255&&e[1]===254){if(e.length>=4&&e[2]===0&&e[3]===0)return null;n="le",t=2}else if(e.length>=2&&e[0]===254&&e[1]===255)n="be",t=2;else{const s=e.subarray(0,8e3),r=s.length-s.length%2;if(r<8)return null;let
|
|
2
|
+
import{resolveServerUrl as ke}from"./redaktyn-session.mjs";import{LOCK_HEADERS as Qe,explainLockError as Ze,lockNetwork as et}from"./redaktyn-lock-net.mjs";import{box as te,humanBytes as ne,k as M,rich as z,rows as tt,say as P}from"./redaktyn-ui.mjs";import{addedLinesWithPlaces as nt,locate as ot,plainPlaces as st,renderFound as rt,safeToDraw as it}from"./redaktyn-places.mjs";import{extractDecodedCandidates as ct,hasDecodableRun as at}from"./redaktyn-decoder.mjs";import{execFileSync as lt,spawnSync as dt}from"node:child_process";import{createRequire as ut}from"node:module";import{readFileSync as F,existsSync as oe,mkdirSync as ft,readdirSync as ht,realpathSync as pt,openSync as mt,readSync as gt,closeSync as kt,statSync as se,writeFileSync as ye,chmodSync as yt,unlinkSync as wt}from"node:fs";import w from"node:path";import{fileURLToPath as Et}from"node:url";const $t=ut(import.meta.url);function St(){try{return $t("./redaktyn-shared.cjs")}catch{console.error("Cannot load @redaktyn/shared. Run `npm install` from the repo root, then `npm run build -w @redaktyn/shared`."),process.exit(2)}}const{deriveHmacKeyFromPassphrase:we,isOrgKeyLock:xt,scopeOf:Rt,unlockOrgKey:Nt,hmacDigestHex:Ee,normalizeSecretPlaintextForMatch:Tt,normalizeSecretPlaintextCollapsed:_t,normalizeSecretPlaintextLower:vt,extractAddedLinesFromUnifiedDiff:j,deriveProjectKey:At,deriveScanPrefilterSeed:$e,scanPrefilterParams:Se,scanPrefilterUnits:xe,scanPrefilterRoller:Re,decodeScanPrefilter:bt,MIN_SECRET_LENGTH:Ne}=St(),re=5*1024*1024,B=`${re/1048576} MB`,Te=4*1024*1024,ie=20*1024*1024,J=["package-lock.json","npm-shrinkwrap.json","yarn.lock","pnpm-lock.yaml","bun.lock","Cargo.lock","Gemfile.lock","poetry.lock","Pipfile.lock","uv.lock","composer.lock","go.sum","Package.resolved","packages.lock.json","pubspec.lock","mix.lock","flake.lock","Podfile.lock","gradle.lockfile"],Ot=e=>J.includes(w.posix.basename(e.replace(/\\/g,"/"))),_e=2e4,ce=3e6,ve=5e6,ae=[{name:"exact",transform:Tt,lengthKey:"length",fingerprint:"exact"},{name:"whitespace-collapse",transform:_t,lengthKey:"lengthStripped",fingerprint:"collapsed"},{name:"case-fold",transform:vt,lengthKey:"lengthLower",fingerprint:"lower"}],Dt=new Set([".png",".jpg",".jpeg",".gif",".webp",".ico",".pdf",".zip",".gz",".exe",".dll",".so",".dylib",".woff",".woff2",".ttf",".eot",".mp4",".mp3",".wasm",".bin"]);function Pt(e){const n={exclude:[],_:[]};for(let o=2;o<e.length;o++){const s=e[o];s==="--staged"?n.staged=!0:s==="--pushing"?n.pushing=!0:s==="--message"?n.message=!0:s==="--remote"?n.remote=e[++o]:s==="--json"?n.json=!0:s==="--no-color"?n.noColor=!0:s==="--verbose"?n.verbose=!0:s==="--file"?n.file=e[++o]:s==="--dir"?n.dir=e[++o]:s==="--org-id"?n.orgId=e[++o]:s==="--server"?n.server=e[++o]:s==="--exclude"?n.exclude.push(e[++o]):s.startsWith("-")?(console.error(`Unknown flag: ${s}`),process.exit(1)):n._.push(s)}const t=n._[0];return{...n,cmd:t}}function It(e){try{const n=e.split(".")[1];if(!n)return null;const t=n+"=".repeat((4-n.length%4)%4),o=Buffer.from(t.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8");return JSON.parse(o)}catch{return null}}function Kt(e,n){const t=e.replace(/\\/g,"/");if(t.includes("node_modules/")||t.startsWith(".git/"))return!0;for(const o of n)if(o.includes("*")){if(new RegExp("^"+o.split("*").map(r=>r.replace(/[.+?^${}()|[\]\\]/g,"\\$&")).join(".*")+"$").test(t))return!0}else if(t.includes(o))return!0;return!1}function W(e){const n=w.relative(process.cwd(),w.resolve(e));return n===""?e:n.startsWith("..")?w.resolve(e):n}function Ct(e,n=8e3){let t;try{t=mt(e,"r");const o=Buffer.alloc(n);return o.subarray(0,gt(t,o,0,n,0))}catch{return null}finally{if(t!==void 0)try{kt(t)}catch{}}}function Ae(e){const n=Number(Ne)>0?Number(Ne):8;return e.toString("utf8").split(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\ufffd]+/).filter(t=>t.length>=n).join(`
|
|
3
|
+
`)}function be(e,n,t={unreadable:[],tooLarge:[],fromBinary:new Set}){const o=se(e);if(!o.isFile())return null;if(o.size>re){n&&console.warn(`Skipping large file (${o.size} bytes): ${e}`);const a=Ct(e);return a===null?t.unreadable.push(e):!Dt.has(w.extname(e).toLowerCase())&&!(a.includes(0)&&de(a)===null)&&t.tooLarge.push(e),null}let s;try{s=F(e)}catch{return t.unreadable.push(e),null}if(!s.includes(0))return s.toString("utf8");const r=de(s);if(r!==null)return r;const c=Ae(s);return c===""?null:(t.fromBinary.add(e),c)}function Oe(e,n,t=[],o=new Set,s={unreadable:[]}){let r;try{r=pt(e)}catch{return s.unreadable.push(e),t}if(o.has(r))return t;o.add(r);let c;try{c=ht(e)}catch{return s.unreadable.push(e),t}for(const a of c){const l=w.join(e,a),d=w.relative(process.cwd(),l);if(Kt(d,n))continue;let i;try{i=se(l)}catch(f){(f?.code==="EACCES"||f?.code==="EPERM")&&s.unreadable.push(l);continue}i.isDirectory()?Oe(l,n,t,o,s):t.push(l)}return t}function De(e,n){const t=new Map;for(const o of e){const s=Number(o[n.lengthKey]);if(!Number.isFinite(s)||s<=0)continue;const r=bt(o.scanPrefilter);t.has(s)||t.set(s,new Set);const c=t.get(s);c!==null&&(r?c.add(r[n.fingerprint]):t.set(s,null))}return t}function*Pe(e,n,t,o=Te){const s=e.length>o?e.slice(0,o):e;e.length>o&&(console.warn(`Content truncated to ${o} characters for scan.`),t.truncatedChars=!0);let r=[];const c=n.map(l=>{const d=new Map;for(const i of l.secrets)for(const f of[i.hmacHex,i.hmacHexStripped,i.hmacHexLower])typeof f=="string"&&d.set(f,i.id??i.secretId);return d});t.hits??=[];let a=null;for(const l of ae){let d=null,i=null;for(const[f,p]of n.entries()){const E=De(p.secrets,l);if(E.size===0)continue;d===null&&(d=l.transform(s),i=xe(d),t.passesRun.includes(l.name)||t.passesRun.push(l.name));const $=Se($e(p.key));for(const[g,A]of E){if(g>d.length)continue;const T=A?Re($,i,g):null;for(let N=0;N+g<=d.length;N++){if(t.windows++,T&&!A.has(T(N)))continue;const b=Ee(d.slice(N,N+g),p.key);r.push(b);const _=c[f].get(b);if(_!==void 0&&t.hits.length<500&&t.hits.push({secretId:_,pass:l.name,window:d.slice(N,N+g)}),t.total++,t.total>=ce){t.exhausted=!0,r.length>0&&(yield r);return}r.length>=_e&&(yield r,r=[])}}}l.name==="exact"&&(a=d)}if(a!==null&&at(a)){const l=n.map(i=>De(i.secrets,ae[0]));let d=0;for(const i of ct(a)){let f=0;for(const E of l)for(const $ of E.keys())f+=Math.max(0,i.decoded.length-$+1);if(f===0||i.bestEffort&&d+f>ve)continue;if(d+=f,d>ve){t.exhausted=!0,r.length>0&&(yield r);return}t.passesRun.includes("encoding")||t.passesRun.push("encoding");const p=xe(i.decoded);for(const[E,$]of n.entries()){const g=l[E];if(g.size===0)continue;const A=Se($e($.key));for(const[T,N]of g){if(T>i.decoded.length)continue;const b=N?Re(A,p,T):null;for(let _=0;_+T<=i.decoded.length;_++){if(t.windows++,b&&!N.has(b(_)))continue;const h=i.decoded.slice(_,_+T),x=Ee(h,$.key);r.push(x);const O=c[E].get(x);if(O!==void 0&&t.hits.length<500&&t.hits.push({secretId:O,pass:"encoding",window:h}),t.total++,t.total>=ce){t.exhausted=!0,r.length>0&&(yield r);return}r.length>=_e&&(yield r,r=[])}}}}}r.length>0&&(yield r)}async function jt({secrets:e,savedKey:n,orgKey:t,serverUrl:o,token:s,isAdmin:r}){if(n)return[{key:n,secrets:e}];const c=[...new Set(e.map(i=>i.projectId).filter(Boolean))];let a=null;if(c.length>0&&r)try{const i=await q(`${o.replace(/\/+$/,"")}/api/projects`,{headers:{Authorization:`Bearer ${s}`}}),f=i.ok?JSON.parse(i.text):null;Array.isArray(f)&&(a=new Map(f.map(p=>[p.id,p.usesOrgKey===!0])))}catch{a=null}const l={key:t,secrets:[]},d=new Map;for(const i of e){const f=i.projectId,p=f&&a?a.get(f):void 0;(!f||p===!0||p===void 0)&&l.secrets.push(i),f&&p!==!0&&(d.has(f)||d.set(f,{key:At(t,f),secrets:[]}),d.get(f).secrets.push(i))}return[l,...d.values()].filter(i=>i.secrets.length>0)}const Lt=6e4;function V(){const e=Number(process.env.REDAKTYN_REQUEST_TIMEOUT_MS);return Number.isFinite(e)&&e>=100?e:Lt}async function q(e,n={}){const t=V();try{const o=await fetch(e,{...n,signal:AbortSignal.timeout(t)}),s=await o.text();return{ok:o.ok,status:o.status,statusText:o.statusText,headers:o.headers,text:s}}catch(o){throw o?.name==="TimeoutError"||o?.name==="AbortError"?Object.assign(new Error(`No answer from ${new URL(e).origin} within ${Math.max(1,Math.round(t/1e3))} s.`),{unreachable:!0}):o}}async function Yt(e,n,t){const o=await q(e,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify(t)}),s=o.text;let r;try{r=s?JSON.parse(s):{}}catch{r={raw:s}}if(!o.ok){const c=new Error(r.error||o.statusText||"HTTP error");throw c.status=o.status,c.body=r,c}return r}async function Ie(e,n,t=!1){const o=`${e.replace(/\/+$/,"")}/api/secrets`,s=await q(o,{headers:{Authorization:`Bearer ${n}`}});if(t&&(s.status===401||s.status===403))throw new Error("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(!s.ok)throw new Error(`GET /api/secrets failed: ${s.status} ${s.text.slice(0,200)}`);try{return{secrets:JSON.parse(s.text),closed:s.headers.get("x-redaktyn-scope")==="closed"}}catch{throw new Error("GET /api/secrets: invalid JSON response")}}function Ke(){const e=["REDAKTYN_DEVICE_TOKEN","REDAKTYN_TOKEN","REDAKTYN_JWT"].find(s=>process.env[s]),n=e?process.env[e]:void 0,t=process.env.REDAKTYN_PASSPHRASE||void 0,o=n&&!t?`${e} is set without REDAKTYN_PASSPHRASE`:t&&!n?"REDAKTYN_PASSPHRASE is set without a token":null;return{token:n,passphrase:t,complete:!!(n&&t),partial:o}}function Ht(e){if(e?.unreachable===!0)return!0;const n=e?.cause?.code??e?.code;return["ECONNREFUSED","ENOTFOUND","EAI_AGAIN","ETIMEDOUT","ECONNRESET","EHOSTUNREACH","ENETUNREACH","UND_ERR_CONNECT_TIMEOUT"].includes(n)?!0:e?.name==="TypeError"&&/fetch failed/i.test(String(e?.message))}let I={serverUrl:null,staged:!1,hook:"commit"};const K=e=>String(e).replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g,"\xB7");function Ce(e){if(!Ht(e))return K(e?.message||e);const t=[`Could not reach the Redaktyn server${I.serverUrl?` at ${I.serverUrl.replace(/\/+$/,"")}`:""}, so nothing was checked. This is not a clean result.`];return I.staged&&t.push(I.hook==="push"?"To push without the check, at your own risk: git push --no-verify":"To commit without the check, at your own risk: git commit --no-verify"),t.join(`
|
|
4
|
+
`)}async function Ut(e,n,t,o){const s=e.replace(/\/+$/,""),r={Authorization:`Bearer ${n}`},c=await q(`${s}/api/org/key-lock`,{headers:{...r,...Qe}});if(c.status===404)return we(t,o);if(!c.ok)throw new Error(`GET /api/org/key-lock failed: ${c.status}`);const{lock:a}=JSON.parse(c.text);if(!a)return we(t,o);if(!xt(a))throw new Error("The server sent a key lock this CLI does not understand. Update it.");let l;try{l=await Nt(a,t,o,et({server:s,token:n,timeoutMs:V()},{scopeOf:Rt}))}catch(d){throw Ze(d)}if(!l)throw new Error("REDAKTYN_PASSPHRASE is not this workspace's passphrase.");return l}function S(e,n={}){return lt("git",e,{encoding:"utf8",maxBuffer:ie,stdio:["ignore","pipe","pipe"],...n})}function X(e,n){const t=dt("git",e,{cwd:n,encoding:"utf8",maxBuffer:ie,stdio:["ignore","pipe","pipe"]});if(t.error?.code==="ENOBUFS")return{patch:t.stdout.slice(0,t.stdout.lastIndexOf(`
|
|
5
|
+
`)+1),cut:!0};if(t.error)throw t.error;if(t.status!==0)throw new Error(t.stderr);return{patch:t.stdout,cut:!1}}const le=["diff","--cached","--unified=0","--no-color","--no-ext-diff","--no-textconv"];function de(e){let n=null,t=0;if(e.length>=2&&e[0]===255&&e[1]===254){if(e.length>=4&&e[2]===0&&e[3]===0)return null;n="le",t=2}else if(e.length>=2&&e[0]===254&&e[1]===255)n="be",t=2;else{const s=e.subarray(0,8e3),r=s.length-s.length%2;if(r<8)return null;let c=0,a=0;for(let d=0;d<r;d+=2)s[d]===0&&c++,s[d+1]===0&&a++;const l=r/2;if(a>=l*.9&&c<=l*.02)n="le";else if(c>=l*.9&&a<=l*.02)n="be";else return null}const o=Buffer.from(e.subarray(t,t+Math.floor((e.length-t)/2)*2));return(n==="le"?o:o.swap16()).toString("utf16le")}function Q(e,n){const t=n.split(`
|
|
6
6
|
`);return t[t.length-1]===""&&t.pop(),t.length===0?"":`diff --git a/${e} b/${e}
|
|
7
7
|
--- /dev/null
|
|
8
8
|
+++ b/${e}
|
|
9
9
|
@@ -0,0 +1,${t.length} @@
|
|
10
10
|
${t.map(o=>`+${o}`).join(`
|
|
11
11
|
`)}
|
|
12
|
-
`}function Mt(e){const n=S(["rev-parse","--show-toplevel"]).trim(),t=X([...le,"--",".",...J.map(i=>`:(exclude,glob)**/${i}`)],n),o=X([...le,"--",...J.map(i=>`:(glob)**/${i}`)],n),s=[
|
|
13
|
-
`);return{text:d(s),lockfiles:d(r),cut:t.cut||o.cut,patches:
|
|
14
|
-
`)){const[,o,,s]=t.trim().split(/\s+/);!
|
|
15
|
-
`);return{empty:!1,text:_(E),lockfiles:_($),cut:f.cut||p.cut||b,patches:g,top:o}}function Jt(e,n){const t=[];for(const o of e){let s=o.local;for(let r=0;r<8;r++){let
|
|
12
|
+
`}function Mt(e){const n=S(["rev-parse","--show-toplevel"]).trim(),t=X([...le,"--",".",...J.map(i=>`:(exclude,glob)**/${i}`)],n),o=X([...le,"--",...J.map(i=>`:(glob)**/${i}`)],n),s=[j(t.patch)],r=[j(o.patch)],c=[t.patch,o.patch],l=S(["diff","--cached","--no-ext-diff","--no-textconv","--numstat","--no-renames","-z"],{cwd:n}).split("\0").filter(i=>i.startsWith("- - ")).map(i=>i.slice(4));je(n,l.map(i=>({rel:i,spec:`:${i}`,asText:()=>S([...le,"--text","--",`:(literal)${i}`],{cwd:n})})),s,r,c,e,"staged");const d=i=>i.filter(f=>f.length>0).join(`
|
|
13
|
+
`);return{text:d(s),lockfiles:d(r),cut:t.cut||o.cut,patches:c,top:n}}function je(e,n,t,o,s,r,c){for(const{rel:a,spec:l,asText:d}of n){let i;try{const p=Number(S(["cat-file","-s",l],{cwd:e}).trim());if(!(p<=re)){r&&console.warn(`Skipping large ${c} file (${p} bytes): ${a}`);continue}i=S(["cat-file","blob",l],{cwd:e,encoding:"buffer"})}catch{continue}let f;if(i.includes(0)){const p=de(i);if(p!==null)f=Q(a,p);else{const E=Ae(i);if(E==="")continue;f=Q(`${a}${ue}`,E)}}else f=d();(Ot(a)?o:t).push(j(f)),s.push(f)}}const ue=" (text inside a binary file)",Le=/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;function Ft(e){const n=[];for(const t of e.split(`
|
|
14
|
+
`)){const[,o,,s]=t.trim().split(/\s+/);!Le.test(o??"")||/^0+$/.test(o)||n.push({local:o,remote:Le.test(s??"")&&!/^0+$/.test(s)?s:null})}return n}const Ye=["log","--format=","-p","--unified=0","--no-color","--no-ext-diff","--no-textconv"],He=2e3;function Bt(e,n,t){const o=S(["rev-parse","--show-toplevel"]).trim(),s=h=>{try{return S(["cat-file","-e",`${h}^{commit}`],{cwd:o}),!0}catch{return!1}},r=[...new Set(n.map(h=>h.local).filter(s))],c=[];e&&c.push(`--remotes=${e}`);for(const h of n)h.remote&&s(h.remote)&&c.push(h.remote);const a=[...r,...c.length>0?["--not",...c]:[]],l=r.length===0||Number(S(["rev-list","--count",...a],{cwd:o}).trim())===0,d=Jt(n,o);if(l&&d.length===0)return{empty:!0};const i={patch:"",cut:!1},f=l?i:X([...Ye,...a,"--",".",...J.map(h=>`:(exclude,glob)**/${h}`)],o),p=l?i:X([...Ye,...a,"--",...J.map(h=>`:(glob)**/${h}`)],o),E=[j(f.patch)],$=[j(p.patch)],g=[f.patch,p.patch],A=l?"":S(["log","--numstat","--no-renames","-z","--format=%x01%H","--no-ext-diff","--no-textconv",...a],{cwd:o}),T=[];for(const h of A.split("")){const x=h.indexOf("\0");if(x<0)continue;const O=h.slice(0,x);for(const C of h.slice(x+1).split("\0")){const m=/^\n?-\t-\t([\s\S]+)$/.exec(C);m&&T.push({rel:m[1],spec:`${O}:${m[1]}`,asText:()=>S(["diff-tree","--root","-p","--unified=0","--no-color","--no-ext-diff","--no-textconv","--text",O,"--",`:(literal)${m[1]}`],{cwd:o})})}}const N=l?"":S(["log","--format=%x01%H%x02%B%x03",...a],{cwd:o});for(const h of N.split("")){const x=h.indexOf(""),O=h.lastIndexOf("");if(x<0||O<x)continue;const C=h.slice(x+1,O);if(C.trim()==="")continue;const m=Q(`message of commit ${h.slice(0,8)}`,C);E.push(j(m)),g.push(m)}for(const h of d){const x=Q(`message of tag ${h.id}`,h.message);E.push(j(x)),g.push(x)}const b=T.length>He;je(o,T.slice(0,He),E,$,g,t,"pushed");const _=h=>h.filter(x=>x.length>0).join(`
|
|
15
|
+
`);return{empty:!1,text:_(E),lockfiles:_($),cut:f.cut||p.cut||b,patches:g,top:o}}function Jt(e,n){const t=[];for(const o of e){let s=o.local;for(let r=0;r<8;r++){let c;try{c=S(["cat-file","-t",s],{cwd:n}).trim()}catch{break}if(c!=="tag")break;const a=S(["cat-file","tag",s],{cwd:n}),l=a.indexOf(`
|
|
16
16
|
|
|
17
|
-
`),d=l<0?"":
|
|
18
|
-
`)}else he(),process.exit(e.cmd?2:0);const{secrets:_,closed:h}=await
|
|
19
|
-
`)),process.exit(0)}catch(
|
|
20
|
-
`)}function
|
|
17
|
+
`),d=l<0?"":a.slice(l+2);d.trim()!==""&&t.push({id:o.local.slice(0,8),message:d});const i=/^object ([0-9a-f]{40,64})$/m.exec(a);if(!i)break;s=i[1]}}return t}function Gt(){if(process.stdin.isTTY)return"";try{return F(0,"utf8")}catch{return""}}async function zt(){(process.argv.includes("--help")||process.argv.includes("-h"))&&(he(),process.exit(0));const e=Pt(process.argv),n=e.cmd;if(n==="install-hooks"){Xt();return}if(n==="uninstall-hooks"){Qt();return}if(n==="status"){await Wt(e);return}!e.staged&&!e.pushing&&!e.file&&!e.dir&&(he(),process.exit(2));const t=Ke();let o=null;I.staged=e.staged===!0||e.pushing===!0||e.message===!0,e.staged===!0&&qt(),I.hook=e.pushing?"push":"commit";const s=e.staged===!0||e.pushing===!0;if(!t.complete){const{loadSession:u,freshSession:k}=await import("./redaktyn-session.mjs"),R=u();if(R){t.partial&&e.verbose&&console.error(`[redaktyn] ${t.partial}; using this machine's saved connection.`),I.serverUrl=R.serverUrl??null;try{o=await k(void 0,{timeoutMs:V()})}catch(y){console.error(Ce(y)),process.exit(2)}}}const r=e.server||o?.serverUrl||ke();I.serverUrl=r;const c=o?.token||t.token,a=o?"session":t.passphrase,l=e.orgId||process.env.REDAKTYN_ORG_ID;(!r||!c||!a)&&(console.error("Not connected. Run `redaktyn-connect` with an invite link, or set REDAKTYN_SERVER_URL, REDAKTYN_PASSPHRASE and REDAKTYN_DEVICE_TOKEN (or REDAKTYN_TOKEN)."),process.exit(2));const d=It(c),i=d?.orgId??d?.org_id??l;i||(console.error("Could not determine orgId (JWT payload orgId or REDAKTYN_ORG_ID)."),process.exit(2)),l&&d?.orgId&&l!==d.orgId&&(console.error("REDAKTYN_ORG_ID does not match JWT orgId."),process.exit(2));let f="",p="",E=!1,$=0,g=0,A=!1,T=[],N=null;const b=[];if(e.staged)try{({text:f,lockfiles:p,cut:A,patches:T,top:N}=Mt(e.verbose))}catch{console.error("Not a git repository or git diff failed."),process.exit(2)}else if(e.pushing){let u=Ft(Gt());if(u.length===0&&process.stdin.isTTY)try{u=[{local:S(["rev-parse","HEAD"]).trim(),remote:null}]}catch{u=[]}let k;try{k=u.length===0?{empty:!0}:Bt(e.remote||null,u,e.verbose)}catch{console.error("Not a git repository or git log failed."),process.exit(2)}k.empty&&(e.verbose&&console.error("Redaktyn: this push sends no commit the remote does not have, so there is nothing to check."),process.exit(0)),{text:f,lockfiles:p,cut:A,patches:T,top:N}=k}else if(e.file){const u={unreadable:[],tooLarge:[],fromBinary:new Set},k=be(w.resolve(e.file),e.verbose,u);k===null&&(E=!0,console.error(u.tooLarge.length>0?`${K(e.file)} is over ${B}, which is more than a scan reads, so nothing was scanned. Split it, or scan the part that matters.`:`Could not read ${K(e.file)} as text \u2014 nothing was scanned.`)),f=k??"",k!==null&&b.push({file:W(e.file)+(u.fromBinary.size>0?ue:""),text:k})}else if(e.dir){const u={unreadable:[],tooLarge:[],fromBinary:new Set},k=Oe(w.resolve(e.dir),e.exclude||[],[],new Set,u),R=[];for(const y of k){const U=be(y,e.verbose,u);U!==null&&(R.push(U),b.push({file:W(y)+(u.fromBinary.has(y)?ue:""),text:U}))}if($=u.unreadable.length,u.unreadable.length>0){for(const y of u.unreadable.slice(0,5))console.error(`Could not read ${K(W(y))} \u2014 it was not scanned.`);u.unreadable.length>5&&console.error(`\u2026 and ${u.unreadable.length-5} more that could not be read.`)}g=u.tooLarge.length;for(const y of u.tooLarge.slice(0,5))console.error(`${K(W(y))} is over ${B}, so it was not scanned (--exclude leaves it out on purpose).`);u.tooLarge.length>5&&console.error(`\u2026 and ${u.tooLarge.length-5} more files over ${B}.`),R.length===0&&(E=!0,console.error(`No readable text files under ${K(e.dir)} \u2014 nothing was scanned.`)),f=R.join(`
|
|
18
|
+
`)}else he(),process.exit(e.cmd?2:0);const{secrets:_,closed:h}=await Ie(r,c,!!o);if(Array.isArray(_)||(console.error("GET /api/secrets did not answer with a list."),process.exit(2)),_.length===0){const u=h?"Redaktyn: this machine's project is closed, so there are no secrets to check against.":"Redaktyn: no secrets are registered yet, so there is nothing to check against.";e.json?console.log(JSON.stringify({ok:!0,matches:[],registered:0,...h?{projectClosed:!0}:{},scan:{passes:[],digests:0,windows:0,complete:!0}},null,2)):console.error(u),process.exit(0)}const x=o?new Uint8Array(Buffer.from(o.key,"hex")):null,O=x?null:await Ut(r,c,a,i),C=await jt({secrets:_,savedKey:x,orgKey:O,serverUrl:r,token:c,isAdmin:d?.type==="admin"}),m={total:0,windows:0,passesRun:[],exhausted:!1,truncatedChars:!1},Ge=`${r.replace(/\/+$/,"")}/api/cli/scan`,pe=new Set,me=new Map;let Z=0,Y=0;const ze=function*(){yield*Pe(f,C,m),!m.exhausted&&p&&(yield*Pe(p,C,m,1/0))};for(const u of ze()){const k=[];for(const y of u)pe.has(y)||(pe.add(y),k.push(y));if(k.length===0)continue;Z+=Buffer.byteLength(JSON.stringify({digestHex:k})),Y+=1;const R=await Yt(Ge,c,{digestHex:k});for(const y of Array.isArray(R.matches)?R.matches:[])y&&y.secretId&&me.set(y.secretId,y)}const D=[...me.values()];function We(){const u=process.cwd(),k=[];if(s)for(const v of T)for(const ee of nt(v))k.push({file:w.relative(u,w.join(N,ee.file)).split(w.sep).join("/"),text:ee.text,line:ee.line});else for(const v of b)k.push(v);const R=ot(m.hits??[],k),y=D.map(v=>v.secretId).filter(v=>!R.placedIds.has(v)),U=[...R.wanted.keys()].every(v=>R.placedWindows.has(v)),qe=ae.map(v=>v.transform),Xe=y.length===0&&U&&R.rows.length>0&&it(R.rows,R.wanted,qe);return{rows:R.rows,show:Xe,unplacedIds:y,files:y.length===0?new Set(R.rows.map(v=>v.file)).size:0}}const L=m.exhausted||m.truncatedChars||A||E||$>0||g>0,ge=D.length>0||L&&!s,G=[...A?[`The staged changes are over ${ne(ie)}; only the start of them was read.`]:[],...m.truncatedChars?[`Content was truncated at ${Te} characters.`]:[],...m.exhausted?[`Hit the ${ce} digest ceiling before the end.`]:[]];e.json&&(console.log(JSON.stringify({ok:!ge,matches:D,scan:{passes:m.passesRun,digests:m.total,windows:m.windows,complete:!L}},null,2)),process.exit(ge?1:0));const Ve=`${ne(Z)} of fingerprints in ${Y} request${Y===1?"":"s"}. 0 B of your code.`,H=D.length>0?We():null;if(z&&D.length>0){P("");for(const u of rt({kind:e.pushing?"pushing":e.staged?"staged":"content",rows:H.rows,show:H.show,matches:D,unplacedIds:H.unplacedIds,files:H.files,windows:m.windows,passes:m.passesRun.length,sent:`${ne(Z)} of fingerprints in ${Y} request${Y===1?"":"s"}`}))P(u);P(""),process.exit(1)}if(z&&L&&s&&(P(""),te(e.pushing?"Push allowed, but not all of it was checked":"Commit allowed, but not all of it was checked",["",...G.map(u=>`${M.amber("\u25B2")} ${u}`),"","Nothing registered was found in the part that was checked.","Smaller commits are checked in full.",""],{tone:"amber",width:72}),P(""),process.exit(0)),z&&L&&(P(""),te("Not a clean result: the scan could not finish",["",...G.map(u=>`${M.amber("\u25B2")} ${u}`),...E?[`${M.amber("\u25B2")} Nothing was read from the path given.`]:[],...g>0?[`${M.amber("\u25B2")} ${g} file${g===1?" is":"s are"} over ${B} and ${g===1?"was":"were"} not scanned.`]:[],...$>0?[`${M.amber("\u25B2")} ${$} folder${$===1?"":"s"} or file${$===1?"":"s"} could not be read.`]:[],"","Scan a narrower path with --file or --dir, or split the commit.",""],{tone:"amber",width:72}),P(""),process.exit(1)),D.length>0){console.error(""),console.error(`Redaktyn: ${D.length} registered fingerprint${D.length===1?"":"s"} matched in scanned content:`);for(const u of D){console.error(` \u2022 ${K(u.label)} (${K(u.secretId)})`);for(const k of st(u.secretId,H.rows))console.error(k)}console.error(""),console.error(e.pushing?"Remove the secret from the commits being pushed (rewrite them), and rotate it: it is in history that is about to leave this machine.":"Remove the secret material, or rotate it and register the new fingerprint, before committing."),process.exit(1)}if(L&&s){console.error(""),console.error(`Redaktyn: ${e.pushing?"push":"commit"} allowed, but not all of it was checked.`);for(const u of G)console.error(` \u2022 ${u}`);console.error("Nothing registered was found in the part that was checked. Smaller commits are checked in full."),process.exit(0)}if(L){console.error(""),console.error("Redaktyn could not finish scanning this input, so this is not a clean result.");for(const u of G)console.error(` \u2022 ${u}`);E&&console.error(" \u2022 Nothing was read from the path given, so no content was checked."),g>0&&console.error(` \u2022 ${g} file${g===1?" is":"s are"} over ${B}, so ${g===1?"it was":"they were"} not checked.`),$>0&&console.error(` \u2022 ${$} folder${$===1?"":"s"} or file${$===1?"":"s"} could not be read, so ${$===1?"its":"their"} contents were not checked.`),console.error("Scan a narrower path with --file or --dir, or split the commit."),process.exit(1)}e.verbose&&(z?(P(""),te("Clean: no registered fingerprint found",["",...tt([["Checked",`${m.windows.toLocaleString("en-US")} windows, ${m.passesRun.join(", ")}`],["Sent to the server",Ve]],{indent:0,labelWidth:20}),""],{tone:"green",width:78}),P("")):console.log(`Redaktyn: no registered fingerprint matches (${m.passesRun.join(", ")}; ${m.windows} windows).`)),process.exit(0)}async function Wt(e){const n=e.json===!0,t=Ke(),o=t.token;let s=null;if(!t.complete){const{loadSession:a,freshSession:l}=await import("./redaktyn-session.mjs");if(a()){t.partial&&e.verbose&&console.error(`[redaktyn] ${t.partial}; using this machine's saved connection.`);try{s=await l(void 0,{timeoutMs:V()})}catch(d){const i={connected:!1,reason:String(d?.message||d)};console.log(n?JSON.stringify(i):i.reason),process.exit(1)}}}const r=e.server||s?.serverUrl||ke(),c=s?.token||o;if(!r||!c){const a={connected:!1,reason:"Not connected. Run `redaktyn-connect` with an invite link, or set REDAKTYN_SERVER_URL and REDAKTYN_TOKEN."};console.log(n?JSON.stringify(a):a.reason),process.exit(2)}try{const{secrets:a}=await Ie(r,c,!!s),l={connected:!0,source:s?"saved session":"environment",serverUrl:r,...s?.orgName?{workspace:s.orgName}:{},...s?.projectName?{project:s.projectName}:{},enrolledSecrets:Array.isArray(a)?a.length:0,hasPassphrase:s?!0:!!process.env.REDAKTYN_PASSPHRASE,...Je()?{hooks:"older version; run `redaktyn-scan install-hooks`"}:{}};console.log(n?JSON.stringify(l,null,2):Object.entries(l).map(([d,i])=>`${d}: ${i}`).join(`
|
|
19
|
+
`)),process.exit(0)}catch(a){const l={connected:!1,error:String(a?.message||a)};console.log(n?JSON.stringify(l):l.error),process.exit(1)}}const Ue="# Redaktyn pre-commit",fe=[{name:"pre-commit",marker:Ue,intro:"blocks commits when registered fingerprints appear in staged text.",scanning:"scanning staged changes...",args:"--staged",pasted:"--staged",blocked:"Redaktyn blocked this commit."},{name:"commit-msg",marker:"# Redaktyn commit-msg",intro:"blocks a commit whose message holds a registered fingerprint.",scanning:"scanning the commit message...",args:'--file "$1" --message',pasted:'--file "$1" --message',blocked:"Redaktyn blocked this commit."},{name:"pre-push",marker:"# Redaktyn pre-push",intro:"blocks a push of commits (not already on the remote) that hold a registered fingerprint.",scanning:"scanning the commits being pushed...",args:'--pushing --remote "$1"',pasted:'--pushing --remote "$1"',blocked:"Redaktyn blocked this push."}];function Me(){try{S(["rev-parse","--show-toplevel"])}catch{console.error("Not inside a git repository."),process.exit(2)}let e=null;try{e=S(["config","--get","core.hooksPath"]).trim()||null}catch{e=null}const n=w.resolve(process.cwd(),S(["rev-parse","--git-path","hooks"]).trim());return{hooksPath:e,dir:n,hookPath:w.join(n,"pre-commit")}}const Fe=e=>`'${String(e).replace(/'/g,"'\\''")}'`,Be=e=>w.sep==="\\"?e.replace(/\\/g,"/"):e;function Vt(e,n,t){return["#!/bin/sh",`${t.marker} \u2014 ${t.intro}`,'export REDAKTYN_SERVER_URL="${REDAKTYN_SERVER_URL:-}"','export REDAKTYN_PASSPHRASE="${REDAKTYN_PASSPHRASE:-}"','export REDAKTYN_DEVICE_TOKEN="${REDAKTYN_DEVICE_TOKEN:-${REDAKTYN_TOKEN:-}}"',`REDAKTYN_SCANNER=${Fe(Be(e))}`,`REDAKTYN_NODE=${Fe(Be(n))}`,"unset NODE_PATH",'if [ -n "${NODE_OPTIONS:-}" ]; then',' redaktyn_opts=""; redaktyn_skip=0'," set -f"," for redaktyn_o in $NODE_OPTIONS; do",' if [ "$redaktyn_skip" = 1 ]; then redaktyn_skip=0; continue; fi',' case "$redaktyn_o" in'," --require|-r|--import|--loader|--experimental-loader) redaktyn_skip=1 ;;"," --require=*|-r=*|--import=*|--loader=*|--experimental-loader=*) ;;",' *) redaktyn_opts="$redaktyn_opts $redaktyn_o" ;;'," esac"," done"," set +f",' NODE_OPTIONS="${redaktyn_opts# }"; export NODE_OPTIONS',"fi",'if [ ! -f "$REDAKTYN_SCANNER" ]; then',' echo "Redaktyn: the scanner this hook runs is no longer at $REDAKTYN_SCANNER, so nothing was checked." >&2',' echo "Run \\`redaktyn-scan install-hooks\\` again, or delete this hook." >&2',' echo "To commit without the check, at your own risk: git commit --no-verify" >&2'," exit 1","fi",'if [ ! -x "$REDAKTYN_NODE" ]; then',' REDAKTYN_NODE=""'," redaktyn_here=$(pwd -P 2>/dev/null)",' redaktyn_clean=""'," redaktyn_ifs=$IFS"," IFS=:"," for redaktyn_dir in $PATH; do",' case "$redaktyn_dir" in /*) ;; *) continue ;; esac',' redaktyn_real=$(cd "$redaktyn_dir" 2>/dev/null && pwd -P) || continue',' case "$redaktyn_real" in',' "$redaktyn_here"|"$redaktyn_here"/*) ;;',' *) redaktyn_clean="$redaktyn_clean:$redaktyn_dir" ;;'," esac"," done"," IFS=$redaktyn_ifs",' REDAKTYN_NODE=$(PATH="${redaktyn_clean#:}" command -v node 2>/dev/null)',"fi",'if [ -z "$REDAKTYN_NODE" ] || [ ! -x "$REDAKTYN_NODE" ]; then',' echo "Redaktyn: node was not found, so nothing was checked. Install Node, or run \\`redaktyn-scan install-hooks\\` again." >&2',' echo "To commit without the check, at your own risk: git commit --no-verify" >&2'," exit 1","fi",`echo "Redaktyn: ${t.scanning}"`,`"$REDAKTYN_NODE" "$REDAKTYN_SCANNER" ${t.args}`,"CODE=$?",'if [ "$CODE" -ne 0 ]; then',` echo "${t.blocked}"`," exit 1","fi",'echo "Redaktyn: OK"',"exit 0",""].join(`
|
|
20
|
+
`)}function Je(){try{if((S(["config","--get","core.hooksPath"]).trim()||null)!==null)return null}catch{}try{const e=w.resolve(process.cwd(),S(["rev-parse","--git-path","hooks"]).trim()),n=s=>oe(w.join(e,s))?F(w.join(e,s),"utf8"):null,t=n("pre-commit");if(t===null||!t.includes(Ue))return null;const o=["commit-msg","pre-push"].some(s=>n(s)===null);return t.includes("REDAKTYN_SCANNER=")&&!o?null:{dir:e,text:"this repository's git hooks were written by an older version of redaktyn-scan (an older pre-commit, and no commit-msg or pre-push hook). Run `redaktyn-scan install-hooks` to bring them up to date."}}catch{return null}}function qt(){const e=Je();if(e===null)return;const n=w.join(w.dirname(e.dir),"redaktyn-hooks-noted");try{if(Date.now()-se(n).mtimeMs<7*24*60*60*1e3)return}catch{}console.error(`Redaktyn: ${e.text}`);try{ye(n,String(Date.now()))}catch{}}function Xt(){const{hooksPath:e,dir:n}=Me(),t=Et(import.meta.url),o=r=>`node "${t}" ${r.pasted}`;if(e){console.error(`This repository's hooks run from ${e} (core.hooksPath), so nothing was written.`),console.error("Add these lines to the hooks there, or to your hook manager's steps:");for(const r of fe)console.error(` ${r.name}: ${o(r)}`);process.exit(1)}let s=0;for(const r of fe){const c=w.join(n,r.name);if(oe(c)){const a=F(c,"utf8");if(a.trim()!==""&&!a.includes(r.marker)){console.error(`There is already a ${r.name} hook at ${c}, and it is not Redaktyn's. Nothing was changed.`),console.error("To run both, add this line to it:"),console.error(` ${o(r)}`),s++;continue}}ft(n,{recursive:!0}),ye(c,Vt(t,process.execPath,r),{mode:493}),yt(c,493),console.log(`Installed ${r.name} hook at`,c)}s>0&&process.exit(1)}function Qt(){const{dir:e}=Me();for(const n of fe){const t=w.join(e,n.name);if(!oe(t)){n.name==="pre-commit"&&console.log("No pre-commit hook found.");continue}if(!F(t,"utf8").includes(n.marker)){console.log(`${n.name} exists but is not a Redaktyn hook \u2014 not removing.`);continue}wt(t),console.log(`Removed Redaktyn ${n.name} hook at`,t)}}function he(){console.log(`
|
|
21
21
|
redaktyn-scan \u2014 Redaktyn digest scanner (ZK: HMAC hex batches only)
|
|
22
22
|
|
|
23
23
|
Commands:
|
|
@@ -41,4 +41,4 @@ Flags:
|
|
|
41
41
|
|
|
42
42
|
Environment:
|
|
43
43
|
REDAKTYN_SERVER_URL, REDAKTYN_PASSPHRASE, REDAKTYN_DEVICE_TOKEN (or REDAKTYN_TOKEN)
|
|
44
|
-
`)}zt().catch(e=>{console.error(
|
|
44
|
+
`)}zt().catch(e=>{console.error(Ce(e)),process.exit(2)});
|