rswag-online/frontend/vendor/@encryptid/sdk/encryptid.browser.js

72 lines
33 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

var b={rpId:"jeffemmett.com",rpName:"EncryptID",origin:typeof window<"u"?window.location.origin:"",userVerification:"required",timeout:60000},u=null;function R(){if(u)u.abort(),u=null}function a(i){let t=new Uint8Array(i),o="";for(let r=0;r<t.byteLength;r++)o+=String.fromCharCode(t[r]);return btoa(o).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function L(i){let t=i.replace(/-/g,"+").replace(/_/g,"/"),o="=".repeat((4-t.length%4)%4),r=atob(t+o),n=new Uint8Array(r.length);for(let s=0;s<r.length;s++)n[s]=r.charCodeAt(s);return n.buffer}function C(){return crypto.getRandomValues(new Uint8Array(32)).buffer}async function q(i){let o=new TextEncoder().encode(`encryptid-prf-salt-${i}-v1`);return crypto.subtle.digest("SHA-256",o)}async function Q(i,t,o={}){R();let r={...b,...o};if(!window.PublicKeyCredential)throw Error("WebAuthn is not supported in this browser");let n=await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),s=crypto.getRandomValues(new Uint8Array(32)),e=C(),d=await q("master-key"),p={publicKey:{challenge:new Uint8Array(e),rp:{id:r.rpId,name:r.rpName},user:{id:s,name:i,displayName:t},pubKeyCredParams:[{alg:-7,type:"public-key"},{alg:-257,type:"public-key"}],authenticatorSelection:{residentKey:"required",requireResidentKey:!0,userVerification:r.userVerification,authenticatorAttachment:n?"platform":void 0},attestation:"none",timeout:r.timeout,extensions:{prf:{eval:{first:new Uint8Array(d)}},credProps:!0}}},v=await navigator.credentials.create(p);if(!v)throw Error("Failed to create credential");let M=v.response,T=v.getClientExtensionResults()?.prf?.enabled===!0,J=M.getPublicKey();if(!J)throw Error("Failed to get public key from credential");return{credentialId:a(v.rawId),publicKey:J,userId:a(s.buffer),username:i,createdAt:Date.now(),prfSupported:T,transports:M.getTransports?.()}}async function D(i,t={}){R();let o={...b,...t};if(!window.PublicKeyCredential)throw Error("WebAuthn is not supported in this browser");let r=C(),n=await q("master-key"),s=i?[{type:"public-key",id:new Uint8Array(L(i))}]:void 0,e={publicKey:{challenge:new Uint8Array(r),rpId:o.rpId,allowCredentials:s,userVerification:o.userVerification,timeout:o.timeout,extensions:{prf:{eval:{first:new Uint8Array(n)}}}}},d=await navigator.credentials.get(e);if(!d)throw Error("Authentication failed");let p=d.response,v=d.getClientExtensionResults()?.prf?.results;return{credentialId:a(d.rawId),userId:p.userHandle?a(p.userHandle):"",prfOutput:v?.first,signature:p.signature,authenticatorData:p.authenticatorData}}async function V(){if(!window.PublicKeyCredential)return!1;if(typeof PublicKeyCredential.isConditionalMediationAvailable==="function")return PublicKeyCredential.isConditionalMediationAvailable();return!1}async function X(i={}){if(!await V())return null;R(),u=new AbortController;let o={...b,...i},r=C(),n=await q("master-key");try{let s=await navigator.credentials.get({publicKey:{challenge:new Uint8Array(r),rpId:o.rpId,userVerification:o.userVerification,timeout:o.timeout,extensions:{prf:{eval:{first:new Uint8Array(n)}}}},mediation:"conditional",signal:u.signal});if(u=null,!s)return null;let e=s.response,d=s.getClientExtensionResults()?.prf?.results;return{credentialId:a(s.rawId),userId:e.userHandle?a(e.userHandle):"",prfOutput:d?.first,signature:e.signature,authenticatorData:e.authenticatorData}}catch{return null}}async function m(){let i={webauthn:!1,platformAuthenticator:!1,conditionalUI:!1,prfExtension:!1};if(!window.PublicKeyCredential)return i;i.webauthn=!0;try{i.platformAuthenticator=await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()}catch{i.platformAuthenticator=!1}return i.conditionalUI=await V(),i.prfExtension=!0,i}var K="https://encryptid.jeffemmett.com";class z{serverUrl;constructor(i=K){this.serverUrl=i.replace(/\/$/,"")}async registerStart(i,t){let o=await fetch(`${this.serverUrl}/api/register/start`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:i,displayName:t||i})});if(!o.ok){let r=await o.json().catch(()=>({error:"Registration start failed"}));throw Error(r.error||`HTTP ${o.status}`)}return o.json()}async registerComplete(i,t,o,r){let n=t.response,s=n.getPublicKey(),e=await fetch(`${this.serverUrl}/api/register/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({challenge:i,userId:o,username:r,credential:{credentialId:a(t.rawId),publicKey:s?a(s):"",transports:n.getTransports?.()||[]}})});if(!e.ok){let d=await e.json().catch(()=>({error:"Registration complete failed"}));throw Error(d.error||`HTTP ${e.status}`)}return e.json()}async authStart(i){let t=await fetch(`${this.serverUrl}/api/auth/start`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i?{credentialId:i}:{})});if(!t.ok){let o=await t.json().catch(()=>({error:"Auth start failed"}));throw Error(o.error||`HTTP ${t.status}`)}return t.json()}async authComplete(i,t){let o=t.response,r=t.getClientExtensionResults()?.prf?.results,n=await fetch(`${this.serverUrl}/api/auth/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({challenge:i,credential:{credentialId:a(t.rawId),signature:a(o.signature),authenticatorData:a(o.authenticatorData),prfOutput:r?.first?a(r.first):null}})});if(!n.ok){let s=await n.json().catch(()=>({error:"Auth complete failed"}));throw Error(s.error||`HTTP ${n.status}`)}return n.json()}async verifySession(i){return(await fetch(`${this.serverUrl}/api/session/verify`,{headers:{Authorization:`Bearer ${i}`}})).json()}async refreshToken(i){let t=await fetch(`${this.serverUrl}/api/session/refresh`,{method:"POST",headers:{Authorization:`Bearer ${i}`}});if(!t.ok){let o=await t.json().catch(()=>({error:"Token refresh failed"}));throw Error(o.error||`HTTP ${t.status}`)}return t.json()}async listCredentials(i){let t=await fetch(`${this.serverUrl}/api/user/credentials`,{headers:{Authorization:`Bearer ${i}`}});if(!t.ok)throw Error("Failed to list credentials");return t.json()}async setRecoveryEmail(i,t){let o=await fetch(`${this.serverUrl}/api/recovery/email/set`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${i}`},body:JSON.stringify({email:t})});if(!o.ok){let r=await o.json().catch(()=>({error:"Failed to set recovery email"}));throw Error(r.error||`HTTP ${o.status}`)}return o.json()}async requestEmailRecovery(i){let t=await fetch(`${this.serverUrl}/api/recovery/email/request`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:i})});if(!t.ok){let o=await t.json().catch(()=>({error:"Recovery request failed"}));throw Error(o.error||`HTTP ${t.status}`)}return t.json()}async verifyRecoveryToken(i){let t=await fetch(`${this.serverUrl}/api/recovery/email/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:i})});if(!t.ok){let o=await t.json().catch(()=>({error:"Recovery verification failed"}));throw Error(o.error||`HTTP ${t.status}`)}return t.json()}async register(i,t,o){let{options:r,userId:n}=await this.registerStart(i,t),s={publicKey:{...r,challenge:l(r.challenge),user:{...r.user,id:l(r.user.id)},pubKeyCredParams:r.pubKeyCredParams,extensions:{credProps:!0,prf:{eval:{first:new Uint8Array(32)}}}}},e=await navigator.credentials.create(s);if(!e)throw Error("Failed to create credential");return this.registerComplete(r.challenge,e,n,i)}async authenticate(i,t){let{options:o}=await this.authStart(i),r={publicKey:{challenge:l(o.challenge),rpId:o.rpId,userVerification:o.userVerification,timeout:o.timeout,allowCredentials:o.allowCredentials?.map((d)=>({type:d.type,id:l(d.id),transports:d.transports})),extensions:{prf:{eval:{first:new Uint8Array(32)}}}}},n=await navigator.credentials.get(r);if(!n)throw Error("Authentication failed");let s=await this.authComplete(o.challenge,n),e=n.getClientExtensionResults()?.prf?.results;return{...s,prfOutput:e?.first}}}function l(i){let t=i.replace(/-/g,"+").replace(/_/g,"/"),o="=".repeat((4-t.length%4)%4),r=atob(t+o),n=new Uint8Array(r.length);for(let s=0;s<r.length;s++)n[s]=r.charCodeAt(s);return n}var c;((n)=>{n[n.BASIC=1]="BASIC";n[n.STANDARD=2]="STANDARD";n[n.ELEVATED=3]="ELEVATED";n[n.CRITICAL=4]="CRITICAL"})(c||={});var Z={"rspace:view-public":{minAuthLevel:1},"rspace:view-private":{minAuthLevel:2},"rspace:edit-board":{minAuthLevel:2},"rspace:create-board":{minAuthLevel:2},"rspace:delete-board":{minAuthLevel:3,maxAgeSeconds:300},"rspace:encrypt-board":{minAuthLevel:3,requiresCapability:"encrypt"},"rwallet:view-balance":{minAuthLevel:1},"rwallet:view-history":{minAuthLevel:2},"rwallet:send-small":{minAuthLevel:2,requiresCapability:"wallet"},"rwallet:send-large":{minAuthLevel:3,requiresCapability:"wallet",maxAgeSeconds:60},"rwallet:add-guardian":{minAuthLevel:4,maxAgeSeconds:60},"rwallet:remove-guardian":{minAuthLevel:4,maxAgeSeconds:60},"rvote:view-proposals":{minAuthLevel:1},"rvote:cast-vote":{minAuthLevel:3,requiresCapability:"sign",maxAgeSeconds:300},"rvote:delegate":{minAuthLevel:3,requiresCapability:"wallet"},"rfiles:list-files":{minAuthLevel:2},"rfiles:download-own":{minAuthLevel:2,requiresCapability:"encrypt"},"rfiles:upload":{minAuthLevel:2,requiresCapability:"encrypt"},"rfiles:share":{minAuthLevel:3,requiresCapability:"encrypt"},"rfiles:delete":{minAuthLevel:3,maxAgeSeconds:300},"rfiles:export-keys":{minAuthLevel:4,maxAgeSeconds:60},"rmaps:view-public":{minAuthLevel:1},"rmaps:add-location":{minAuthLevel:2},"rmaps:edit-location":{minAuthLevel:2,requiresCapability:"sign"},"account:view-profile":{minAuthLevel:2},"account:edit-profile":{minAuthLevel:3},"account:export-data":{minAuthLevel:4,maxAgeSeconds:60},"account:delete":{minAuthLevel:4,maxAgeSeconds:60},"rspace:create-space":{minAuthLevel:2},"rspace:configure-space":{minAuthLevel:3,maxAgeSeconds:300},"rspace:delete-space":{minAuthLevel:4,maxAgeSeconds:60},"rspace:invite-member":{minAuthLevel:2},"rspace:remove-member":{minAuthLevel:3,maxAgeSeconds:300},"rspace:change-visibility":{minAuthLevel:3,maxAgeSeconds:300},"rfunds:create-space":{minAuthLevel:2},"rfunds:edit-flows":{minAuthLevel:2},"rfunds:share-space":{minAuthLevel:2}},x="encryptid_session",A=300000;class j{session=null;refreshTimer=null;constructor(){this.restoreSession()}async createSession(i,t,o){let r=Math.floor(Date.now()/1000),n={iss:"https://encryptid.jeffemmett.com",sub:t,aud:["rspace.online","rwallet.online","rvote.online","rfiles.online","rmaps.online"],iat:r,exp:r+900,jti:a(crypto.getRandomValues(new Uint8Array(16)).buffer),eid:{credentialId:i.credentialId,authLevel:3,authTime:r,capabilities:o,recoveryConfigured:!1}},s=this.createUnsignedToken(n),e=this.createRefreshToken(t);return this.session={accessToken:s,refreshToken:e,claims:n,lastAuthTime:Date.now()},this.persistSession(),this.scheduleRefresh(),this.session}getSession(){return this.session}getDID(){return this.session?.claims.sub??null}getAccessToken(){return this.session?.accessToken??null}getAuthLevel(){if(!this.session)return 1;let i=Math.floor(Date.now()/1000);if(i>=this.session.claims.exp)return 1;let t=i-this.session.claims.eid.authTime;if(t<60)return 3;if(t<900)return 2;return 1}canPerform(i){let t=Z[i];if(!t)return{allowed:!1,reason:"Unknown operation"};if(!this.session)return{allowed:!1,reason:"Not authenticated"};let o=this.getAuthLevel();if(o<t.minAuthLevel)return{allowed:!1,reason:`Requires ${c[t.minAuthLevel]} auth level (current: ${c[o]})`};if(t.requiresCapability){if(!this.session.claims.eid.capabilities[t.requiresCapability])return{allowed:!1,reason:`Requires ${t.requiresCapability} capability`}}if(t.maxAgeSeconds){let r=Math.floor(Date.now()/1000)-this.session.claims.eid.authTime;if(r>t.maxAgeSeconds)return{allowed:!1,reason:`Authentication too old (${r}s > ${t.maxAgeSeconds}s)`}}return{allowed:!0}}requiresFreshAuth(i){let t=Z[i];if(!t)return!0;if(t.minAuthLevel>=4)return!0;if(t.maxAgeSeconds&&t.maxAgeSeconds<=60)return!0;return!1}upgradeAuthLevel(i=3){if(!this.session)return;this.session.claims.eid.authLevel=i,this.session.claims.eid.authTime=Math.floor(Date.now()/1000),this.session.lastAuthTime=Date.now(),this.persistSession()}clearSession(){if(this.session=null,this.refreshTimer)clearTimeout(this.refreshTimer),this.refreshTimer=null;try{localStorage.removeItem(x)}catch{}}isValid(){if(!this.session)return!1;return Math.floor(Date.now()/1000)<this.session.claims.exp}createUnsignedToken(i){return`${btoa(JSON.stringify({alg:"none",typ:"JWT"}))}.${btoa(JSON.stringify(i))}.`}createRefreshToken(i){return btoa(JSON.stringify({sub:i,iat:Math.floor(Date.now()/1000),exp:Math.floor(Date.now()/1000)+604800,jti:a(crypto.getRandomValues(new Uint8Array(16)).buffer)}))}persistSession(){if(!this.session)return;try{localStorage.setItem(x,JSON.stringify(this.session))}catch{}}restoreSession(){try{let i=localStorage.getItem(x);if(i){let t=JSON.parse(i);if(Math.floor(Date.now()/1000)<t.claims.exp)this.session=t,this.scheduleRefresh();else localStorage.removeItem(x)}}catch{}}scheduleRefresh(){if(!this.session)return;if(this.refreshTimer)clearTimeout(this.refreshTimer);let t=this.session.claims.exp*1000-A,o=Math.max(t-Date.now(),0);this.refreshTimer=setTimeout(()=>this.refreshTokens(),o)}async refreshTokens(){if(!this.session)return;let i=Math.floor(Date.now()/1000);this.session.claims.eid.authLevel=Math.min(this.session.claims.eid.authLevel,2),this.session.claims.iat=i,this.session.claims.exp=i+900,this.session.claims.jti=a(crypto.getRandomValues(new Uint8Array(16)).buffer),this.session.accessToken=this.createUnsignedToken(this.session.claims),this.persistSession(),this.scheduleRefresh()}}var $=null;function g(){if(!$)$=new j;return $}class F{masterKey=null;derivedKeys=null;fromPRF=!1;async initFromPRF(i){this.masterKey=await crypto.subtle.importKey("raw",i,{name:"HKDF"},!1,["deriveKey","deriveBits"]),this.fromPRF=!0,this.derivedKeys=null}async initFromPassphrase(i,t){let o=new TextEncoder,r=await crypto.subtle.importKey("raw",o.encode(i),{name:"PBKDF2"},!1,["deriveBits"]),n=await crypto.subtle.deriveBits({name:"PBKDF2",salt:t,iterations:600000,hash:"SHA-256"},r,256);this.masterKey=await crypto.subtle.importKey("raw",n,{name:"HKDF"},!1,["deriveKey","deriveBits"]),this.fromPRF=!1,this.derivedKeys=null}static generateSalt(){return crypto.getRandomValues(new Uint8Array(32))}isInitialized(){return this.masterKey!==null}async getKeys(){if(!this.masterKey)throw Error("Key manager not initialized");if(this.derivedKeys)return this.derivedKeys;let[i,t,o]=await Promise.all([this.deriveEncryptionKey(),this.deriveSigningKeyPair(),this.deriveDIDSeed()]),r=await this.generateDID(o);return this.derivedKeys={encryptionKey:i,signingKeyPair:t,didSeed:o,did:r,fromPRF:this.fromPRF},this.derivedKeys}async deriveEncryptionKey(){let i=new TextEncoder;return crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:i.encode("encryptid-encryption-key-v1"),info:i.encode("AES-256-GCM")},this.masterKey,{name:"AES-GCM",length:256},!1,["encrypt","decrypt","wrapKey","unwrapKey"])}async deriveSigningKeyPair(){return crypto.subtle.generateKey({name:"ECDSA",namedCurve:"P-256"},!1,["sign","verify"])}async deriveDIDSeed(){let i=new TextEncoder,t=await crypto.subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:i.encode("encryptid-did-key-v1"),info:i.encode("Ed25519-seed")},this.masterKey,256);return new Uint8Array(t)}async generateDID(i){let t=await crypto.subtle.digest("SHA-256",i),o=new Uint8Array(t).slice(0,32),r=new Uint8Array([237,1]),n=new Uint8Array(34);return n.set(r),n.set(o,2),`did:key:z${a(n.buffer).replace(/-/g,"").replace(/_/g,"")}`}clear(){this.masterKey=null,this.derivedKeys=null,this.fromPRF=!1}}var E=null;function y(){if(!E)E=new F;return E}var ii='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="passkey-icon"><circle cx="12" cy="10" r="3"/><path d="M12 13v8"/><path d="M9 18h6"/><circle cx="12" cy="10" r="7"/></svg>',ti=`
:host { --eid-primary: #06b6d4; --eid-primary-hover: #0891b2; --eid-bg: #0f172a; --eid-bg-hover: #1e293b; --eid-text: #f1f5f9; --eid-text-secondary: #94a3b8; --eid-radius: 8px; display: inline-block; font-family: system-ui, -apple-system, sans-serif; }
.login-btn { display: flex; align-items: center; gap: 12px; padding: 12px 24px; background: var(--eid-primary); color: white; border: none; border-radius: var(--eid-radius); font-size: 1rem; font-weight: 500; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3); }
.login-btn:hover { background: var(--eid-primary-hover); transform: translateY(-1px); }
.login-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
.login-btn.outline { background: transparent; border: 2px solid var(--eid-primary); color: var(--eid-primary); }
.login-btn.outline:hover { background: var(--eid-primary); color: white; }
.login-btn.small { padding: 8px 16px; font-size: 0.875rem; }
.login-btn.large { padding: 16px 32px; font-size: 1.125rem; }
.passkey-icon { width: 24px; height: 24px; }
.user-info { display: flex; align-items: center; gap: 12px; padding: 8px 16px; background: var(--eid-bg); border-radius: var(--eid-radius); color: var(--eid-text); cursor: pointer; }
.user-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--eid-primary); display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.875rem; }
.user-did { font-size: 0.75rem; color: var(--eid-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
.auth-level { font-size: 0.625rem; padding: 2px 6px; border-radius: 4px; }
.auth-level.elevated { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
.auth-level.standard { background: rgba(234, 179, 8, 0.2); color: #eab308; }
.dropdown { position: absolute; top: 100%; right: 0; margin-top: 8px; background: var(--eid-bg); border-radius: var(--eid-radius); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3); min-width: 200px; z-index: 100; overflow: hidden; }
.dropdown-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; color: var(--eid-text); cursor: pointer; transition: background 0.2s; }
.dropdown-item:hover { background: var(--eid-bg-hover); }
.dropdown-item.danger { color: #ef4444; }
.dropdown-divider { height: 1px; background: #334155; margin: 4px 0; }
.loading-spinner { width: 20px; height: 20px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
`;class Y extends HTMLElement{shadow;loading=!1;showDropdown=!1;capabilities=null;static get observedAttributes(){return["size","variant","label","show-user"]}constructor(){super();this.shadow=this.attachShadow({mode:"open"})}async connectedCallback(){if(this.capabilities=await m(),this.capabilities.conditionalUI)this.startConditionalAuth();this.render(),document.addEventListener("click",(i)=>{if(!this.contains(i.target))this.showDropdown=!1,this.render()})}attributeChangedCallback(){this.render()}get size(){return this.getAttribute("size")||"medium"}get variant(){return this.getAttribute("variant")||"primary"}get label(){return this.getAttribute("label")||"Sign in with Passkey"}get showUser(){return this.hasAttribute("show-user")}render(){let i=g(),t=i.isValid(),o=i.getDID(),r=i.getAuthLevel();this.shadow.innerHTML=`<style>${ti}</style><div class="login-container" style="position:relative">
${t&&this.showUser?this.renderUserInfo(o,r):this.renderLoginButton()}
${this.showDropdown?this.renderDropdown():""}
</div>`,this.attachEventListeners()}renderLoginButton(){let i=this.size==="medium"?"":this.size,t=this.variant==="primary"?"":this.variant;return`<button class="login-btn ${i} ${t}" ${this.loading?"disabled":""}>
${this.loading?'<div class="loading-spinner"></div>':ii}
<span>${this.loading?"Authenticating...":this.label}</span></button>`}renderUserInfo(i,t){let o=i.slice(0,20)+"..."+i.slice(-8),r=i.slice(8,10).toUpperCase(),n=c[t].toLowerCase();return`<div class="user-info"><div class="user-avatar">${r}</div>
<div><div class="user-did">${o}</div><span class="auth-level ${n}">${n}</span></div></div>`}renderDropdown(){return`<div class="dropdown">
<div class="dropdown-item" data-action="profile">Profile</div>
<div class="dropdown-item" data-action="recovery">Recovery Settings</div>
<div class="dropdown-item" data-action="upgrade">Upgrade Auth Level</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item danger" data-action="logout">Sign Out</div></div>`}attachEventListeners(){if(g().isValid()&&this.showUser)this.shadow.querySelector(".user-info")?.addEventListener("click",()=>{this.showDropdown=!this.showDropdown,this.render()}),this.shadow.querySelectorAll(".dropdown-item").forEach((t)=>{t.addEventListener("click",(o)=>{o.stopPropagation(),this.handleDropdownAction(t.dataset.action)})});else this.shadow.querySelector(".login-btn")?.addEventListener("click",()=>this.handleLogin())}async handleLogin(){if(this.loading)return;this.loading=!0,this.render();try{let i=await D(),t=y();if(i.prfOutput)await t.initFromPRF(i.prfOutput);let o=await t.getKeys();await g().createSession(i,o.did,{encrypt:!0,sign:!0,wallet:!1}),this.dispatchEvent(new CustomEvent("login-success",{detail:{did:o.did,credentialId:i.credentialId,prfAvailable:!!i.prfOutput},bubbles:!0}))}catch(i){if(i.name==="NotAllowedError"||i.message?.includes("No credential"))this.dispatchEvent(new CustomEvent("login-register-needed",{bubbles:!0}));else this.dispatchEvent(new CustomEvent("login-error",{detail:{error:i.message},bubbles:!0}))}finally{this.loading=!1,this.render()}}async handleDropdownAction(i){if(this.showDropdown=!1,i==="logout")g().clearSession(),y().clear(),this.dispatchEvent(new CustomEvent("logout",{bubbles:!0}));else if(i==="upgrade")try{await D(),g().upgradeAuthLevel(3),this.dispatchEvent(new CustomEvent("auth-upgraded",{detail:{level:3},bubbles:!0}))}catch{}else this.dispatchEvent(new CustomEvent("navigate",{detail:{path:`/${i}`},bubbles:!0}));this.render()}async startConditionalAuth(){try{let i=await X();if(i){let t=y();if(i.prfOutput)await t.initFromPRF(i.prfOutput);let o=await t.getKeys();await g().createSession(i,o.did,{encrypt:!0,sign:!0,wallet:!1}),this.dispatchEvent(new CustomEvent("login-success",{detail:{did:o.did,credentialId:i.credentialId,viaConditionalUI:!0},bubbles:!0})),this.render()}}catch{}}async register(i,t){this.loading=!0,this.render();try{let o=await Q(i,t);this.dispatchEvent(new CustomEvent("register-success",{detail:{credentialId:o.credentialId,prfSupported:o.prfSupported},bubbles:!0})),await this.handleLogin()}catch(o){this.dispatchEvent(new CustomEvent("register-error",{detail:{error:o.message},bubbles:!0}))}finally{this.loading=!1,this.render()}}}customElements.define("encryptid-login",Y);class S{config=null;activeRequest=null;constructor(){this.loadConfig()}async initializeRecovery(i=3){return this.config={threshold:i,delaySeconds:172800,guardians:[],guardianListHash:"",updatedAt:Date.now()},await this.saveConfig(),this.config}async addGuardian(i){if(!this.config)throw Error("Recovery not initialized");if(this.config.guardians.length>=7)throw Error("Maximum of 7 guardians allowed");let t={...i,id:a(crypto.getRandomValues(new Uint8Array(16)).buffer),addedAt:Date.now()};return this.config.guardians.push(t),this.config.guardianListHash=await this.hashGuardianList(),this.config.updatedAt=Date.now(),await this.saveConfig(),t}async removeGuardian(i){if(!this.config)throw Error("Recovery not initialized");let t=this.config.guardians.findIndex((r)=>r.id===i);if(t===-1)throw Error("Guardian not found");if(this.config.guardians.filter((r)=>r.id!==i).reduce((r,n)=>r+n.weight,0)<this.config.threshold)throw Error("Cannot remove guardian: would make recovery impossible");this.config.guardians.splice(t,1),this.config.guardianListHash=await this.hashGuardianList(),this.config.updatedAt=Date.now(),await this.saveConfig()}async setThreshold(i){if(!this.config)throw Error("Recovery not initialized");let t=this.config.guardians.reduce((o,r)=>o+r.weight,0);if(i>t)throw Error("Threshold cannot exceed total guardian weight");if(i<1)throw Error("Threshold must be at least 1");this.config.threshold=i,this.config.updatedAt=Date.now(),await this.saveConfig()}async setDelay(i){if(!this.config)throw Error("Recovery not initialized");if(i<3600||i>604800)throw Error("Delay must be between 1 hour and 7 days");this.config.delaySeconds=i,this.config.updatedAt=Date.now(),await this.saveConfig()}getConfig(){return this.config}isConfigured(){if(!this.config)return!1;return this.config.guardians.reduce((i,t)=>i+t.weight,0)>=this.config.threshold}async verifyGuardian(i){if(!this.config)throw Error("Recovery not initialized");let t=this.config.guardians.find((o)=>o.id===i);if(!t)throw Error("Guardian not found");return t.lastVerified=Date.now(),await this.saveConfig(),!0}async initiateRecovery(i){if(!this.config)throw Error("Recovery not configured");if(this.activeRequest?.status==="pending")throw Error("Recovery already in progress");let t=Date.now();return this.activeRequest={id:a(crypto.getRandomValues(new Uint8Array(16)).buffer),accountDID:"",newCredentialId:i,initiatedAt:t,completesAt:t+this.config.delaySeconds*1000,status:"pending",approvals:[],approvalWeight:0},this.activeRequest}async approveRecovery(i,t){if(!this.activeRequest||this.activeRequest.status!=="pending")throw Error("No pending recovery request");if(!this.config)throw Error("Recovery not configured");let o=this.config.guardians.find((r)=>r.id===i);if(!o)throw Error("Guardian not found");if(this.activeRequest.approvals.some((r)=>r.guardianId===i))throw Error("Guardian already approved");if(this.activeRequest.approvals.push({guardianId:i,approvedAt:Date.now(),signature:t}),this.activeRequest.approvalWeight+=o.weight,this.activeRequest.approvalWeight>=this.config.threshold)this.activeRequest.status="approved";return this.activeRequest}async cancelRecovery(){if(!this.activeRequest||this.activeRequest.status!=="pending")throw Error("No pending recovery request to cancel");this.activeRequest.status="cancelled",this.activeRequest=null}async completeRecovery(){if(!this.activeRequest)throw Error("No recovery request");if(this.activeRequest.status!=="approved")throw Error("Recovery not approved");if(Date.now()<this.activeRequest.completesAt){let i=this.activeRequest.completesAt-Date.now();throw Error(`Time-lock not expired. ${Math.ceil(i/1000/60)} minutes remaining.`)}this.activeRequest.status="completed",this.activeRequest=null}getActiveRequest(){return this.activeRequest}async hashGuardianList(){if(!this.config)return"";let i=this.config.guardians.map((o)=>o.id).sort().join(","),t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(i));return a(t)}async saveConfig(){if(!this.config)return;try{localStorage.setItem("encryptid_recovery",JSON.stringify(this.config))}catch{}}loadConfig(){try{let i=localStorage.getItem("encryptid_recovery");if(i)this.config=JSON.parse(i)}catch{}}}var k=null;function G(){if(!k)k=new S;return k}function N(i){switch(i){case"secondary_passkey":return{name:"Backup Passkey",description:"Another device you own (phone, YubiKey, etc.)",icon:"key",setupInstructions:"Register a passkey on a second device you control."};case"trusted_contact":return{name:"Trusted Contact",description:"A friend or family member with their own EncryptID",icon:"user",setupInstructions:"Ask a trusted person to create an EncryptID account."};case"hardware_key":return{name:"Hardware Security Key",description:"A YubiKey or similar device stored offline",icon:"shield",setupInstructions:"Register a hardware security key and store it safely."};case"institutional":return{name:"Recovery Service",description:"A professional recovery service provider",icon:"building",setupInstructions:"Connect with a trusted recovery service."};case"time_delayed_self":return{name:"Time-Delayed Self",description:"Recover yourself after a waiting period",icon:"clock",setupInstructions:"Set up a recovery option that requires waiting before completing."};default:return{name:"Unknown",description:"Unknown guardian type",icon:"question",setupInstructions:""}}}class I extends HTMLElement{shadow;constructor(){super();this.shadow=this.attachShadow({mode:"open"})}connectedCallback(){let i=G();if(!i.getConfig())i.initializeRecovery(3).then(()=>this.render());else this.render()}render(){let t=G().getConfig(),o=t?.guardians??[],r=t?.threshold??3,n=o.reduce((e,d)=>e+d.weight,0),s=n>=r;this.shadow.innerHTML=`
<style>
:host { display: block; font-family: system-ui, sans-serif; color: #f1f5f9; }
.setup { background: #0f172a; border-radius: 8px; padding: 24px; max-width: 600px; }
h2 { margin: 0 0 8px; font-size: 1.5rem; }
.subtitle { color: #94a3b8; font-size: 0.875rem; margin: 0 0 24px; }
.status { display: flex; align-items: center; gap: 16px; padding: 16px; background: #1e293b; border-radius: 8px; margin-bottom: 24px; }
.dot { width: 12px; height: 12px; border-radius: 50%; background: ${s?"#22c55e":"#eab308"}; }
.guardian { display: flex; align-items: center; gap: 16px; padding: 16px; background: #1e293b; border-radius: 8px; margin-bottom: 12px; border: 1px solid #475569; }
.icon { width: 48px; height: 48px; border-radius: 50%; background: #334155; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; }
.info { flex: 1; }
.name { font-weight: 500; }
.type { font-size: 0.75rem; color: #94a3b8; }
</style>
<div class="setup">
<h2>Social Recovery</h2>
<p class="subtitle">Set up guardians to recover your account without seed phrases</p>
<div class="status">
<div class="dot"></div>
<div>
<div>${s?"Recovery Configured":"Setup Incomplete"}</div>
<div style="font-size:0.75rem;color:#94a3b8">${n}/${r} guardians</div>
</div>
</div>
${o.map((e)=>{let d=N(e.type);return`<div class="guardian"><div class="icon">${d.icon==="key"?"\uD83D\uDD11":d.icon==="user"?"\uD83D\uDC64":d.icon==="shield"?"\uD83D\uDEE1":d.icon==="building"?"\uD83C\uDFE2":"⏰"}</div><div class="info"><div class="name">${e.name}</div><div class="type">${d.name}</div></div></div>`}).join("")}
</div>
`}}customElements.define("encryptid-guardian-setup",I);var w="encryptid_token",f="encryptid_user",oi="https://encryptid.jeffemmett.com",h=new z(oi);async function U(){let i=await h.authenticate(),t={did:i.did,username:i.username,token:i.token};return localStorage.setItem(w,i.token),localStorage.setItem(f,JSON.stringify(t)),t}async function B(i,t){let o=await h.register(i,t),r={did:o.did,username:i,token:o.token};return localStorage.setItem(w,o.token),localStorage.setItem(f,JSON.stringify(r)),r}function P(){localStorage.removeItem(w),localStorage.removeItem(f),localStorage.removeItem("encryptid_session")}function O(){return!!localStorage.getItem(w)}function W(){let i=localStorage.getItem(f);if(!i)return null;try{return JSON.parse(i)}catch{return null}}function H(){return localStorage.getItem(w)}function ri(i){if(O())return!0;let t=i||window.location.href;return window.location.href=`/?login=required&return=${encodeURIComponent(t)}`,!1}async function ni(i){let t=H();if(!t)throw Error("Not authenticated");await h.setRecoveryEmail(t,i)}async function si(i){await h.requestEmailRecovery(i)}function ei(i){let t=document.getElementById(i);if(!t){console.warn(`EncryptID: Container #${i} not found`);return}function o(){let r=W();if(r)t.innerHTML=`
<div style="display:flex;align-items:center;gap:12px;padding:8px 16px;background:rgba(6,182,212,0.1);border-radius:8px;font-family:system-ui,sans-serif;">
<span style="color:#06b6d4;font-weight:500;">${r.username||r.did.slice(0,16)+"..."}</span>
<button id="eid-signout" style="background:none;border:1px solid #64748b;color:#94a3b8;padding:4px 12px;border-radius:6px;cursor:pointer;font-size:0.875rem;">Sign Out</button>
</div>`,document.getElementById("eid-signout")?.addEventListener("click",()=>{P(),o(),t.dispatchEvent(new CustomEvent("encryptid-logout",{bubbles:!0}))});else t.innerHTML=`
<button id="eid-signin" style="display:flex;align-items:center;gap:8px;padding:10px 20px;background:#06b6d4;color:white;border:none;border-radius:8px;cursor:pointer;font-weight:500;font-family:system-ui,sans-serif;font-size:0.95rem;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:20px;height:20px;">
<circle cx="12" cy="10" r="3"/><path d="M12 13v8"/><path d="M9 18h6"/><circle cx="12" cy="10" r="7"/>
</svg>
Sign in with Passkey
</button>`,document.getElementById("eid-signin")?.addEventListener("click",async()=>{try{let n=await U();o(),t.dispatchEvent(new CustomEvent("encryptid-login",{bubbles:!0,detail:n}))}catch(n){if(n.name==="NotAllowedError"||n.message?.includes("No credential")){let s=prompt("No passkey found. Register with a username:");if(s)try{let e=await B(s);o(),t.dispatchEvent(new CustomEvent("encryptid-login",{bubbles:!0,detail:e}))}catch(e){alert(`Registration failed: ${e.message}`)}}else console.error("EncryptID auth error:",n)}})}o()}async function ai(){let i=H();if(!i)return!1;try{if((await h.verifySession(i)).valid)return!0;try{let o=await h.refreshToken(i);localStorage.setItem(w,o.token);let r=W();if(r)r.token=o.token,localStorage.setItem(f,JSON.stringify(r));return!0}catch{return P(),!1}}catch{return!1}}var _={client:h,authenticate:U,register:B,logout:P,isAuthenticated:O,getUser:W,getToken:H,requireAuth:ri,setRecoveryEmail:ni,requestRecovery:si,renderAuthButton:ei,verifySession:ai,detectCapabilities:m,AuthLevel:c,VERSION:"0.1.0"};if(typeof window<"u")window.EncryptID=_;var Xi=_;export{Xi as default};