@redaktyn/cli 1.6.4 → 1.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/package.json +4 -1
- package/redaktyn-clipboard.mjs +1 -1
- package/redaktyn-connect.mjs +6 -6
- package/redaktyn-decoder.mjs +6 -0
- package/redaktyn-invite.mjs +8 -8
- package/redaktyn-lock-net.mjs +1 -1
- package/redaktyn-login.mjs +7 -7
- package/redaktyn-places.mjs +6 -0
- package/redaktyn-rail.mjs +21 -0
- package/redaktyn-scan.mjs +25 -10
- package/redaktyn-session.mjs +8 -8
- package/redaktyn-shared.cjs +6 -6
- package/redaktyn-ui.mjs +5 -5
package/README.md
CHANGED
|
@@ -622,10 +622,19 @@ towards the 4 MB and are scanned in full, because a registry credential can end
|
|
|
622
622
|
**64-char hex HMAC digests** go to `POST /api/cli/scan`; raw content never leaves your machine.
|
|
623
623
|
4. Server checks digests against enrolled rows and returns label matches.
|
|
624
624
|
|
|
625
|
-
**The git
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
625
|
+
**The git hooks.** `install-hooks` writes three hooks where git looks for them, including from a worktree: `pre-commit` (what is
|
|
626
|
+
staged), `commit-msg` (the message) and `pre-push` (the commits a push would send that the remote does not already have, their
|
|
627
|
+
messages, and the message of an annotated tag). A merge git completes by itself, a rebase, a cherry-pick or a `--no-verify`
|
|
628
|
+
commit is still checked before it leaves the machine, and history the remote already has is never read again, so an old secret
|
|
629
|
+
does not stop a new push. A hook never overwrites one of the same name that is not Redaktyn's: it prints the line to add to it
|
|
630
|
+
instead. If `core.hooksPath` is set (husky, lefthook, a shared hooks folder), it writes nothing and prints the lines for your hook
|
|
631
|
+
manager. `uninstall-hooks` removes only the hooks it wrote. The hooks run the `node` that was recorded when they were installed
|
|
632
|
+
(not whichever `node` is first on the PATH of the shell that commits), drop code-loading options such as `--require` from
|
|
633
|
+
`NODE_OPTIONS`, and say so, and how to get past it, when the scanner has moved or the server does not answer within a minute
|
|
634
|
+
(`REDAKTYN_REQUEST_TIMEOUT_MS` changes that). Hooks written by an earlier version keep their text until you run
|
|
635
|
+
`redaktyn-scan install-hooks` again; `redaktyn-scan status` and a commit say so once a week. A staged UTF-16 file (what
|
|
636
|
+
PowerShell 5.1's `>` writes) is decoded and checked like any other, and so is a registered secret that was committed as base64,
|
|
637
|
+
base64url or hex, or escaped by a URL, JSON, shell or XML.
|
|
629
638
|
|
|
630
639
|
**Scan flags:**
|
|
631
640
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redaktyn/cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
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": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"README.md",
|
|
39
39
|
"redaktyn-clipboard.mjs",
|
|
40
40
|
"redaktyn-connect.mjs",
|
|
41
|
+
"redaktyn-decoder.mjs",
|
|
41
42
|
"redaktyn-demo.mjs",
|
|
42
43
|
"redaktyn-fingerprint.mjs",
|
|
43
44
|
"redaktyn-import.mjs",
|
|
@@ -45,6 +46,8 @@
|
|
|
45
46
|
"redaktyn-lock-net.mjs",
|
|
46
47
|
"redaktyn-login.mjs",
|
|
47
48
|
"redaktyn-passphrase.mjs",
|
|
49
|
+
"redaktyn-places.mjs",
|
|
50
|
+
"redaktyn-rail.mjs",
|
|
48
51
|
"redaktyn-scan.mjs",
|
|
49
52
|
"redaktyn-session.mjs",
|
|
50
53
|
"redaktyn-share.mjs",
|
package/redaktyn-clipboard.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawnSync as
|
|
1
|
+
import{spawnSync as h}from"node:child_process";import{accessSync as y,constants as w,existsSync as f,realpathSync as g,statSync as b}from"node:fs";import{homedir as x}from"node:os";import e from"node:path";const l=o=>{try{return g(o)}catch{return null}},d=(o,n)=>{const s=e.relative(n,o);return s===""||!s.startsWith("..")&&!e.isAbsolute(s)};function S(o){const n=[],s=l(o);s&&n.push(s);for(let t=e.resolve(o);;){if(f(e.join(t,".git"))){const c=l(t);c&&n.push(c);break}const r=e.dirname(t);if(r===t)break;t=r}const i=l(x());return n.filter(t=>t!==e.parse(t).root&&t!==i)}function m(o,{path:n=process.env.PATH??"",cwd:s=process.cwd(),platform:i=process.platform}={}){if(i==="win32"){const r=e.win32.join(process.env.SystemRoot||process.env.windir||"C:\\Windows","System32",/\.exe$/i.test(o)?o:`${o}.exe`);return f(r)?r:null}const t=S(s);for(const r of String(n).split(e.delimiter)){if(!r||!e.isAbsolute(r))continue;const c=l(r);if(c===null||c.split(e.sep).includes("node_modules")||r.split(e.sep).includes("node_modules")||t.some(a=>d(c,a)))continue;const u=e.join(r,o);try{if(!b(u).isFile())continue;y(u,w.X_OK)}catch{continue}const p=l(u);if(!(p===null||p.split(e.sep).includes("node_modules")||t.some(a=>d(p,a))))return u}return null}function v(){return process.platform==="darwin"?[["pbcopy",[]]]:process.platform==="win32"?[["clip",[]]]:[["wl-copy",[]],["xclip",["-selection","clipboard"]],["xsel",["--clipboard","--input"]]]}function A(o){for(const[n,s]of v()){const i=m(n);if(i===null)continue;const t=h(i,s,{input:o,stdio:["pipe","ignore","ignore"],timeout:4e3});if(!t.error&&t.status===0)return{ok:!0,tool:n}}return{ok:!1}}function C(o,{platform:n=process.platform,resolve:s=m}={}){let i;try{i=new URL(String(o))}catch{return null}if(i.protocol!=="https:"&&i.protocol!=="http:")return null;const r=s(n==="darwin"?"open":n==="win32"?"rundll32":"xdg-open");return r?[r,n==="win32"?["url.dll,FileProtocolHandler",i.href]:[i.href]]:null}export{C as browserCommand,A as copyToClipboard,m as trustedTool};
|
package/redaktyn-connect.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{pathToFileURL as
|
|
2
|
+
import{pathToFileURL as T}from"node:url";import{realpathSync as N}from"node:fs";import*as S from"node:readline";import{SessionError as v,apiJson as E,cleanUserName as f,connectWithInvite as I,deleteSession as R,freshSession as Y,inviteInfo as j,linkNeedsInviteCode as C,loadSession as g,sessionDir as m}from"./redaktyn-session.mjs";import{banner as U,box as x,k as o,rich as d,rows as A,say as h,step as O}from"./redaktyn-ui.mjs";import{Cancelled as w,createRail as W,shortLink as D}from"./redaktyn-rail.mjs";import{createRequire as F}from"node:module";const $=F(import.meta.url)("./package.json").version;function K(){console.log(`redaktyn-connect: set this machine up from an invite link
|
|
3
3
|
|
|
4
4
|
redaktyn-connect prompt for the link (not saved in shell history)
|
|
5
5
|
redaktyn-connect --stdin read the link from standard input
|
|
@@ -13,8 +13,8 @@ import{pathToFileURL as g}from"node:url";import{realpathSync as w}from"node:fs";
|
|
|
13
13
|
Without this such a link is refused, because the first workspace's repositories would stop
|
|
14
14
|
being checked here.
|
|
15
15
|
|
|
16
|
-
The session is stored in ${
|
|
17
|
-
Could not confirm this with the server (${
|
|
18
|
-
The server no longer accepts this machine: it was removed or its session ended. Ask your admin for a new invite link and run redaktyn-connect again.`),1):s.ok?(console.log(" confirmed the server accepts this machine"),0):n(`HTTP ${s.status}`)}catch(
|
|
19
|
-
${
|
|
20
|
-
Next: redaktyn-import --env .env (add --apply to register)`),0}catch(t){if(t instanceof
|
|
16
|
+
The session is stored in ${m()} (readable only by you).`)}function p(e){return new Promise(n=>{const i=S.createInterface({input:process.stdin,output:process.stderr});i.question(e,s=>{i.close(),n(s.trim())})})}async function P(){let e="";for await(const n of process.stdin)e+=n;return e.trim()}function b(e){if(d){h(""),x("Connected",["",...A([["Workspace",e.orgName??e.orgId],["Project",e.projectName??"default"],["Access",e.canEnrol?o.amber("scan, and enrol secrets on this project only"):"scan only. Ask for an invite that allows enrolling to add secrets"],["Key",`${e.keyFingerprint} ${o.dim("(a fingerprint of the key, never the key)")}`],["Server",e.serverUrl],["Stored in",`${m()} ${o.dim("(readable only by you)")}`]],{indent:0}),""],{tone:"green",width:78});return}console.log(`Connected to ${e.orgName??e.orgId}${e.projectName?`, project ${e.projectName}`:""}.`),console.log(` server ${e.serverUrl}`),console.log(` can enrol ${e.canEnrol?"yes, on this project only":"no (scan only). Ask for an invite that allows enrolling."}`),console.log(` key ${e.keyFingerprint} (a fingerprint, never the key)`)}function _(e,n){e.note("Connected",[`${n.orgName??n.orgId} ${o.dim("\u203A")} ${n.projectName??"default"}`,n.canEnrol?o.amber("scan, and enrol secrets on this project only"):"scan only. Ask for an invite that allows enrolling to add secrets",`key ${n.keyFingerprint} ${o.dim("(a fingerprint of the key, never the key)")}`,o.dim(n.serverUrl),o.dim(`stored in ${m()} (readable only by you)`)])}async function q(e){const n=i=>(console.log(`
|
|
17
|
+
Could not confirm this with the server (${i}). The details above are what this machine saved.`),0);try{const i=await Y(e),s=await E(i.serverUrl,"GET","/api/secrets",{token:i.token});return s.status===401||s.status===403?(console.log(`
|
|
18
|
+
The server no longer accepts this machine: it was removed or its session ended. Ask your admin for a new invite link and run redaktyn-connect again.`),1):s.ok?(console.log(" confirmed the server accepts this machine"),0):n(`HTTP ${s.status}`)}catch(i){if(i?.unreachable)return n(`could not reach ${e.serverUrl}`);if(i instanceof v)return console.log(`
|
|
19
|
+
${i.message}`),1;throw i}}async function L(e){const n=e.slice(2);if(n.includes("--help")||n.includes("-h"))return K(),0;if(n.includes("--logout")){const t=!!g();return R(),console.log(t?"Forgot this machine's Redaktyn session. Your admin still sees this machine, and it keeps its seat, until they remove it.":"There was no session to forget."),0}if(n.includes("--status")){const t=g();return t?(b(t),q(t)):(console.log("Not connected. Run: redaktyn-connect"),1)}const i=n.indexOf("--server"),s=i>=0?n[i+1]:void 0,y=n.indexOf("--name");let a=y>=0?n[y+1]:void 0,c=n.find((t,k)=>!t.startsWith("--")&&n[k-1]!=="--server"&&n[k-1]!=="--name");n.includes("--stdin")&&(c=await P());const r=d&&process.stdin.isTTY===!0&&!n.includes("--stdin")?W():null;r&&r.open("redaktyn connect",$);let l=process.env.REDAKTYN_SECURITY_CODE;try{if(c)r&&r.said("Invite link",D(c));else{if(!process.stdin.isTTY)return console.error("Pass the link, use --stdin, or run this in a terminal."),2;c=r?await r.ask({label:"Invite link"}):await p("Invite link: ")}if(a===void 0&&process.stdin.isTTY&&n.indexOf("--stdin")<0){const t=await j(c,{serverUrl:s});t&&!t.assignedName&&(a=r?await r.ask({label:"Your name",hint:"your admin sees it in the dashboard. Enter to skip",optional:!0}):await p("Your name (your admin sees it in the dashboard; press Enter to skip): "))}else r&&a!==void 0&&f(a)&&r.said("Your name",f(a));try{!l&&C(c)&&process.stdin.isTTY?l=r?await r.ask({label:"Security code",hint:"your admin sent it with the link",mask:!0}):await p("Security code (your admin sent it with the link): "):r&&l&&r.said("Security code",o.dim("(from REDAKTYN_SECURITY_CODE)"))}catch(t){if(t instanceof w)throw t}}catch(t){if(t instanceof w)return r?.cancelled(),130;throw t}r||U("connect this machine",$);const u=O("Redeeming the invite and unsealing the project key on this machine",{rail:r!==null});try{const t=await I(c,{serverUrl:s,replaceOtherWorkspace:n.includes("--switch"),...l?{inviteCode:l}:{},...f(a)?{userName:a}:{},onWaiting:()=>{u.update("Waiting for your admin to approve this device. Keep this open: it finishes by itself"),d||console.log("Waiting for your admin to approve this device. Keep this open: it finishes by itself.")}});return u.succeed(`Invite redeemed ${o.dim("(the key was unsealed here; the server never saw it)")}`),r?(r.gap(),_(r,t),r.close(`next ${o.bold("redaktyn-import --env .env")} ${o.dim("(add --apply to register)")}`),0):(b(t),d?(h(""),h(` ${o.dim("Next")} ${o.bold("redaktyn-import --env .env")} ${o.dim("(add --apply to register)")}`),h("")):console.log(`
|
|
20
|
+
Next: redaktyn-import --env .env (add --apply to register)`),0)}catch(t){if(t instanceof v)return u.fail(t.message),r?r.close():d||console.error(t.message),1;throw t}}function G(){try{return N(process.argv[1])}catch{return process.argv[1]}}process.argv[1]&&import.meta.url===T(G()).href&&L(process.argv).then(e=>process.exit(e),e=>{console.error(e.message),process.exit(1)});export{L as main};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const V=8,Y=11,J=16,Q=128,g=e=>e>=16?{bestEffort:!0}:{},R=new RegExp("[A-Za-z0-9+/]{11,}={0,2}","g"),I=new RegExp("[A-Za-z0-9\\-_]{11,}={0,2}","g"),C=/[0-9a-fA-F]{16,}/g,M=new RegExp("[A-Za-z0-9+/]{11,}|[A-Za-z0-9\\-_]{11,}|[0-9a-fA-F]{16,}"),S=11;function O(e){return!e||e.length<8?!1:e.length>=S&&M.test(e)||$.test(e)}function N(e){let t=!0;for(let n=0;n<e.length;n++)if(e.charCodeAt(n)>127){t=!1;break}if(t)return e;try{return new TextDecoder("utf-8",{fatal:!0}).decode(Uint8Array.from(e,n=>n.charCodeAt(0)))}catch{return e}}function _(e){let t=0;for(let n=0;n<e.length;n++){const r=e.charCodeAt(n);if(r>=32&&r<=126&&t++,t>=8)return!0}return!1}function b(e){const t=[];R.lastIndex=0;let n;for(;(n=R.exec(e))!==null&&t.length<128;)try{const r=N(atob(n[0]));r.length>=8&&_(r)&&t.push({decoded:r,encoding:"base64",...g(t.length),sourceStart:n.index,sourceLength:n[0].length})}catch{}return t}function w(e){const t=[];I.lastIndex=0;let n;for(;(n=I.exec(e))!==null&&t.length<128;){const r=n[0];if(!(!r.includes("-")&&!r.includes("_")))try{const c=N(atob(r.replace(/-/g,"+").replace(/_/g,"/")));c.length>=8&&_(c)&&t.push({decoded:c,encoding:"base64url",...g(t.length),sourceStart:n.index,sourceLength:r.length})}catch{}}return t}function L(e){try{const t=new Uint8Array(e.length/2);for(let r=0;r<e.length;r+=2)t[r>>1]=parseInt(e.slice(r,r+2),16);const n=new TextDecoder("utf-8",{fatal:!1}).decode(t);return n.length>=8&&_(n)?n:null}catch{return null}}function x(e){const t=[];C.lastIndex=0;let n;for(;(n=C.exec(e))!==null&&t.length<128;){const r=n[0],c=r.length%2===0?[{run:r,offset:0}]:[{run:r.slice(0,-1),offset:0},{run:r.slice(1),offset:1}];for(const{run:s,offset:i}of c){if(t.length>=128)break;const o=L(s);o!==null&&t.push({decoded:o,encoding:"hex",...g(t.length),sourceStart:n.index+i,sourceLength:s.length})}}return t}const A=16,B=16,P=8*1024*1024,H=new RegExp(`^[A-Za-z0-9+/]{${A},}={0,2}$`),X=new RegExp(`^[0-9a-fA-F]{${A},}$`),m=/^[0-9a-fA-F]{6,8}:[ ]((?:[0-9a-fA-F]{2,4}[ ]?)+)/,F=/^[0-9a-fA-F]{8}[ ]{2}((?:[0-9a-fA-F]{2}[ ]{1,2})+)\|/;function U(e,t){if(e==="base64")return H.test(t)?t:null;if(e==="hex")return X.test(t)?t:null;const n=(e==="xxd"?m:F).exec(t);return n?n[1].replace(/ /g,""):null}function G(e){if(e.length<2*A||e.indexOf(`
|
|
2
|
+
`)<0)return[];const t=[],n=["base64","hex","xxd","hexdump"],r=new Map,c={base64:0,hex:0,xxd:0,hexdump:0},s=o=>{const u=r.get(o);if(r.delete(o),!u||u.pieces.length<2||c[o]>=B)return;const l=u.pieces.join("");if(l.length>P)return;c[o]+=1;let a=null;try{if(o==="base64"){const d=N(atob(l+"=".repeat((4-l.length%4)%4)));a=d.length>=8&&_(d)?d:null}else a=L(l.length%2===0?l:l.slice(0,-1))}catch{a=null}a!==null&&t.push({decoded:a,encoding:o==="base64"?"base64":"hex",bestEffort:!0,sourceStart:u.start,sourceLength:u.end-u.start})};let i=0;for(;i<=e.length;){const o=e.indexOf(`
|
|
3
|
+
`,i),u=o<0?e.length:o;let l=i,a=u;for(a>l&&e.charCodeAt(a-1)===13&&(a-=1);l<a&&(e.charCodeAt(l)===32||e.charCodeAt(l)===9);)l+=1;for(;a>l&&(e.charCodeAt(a-1)===32||e.charCodeAt(a-1)===9);)a-=1;const d=e.slice(l,a);for(const h of n){const f=d.length>=A?U(h,d):null,E=r.get(h);f!==null&&(!E||E.pieces[E.pieces.length-1].length===E.width&&f.length<=E.width)?E?(E.pieces.push(f),E.end=a):r.set(h,{pieces:[f],start:l,end:a,width:f.length}):(s(h),f!==null&&r.set(h,{pieces:[f],start:l,end:a,width:f.length}))}if(o<0)break;i=o+1}for(const o of n)s(o);return t}const y=16,$=/%[0-9A-Fa-f]{2}|\\|&(?:#[0-9]|#[xX][0-9A-Fa-f]|amp;|lt;|gt;|quot;|apos;)|=[^\s&#]*\+/,W=/%[0-9A-Fa-f]{2}|(?:^|[?&])[\w.[\]-]+=[^\s&#]*\+/m,z=/&(?:#[0-9]{1,7}|#[xX][0-9A-Fa-f]{1,6}|amp|lt|gt|quot|apos);/,K=new TextDecoder("utf-8",{fatal:!1});function D(e,t){return(t?e.replace(/\+/g," "):e).replace(/(?:%[0-9A-Fa-f]{2})+/g,r=>{const c=new Uint8Array(r.length/3);for(let s=0;s<c.length;s++)c[s]=parseInt(r.slice(s*3+1,s*3+3),16);return K.decode(c)})}function T(e){return e.replace(/'\\''/g,"'").replace(/\\(?:u([0-9A-Fa-f]{4})|x([0-9A-Fa-f]{2})|([nrt])|([\s\S]))/g,(t,n,r,c,s)=>n!==void 0?String.fromCharCode(parseInt(n,16)):r!==void 0?String.fromCharCode(parseInt(r,16)):c==="n"?`
|
|
4
|
+
`:c==="r"?"":c==="t"?" ":s??"")}const Z={amp:"&",lt:"<",gt:">",quot:'"',apos:"'"};function q(e){return e.replace(/&(?:#([0-9]{1,7})|#[xX]([0-9A-Fa-f]{1,6})|(amp|lt|gt|quot|apos));/g,(t,n,r,c)=>{if(c!==void 0)return Z[c];const s=n!==void 0?parseInt(n,10):parseInt(r,16);return s>0&&s<=1114111&&!(s>=55296&&s<=57343)?String.fromCodePoint(s):t})}function p(e,t,n,r){if(!n.test(e))return[];const c=[],s=[];let i=0;for(;i<=e.length;){let o=e.indexOf(`
|
|
5
|
+
`,i);o===-1&&(o=e.length);const u=e.slice(i,o);if(u.length>=8&&n.test(u))for(const l of r(u))l===u||l.length<8||!_(l)||(c.length<y?c.push({decoded:l,encoding:t,bestEffort:!0,sourceStart:i,sourceLength:u.length}):s.push({decoded:l,start:i,end:o}));i=o+1}return s.length>0&&c.push({decoded:s.map(o=>o.decoded).join(`
|
|
6
|
+
`),encoding:t,bestEffort:!0,sourceStart:s[0].start,sourceLength:s[s.length-1].end-s[0].start}),c}function v(e){const t=T(e);return t.includes("\\")&&t!==e?[t,T(t)]:[t]}function j(e){return[...p(e,"percent",W,t=>t.includes("+")?[D(t,!1),D(t,!0)]:[D(t,!1)]),...p(e,"escape",/\\/,v),...p(e,"entity",z,t=>[q(t)])]}function k(e){if(!e||e.length<8)return[];try{return[...b(e),...w(e),...x(e),...G(e),...j(e)]}catch{return[]}}export{k as extractDecodedCandidates,O as hasDecodableRun};
|
package/redaktyn-invite.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{pathToFileURL as
|
|
2
|
+
import{pathToFileURL as z}from"node:url";import{existsSync as P,realpathSync as M,readFileSync as X,statSync as J,lstatSync as V,unlinkSync as Q,openSync as Z,writeSync as ee,closeSync as te,constants as R}from"node:fs";import{randomBytes as ne}from"node:crypto";import{createInterface as oe}from"node:readline";import{deriveProjectKey as U}from"./redaktyn-fingerprint.mjs";import{SessionError as l,apiJson as I,freshAdminSession as se,loadAdminSession as re,resolveServerUrl as ie,sealInviteKey as _}from"./redaktyn-session.mjs";import{askWorkspaceKey as W}from"./redaktyn-passphrase.mjs";import{copyToClipboard as ae}from"./redaktyn-clipboard.mjs";import{banner as le,box as ce,k as d,rich as D,rows as de,say as w,step as C}from"./redaktyn-ui.mjs";import{createRequire as pe}from"node:module";const F=pe(import.meta.url),he=F("./package.json").version;function ue(){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,10 +16,10 @@ import{pathToFileURL as H}from"node:url";import{existsSync as D,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
|
|
20
|
-
Read ${s.rows.length} row${s.rows.length===1?"":"s"} from ${
|
|
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 ${
|
|
22
|
-
`,
|
|
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
|
|
24
|
-
`),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/${
|
|
19
|
+
Needs: redaktyn-login first (or REDAKTYN_TOKEN), and the workspace passphrase (REDAKTYN_PASSPHRASE or a prompt)`)}function fe(t){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<t.length;r++){const n=t[r],i=()=>{const a=t[++r];if(a===void 0||a.startsWith("--"))throw new l(`${n} needs a value`);return a};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 l(`Unknown flag: ${n}`)}if(!Number.isInteger(e.days)||e.days<1||e.days>365)throw new l("--expires-days must be 1 to 365");if(e.csv&&(e.name||e.email||e.copy!==null))throw new l("--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 l("--apply, --out and --force go with --csv");return e}function me(t){return new Promise(e=>{const r=oe({input:process.stdin,output:process.stderr});r.question(t,n=>{r.close(),e(!/^\s*n/i.test(n))})})}async function Y(t,e){let r=e;if(r===null&&(r=process.stdin.isTTY&&D?await me(` Copy the link to your clipboard? ${d.dim("[Y/n]")} `):!1),!r)return;const n=ae(t),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.";D?w(` ${n.ok?d.green("\u2714"):d.amber("\u25B2")} ${i}`):console.log(i)}function ye(t){try{return JSON.parse(Buffer.from(t.split(".")[1],"base64url").toString()).orgId}catch{return null}}function B(){try{return F("./redaktyn-shared.cjs")}catch{throw new l("Cannot load the shared code. From a checkout, run `npm install` then `npm run build -w @redaktyn/shared`; otherwise reinstall the CLI.")}}const ge=t=>String(t).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"project";async function ve({o:t,server:e,token:r,orgId:n,who:i,project:a}){const c=B(),g=c.checkRosterFile({name:t.csv,size:P(t.csv)?J(t.csv).size:0});if(!P(t.csv))throw new l(`No such file: ${t.csv}`);if(!g.ok)throw new l(g.reason);const S=X(t.csv,"utf8"),u=await I(e,"GET","/api/invites",{token:r}),m=Date.now(),T=new Set((Array.isArray(u.data)?u.data:[]).filter(o=>o.projectId===a.id&&o.assignedEmail&&o.currentUses<o.maxUses&&(!o.expiresAt||new Date(o.expiresAt).getTime()>m)).map(o=>String(o.assignedEmail).toLowerCase())),s=c.parseRoster(S,{existingEmails:T}),k=s.rows.filter(o=>o.issues.length>0);console.log(`
|
|
20
|
+
Read ${s.rows.length} row${s.rows.length===1?"":"s"} from ${t.csv}${s.hadHeader?" (column titles found)":""}: ${s.people.length} to invite, ${k.length} set aside.`);for(const o of k.slice(0,20))console.log(` row ${o.line}: ${o.email||"(no address)"} ${o.issues.map(v=>c.ISSUE_LABEL[v]).join(", ")}`);k.length>20&&console.log(` and ${k.length-20} more`),s.skipped>0&&console.log(` ${s.skipped} rows past the first ${c.ROSTER_MAX_ROWS} were not read: invite in smaller batches.`);const j=s.rows.filter(o=>o.issues.length===0&&o.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 l("Nothing to invite in that file.");const f=await I(e,"GET","/api/plans/current",{token:r});if(f.ok&&f.data?.invites&&Number.isFinite(f.data.invites.max)&&f.data.invites.used+s.people.length>f.data.invites.max)throw new l(`That would make ${f.data.invites.used+s.people.length} unused invite links, but your plan allows ${f.data.invites.max}. Revoke unused links, move up a size, or invite fewer people.`);const b=t.out||`redaktyn-invites-${ge(a.name)}.csv`;if(H(b)&&!t.force)throw new l(`${b} already exists. Choose another --out, or add --force to overwrite it.`);if(!t.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 ${b}.`),0;const{orgKey:N}=await W({server:e,token:r,orgId:n,who:i}),A=C("Deriving the project key on this machine"),L=a.usesOrgKey?N:U(N,a.id);A.succeed(`Project key derived here ${d.dim("(PBKDF2-SHA256, 600,000 rounds)")}`);const $=C(`Creating ${s.people.length} link${s.people.length===1?"":"s"}`),h=[];let x=null;for(const o of s.people){if(x){h.push({...o,link:null,error:x});continue}try{const{w:v,code:G,blob:q}=await _(L,a.usesOrgKey?1:2),O=await I(e,"POST","/api/invites",{token:r,body:{projectId:a.id,keyBlob:q,maxUses:1,expiresInDays:t.days,assignedName:o.name,assignedEmail:o.email,...t.canEnrol?{canEnrol:!0}:{}}});if(!O.ok){const K=O.data?.error||`HTTP ${O.status}`;h.push({...o,link:null,error:K}),(O.status===401||O.status===403)&&(x=`not attempted: ${K}`);continue}h.push({...o,link:`${t.appUrl.replace(/\/+$/,"")}/connect?code=${O.data.code}#w2=${v}`,inviteCode:G,error:null})}catch(v){h.push({...o,link:null,error:v instanceof Error?v.message:"failed"})}}const p=h.filter(o=>o.link).length,E=h.length-p;E===0?$.succeed(`${p} link${p===1?"":"s"} created ${d.dim("(the server stored ciphertext it cannot open)")}`):$.fail(`${p} of ${h.length} links created`);const y=we(b,c.inviteLinksCsv(h)+`
|
|
22
|
+
`,t.force===!0);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 h.filter(v=>!v.link).slice(0,20))console.log(` not created for ${o.email}: ${o.error}`);return E===0?0:1}function H(t){try{return V(t),!0}catch{return!1}}function we(t,e,r){const n=R.O_WRONLY|R.O_CREAT|R.O_EXCL|(R.O_NOFOLLOW??0),i=c=>{const g=Z(c,n,384);try{ee(g,e)}finally{te(g)}return c};if(r&&H(t))try{Q(t)}catch{}try{return i(t)}catch(c){if(c?.code!=="EEXIST"&&c?.code!=="ELOOP")throw c}const a=t.replace(/(\.csv)?$/i,`-${ne(4).toString("hex")}.csv`);return i(a)}async function ke(t){const e=fe(t);if(e.help)return ue(),0;if(!e.project)throw new l("Say which project the link is for: --project <name|id>");const r=process.env.REDAKTYN_TOKEN,n=r?null:await se(re()),i=e.server||(n&&!r?n.serverUrl:null)||ie(),a=r||n.token;e.appUrl||(e.appUrl=i);const c=ye(a);if(!c)throw new l("That token carries no org id.");const g=n?`Signed in as ${n.email}${n.orgName?` (${n.orgName})`:""}.`:null;le("invite link",he);const S=C("Looking up the project"),u=await I(i,"GET","/api/projects",{token:a});if(!u.ok||!Array.isArray(u.data))throw S.fail("Could not read your projects"),new l(`GET /api/projects failed (${u.status}). Is the token an OWNER/ADMIN token?`);const m=e.project.trim(),T=u.data.filter(y=>y.id===m).length?u.data.filter(y=>y.id===m):u.data.filter(y=>String(y.name).toLowerCase()===m.toLowerCase());if(T.length!==1)throw S.fail(T.length?`"${m}" matches several projects`:`No project "${m}"`),new l(T.length?`"${m}" matches several projects; pass the id.`:`No project "${m}". Known: ${u.data.map(y=>y.name).join(", ")}.`);const s=T[0];if(s.status==="CLOSED")throw S.fail("That project is closed"),new l(`Project ${s.name} is closed.`);if(S.succeed(`Project ${s.name}`),e.csv)return ve({o:e,server:i,token:a,orgId:c,who:g,project:s});const{SECURITY_CODE_WHERE_TO_TYPE:k,inviteShareText:j}=B(),{orgKey:f}=await W({server:i,token:a,orgId:c,who:g}),b=C("Deriving the project key on this machine"),N=s.usesOrgKey?f:U(f,s.id);b.succeed(`Project key derived here ${d.dim("(PBKDF2-SHA256, 600,000 rounds)")}`);const A=C("Sealing the key into the link"),{w:L,code:$,blob:h}=await _(N,s.usesOrgKey?1:2);A.succeed(`Key sealed ${d.dim("(AES-GCM; the opener lives only in the link's #fragment)")}`);const x=C("Registering the link with the server"),p=await I(i,"POST","/api/invites",{token:a,body:{projectId:s.id,keyBlob:h,maxUses:1,expiresInDays:e.days,...e.canEnrol?{canEnrol:!0}:{},...e.name?{assignedName:e.name}:{},...e.email?{assignedEmail:e.email}:{}}});if(!p.ok)throw x.fail("The server refused the invite"),new l(`The server refused the invite (${p.status}): ${p.data?.error||p.text.slice(0,200)}`);x.succeed(`Registered ${d.dim("(the server stored ciphertext it cannot open)")}`);const E=`${e.appUrl.replace(/\/+$/,"")}/connect?code=${p.data.code}#w2=${L}`;return D?(w(""),ce("Invite ready",["",...de([["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/${p.data.id}`]],{indent:0}),""],{tone:"green",width:78}),w(""),w(` ${d.bold("Send this link over a channel you trust.")} ${d.dim("Whoever opens it first gets this project's key.")}`),w(` ${d.dim("They run")} ${d.bold("redaktyn-connect")} ${d.dim("and paste it.")}`),w(""),console.log(E),console.log(`Security code: ${$}`),w(` ${d.dim(k)}`),w(""),await Y(j(E,$),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(E),console.log(`Security code: ${$}`),console.log(k),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/${p.data.id}`),await Y(j(E,$),e.copy),0)}function $e(){try{return M(process.argv[1])}catch{return process.argv[1]}}process.argv[1]&&import.meta.url===z($e()).href&&ke(process.argv).then(t=>process.exit(t),t=>{console.error(t.message),process.exit(t instanceof l?1:2)});export{ke as main};
|
package/redaktyn-lock-net.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SessionError as o,apiJson as
|
|
1
|
+
import{SessionError as o,apiJson as u,oprfPinStore as c}from"./redaktyn-session.mjs";const l={"X-Redaktyn-Lock-Versions":"1,2,3"},p=15e3;async function h({share:e,blinded:s}){const a=process.env.REDAKTYN_SHARE_TOKEN,n=new URL(e.url).host;let r;try{r=await fetch(`${e.url.replace(/\/+$/,"")}/evaluate`,{method:"POST",headers:{"Content-Type":"application/json",...a?{Authorization:`Bearer ${a}`}:{}},body:JSON.stringify({blinded:s}),signal:AbortSignal.timeout(p)})}catch{throw new o(`Could not reach your key-share server at ${n}. This workspace's key cannot be opened without it.`)}const t=await r.json().catch(()=>null);if(!r.ok)throw new o(r.status===401?`Your key-share server at ${n} wants a token. Set REDAKTYN_SHARE_TOKEN and run this again.`:`Your key-share server at ${n} refused the request (HTTP ${r.status}).`);if(typeof t?.evaluated!="string"||typeof t?.proof!="string")throw new o("Your key-share server's answer was not understood.");return{evaluated:t.evaluated,proof:t.proof}}function f({server:e,token:s,timeoutMs:a},n){return{evaluate:async r=>{const t=await u(e,"POST","/api/org/key-lock/evaluate",{token:s,body:r,headers:l,...a>0?{timeoutMs:a}:{}});if(!t.ok){const i=new o(t.data?.error??`The server could not help open the workspace key (HTTP ${t.status}).`);throw i.code=t.data?.code,i}return t.data},trust:{scope:n.scopeOf(e),pins:c(),firstUse:"allow"},evaluateShare:h,secretKey:process.env.REDAKTYN_SECRET_KEY||null}}function w(e){return e?.name==="SecretKeyNeededError"?new o(`${e.message} Run this in a terminal and it will ask, or set REDAKTYN_SECRET_KEY for a script.`):e?.name==="OprfProofError"||e?.name==="KeyShareUnavailableError"?new o(e.message):e}export{l as LOCK_HEADERS,w as explainLockError,f as lockNetwork};
|
package/redaktyn-login.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{pathToFileURL as N}from"node:url";import{realpathSync as
|
|
2
|
+
import{pathToFileURL as N}from"node:url";import{realpathSync as A}from"node:fs";import{spawn as R}from"node:child_process";import{createRequire as T}from"node:module";import{banner as x,box as y,k as n,rich as u,rows as P,say as a,step as I}from"./redaktyn-ui.mjs";import{browserCommand as O}from"./redaktyn-clipboard.mjs";const W=T(import.meta.url)("./package.json").version;import*as E from"node:readline";import{SessionError as v,apiJson as j,deleteAdminSession as C,loadAdminSession as k,loginWithBrowser as D,loginWithPassword as U,readHidden as Y,resolveServerUrl as B,sessionDir as $}from"./redaktyn-session.mjs";function L(r){if(process.env.REDAKTYN_NO_BROWSER)return!1;try{const o=O(r);if(o===null)return!1;const i=R(o[0],o[1],{stdio:"ignore",detached:!0});return i.on("error",()=>{}),i.unref(),!0}catch{return!1}}function M(){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)
|
|
@@ -9,10 +9,10 @@ import{pathToFileURL as N}from"node:url";import{realpathSync as x}from"node:fs";
|
|
|
9
9
|
redaktyn-login --status show who this machine is logged in as
|
|
10
10
|
redaktyn-login --logout forget the login on this machine
|
|
11
11
|
|
|
12
|
-
--server <url> API address (default ${
|
|
12
|
+
--server <url> API address (default ${B()}; 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
|
|
15
|
-
${
|
|
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}`),
|
|
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
|
|
18
|
-
Next: redaktyn-import --env .env --project <name> (add --apply to register)`),0}catch(e){if(e instanceof v)return console.error(e.message),
|
|
14
|
+
The login is stored in ${$()} (readable only by you). Your password is never saved.`)}function q(r){return new Promise(o=>{const i=E.createInterface({input:process.stdin,output:process.stderr});i.question(r,l=>{i.close(),o(l.trim())})})}function m(r){if(u){a(""),y("Signed in",[...P([["Account",`${r.email} ${n.dim(`(${r.role})`)}`],["Workspace",r.orgName??r.orgId??"your workspace"],["Server",r.serverUrl],["Session",`${n.dim("kept in")} ${$()}${n.dim("/admin.json, readable only by you")}`],["Password",n.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 F(r){const o=r.slice(2);if(o.includes("--help")||o.includes("-h"))return M(),0;if(o.includes("--logout")){const e=k();let s=!1;if(e)try{const t=await j(e.serverUrl,"POST","/api/auth/logout",{token:e.token});s=t.ok&&t.data?.ended===!0}catch{}return C(),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(o.includes("--status")){const e=k();return e?(m(e),0):(console.log("Not logged in. Run: redaktyn-login"),1)}const i=e=>{const s=o.indexOf(e);return s>=0?o[s+1]:void 0},l=i("--server"),d=o.includes("--password-stdin");if(!(d||o.includes("--password")||i("--email")!==void 0)){let e=null,s=null;try{x("sign in",W);const t=await D({serverUrl:l,onCode:({userCode:h,verificationUrl:g,expiresIn:b})=>{const f=!o.includes("--no-browser")&&L(g);if(u){y("Approve in your browser",["",` ${n.gray("Your code")} ${n.bold(n.clay(h))}`,"",` ${f?"A browser tab is opening on this link.":"Open this link in a browser where you are signed in:"}`,` ${n.blue(g)}`,"",` ${n.dim("Check the page shows the same code, then press Approve.")}`,` ${n.dim("Nothing is typed in this terminal, and no password is sent from it.")}`,""],{tone:"clay",width:70}),a("");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 ${n.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
|
+
${g}
|
|
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}`),u||console.log(""),m(t),u?(a(""),a(` ${n.dim("Next")} ${n.bold("redaktyn-import --env .env --project <name>")} ${n.dim("(add --apply to register)")}`),a("")):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 q("Email: ")}if(!c)return console.error("No email given."),2;if(!d&&!process.stdin.isTTY)return console.error("Run this in a terminal, or use --password-stdin."),2;const p=await Y(d?"":"Password: ");if(!p.trim())return console.error("No password given."),2;try{const e=await U(c,p,{serverUrl:l});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),d&&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 H(){try{return A(process.argv[1])}catch{return process.argv[1]}}process.argv[1]&&import.meta.url===N(H()).href&&F(process.argv).then(r=>process.exit(r),r=>{console.error(r.message),process.exit(1)});export{F as main};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{k as i}from"./redaktyn-ui.mjs";function k(s){if(!(s.startsWith('"')&&s.endsWith('"')&&s.length>=2))return s;const o=[],t=s.slice(1,-1),n={n:10,t:9,r:13,a:7,b:8,f:12,v:11,"\\":92,'"':34};for(let e=0;e<t.length;e++){const r=t[e];if(r!=="\\"){o.push(...Buffer.from(r,"utf8"));continue}const c=t[e+1],f=/^[0-3][0-7]{2}/.exec(t.slice(e+1,e+4));if(f){o.push(parseInt(f[0],8)),e+=3;continue}if(c!==void 0&&c in n){o.push(n[c]),e+=1;continue}o.push(92)}return Buffer.from(o).toString("utf8")}function y(s){const o=k(s.replace(/\t.*$/,"").trim());return o==="/dev/null"?null:o.startsWith("b/")?o.slice(2):o}function C(s){const o=[];let t=null,n=0,e=!1;for(const r of s.split(`
|
|
2
|
+
`)){if(r.startsWith("diff --git ")){t=null,e=!1;continue}if(!e&&r.startsWith("+++ ")){t=y(r.slice(4));continue}const c=/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(r);if(c){n=Number(c[1]),e=!0;continue}if(e)if(r.startsWith("+"))t!==null&&o.push({file:t,line:n,text:r.slice(1)}),n+=1;else{if(r.startsWith("-")||r.startsWith("\\"))continue;n+=1}}return o}function L(s){const o=[0];for(let t=s.indexOf(`
|
|
3
|
+
`);t>=0;t=s.indexOf(`
|
|
4
|
+
`,t+1))o.push(t+1);return o}function R(s,o){let t=0,n=s.length-1;for(;t<n;){const e=t+n+1>>1;s[e]<=o?t=e:n=e-1}return t}function T(s,o,{maxOccurrences:t=400}={}){const n=new Map;for(const d of s){if(typeof d.window!="string"||d.window.length===0)continue;const m=d.pass==="case-fold",g=`${m?"f":"e"}\0${d.window}`,p=n.get(g)??{window:d.window,fold:m,ids:new Set};p.ids.add(d.secretId),n.set(g,p)}const e=new Map,r=new Set,c=new Set;let f=0;for(const d of o){const m=d.text.toLowerCase(),g=m.length===d.text.length;let p=null;for(const[b,l]of n){if(l.window.includes(`
|
|
5
|
+
`))continue;const x=l.fold?g?m:null:d.text;if(x!==null)for(let $=0;f<t;){const h=x.indexOf(l.window,$);if(h<0)break;$=h+l.window.length,f+=1;let a=d.line,W=d.text,S=h;if(a===void 0){p??=L(d.text);const u=R(p,h);a=u+1;const w=d.text.indexOf(`
|
|
6
|
+
`,p[u]);W=d.text.slice(p[u],w<0?void 0:w),S=h-p[u]}const M=`${d.file}\0${a}`,v=e.get(M)??{file:d.file,line:a,text:W,strikes:[]};if(!v.strikes.some(u=>u.start===S&&u.end===S+l.window.length))for(const u of l.ids)v.strikes.push({start:S,end:S+l.window.length,secretId:u});e.set(M,v),r.add(b);for(const u of l.ids)c.add(u)}}}return{rows:[...e.values()],placedIds:c,wanted:n,placedWindows:r}}const F=/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g,I=s=>String(s).replace(/\t/g," ").replace(F,"\xB7");function H(s){const o=[...s].sort((n,e)=>n.start-e.start||e.end-n.end),t=[];for(const n of o){const e=t[t.length-1];e&&n.start<=e.end?(e.end=Math.max(e.end,n.end),e.ids.add(n.secretId)):t.push({start:n.start,end:n.end,ids:new Set([n.secretId])})}return t}function O(s){const o=H(s.strikes);let t="",n=0;for(const e of o)t+=s.text.slice(n,e.start)+"\u2022".repeat(e.end-e.start),n=e.end;return t+=s.text.slice(n),{text:t,parts:o}}function N(s,o,t){for(const{window:n}of o.values())for(const e of t){let r;try{r=e(s)}catch{return!0}if(r.includes(n))return!0}return!1}function A(s,o,t){return s.every(n=>!N(O(n).text,o,t))}const P=100;function j(s,o){const{text:t,parts:n}=O(s),e=n[0],r=Math.max(P,e.end-e.start+40);let c=0;t.length>r&&(c=Math.max(0,Math.min(e.start-40,t.length-r)));const f=Math.min(t.length,c+r),d=c>0?"\u2026":"",m=f<t.length?"\u2026":"",g=d+I(t.slice(c,f))+m;let p="",b=0;const l=n.filter($=>$.end>c&&$.start<f);for(const $ of l){const h=d.length+Math.max($.start,c)-c,a=d.length+Math.min($.end,f)-c;p+=" ".repeat(h-b)+"^".repeat(a-h),b=a}const x=[];for(const $ of l)for(const h of $.ids){const a=o(h);a&&!x.includes(a)&&x.push(a)}return{code:g,carets:p,labels:x,column:e.start+1}}function D({kind:s,rows:o,show:t,matches:n,unplacedIds:e,files:r,windows:c,passes:f,sent:d,requests:m,maxBlocks:g=5,shortId:p=b=>String(b).slice(0,8)}){const b=u=>I(n.find(w=>w.secretId===u)?.label??""),l=[],x=s==="staged"?"a registered secret is staged":s==="pushing"?"a registered secret is in a commit being pushed":"a registered secret is in this content";l.push(`${i.red(i.bold("error"))}${i.bold(`: ${x}`)}`);const $=Math.max(1,...o.slice(0,g).map(u=>String(u.line).length)),h=" ".repeat($),a=i.blue(`${h} \u2502`),W=o.slice(0,g);for(const u of W){const w=j(u,b);l.push(`${i.blue(`${h} \u250C\u2500`)} ${I(u.file)}:${u.line}:${w.column}`),t?(l.push(a),l.push(`${i.blue(`${String(u.line).padStart($)} \u2502`)} ${w.code}`),l.push(`${a} ${i.red(w.carets)} ${i.red(w.labels.join(", "))}`.replace(/\s+$/,""))):l.push(`${a} ${i.red(w.labels.join(", "))}`.replace(/\s+$/,"")),l.push(a)}o.length>g&&l.push(`${a} ${i.dim(`\u2026 and ${o.length-g} more place${o.length-g===1?"":"s"}`)}`);for(const u of e)l.push(`${i.blue(`${h} =`)} ${i.bold("not found")} ${i.bold(b(u))} ${i.dim(p(u))} ${i.dim("(split, altered or over several lines, so no place can be shown)")}`);l.push(`${i.blue(`${h} =`)} ${i.bold("checked")} ${c.toLocaleString("en-US")} windows, ${f} pass${f===1?"":"es"}`),l.push(`${i.blue(`${h} =`)} ${i.bold("sent")} ${d}. Your code: 0 B`),l.push(`${i.green("help")}: move the value out of the code, into an environment variable`),l.push(`${i.green("help")}: if it was ever pushed or shared, rotate it, and treat the old value as public`);const S=n.length,M=`${S} secret${S===1?"":"s"}`,v=r>0?` in ${r} file${r===1?"":"s"}`:"";return l.push(""),l.push(s==="staged"?`${i.red(i.bold("error"))}${i.bold(`: commit aborted. ${M}${v}`)}`:s==="pushing"?`${i.red(i.bold("error"))}${i.bold(`: push aborted. ${M}${v}`)}`:`${i.red(i.bold("error"))}${i.bold(`: ${M} found${v}`)}`),l}function E(s,o,t=5){const n=o.filter(r=>r.strikes.some(c=>c.secretId===s)),e=n.slice(0,t).map(r=>{const c=Math.min(...r.strikes.filter(f=>f.secretId===s).map(f=>f.start))+1;return` ${I(r.file)}:${r.line}:${c}`});return n.length>t&&e.push(` \u2026 and ${n.length-t} more`),e}export{C as addedLinesWithPlaces,I as clean,T as locate,E as plainPlaces,D as renderFound,A as safeToDraw,N as stillReadable,k as unquoteGitPath};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import{g as S,k as e}from"./redaktyn-ui.mjs";const T=S.ok==="\u2714",o=T?{top:"\u250C",bar:"\u2502",end:"\u2514",done:"\u25C7",ask:"\u25C6",err:"\u25A0",dot:"\u2022",cut:"\u2026"}:{top:"+",bar:"|",end:"+",done:"o",ask:"*",err:"x",dot:"*",cut:"~"};class j extends Error{constructor(){super("Cancelled"),this.name="Cancelled"}}const A=/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g,m=n=>String(n).replace(A,"\xB7");function E(n){try{const r=new URL(String(n).trim());return`${r.origin}${r.pathname==="/"?"":m(r.pathname)}${r.search?"?\u2026":""}${r.hash?"#\u2026":""}`}catch{const r=m(String(n).trim());return r.length>24?`${r.slice(0,24)}${o.cut}`:r}}function J({input:n=process.stdin,output:r=process.stderr}={}){const s=t=>r.write(t),w=()=>Math.max(40,r.columns||80),$=(t=e.dim)=>t(o.bar);return{open(t,d){s(`${e.dim(o.top)} ${e.bold(t)}${d?e.dim(` v${d}`):""}
|
|
2
|
+
${$()}
|
|
3
|
+
`)},said(t,d){s(`${e.green(o.done)} ${t}
|
|
4
|
+
${$()} ${d}
|
|
5
|
+
${$()}
|
|
6
|
+
`)},ask({label:t,hint:d,mask:h=!1,optional:y=!1}){return s(`${e.clay(o.ask)} ${e.bold(t)}${d?e.dim(` (${d})`):""}
|
|
7
|
+
${e.clay(o.bar)}
|
|
8
|
+
${e.clay(o.end)}`),s("\x1B[1A\r\x1B[3C"),new Promise((R,k)=>{let i="";const C=()=>{const l=w()-6,x=h?o.dot.repeat([...i].length):i,f=[...x],c=f.length>l?o.cut+f.slice(-(l-1)).join(""):x;s(`\r\x1B[2K${e.clay(o.bar)} ${m(c)}`)},b=()=>{n.removeListener("data",g),n.isTTY&&typeof n.setRawMode=="function"&&n.setRawMode(!1),n.pause()},M=l=>{s("\x1B[1A\r\x1B[J"),s(`${e.green(o.done)} ${t}
|
|
9
|
+
${$()} ${l}
|
|
10
|
+
${$()}
|
|
11
|
+
`)};function g(l){const f=[...String(l)];for(let c=0;c<f.length;c++){const a=f[c];if(a==="\r"||a===`
|
|
12
|
+
`){const u=i.trim();if(u===""&&!y)continue;const p=f.slice(c+1).join("");b(),p&&typeof n.unshift=="function"&&n.unshift(p),M(u===""?e.dim("(skipped)"):h?e.dim("(hidden)"):m(u.startsWith("http")?E(u):u)),R(u);return}if(a===""||a===""&&i===""){b(),s("\x1B[1A\r\x1B[J"),k(new j);return}if(a==="\x7F"||a==="\b"){i=[...i].slice(0,-1).join("");continue}if(a===""){i="";continue}if(a===""){i=i.replace(/\S*\s*$/,"");continue}if(a==="\x1B"){if(f[c+1]==="[")for(c+=2;c<f.length&&!/[@-~]/.test(f[c]);)c++;else c+=1;continue}a<" "||(i+=a)}C()}n.isTTY&&typeof n.setRawMode=="function"&&n.setRawMode(!0),typeof n.setEncoding=="function"&&n.setEncoding("utf8"),n.resume(),n.on("data",g)})},gap(){s(`${$()}
|
|
13
|
+
`)},note(t,d){s(`${e.green(o.done)} ${e.bold(t)}
|
|
14
|
+
`);for(const h of d)s(`${$()} ${h}
|
|
15
|
+
`)},close(t=""){s(`${$()}
|
|
16
|
+
${e.dim(o.end)}${t?` ${t}`:""}
|
|
17
|
+
`)},cancelled(){s(`${e.amber(o.err)} Cancelled
|
|
18
|
+
${e.dim(o.end)}
|
|
19
|
+
`)},failed(t){s(`${e.red(o.err)} ${t}
|
|
20
|
+
${e.dim(o.end)}
|
|
21
|
+
`)}}}export{j as Cancelled,o as R,J as createRail,E as shortLink};
|
package/redaktyn-scan.mjs
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{resolveServerUrl as
|
|
3
|
-
`)}
|
|
4
|
-
`)
|
|
5
|
-
`)
|
|
6
|
-
`)
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import{resolveServerUrl as ke}from"./redaktyn-session.mjs";import{LOCK_HEADERS as Ze,explainLockError as et,lockNetwork as tt}from"./redaktyn-lock-net.mjs";import{box as te,humanBytes as ne,k as M,rich as z,rows as nt,say as P}from"./redaktyn-ui.mjs";import{addedLinesWithPlaces as ot,locate as st,plainPlaces as rt,renderFound as it,safeToDraw as ct}from"./redaktyn-places.mjs";import{extractDecodedCandidates as at,hasDecodableRun as lt}from"./redaktyn-decoder.mjs";import{execFileSync as dt,spawnSync as ut}from"node:child_process";import{createRequire as ft}from"node:module";import{readFileSync as F,existsSync as oe,mkdirSync as ht,readdirSync as pt,realpathSync as mt,openSync as gt,readSync as kt,closeSync as yt,statSync as se,writeFileSync as ye,chmodSync as wt,unlinkSync as Et}from"node:fs";import y from"node:path";import{fileURLToPath as $t}from"node:url";const St=ft(import.meta.url);function xt(){try{return St("./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:Rt,scopeOf:Nt,unlockOrgKey:Tt,hmacDigestHex:Ee,normalizeSecretPlaintextForMatch:_t,normalizeSecretPlaintextCollapsed:vt,normalizeSecretPlaintextLower:At,extractAddedLinesFromUnifiedDiff:C,deriveProjectKey:bt,deriveScanPrefilterSeed:$e,scanPrefilterParams:Se,scanPrefilterUnits:xe,scanPrefilterRoller:Re,decodeScanPrefilter:Ot,MIN_SECRET_LENGTH:Ne}=xt(),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"],Dt=e=>J.includes(y.posix.basename(e.replace(/\\/g,"/"))),_e=2e4,ce=3e6,ve=5e6,ae=[{name:"exact",transform:_t,lengthKey:"length",fingerprint:"exact"},{name:"whitespace-collapse",transform:vt,lengthKey:"lengthStripped",fingerprint:"collapsed"},{name:"case-fold",transform:At,lengthKey:"lengthLower",fingerprint:"lower"}],Ae=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=y.relative(process.cwd(),y.resolve(e));return n===""?e:n.startsWith("..")?y.resolve(e):n}function Ct(e,n=8e3){let t;try{t=gt(e,"r");const o=Buffer.alloc(n);return o.subarray(0,kt(t,o,0,n,0))}catch{return null}finally{if(t!==void 0)try{yt(t)}catch{}}}function be(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 Oe(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 c=Ct(e);return c===null?t.unreadable.push(e):!Ae.has(y.extname(e).toLowerCase())&&!(c.includes(0)&&de(c)===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 a=be(s);return a===""?null:(t.fromBinary.add(e),a)}function De(e,n,t=[],o=new Set,s={unreadable:[]}){let r;try{r=mt(e)}catch{return s.unreadable.push(e),t}if(o.has(r))return t;o.add(r);let a;try{a=pt(e)}catch{return s.unreadable.push(e),t}for(const c of a){const l=y.join(e,c),d=y.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()?De(l,n,t,o,s):t.push(l)}return t}function Pe(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=Ot(o.scanPrefilter);t.has(s)||t.set(s,new Set);const a=t.get(s);a!==null&&(r?a.add(r[n.fingerprint]):t.set(s,null))}return t}function*Ie(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 a=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 c=null;for(const l of ae){let d=null,i=null;for(const[f,p]of n.entries()){const E=Pe(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 _=a[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"&&(c=d)}if(c!==null&<(c)){const l=n.map(i=>Pe(i.secrets,ae[0]));let d=0;for(const i of at(c)){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=a[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 a=[...new Set(e.map(i=>i.projectId).filter(Boolean))];let c=null;if(a.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)&&(c=new Map(f.map(p=>[p.id,p.usesOrgKey===!0])))}catch{c=null}const l={key:t,secrets:[]},d=new Map;for(const i of e){const f=i.projectId,p=f&&c?c.get(f):void 0;(!f||p===!0||p===void 0)&&l.secrets.push(i),f&&p!==!0&&(d.has(f)||d.set(f,{key:bt(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 a=new Error(r.error||o.statusText||"HTTP error");throw a.status=o.status,a.body=r,a}return r}async function Ke(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 Ce(){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 j=e=>String(e).replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g,"\xB7");function je(e){if(!Ht(e))return j(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}`},a=await q(`${s}/api/org/key-lock`,{headers:{...r,...Ze}});if(a.status===404)return we(t,o);if(!a.ok)throw new Error(`GET /api/org/key-lock failed: ${a.status}`);const{lock:c}=JSON.parse(a.text);if(!c)return we(t,o);if(!Rt(c))throw new Error("The server sent a key lock this CLI does not understand. Update it.");let l;try{l=await Tt(c,t,o,tt({server:s,token:n,timeoutMs:V()},{scopeOf:Nt}))}catch(d){throw et(d)}if(!l)throw new Error("REDAKTYN_PASSPHRASE is not this workspace's passphrase.");return l}function S(e,n={}){return dt("git",e,{encoding:"utf8",maxBuffer:ie,stdio:["ignore","pipe","pipe"],...n})}function X(e,n){const t=ut("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 a=0,c=0;for(let d=0;d<r;d+=2)s[d]===0&&a++,s[d+1]===0&&c++;const l=r/2;if(c>=l*.9&&a<=l*.02)n="le";else if(a>=l*.9&&c<=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
|
+
`);return t[t.length-1]===""&&t.pop(),t.length===0?"":`diff --git a/${e} b/${e}
|
|
7
|
+
--- /dev/null
|
|
8
|
+
+++ b/${e}
|
|
9
|
+
@@ -0,0 +1,${t.length} @@
|
|
10
|
+
${t.map(o=>`+${o}`).join(`
|
|
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=[C(t.patch)],r=[C(o.patch)],a=[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));Le(n,l.map(i=>({rel:i,spec:`:${i}`,asText:()=>S([...le,"--text","--",`:(literal)${i}`],{cwd:n})})),s,r,a,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:a,top:n}}function Le(e,n,t,o,s,r,a){for(const{rel:c,spec:l,asText:d}of n){if(Ae.has(y.extname(c).toLowerCase()))continue;let i;try{const p=Number(S(["cat-file","-s",l],{cwd:e}).trim());if(!(p<=re)){r&&console.warn(`Skipping large ${a} file (${p} bytes): ${c}`);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(c,p);else{const E=be(i);if(E==="")continue;f=Q(`${c}${ue}`,E)}}else f=d();(Dt(c)?o:t).push(C(f)),s.push(f)}}const ue=" (text inside a binary file)",Ye=/^[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+/);!Ye.test(o??"")||/^0+$/.test(o)||n.push({local:o,remote:Ye.test(s??"")&&!/^0+$/.test(s)?s:null})}return n}const He=["log","--format=","-p","--unified=0","--no-color","--no-ext-diff","--no-textconv"],Ue=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))],a=[];e&&a.push(`--remotes=${e}`);for(const h of n)h.remote&&s(h.remote)&&a.push(h.remote);const c=[...r,...a.length>0?["--not",...a]:[]],l=r.length===0||Number(S(["rev-list","--count",...c],{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([...He,...c,"--",".",...J.map(h=>`:(exclude,glob)**/${h}`)],o),p=l?i:X([...He,...c,"--",...J.map(h=>`:(glob)**/${h}`)],o),E=[C(f.patch)],$=[C(p.patch)],g=[f.patch,p.patch],A=l?"":S(["log","--numstat","--no-renames","-z","--format=%x01%H","--no-ext-diff","--no-textconv",...c],{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 K of h.slice(x+1).split("\0")){const m=/^\n?-\t-\t([\s\S]+)$/.exec(K);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",...c],{cwd:o});for(const h of N.split("")){const x=h.indexOf(""),O=h.lastIndexOf("");if(x<0||O<x)continue;const K=h.slice(x+1,O);if(K.trim()==="")continue;const m=Q(`message of commit ${h.slice(0,8)}`,K);E.push(C(m)),g.push(m)}for(const h of d){const x=Q(`message of tag ${h.id}`,h.message);E.push(C(x)),g.push(x)}const b=T.length>Ue;Le(o,T.slice(0,Ue),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 a;try{a=S(["cat-file","-t",s],{cwd:n}).trim()}catch{break}if(a!=="tag")break;const c=S(["cat-file","tag",s],{cwd:n}),l=c.indexOf(`
|
|
16
|
+
|
|
17
|
+
`),d=l<0?"":c.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(c);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=Ce();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(w){console.error(je(w)),process.exit(2)}}}const r=e.server||o?.serverUrl||ke();I.serverUrl=r;const a=o?.token||t.token,c=o?"session":t.passphrase,l=e.orgId||process.env.REDAKTYN_ORG_ID;(!r||!a||!c)&&(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(a),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=Oe(y.resolve(e.file),e.verbose,u);k===null&&(E=!0,console.error(u.tooLarge.length>0?`${j(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 ${j(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=De(y.resolve(e.dir),e.exclude||[],[],new Set,u),R=[];for(const w of k){const U=Oe(w,e.verbose,u);U!==null&&(R.push(U),b.push({file:W(w)+(u.fromBinary.has(w)?ue:""),text:U}))}if($=u.unreadable.length,u.unreadable.length>0){for(const w of u.unreadable.slice(0,5))console.error(`Could not read ${j(W(w))} \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 w of u.tooLarge.slice(0,5))console.error(`${j(W(w))} 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 ${e.dir} \u2014 nothing was scanned.`)),f=R.join(`
|
|
18
|
+
`)}else he(),process.exit(e.cmd?2:0);const{secrets:_,closed:h}=await Ke(r,a,!!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,a,c,i),K=await jt({secrets:_,savedKey:x,orgKey:O,serverUrl:r,token:a,isAdmin:d?.type==="admin"}),m={total:0,windows:0,passesRun:[],exhausted:!1,truncatedChars:!1},ze=`${r.replace(/\/+$/,"")}/api/cli/scan`,pe=new Set,me=new Map;let Z=0,Y=0;const We=function*(){yield*Ie(f,K,m),!m.exhausted&&p&&(yield*Ie(p,K,m,1/0))};for(const u of We()){const k=[];for(const w of u)pe.has(w)||(pe.add(w),k.push(w));if(k.length===0)continue;Z+=Buffer.byteLength(JSON.stringify({digestHex:k})),Y+=1;const R=await Yt(ze,a,{digestHex:k});for(const w of Array.isArray(R.matches)?R.matches:[])w&&w.secretId&&me.set(w.secretId,w)}const D=[...me.values()];function Ve(){const u=process.cwd(),k=[];if(s)for(const v of T)for(const ee of ot(v))k.push({file:y.relative(u,y.join(N,ee.file)).split(y.sep).join("/"),text:ee.text,line:ee.line});else for(const v of b)k.push(v);const R=st(m.hits??[],k),w=D.map(v=>v.secretId).filter(v=>!R.placedIds.has(v)),U=[...R.wanted.keys()].every(v=>R.placedWindows.has(v)),Xe=ae.map(v=>v.transform),Qe=w.length===0&&U&&R.rows.length>0&&ct(R.rows,R.wanted,Xe);return{rows:R.rows,show:Qe,unplacedIds:w,files:w.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 qe=`${ne(Z)} of fingerprints in ${Y} request${Y===1?"":"s"}. 0 B of your code.`,H=D.length>0?Ve():null;if(z&&D.length>0){P("");for(const u of it({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 ${j(u.label)} (${j(u.secretId)})`);for(const k of rt(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",["",...nt([["Checked",`${m.windows.toLocaleString("en-US")} windows, ${m.passesRun.join(", ")}`],["Sent to the server",qe]],{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=Ce(),o=t.token;let s=null;if(!t.complete){const{loadSession:c,freshSession:l}=await import("./redaktyn-session.mjs");if(c()){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(),a=s?.token||o;if(!r||!a){const c={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(c):c.reason),process.exit(2)}try{const{secrets:c}=await Ke(r,a,!!s),l={connected:!0,source:s?"saved session":"environment",serverUrl:r,...s?.orgName?{workspace:s.orgName}:{},...s?.projectName?{project:s.projectName}:{},enrolledSecrets:Array.isArray(c)?c.length:0,hasPassphrase:s?!0:!!process.env.REDAKTYN_PASSPHRASE,...Ge()?{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(c){const l={connected:!1,error:String(c?.message||c)};console.log(n?JSON.stringify(l):l.error),process.exit(1)}}const Me="# Redaktyn pre-commit",fe=[{name:"pre-commit",marker:Me,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 Fe(){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=y.resolve(process.cwd(),S(["rev-parse","--git-path","hooks"]).trim());return{hooksPath:e,dir:n,hookPath:y.join(n,"pre-commit")}}const Be=e=>`'${String(e).replace(/'/g,"'\\''")}'`,Je=e=>y.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=${Be(Je(e))}`,`REDAKTYN_NODE=${Be(Je(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 Ge(){try{if((S(["config","--get","core.hooksPath"]).trim()||null)!==null)return null}catch{}try{const e=y.resolve(process.cwd(),S(["rev-parse","--git-path","hooks"]).trim()),n=s=>oe(y.join(e,s))?F(y.join(e,s),"utf8"):null,t=n("pre-commit");if(t===null||!t.includes(Me))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=Ge();if(e===null)return;const n=y.join(y.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}=Fe(),t=$t(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 a=y.join(n,r.name);if(oe(a)){const c=F(a,"utf8");if(c.trim()!==""&&!c.includes(r.marker)){console.error(`There is already a ${r.name} hook at ${a}, 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}}ht(n,{recursive:!0}),ye(a,Vt(t,process.execPath,r),{mode:493}),wt(a,493),console.log(`Installed ${r.name} hook at`,a)}s>0&&process.exit(1)}function Qt(){const{dir:e}=Fe();for(const n of fe){const t=y.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}Et(t),console.log(`Removed Redaktyn ${n.name} hook at`,t)}}function he(){console.log(`
|
|
9
21
|
redaktyn-scan \u2014 Redaktyn digest scanner (ZK: HMAC hex batches only)
|
|
10
22
|
|
|
11
23
|
Commands:
|
|
12
24
|
(default) --staged | --file <path> | --dir <path>
|
|
13
25
|
status Show connection + registered fingerprint count
|
|
14
|
-
install-hooks Write this repository's pre-commit
|
|
15
|
-
uninstall-hooks Remove the Redaktyn
|
|
26
|
+
install-hooks Write this repository's pre-commit, commit-msg and pre-push hooks (never over one that is not Redaktyn's)
|
|
27
|
+
uninstall-hooks Remove the Redaktyn git hooks
|
|
16
28
|
|
|
17
29
|
Flags:
|
|
18
30
|
--staged Scan git staged additions (git diff --cached)
|
|
31
|
+
--pushing Scan the commits a push would send that the remote does not have (the pre-push hook feeds this on stdin)
|
|
32
|
+
--remote <name> With --pushing: the remote being pushed to
|
|
33
|
+
--message With --file: the file is a commit message (the commit-msg hook)
|
|
19
34
|
--file <path> Scan a single file
|
|
20
35
|
--dir <path> Scan all text files under directory
|
|
21
36
|
--server <url> Override REDAKTYN_SERVER_URL
|
|
@@ -26,4 +41,4 @@ Flags:
|
|
|
26
41
|
|
|
27
42
|
Environment:
|
|
28
43
|
REDAKTYN_SERVER_URL, REDAKTYN_PASSPHRASE, REDAKTYN_DEVICE_TOKEN (or REDAKTYN_TOKEN)
|
|
29
|
-
`)}
|
|
44
|
+
`)}zt().catch(e=>{console.error(je(e)),process.exit(2)});
|