@openkey/sdk 0.8.0 → 0.8.2

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/dist/index.js CHANGED
@@ -1,20 +1,139 @@
1
- "use strict";var P=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var S=Object.prototype.hasOwnProperty;var R=(h,e)=>{for(var t in e)P(h,t,{get:e[t],enumerable:!0})},N=(h,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of M(e))!S.call(h,r)&&r!==t&&P(h,r,{get:()=>e[r],enumerable:!(s=I(e,r))||s.enumerable});return h};var D=h=>N(P({},"__esModule",{value:!0}),h);var q={};R(q,{OpenKey:()=>w,OpenKeyEIP1193Provider:()=>_,OpenKeyProvider:()=>T,default:()=>K,getOpenKey:()=>$});module.exports=D(q);var y=require("@openkey/core"),L="https://openkey.so",k=400,v=600,b=300*1e3,U=3e3,E="openkey_oauth",O=class{root;shadow;iframe;onClose;onMessage;messageHandler;host;constructor(e){this.host=e.host,this.onClose=e.onClose,this.onMessage=e.onMessage;let t=window.matchMedia("(max-width: 639px)").matches;this.root=document.createElement("div"),this.shadow=this.root.attachShadow({mode:"closed"});let s=document.createElement("style");s.textContent=`
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ OpenKey: () => OpenKey,
24
+ OpenKeyProvider: () => OpenKeyProvider,
25
+ default: () => index_default,
26
+ getOpenKey: () => getOpenKey
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var import_core = require("@openkey/core");
30
+ var DEFAULT_HOST = "https://openkey.so";
31
+ var POPUP_WIDTH = 400;
32
+ var POPUP_HEIGHT = 600;
33
+ var DEFAULT_TIMEOUT = 5 * 60 * 1e3;
34
+ var IFRAME_READY_TIMEOUT = 3e3;
35
+ var OAUTH_STORAGE_KEY = "openkey_oauth";
36
+ var IframeModal = class {
37
+ root;
38
+ shadow;
39
+ iframe;
40
+ onClose;
41
+ onMessage;
42
+ messageHandler;
43
+ host;
44
+ constructor(opts) {
45
+ this.host = opts.host;
46
+ this.onClose = opts.onClose;
47
+ this.onMessage = opts.onMessage;
48
+ const isMobile = window.matchMedia("(max-width: 639px)").matches;
49
+ this.root = document.createElement("div");
50
+ this.shadow = this.root.attachShadow({ mode: "closed" });
51
+ const style = document.createElement("style");
52
+ style.textContent = `
2
53
  :host{all:initial}
3
- .ok-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.4);backdrop-filter:blur(4px);z-index:999999;display:flex;align-items:${t?"flex-end":"center"};justify-content:center;animation:ok-fade-in 150ms ease-out}
4
- .ok-card{position:relative;background:#fafafa;width:${t?"100%":"400px"};border-radius:${t?"16px 16px 0 0":"16px"};box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);overflow:hidden;overflow-y:auto;animation:${t?"ok-slide-up 250ms ease-out":"ok-scale-in 200ms ease-out"};max-height:90vh}
54
+ .ok-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.4);backdrop-filter:blur(4px);z-index:999999;display:flex;align-items:${isMobile ? "flex-end" : "center"};justify-content:center;animation:ok-fade-in 150ms ease-out}
55
+ .ok-card{position:relative;background:#fafafa;width:${isMobile ? "100%" : "400px"};border-radius:${isMobile ? "16px 16px 0 0" : "16px"};box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);overflow:hidden;overflow-y:auto;animation:${isMobile ? "ok-slide-up 250ms ease-out" : "ok-scale-in 200ms ease-out"};max-height:90vh}
5
56
  .ok-close{position:absolute;top:8px;right:8px;width:24px;height:24px;border:none;background:transparent;color:#6b7280;font-size:18px;cursor:pointer;z-index:1;display:flex;align-items:center;justify-content:center;border-radius:4px}
6
57
  .ok-close:hover{color:#111827}
7
58
  .ok-handle{width:40px;height:4px;background:#d4d4d4;border-radius:2px;margin:8px auto 0}
8
59
  iframe{border:none;width:100%;height:400px;display:block;transition:height 200ms ease}
9
60
  .ok-backdrop.ok-exit{animation:ok-fade-out 150ms ease-in}
10
- .ok-backdrop.ok-exit .ok-card{animation:${t?"ok-slide-down 200ms ease-in":"ok-scale-out 150ms ease-in"}}
61
+ .ok-backdrop.ok-exit .ok-card{animation:${isMobile ? "ok-slide-down 200ms ease-in" : "ok-scale-out 150ms ease-in"}}
11
62
  @keyframes ok-fade-in{from{opacity:0}to{opacity:1}}
12
63
  @keyframes ok-fade-out{from{opacity:1}to{opacity:0}}
13
64
  @keyframes ok-scale-in{from{opacity:0;transform:scale(0.95)}to{opacity:1;transform:scale(1)}}
14
65
  @keyframes ok-scale-out{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(0.95)}}
15
66
  @keyframes ok-slide-up{from{transform:translateY(100%)}to{transform:translateY(0)}}
16
67
  @keyframes ok-slide-down{from{transform:translateY(0)}to{transform:translateY(100%)}}
17
- `;let r=document.createElement("div");r.className="ok-backdrop",r.addEventListener("click",i=>{i.target===r&&this.close()});let o=document.createElement("div");if(o.className="ok-card",t){let i=document.createElement("div");i.className="ok-handle",o.appendChild(i)}let a=document.createElement("button");a.className="ok-close",a.textContent="\xD7",a.addEventListener("click",()=>this.close()),o.appendChild(a),this.iframe=document.createElement("iframe"),this.iframe.src=e.url,this.iframe.setAttribute("allow","publickey-credentials-get *; publickey-credentials-create *"),this.iframe.setAttribute("sandbox","allow-scripts allow-same-origin allow-forms allow-popups"),o.appendChild(this.iframe),r.appendChild(o),this.shadow.appendChild(s),this.shadow.appendChild(r),this.messageHandler=i=>{if(i.origin!==this.host||i.source!==this.iframe.contentWindow)return;let c=i.data;if(c.type==="openkey:resize"){let l=Math.min(c.height,Math.floor(window.innerHeight*.85));this.iframe.style.height=`${l}px`;return}this.onMessage(c)},window.addEventListener("message",this.messageHandler),document.body.appendChild(this.root)}postMessage(e){this.iframe.contentWindow?.postMessage(e,this.host)}destroy(){window.removeEventListener("message",this.messageHandler),this.root.remove()}close(){let e=this.shadow.querySelector(".ok-backdrop");e?(e.classList.add("ok-exit"),setTimeout(()=>{this.destroy(),this.onClose()},200)):(this.destroy(),this.onClose())}},x=class{root;shadow;constructor(e){this.root=document.createElement("div"),this.shadow=this.root.attachShadow({mode:"closed"});let t=document.createElement("style");t.textContent=`
68
+ `;
69
+ const backdrop = document.createElement("div");
70
+ backdrop.className = "ok-backdrop";
71
+ backdrop.addEventListener("click", (e) => {
72
+ if (e.target === backdrop) this.close();
73
+ });
74
+ const card = document.createElement("div");
75
+ card.className = "ok-card";
76
+ if (isMobile) {
77
+ const handle = document.createElement("div");
78
+ handle.className = "ok-handle";
79
+ card.appendChild(handle);
80
+ }
81
+ const closeBtn = document.createElement("button");
82
+ closeBtn.className = "ok-close";
83
+ closeBtn.textContent = "\xD7";
84
+ closeBtn.addEventListener("click", () => this.close());
85
+ card.appendChild(closeBtn);
86
+ this.iframe = document.createElement("iframe");
87
+ this.iframe.src = opts.url;
88
+ this.iframe.setAttribute("allow", "publickey-credentials-get *; publickey-credentials-create *");
89
+ this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups");
90
+ card.appendChild(this.iframe);
91
+ backdrop.appendChild(card);
92
+ this.shadow.appendChild(style);
93
+ this.shadow.appendChild(backdrop);
94
+ this.messageHandler = (event) => {
95
+ if (event.origin !== this.host) return;
96
+ if (event.source !== this.iframe.contentWindow) return;
97
+ const data = event.data;
98
+ if (data.type === "openkey:resize") {
99
+ const h = Math.min(data.height, Math.floor(window.innerHeight * 0.85));
100
+ this.iframe.style.height = `${h}px`;
101
+ return;
102
+ }
103
+ this.onMessage(data);
104
+ };
105
+ window.addEventListener("message", this.messageHandler);
106
+ document.body.appendChild(this.root);
107
+ }
108
+ postMessage(message) {
109
+ this.iframe.contentWindow?.postMessage(message, this.host);
110
+ }
111
+ destroy() {
112
+ window.removeEventListener("message", this.messageHandler);
113
+ this.root.remove();
114
+ }
115
+ close() {
116
+ const backdrop = this.shadow.querySelector(".ok-backdrop");
117
+ if (backdrop) {
118
+ backdrop.classList.add("ok-exit");
119
+ setTimeout(() => {
120
+ this.destroy();
121
+ this.onClose();
122
+ }, 200);
123
+ } else {
124
+ this.destroy();
125
+ this.onClose();
126
+ }
127
+ }
128
+ };
129
+ var WalletPicker = class {
130
+ root;
131
+ shadow;
132
+ constructor(opts) {
133
+ this.root = document.createElement("div");
134
+ this.shadow = this.root.attachShadow({ mode: "closed" });
135
+ const style = document.createElement("style");
136
+ style.textContent = `
18
137
  :host{all:initial}
19
138
  .wp-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.4);backdrop-filter:blur(4px);z-index:1000000;display:flex;align-items:center;justify-content:center;animation:wp-fade 150ms ease-out}
20
139
  .wp-card{background:#fff;width:340px;border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);padding:20px;animation:wp-scale 200ms ease-out}
@@ -37,5 +156,771 @@
37
156
  .wp-cancel{color:#9ca3af}
38
157
  .wp-cancel:hover{background:#262626;color:#f3f4f6}
39
158
  }
40
- `;let s=document.createElement("div");s.className="wp-backdrop",s.addEventListener("click",i=>{i.target===s&&(this.destroy(),e.onCancel())});let r=document.createElement("div");r.className="wp-card";let o=document.createElement("h2");if(o.className="wp-title",o.textContent="Select a wallet",r.appendChild(o),e.providers.length===0){let i=document.createElement("div");i.className="wp-empty",i.textContent="No wallets detected",r.appendChild(i)}else{let i=document.createElement("ul");i.className="wp-list";for(let{info:c,provider:l}of e.providers){let p=document.createElement("button");if(p.className="wp-item",c.icon){let n=document.createElement("img");n.className="wp-icon",n.src=c.icon,n.alt=c.name,p.appendChild(n)}let u=document.createElement("span");u.textContent=c.name,p.appendChild(u),p.addEventListener("click",()=>{this.destroy(),e.onSelect(l)}),i.appendChild(p)}r.appendChild(i)}let a=document.createElement("button");a.className="wp-cancel",a.textContent="Cancel",a.addEventListener("click",()=>{this.destroy(),e.onCancel()}),r.appendChild(a),s.appendChild(r),this.shadow.appendChild(t),this.shadow.appendChild(s),document.body.appendChild(this.root)}destroy(){this.root.remove()}};function g(h="Opening in new window\u2026",e="info"){let t=e==="error"?"#dc2626":"#1f2937",s=document.createElement("div"),r=s.attachShadow({mode:"closed"});r.innerHTML=`<style>:host{all:initial}.ok-toast{position:fixed;top:16px;right:16px;z-index:1000000;background:${t};color:#f9fafb;padding:10px 16px;border-radius:8px;font:14px/1.4 system-ui,sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.15);animation:ok-t-in 200ms ease-out;max-width:360px}@keyframes ok-t-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}</style><div class="ok-toast">${h}</div>`,document.body.appendChild(s),setTimeout(()=>s.remove(),4e3)}var A=class extends Error{constructor(e){super(e),this.name="ExternalWalletError"}},w=class{host;oauthHost;appName;mode;config;popup=null;lastAuth=null;discoveredProviders=[];sessionToken=null;constructor(e={}){this.config=e,this.host=e.host||L,this.oauthHost=e.oauthHost||this.deriveOAuthHost(this.host),this.appName=e.appName||window.location.hostname,this.mode=e.mode??"iframe",typeof window<"u"&&(this.discoveredProviders=[],window.addEventListener("eip6963:announceProvider",t=>{this.discoveredProviders.push(t.detail)}),window.dispatchEvent(new Event("eip6963:requestProvider")))}deriveOAuthHost(e){try{let t=new URL(e);return t.hostname==="localhost"||t.hostname==="127.0.0.1"?e.replace(/\/$/,""):(t.hostname="api."+t.hostname,t.toString().replace(/\/$/,""))}catch{return"https://api.openkey.so"}}async connect(e){let t=await this.openFlow("connect",{type:"openkey:auth:request",appName:this.appName},e?.mode);return this.lastAuth=t,t}async linkWallet(e){return this.openFlow("link-wallet",{type:"openkey:link-wallet:request"},e?.mode)}async signMessage(e,t){return this.lastAuth?.keyType==="EXTERNAL"?this.signWithExternalWallet(e):this.signWithOpenKey(e,t?.mode)}async signTypedData(e,t){return this.lastAuth?.keyType==="EXTERNAL"?this.signTypedDataWithExternalWallet(e):this.signTypedDataWithOpenKey(e,t?.mode)}async findWalletProvider(e){if(this.config.externalProvider)return this.config.externalProvider;for(let{provider:t}of this.discoveredProviders)try{if((await t.request({method:"eth_accounts"})).some(r=>r.toLowerCase()===e.toLowerCase()))return t}catch{}if(typeof window<"u"&&window.ethereum){let t=window.ethereum;try{if((await t.request({method:"eth_accounts"})).some(r=>r.toLowerCase()===e.toLowerCase()))return t}catch{}}throw new A("External wallet not found. Connect the wallet that owns this key and try again.")}async signWithExternalWallet(e){let t;try{t=await this.findWalletProvider(this.lastAuth.address)}catch(s){throw g(s.message,"error"),s}try{let s=this.toHex(e.message);return{signature:await t.request({method:"personal_sign",params:[s,this.lastAuth.address]}),address:this.lastAuth.address}}catch(s){let r=s?.message?.includes("not been authorized")?"Wallet rejected the request. Approve the connection in your wallet and try again.":s?.message||"Signing failed";throw g(r,"error"),new Error(r)}}async signTypedDataWithExternalWallet(e){let t;try{t=await this.findWalletProvider(this.lastAuth.address)}catch(s){throw g(s.message,"error"),s}try{return{signature:await t.request({method:"eth_signTypedData_v4",params:[this.lastAuth.address,JSON.stringify(e)]}),address:this.lastAuth.address}}catch(s){let r=s?.message?.includes("not been authorized")?"Wallet rejected the request. Approve the connection in your wallet and try again.":s?.message||"Signing failed";throw g(r,"error"),new Error(r)}}signWithOpenKey(e,t){return this.openFlow("sign",{type:"openkey:sign:request",message:e.message,keyId:e.keyId,sessionToken:this.sessionToken||void 0},t)}signTypedDataWithOpenKey(e,t){return this.openFlow("sign-typed-data",{type:"openkey:signTypedData:request",data:e,sessionToken:this.sessionToken||void 0},t)}toHex(e){return"0x"+Array.from(new TextEncoder().encode(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}async isConnected(){try{let e=await fetch(`${this.host}/api/auth/session`,{credentials:"include"});return e.ok?!!(await e.json()).user:!1}catch{return!1}}oauth={connect:async e=>{let t=(0,y.generateCodeVerifier)(),s=await(0,y.generateCodeChallenge)(t),r=e.state||(0,y.generateState)();sessionStorage.setItem(E,JSON.stringify({verifier:t,state:r}));let o=(0,y.buildAuthorizationUrl)({host:this.oauthHost,clientId:e.clientId,redirectUri:e.redirectUri,codeChallenge:s,state:r,scopes:["openid"]});return this.openOAuthFlow(o,r,e.redirectUri)},exchangeCode:async(e,t)=>{let s=sessionStorage.getItem(E);if(!s)throw new Error("No PKCE verifier found. Start a new OAuth flow.");let{verifier:r}=JSON.parse(s),o=await fetch(`${this.oauthHost}/api/auth/oauth2/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:t.redirectUri,client_id:t.clientId,code_verifier:r})});if(sessionStorage.removeItem(E),!o.ok){let a=await o.json();throw new Error(a.error_description||"Token exchange failed")}return o.json()},verifyState:e=>{let t=sessionStorage.getItem(E);if(!t)return!1;let{state:s}=JSON.parse(t);return s===e},parseCallback:e=>{let t=new URL(e||window.location.href),s=t.searchParams.get("error");if(s)return{error:s,errorDescription:t.searchParams.get("error_description")||void 0};let r=t.searchParams.get("code"),o=t.searchParams.get("state");return!r||!o?{error:"missing_params",errorDescription:"Missing code or state parameter"}:{code:r,state:o}}};openOAuthFlow(e,t,s){return new Promise((r,o)=>{this.mode!=="redirect"?this.openOAuthPopup(e,t,s,r,o):window.location.href=e})}openOAuthPopup(e,t,s,r,o){let a=window.screenX+(window.outerWidth-k)/2,i=window.screenY+(window.outerHeight-v)/2;if(this.popup=window.open(e,"openkey-oauth",`width=${k},height=${v},left=${a},top=${i},popup=true`),!this.popup){g("Popup was blocked. Please allow popups for this site.","error"),o({code:"POPUP_BLOCKED",message:"Popup was blocked. Please allow popups or use redirect mode."});return}let c=new URL(s).origin,l=setInterval(()=>{try{if(this.popup?.closed){clearInterval(l),clearTimeout(p),o({code:"USER_CANCELLED",message:"User closed the popup"});return}let u=this.popup?.location?.href;if(u&&u.startsWith(c)){clearInterval(l),clearTimeout(p);let n=new URL(u),f=n.searchParams.get("code"),d=n.searchParams.get("state"),m=n.searchParams.get("error");if(this.popup?.close(),m){o({code:"UNAUTHORIZED",message:n.searchParams.get("error_description")||m});return}if(d!==t){o({code:"STATE_MISMATCH",message:"State mismatch - possible CSRF attack"});return}f?r({code:f,state:d}):o({code:"UNAUTHORIZED",message:"Authorization failed - no code returned"})}}catch{}},100),p=setTimeout(()=>{clearInterval(l),this.popup?.close(),o({code:"TIMEOUT",message:"OAuth flow timed out"})},b)}resolveMode(e){return e??this.mode}hasDetectedEoa(){return!!(this.discoveredProviders.length>0||typeof window<"u"&&window.ethereum)}async openFlow(e,t,s){let r=this.resolveMode(s),o=encodeURIComponent(window.location.origin),a=e==="connect"&&this.hasDetectedEoa()?"&hasEoa=true":"";if(r==="popup"){let c=`${this.host}/widget/${e}?origin=${o}${a}`;return new Promise((l,p)=>this.openPopup(c,t,l,p))}if(r==="redirect"){let c=`${this.host}/widget/${e}?origin=${o}${a}`;return window.location.href=c,new Promise(()=>{})}let i=`${this.host}/widget/embed/${e}?origin=${o}${a}`;return this.openIframeModal(i,e,t,o)}openIframeModal(e,t,s,r){return new Promise((o,a)=>{let i=!1,c=!1,l=null,p=d=>{c||(c=!0,d())},u=()=>{clearTimeout(n),clearTimeout(f),l?.destroy(),l=null};l=new O({url:e,host:this.host,onClose:()=>{p(()=>{clearTimeout(n),clearTimeout(f),l=null,a({code:"USER_CANCELLED",message:"User cancelled the request"})})},onMessage:d=>{if(d.type==="openkey:ready"){i=!0,l?.postMessage(s);return}if(d.type==="openkey:close"){p(()=>{u(),a({code:"USER_CANCELLED",message:"User cancelled the request"})});return}if(d.type==="openkey:link-wallet:delegate"){this.handleWalletLinkDelegation(l);return}if(d.type==="openkey:auth:use-external-wallet"){u(),this.handleExternalWalletConnect(o,a);return}(d.type==="openkey:auth:response"||d.type==="openkey:sign:response"||d.type==="openkey:signTypedData:response"||d.type==="openkey:link-wallet:response")&&p(()=>{u(),d.success?d.type==="openkey:auth:response"?(d.sessionToken&&(this.sessionToken=d.sessionToken),o({address:d.address,keyId:d.keyId,keyType:d.keyType||"MANAGED"})):d.type==="openkey:link-wallet:response"?o({address:d.address,keyId:d.keyId}):o({signature:d.signature,address:d.address}):a(d.error)})}});let n=setTimeout(()=>{if(i||c)return;u(),console.warn("OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP."),g();let d=`${this.host}/widget/${t}?origin=${r}`;this.openPopup(d,s,m=>p(()=>o(m)),m=>p(()=>a(m)))},U),f=setTimeout(()=>{p(()=>{u(),a({code:"TIMEOUT",message:"Request timed out"})})},b)})}handleWalletLinkDelegation(e){let t=s=>e.postMessage(s);new x({providers:this.discoveredProviders,onCancel:()=>{t({type:"openkey:link-wallet:result",success:!1,error:{code:"USER_CANCELLED",message:"Wallet linking cancelled"}})},onSelect:async s=>{try{let o=(await s.request({method:"eth_requestAccounts"}))[0];if(!o)throw new Error("No account returned");let a=await fetch(`${this.host}/api/keys/link/challenge`,{method:"POST",credentials:"include"});if(!a.ok)throw new Error("Failed to get challenge");let{message:i}=await a.json(),c=this.toHex(i),l=await s.request({method:"personal_sign",params:[c,o]}),p=await fetch(`${this.host}/api/keys/link`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"include",body:JSON.stringify({address:o,signature:l,message:i})});if(!p.ok){let n=await p.json();throw new Error(n.error||"Failed to link wallet")}let{key:u}=await p.json();t({type:"openkey:link-wallet:result",success:!0,address:u.address,keyId:u.id})}catch(r){t({type:"openkey:link-wallet:result",success:!1,error:{code:"UNKNOWN",message:r.message||"Wallet linking failed"}})}}})}handleExternalWalletConnect(e,t){new x({providers:this.discoveredProviders,onCancel:()=>{t({code:"USER_CANCELLED",message:"User cancelled wallet selection"})},onSelect:async s=>{try{let o=(await s.request({method:"eth_requestAccounts"}))[0];if(!o)throw new Error("No account returned");let a={address:o,keyId:`external:${o}`,keyType:"EXTERNAL"};this.lastAuth=a,e(a)}catch(r){t({code:"UNKNOWN",message:r.message||"Failed to connect wallet"})}}})}openPopup(e,t,s,r){let o=window.screenX+(window.outerWidth-k)/2,a=window.screenY+(window.outerHeight-v)/2;if(this.popup=window.open(e,"openkey",`width=${k},height=${v},left=${o},top=${a},popup=true`),!this.popup){g("Popup was blocked. Please allow popups for this site.","error"),r({code:"POPUP_BLOCKED",message:"Popup was blocked. Please allow popups for this site."});return}let i=()=>{window.removeEventListener("message",p),clearInterval(c),clearTimeout(l)},c=setInterval(()=>{this.popup?.closed&&(i(),r({code:"USER_CANCELLED",message:"User closed the popup"}))},500),l=setTimeout(()=>{i(),this.popup?.close(),r({code:"TIMEOUT",message:"Request timed out"})},b),p=u=>{if(u.origin!==this.host||u.source!==this.popup)return;let n=u.data;if(n.type==="openkey:ready"){this.popup?.postMessage(t,this.host);return}if(n.type==="openkey:close"){i(),this.popup?.close(),r({code:"USER_CANCELLED",message:"User cancelled the request"});return}if(n.type==="openkey:auth:use-external-wallet"){i(),this.popup?.close(),this.handleExternalWalletConnect(s,r);return}(n.type==="openkey:auth:response"||n.type==="openkey:sign:response"||n.type==="openkey:signTypedData:response"||n.type==="openkey:link-wallet:response")&&(i(),this.popup?.close(),n.success?n.type==="openkey:auth:response"?(n.sessionToken&&(this.sessionToken=n.sessionToken),s({address:n.address,keyId:n.keyId,keyType:n.keyType||"MANAGED"})):n.type==="openkey:link-wallet:response"?s({address:n.address,keyId:n.keyId}):s({signature:n.signature,address:n.address}):r(n.error))};window.addEventListener("message",p)}disconnect(){this.popup?.close(),this.popup=null}},T=class{openkey;address;keyId;keyType;constructor(e,t){this.openkey=e,this.address=t.address,this.keyId=t.keyId,this.keyType=t.keyType}async request({method:e,params:t}){switch(e){case"eth_accounts":case"eth_requestAccounts":return[this.address];case"eth_chainId":return"0x1";case"personal_sign":{if(this.keyType==="EXTERNAL")try{return await(await this.openkey.findWalletProvider(this.address)).request({method:e,params:t})}catch(o){throw g(o?.message||"External wallet signing failed","error"),o}let s=this.hexToString(t[0]);return(await this.openkey.signMessage({message:s,keyId:this.keyId})).signature}case"eth_signTypedData_v4":{if(this.keyType==="EXTERNAL")try{return await(await this.openkey.findWalletProvider(this.address)).request({method:e,params:t})}catch(o){throw g(o?.message||"External wallet signing failed","error"),o}let s=JSON.parse(t[1]);return(await this.openkey.signTypedData(s)).signature}default:throw new Error(`Unsupported method: ${e}`)}}hexToString(e){let t=new Uint8Array((e.startsWith("0x")?e.slice(2):e).match(/.{1,2}/g).map(s=>parseInt(s,16)));return new TextDecoder().decode(t)}},_=T,K=w,C=null;function $(h){return(!C||h)&&(C=new w(h)),C}0&&(module.exports={OpenKey,OpenKeyEIP1193Provider,OpenKeyProvider,getOpenKey});
159
+ `;
160
+ const backdrop = document.createElement("div");
161
+ backdrop.className = "wp-backdrop";
162
+ backdrop.addEventListener("click", (e) => {
163
+ if (e.target === backdrop) {
164
+ this.destroy();
165
+ opts.onCancel();
166
+ }
167
+ });
168
+ const card = document.createElement("div");
169
+ card.className = "wp-card";
170
+ const title = document.createElement("h2");
171
+ title.className = "wp-title";
172
+ title.textContent = "Select a wallet";
173
+ card.appendChild(title);
174
+ if (opts.providers.length === 0) {
175
+ const empty = document.createElement("div");
176
+ empty.className = "wp-empty";
177
+ empty.textContent = "No wallets detected";
178
+ card.appendChild(empty);
179
+ } else {
180
+ const list = document.createElement("ul");
181
+ list.className = "wp-list";
182
+ for (const { info, provider } of opts.providers) {
183
+ const item = document.createElement("button");
184
+ item.className = "wp-item";
185
+ if (info.icon) {
186
+ const icon = document.createElement("img");
187
+ icon.className = "wp-icon";
188
+ icon.src = info.icon;
189
+ icon.alt = info.name;
190
+ item.appendChild(icon);
191
+ }
192
+ const name = document.createElement("span");
193
+ name.textContent = info.name;
194
+ item.appendChild(name);
195
+ item.addEventListener("click", () => {
196
+ this.destroy();
197
+ opts.onSelect(provider);
198
+ });
199
+ list.appendChild(item);
200
+ }
201
+ card.appendChild(list);
202
+ }
203
+ const cancel = document.createElement("button");
204
+ cancel.className = "wp-cancel";
205
+ cancel.textContent = "Cancel";
206
+ cancel.addEventListener("click", () => {
207
+ this.destroy();
208
+ opts.onCancel();
209
+ });
210
+ card.appendChild(cancel);
211
+ backdrop.appendChild(card);
212
+ this.shadow.appendChild(style);
213
+ this.shadow.appendChild(backdrop);
214
+ document.body.appendChild(this.root);
215
+ }
216
+ destroy() {
217
+ this.root.remove();
218
+ }
219
+ };
220
+ function showToast(message = "Opening in new window\u2026", variant = "info") {
221
+ const bg = variant === "error" ? "#dc2626" : "#1f2937";
222
+ const root = document.createElement("div");
223
+ const shadow = root.attachShadow({ mode: "closed" });
224
+ shadow.innerHTML = `<style>:host{all:initial}.ok-toast{position:fixed;top:16px;right:16px;z-index:1000000;background:${bg};color:#f9fafb;padding:10px 16px;border-radius:8px;font:14px/1.4 system-ui,sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.15);animation:ok-t-in 200ms ease-out;max-width:360px}@keyframes ok-t-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}</style><div class="ok-toast">${message}</div>`;
225
+ document.body.appendChild(root);
226
+ setTimeout(() => root.remove(), 4e3);
227
+ }
228
+ var ExternalWalletError = class extends Error {
229
+ constructor(message) {
230
+ super(message);
231
+ this.name = "ExternalWalletError";
232
+ }
233
+ };
234
+ var OpenKey = class {
235
+ host;
236
+ oauthHost;
237
+ appName;
238
+ mode;
239
+ config;
240
+ popup = null;
241
+ lastAuth = null;
242
+ discoveredProviders = [];
243
+ sessionToken = null;
244
+ constructor(config = {}) {
245
+ this.config = config;
246
+ this.host = config.host || DEFAULT_HOST;
247
+ this.oauthHost = config.oauthHost || this.deriveOAuthHost(this.host);
248
+ this.appName = config.appName || window.location.hostname;
249
+ this.mode = config.mode ?? "iframe";
250
+ if (typeof window !== "undefined") {
251
+ this.discoveredProviders = [];
252
+ window.addEventListener("eip6963:announceProvider", (event) => {
253
+ this.discoveredProviders.push(event.detail);
254
+ });
255
+ window.dispatchEvent(new Event("eip6963:requestProvider"));
256
+ }
257
+ }
258
+ /**
259
+ * Derive the OAuth API host from the main host by prefixing 'api.'
260
+ * @param host - The main host URL
261
+ * @returns The derived OAuth API host URL
262
+ */
263
+ deriveOAuthHost(host) {
264
+ try {
265
+ const url = new URL(host);
266
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
267
+ return host.replace(/\/$/, "");
268
+ }
269
+ url.hostname = "api." + url.hostname;
270
+ return url.toString().replace(/\/$/, "");
271
+ } catch {
272
+ return "https://api.openkey.so";
273
+ }
274
+ }
275
+ /**
276
+ * Connect to OpenKey and get user's wallet address
277
+ * Opens auth flow for user to select/create a key
278
+ */
279
+ async connect(opts) {
280
+ const result = await this.openFlow("connect", {
281
+ type: "openkey:auth:request",
282
+ appName: this.appName
283
+ }, opts?.mode);
284
+ this.lastAuth = result;
285
+ return result;
286
+ }
287
+ /**
288
+ * Link an external wallet to the user's OpenKey account
289
+ * Opens link-wallet widget flow
290
+ */
291
+ async linkWallet(opts) {
292
+ return this.openFlow("link-wallet", {
293
+ type: "openkey:link-wallet:request"
294
+ }, opts?.mode);
295
+ }
296
+ /**
297
+ * Sign a message with the user's OpenKey wallet
298
+ * For external keys, routes directly to the user's wallet provider
299
+ */
300
+ async signMessage(request, opts) {
301
+ if (this.lastAuth?.keyType === "EXTERNAL") {
302
+ return this.signWithExternalWallet(request);
303
+ }
304
+ return this.signWithOpenKey(request, opts?.mode);
305
+ }
306
+ /**
307
+ * Sign typed data (EIP-712) with the user's OpenKey wallet
308
+ * For external keys, routes directly to the user's wallet provider
309
+ */
310
+ async signTypedData(request, opts) {
311
+ if (this.lastAuth?.keyType === "EXTERNAL") {
312
+ return this.signTypedDataWithExternalWallet(request);
313
+ }
314
+ return this.signTypedDataWithOpenKey(request, opts?.mode);
315
+ }
316
+ /**
317
+ * Find a wallet provider that controls the given address.
318
+ * Checks: 1) app-provided externalProvider, 2) EIP-6963 discovered wallets, 3) window.ethereum
319
+ */
320
+ async findWalletProvider(targetAddress) {
321
+ if (this.config.externalProvider) {
322
+ return this.config.externalProvider;
323
+ }
324
+ for (const { provider } of this.discoveredProviders) {
325
+ try {
326
+ const accounts = await provider.request({ method: "eth_accounts" });
327
+ if (accounts.some((a) => a.toLowerCase() === targetAddress.toLowerCase())) {
328
+ return provider;
329
+ }
330
+ } catch {
331
+ }
332
+ }
333
+ if (typeof window !== "undefined" && window.ethereum) {
334
+ const provider = window.ethereum;
335
+ try {
336
+ const accounts = await provider.request({ method: "eth_accounts" });
337
+ if (accounts.some((a) => a.toLowerCase() === targetAddress.toLowerCase())) {
338
+ return provider;
339
+ }
340
+ } catch {
341
+ }
342
+ }
343
+ throw new ExternalWalletError("External wallet not found. Connect the wallet that owns this key and try again.");
344
+ }
345
+ async signWithExternalWallet(request) {
346
+ let provider;
347
+ try {
348
+ provider = await this.findWalletProvider(this.lastAuth.address);
349
+ } catch (e) {
350
+ showToast(e.message, "error");
351
+ throw e;
352
+ }
353
+ try {
354
+ const hexMessage = this.toHex(request.message);
355
+ const signature = await provider.request({
356
+ method: "personal_sign",
357
+ params: [hexMessage, this.lastAuth.address]
358
+ });
359
+ return { signature, address: this.lastAuth.address };
360
+ } catch (e) {
361
+ const msg = e?.message?.includes("not been authorized") ? "Wallet rejected the request. Approve the connection in your wallet and try again." : e?.message || "Signing failed";
362
+ showToast(msg, "error");
363
+ throw new Error(msg);
364
+ }
365
+ }
366
+ async signTypedDataWithExternalWallet(request) {
367
+ let provider;
368
+ try {
369
+ provider = await this.findWalletProvider(this.lastAuth.address);
370
+ } catch (e) {
371
+ showToast(e.message, "error");
372
+ throw e;
373
+ }
374
+ try {
375
+ const signature = await provider.request({
376
+ method: "eth_signTypedData_v4",
377
+ params: [this.lastAuth.address, JSON.stringify(request)]
378
+ });
379
+ return { signature, address: this.lastAuth.address };
380
+ } catch (e) {
381
+ const msg = e?.message?.includes("not been authorized") ? "Wallet rejected the request. Approve the connection in your wallet and try again." : e?.message || "Signing failed";
382
+ showToast(msg, "error");
383
+ throw new Error(msg);
384
+ }
385
+ }
386
+ signWithOpenKey(request, mode) {
387
+ return this.openFlow("sign", {
388
+ type: "openkey:sign:request",
389
+ message: request.message,
390
+ keyId: request.keyId,
391
+ sessionToken: this.sessionToken || void 0
392
+ }, mode);
393
+ }
394
+ signTypedDataWithOpenKey(request, mode) {
395
+ return this.openFlow("sign-typed-data", {
396
+ type: "openkey:signTypedData:request",
397
+ data: request,
398
+ sessionToken: this.sessionToken || void 0
399
+ }, mode);
400
+ }
401
+ toHex(str) {
402
+ return "0x" + Array.from(new TextEncoder().encode(str)).map((b) => b.toString(16).padStart(2, "0")).join("");
403
+ }
404
+ /**
405
+ * Check if user is already authenticated
406
+ */
407
+ async isConnected() {
408
+ try {
409
+ const res = await fetch(`${this.host}/api/auth/session`, {
410
+ credentials: "include"
411
+ });
412
+ if (!res.ok) return false;
413
+ const data = await res.json();
414
+ return !!data.user;
415
+ } catch {
416
+ return false;
417
+ }
418
+ }
419
+ // ======= OAuth 2.1 Provider Methods =======
420
+ /**
421
+ * OAuth 2.1 methods for third-party app authentication
422
+ * Use this when your app has registered OAuth client credentials with OpenKey
423
+ */
424
+ oauth = {
425
+ /**
426
+ * Start OAuth authorization flow
427
+ * Opens popup/redirect to OpenKey authorization endpoint
428
+ * @returns Promise that resolves with authorization code after user consent
429
+ */
430
+ connect: async (config) => {
431
+ const verifier = (0, import_core.generateCodeVerifier)();
432
+ const challenge = await (0, import_core.generateCodeChallenge)(verifier);
433
+ const state = config.state || (0, import_core.generateState)();
434
+ sessionStorage.setItem(
435
+ OAUTH_STORAGE_KEY,
436
+ JSON.stringify({ verifier, state })
437
+ );
438
+ const authUrl = (0, import_core.buildAuthorizationUrl)({
439
+ host: this.oauthHost,
440
+ clientId: config.clientId,
441
+ redirectUri: config.redirectUri,
442
+ codeChallenge: challenge,
443
+ state,
444
+ scopes: ["openid"]
445
+ });
446
+ return this.openOAuthFlow(authUrl, state, config.redirectUri);
447
+ },
448
+ /**
449
+ * Exchange authorization code for tokens
450
+ * Call this after receiving the callback with authorization code
451
+ * @param code - Authorization code from callback URL
452
+ * @param config - Same config used for connect()
453
+ * @returns Promise that resolves with access_token, id_token, etc.
454
+ */
455
+ exchangeCode: async (code, config) => {
456
+ const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);
457
+ if (!stored) {
458
+ throw new Error("No PKCE verifier found. Start a new OAuth flow.");
459
+ }
460
+ const { verifier } = JSON.parse(stored);
461
+ const response = await fetch(`${this.oauthHost}/api/auth/oauth2/token`, {
462
+ method: "POST",
463
+ headers: {
464
+ "Content-Type": "application/x-www-form-urlencoded"
465
+ },
466
+ body: new URLSearchParams({
467
+ grant_type: "authorization_code",
468
+ code,
469
+ redirect_uri: config.redirectUri,
470
+ client_id: config.clientId,
471
+ code_verifier: verifier
472
+ })
473
+ });
474
+ sessionStorage.removeItem(OAUTH_STORAGE_KEY);
475
+ if (!response.ok) {
476
+ const error = await response.json();
477
+ throw new Error(error.error_description || "Token exchange failed");
478
+ }
479
+ return response.json();
480
+ },
481
+ /**
482
+ * Verify state parameter from callback
483
+ * Call this before exchangeCode to prevent CSRF attacks
484
+ * @param receivedState - State parameter from callback URL
485
+ * @returns true if state matches, false otherwise
486
+ */
487
+ verifyState: (receivedState) => {
488
+ const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);
489
+ if (!stored) return false;
490
+ const { state } = JSON.parse(stored);
491
+ return state === receivedState;
492
+ },
493
+ /**
494
+ * Parse authorization response from callback URL
495
+ * @param url - Callback URL (defaults to current window.location.href)
496
+ * @returns Parsed code and state, or error
497
+ */
498
+ parseCallback: (url) => {
499
+ const urlObj = new URL(url || window.location.href);
500
+ const error = urlObj.searchParams.get("error");
501
+ if (error) {
502
+ return {
503
+ error,
504
+ errorDescription: urlObj.searchParams.get("error_description") || void 0
505
+ };
506
+ }
507
+ const code = urlObj.searchParams.get("code");
508
+ const state = urlObj.searchParams.get("state");
509
+ if (!code || !state) {
510
+ return { error: "missing_params", errorDescription: "Missing code or state parameter" };
511
+ }
512
+ return { code, state };
513
+ }
514
+ };
515
+ openOAuthFlow(url, state, redirectUri) {
516
+ return new Promise((resolve, reject) => {
517
+ if (this.mode !== "redirect") {
518
+ this.openOAuthPopup(url, state, redirectUri, resolve, reject);
519
+ } else {
520
+ window.location.href = url;
521
+ }
522
+ });
523
+ }
524
+ openOAuthPopup(url, state, redirectUri, resolve, reject) {
525
+ const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
526
+ const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
527
+ this.popup = window.open(
528
+ url,
529
+ "openkey-oauth",
530
+ `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
531
+ );
532
+ if (!this.popup) {
533
+ showToast("Popup was blocked. Please allow popups for this site.", "error");
534
+ reject({
535
+ code: "POPUP_BLOCKED",
536
+ message: "Popup was blocked. Please allow popups or use redirect mode."
537
+ });
538
+ return;
539
+ }
540
+ const redirectHost = new URL(redirectUri).origin;
541
+ const pollInterval = setInterval(() => {
542
+ try {
543
+ if (this.popup?.closed) {
544
+ clearInterval(pollInterval);
545
+ clearTimeout(timeout);
546
+ reject({
547
+ code: "USER_CANCELLED",
548
+ message: "User closed the popup"
549
+ });
550
+ return;
551
+ }
552
+ const popupUrl = this.popup?.location?.href;
553
+ if (popupUrl && popupUrl.startsWith(redirectHost)) {
554
+ clearInterval(pollInterval);
555
+ clearTimeout(timeout);
556
+ const urlObj = new URL(popupUrl);
557
+ const code = urlObj.searchParams.get("code");
558
+ const returnedState = urlObj.searchParams.get("state");
559
+ const error = urlObj.searchParams.get("error");
560
+ this.popup?.close();
561
+ if (error) {
562
+ reject({
563
+ code: "UNAUTHORIZED",
564
+ message: urlObj.searchParams.get("error_description") || error
565
+ });
566
+ return;
567
+ }
568
+ if (returnedState !== state) {
569
+ reject({
570
+ code: "STATE_MISMATCH",
571
+ message: "State mismatch - possible CSRF attack"
572
+ });
573
+ return;
574
+ }
575
+ if (code) {
576
+ resolve({ code, state: returnedState });
577
+ } else {
578
+ reject({
579
+ code: "UNAUTHORIZED",
580
+ message: "Authorization failed - no code returned"
581
+ });
582
+ }
583
+ }
584
+ } catch {
585
+ }
586
+ }, 100);
587
+ const timeout = setTimeout(() => {
588
+ clearInterval(pollInterval);
589
+ this.popup?.close();
590
+ reject({
591
+ code: "TIMEOUT",
592
+ message: "OAuth flow timed out"
593
+ });
594
+ }, DEFAULT_TIMEOUT);
595
+ }
596
+ resolveMode(override) {
597
+ return override ?? this.mode;
598
+ }
599
+ hasDetectedEoa() {
600
+ if (this.discoveredProviders.length > 0) return true;
601
+ if (typeof window !== "undefined" && window.ethereum) return true;
602
+ return false;
603
+ }
604
+ async openFlow(action, message, modeOverride) {
605
+ const mode = this.resolveMode(modeOverride);
606
+ const origin = encodeURIComponent(window.location.origin);
607
+ const eoaFlag = action === "connect" && this.hasDetectedEoa() ? "&hasEoa=true" : "";
608
+ if (mode === "popup") {
609
+ const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
610
+ return new Promise((resolve, reject) => this.openPopup(url2, message, resolve, reject));
611
+ }
612
+ if (mode === "redirect") {
613
+ const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
614
+ window.location.href = url2;
615
+ return new Promise(() => {
616
+ });
617
+ }
618
+ const url = `${this.host}/widget/embed/${action}?origin=${origin}${eoaFlag}`;
619
+ return this.openIframeModal(url, action, message, origin);
620
+ }
621
+ openIframeModal(url, action, message, origin) {
622
+ return new Promise((resolve, reject) => {
623
+ let readyReceived = false;
624
+ let settled = false;
625
+ let modal = null;
626
+ const settle = (fn) => {
627
+ if (settled) return;
628
+ settled = true;
629
+ fn();
630
+ };
631
+ const cleanup = () => {
632
+ clearTimeout(readyTimeout);
633
+ clearTimeout(overallTimeout);
634
+ modal?.destroy();
635
+ modal = null;
636
+ };
637
+ modal = new IframeModal({
638
+ url,
639
+ host: this.host,
640
+ onClose: () => {
641
+ settle(() => {
642
+ clearTimeout(readyTimeout);
643
+ clearTimeout(overallTimeout);
644
+ modal = null;
645
+ reject({ code: "USER_CANCELLED", message: "User cancelled the request" });
646
+ });
647
+ },
648
+ onMessage: (data) => {
649
+ if (data.type === "openkey:ready") {
650
+ readyReceived = true;
651
+ modal?.postMessage(message);
652
+ return;
653
+ }
654
+ if (data.type === "openkey:close") {
655
+ settle(() => {
656
+ cleanup();
657
+ reject({ code: "USER_CANCELLED", message: "User cancelled the request" });
658
+ });
659
+ return;
660
+ }
661
+ if (data.type === "openkey:link-wallet:delegate") {
662
+ this.handleWalletLinkDelegation(modal);
663
+ return;
664
+ }
665
+ if (data.type === "openkey:auth:use-external-wallet") {
666
+ cleanup();
667
+ this.handleExternalWalletConnect(resolve, reject);
668
+ return;
669
+ }
670
+ if (data.type === "openkey:auth:response" || data.type === "openkey:sign:response" || data.type === "openkey:signTypedData:response" || data.type === "openkey:link-wallet:response") {
671
+ settle(() => {
672
+ cleanup();
673
+ if (data.success) {
674
+ if (data.type === "openkey:auth:response") {
675
+ if (data.sessionToken) this.sessionToken = data.sessionToken;
676
+ resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
677
+ } else if (data.type === "openkey:link-wallet:response") {
678
+ resolve({ address: data.address, keyId: data.keyId });
679
+ } else {
680
+ resolve({ signature: data.signature, address: data.address });
681
+ }
682
+ } else {
683
+ reject(data.error);
684
+ }
685
+ });
686
+ }
687
+ }
688
+ });
689
+ const readyTimeout = setTimeout(() => {
690
+ if (readyReceived || settled) return;
691
+ cleanup();
692
+ console.warn("OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP.");
693
+ showToast();
694
+ const popupUrl = `${this.host}/widget/${action}?origin=${origin}`;
695
+ this.openPopup(popupUrl, message, (val) => settle(() => resolve(val)), (err) => settle(() => reject(err)));
696
+ }, IFRAME_READY_TIMEOUT);
697
+ const overallTimeout = setTimeout(() => {
698
+ settle(() => {
699
+ cleanup();
700
+ reject({ code: "TIMEOUT", message: "Request timed out" });
701
+ });
702
+ }, DEFAULT_TIMEOUT);
703
+ });
704
+ }
705
+ handleWalletLinkDelegation(modal) {
706
+ const sendResult = (result) => modal.postMessage(result);
707
+ new WalletPicker({
708
+ providers: this.discoveredProviders,
709
+ onCancel: () => {
710
+ sendResult({ type: "openkey:link-wallet:result", success: false, error: { code: "USER_CANCELLED", message: "Wallet linking cancelled" } });
711
+ },
712
+ onSelect: async (provider) => {
713
+ try {
714
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
715
+ const address = accounts[0];
716
+ if (!address) throw new Error("No account returned");
717
+ const challengeRes = await fetch(`${this.host}/api/keys/link/challenge`, {
718
+ method: "POST",
719
+ credentials: "include"
720
+ });
721
+ if (!challengeRes.ok) throw new Error("Failed to get challenge");
722
+ const { message } = await challengeRes.json();
723
+ const hexMessage = this.toHex(message);
724
+ const signature = await provider.request({
725
+ method: "personal_sign",
726
+ params: [hexMessage, address]
727
+ });
728
+ const linkRes = await fetch(`${this.host}/api/keys/link`, {
729
+ method: "POST",
730
+ headers: { "Content-Type": "application/json" },
731
+ credentials: "include",
732
+ body: JSON.stringify({ address, signature, message })
733
+ });
734
+ if (!linkRes.ok) {
735
+ const err = await linkRes.json();
736
+ throw new Error(err.error || "Failed to link wallet");
737
+ }
738
+ const { key } = await linkRes.json();
739
+ sendResult({ type: "openkey:link-wallet:result", success: true, address: key.address, keyId: key.id });
740
+ } catch (e) {
741
+ sendResult({ type: "openkey:link-wallet:result", success: false, error: { code: "UNKNOWN", message: e.message || "Wallet linking failed" } });
742
+ }
743
+ }
744
+ });
745
+ }
746
+ handleExternalWalletConnect(resolve, reject) {
747
+ new WalletPicker({
748
+ providers: this.discoveredProviders,
749
+ onCancel: () => {
750
+ reject({ code: "USER_CANCELLED", message: "User cancelled wallet selection" });
751
+ },
752
+ onSelect: async (provider) => {
753
+ try {
754
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
755
+ const address = accounts[0];
756
+ if (!address) throw new Error("No account returned");
757
+ const result = { address, keyId: `external:${address}`, keyType: "EXTERNAL" };
758
+ this.lastAuth = result;
759
+ resolve(result);
760
+ } catch (e) {
761
+ reject({ code: "UNKNOWN", message: e.message || "Failed to connect wallet" });
762
+ }
763
+ }
764
+ });
765
+ }
766
+ openPopup(url, message, resolve, reject) {
767
+ const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
768
+ const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
769
+ this.popup = window.open(
770
+ url,
771
+ "openkey",
772
+ `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
773
+ );
774
+ if (!this.popup) {
775
+ showToast("Popup was blocked. Please allow popups for this site.", "error");
776
+ reject({
777
+ code: "POPUP_BLOCKED",
778
+ message: "Popup was blocked. Please allow popups for this site."
779
+ });
780
+ return;
781
+ }
782
+ const cleanup = () => {
783
+ window.removeEventListener("message", handleMessage);
784
+ clearInterval(pollClosed);
785
+ clearTimeout(timeout);
786
+ };
787
+ const pollClosed = setInterval(() => {
788
+ if (this.popup?.closed) {
789
+ cleanup();
790
+ reject({
791
+ code: "USER_CANCELLED",
792
+ message: "User closed the popup"
793
+ });
794
+ }
795
+ }, 500);
796
+ const timeout = setTimeout(() => {
797
+ cleanup();
798
+ this.popup?.close();
799
+ reject({
800
+ code: "TIMEOUT",
801
+ message: "Request timed out"
802
+ });
803
+ }, DEFAULT_TIMEOUT);
804
+ const handleMessage = (event) => {
805
+ if (event.origin !== this.host) return;
806
+ if (event.source !== this.popup) return;
807
+ const data = event.data;
808
+ if (data.type === "openkey:ready") {
809
+ this.popup?.postMessage(message, this.host);
810
+ return;
811
+ }
812
+ if (data.type === "openkey:close") {
813
+ cleanup();
814
+ this.popup?.close();
815
+ reject({
816
+ code: "USER_CANCELLED",
817
+ message: "User cancelled the request"
818
+ });
819
+ return;
820
+ }
821
+ if (data.type === "openkey:auth:use-external-wallet") {
822
+ cleanup();
823
+ this.popup?.close();
824
+ this.handleExternalWalletConnect(resolve, reject);
825
+ return;
826
+ }
827
+ if (data.type === "openkey:auth:response" || data.type === "openkey:sign:response" || data.type === "openkey:signTypedData:response" || data.type === "openkey:link-wallet:response") {
828
+ cleanup();
829
+ this.popup?.close();
830
+ if (data.success) {
831
+ if (data.type === "openkey:auth:response") {
832
+ if (data.sessionToken) this.sessionToken = data.sessionToken;
833
+ resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
834
+ } else if (data.type === "openkey:link-wallet:response") {
835
+ resolve({ address: data.address, keyId: data.keyId });
836
+ } else {
837
+ resolve({ signature: data.signature, address: data.address });
838
+ }
839
+ } else {
840
+ reject(data.error);
841
+ }
842
+ }
843
+ };
844
+ window.addEventListener("message", handleMessage);
845
+ }
846
+ /**
847
+ * Disconnect and close any open flows
848
+ */
849
+ disconnect() {
850
+ this.popup?.close();
851
+ this.popup = null;
852
+ }
853
+ };
854
+ var OpenKeyProvider = class {
855
+ openkey;
856
+ address;
857
+ keyId;
858
+ keyType;
859
+ constructor(openkey, authResult) {
860
+ this.openkey = openkey;
861
+ this.address = authResult.address;
862
+ this.keyId = authResult.keyId;
863
+ this.keyType = authResult.keyType;
864
+ }
865
+ async request({ method, params }) {
866
+ switch (method) {
867
+ case "eth_accounts":
868
+ case "eth_requestAccounts":
869
+ return [this.address];
870
+ case "eth_chainId":
871
+ return "0x1";
872
+ // mainnet
873
+ case "personal_sign": {
874
+ if (this.keyType === "EXTERNAL") {
875
+ try {
876
+ const provider = await this.openkey.findWalletProvider(this.address);
877
+ return await provider.request({ method, params });
878
+ } catch (e) {
879
+ showToast(e?.message || "External wallet signing failed", "error");
880
+ throw e;
881
+ }
882
+ }
883
+ const message = this.hexToString(params[0]);
884
+ const result = await this.openkey.signMessage({ message, keyId: this.keyId });
885
+ return result.signature;
886
+ }
887
+ case "eth_signTypedData_v4": {
888
+ if (this.keyType === "EXTERNAL") {
889
+ try {
890
+ const provider = await this.openkey.findWalletProvider(this.address);
891
+ return await provider.request({ method, params });
892
+ } catch (e) {
893
+ showToast(e?.message || "External wallet signing failed", "error");
894
+ throw e;
895
+ }
896
+ }
897
+ const data = JSON.parse(params[1]);
898
+ const result = await this.openkey.signTypedData(data);
899
+ return result.signature;
900
+ }
901
+ default:
902
+ throw new Error(`Unsupported method: ${method}`);
903
+ }
904
+ }
905
+ hexToString(hex) {
906
+ const bytes = new Uint8Array(
907
+ (hex.startsWith("0x") ? hex.slice(2) : hex).match(/.{1,2}/g).map((b) => parseInt(b, 16))
908
+ );
909
+ return new TextDecoder().decode(bytes);
910
+ }
911
+ };
912
+ var index_default = OpenKey;
913
+ var defaultInstance = null;
914
+ function getOpenKey(config) {
915
+ if (!defaultInstance || config) {
916
+ defaultInstance = new OpenKey(config);
917
+ }
918
+ return defaultInstance;
919
+ }
920
+ // Annotate the CommonJS export names for ESM import in node:
921
+ 0 && (module.exports = {
922
+ OpenKey,
923
+ OpenKeyProvider,
924
+ getOpenKey
925
+ });
41
926
  //# sourceMappingURL=index.js.map