@redaktyn/cli 1.6.1 → 1.6.3

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.
@@ -1,8 +1,8 @@
1
- import{createHash as Z,createHmac as Q,randomBytes as I,webcrypto as S}from"node:crypto";import{existsSync as T,mkdirSync as ee,readFileSync as x,rmSync as K,writeFileSync as te,chmodSync as F,renameSync as ne}from"node:fs";import{homedir as re,hostname as oe}from"node:os";import{join as k}from"node:path";const L=20*60*60*1e3,$=32,ae="https://redaktyn.com";function M(e){return(e||process.env.REDAKTYN_SERVER_URL||process.env.REDAKTYN_SERVER||ae).replace(/\/+$/,"")}class a extends Error{}function v(){return process.env.REDAKTYN_CONFIG_DIR||(process.env.XDG_CONFIG_HOME?k(process.env.XDG_CONFIG_HOME,"redaktyn"):k(re(),".config","redaktyn"))}const C=()=>k(v(),"session.json"),ie=()=>k(v(),"device-id");function se(){ee(v(),{recursive:!0,mode:448});try{F(v(),448)}catch{}}function R(e,t){se();const n=`${e}.${process.pid}.tmp`;te(n,t,{mode:384});try{F(n,384)}catch{}ne(n,e)}const H=()=>k(v(),"oprf-pins.json");function Te(){const e=()=>{try{const t=JSON.parse(x(H(),"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){R(H(),JSON.stringify({...e(),[`${t}|${n}`]:o})+`
2
- `)}}}function _(){const e=C();if(!T(e))return null;try{const t=JSON.parse(x(e,"utf8"));if(t&&t.token&&t.key&&t.serverUrl&&t.orgId)return t}catch{}return null}function W(e){R(C(),JSON.stringify(e,null,2)+`
3
- `)}function Ne(){const e=C();return T(e)&&K(e),!0}const P=()=>k(v(),"admin.json");function ce(){const e=P();if(!T(e))return null;try{const t=JSON.parse(x(e,"utf8"));if(t&&t.token&&t.serverUrl)return t}catch{}return null}function b(e){R(P(),JSON.stringify(e,null,2)+`
4
- `)}function Ee(){const e=P();return T(e)&&K(e),!0}function de(){const e=ie();if(T(e)){const n=x(e,"utf8").trim();if(/^cli-[a-f0-9]{32}$/.test(n))return n}const t=`cli-${I(16).toString("hex")}`;return R(e,t+`
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 xe(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 Re(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?.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 b(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 b(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(`
1
+ import{createHash as Z,createHmac as Q,randomBytes as I,webcrypto as S}from"node:crypto";import{existsSync as T,mkdirSync as ee,readFileSync as R,rmSync as K,writeFileSync as te,chmodSync as F,renameSync as ne}from"node:fs";import{homedir as re,hostname as oe}from"node:os";import{join as k}from"node:path";const L=20*60*60*1e3,$=32,ae="https://redaktyn.com";function M(e){return(e||process.env.REDAKTYN_SERVER_URL||process.env.REDAKTYN_SERVER||ae).replace(/\/+$/,"")}class a extends Error{}function v(){return process.env.REDAKTYN_CONFIG_DIR||(process.env.XDG_CONFIG_HOME?k(process.env.XDG_CONFIG_HOME,"redaktyn"):k(re(),".config","redaktyn"))}const C=()=>k(v(),"session.json"),ie=()=>k(v(),"device-id");function se(){ee(v(),{recursive:!0,mode:448});try{F(v(),448)}catch{}}function x(e,t){se();const n=`${e}.${process.pid}.tmp`;te(n,t,{mode:384});try{F(n,384)}catch{}ne(n,e)}const H=()=>k(v(),"oprf-pins.json");function Te(){const e=()=>{try{const t=JSON.parse(R(H(),"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){x(H(),JSON.stringify({...e(),[`${t}|${n}`]:o})+`
2
+ `)}}}function _(){const e=C();if(!T(e))return null;try{const t=JSON.parse(R(e,"utf8"));if(t&&t.token&&t.key&&t.serverUrl&&t.orgId)return t}catch{}return null}function W(e){x(C(),JSON.stringify(e,null,2)+`
3
+ `)}function Ne(){const e=C();return T(e)&&K(e),!0}const b=()=>k(v(),"admin.json");function ce(){const e=b();if(!T(e))return null;try{const t=JSON.parse(R(e,"utf8"));if(t&&t.token&&t.serverUrl)return t}catch{}return null}function P(e){x(b(),JSON.stringify(e,null,2)+`
4
+ `)}function Ee(){const e=b();return T(e)&&K(e),!0}function de(){const e=ie();if(T(e)){const n=R(e,"utf8").trim();if(/^cli-[a-f0-9]{32}$/.test(n))return n}const t=`cli-${I(16).toString("hex")}`;return x(e,t+`
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
6
  `),t(o)},s=d=>{for(const c of d){if(c==="\r"||c===`
7
7
  `||c==="")return r();c===""&&(n.setRawMode(!1),process.stderr.write(`
8
- `),process.exit(130)),c==="\x7F"||c==="\b"?o=o.slice(0,-1):o+=c}};n.on("data",s)})}async function Ce({serverUrl:e,onCode:t,sleep:n=o=>new Promise(r=>setTimeout(r,o))}={}){const o=M(e),r=await m(o,"POST","/api/cli-login/start",{body:{clientName:oe()}});if(r.status===429)throw new a("Too many sign-in requests. Wait a few minutes and try again.");if(!r.ok||!r.data?.deviceCode)throw new a(`Could not start a browser sign-in (${r.status}). This server may be older; use: redaktyn-login --password`);const{deviceCode:s,userCode:d,verificationUrl:c,expiresIn:p}=r.data;let y=(r.data.interval||2)*1e3;const w=Date.now()+(p||600)*1e3;t&&t({userCode:d,verificationUrl:c,expiresIn:p});let h=0;for(;Date.now()<w;){await n(y);let i;try{i=await m(o,"POST","/api/cli-login/poll",{body:{deviceCode:s}}),h=0}catch(E){if(++h>=5)throw E;continue}if(i.status===429){y=((i.data?.interval||5)+1)*1e3;continue}if(i.status===403)throw new a("The sign-in was denied in the browser.");if(i.status===410)throw new a("The sign-in request expired. Run the command again.");if(!i.ok)throw new a(`Sign-in failed (${i.status}): ${i.data?.error||i.text.slice(0,160)}`);if(i.data?.status!=="approved")continue;const u=i.data.user?.role;if(u!=="OWNER"&&u!=="ADMIN")throw new a(`That account is a ${u??"non-admin"}. Bulk import and invites need an OWNER or ADMIN.`);const l={serverUrl:o,token:i.data.token,tokenIssuedAt:Date.now(),loggedInAt:new Date().toISOString(),email:i.data.user.email,role:u,orgId:i.data.org?.id??null,orgName:i.data.org?.name??null};return b(l),l}throw new a("Timed out waiting for approval. Run the command again.")}export{ae as DEFAULT_SERVER_URL,$ as INVITE_KEY_BYTES,a as SessionError,L as TOKEN_REFRESH_AFTER_MS,m as apiJson,ue as bytesToB64u,X as cleanUserName,Re as connectWithInvite,Ee as deleteAdminSession,Ne as deleteSession,de as deviceId,Oe as freshAdminSession,Ae as freshSession,fe as inviteCodeFitsLink,ge as inviteInfo,Ie as linkNeedsInviteCode,ce as loadAdminSession,_ as loadSession,Ce as loginWithBrowser,De as loginWithPassword,Te as oprfPinStore,D as parseInviteLink,$e as readHidden,M as resolveServerUrl,b as saveAdminSession,W as saveSession,xe as sealInviteKey,v as sessionDir,he as unsealInviteKey};
8
+ `),process.exit(130)),c==="\x7F"||c==="\b"?o=o.slice(0,-1):o+=c}};n.on("data",s)})}async function Ce({serverUrl:e,onCode:t,sleep:n=o=>new Promise(r=>setTimeout(r,o))}={}){const o=M(e),r=await m(o,"POST","/api/cli-login/start",{body:{clientName:oe()}});if(r.status===429)throw new a("Too many sign-in requests. Wait a few minutes and try again.");if(!r.ok||!r.data?.deviceCode)throw new a(`Could not start a browser sign-in (${r.status}). This server may be older; use: redaktyn-login --password`);const{deviceCode:s,userCode:d,verificationUrl:c,expiresIn:p}=r.data;let y=(r.data.interval||2)*1e3;const w=Date.now()+(p||600)*1e3;t&&t({userCode:d,verificationUrl:c,expiresIn:p});let h=0;for(;Date.now()<w;){await n(y);let i;try{i=await m(o,"POST","/api/cli-login/poll",{body:{deviceCode:s}}),h=0}catch(E){if(++h>=5)throw E;continue}if(i.status===429){y=((i.data?.interval||5)+1)*1e3;continue}if(i.status===403)throw new a("The sign-in was denied in the browser.");if(i.status===410)throw new a("The sign-in request expired. Run the command again.");if(!i.ok)throw new a(`Sign-in failed (${i.status}): ${i.data?.error||i.text.slice(0,160)}`);if(i.data?.status!=="approved")continue;const u=i.data.user?.role;if(u!=="OWNER"&&u!=="ADMIN")throw new a(`That account is a ${u??"non-admin"}. Bulk import and invites need an OWNER or ADMIN.`);const l={serverUrl:o,token:i.data.token,tokenIssuedAt:Date.now(),loggedInAt:new Date().toISOString(),email:i.data.user.email,role:u,orgId:i.data.org?.id??null,orgName:i.data.org?.name??null};return P(l),l}throw new a("Timed out waiting for approval. Run the command again.")}export{ae as DEFAULT_SERVER_URL,$ as INVITE_KEY_BYTES,a as SessionError,L as TOKEN_REFRESH_AFTER_MS,m as apiJson,ue as bytesToB64u,X as cleanUserName,xe as connectWithInvite,Ee as deleteAdminSession,Ne as deleteSession,de as deviceId,Oe as freshAdminSession,Ae as freshSession,fe as inviteCodeFitsLink,ge as inviteInfo,Ie as linkNeedsInviteCode,ce as loadAdminSession,_ as loadSession,Ce as loginWithBrowser,De as loginWithPassword,Te as oprfPinStore,D as parseInviteLink,$e as readHidden,M as resolveServerUrl,P as saveAdminSession,W as saveSession,Re as sealInviteKey,v as sessionDir,he as unsealInviteKey};