@redaktyn/cli 1.6.4 → 1.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/package.json +4 -1
- package/redaktyn-clipboard.mjs +1 -1
- package/redaktyn-connect.mjs +6 -6
- package/redaktyn-decoder.mjs +6 -0
- package/redaktyn-invite.mjs +8 -8
- package/redaktyn-lock-net.mjs +1 -1
- package/redaktyn-login.mjs +7 -7
- package/redaktyn-places.mjs +6 -0
- package/redaktyn-rail.mjs +21 -0
- package/redaktyn-scan.mjs +25 -10
- package/redaktyn-session.mjs +8 -8
- package/redaktyn-shared.cjs +6 -6
- package/redaktyn-ui.mjs +5 -5
package/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};
|