@redaktyn/cli 1.6.5 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/redaktyn-clipboard.mjs +1 -1
- package/redaktyn-invite.mjs +8 -8
- package/redaktyn-login.mjs +7 -7
- package/redaktyn-places.mjs +1 -1
- package/redaktyn-scan.mjs +13 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redaktyn/cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.7",
|
|
4
4
|
"description": "Redaktyn CLI: bulk-enrol secrets from a .env or any vault (--project aware), local fingerprints, git/directory digest scans. Digests are computed on your machine; secret values never leave it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/redaktyn-clipboard.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawnSync as
|
|
1
|
+
import{spawnSync as h}from"node:child_process";import{accessSync as y,constants as w,existsSync as f,realpathSync as g,statSync as b}from"node:fs";import{homedir as x}from"node:os";import e from"node:path";const l=o=>{try{return g(o)}catch{return null}},d=(o,n)=>{const s=e.relative(n,o);return s===""||!s.startsWith("..")&&!e.isAbsolute(s)};function S(o){const n=[],s=l(o);s&&n.push(s);for(let t=e.resolve(o);;){if(f(e.join(t,".git"))){const c=l(t);c&&n.push(c);break}const r=e.dirname(t);if(r===t)break;t=r}const i=l(x());return n.filter(t=>t!==e.parse(t).root&&t!==i)}function m(o,{path:n=process.env.PATH??"",cwd:s=process.cwd(),platform:i=process.platform}={}){if(i==="win32"){const r=e.win32.join(process.env.SystemRoot||process.env.windir||"C:\\Windows","System32",/\.exe$/i.test(o)?o:`${o}.exe`);return f(r)?r:null}const t=S(s);for(const r of String(n).split(e.delimiter)){if(!r||!e.isAbsolute(r))continue;const c=l(r);if(c===null||c.split(e.sep).includes("node_modules")||r.split(e.sep).includes("node_modules")||t.some(a=>d(c,a)))continue;const u=e.join(r,o);try{if(!b(u).isFile())continue;y(u,w.X_OK)}catch{continue}const p=l(u);if(!(p===null||p.split(e.sep).includes("node_modules")||t.some(a=>d(p,a))))return u}return null}function v(){return process.platform==="darwin"?[["pbcopy",[]]]:process.platform==="win32"?[["clip",[]]]:[["wl-copy",[]],["xclip",["-selection","clipboard"]],["xsel",["--clipboard","--input"]]]}function A(o){for(const[n,s]of v()){const i=m(n);if(i===null)continue;const t=h(i,s,{input:o,stdio:["pipe","ignore","ignore"],timeout:4e3});if(!t.error&&t.status===0)return{ok:!0,tool:n}}return{ok:!1}}function C(o,{platform:n=process.platform,resolve:s=m}={}){let i;try{i=new URL(String(o))}catch{return null}if(i.protocol!=="https:"&&i.protocol!=="http:")return null;const r=s(n==="darwin"?"open":n==="win32"?"rundll32":"xdg-open");return r?[r,n==="win32"?["url.dll,FileProtocolHandler",i.href]:[i.href]]:null}export{C as browserCommand,A as copyToClipboard,m as trustedTool};
|
package/redaktyn-invite.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{pathToFileURL as
|
|
2
|
+
import{pathToFileURL as z}from"node:url";import{existsSync as P,realpathSync as M,readFileSync as X,statSync as J,lstatSync as V,unlinkSync as Q,openSync as Z,writeSync as ee,closeSync as te,constants as R}from"node:fs";import{randomBytes as ne}from"node:crypto";import{createInterface as oe}from"node:readline";import{deriveProjectKey as U}from"./redaktyn-fingerprint.mjs";import{SessionError as l,apiJson as I,freshAdminSession as se,loadAdminSession as re,resolveServerUrl as ie,sealInviteKey as _}from"./redaktyn-session.mjs";import{askWorkspaceKey as W}from"./redaktyn-passphrase.mjs";import{copyToClipboard as ae}from"./redaktyn-clipboard.mjs";import{banner as le,box as ce,k as d,rich as D,rows as de,say as w,step as C}from"./redaktyn-ui.mjs";import{createRequire as pe}from"node:module";const F=pe(import.meta.url),he=F("./package.json").version;function ue(){console.log(`redaktyn-invite: mint a single-use invite link for one project
|
|
3
3
|
|
|
4
4
|
--project <name|id> The project the link is for (required)
|
|
5
5
|
--can-enrol Let the redeeming CLI register secrets on that project
|
|
@@ -16,10 +16,10 @@ import{pathToFileURL as H}from"node:url";import{existsSync as D,realpathSync as
|
|
|
16
16
|
--app-url <url> Address to put in the link (default: the server you are signed in to)
|
|
17
17
|
--server <url> API address (default https://redaktyn.com; self-hosted only)
|
|
18
18
|
|
|
19
|
-
Needs: redaktyn-login first (or REDAKTYN_TOKEN), and the workspace passphrase (REDAKTYN_PASSPHRASE or a prompt)`)}function
|
|
20
|
-
Read ${s.rows.length} row${s.rows.length===1?"":"s"} from ${
|
|
21
|
-
Dry run: nothing was created. Re-run with --apply to make ${s.people.length} link${s.people.length===1?"":"s"} and write them to ${
|
|
22
|
-
`,
|
|
23
|
-
Links written to ${y} (readable only by you). They are not printed here: each is a credential that works once.`),console.log("Send each person their own link over a channel you trust, then delete the file.");for(const
|
|
24
|
-
`),console.log(
|
|
25
|
-
Send it over a channel you trust: whoever opens it first gets this project's key.`),console.log(`They run: redaktyn-connect and paste it. Revoke before use: DELETE /api/invites/${
|
|
19
|
+
Needs: redaktyn-login first (or REDAKTYN_TOKEN), and the workspace passphrase (REDAKTYN_PASSPHRASE or a prompt)`)}function fe(t){const e={project:null,canEnrol:!1,days:7,name:null,email:null,appUrl:null,server:null,help:!1,copy:null,csv:null,apply:!1,out:null,force:!1};for(let r=2;r<t.length;r++){const n=t[r],i=()=>{const a=t[++r];if(a===void 0||a.startsWith("--"))throw new l(`${n} needs a value`);return a};if(n==="--help"||n==="-h")e.help=!0;else if(n==="--project")e.project=i();else if(n==="--can-enrol"||n==="--can-enroll")e.canEnrol=!0;else if(n==="--expires-days")e.days=Number(i());else if(n==="--name")e.name=i();else if(n==="--email")e.email=i();else if(n==="--copy")e.copy=!0;else if(n==="--no-copy")e.copy=!1;else if(n==="--csv")e.csv=i();else if(n==="--apply")e.apply=!0;else if(n==="--out")e.out=i();else if(n==="--force")e.force=!0;else if(n==="--app-url")e.appUrl=i();else if(n==="--server")e.server=i();else throw new l(`Unknown flag: ${n}`)}if(!Number.isInteger(e.days)||e.days<1||e.days>365)throw new l("--expires-days must be 1 to 365");if(e.csv&&(e.name||e.email||e.copy!==null))throw new l("--csv takes the names and emails from the file: drop --name, --email and --copy");if(!e.csv&&(e.apply||e.out||e.force))throw new l("--apply, --out and --force go with --csv");return e}function me(t){return new Promise(e=>{const r=oe({input:process.stdin,output:process.stderr});r.question(t,n=>{r.close(),e(!/^\s*n/i.test(n))})})}async function Y(t,e){let r=e;if(r===null&&(r=process.stdin.isTTY&&D?await me(` Copy the link to your clipboard? ${d.dim("[Y/n]")} `):!1),!r)return;const n=ae(t),i=n.ok?"Copied. Paste it where you are sending it; it works once.":"Could not copy (no clipboard tool found). The link is printed above.";D?w(` ${n.ok?d.green("\u2714"):d.amber("\u25B2")} ${i}`):console.log(i)}function ye(t){try{return JSON.parse(Buffer.from(t.split(".")[1],"base64url").toString()).orgId}catch{return null}}function B(){try{return F("./redaktyn-shared.cjs")}catch{throw new l("Cannot load the shared code. From a checkout, run `npm install` then `npm run build -w @redaktyn/shared`; otherwise reinstall the CLI.")}}const ge=t=>String(t).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"project";async function ve({o:t,server:e,token:r,orgId:n,who:i,project:a}){const c=B(),g=c.checkRosterFile({name:t.csv,size:P(t.csv)?J(t.csv).size:0});if(!P(t.csv))throw new l(`No such file: ${t.csv}`);if(!g.ok)throw new l(g.reason);const S=X(t.csv,"utf8"),u=await I(e,"GET","/api/invites",{token:r}),m=Date.now(),T=new Set((Array.isArray(u.data)?u.data:[]).filter(o=>o.projectId===a.id&&o.assignedEmail&&o.currentUses<o.maxUses&&(!o.expiresAt||new Date(o.expiresAt).getTime()>m)).map(o=>String(o.assignedEmail).toLowerCase())),s=c.parseRoster(S,{existingEmails:T}),k=s.rows.filter(o=>o.issues.length>0);console.log(`
|
|
20
|
+
Read ${s.rows.length} row${s.rows.length===1?"":"s"} from ${t.csv}${s.hadHeader?" (column titles found)":""}: ${s.people.length} to invite, ${k.length} set aside.`);for(const o of k.slice(0,20))console.log(` row ${o.line}: ${o.email||"(no address)"} ${o.issues.map(v=>c.ISSUE_LABEL[v]).join(", ")}`);k.length>20&&console.log(` and ${k.length-20} more`),s.skipped>0&&console.log(` ${s.skipped} rows past the first ${c.ROSTER_MAX_ROWS} were not read: invite in smaller batches.`);const j=s.rows.filter(o=>o.issues.length===0&&o.nameDerived).length;if(j>0&&console.log(` ${j} name${j===1?" was":"s were"} made from the email address because the list had none. It is locked into that person's link.`),s.people.length===0)throw new l("Nothing to invite in that file.");const f=await I(e,"GET","/api/plans/current",{token:r});if(f.ok&&f.data?.invites&&Number.isFinite(f.data.invites.max)&&f.data.invites.used+s.people.length>f.data.invites.max)throw new l(`That would make ${f.data.invites.used+s.people.length} unused invite links, but your plan allows ${f.data.invites.max}. Revoke unused links, move up a size, or invite fewer people.`);const b=t.out||`redaktyn-invites-${ge(a.name)}.csv`;if(H(b)&&!t.force)throw new l(`${b} already exists. Choose another --out, or add --force to overwrite it.`);if(!t.apply)return console.log(`
|
|
21
|
+
Dry run: nothing was created. Re-run with --apply to make ${s.people.length} link${s.people.length===1?"":"s"} and write them to ${b}.`),0;const{orgKey:N}=await W({server:e,token:r,orgId:n,who:i}),A=C("Deriving the project key on this machine"),L=a.usesOrgKey?N:U(N,a.id);A.succeed(`Project key derived here ${d.dim("(PBKDF2-SHA256, 600,000 rounds)")}`);const $=C(`Creating ${s.people.length} link${s.people.length===1?"":"s"}`),h=[];let x=null;for(const o of s.people){if(x){h.push({...o,link:null,error:x});continue}try{const{w:v,code:G,blob:q}=await _(L,a.usesOrgKey?1:2),O=await I(e,"POST","/api/invites",{token:r,body:{projectId:a.id,keyBlob:q,maxUses:1,expiresInDays:t.days,assignedName:o.name,assignedEmail:o.email,...t.canEnrol?{canEnrol:!0}:{}}});if(!O.ok){const K=O.data?.error||`HTTP ${O.status}`;h.push({...o,link:null,error:K}),(O.status===401||O.status===403)&&(x=`not attempted: ${K}`);continue}h.push({...o,link:`${t.appUrl.replace(/\/+$/,"")}/connect?code=${O.data.code}#w2=${v}`,inviteCode:G,error:null})}catch(v){h.push({...o,link:null,error:v instanceof Error?v.message:"failed"})}}const p=h.filter(o=>o.link).length,E=h.length-p;E===0?$.succeed(`${p} link${p===1?"":"s"} created ${d.dim("(the server stored ciphertext it cannot open)")}`):$.fail(`${p} of ${h.length} links created`);const y=we(b,c.inviteLinksCsv(h)+`
|
|
22
|
+
`,t.force===!0);console.log(`
|
|
23
|
+
Links written to ${y} (readable only by you). They are not printed here: each is a credential that works once.`),console.log("Send each person their own link over a channel you trust, then delete the file.");for(const o of h.filter(v=>!v.link).slice(0,20))console.log(` not created for ${o.email}: ${o.error}`);return E===0?0:1}function H(t){try{return V(t),!0}catch{return!1}}function we(t,e,r){const n=R.O_WRONLY|R.O_CREAT|R.O_EXCL|(R.O_NOFOLLOW??0),i=c=>{const g=Z(c,n,384);try{ee(g,e)}finally{te(g)}return c};if(r&&H(t))try{Q(t)}catch{}try{return i(t)}catch(c){if(c?.code!=="EEXIST"&&c?.code!=="ELOOP")throw c}const a=t.replace(/(\.csv)?$/i,`-${ne(4).toString("hex")}.csv`);return i(a)}async function ke(t){const e=fe(t);if(e.help)return ue(),0;if(!e.project)throw new l("Say which project the link is for: --project <name|id>");const r=process.env.REDAKTYN_TOKEN,n=r?null:await se(re()),i=e.server||(n&&!r?n.serverUrl:null)||ie(),a=r||n.token;e.appUrl||(e.appUrl=i);const c=ye(a);if(!c)throw new l("That token carries no org id.");const g=n?`Signed in as ${n.email}${n.orgName?` (${n.orgName})`:""}.`:null;le("invite link",he);const S=C("Looking up the project"),u=await I(i,"GET","/api/projects",{token:a});if(!u.ok||!Array.isArray(u.data))throw S.fail("Could not read your projects"),new l(`GET /api/projects failed (${u.status}). Is the token an OWNER/ADMIN token?`);const m=e.project.trim(),T=u.data.filter(y=>y.id===m).length?u.data.filter(y=>y.id===m):u.data.filter(y=>String(y.name).toLowerCase()===m.toLowerCase());if(T.length!==1)throw S.fail(T.length?`"${m}" matches several projects`:`No project "${m}"`),new l(T.length?`"${m}" matches several projects; pass the id.`:`No project "${m}". Known: ${u.data.map(y=>y.name).join(", ")}.`);const s=T[0];if(s.status==="CLOSED")throw S.fail("That project is closed"),new l(`Project ${s.name} is closed.`);if(S.succeed(`Project ${s.name}`),e.csv)return ve({o:e,server:i,token:a,orgId:c,who:g,project:s});const{SECURITY_CODE_WHERE_TO_TYPE:k,inviteShareText:j}=B(),{orgKey:f}=await W({server:i,token:a,orgId:c,who:g}),b=C("Deriving the project key on this machine"),N=s.usesOrgKey?f:U(f,s.id);b.succeed(`Project key derived here ${d.dim("(PBKDF2-SHA256, 600,000 rounds)")}`);const A=C("Sealing the key into the link"),{w:L,code:$,blob:h}=await _(N,s.usesOrgKey?1:2);A.succeed(`Key sealed ${d.dim("(AES-GCM; the opener lives only in the link's #fragment)")}`);const x=C("Registering the link with the server"),p=await I(i,"POST","/api/invites",{token:a,body:{projectId:s.id,keyBlob:h,maxUses:1,expiresInDays:e.days,...e.canEnrol?{canEnrol:!0}:{},...e.name?{assignedName:e.name}:{},...e.email?{assignedEmail:e.email}:{}}});if(!p.ok)throw x.fail("The server refused the invite"),new l(`The server refused the invite (${p.status}): ${p.data?.error||p.text.slice(0,200)}`);x.succeed(`Registered ${d.dim("(the server stored ciphertext it cannot open)")}`);const E=`${e.appUrl.replace(/\/+$/,"")}/connect?code=${p.data.code}#w2=${L}`;return D?(w(""),ce("Invite ready",["",...de([["Project",s.name],["Access",e.canEnrol?d.amber("may enrol secrets on this project only"):"scan only"],["Lifetime",`single use ${d.dim("\xB7")} expires in ${e.days} day${e.days===1?"":"s"}`],["Opens with","the part after # in the link. It never reaches the server"],["Revoke",`DELETE /api/invites/${p.data.id}`]],{indent:0}),""],{tone:"green",width:78}),w(""),w(` ${d.bold("Send this link over a channel you trust.")} ${d.dim("Whoever opens it first gets this project's key.")}`),w(` ${d.dim("They run")} ${d.bold("redaktyn-connect")} ${d.dim("and paste it.")}`),w(""),console.log(E),console.log(`Security code: ${$}`),w(` ${d.dim(k)}`),w(""),await Y(j(E,$),e.copy),0):(console.log(`Invite for project ${s.name}${e.canEnrol?" (may enrol secrets)":" (scan only)"}, single use, expires in ${e.days} day(s):
|
|
24
|
+
`),console.log(E),console.log(`Security code: ${$}`),console.log(k),console.log(`
|
|
25
|
+
Send it over a channel you trust: whoever opens it first gets this project's key.`),console.log(`They run: redaktyn-connect and paste it. Revoke before use: DELETE /api/invites/${p.data.id}`),await Y(j(E,$),e.copy),0)}function $e(){try{return M(process.argv[1])}catch{return process.argv[1]}}process.argv[1]&&import.meta.url===z($e()).href&&ke(process.argv).then(t=>process.exit(t),t=>{console.error(t.message),process.exit(t instanceof l?1:2)});export{ke as main};
|
package/redaktyn-login.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{pathToFileURL as N}from"node:url";import{realpathSync as
|
|
2
|
+
import{pathToFileURL as N}from"node:url";import{realpathSync as A}from"node:fs";import{spawn as R}from"node:child_process";import{createRequire as T}from"node:module";import{banner as x,box as y,k as n,rich as u,rows as P,say as a,step as I}from"./redaktyn-ui.mjs";import{browserCommand as O}from"./redaktyn-clipboard.mjs";const W=T(import.meta.url)("./package.json").version;import*as E from"node:readline";import{SessionError as v,apiJson as j,deleteAdminSession as C,loadAdminSession as k,loginWithBrowser as D,loginWithPassword as U,readHidden as Y,resolveServerUrl as B,sessionDir as $}from"./redaktyn-session.mjs";function L(r){if(process.env.REDAKTYN_NO_BROWSER)return!1;try{const o=O(r);if(o===null)return!1;const i=R(o[0],o[1],{stdio:"ignore",detached:!0});return i.on("error",()=>{}),i.unref(),!0}catch{return!1}}function M(){console.log(`redaktyn-login: sign in as a workspace admin
|
|
3
3
|
|
|
4
4
|
redaktyn-login open the browser and approve the code there (nothing typed here)
|
|
5
5
|
redaktyn-login --no-browser print the link instead of opening it (SSH, containers)
|
|
@@ -9,10 +9,10 @@ import{pathToFileURL as N}from"node:url";import{realpathSync as x}from"node:fs";
|
|
|
9
9
|
redaktyn-login --status show who this machine is logged in as
|
|
10
10
|
redaktyn-login --logout forget the login on this machine
|
|
11
11
|
|
|
12
|
-
--server <url> API address (default ${
|
|
12
|
+
--server <url> API address (default ${B()}; only a self-hosted install changes it)
|
|
13
13
|
|
|
14
|
-
The login is stored in ${$()} (readable only by you). Your password is never saved.`)}function
|
|
15
|
-
${
|
|
16
|
-
`),console.error(`Check that it shows the code ${h} and approve it. Waiting\u2026 (Ctrl+C to cancel)`)}});return s&&clearInterval(s),e&&e.succeed(`Approved by ${t.email}`),
|
|
17
|
-
Next: redaktyn-import --env .env --project <name> (add --apply to register)`),0}catch(t){if(s&&clearInterval(s),e&&e.fail(t instanceof Error?t.message:String(t)),t instanceof v)return e||console.error(t.message),1;throw t}}let c=i("--email");if(!c){if(!process.stdin.isTTY)return console.error("Pass --email, or run this in a terminal."),2;c=await
|
|
18
|
-
Next: redaktyn-import --env .env --project <name> (add --apply to register)`),0}catch(e){if(e instanceof v)return console.error(e.message),
|
|
14
|
+
The login is stored in ${$()} (readable only by you). Your password is never saved.`)}function q(r){return new Promise(o=>{const i=E.createInterface({input:process.stdin,output:process.stderr});i.question(r,l=>{i.close(),o(l.trim())})})}function m(r){if(u){a(""),y("Signed in",[...P([["Account",`${r.email} ${n.dim(`(${r.role})`)}`],["Workspace",r.orgName??r.orgId??"your workspace"],["Server",r.serverUrl],["Session",`${n.dim("kept in")} ${$()}${n.dim("/admin.json, readable only by you")}`],["Password",n.green("never typed here, never stored")]],{indent:0})],{tone:"green"});return}console.log(`Logged in as ${r.email} (${r.role}) of ${r.orgName??r.orgId??"your workspace"}.`),console.log(` server ${r.serverUrl}`)}async function F(r){const o=r.slice(2);if(o.includes("--help")||o.includes("-h"))return M(),0;if(o.includes("--logout")){const e=k();let s=!1;if(e)try{const t=await j(e.serverUrl,"POST","/api/auth/logout",{token:e.token});s=t.ok&&t.data?.ended===!0}catch{}return C(),console.log(e?s?"Logged out: this machine's Redaktyn admin login is forgotten and ended on the server.":"Forgot this machine's Redaktyn admin login. The server could not be told, so its token runs until it expires (within 24 hours).":"There was no admin login to forget."),0}if(o.includes("--status")){const e=k();return e?(m(e),0):(console.log("Not logged in. Run: redaktyn-login"),1)}const i=e=>{const s=o.indexOf(e);return s>=0?o[s+1]:void 0},l=i("--server"),d=o.includes("--password-stdin");if(!(d||o.includes("--password")||i("--email")!==void 0)){let e=null,s=null;try{x("sign in",W);const t=await D({serverUrl:l,onCode:({userCode:h,verificationUrl:g,expiresIn:b})=>{const f=!o.includes("--no-browser")&&L(g);if(u){y("Approve in your browser",["",` ${n.gray("Your code")} ${n.bold(n.clay(h))}`,"",` ${f?"A browser tab is opening on this link.":"Open this link in a browser where you are signed in:"}`,` ${n.blue(g)}`,"",` ${n.dim("Check the page shows the same code, then press Approve.")}`,` ${n.dim("Nothing is typed in this terminal, and no password is sent from it.")}`,""],{tone:"clay",width:70}),a("");const S=Date.now()+(b||600)*1e3;e=I("Waiting for you to approve in the browser"),s=setInterval(()=>{const w=Math.max(0,Math.round((S-Date.now())/1e3));e.update(`Waiting for you to approve in the browser ${n.dim(`${Math.floor(w/60)}:${String(w%60).padStart(2,"0")} left`)}`)},1e3);return}console.error(f?"Opening your browser to sign in.":"Open this page in a browser where you are signed in:"),console.error(`
|
|
15
|
+
${g}
|
|
16
|
+
`),console.error(`Check that it shows the code ${h} and approve it. Waiting\u2026 (Ctrl+C to cancel)`)}});return s&&clearInterval(s),e&&e.succeed(`Approved by ${t.email}`),u||console.log(""),m(t),u?(a(""),a(` ${n.dim("Next")} ${n.bold("redaktyn-import --env .env --project <name>")} ${n.dim("(add --apply to register)")}`),a("")):console.log(`
|
|
17
|
+
Next: redaktyn-import --env .env --project <name> (add --apply to register)`),0}catch(t){if(s&&clearInterval(s),e&&e.fail(t instanceof Error?t.message:String(t)),t instanceof v)return e||console.error(t.message),1;throw t}}let c=i("--email");if(!c){if(!process.stdin.isTTY)return console.error("Pass --email, or run this in a terminal."),2;c=await q("Email: ")}if(!c)return console.error("No email given."),2;if(!d&&!process.stdin.isTTY)return console.error("Run this in a terminal, or use --password-stdin."),2;const p=await Y(d?"":"Password: ");if(!p.trim())return console.error("No password given."),2;try{const e=await U(c,p,{serverUrl:l});return m(e),console.log(`
|
|
18
|
+
Next: redaktyn-import --env .env --project <name> (add --apply to register)`),0}catch(e){if(e instanceof v)return console.error(e.message),d&&p!==p.trim()&&console.error(`The password read from standard input starts or ends with a space. If that is not part of it, send it with: printf '%s' "$PASSWORD"`),1;throw e}}function H(){try{return A(process.argv[1])}catch{return process.argv[1]}}process.argv[1]&&import.meta.url===N(H()).href&&F(process.argv).then(r=>process.exit(r),r=>{console.error(r.message),process.exit(1)});export{F as main};
|
package/redaktyn-places.mjs
CHANGED
|
@@ -3,4 +3,4 @@ import{k as i}from"./redaktyn-ui.mjs";function k(s){if(!(s.startsWith('"')&&s.en
|
|
|
3
3
|
`);t>=0;t=s.indexOf(`
|
|
4
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
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};
|
|
6
|
+
`,p[u]);W=d.text.slice(p[u],w<0?void 0:w),S=h-p[u]}const M=`${d.file}\0${a}`,v=e.get(M)??{file:d.file,line:a,text:W,strikes:[]};if(!v.strikes.some(u=>u.start===S&&u.end===S+l.window.length))for(const u of l.ids)v.strikes.push({start:S,end:S+l.window.length,secretId:u});e.set(M,v),r.add(b);for(const u of l.ids)c.add(u)}}}return{rows:[...e.values()],placedIds:c,wanted:n,placedWindows:r}}const F=/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g,I=s=>String(s).replace(/\t/g," ").replace(F,"\xB7");function H(s){const o=[...s].sort((n,e)=>n.start-e.start||e.end-n.end),t=[];for(const n of o){const e=t[t.length-1];e&&n.start<=e.end?(e.end=Math.max(e.end,n.end),e.ids.add(n.secretId)):t.push({start:n.start,end:n.end,ids:new Set([n.secretId])})}return t}function O(s){const o=H(s.strikes);let t="",n=0;for(const e of o)t+=s.text.slice(n,e.start)+"\u2022".repeat(e.end-e.start),n=e.end;return t+=s.text.slice(n),{text:t,parts:o}}function N(s,o,t){for(const{window:n}of o.values())for(const e of t){let r;try{r=e(s)}catch{return!0}if(r.includes(n))return!0}return!1}function A(s,o,t){return s.every(n=>!N(O(n).text,o,t))}const P=100;function j(s,o){const{text:t,parts:n}=O(s),e=n[0],r=Math.max(P,e.end-e.start+40);let c=0;t.length>r&&(c=Math.max(0,Math.min(e.start-40,t.length-r)));const f=Math.min(t.length,c+r),d=c>0?"\u2026":"",m=f<t.length?"\u2026":"",g=d+I(t.slice(c,f))+m;let p="",b=0;const l=n.filter($=>$.end>c&&$.start<f);for(const $ of l){const h=d.length+Math.max($.start,c)-c,a=d.length+Math.min($.end,f)-c;p+=" ".repeat(h-b)+"^".repeat(a-h),b=a}const x=[];for(const $ of l)for(const h of $.ids){const a=o(h);a&&!x.includes(a)&&x.push(a)}return{code:g,carets:p,labels:x,column:e.start+1}}function D({kind:s,rows:o,show:t,matches:n,unplacedIds:e,files:r,windows:c,passes:f,sent:d,requests:m,maxBlocks:g=5,shortId:p=b=>String(b).slice(0,8)}){const b=u=>I(n.find(w=>w.secretId===u)?.label??""),l=[],x=s==="staged"?"a registered secret is staged":s==="pushing"?"a registered secret is in a commit being pushed":"a registered secret is in this content";l.push(`${i.red(i.bold("error"))}${i.bold(`: ${x}`)}`);const $=Math.max(1,...o.slice(0,g).map(u=>String(u.line).length)),h=" ".repeat($),a=i.blue(`${h} \u2502`),W=o.slice(0,g);for(const u of W){const w=j(u,b);l.push(`${i.blue(`${h} \u250C\u2500`)} ${I(u.file)}:${u.line}:${w.column}`),t?(l.push(a),l.push(`${i.blue(`${String(u.line).padStart($)} \u2502`)} ${w.code}`),l.push(`${a} ${i.red(w.carets)} ${i.red(w.labels.join(", "))}`.replace(/\s+$/,""))):l.push(`${a} ${i.red(w.labels.join(", "))}`.replace(/\s+$/,"")),l.push(a)}o.length>g&&l.push(`${a} ${i.dim(`\u2026 and ${o.length-g} more place${o.length-g===1?"":"s"}`)}`);for(const u of e)l.push(`${i.blue(`${h} =`)} ${i.bold("not found")} ${i.bold(b(u))} ${i.dim(p(u))} ${i.dim("(split, altered or over several lines, so no place can be shown)")}`);l.push(`${i.blue(`${h} =`)} ${i.bold("checked")} ${c.toLocaleString("en-US")} windows, ${f} pass${f===1?"":"es"}`),l.push(`${i.blue(`${h} =`)} ${i.bold("sent")} ${d}. Your code: 0 B`),l.push(`${i.green("help")}: move the value out of the code, into an environment variable`),l.push(`${i.green("help")}: if it was ever pushed or shared, rotate it, and treat the old value as public`);const S=n.length,M=`${S} secret${S===1?"":"s"}`,v=r>0?` in ${r} file${r===1?"":"s"}`:"";return l.push(""),l.push(s==="staged"?`${i.red(i.bold("error"))}${i.bold(`: commit aborted. ${M}${v}`)}`:s==="pushing"?`${i.red(i.bold("error"))}${i.bold(`: push aborted. ${M}${v}`)}`:`${i.red(i.bold("error"))}${i.bold(`: ${M} found${v}`)}`),l}function E(s,o,t=5){const n=o.filter(r=>r.strikes.some(c=>c.secretId===s)),e=n.slice(0,t).map(r=>{const c=Math.min(...r.strikes.filter(f=>f.secretId===s).map(f=>f.start))+1;return` ${I(r.file)}:${r.line}:${c}`});return n.length>t&&e.push(` \u2026 and ${n.length-t} more`),e}export{C as addedLinesWithPlaces,I as clean,T as locate,E as plainPlaces,D as renderFound,A as safeToDraw,N as stillReadable,k as unquoteGitPath};
|
package/redaktyn-scan.mjs
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{resolveServerUrl as
|
|
3
|
-
`)}
|
|
4
|
-
`)
|
|
2
|
+
import{resolveServerUrl as ke}from"./redaktyn-session.mjs";import{LOCK_HEADERS as Qe,explainLockError as Ze,lockNetwork as et}from"./redaktyn-lock-net.mjs";import{box as te,humanBytes as ne,k as M,rich as z,rows as tt,say as P}from"./redaktyn-ui.mjs";import{addedLinesWithPlaces as nt,locate as ot,plainPlaces as st,renderFound as rt,safeToDraw as it}from"./redaktyn-places.mjs";import{extractDecodedCandidates as ct,hasDecodableRun as at}from"./redaktyn-decoder.mjs";import{execFileSync as lt,spawnSync as dt}from"node:child_process";import{createRequire as ut}from"node:module";import{readFileSync as F,existsSync as oe,mkdirSync as ft,readdirSync as ht,realpathSync as pt,openSync as mt,readSync as gt,closeSync as kt,statSync as se,writeFileSync as ye,chmodSync as yt,unlinkSync as wt}from"node:fs";import w from"node:path";import{fileURLToPath as Et}from"node:url";const $t=ut(import.meta.url);function St(){try{return $t("./redaktyn-shared.cjs")}catch{console.error("Cannot load @redaktyn/shared. Run `npm install` from the repo root, then `npm run build -w @redaktyn/shared`."),process.exit(2)}}const{deriveHmacKeyFromPassphrase:we,isOrgKeyLock:xt,scopeOf:Rt,unlockOrgKey:Nt,hmacDigestHex:Ee,normalizeSecretPlaintextForMatch:Tt,normalizeSecretPlaintextCollapsed:_t,normalizeSecretPlaintextLower:vt,extractAddedLinesFromUnifiedDiff:j,deriveProjectKey:At,deriveScanPrefilterSeed:$e,scanPrefilterParams:Se,scanPrefilterUnits:xe,scanPrefilterRoller:Re,decodeScanPrefilter:bt,MIN_SECRET_LENGTH:Ne}=St(),re=5*1024*1024,B=`${re/1048576} MB`,Te=4*1024*1024,ie=20*1024*1024,J=["package-lock.json","npm-shrinkwrap.json","yarn.lock","pnpm-lock.yaml","bun.lock","Cargo.lock","Gemfile.lock","poetry.lock","Pipfile.lock","uv.lock","composer.lock","go.sum","Package.resolved","packages.lock.json","pubspec.lock","mix.lock","flake.lock","Podfile.lock","gradle.lockfile"],Ot=e=>J.includes(w.posix.basename(e.replace(/\\/g,"/"))),_e=2e4,ce=3e6,ve=5e6,ae=[{name:"exact",transform:Tt,lengthKey:"length",fingerprint:"exact"},{name:"whitespace-collapse",transform:_t,lengthKey:"lengthStripped",fingerprint:"collapsed"},{name:"case-fold",transform:vt,lengthKey:"lengthLower",fingerprint:"lower"}],Dt=new Set([".png",".jpg",".jpeg",".gif",".webp",".ico",".pdf",".zip",".gz",".exe",".dll",".so",".dylib",".woff",".woff2",".ttf",".eot",".mp4",".mp3",".wasm",".bin"]);function Pt(e){const n={exclude:[],_:[]};for(let o=2;o<e.length;o++){const s=e[o];s==="--staged"?n.staged=!0:s==="--pushing"?n.pushing=!0:s==="--message"?n.message=!0:s==="--remote"?n.remote=e[++o]:s==="--json"?n.json=!0:s==="--no-color"?n.noColor=!0:s==="--verbose"?n.verbose=!0:s==="--file"?n.file=e[++o]:s==="--dir"?n.dir=e[++o]:s==="--org-id"?n.orgId=e[++o]:s==="--server"?n.server=e[++o]:s==="--exclude"?n.exclude.push(e[++o]):s.startsWith("-")?(console.error(`Unknown flag: ${s}`),process.exit(1)):n._.push(s)}const t=n._[0];return{...n,cmd:t}}function It(e){try{const n=e.split(".")[1];if(!n)return null;const t=n+"=".repeat((4-n.length%4)%4),o=Buffer.from(t.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8");return JSON.parse(o)}catch{return null}}function Kt(e,n){const t=e.replace(/\\/g,"/");if(t.includes("node_modules/")||t.startsWith(".git/"))return!0;for(const o of n)if(o.includes("*")){if(new RegExp("^"+o.split("*").map(r=>r.replace(/[.+?^${}()|[\]\\]/g,"\\$&")).join(".*")+"$").test(t))return!0}else if(t.includes(o))return!0;return!1}function W(e){const n=w.relative(process.cwd(),w.resolve(e));return n===""?e:n.startsWith("..")?w.resolve(e):n}function Ct(e,n=8e3){let t;try{t=mt(e,"r");const o=Buffer.alloc(n);return o.subarray(0,gt(t,o,0,n,0))}catch{return null}finally{if(t!==void 0)try{kt(t)}catch{}}}function Ae(e){const n=Number(Ne)>0?Number(Ne):8;return e.toString("utf8").split(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\ufffd]+/).filter(t=>t.length>=n).join(`
|
|
3
|
+
`)}function be(e,n,t={unreadable:[],tooLarge:[],fromBinary:new Set}){const o=se(e);if(!o.isFile())return null;if(o.size>re){n&&console.warn(`Skipping large file (${o.size} bytes): ${e}`);const a=Ct(e);return a===null?t.unreadable.push(e):!Dt.has(w.extname(e).toLowerCase())&&!(a.includes(0)&&de(a)===null)&&t.tooLarge.push(e),null}let s;try{s=F(e)}catch{return t.unreadable.push(e),null}if(!s.includes(0))return s.toString("utf8");const r=de(s);if(r!==null)return r;const c=Ae(s);return c===""?null:(t.fromBinary.add(e),c)}function Oe(e,n,t=[],o=new Set,s={unreadable:[]}){let r;try{r=pt(e)}catch{return s.unreadable.push(e),t}if(o.has(r))return t;o.add(r);let c;try{c=ht(e)}catch{return s.unreadable.push(e),t}for(const a of c){const l=w.join(e,a),d=w.relative(process.cwd(),l);if(Kt(d,n))continue;let i;try{i=se(l)}catch(f){(f?.code==="EACCES"||f?.code==="EPERM")&&s.unreadable.push(l);continue}i.isDirectory()?Oe(l,n,t,o,s):t.push(l)}return t}function De(e,n){const t=new Map;for(const o of e){const s=Number(o[n.lengthKey]);if(!Number.isFinite(s)||s<=0)continue;const r=bt(o.scanPrefilter);t.has(s)||t.set(s,new Set);const c=t.get(s);c!==null&&(r?c.add(r[n.fingerprint]):t.set(s,null))}return t}function*Pe(e,n,t,o=Te){const s=e.length>o?e.slice(0,o):e;e.length>o&&(console.warn(`Content truncated to ${o} characters for scan.`),t.truncatedChars=!0);let r=[];const c=n.map(l=>{const d=new Map;for(const i of l.secrets)for(const f of[i.hmacHex,i.hmacHexStripped,i.hmacHexLower])typeof f=="string"&&d.set(f,i.id??i.secretId);return d});t.hits??=[];let a=null;for(const l of ae){let d=null,i=null;for(const[f,p]of n.entries()){const E=De(p.secrets,l);if(E.size===0)continue;d===null&&(d=l.transform(s),i=xe(d),t.passesRun.includes(l.name)||t.passesRun.push(l.name));const $=Se($e(p.key));for(const[g,A]of E){if(g>d.length)continue;const T=A?Re($,i,g):null;for(let N=0;N+g<=d.length;N++){if(t.windows++,T&&!A.has(T(N)))continue;const b=Ee(d.slice(N,N+g),p.key);r.push(b);const _=c[f].get(b);if(_!==void 0&&t.hits.length<500&&t.hits.push({secretId:_,pass:l.name,window:d.slice(N,N+g)}),t.total++,t.total>=ce){t.exhausted=!0,r.length>0&&(yield r);return}r.length>=_e&&(yield r,r=[])}}}l.name==="exact"&&(a=d)}if(a!==null&&at(a)){const l=n.map(i=>De(i.secrets,ae[0]));let d=0;for(const i of ct(a)){let f=0;for(const E of l)for(const $ of E.keys())f+=Math.max(0,i.decoded.length-$+1);if(f===0||i.bestEffort&&d+f>ve)continue;if(d+=f,d>ve){t.exhausted=!0,r.length>0&&(yield r);return}t.passesRun.includes("encoding")||t.passesRun.push("encoding");const p=xe(i.decoded);for(const[E,$]of n.entries()){const g=l[E];if(g.size===0)continue;const A=Se($e($.key));for(const[T,N]of g){if(T>i.decoded.length)continue;const b=N?Re(A,p,T):null;for(let _=0;_+T<=i.decoded.length;_++){if(t.windows++,b&&!N.has(b(_)))continue;const h=i.decoded.slice(_,_+T),x=Ee(h,$.key);r.push(x);const O=c[E].get(x);if(O!==void 0&&t.hits.length<500&&t.hits.push({secretId:O,pass:"encoding",window:h}),t.total++,t.total>=ce){t.exhausted=!0,r.length>0&&(yield r);return}r.length>=_e&&(yield r,r=[])}}}}}r.length>0&&(yield r)}async function jt({secrets:e,savedKey:n,orgKey:t,serverUrl:o,token:s,isAdmin:r}){if(n)return[{key:n,secrets:e}];const c=[...new Set(e.map(i=>i.projectId).filter(Boolean))];let a=null;if(c.length>0&&r)try{const i=await q(`${o.replace(/\/+$/,"")}/api/projects`,{headers:{Authorization:`Bearer ${s}`}}),f=i.ok?JSON.parse(i.text):null;Array.isArray(f)&&(a=new Map(f.map(p=>[p.id,p.usesOrgKey===!0])))}catch{a=null}const l={key:t,secrets:[]},d=new Map;for(const i of e){const f=i.projectId,p=f&&a?a.get(f):void 0;(!f||p===!0||p===void 0)&&l.secrets.push(i),f&&p!==!0&&(d.has(f)||d.set(f,{key:At(t,f),secrets:[]}),d.get(f).secrets.push(i))}return[l,...d.values()].filter(i=>i.secrets.length>0)}const Lt=6e4;function V(){const e=Number(process.env.REDAKTYN_REQUEST_TIMEOUT_MS);return Number.isFinite(e)&&e>=100?e:Lt}async function q(e,n={}){const t=V();try{const o=await fetch(e,{...n,signal:AbortSignal.timeout(t)}),s=await o.text();return{ok:o.ok,status:o.status,statusText:o.statusText,headers:o.headers,text:s}}catch(o){throw o?.name==="TimeoutError"||o?.name==="AbortError"?Object.assign(new Error(`No answer from ${new URL(e).origin} within ${Math.max(1,Math.round(t/1e3))} s.`),{unreachable:!0}):o}}async function Yt(e,n,t){const o=await q(e,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify(t)}),s=o.text;let r;try{r=s?JSON.parse(s):{}}catch{r={raw:s}}if(!o.ok){const c=new Error(r.error||o.statusText||"HTTP error");throw c.status=o.status,c.body=r,c}return r}async function Ie(e,n,t=!1){const o=`${e.replace(/\/+$/,"")}/api/secrets`,s=await q(o,{headers:{Authorization:`Bearer ${n}`}});if(t&&(s.status===401||s.status===403))throw new Error("This machine's Redaktyn session is no longer valid: the device was removed or its session ended. Ask your admin for a new invite link and run `redaktyn-connect` again.");if(!s.ok)throw new Error(`GET /api/secrets failed: ${s.status} ${s.text.slice(0,200)}`);try{return{secrets:JSON.parse(s.text),closed:s.headers.get("x-redaktyn-scope")==="closed"}}catch{throw new Error("GET /api/secrets: invalid JSON response")}}function Ke(){const e=["REDAKTYN_DEVICE_TOKEN","REDAKTYN_TOKEN","REDAKTYN_JWT"].find(s=>process.env[s]),n=e?process.env[e]:void 0,t=process.env.REDAKTYN_PASSPHRASE||void 0,o=n&&!t?`${e} is set without REDAKTYN_PASSPHRASE`:t&&!n?"REDAKTYN_PASSPHRASE is set without a token":null;return{token:n,passphrase:t,complete:!!(n&&t),partial:o}}function Ht(e){if(e?.unreachable===!0)return!0;const n=e?.cause?.code??e?.code;return["ECONNREFUSED","ENOTFOUND","EAI_AGAIN","ETIMEDOUT","ECONNRESET","EHOSTUNREACH","ENETUNREACH","UND_ERR_CONNECT_TIMEOUT"].includes(n)?!0:e?.name==="TypeError"&&/fetch failed/i.test(String(e?.message))}let I={serverUrl:null,staged:!1,hook:"commit"};const K=e=>String(e).replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g,"\xB7");function Ce(e){if(!Ht(e))return K(e?.message||e);const t=[`Could not reach the Redaktyn server${I.serverUrl?` at ${I.serverUrl.replace(/\/+$/,"")}`:""}, so nothing was checked. This is not a clean result.`];return I.staged&&t.push(I.hook==="push"?"To push without the check, at your own risk: git push --no-verify":"To commit without the check, at your own risk: git commit --no-verify"),t.join(`
|
|
4
|
+
`)}async function Ut(e,n,t,o){const s=e.replace(/\/+$/,""),r={Authorization:`Bearer ${n}`},c=await q(`${s}/api/org/key-lock`,{headers:{...r,...Qe}});if(c.status===404)return we(t,o);if(!c.ok)throw new Error(`GET /api/org/key-lock failed: ${c.status}`);const{lock:a}=JSON.parse(c.text);if(!a)return we(t,o);if(!xt(a))throw new Error("The server sent a key lock this CLI does not understand. Update it.");let l;try{l=await Nt(a,t,o,et({server:s,token:n,timeoutMs:V()},{scopeOf:Rt}))}catch(d){throw Ze(d)}if(!l)throw new Error("REDAKTYN_PASSPHRASE is not this workspace's passphrase.");return l}function S(e,n={}){return lt("git",e,{encoding:"utf8",maxBuffer:ie,stdio:["ignore","pipe","pipe"],...n})}function X(e,n){const t=dt("git",e,{cwd:n,encoding:"utf8",maxBuffer:ie,stdio:["ignore","pipe","pipe"]});if(t.error?.code==="ENOBUFS")return{patch:t.stdout.slice(0,t.stdout.lastIndexOf(`
|
|
5
|
+
`)+1),cut:!0};if(t.error)throw t.error;if(t.status!==0)throw new Error(t.stderr);return{patch:t.stdout,cut:!1}}const le=["diff","--cached","--unified=0","--no-color","--no-ext-diff","--no-textconv"];function de(e){let n=null,t=0;if(e.length>=2&&e[0]===255&&e[1]===254){if(e.length>=4&&e[2]===0&&e[3]===0)return null;n="le",t=2}else if(e.length>=2&&e[0]===254&&e[1]===255)n="be",t=2;else{const s=e.subarray(0,8e3),r=s.length-s.length%2;if(r<8)return null;let c=0,a=0;for(let d=0;d<r;d+=2)s[d]===0&&c++,s[d+1]===0&&a++;const l=r/2;if(a>=l*.9&&c<=l*.02)n="le";else if(c>=l*.9&&a<=l*.02)n="be";else return null}const o=Buffer.from(e.subarray(t,t+Math.floor((e.length-t)/2)*2));return(n==="le"?o:o.swap16()).toString("utf16le")}function Q(e,n){const t=n.split(`
|
|
5
6
|
`);return t[t.length-1]===""&&t.pop(),t.length===0?"":`diff --git a/${e} b/${e}
|
|
6
7
|
--- /dev/null
|
|
7
8
|
+++ b/${e}
|
|
8
9
|
@@ -0,0 +1,${t.length} @@
|
|
9
10
|
${t.map(o=>`+${o}`).join(`
|
|
10
11
|
`)}
|
|
11
|
-
`}function
|
|
12
|
-
`);return{text:d(s),lockfiles:d(r),cut:t.cut||o.cut,patches:
|
|
13
|
-
`)){const[,o,,s]=t.trim().split(/\s+/);!
|
|
14
|
-
`);return{empty:!1,text:_(
|
|
12
|
+
`}function Mt(e){const n=S(["rev-parse","--show-toplevel"]).trim(),t=X([...le,"--",".",...J.map(i=>`:(exclude,glob)**/${i}`)],n),o=X([...le,"--",...J.map(i=>`:(glob)**/${i}`)],n),s=[j(t.patch)],r=[j(o.patch)],c=[t.patch,o.patch],l=S(["diff","--cached","--no-ext-diff","--no-textconv","--numstat","--no-renames","-z"],{cwd:n}).split("\0").filter(i=>i.startsWith("- - ")).map(i=>i.slice(4));je(n,l.map(i=>({rel:i,spec:`:${i}`,asText:()=>S([...le,"--text","--",`:(literal)${i}`],{cwd:n})})),s,r,c,e,"staged");const d=i=>i.filter(f=>f.length>0).join(`
|
|
13
|
+
`);return{text:d(s),lockfiles:d(r),cut:t.cut||o.cut,patches:c,top:n}}function je(e,n,t,o,s,r,c){for(const{rel:a,spec:l,asText:d}of n){let i;try{const p=Number(S(["cat-file","-s",l],{cwd:e}).trim());if(!(p<=re)){r&&console.warn(`Skipping large ${c} file (${p} bytes): ${a}`);continue}i=S(["cat-file","blob",l],{cwd:e,encoding:"buffer"})}catch{continue}let f;if(i.includes(0)){const p=de(i);if(p!==null)f=Q(a,p);else{const E=Ae(i);if(E==="")continue;f=Q(`${a}${ue}`,E)}}else f=d();(Ot(a)?o:t).push(j(f)),s.push(f)}}const ue=" (text inside a binary file)",Le=/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;function Ft(e){const n=[];for(const t of e.split(`
|
|
14
|
+
`)){const[,o,,s]=t.trim().split(/\s+/);!Le.test(o??"")||/^0+$/.test(o)||n.push({local:o,remote:Le.test(s??"")&&!/^0+$/.test(s)?s:null})}return n}const Ye=["log","--format=","-p","--unified=0","--no-color","--no-ext-diff","--no-textconv"],He=2e3;function Bt(e,n,t){const o=S(["rev-parse","--show-toplevel"]).trim(),s=h=>{try{return S(["cat-file","-e",`${h}^{commit}`],{cwd:o}),!0}catch{return!1}},r=[...new Set(n.map(h=>h.local).filter(s))],c=[];e&&c.push(`--remotes=${e}`);for(const h of n)h.remote&&s(h.remote)&&c.push(h.remote);const a=[...r,...c.length>0?["--not",...c]:[]],l=r.length===0||Number(S(["rev-list","--count",...a],{cwd:o}).trim())===0,d=Jt(n,o);if(l&&d.length===0)return{empty:!0};const i={patch:"",cut:!1},f=l?i:X([...Ye,...a,"--",".",...J.map(h=>`:(exclude,glob)**/${h}`)],o),p=l?i:X([...Ye,...a,"--",...J.map(h=>`:(glob)**/${h}`)],o),E=[j(f.patch)],$=[j(p.patch)],g=[f.patch,p.patch],A=l?"":S(["log","--numstat","--no-renames","-z","--format=%x01%H","--no-ext-diff","--no-textconv",...a],{cwd:o}),T=[];for(const h of A.split("")){const x=h.indexOf("\0");if(x<0)continue;const O=h.slice(0,x);for(const C of h.slice(x+1).split("\0")){const m=/^\n?-\t-\t([\s\S]+)$/.exec(C);m&&T.push({rel:m[1],spec:`${O}:${m[1]}`,asText:()=>S(["diff-tree","--root","-p","--unified=0","--no-color","--no-ext-diff","--no-textconv","--text",O,"--",`:(literal)${m[1]}`],{cwd:o})})}}const N=l?"":S(["log","--format=%x01%H%x02%B%x03",...a],{cwd:o});for(const h of N.split("")){const x=h.indexOf(""),O=h.lastIndexOf("");if(x<0||O<x)continue;const C=h.slice(x+1,O);if(C.trim()==="")continue;const m=Q(`message of commit ${h.slice(0,8)}`,C);E.push(j(m)),g.push(m)}for(const h of d){const x=Q(`message of tag ${h.id}`,h.message);E.push(j(x)),g.push(x)}const b=T.length>He;je(o,T.slice(0,He),E,$,g,t,"pushed");const _=h=>h.filter(x=>x.length>0).join(`
|
|
15
|
+
`);return{empty:!1,text:_(E),lockfiles:_($),cut:f.cut||p.cut||b,patches:g,top:o}}function Jt(e,n){const t=[];for(const o of e){let s=o.local;for(let r=0;r<8;r++){let c;try{c=S(["cat-file","-t",s],{cwd:n}).trim()}catch{break}if(c!=="tag")break;const a=S(["cat-file","tag",s],{cwd:n}),l=a.indexOf(`
|
|
15
16
|
|
|
16
|
-
`),d=
|
|
17
|
-
`)}else
|
|
18
|
-
`)),process.exit(0)}catch(
|
|
19
|
-
`)}function
|
|
17
|
+
`),d=l<0?"":a.slice(l+2);d.trim()!==""&&t.push({id:o.local.slice(0,8),message:d});const i=/^object ([0-9a-f]{40,64})$/m.exec(a);if(!i)break;s=i[1]}}return t}function Gt(){if(process.stdin.isTTY)return"";try{return F(0,"utf8")}catch{return""}}async function zt(){(process.argv.includes("--help")||process.argv.includes("-h"))&&(he(),process.exit(0));const e=Pt(process.argv),n=e.cmd;if(n==="install-hooks"){Xt();return}if(n==="uninstall-hooks"){Qt();return}if(n==="status"){await Wt(e);return}!e.staged&&!e.pushing&&!e.file&&!e.dir&&(he(),process.exit(2));const t=Ke();let o=null;I.staged=e.staged===!0||e.pushing===!0||e.message===!0,e.staged===!0&&qt(),I.hook=e.pushing?"push":"commit";const s=e.staged===!0||e.pushing===!0;if(!t.complete){const{loadSession:u,freshSession:k}=await import("./redaktyn-session.mjs"),R=u();if(R){t.partial&&e.verbose&&console.error(`[redaktyn] ${t.partial}; using this machine's saved connection.`),I.serverUrl=R.serverUrl??null;try{o=await k(void 0,{timeoutMs:V()})}catch(y){console.error(Ce(y)),process.exit(2)}}}const r=e.server||o?.serverUrl||ke();I.serverUrl=r;const c=o?.token||t.token,a=o?"session":t.passphrase,l=e.orgId||process.env.REDAKTYN_ORG_ID;(!r||!c||!a)&&(console.error("Not connected. Run `redaktyn-connect` with an invite link, or set REDAKTYN_SERVER_URL, REDAKTYN_PASSPHRASE and REDAKTYN_DEVICE_TOKEN (or REDAKTYN_TOKEN)."),process.exit(2));const d=It(c),i=d?.orgId??d?.org_id??l;i||(console.error("Could not determine orgId (JWT payload orgId or REDAKTYN_ORG_ID)."),process.exit(2)),l&&d?.orgId&&l!==d.orgId&&(console.error("REDAKTYN_ORG_ID does not match JWT orgId."),process.exit(2));let f="",p="",E=!1,$=0,g=0,A=!1,T=[],N=null;const b=[];if(e.staged)try{({text:f,lockfiles:p,cut:A,patches:T,top:N}=Mt(e.verbose))}catch{console.error("Not a git repository or git diff failed."),process.exit(2)}else if(e.pushing){let u=Ft(Gt());if(u.length===0&&process.stdin.isTTY)try{u=[{local:S(["rev-parse","HEAD"]).trim(),remote:null}]}catch{u=[]}let k;try{k=u.length===0?{empty:!0}:Bt(e.remote||null,u,e.verbose)}catch{console.error("Not a git repository or git log failed."),process.exit(2)}k.empty&&(e.verbose&&console.error("Redaktyn: this push sends no commit the remote does not have, so there is nothing to check."),process.exit(0)),{text:f,lockfiles:p,cut:A,patches:T,top:N}=k}else if(e.file){const u={unreadable:[],tooLarge:[],fromBinary:new Set},k=be(w.resolve(e.file),e.verbose,u);k===null&&(E=!0,console.error(u.tooLarge.length>0?`${K(e.file)} is over ${B}, which is more than a scan reads, so nothing was scanned. Split it, or scan the part that matters.`:`Could not read ${K(e.file)} as text \u2014 nothing was scanned.`)),f=k??"",k!==null&&b.push({file:W(e.file)+(u.fromBinary.size>0?ue:""),text:k})}else if(e.dir){const u={unreadable:[],tooLarge:[],fromBinary:new Set},k=Oe(w.resolve(e.dir),e.exclude||[],[],new Set,u),R=[];for(const y of k){const U=be(y,e.verbose,u);U!==null&&(R.push(U),b.push({file:W(y)+(u.fromBinary.has(y)?ue:""),text:U}))}if($=u.unreadable.length,u.unreadable.length>0){for(const y of u.unreadable.slice(0,5))console.error(`Could not read ${K(W(y))} \u2014 it was not scanned.`);u.unreadable.length>5&&console.error(`\u2026 and ${u.unreadable.length-5} more that could not be read.`)}g=u.tooLarge.length;for(const y of u.tooLarge.slice(0,5))console.error(`${K(W(y))} is over ${B}, so it was not scanned (--exclude leaves it out on purpose).`);u.tooLarge.length>5&&console.error(`\u2026 and ${u.tooLarge.length-5} more files over ${B}.`),R.length===0&&(E=!0,console.error(`No readable text files under ${K(e.dir)} \u2014 nothing was scanned.`)),f=R.join(`
|
|
18
|
+
`)}else he(),process.exit(e.cmd?2:0);const{secrets:_,closed:h}=await Ie(r,c,!!o);if(Array.isArray(_)||(console.error("GET /api/secrets did not answer with a list."),process.exit(2)),_.length===0){const u=h?"Redaktyn: this machine's project is closed, so there are no secrets to check against.":"Redaktyn: no secrets are registered yet, so there is nothing to check against.";e.json?console.log(JSON.stringify({ok:!0,matches:[],registered:0,...h?{projectClosed:!0}:{},scan:{passes:[],digests:0,windows:0,complete:!0}},null,2)):console.error(u),process.exit(0)}const x=o?new Uint8Array(Buffer.from(o.key,"hex")):null,O=x?null:await Ut(r,c,a,i),C=await jt({secrets:_,savedKey:x,orgKey:O,serverUrl:r,token:c,isAdmin:d?.type==="admin"}),m={total:0,windows:0,passesRun:[],exhausted:!1,truncatedChars:!1},Ge=`${r.replace(/\/+$/,"")}/api/cli/scan`,pe=new Set,me=new Map;let Z=0,Y=0;const ze=function*(){yield*Pe(f,C,m),!m.exhausted&&p&&(yield*Pe(p,C,m,1/0))};for(const u of ze()){const k=[];for(const y of u)pe.has(y)||(pe.add(y),k.push(y));if(k.length===0)continue;Z+=Buffer.byteLength(JSON.stringify({digestHex:k})),Y+=1;const R=await Yt(Ge,c,{digestHex:k});for(const y of Array.isArray(R.matches)?R.matches:[])y&&y.secretId&&me.set(y.secretId,y)}const D=[...me.values()];function We(){const u=process.cwd(),k=[];if(s)for(const v of T)for(const ee of nt(v))k.push({file:w.relative(u,w.join(N,ee.file)).split(w.sep).join("/"),text:ee.text,line:ee.line});else for(const v of b)k.push(v);const R=ot(m.hits??[],k),y=D.map(v=>v.secretId).filter(v=>!R.placedIds.has(v)),U=[...R.wanted.keys()].every(v=>R.placedWindows.has(v)),qe=ae.map(v=>v.transform),Xe=y.length===0&&U&&R.rows.length>0&&it(R.rows,R.wanted,qe);return{rows:R.rows,show:Xe,unplacedIds:y,files:y.length===0?new Set(R.rows.map(v=>v.file)).size:0}}const L=m.exhausted||m.truncatedChars||A||E||$>0||g>0,ge=D.length>0||L&&!s,G=[...A?[`The staged changes are over ${ne(ie)}; only the start of them was read.`]:[],...m.truncatedChars?[`Content was truncated at ${Te} characters.`]:[],...m.exhausted?[`Hit the ${ce} digest ceiling before the end.`]:[]];e.json&&(console.log(JSON.stringify({ok:!ge,matches:D,scan:{passes:m.passesRun,digests:m.total,windows:m.windows,complete:!L}},null,2)),process.exit(ge?1:0));const Ve=`${ne(Z)} of fingerprints in ${Y} request${Y===1?"":"s"}. 0 B of your code.`,H=D.length>0?We():null;if(z&&D.length>0){P("");for(const u of rt({kind:e.pushing?"pushing":e.staged?"staged":"content",rows:H.rows,show:H.show,matches:D,unplacedIds:H.unplacedIds,files:H.files,windows:m.windows,passes:m.passesRun.length,sent:`${ne(Z)} of fingerprints in ${Y} request${Y===1?"":"s"}`}))P(u);P(""),process.exit(1)}if(z&&L&&s&&(P(""),te(e.pushing?"Push allowed, but not all of it was checked":"Commit allowed, but not all of it was checked",["",...G.map(u=>`${M.amber("\u25B2")} ${u}`),"","Nothing registered was found in the part that was checked.","Smaller commits are checked in full.",""],{tone:"amber",width:72}),P(""),process.exit(0)),z&&L&&(P(""),te("Not a clean result: the scan could not finish",["",...G.map(u=>`${M.amber("\u25B2")} ${u}`),...E?[`${M.amber("\u25B2")} Nothing was read from the path given.`]:[],...g>0?[`${M.amber("\u25B2")} ${g} file${g===1?" is":"s are"} over ${B} and ${g===1?"was":"were"} not scanned.`]:[],...$>0?[`${M.amber("\u25B2")} ${$} folder${$===1?"":"s"} or file${$===1?"":"s"} could not be read.`]:[],"","Scan a narrower path with --file or --dir, or split the commit.",""],{tone:"amber",width:72}),P(""),process.exit(1)),D.length>0){console.error(""),console.error(`Redaktyn: ${D.length} registered fingerprint${D.length===1?"":"s"} matched in scanned content:`);for(const u of D){console.error(` \u2022 ${K(u.label)} (${K(u.secretId)})`);for(const k of st(u.secretId,H.rows))console.error(k)}console.error(""),console.error(e.pushing?"Remove the secret from the commits being pushed (rewrite them), and rotate it: it is in history that is about to leave this machine.":"Remove the secret material, or rotate it and register the new fingerprint, before committing."),process.exit(1)}if(L&&s){console.error(""),console.error(`Redaktyn: ${e.pushing?"push":"commit"} allowed, but not all of it was checked.`);for(const u of G)console.error(` \u2022 ${u}`);console.error("Nothing registered was found in the part that was checked. Smaller commits are checked in full."),process.exit(0)}if(L){console.error(""),console.error("Redaktyn could not finish scanning this input, so this is not a clean result.");for(const u of G)console.error(` \u2022 ${u}`);E&&console.error(" \u2022 Nothing was read from the path given, so no content was checked."),g>0&&console.error(` \u2022 ${g} file${g===1?" is":"s are"} over ${B}, so ${g===1?"it was":"they were"} not checked.`),$>0&&console.error(` \u2022 ${$} folder${$===1?"":"s"} or file${$===1?"":"s"} could not be read, so ${$===1?"its":"their"} contents were not checked.`),console.error("Scan a narrower path with --file or --dir, or split the commit."),process.exit(1)}e.verbose&&(z?(P(""),te("Clean: no registered fingerprint found",["",...tt([["Checked",`${m.windows.toLocaleString("en-US")} windows, ${m.passesRun.join(", ")}`],["Sent to the server",Ve]],{indent:0,labelWidth:20}),""],{tone:"green",width:78}),P("")):console.log(`Redaktyn: no registered fingerprint matches (${m.passesRun.join(", ")}; ${m.windows} windows).`)),process.exit(0)}async function Wt(e){const n=e.json===!0,t=Ke(),o=t.token;let s=null;if(!t.complete){const{loadSession:a,freshSession:l}=await import("./redaktyn-session.mjs");if(a()){t.partial&&e.verbose&&console.error(`[redaktyn] ${t.partial}; using this machine's saved connection.`);try{s=await l(void 0,{timeoutMs:V()})}catch(d){const i={connected:!1,reason:String(d?.message||d)};console.log(n?JSON.stringify(i):i.reason),process.exit(1)}}}const r=e.server||s?.serverUrl||ke(),c=s?.token||o;if(!r||!c){const a={connected:!1,reason:"Not connected. Run `redaktyn-connect` with an invite link, or set REDAKTYN_SERVER_URL and REDAKTYN_TOKEN."};console.log(n?JSON.stringify(a):a.reason),process.exit(2)}try{const{secrets:a}=await Ie(r,c,!!s),l={connected:!0,source:s?"saved session":"environment",serverUrl:r,...s?.orgName?{workspace:s.orgName}:{},...s?.projectName?{project:s.projectName}:{},enrolledSecrets:Array.isArray(a)?a.length:0,hasPassphrase:s?!0:!!process.env.REDAKTYN_PASSPHRASE,...Je()?{hooks:"older version; run `redaktyn-scan install-hooks`"}:{}};console.log(n?JSON.stringify(l,null,2):Object.entries(l).map(([d,i])=>`${d}: ${i}`).join(`
|
|
19
|
+
`)),process.exit(0)}catch(a){const l={connected:!1,error:String(a?.message||a)};console.log(n?JSON.stringify(l):l.error),process.exit(1)}}const Ue="# Redaktyn pre-commit",fe=[{name:"pre-commit",marker:Ue,intro:"blocks commits when registered fingerprints appear in staged text.",scanning:"scanning staged changes...",args:"--staged",pasted:"--staged",blocked:"Redaktyn blocked this commit."},{name:"commit-msg",marker:"# Redaktyn commit-msg",intro:"blocks a commit whose message holds a registered fingerprint.",scanning:"scanning the commit message...",args:'--file "$1" --message',pasted:'--file "$1" --message',blocked:"Redaktyn blocked this commit."},{name:"pre-push",marker:"# Redaktyn pre-push",intro:"blocks a push of commits (not already on the remote) that hold a registered fingerprint.",scanning:"scanning the commits being pushed...",args:'--pushing --remote "$1"',pasted:'--pushing --remote "$1"',blocked:"Redaktyn blocked this push."}];function Me(){try{S(["rev-parse","--show-toplevel"])}catch{console.error("Not inside a git repository."),process.exit(2)}let e=null;try{e=S(["config","--get","core.hooksPath"]).trim()||null}catch{e=null}const n=w.resolve(process.cwd(),S(["rev-parse","--git-path","hooks"]).trim());return{hooksPath:e,dir:n,hookPath:w.join(n,"pre-commit")}}const Fe=e=>`'${String(e).replace(/'/g,"'\\''")}'`,Be=e=>w.sep==="\\"?e.replace(/\\/g,"/"):e;function Vt(e,n,t){return["#!/bin/sh",`${t.marker} \u2014 ${t.intro}`,'export REDAKTYN_SERVER_URL="${REDAKTYN_SERVER_URL:-}"','export REDAKTYN_PASSPHRASE="${REDAKTYN_PASSPHRASE:-}"','export REDAKTYN_DEVICE_TOKEN="${REDAKTYN_DEVICE_TOKEN:-${REDAKTYN_TOKEN:-}}"',`REDAKTYN_SCANNER=${Fe(Be(e))}`,`REDAKTYN_NODE=${Fe(Be(n))}`,"unset NODE_PATH",'if [ -n "${NODE_OPTIONS:-}" ]; then',' redaktyn_opts=""; redaktyn_skip=0'," set -f"," for redaktyn_o in $NODE_OPTIONS; do",' if [ "$redaktyn_skip" = 1 ]; then redaktyn_skip=0; continue; fi',' case "$redaktyn_o" in'," --require|-r|--import|--loader|--experimental-loader) redaktyn_skip=1 ;;"," --require=*|-r=*|--import=*|--loader=*|--experimental-loader=*) ;;",' *) redaktyn_opts="$redaktyn_opts $redaktyn_o" ;;'," esac"," done"," set +f",' NODE_OPTIONS="${redaktyn_opts# }"; export NODE_OPTIONS',"fi",'if [ ! -f "$REDAKTYN_SCANNER" ]; then',' echo "Redaktyn: the scanner this hook runs is no longer at $REDAKTYN_SCANNER, so nothing was checked." >&2',' echo "Run \\`redaktyn-scan install-hooks\\` again, or delete this hook." >&2',' echo "To commit without the check, at your own risk: git commit --no-verify" >&2'," exit 1","fi",'if [ ! -x "$REDAKTYN_NODE" ]; then',' REDAKTYN_NODE=""'," redaktyn_here=$(pwd -P 2>/dev/null)",' redaktyn_clean=""'," redaktyn_ifs=$IFS"," IFS=:"," for redaktyn_dir in $PATH; do",' case "$redaktyn_dir" in /*) ;; *) continue ;; esac',' redaktyn_real=$(cd "$redaktyn_dir" 2>/dev/null && pwd -P) || continue',' case "$redaktyn_real" in',' "$redaktyn_here"|"$redaktyn_here"/*) ;;',' *) redaktyn_clean="$redaktyn_clean:$redaktyn_dir" ;;'," esac"," done"," IFS=$redaktyn_ifs",' REDAKTYN_NODE=$(PATH="${redaktyn_clean#:}" command -v node 2>/dev/null)',"fi",'if [ -z "$REDAKTYN_NODE" ] || [ ! -x "$REDAKTYN_NODE" ]; then',' echo "Redaktyn: node was not found, so nothing was checked. Install Node, or run \\`redaktyn-scan install-hooks\\` again." >&2',' echo "To commit without the check, at your own risk: git commit --no-verify" >&2'," exit 1","fi",`echo "Redaktyn: ${t.scanning}"`,`"$REDAKTYN_NODE" "$REDAKTYN_SCANNER" ${t.args}`,"CODE=$?",'if [ "$CODE" -ne 0 ]; then',` echo "${t.blocked}"`," exit 1","fi",'echo "Redaktyn: OK"',"exit 0",""].join(`
|
|
20
|
+
`)}function Je(){try{if((S(["config","--get","core.hooksPath"]).trim()||null)!==null)return null}catch{}try{const e=w.resolve(process.cwd(),S(["rev-parse","--git-path","hooks"]).trim()),n=s=>oe(w.join(e,s))?F(w.join(e,s),"utf8"):null,t=n("pre-commit");if(t===null||!t.includes(Ue))return null;const o=["commit-msg","pre-push"].some(s=>n(s)===null);return t.includes("REDAKTYN_SCANNER=")&&!o?null:{dir:e,text:"this repository's git hooks were written by an older version of redaktyn-scan (an older pre-commit, and no commit-msg or pre-push hook). Run `redaktyn-scan install-hooks` to bring them up to date."}}catch{return null}}function qt(){const e=Je();if(e===null)return;const n=w.join(w.dirname(e.dir),"redaktyn-hooks-noted");try{if(Date.now()-se(n).mtimeMs<7*24*60*60*1e3)return}catch{}console.error(`Redaktyn: ${e.text}`);try{ye(n,String(Date.now()))}catch{}}function Xt(){const{hooksPath:e,dir:n}=Me(),t=Et(import.meta.url),o=r=>`node "${t}" ${r.pasted}`;if(e){console.error(`This repository's hooks run from ${e} (core.hooksPath), so nothing was written.`),console.error("Add these lines to the hooks there, or to your hook manager's steps:");for(const r of fe)console.error(` ${r.name}: ${o(r)}`);process.exit(1)}let s=0;for(const r of fe){const c=w.join(n,r.name);if(oe(c)){const a=F(c,"utf8");if(a.trim()!==""&&!a.includes(r.marker)){console.error(`There is already a ${r.name} hook at ${c}, and it is not Redaktyn's. Nothing was changed.`),console.error("To run both, add this line to it:"),console.error(` ${o(r)}`),s++;continue}}ft(n,{recursive:!0}),ye(c,Vt(t,process.execPath,r),{mode:493}),yt(c,493),console.log(`Installed ${r.name} hook at`,c)}s>0&&process.exit(1)}function Qt(){const{dir:e}=Me();for(const n of fe){const t=w.join(e,n.name);if(!oe(t)){n.name==="pre-commit"&&console.log("No pre-commit hook found.");continue}if(!F(t,"utf8").includes(n.marker)){console.log(`${n.name} exists but is not a Redaktyn hook \u2014 not removing.`);continue}wt(t),console.log(`Removed Redaktyn ${n.name} hook at`,t)}}function he(){console.log(`
|
|
20
21
|
redaktyn-scan \u2014 Redaktyn digest scanner (ZK: HMAC hex batches only)
|
|
21
22
|
|
|
22
23
|
Commands:
|
|
@@ -40,4 +41,4 @@ Flags:
|
|
|
40
41
|
|
|
41
42
|
Environment:
|
|
42
43
|
REDAKTYN_SERVER_URL, REDAKTYN_PASSPHRASE, REDAKTYN_DEVICE_TOKEN (or REDAKTYN_TOKEN)
|
|
43
|
-
`)}
|
|
44
|
+
`)}zt().catch(e=>{console.error(Ce(e)),process.exit(2)});
|