@redaktyn/cli 1.6.3 → 1.6.5
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-connect.mjs +6 -6
- package/redaktyn-decoder.mjs +6 -0
- package/redaktyn-lock-net.mjs +1 -1
- package/redaktyn-passphrase.mjs +2 -2
- package/redaktyn-places.mjs +6 -0
- package/redaktyn-rail.mjs +21 -0
- package/redaktyn-scan.mjs +24 -10
- package/redaktyn-session.mjs +8 -8
- package/redaktyn-shared.cjs +10 -10
- 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.5",
|
|
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-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-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-passphrase.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createHmac as H}from"node:crypto";import{createRequire as Y}from"node:module";import{createInterface as C}from"node:readline";import{deriveKey as
|
|
2
|
-
`),G=Y(import.meta.url);function U(s){return new Promise(
|
|
1
|
+
import{createHmac as H}from"node:crypto";import{createRequire as Y}from"node:module";import{createInterface as C}from"node:readline";import{deriveKey as S}from"./redaktyn-fingerprint.mjs";import{SessionError as h,apiJson as v,readHidden as K}from"./redaktyn-session.mjs";import{LOCK_HEADERS as O,explainLockError as N,lockNetwork as A}from"./redaktyn-lock-net.mjs";import{box as b,k as u,rich as y,say as m,step as P}from"./redaktyn-ui.mjs";const W="vaultx-passphrase-verify-v1",$=3,d=s=>process.stderr.write(s+`
|
|
2
|
+
`),G=Y(import.meta.url);function U(s){return new Promise(a=>{const e=C({input:process.stdin,output:process.stderr});e.question(s,t=>{e.close(),a(t)})})}function E(){try{return G("./redaktyn-shared.cjs")}catch{return null}}function L(){const s=E();if(!s||typeof s.generateOrgPassphrase!="function"||typeof s.validateGeneratedPassphrase!="function")throw new h("This workspace has no passphrase yet, and this copy of the CLI cannot generate one. Reinstall it (npm install -g @redaktyn/cli@latest), or from a checkout rebuild shared, and run this again.");return s}async function B({server:s,token:a}){const e=await v(s,"GET","/api/org/passphrase-verifier",{token:a});if(!e.ok)return{state:"unknown"};const t=e.data?.verifier;return typeof t=="string"&&t.length>0?{state:"anchored",verifier:t}:{state:"none"}}const _=s=>H("sha256",s).update(W,"utf8").digest("hex");async function q({server:s,token:a}){const e=await v(s,"GET","/api/org/key-lock",{token:a,headers:O});if(e.status===404)return{supported:!1,lock:null};if(!e.ok)throw new h(e.data?.error??`Could not read this workspace's key lock (HTTP ${e.status}).`);const t=E(),n=e.data?.lock??null;if(n&&(!t||typeof t.unlockOrgKey!="function"))throw new h("This workspace's key is locked with the server, and this copy of the CLI cannot open it. Update it (npm install -g @redaktyn/cli@latest) and run this again.");return{supported:!0,lock:n&&t.isOrgKeyLock(n)?n:null}}async function I(s,a,e,t,n){if(e.lock){const r=E();let i;try{try{i=await r.unlockOrgKey(e.lock,s,t.orgId,A(t,r))}catch(w){if(w?.name!=="SecretKeyNeededError"||!process.stdin.isTTY)throw w;n?.pause?.(),d(y?` ${u.dim(w.message)}`:w.message);const l=await K(y?` ${u.clay("\u203A")} Secret Key ${u.dim("(hidden)")} `:"Secret Key: ");n?.resume?.(),i=await r.unlockOrgKey(e.lock,r.joinLockCredential(s,l.trim()),t.orgId,A(t,r))}}catch(w){throw N(w)}return i?{verdict:"ok",key:Buffer.from(i)}:{verdict:"wrong"}}const p=await S(s,t.orgId);return a.state!=="anchored"?{verdict:"unchecked",key:p}:_(p)===a.verifier?{verdict:"ok",key:p}:{verdict:"wrong"}}const D="This workspace already has a different passphrase, set a moment ago by someone else. Run this again and use that one.";async function F({server:s,token:a}){const e=await v(s,"GET","/api/secrets",{token:a});if(!e.ok||!Array.isArray(e.data))throw new h(`This workspace has no passphrase recorded, and the server could not say whether it already has secrets (HTTP ${e.status}), so none was set. Run this again.`);const t=e.data.filter(n=>n?.status!=="NEEDS_REENROLLMENT").length;if(t!==0)throw new h(`This workspace already has ${t} registered secret${t===1?"":"s"}, but its passphrase was never recorded. A passphrase set here would be a new one, which gives the workspace a different key, and none of them would be caught any more. Nothing was changed. To start over, reset the passphrase with passphrase recovery in the dashboard (it marks them for re-registration), then run this again.`)}async function V(s,a,e){const t=E();if(a.supported&&t&&typeof t.createLockedOrgKey=="function")try{const r=await t.createLockedOrgKey(s,e.orgId,A(e,t)),i=await v(e.server,"PUT","/api/org/key-lock",{token:e.token,body:{lock:r.lock,verifier:r.verifier}});if(i.status===409)throw new h(D);if(i.ok)return{recorded:!0,locked:!0,key:Buffer.from(r.orgKey)};if(i.status!==503)throw new h(i.data?.error??`Could not save the workspace key (HTTP ${i.status}).`)}catch(r){if(r?.code!=="KEY_LOCK_UNAVAILABLE")throw N(r)}const n=await S(s,e.orgId),p=await v(e.server,"PUT","/api/org/passphrase-verifier",{token:e.token,body:{verifier:_(n)}});if(p.status===409&&p.data?.code==="KEY_LOCK_REQUIRED")throw new h(p.data.error);if(p.status===409)throw new h(D);return{recorded:p.ok,locked:!1,key:n}}async function ee(s){return(await j(s)).passphrase}async function j({server:s,token:a,orgId:e,who:t=null,label:n="Workspace passphrase",record:p=!0}){t&&(y?m(` ${u.green("\u2714")} ${t.replace(/^Signed in as /,"Signed in as ").replace(/\.$/,"")}`):d(t));const r={server:s,token:a,orgId:e},i=await B({server:s,token:a}),w=await q({server:s,token:a}),l=i.state==="none"&&!w.lock;l&&await F(r);const g=process.env.REDAKTYN_PASSPHRASE,R=async o=>{if(!p)return d(y?` ${u.dim("Dry run: this passphrase is accepted for the preview but not recorded. Run with --apply to set it.")}`:"Dry run: this passphrase is accepted for the preview but not recorded. Run with --apply to set it."),{passphrase:o,orgKey:await S(o,e)};const c=P("Setting this as the workspace passphrase"),{recorded:f,locked:k,key:T}=await V(o,w,r);return f?c.succeed(k?"Workspace passphrase set. The workspace key is locked with the server, so a copy of its database cannot be used to guess it":"Workspace passphrase set. Every device and the dashboard are now checked against it"):c.succeed("Passphrase accepted, but the server did not record it yet. It will be checked once it is"),d(y?` ${u.dim("Nobody can reset it for you. Set recovery codes in the dashboard if you have not.")}`:"Nobody can reset it for you. Set recovery codes in the dashboard if you have not."),{passphrase:o,orgKey:T}};if(g){if(l){if(!L().validateGeneratedPassphrase(g).ok)throw new h("This workspace has no passphrase yet, so REDAKTYN_PASSPHRASE would become it, and a new workspace's passphrase has to be generated. Run this once in a terminal to get one, then set REDAKTYN_PASSPHRASE to it.");return R(g)}const o=P("Checking the passphrase from REDAKTYN_PASSPHRASE"),{verdict:c,key:f}=await I(g,i,w,r,o);if(c==="wrong")throw o.fail("REDAKTYN_PASSPHRASE is not this workspace's passphrase"),new h("REDAKTYN_PASSPHRASE is not this workspace's passphrase.");return o.succeed(c==="ok"?"Passphrase from REDAKTYN_PASSPHRASE verified":"Passphrase from REDAKTYN_PASSPHRASE accepted. The server has nothing to check it against"),{passphrase:g,orgKey:f}}if(!process.stdin.isTTY)throw new h("Set REDAKTYN_PASSPHRASE. It is used locally to derive the key and is never sent.");if(l){const c=L().generateOrgPassphrase();if(!p)return d(y?` ${u.dim("Dry run: this workspace has no passphrase yet. The preview uses a throwaway one. Run with --apply to generate the real one.")}`:"Dry run: this workspace has no passphrase yet. The preview uses a throwaway one. Run with --apply to generate the real one."),{passphrase:c,orgKey:await S(c,e)};const f=["This workspace has no passphrase yet, so one was generated for you.","It derives your keys on this machine and is never sent.","Nobody can reset it for you.","",c,"","Save it in your password manager now.","It stays in this terminal's scrollback until you clear it."];y?(m(""),b("Your workspace passphrase",f,{tone:"clay"}),m("")):(d(""),f.forEach(k=>d(k)),d(""));for(let k=$;k>0;k--){if((await U(y?` ${u.clay("\u203A")} Type ${u.dim("saved")} once you have kept it `:"Type 'saved' once you have kept it > ")).trim().toLowerCase()==="saved")return R(c);d("Not set yet. Save the passphrase above, then type: saved")}throw new h("No passphrase was set after 3 tries. Nothing was changed.")}y?(m(""),b(n,["This derives your keys on this machine and is never sent,","never stored, and not shown while you type."],{tone:"clay"}),m("")):(d(""),d(`${n}: this derives the key on your machine and is never sent. Nothing shows while you type.`));for(let o=$;o>0;o--){const c=await K(y?` ${u.clay("\u203A")} Passphrase ${u.dim("(hidden)")} `:"Type it, then press Enter > ");if(!c){d("Nothing was typed.");continue}const f=P(y?"Checking it against your workspace":"Checking"),{verdict:k,key:T}=await I(c,i,w,r,f);if(k!=="wrong")return f.succeed(k==="ok"?"Passphrase verified. It matches your workspace":"Passphrase accepted. This workspace has nothing to check it against"),{passphrase:c,orgKey:T};f.fail("That is not this workspace's passphrase"),d(y?o>1?` ${u.dim(`Try again (${o-1} left).`)}`:"":o>1?`That is not this workspace's passphrase. Try again (${o-1} left).`:"That is not this workspace's passphrase.")}throw new h("No correct passphrase after 3 tries. Nothing was changed.")}export{j as askWorkspaceKey,ee as askWorkspacePassphrase};
|
|
@@ -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=>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,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{resolveServerUrl as
|
|
3
|
-
`)}async function
|
|
4
|
-
`)+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
|
|
5
|
-
`);return
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import{resolveServerUrl as fe}from"./redaktyn-session.mjs";import{LOCK_HEADERS as Ge,explainLockError as ze,lockNetwork as We}from"./redaktyn-lock-net.mjs";import{box as X,humanBytes as Q,k as B,rich as J,rows as Ve,say as K}from"./redaktyn-ui.mjs";import{addedLinesWithPlaces as qe,locate as Xe,plainPlaces as Qe,renderFound as Ze,safeToDraw as et}from"./redaktyn-places.mjs";import{extractDecodedCandidates as tt,hasDecodableRun as nt}from"./redaktyn-decoder.mjs";import{execFileSync as ot,spawnSync as st}from"node:child_process";import{createRequire as rt}from"node:module";import{readFileSync as L,existsSync as Z,mkdirSync as it,readdirSync as ct,realpathSync as at,statSync as ee,writeFileSync as ue,chmodSync as lt,unlinkSync as dt}from"node:fs";import m from"node:path";import{fileURLToPath as ft}from"node:url";const ut=rt(import.meta.url);function ht(){try{return ut("./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:he,isOrgKeyLock:pt,scopeOf:mt,unlockOrgKey:gt,hmacDigestHex:pe,normalizeSecretPlaintextForMatch:kt,normalizeSecretPlaintextCollapsed:yt,normalizeSecretPlaintextLower:wt,extractAddedLinesFromUnifiedDiff:C,deriveProjectKey:Et,deriveScanPrefilterSeed:me,scanPrefilterParams:ge,scanPrefilterUnits:ke,scanPrefilterRoller:ye,decodeScanPrefilter:xt}=ht(),we=5*1024*1024,Ee=4*1024*1024,te=20*1024*1024,M=["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"],St=e=>M.includes(m.posix.basename(e.replace(/\\/g,"/"))),xe=2e4,ne=3e6,Se=5e6,oe=[{name:"exact",transform:kt,lengthKey:"length",fingerprint:"exact"},{name:"whitespace-collapse",transform:yt,lengthKey:"lengthStripped",fingerprint:"collapsed"},{name:"case-fold",transform:wt,lengthKey:"lengthLower",fingerprint:"lower"}],$e=new Set([".png",".jpg",".jpeg",".gif",".webp",".ico",".pdf",".zip",".gz",".exe",".dll",".so",".dylib",".woff",".woff2",".ttf",".eot",".mp4",".mp3",".wasm",".bin"]);function $t(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 Rt(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 Nt(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 Re(e){const n=m.relative(process.cwd(),m.resolve(e));return n===""?e:n.startsWith("..")?m.resolve(e):n}function Ne(e,n){const t=ee(e);if(!t.isFile())return null;if(t.size>we)return n&&console.warn(`Skipping large file (${t.size} bytes): ${e}`),null;const o=m.extname(e).toLowerCase();if($e.has(o))return null;try{const s=L(e);return s.includes(0)?null:s.toString("utf8")}catch{return null}}function Te(e,n,t=[],o=new Set,s={unreadable:[]}){let r;try{r=at(e)}catch{return s.unreadable.push(e),t}if(o.has(r))return t;o.add(r);let a;try{a=ct(e)}catch{return s.unreadable.push(e),t}for(const l of a){const c=m.join(e,l),d=m.relative(process.cwd(),c);if(Nt(d,n))continue;let i;try{i=ee(c)}catch{continue}i.isDirectory()?Te(c,n,t,o,s):t.push(c)}return t}function _e(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=xt(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*ve(e,n,t,o=Ee){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(c=>{const d=new Map;for(const i of c.secrets)for(const u of[i.hmacHex,i.hmacHexStripped,i.hmacHexLower])typeof u=="string"&&d.set(u,i.id??i.secretId);return d});t.hits??=[];let l=null;for(const c of oe){let d=null,i=null;for(const[u,p]of n.entries()){const w=_e(p.secrets,c);if(w.size===0)continue;d===null&&(d=c.transform(s),i=ke(d),t.passesRun.includes(c.name)||t.passesRun.push(c.name));const E=ge(me(p.key));for(const[x,O]of w){if(x>d.length)continue;const T=O?ye(E,i,x):null;for(let N=0;N+x<=d.length;N++){if(t.windows++,T&&!O.has(T(N)))continue;const A=pe(d.slice(N,N+x),p.key);r.push(A);const _=a[u].get(A);if(_!==void 0&&t.hits.length<500&&t.hits.push({secretId:_,pass:c.name,window:d.slice(N,N+x)}),t.total++,t.total>=ne){t.exhausted=!0,r.length>0&&(yield r);return}r.length>=xe&&(yield r,r=[])}}}c.name==="exact"&&(l=d)}if(l!==null&&nt(l)){const c=n.map(i=>_e(i.secrets,oe[0]));let d=0;for(const i of tt(l)){let u=0;for(const w of c)for(const E of w.keys())u+=Math.max(0,i.decoded.length-E+1);if(u===0||i.bestEffort&&d+u>Se)continue;if(d+=u,d>Se){t.exhausted=!0,r.length>0&&(yield r);return}t.passesRun.includes("encoding")||t.passesRun.push("encoding");const p=ke(i.decoded);for(const[w,E]of n.entries()){const x=c[w];if(x.size===0)continue;const O=ge(me(E.key));for(const[T,N]of x){if(T>i.decoded.length)continue;const A=N?ye(O,p,T):null;for(let _=0;_+T<=i.decoded.length;_++){if(t.windows++,A&&!N.has(A(_)))continue;const h=i.decoded.slice(_,_+T),R=pe(h,E.key);r.push(R);const b=a[w].get(R);if(b!==void 0&&t.hits.length<500&&t.hits.push({secretId:b,pass:"encoding",window:h}),t.total++,t.total>=ne){t.exhausted=!0,r.length>0&&(yield r);return}r.length>=xe&&(yield r,r=[])}}}}}r.length>0&&(yield r)}async function Tt({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 l=null;if(a.length>0&&r)try{const i=await z(`${o.replace(/\/+$/,"")}/api/projects`,{headers:{Authorization:`Bearer ${s}`}}),u=i.ok?JSON.parse(i.text):null;Array.isArray(u)&&(l=new Map(u.map(p=>[p.id,p.usesOrgKey===!0])))}catch{l=null}const c={key:t,secrets:[]},d=new Map;for(const i of e){const u=i.projectId,p=u&&l?l.get(u):void 0;(!u||p===!0||p===void 0)&&c.secrets.push(i),u&&p!==!0&&(d.has(u)||d.set(u,{key:Et(t,u),secrets:[]}),d.get(u).secrets.push(i))}return[c,...d.values()].filter(i=>i.secrets.length>0)}const _t=6e4;function G(){const e=Number(process.env.REDAKTYN_REQUEST_TIMEOUT_MS);return Number.isFinite(e)&&e>=100?e:_t}async function z(e,n={}){const t=G();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 vt(e,n,t){const o=await z(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 Ae(e,n,t=!1){const o=`${e.replace(/\/+$/,"")}/api/secrets`,s=await z(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 be(){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 At(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"};function Oe(e){if(!At(e))return String(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(`
|
|
3
|
+
`)}async function bt(e,n,t,o){const s=e.replace(/\/+$/,""),r={Authorization:`Bearer ${n}`},a=await z(`${s}/api/org/key-lock`,{headers:{...r,...Ge}});if(a.status===404)return he(t,o);if(!a.ok)throw new Error(`GET /api/org/key-lock failed: ${a.status}`);const{lock:l}=JSON.parse(a.text);if(!l)return he(t,o);if(!pt(l))throw new Error("The server sent a key lock this CLI does not understand. Update it.");let c;try{c=await gt(l,t,o,We({server:s,token:n,timeoutMs:G()},{scopeOf:mt}))}catch(d){throw ze(d)}if(!c)throw new Error("REDAKTYN_PASSPHRASE is not this workspace's passphrase.");return c}function y(e,n={}){return ot("git",e,{encoding:"utf8",maxBuffer:te,stdio:["ignore","pipe","pipe"],...n})}function W(e,n){const t=st("git",e,{cwd:n,encoding:"utf8",maxBuffer:te,stdio:["ignore","pipe","pipe"]});if(t.error?.code==="ENOBUFS")return{patch:t.stdout.slice(0,t.stdout.lastIndexOf(`
|
|
4
|
+
`)+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 se=["diff","--cached","--unified=0","--no-color","--no-ext-diff","--no-textconv"];function Ot(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,l=0;for(let d=0;d<r;d+=2)s[d]===0&&a++,s[d+1]===0&&l++;const c=r/2;if(l>=c*.9&&a<=c*.02)n="le";else if(a>=c*.9&&l<=c*.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 re(e,n){const t=n.split(`
|
|
5
|
+
`);return t[t.length-1]===""&&t.pop(),t.length===0?"":`diff --git a/${e} b/${e}
|
|
6
|
+
--- /dev/null
|
|
7
|
+
+++ b/${e}
|
|
8
|
+
@@ -0,0 +1,${t.length} @@
|
|
9
|
+
${t.map(o=>`+${o}`).join(`
|
|
10
|
+
`)}
|
|
11
|
+
`}function Dt(e){const n=y(["rev-parse","--show-toplevel"]).trim(),t=W([...se,"--",".",...M.map(i=>`:(exclude,glob)**/${i}`)],n),o=W([...se,"--",...M.map(i=>`:(glob)**/${i}`)],n),s=[C(t.patch)],r=[C(o.patch)],a=[t.patch,o.patch],c=y(["diff","--cached","--no-ext-diff","--no-textconv","--numstat","--no-renames","-z"],{cwd:n}).split("\0").filter(i=>i.startsWith("- - ")).map(i=>i.slice(4));De(n,c.map(i=>({rel:i,spec:`:${i}`,asText:()=>y([...se,"--text","--",`:(literal)${i}`],{cwd:n})})),s,r,a,e,"staged");const d=i=>i.filter(u=>u.length>0).join(`
|
|
12
|
+
`);return{text:d(s),lockfiles:d(r),cut:t.cut||o.cut,patches:a,top:n}}function De(e,n,t,o,s,r,a){for(const{rel:l,spec:c,asText:d}of n){if($e.has(m.extname(l).toLowerCase()))continue;let i;try{const p=Number(y(["cat-file","-s",c],{cwd:e}).trim());if(!(p<=we)){r&&console.warn(`Skipping large ${a} file (${p} bytes): ${l}`);continue}i=y(["cat-file","blob",c],{cwd:e,encoding:"buffer"})}catch{continue}let u;if(i.includes(0)){const p=Ot(i);if(p===null)continue;u=re(l,p)}else u=d();(St(l)?o:t).push(C(u)),s.push(u)}}const Pe=/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;function Pt(e){const n=[];for(const t of e.split(`
|
|
13
|
+
`)){const[,o,,s]=t.trim().split(/\s+/);!Pe.test(o??"")||/^0+$/.test(o)||n.push({local:o,remote:Pe.test(s??"")&&!/^0+$/.test(s)?s:null})}return n}const Ke=["log","--format=","-p","--unified=0","--no-color","--no-ext-diff","--no-textconv"],Ie=2e3;function Kt(e,n,t){const o=y(["rev-parse","--show-toplevel"]).trim(),s=h=>{try{return y(["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 l=[...r,...a.length>0?["--not",...a]:[]],c=r.length===0||Number(y(["rev-list","--count",...l],{cwd:o}).trim())===0,d=It(n,o);if(c&&d.length===0)return{empty:!0};const i={patch:"",cut:!1},u=c?i:W([...Ke,...l,"--",".",...M.map(h=>`:(exclude,glob)**/${h}`)],o),p=c?i:W([...Ke,...l,"--",...M.map(h=>`:(glob)**/${h}`)],o),w=[C(u.patch)],E=[C(p.patch)],x=[u.patch,p.patch],O=c?"":y(["log","--numstat","--no-renames","-z","--format=%x01%H","--no-ext-diff","--no-textconv",...l],{cwd:o}),T=[];for(const h of O.split("")){const R=h.indexOf("\0");if(R<0)continue;const b=h.slice(0,R);for(const g of h.slice(R+1).split("\0")){const P=/^\n?-\t-\t([\s\S]+)$/.exec(g);P&&T.push({rel:P[1],spec:`${b}:${P[1]}`,asText:()=>y(["diff-tree","--root","-p","--unified=0","--no-color","--no-ext-diff","--no-textconv","--text",b,"--",`:(literal)${P[1]}`],{cwd:o})})}}const N=c?"":y(["log","--format=%x01%H%x02%B%x03",...l],{cwd:o});for(const h of N.split("")){const R=h.indexOf(""),b=h.lastIndexOf("");if(R<0||b<R)continue;const g=h.slice(R+1,b);if(g.trim()==="")continue;const P=re(`message of commit ${h.slice(0,8)}`,g);w.push(C(P)),x.push(P)}for(const h of d){const R=re(`message of tag ${h.id}`,h.message);w.push(C(R)),x.push(R)}const A=T.length>Ie;De(o,T.slice(0,Ie),w,E,x,t,"pushed");const _=h=>h.filter(R=>R.length>0).join(`
|
|
14
|
+
`);return{empty:!1,text:_(w),lockfiles:_(E),cut:u.cut||p.cut||A,patches:x,top:o}}function It(e,n){const t=[];for(const o of e){let s=o.local;for(let r=0;r<8;r++){let a;try{a=y(["cat-file","-t",s],{cwd:n}).trim()}catch{break}if(a!=="tag")break;const l=y(["cat-file","tag",s],{cwd:n}),c=l.indexOf(`
|
|
15
|
+
|
|
16
|
+
`),d=c<0?"":l.slice(c+2);d.trim()!==""&&t.push({id:o.local.slice(0,8),message:d});const i=/^object ([0-9a-f]{40,64})$/m.exec(l);if(!i)break;s=i[1]}}return t}function Ct(){if(process.stdin.isTTY)return"";try{return L(0,"utf8")}catch{return""}}async function jt(){(process.argv.includes("--help")||process.argv.includes("-h"))&&(ce(),process.exit(0));const e=$t(process.argv),n=e.cmd;if(n==="install-hooks"){Lt();return}if(n==="uninstall-hooks"){Mt();return}if(n==="status"){await Yt(e);return}!e.staged&&!e.pushing&&!e.file&&!e.dir&&(ce(),process.exit(2));const t=be();let o=null;I.staged=e.staged===!0||e.pushing===!0||e.message===!0,e.staged===!0&&Ht(),I.hook=e.pushing?"push":"commit";const s=e.staged===!0||e.pushing===!0;if(!t.complete){const{loadSession:f,freshSession:S}=await import("./redaktyn-session.mjs"),$=f();if($){t.partial&&e.verbose&&console.error(`[redaktyn] ${t.partial}; using this machine's saved connection.`),I.serverUrl=$.serverUrl??null;try{o=await S(void 0,{timeoutMs:G()})}catch(k){console.error(Oe(k)),process.exit(2)}}}const r=e.server||o?.serverUrl||fe();I.serverUrl=r;const a=o?.token||t.token,l=o?"session":t.passphrase,c=e.orgId||process.env.REDAKTYN_ORG_ID;(!r||!a||!l)&&(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=Rt(a),i=d?.orgId??d?.org_id??c;i||(console.error("Could not determine orgId (JWT payload orgId or REDAKTYN_ORG_ID)."),process.exit(2)),c&&d?.orgId&&c!==d.orgId&&(console.error("REDAKTYN_ORG_ID does not match JWT orgId."),process.exit(2));let u="",p="",w=!1,E=0,x=!1,O=[],T=null;const N=[];if(e.staged)try{({text:u,lockfiles:p,cut:x,patches:O,top:T}=Dt(e.verbose))}catch{console.error("Not a git repository or git diff failed."),process.exit(2)}else if(e.pushing){let f=Pt(Ct());if(f.length===0&&process.stdin.isTTY)try{f=[{local:y(["rev-parse","HEAD"]).trim(),remote:null}]}catch{f=[]}let S;try{S=f.length===0?{empty:!0}:Kt(e.remote||null,f,e.verbose)}catch{console.error("Not a git repository or git log failed."),process.exit(2)}S.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:u,lockfiles:p,cut:x,patches:O,top:T}=S}else if(e.file){const f=Ne(m.resolve(e.file),e.verbose);f===null&&(w=!0,console.error(`Could not read ${e.file} as text \u2014 nothing was scanned.`)),u=f??"",f!==null&&N.push({file:Re(e.file),text:f})}else if(e.dir){const f={unreadable:[]},S=Te(m.resolve(e.dir),e.exclude||[],[],new Set,f);if(E=f.unreadable.length,f.unreadable.length>0){for(const k of f.unreadable.slice(0,5))console.error(`Could not read the folder ${k} \u2014 it was not scanned.`);f.unreadable.length>5&&console.error(`\u2026 and ${f.unreadable.length-5} more folders.`)}const $=[];for(const k of S){const H=Ne(k,e.verbose);H!==null&&($.push(H),N.push({file:Re(k),text:H}))}$.length===0&&(w=!0,console.error(`No readable text files under ${e.dir} \u2014 nothing was scanned.`)),u=$.join(`
|
|
17
|
+
`)}else ce(),process.exit(e.cmd?2:0);const{secrets:A,closed:_}=await Ae(r,a,!!o);if(Array.isArray(A)||(console.error("GET /api/secrets did not answer with a list."),process.exit(2)),A.length===0){const f=_?"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,..._?{projectClosed:!0}:{},scan:{passes:[],digests:0,windows:0,complete:!0}},null,2)):console.error(f),process.exit(0)}const h=o?new Uint8Array(Buffer.from(o.key,"hex")):null,R=h?null:await bt(r,a,l,i),b=await Tt({secrets:A,savedKey:h,orgKey:R,serverUrl:r,token:a,isAdmin:d?.type==="admin"}),g={total:0,windows:0,passesRun:[],exhausted:!1,truncatedChars:!1},P=`${r.replace(/\/+$/,"")}/api/cli/scan`,ae=new Set,le=new Map;let V=0,Y=0;const Le=function*(){yield*ve(u,b,g),!g.exhausted&&p&&(yield*ve(p,b,g,1/0))};for(const f of Le()){const S=[];for(const k of f)ae.has(k)||(ae.add(k),S.push(k));if(S.length===0)continue;V+=Buffer.byteLength(JSON.stringify({digestHex:S})),Y+=1;const $=await vt(P,a,{digestHex:S});for(const k of Array.isArray($.matches)?$.matches:[])k&&k.secretId&&le.set(k.secretId,k)}const D=[...le.values()];function Me(){const f=process.cwd(),S=[];if(s)for(const v of O)for(const q of qe(v))S.push({file:m.relative(f,m.join(T,q.file)).split(m.sep).join("/"),text:q.text,line:q.line});else for(const v of N)S.push(v);const $=Xe(g.hits??[],S),k=D.map(v=>v.secretId).filter(v=>!$.placedIds.has(v)),H=[...$.wanted.keys()].every(v=>$.placedWindows.has(v)),Be=oe.map(v=>v.transform),Je=k.length===0&&H&&$.rows.length>0&&et($.rows,$.wanted,Be);return{rows:$.rows,show:Je,unplacedIds:k,files:k.length===0?new Set($.rows.map(v=>v.file)).size:0}}const j=g.exhausted||g.truncatedChars||x||w||E>0,de=D.length>0||j&&!s,F=[...x?[`The staged changes are over ${Q(te)}; only the start of them was read.`]:[],...g.truncatedChars?[`Content was truncated at ${Ee} characters.`]:[],...g.exhausted?[`Hit the ${ne} digest ceiling before the end.`]:[]];e.json&&(console.log(JSON.stringify({ok:!de,matches:D,scan:{passes:g.passesRun,digests:g.total,windows:g.windows,complete:!j}},null,2)),process.exit(de?1:0));const Fe=`${Q(V)} of fingerprints in ${Y} request${Y===1?"":"s"}. 0 B of your code.`,U=D.length>0?Me():null;if(J&&D.length>0){K("");for(const f of Ze({kind:e.pushing?"pushing":e.staged?"staged":"content",rows:U.rows,show:U.show,matches:D,unplacedIds:U.unplacedIds,files:U.files,windows:g.windows,passes:g.passesRun.length,sent:`${Q(V)} of fingerprints in ${Y} request${Y===1?"":"s"}`}))K(f);K(""),process.exit(1)}if(J&&j&&s&&(K(""),X(e.pushing?"Push allowed, but not all of it was checked":"Commit allowed, but not all of it was checked",["",...F.map(f=>`${B.amber("\u25B2")} ${f}`),"","Nothing registered was found in the part that was checked.","Smaller commits are checked in full.",""],{tone:"amber",width:72}),K(""),process.exit(0)),J&&j&&(K(""),X("Not a clean result: the scan could not finish",["",...F.map(f=>`${B.amber("\u25B2")} ${f}`),...w?[`${B.amber("\u25B2")} Nothing was read from the path given.`]:[],...E>0?[`${B.amber("\u25B2")} ${E} folder${E===1?"":"s"} could not be read.`]:[],"","Scan a narrower path with --file or --dir, or split the commit.",""],{tone:"amber",width:72}),K(""),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 f of D){console.error(` \u2022 ${f.label} (${f.secretId})`);for(const S of Qe(f.secretId,U.rows))console.error(S)}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(j&&s){console.error(""),console.error(`Redaktyn: ${e.pushing?"push":"commit"} allowed, but not all of it was checked.`);for(const f of F)console.error(` \u2022 ${f}`);console.error("Nothing registered was found in the part that was checked. Smaller commits are checked in full."),process.exit(0)}if(j){console.error(""),console.error("Redaktyn could not finish scanning this input, so this is not a clean result.");for(const f of F)console.error(` \u2022 ${f}`);w&&console.error(" \u2022 Nothing was read from the path given, so no content was checked."),E>0&&console.error(` \u2022 ${E} folder${E===1?"":"s"} could not be read, so their contents were not checked.`),console.error("Scan a narrower path with --file or --dir, or split the commit."),process.exit(1)}e.verbose&&(J?(K(""),X("Clean: no registered fingerprint found",["",...Ve([["Checked",`${g.windows.toLocaleString("en-US")} windows, ${g.passesRun.join(", ")}`],["Sent to the server",Fe]],{indent:0,labelWidth:20}),""],{tone:"green",width:78}),K("")):console.log(`Redaktyn: no registered fingerprint matches (${g.passesRun.join(", ")}; ${g.windows} windows).`)),process.exit(0)}async function Yt(e){const n=e.json===!0,t=be(),o=t.token;let s=null;if(!t.complete){const{loadSession:l,freshSession:c}=await import("./redaktyn-session.mjs");if(l()){t.partial&&e.verbose&&console.error(`[redaktyn] ${t.partial}; using this machine's saved connection.`);try{s=await c(void 0,{timeoutMs:G()})}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||fe(),a=s?.token||o;if(!r||!a){const l={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(l):l.reason),process.exit(2)}try{const{secrets:l}=await Ae(r,a,!!s),c={connected:!0,source:s?"saved session":"environment",serverUrl:r,...s?.orgName?{workspace:s.orgName}:{},...s?.projectName?{project:s.projectName}:{},enrolledSecrets:Array.isArray(l)?l.length:0,hasPassphrase:s?!0:!!process.env.REDAKTYN_PASSPHRASE,...He()?{hooks:"older version; run `redaktyn-scan install-hooks`"}:{}};console.log(n?JSON.stringify(c,null,2):Object.entries(c).map(([d,i])=>`${d}: ${i}`).join(`
|
|
18
|
+
`)),process.exit(0)}catch(l){const c={connected:!1,error:String(l?.message||l)};console.log(n?JSON.stringify(c):c.error),process.exit(1)}}const Ce="# Redaktyn pre-commit",ie=[{name:"pre-commit",marker:Ce,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 je(){try{y(["rev-parse","--show-toplevel"])}catch{console.error("Not inside a git repository."),process.exit(2)}let e=null;try{e=y(["config","--get","core.hooksPath"]).trim()||null}catch{e=null}const n=m.resolve(process.cwd(),y(["rev-parse","--git-path","hooks"]).trim());return{hooksPath:e,dir:n,hookPath:m.join(n,"pre-commit")}}const Ye=e=>`'${String(e).replace(/'/g,"'\\''")}'`,Ue=e=>m.sep==="\\"?e.replace(/\\/g,"/"):e;function Ut(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=${Ye(Ue(e))}`,`REDAKTYN_NODE=${Ye(Ue(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(`
|
|
19
|
+
`)}function He(){try{if((y(["config","--get","core.hooksPath"]).trim()||null)!==null)return null}catch{}try{const e=m.resolve(process.cwd(),y(["rev-parse","--git-path","hooks"]).trim()),n=s=>Z(m.join(e,s))?L(m.join(e,s),"utf8"):null,t=n("pre-commit");if(t===null||!t.includes(Ce))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 Ht(){const e=He();if(e===null)return;const n=m.join(m.dirname(e.dir),"redaktyn-hooks-noted");try{if(Date.now()-ee(n).mtimeMs<7*24*60*60*1e3)return}catch{}console.error(`Redaktyn: ${e.text}`);try{ue(n,String(Date.now()))}catch{}}function Lt(){const{hooksPath:e,dir:n}=je(),t=ft(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 ie)console.error(` ${r.name}: ${o(r)}`);process.exit(1)}let s=0;for(const r of ie){const a=m.join(n,r.name);if(Z(a)){const l=L(a,"utf8");if(l.trim()!==""&&!l.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}}it(n,{recursive:!0}),ue(a,Ut(t,process.execPath,r),{mode:493}),lt(a,493),console.log(`Installed ${r.name} hook at`,a)}s>0&&process.exit(1)}function Mt(){const{dir:e}=je();for(const n of ie){const t=m.join(e,n.name);if(!Z(t)){n.name==="pre-commit"&&console.log("No pre-commit hook found.");continue}if(!L(t,"utf8").includes(n.marker)){console.log(`${n.name} exists but is not a Redaktyn hook \u2014 not removing.`);continue}dt(t),console.log(`Removed Redaktyn ${n.name} hook at`,t)}}function ce(){console.log(`
|
|
9
20
|
redaktyn-scan \u2014 Redaktyn digest scanner (ZK: HMAC hex batches only)
|
|
10
21
|
|
|
11
22
|
Commands:
|
|
12
23
|
(default) --staged | --file <path> | --dir <path>
|
|
13
24
|
status Show connection + registered fingerprint count
|
|
14
|
-
install-hooks Write this repository's pre-commit
|
|
15
|
-
uninstall-hooks Remove the Redaktyn
|
|
25
|
+
install-hooks Write this repository's pre-commit, commit-msg and pre-push hooks (never over one that is not Redaktyn's)
|
|
26
|
+
uninstall-hooks Remove the Redaktyn git hooks
|
|
16
27
|
|
|
17
28
|
Flags:
|
|
18
29
|
--staged Scan git staged additions (git diff --cached)
|
|
30
|
+
--pushing Scan the commits a push would send that the remote does not have (the pre-push hook feeds this on stdin)
|
|
31
|
+
--remote <name> With --pushing: the remote being pushed to
|
|
32
|
+
--message With --file: the file is a commit message (the commit-msg hook)
|
|
19
33
|
--file <path> Scan a single file
|
|
20
34
|
--dir <path> Scan all text files under directory
|
|
21
35
|
--server <url> Override REDAKTYN_SERVER_URL
|
|
@@ -26,4 +40,4 @@ Flags:
|
|
|
26
40
|
|
|
27
41
|
Environment:
|
|
28
42
|
REDAKTYN_SERVER_URL, REDAKTYN_PASSPHRASE, REDAKTYN_DEVICE_TOKEN (or REDAKTYN_TOKEN)
|
|
29
|
-
`)}
|
|
43
|
+
`)}jt().catch(e=>{console.error(Oe(e)),process.exit(2)});
|
package/redaktyn-session.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{createHash as
|
|
2
|
-
`)}}}function
|
|
3
|
-
`)}function
|
|
4
|
-
`)}function
|
|
5
|
-
`),t}const A=e=>Buffer.from(e.replace(/-/g,"+").replace(/_/g,"/"),"base64"),ue=e=>Buffer.from(e).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");function le(e){const t=String(e).trim();for(const[n,o]of[['"','"'],["'","'"],["<",">"],["`","`"]])if(t.length>2&&t.startsWith(n)&&t.endsWith(o))return t.slice(1,-1).trim();return t}function D(e){let t;try{t=new URL(le(e))}catch{throw new a("That is not an invite link. It looks like https://\u2026/connect?code=\u2026#w=\u2026")}const n=t.searchParams.get("code"),o=new URLSearchParams(t.hash.replace(/^#/,"")),r=o.has("w2"),s=o.get("w2")??o.get("w");if(!n)throw new a("The link has no invite code.");if(!s)throw new a("The link has no key part (the bit after #w=). Copy the whole link, including everything after the #, and quote it so the shell does not cut it.");if(A(s).length!==$)throw new a("The key part of the link is the wrong length.");return{origin:t.origin,code:n,w:s,needsCode:r}}const U="0123456789ABCDEFGHJKMNPQRSTVWXYZ",N=16,Y=e=>String(e??"").toUpperCase().replace(/[^0-9A-Z]/g,"").replace(/O/g,"0").replace(/[IL]/g,"1");function J(e,t){const n=Q("sha256",e).update(`redaktyn-invite-code|${t}`).digest();return U[n[0]>>3]+U[(n[0]&7)<<2|n[1]>>6]}function Ie(e){return D(e).needsCode}function fe(e,t){const n=Y(t);return n.length!==N+2?!1:J(A(D(e).w),n.slice(0,N))===n.slice(N)}async function V(e,t,n){const o=await S.subtle.importKey("raw",e,"HKDF",!1,["deriveKey"]);return S.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:Buffer.from("redaktyn-invite-wrap-v2"),info:Buffer.from(t)},o,{name:"AES-GCM",length:256},!1,[n])}async function he(e,t,n){if(!e||typeof e!="object"||e.v!==1&&e.v!==2||typeof e.iv!="string"||typeof e.ct!="string")throw new a("The server sent no sealed key for this invite.");if(e.s!==void 0&&e.s!==1)throw new a("This invite was made by a newer version. Update and try again.");if(e.s===1&&!n)throw new a("This invite needs the security code your admin sent with the link.");const o=e.s===1?await V(A(t),Y(n).slice(0,N),"decrypt"):await S.subtle.importKey("raw",A(t),{name:"AES-GCM"},!1,["decrypt"]);try{const r=await S.subtle.decrypt({name:"AES-GCM",iv:Buffer.from(e.iv,"base64")},o,Buffer.from(e.ct,"base64")),s=Buffer.from(r);if(s.length!==$)throw new Error("length");return s}catch{throw new a("Could not open the sealed key with this link. The link may be incomplete or altered.")}}async function Re(e,t){const n=I($),o=I(12);let r="";for(const p of I(N))r+=U[p&31];const s=(r+J(n,r)).match(/.{1,4}/g).join("-"),d=await V(n,r,"encrypt"),c=Buffer.from(await S.subtle.encrypt({name:"AES-GCM",iv:o},d,e));return{w:ue(n),code:s,blob:{v:t,s:1,iv:o.toString("base64"),ct:c.toString("base64")}}}const j=e=>e.replace(/\/+$/,"");async function m(e,t,n,{token:o,body:r,headers:s}={}){let d;try{d=await fetch(`${j(e)}${n}`,{method:t,headers:{...o?{Authorization:`Bearer ${o}`}:{},...r?{"Content-Type":"application/json"}:{},...s??{}},...r?{body:JSON.stringify(r)}:{}})}catch(y){const w=new a(`Cannot reach ${j(e)}: ${y.message}`);throw w.unreachable=!0,w}const c=await d.text();let p=null;try{p=c?JSON.parse(c):null}catch{p=null}return{status:d.status,ok:d.ok,data:p,text:c}}const pe=4e3,we=e=>new Promise(t=>setTimeout(t,e));async function me(e,t,n){n?.({expiresAt:t.expiresAt});const o=Date.parse(t.expiresAt);for(;Date.now()<o;){await we(pe);let r;try{r=await m(e,"GET",`/api/device-approvals/status/${encodeURIComponent(t.approvalId)}`,{headers:{"X-Approval-Secret":t.approvalSecret}})}catch{continue}if(r.status===404)break;if(r.data?.status==="APPROVED")return;if(r.data?.status==="REJECTED")throw new a("Your admin declined this device. Ask them for a new link if you think that was a mistake.");if(r.data?.status==="EXPIRED")break}throw new a("Your admin did not answer in time. Run the command again to ask again.")}async function ge(e,{serverUrl:t}={}){try{const{origin:n,code:o}=D(e),r=await m(t||process.env.REDAKTYN_SERVER_URL||n,"GET",`/api/auth/invite-info?code=${encodeURIComponent(o)}`);return!r.ok||!r.data?null:{assignedName:r.data.assignedName??null,orgId:typeof r.data.orgId=="string"?r.data.orgId:null,orgName:r.data.orgName??null,managed:r.data.managed===!0}}catch{return null}}function G(e,t){const n=o=>o.orgName?`the ${o.orgName} workspace`:"another workspace";return new a(`This machine is connected to ${n(e)}, and this link is for ${t.orgName?`the ${t.orgName} workspace`:"a different one"}. Connecting would replace that session, and ${e.orgName??"its"} repositories would no longer be checked on this machine. ${t.spent?"The link was used, but this machine kept its current session.":"The link was not used and still works."} To switch, run redaktyn-connect --switch with the link, or disconnect first (redaktyn-connect --logout, or "Redaktyn: Disconnect this device" in VS Code).`)}function X(e){const t=typeof e=="string"?e.trim().slice(0,100):"";return t.length>0?t:void 0}async function xe(e,{serverUrl:t,onWaiting:n,userName:o,inviteCode:r,replaceOtherWorkspace:s=!1}={}){const{origin:d,code:c,w:p,needsCode:y}=D(e);if(y&&!fe(e,r))throw new a(r?"That security code does not fit this link. Check it against what your admin sent. Nothing was used up.":"This link also needs the security code your admin sent with it.");const w=t||process.env.REDAKTYN_SERVER_URL||d,h=s?null:_();if(h?.orgId){const f=await ge(e,{serverUrl:w});if(f?.orgId?f.orgId!==h.orgId:!!(f?.orgName&&h.orgName&&f.orgName!==h.orgName))throw G(h,{orgName:f.orgName,spent:!1})}let i,u;for(let f=0;u=await m(w,"POST","/api/auth/connect",{body:{inviteCode:c,extensionId:de(),client:"cli",...X(o)?{userName:X(o)}:{},...i?{approval:i}:{}}}),u.status===202;f+=1){const g=u.data||{},O=g.approvalSecret??(i&&i.id===g.approvalId?i.secret:void 0);if(f>=3||!g.approvalId||!O||!g.expiresAt)throw new a("Your admin has not approved this device yet. Run the command again in a moment.");i={id:g.approvalId,secret:O},await me(w,{approvalId:i.id,approvalSecret:O,expiresAt:g.expiresAt},n)}if(u.status===409&&u.data?.code==="ALREADY_ON_PROJECT"){const f=String(u.data.error??"").match(/already protecting the (.+?) project\./)?.[1]??_()?.projectName;throw new a(`This machine is already connected to ${f?`the ${f} project`:"another project"}, and one machine covers one project for now. The link was not used and still works: ask your admin to remove this machine from ${f??"that project"}, then run redaktyn-connect with the same link again.`)}if(!u.ok){const f=u.data?.error||u.text.slice(0,200)||`HTTP ${u.status}`;throw new a(`The server refused the invite (${u.status}): ${f}`)}const l=u.data||{};if(!l.token||!l.keyBlob)throw new a("The server accepted the invite but sent no sealed key, so nothing could be set up. Ask for a new link.");if(h?.orgId&&l.orgId&&l.orgId!==h.orgId)throw G(h,{orgName:l.orgName??null,spent:!0});const E=await he(l.keyBlob,p,r),B={serverUrl:j(w),orgId:l.orgId,orgName:l.orgName??null,projectId:l.projectId??null,projectName:l.projectName??null,projectUsesOrgKey:l.projectUsesOrgKey??null,canEnrol:l.canEnrol===!0,token:l.token,tokenIssuedAt:Date.now(),key:E.toString("hex"),keyFingerprint:Z("sha256").update(E).digest("hex").slice(0,8),connectedAt:new Date().toISOString()};return W(B),B}function q(e){try{const t=JSON.parse(Buffer.from(String(e).split(".")[1],"base64url").toString("utf8")).exp;return typeof t=="number"&&t*1e3>Date.now()+6e4}catch{return!1}}const z=e=>e.status===401||e.status===403;async function Ae(e=_()){if(!e)throw new a("Not connected. Run: redaktyn-connect '<invite link>'");if(Date.now()-(e.tokenIssuedAt||0)<L)return e;const t=await m(e.serverUrl,"POST","/api/auth/refresh",{token:e.token});if(!t.ok||!t.data?.token){if(!z(t)){if(q(e.token))return e;throw new a(`The server could not renew this machine's session just now (HTTP ${t.status}). Try again in a few minutes; you do not need a new invite link.`)}const o=t.data?.code,r=o==="ENROLLMENT_REVOKED"||o==="SESSION_REVOKED"?"This device was removed by an admin.":"The session could not be renewed (it may have passed its maximum age).";throw new a(`${r} Ask for a new invite link and run redaktyn-connect again.`)}const n={...e,token:t.data.token,tokenIssuedAt:Date.now()};return W(n),n}async function De(e,t,{serverUrl:n}={}){const o=M(n),r=await m(o,"POST","/api/auth/login",{body:{email:e,password:t}});if(r.status===429)throw new a("Too many attempts for this account. Wait a few minutes and try again.");if(r.ok&&r.data?.secondStep===!0)throw new a("This account has two-step sign-in. Run redaktyn-login without a password: it signs in through your browser, where the dashboard asks for the code.");if(!r.ok||!r.data?.token)throw new a(r.status===401?"Wrong email or password.":`Login failed (${r.status}): ${r.data?.error||r.text.slice(0,160)}`);const s=r.data.user?.role;if(s!=="OWNER"&&s!=="ADMIN")throw new a(`This account is a ${s??"non-admin"}. Bulk import and invites need an OWNER or ADMIN login.`);const d={serverUrl:o,token:r.data.token,tokenIssuedAt:Date.now(),loggedInAt:new Date().toISOString(),email:r.data.user.email,role:s,orgId:r.data.org?.id??null,orgName:r.data.org?.name??null};return P(d),d}async function Oe(e=ce()){if(!e)throw new a("Not logged in. Run: redaktyn-login");if(Date.now()-(e.tokenIssuedAt||0)<L)return e;const t=await m(e.serverUrl,"POST","/api/auth/refresh",{token:e.token});if(!t.ok||!t.data?.token){if(z(t))throw new a("Your login has expired. Run: redaktyn-login");if(q(e.token))return e;throw new a(`The server could not renew your login just now (HTTP ${t.status}). Try again in a few minutes.`)}const n={...e,token:t.data.token,tokenIssuedAt:Date.now()};return P(n),n}function $e(e){return new Promise(t=>{const n=process.stdin;if(!n.isTTY||typeof n.setRawMode!="function"){let d="";n.setEncoding("utf8"),n.on("data",c=>d+=c),n.on("end",()=>t(d.split(/\r?\n/)[0]??""));return}process.stderr.write(e);let o="";n.setRawMode(!0),n.resume(),n.setEncoding("utf8");const r=()=>{n.setRawMode(!1),n.pause(),n.off("data",s),process.stderr.write(`
|
|
6
|
-
`),t(o)},s
|
|
7
|
-
`||
|
|
8
|
-
`),process.exit(130)),
|
|
1
|
+
import{createHash as re,createHmac as oe,randomBytes as x,webcrypto as N}from"node:crypto";import{existsSync as I,mkdirSync as ae,readFileSync as O,rmSync as H,writeFileSync as ie,chmodSync as W,renameSync as se}from"node:fs";import{homedir as ce,hostname as de}from"node:os";import{join as S}from"node:path";const Y=20*60*60*1e3,P=32,ue="https://redaktyn.com";function V(e){return(e||process.env.REDAKTYN_SERVER_URL||process.env.REDAKTYN_SERVER||ue).replace(/\/+$/,"")}class i extends Error{}function T(){return process.env.REDAKTYN_CONFIG_DIR||(process.env.XDG_CONFIG_HOME?S(process.env.XDG_CONFIG_HOME,"redaktyn"):S(ce(),".config","redaktyn"))}const L=()=>S(T(),"session.json"),le=()=>S(T(),"device-id");function fe(){ae(T(),{recursive:!0,mode:448});try{W(T(),448)}catch{}}function b(e,t){fe();const n=`${e}.${process.pid}.tmp`;ie(n,t,{mode:384});try{W(n,384)}catch{}se(n,e)}const J=()=>S(T(),"oprf-pins.json");function Oe(){const e=()=>{try{const t=JSON.parse(O(J(),"utf8"));return t&&typeof t=="object"&&!Array.isArray(t)?t:{}}catch{return{}}};return{get(t,n){const o=e()[`${t}|${n}`];return typeof o=="string"?o:null},set(t,n,o){b(J(),JSON.stringify({...e(),[`${t}|${n}`]:o})+`
|
|
2
|
+
`)}}}function j(){const e=L();if(!I(e))return null;try{const t=JSON.parse(O(e,"utf8"));if(t&&t.token&&t.key&&t.serverUrl&&t.orgId)return t}catch{}return null}function G(e){b(L(),JSON.stringify(e,null,2)+`
|
|
3
|
+
`)}function be(){const e=L();return I(e)&&H(e),!0}const U=()=>S(T(),"admin.json");function he(){const e=U();if(!I(e))return null;try{const t=JSON.parse(O(e,"utf8"));if(t&&t.token&&t.serverUrl)return t}catch{}return null}function B(e){b(U(),JSON.stringify(e,null,2)+`
|
|
4
|
+
`)}function De(){const e=U();return I(e)&&H(e),!0}function we(){const e=le();if(I(e)){const n=O(e,"utf8").trim();if(/^cli-[a-f0-9]{32}$/.test(n))return n}const t=`cli-${x(16).toString("hex")}`;return b(e,t+`
|
|
5
|
+
`),t}const D=e=>Buffer.from(e.replace(/-/g,"+").replace(/_/g,"/"),"base64"),pe=e=>Buffer.from(e).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");function me(e){const t=String(e).trim();for(const[n,o]of[['"','"'],["'","'"],["<",">"],["`","`"]])if(t.length>2&&t.startsWith(n)&&t.endsWith(o))return t.slice(1,-1).trim();return t}function $(e){let t;try{t=new URL(me(e))}catch{throw new i("That is not an invite link. It looks like https://\u2026/connect?code=\u2026#w=\u2026")}const n=t.searchParams.get("code"),o=new URLSearchParams(t.hash.replace(/^#/,"")),r=o.has("w2"),a=o.get("w2")??o.get("w");if(!n)throw new i("The link has no invite code.");if(!a)throw new i("The link has no key part (the bit after #w=). Copy the whole link, including everything after the #, and quote it so the shell does not cut it.");if(D(a).length!==P)throw new i("The key part of the link is the wrong length.");return{origin:t.origin,code:n,w:a,needsCode:r}}const K="0123456789ABCDEFGHJKMNPQRSTVWXYZ",A=16,X=e=>String(e??"").toUpperCase().replace(/[^0-9A-Z]/g,"").replace(/O/g,"0").replace(/[IL]/g,"1");function q(e,t){const n=oe("sha256",e).update(`redaktyn-invite-code|${t}`).digest();return K[n[0]>>3]+K[(n[0]&7)<<2|n[1]>>6]}function $e(e){return $(e).needsCode}function ge(e,t){const n=X(t);return n.length!==A+2?!1:q(D($(e).w),n.slice(0,A))===n.slice(A)}async function z(e,t,n){const o=await N.subtle.importKey("raw",e,"HKDF",!1,["deriveKey"]);return N.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:Buffer.from("redaktyn-invite-wrap-v2"),info:Buffer.from(t)},o,{name:"AES-GCM",length:256},!1,[n])}async function ye(e,t,n){if(!e||typeof e!="object"||e.v!==1&&e.v!==2||typeof e.iv!="string"||typeof e.ct!="string")throw new i("The server sent no sealed key for this invite.");if(e.s!==void 0&&e.s!==1)throw new i("This invite was made by a newer version. Update and try again.");if(e.s===1&&!n)throw new i("This invite needs the security code your admin sent with the link.");const o=e.s===1?await z(D(t),X(n).slice(0,A),"decrypt"):await N.subtle.importKey("raw",D(t),{name:"AES-GCM"},!1,["decrypt"]);try{const r=await N.subtle.decrypt({name:"AES-GCM",iv:Buffer.from(e.iv,"base64")},o,Buffer.from(e.ct,"base64")),a=Buffer.from(r);if(a.length!==P)throw new Error("length");return a}catch{throw new i("Could not open the sealed key with this link. The link may be incomplete or altered.")}}async function Ce(e,t){const n=x(P),o=x(12);let r="";for(const w of x(A))r+=K[w&31];const a=(r+q(n,r)).match(/.{1,4}/g).join("-"),s=await z(n,r,"encrypt"),d=Buffer.from(await N.subtle.encrypt({name:"AES-GCM",iv:o},s,e));return{w:pe(n),code:a,blob:{v:t,s:1,iv:o.toString("base64"),ct:d.toString("base64")}}}const C=e=>e.replace(/\/+$/,"");async function g(e,t,n,{token:o,body:r,headers:a,signal:s,timeoutMs:d}={}){if(s?.aborted)throw R();const w=new AbortController,k=d>0?setTimeout(()=>w.abort(),d):void 0,v=()=>w.abort();s?.addEventListener("abort",v,{once:!0});try{let m;try{m=await fetch(`${C(e)}${n}`,{method:t,headers:{...o?{Authorization:`Bearer ${o}`}:{},...r?{"Content-Type":"application/json"}:{},...a??{}},...r?{body:JSON.stringify(r)}:{},signal:w.signal})}catch(u){throw Z(u,e,s,d)}let c;try{c=await m.text()}catch(u){throw Z(u,e,s,d)}return ke(m,c)}finally{clearTimeout(k),s?.removeEventListener("abort",v)}}const R=()=>Object.assign(new i("Connecting was cancelled. Nothing was saved."),{aborted:!0});function Z(e,t,n,o){if(n?.aborted)return R();if(e?.name==="AbortError"){const a=Math.max(1,Math.round(o/1e3));return Object.assign(new i(`No answer from ${C(t)} within ${a} s.`),{unreachable:!0,timedOut:!0})}const r=new i(`Cannot reach ${C(t)}: ${e.message}`);return r.unreachable=!0,r}function ke(e,t){let n=null;try{n=t?JSON.parse(t):null}catch{n=null}return{status:e.status,ok:e.ok,data:n,text:t}}const ve=4e3,Se=(e,t)=>new Promise((n,o)=>{if(t?.aborted)return o(R());const r=()=>{clearTimeout(s),t?.removeEventListener("abort",a)},a=()=>{r(),o(R())},s=setTimeout(()=>{r(),n()},e);t?.addEventListener("abort",a,{once:!0})}),Te=15e3;async function Ee(e,t,n,o){n?.({expiresAt:t.expiresAt});const r=Date.parse(t.expiresAt);for(;Date.now()<r;){await Se(ve,o);let a;try{a=await g(e,"GET",`/api/device-approvals/status/${encodeURIComponent(t.approvalId)}`,{headers:{"X-Approval-Secret":t.approvalSecret},...o?{signal:o,timeoutMs:Te}:{}})}catch(s){if(s?.aborted)throw s;continue}if(a.status===404)break;if(a.data?.status==="APPROVED")return;if(a.data?.status==="REJECTED")throw new i("Your admin declined this device. Ask them for a new link if you think that was a mistake.");if(a.data?.status==="EXPIRED")break}throw new i("Your admin did not answer in time. Run the command again to ask again.")}async function Ne(e,{serverUrl:t,signal:n,timeoutMs:o}={}){try{const{origin:r,code:a}=$(e),s=await g(t||process.env.REDAKTYN_SERVER_URL||r,"GET",`/api/auth/invite-info?code=${encodeURIComponent(a)}`,{signal:n,timeoutMs:o});return!s.ok||!s.data?null:{assignedName:s.data.assignedName??null,orgId:typeof s.data.orgId=="string"?s.data.orgId:null,orgName:s.data.orgName??null,managed:s.data.managed===!0}}catch{return null}}function Q(e,t){const n=o=>o.orgName?`the ${o.orgName} workspace`:"another workspace";return new i(`This machine is connected to ${n(e)}, and this link is for ${t.orgName?`the ${t.orgName} workspace`:"a different one"}. Connecting would replace that session, and ${e.orgName??"its"} repositories would no longer be checked on this machine. ${t.spent?"The link was used, but this machine kept its current session.":"The link was not used and still works."} To switch, run redaktyn-connect --switch with the link, or disconnect first (redaktyn-connect --logout, or "Redaktyn: Disconnect this device" in VS Code).`)}function ee(e){const t=typeof e=="string"?e.trim().slice(0,100):"";return t.length>0?t:void 0}async function _e(e,{serverUrl:t,onWaiting:n,userName:o,inviteCode:r,replaceOtherWorkspace:a=!1,signal:s,requestTimeoutMs:d}={}){const{origin:w,code:k,w:v,needsCode:m}=$(e);if(m&&!ge(e,r))throw new i(r?"That security code does not fit this link. Check it against what your admin sent. Nothing was used up.":"This link also needs the security code your admin sent with it.");const c=t||process.env.REDAKTYN_SERVER_URL||w,u=a?null:j();if(u?.orgId){const f=await Ne(e,{serverUrl:c,signal:s,timeoutMs:d});if(f?.orgId?f.orgId!==u.orgId:!!(f?.orgName&&u.orgName&&f.orgName!==u.orgName))throw Q(u,{orgName:f.orgName,spent:!1})}let p,l;for(let f=0;;f+=1){if(s?.aborted)throw R();try{l=await g(c,"POST","/api/auth/connect",{signal:s,timeoutMs:d,body:{inviteCode:k,extensionId:we(),client:"cli",...ee(o)?{userName:ee(o)}:{},...p?{approval:p}:{}}})}catch(E){throw E?.timedOut?Object.assign(new i(`${E.message} The link may not have been used: try again, and if the server says it was, ask your admin for a new one.`),{unreachable:!0,timedOut:!0}):(E?.aborted&&(E.linkMayBeUsed=!0),E)}if(l.status!==202)break;const y=l.data||{},_=y.approvalSecret??(p&&p.id===y.approvalId?p.secret:void 0);if(f>=3||!y.approvalId||!_||!y.expiresAt)throw new i("Your admin has not approved this device yet. Run the command again in a moment.");p={id:y.approvalId,secret:_},await Ee(c,{approvalId:p.id,approvalSecret:_,expiresAt:y.expiresAt},n,s)}if(l.status===409&&l.data?.code==="ALREADY_ON_PROJECT"){const f=String(l.data.error??"").match(/already protecting the (.+?) project\./)?.[1]??j()?.projectName;throw new i(`This machine is already connected to ${f?`the ${f} project`:"another project"}, and one machine covers one project for now. The link was not used and still works: ask your admin to remove this machine from ${f??"that project"}, then run redaktyn-connect with the same link again.`)}if(!l.ok){const f=l.data?.error||l.text.slice(0,200)||`HTTP ${l.status}`;throw new i(`The server refused the invite (${l.status}): ${f}`)}const h=l.data||{};if(!h.token||!h.keyBlob)throw new i("The server accepted the invite but sent no sealed key, so nothing could be set up. Ask for a new link.");if(u?.orgId&&h.orgId&&h.orgId!==u.orgId)throw Q(u,{orgName:h.orgName??null,spent:!0});const M=await ye(h.keyBlob,v,r),F={serverUrl:C(c),orgId:h.orgId,orgName:h.orgName??null,projectId:h.projectId??null,projectName:h.projectName??null,projectUsesOrgKey:h.projectUsesOrgKey??null,canEnrol:h.canEnrol===!0,token:h.token,tokenIssuedAt:Date.now(),key:M.toString("hex"),keyFingerprint:re("sha256").update(M).digest("hex").slice(0,8),connectedAt:new Date().toISOString()};return G(F),F}function te(e){try{const t=JSON.parse(Buffer.from(String(e).split(".")[1],"base64url").toString("utf8")).exp;return typeof t=="number"&&t*1e3>Date.now()+6e4}catch{return!1}}const ne=e=>e.status===401||e.status===403;async function Pe(e=j(),{timeoutMs:t}={}){if(!e)throw new i("Not connected. Run: redaktyn-connect '<invite link>'");if(Date.now()-(e.tokenIssuedAt||0)<Y)return e;const n=await g(e.serverUrl,"POST","/api/auth/refresh",{token:e.token,...t>0?{timeoutMs:t}:{}});if(!n.ok||!n.data?.token){if(!ne(n)){if(te(e.token))return e;throw new i(`The server could not renew this machine's session just now (HTTP ${n.status}). Try again in a few minutes; you do not need a new invite link.`)}const r=n.data?.code,a=r==="ENROLLMENT_REVOKED"||r==="SESSION_REVOKED"?"This device was removed by an admin.":"The session could not be renewed (it may have passed its maximum age).";throw new i(`${a} Ask for a new invite link and run redaktyn-connect again.`)}const o={...e,token:n.data.token,tokenIssuedAt:Date.now()};return G(o),o}async function Le(e,t,{serverUrl:n}={}){const o=V(n),r=await g(o,"POST","/api/auth/login",{body:{email:e,password:t}});if(r.status===429)throw new i("Too many attempts for this account. Wait a few minutes and try again.");if(r.ok&&r.data?.secondStep===!0)throw new i("This account has two-step sign-in. Run redaktyn-login without a password: it signs in through your browser, where the dashboard asks for the code.");if(!r.ok||!r.data?.token)throw new i(r.status===401?"Wrong email or password.":`Login failed (${r.status}): ${r.data?.error||r.text.slice(0,160)}`);const a=r.data.user?.role;if(a!=="OWNER"&&a!=="ADMIN")throw new i(`This account is a ${a??"non-admin"}. Bulk import and invites need an OWNER or ADMIN login.`);const s={serverUrl:o,token:r.data.token,tokenIssuedAt:Date.now(),loggedInAt:new Date().toISOString(),email:r.data.user.email,role:a,orgId:r.data.org?.id??null,orgName:r.data.org?.name??null};return B(s),s}async function je(e=he()){if(!e)throw new i("Not logged in. Run: redaktyn-login");if(Date.now()-(e.tokenIssuedAt||0)<Y)return e;const t=await g(e.serverUrl,"POST","/api/auth/refresh",{token:e.token});if(!t.ok||!t.data?.token){if(ne(t))throw new i("Your login has expired. Run: redaktyn-login");if(te(e.token))return e;throw new i(`The server could not renew your login just now (HTTP ${t.status}). Try again in a few minutes.`)}const n={...e,token:t.data.token,tokenIssuedAt:Date.now()};return B(n),n}function Ue(e){return new Promise(t=>{const n=process.stdin;if(!n.isTTY||typeof n.setRawMode!="function"){let s="";n.setEncoding("utf8"),n.on("data",d=>s+=d),n.on("end",()=>t(s.split(/\r?\n/)[0]??""));return}process.stderr.write(e);let o="";n.setRawMode(!0),n.resume(),n.setEncoding("utf8");const r=()=>{n.setRawMode(!1),n.pause(),n.off("data",a),process.stderr.write(`
|
|
6
|
+
`),t(o)},a=s=>{for(const d of s){if(d==="\r"||d===`
|
|
7
|
+
`||d==="")return r();d===""&&(n.setRawMode(!1),process.stderr.write(`
|
|
8
|
+
`),process.exit(130)),d==="\x7F"||d==="\b"?o=o.slice(0,-1):o+=d}};n.on("data",a)})}async function Be({serverUrl:e,onCode:t,sleep:n=o=>new Promise(r=>setTimeout(r,o))}={}){const o=V(e),r=await g(o,"POST","/api/cli-login/start",{body:{clientName:de()}});if(r.status===429)throw new i("Too many sign-in requests. Wait a few minutes and try again.");if(!r.ok||!r.data?.deviceCode)throw new i(`Could not start a browser sign-in (${r.status}). This server may be older; use: redaktyn-login --password`);const{deviceCode:a,userCode:s,verificationUrl:d,expiresIn:w}=r.data;let k=(r.data.interval||2)*1e3;const v=Date.now()+(w||600)*1e3;t&&t({userCode:s,verificationUrl:d,expiresIn:w});let m=0;for(;Date.now()<v;){await n(k);let c;try{c=await g(o,"POST","/api/cli-login/poll",{body:{deviceCode:a}}),m=0}catch(l){if(++m>=5)throw l;continue}if(c.status===429){k=((c.data?.interval||5)+1)*1e3;continue}if(c.status===403)throw new i("The sign-in was denied in the browser.");if(c.status===410)throw new i("The sign-in request expired. Run the command again.");if(!c.ok)throw new i(`Sign-in failed (${c.status}): ${c.data?.error||c.text.slice(0,160)}`);if(c.data?.status!=="approved")continue;const u=c.data.user?.role;if(u!=="OWNER"&&u!=="ADMIN")throw new i(`That account is a ${u??"non-admin"}. Bulk import and invites need an OWNER or ADMIN.`);const p={serverUrl:o,token:c.data.token,tokenIssuedAt:Date.now(),loggedInAt:new Date().toISOString(),email:c.data.user.email,role:u,orgId:c.data.org?.id??null,orgName:c.data.org?.name??null};return B(p),p}throw new i("Timed out waiting for approval. Run the command again.")}export{ue as DEFAULT_SERVER_URL,P as INVITE_KEY_BYTES,i as SessionError,Y as TOKEN_REFRESH_AFTER_MS,g as apiJson,pe as bytesToB64u,ee as cleanUserName,_e as connectWithInvite,De as deleteAdminSession,be as deleteSession,we as deviceId,je as freshAdminSession,Pe as freshSession,ge as inviteCodeFitsLink,Ne as inviteInfo,$e as linkNeedsInviteCode,he as loadAdminSession,j as loadSession,Be as loginWithBrowser,Le as loginWithPassword,Oe as oprfPinStore,$ as parseInviteLink,Ue as readHidden,V as resolveServerUrl,B as saveAdminSession,G as saveSession,Ce as sealInviteKey,T as sessionDir,ye as unsealInviteKey};
|