@openkey/sdk 0.0.1 → 0.0.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/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # @openkey/sdk
2
+
3
+ The official JavaScript/TypeScript SDK for [OpenKey](https://openkey.so) - secure key management powered by passkeys.
4
+
5
+ ## What is OpenKey?
6
+
7
+ OpenKey provides secure cryptographic key management using passkeys (WebAuthn). Instead of managing seed phrases or private keys directly, OpenKey stores your keys securely and lets you authorize operations with biometric authentication (Face ID, Touch ID, etc.).
8
+
9
+ **Key features:**
10
+ - **Passkey-secured keys** - Keys are protected by WebAuthn, not passwords
11
+ - **No seed phrases** - Never worry about losing or exposing recovery phrases
12
+ - **Popup-based signing** - Clean UX with authorization popups
13
+ - **EIP-1193 compatible** - Works with ethers.js, viem, and other web3 libraries
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @openkey/sdk
19
+ # or
20
+ yarn add @openkey/sdk
21
+ # or
22
+ bun add @openkey/sdk
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```typescript
28
+ import { OpenKey } from '@openkey/sdk';
29
+
30
+ // Initialize the SDK
31
+ const openkey = new OpenKey({
32
+ host: 'https://openkey.so', // Production
33
+ // host: 'http://localhost:5173', // Development
34
+ });
35
+
36
+ // Connect to OpenKey (opens popup for user to select/create a key)
37
+ const { address, keyId } = await openkey.connect();
38
+ console.log('Connected:', address);
39
+
40
+ // Sign a message (opens popup for user approval)
41
+ const signature = await openkey.signMessage({
42
+ message: 'Hello, OpenKey!',
43
+ keyId,
44
+ });
45
+ console.log('Signature:', signature);
46
+ ```
47
+
48
+ ## API Reference
49
+
50
+ ### `new OpenKey(options)`
51
+
52
+ Create a new OpenKey instance.
53
+
54
+ ```typescript
55
+ const openkey = new OpenKey({
56
+ host: 'https://openkey.so', // OpenKey server URL
57
+ });
58
+ ```
59
+
60
+ **Options:**
61
+ - `host` (string): The OpenKey server URL. Use `https://openkey.so` for production.
62
+
63
+ ### `openkey.connect()`
64
+
65
+ Opens the OpenKey popup for the user to authenticate and select a key.
66
+
67
+ ```typescript
68
+ const { address, keyId } = await openkey.connect();
69
+ ```
70
+
71
+ **Returns:**
72
+ - `address` (string): The Ethereum address of the selected key
73
+ - `keyId` (string): The unique identifier for the key (used in signing operations)
74
+
75
+ ### `openkey.signMessage(params)`
76
+
77
+ Sign a message with the user's key. Opens a popup for user approval.
78
+
79
+ ```typescript
80
+ const signature = await openkey.signMessage({
81
+ message: 'Hello, World!',
82
+ keyId: 'key-id-from-connect',
83
+ });
84
+ ```
85
+
86
+ **Parameters:**
87
+ - `message` (string): The message to sign
88
+ - `keyId` (string): The key ID from `connect()`
89
+
90
+ **Returns:**
91
+ - `signature` (string): The hex-encoded signature
92
+
93
+ ### `openkey.signTypedData(params)`
94
+
95
+ Sign EIP-712 typed data with the user's key.
96
+
97
+ ```typescript
98
+ const signature = await openkey.signTypedData({
99
+ domain: {
100
+ name: 'My App',
101
+ version: '1',
102
+ chainId: 1,
103
+ },
104
+ types: {
105
+ Message: [{ name: 'content', type: 'string' }],
106
+ },
107
+ primaryType: 'Message',
108
+ message: {
109
+ content: 'Hello!',
110
+ },
111
+ keyId: 'key-id-from-connect',
112
+ });
113
+ ```
114
+
115
+ ## Integration with ethers.js
116
+
117
+ OpenKey can be used as an EIP-1193 provider with ethers.js:
118
+
119
+ ```typescript
120
+ import { OpenKey } from '@openkey/sdk';
121
+ import { BrowserProvider } from 'ethers';
122
+
123
+ // Create OpenKey instance
124
+ const openkey = new OpenKey({ host: 'https://openkey.so' });
125
+
126
+ // Connect and get the address
127
+ const { address, keyId } = await openkey.connect();
128
+
129
+ // Create an EIP-1193 compatible provider
130
+ const eip1193Provider = {
131
+ async request({ method, params }: { method: string; params?: any[] }) {
132
+ switch (method) {
133
+ case 'eth_accounts':
134
+ case 'eth_requestAccounts':
135
+ return [address];
136
+ case 'eth_chainId':
137
+ return '0x1'; // Mainnet
138
+ case 'personal_sign': {
139
+ const message = hexToString(params![0]);
140
+ const result = await openkey.signMessage({ message, keyId });
141
+ return result.signature;
142
+ }
143
+ default:
144
+ throw new Error(`Unsupported method: ${method}`);
145
+ }
146
+ },
147
+ };
148
+
149
+ // Use with ethers
150
+ const provider = new BrowserProvider(eip1193Provider);
151
+ const signer = await provider.getSigner();
152
+ ```
153
+
154
+ ## Integration with TinyCloud
155
+
156
+ OpenKey works seamlessly with [TinyCloud](https://tinycloud.xyz) for decentralized storage:
157
+
158
+ ```typescript
159
+ import { OpenKey } from '@openkey/sdk';
160
+ import { TinyCloudWeb } from '@tinycloud/web-sdk';
161
+ import { BrowserProvider } from 'ethers';
162
+
163
+ // Setup OpenKey
164
+ const openkey = new OpenKey({ host: 'https://openkey.so' });
165
+ const { address, keyId } = await openkey.connect();
166
+
167
+ // Create EIP-1193 provider (see above)
168
+ const eip1193Provider = createOpenKeyProvider(openkey, address, keyId);
169
+ const ethersProvider = new BrowserProvider(eip1193Provider);
170
+
171
+ // Initialize TinyCloud with OpenKey as the signer
172
+ const tcw = new TinyCloudWeb({
173
+ providers: {
174
+ web3: {
175
+ driver: ethersProvider,
176
+ },
177
+ },
178
+ });
179
+
180
+ // Sign in to TinyCloud (uses OpenKey for SIWE signature)
181
+ await tcw.signIn();
182
+
183
+ // Now you can use TinyCloud storage
184
+ await tcw.kv.put('hello', 'world');
185
+ const result = await tcw.kv.get('hello');
186
+ ```
187
+
188
+ ## Development
189
+
190
+ For local development, point the SDK to your local OpenKey instance:
191
+
192
+ ```typescript
193
+ const openkey = new OpenKey({
194
+ host: 'http://localhost:5173',
195
+ });
196
+ ```
197
+
198
+ You can also enable dev mode in your app to use local OpenKey:
199
+
200
+ ```javascript
201
+ localStorage.setItem('__DEV_MODE__', 'true');
202
+ ```
203
+
204
+ ## Browser Support
205
+
206
+ OpenKey requires WebAuthn support:
207
+ - Chrome 67+
208
+ - Firefox 60+
209
+ - Safari 13+
210
+ - Edge 79+
211
+
212
+ ## Security
213
+
214
+ - Keys never leave the OpenKey TEE (Trusted Execution Environment)
215
+ - All signing operations require explicit user approval via passkey
216
+ - No seed phrases or private keys to manage or lose
217
+
218
+ ## Links
219
+
220
+ - [OpenKey Website](https://openkey.so)
221
+ - [Documentation](https://docs.openkey.so)
222
+ - [GitHub](https://github.com/AstroX11/openkey)
223
+ - [TinyCloud Integration](https://tinycloud.xyz)
224
+
225
+ ## License
226
+
227
+ MIT
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";var l=Object.defineProperty;var m=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var f=Object.prototype.hasOwnProperty;var E=(o,e)=>{for(var n in e)l(o,n,{get:e[n],enumerable:!0})},k=(o,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of T(e))!f.call(o,s)&&s!==n&&l(o,s,{get:()=>e[s],enumerable:!(i=m(e,s))||i.enumerable});return o};var P=o=>k(l({},"__esModule",{value:!0}),o);var U={};E(U,{OpenKey:()=>c,default:()=>I,getOpenKey:()=>O});module.exports=P(U);var w="https://openkey.so";var c=class{host;appName;usePopup;popup=null;iframe=null;constructor(e={}){this.host=e.host||w,this.appName=e.appName||window.location.hostname,this.usePopup=e.usePopup??!0}async connect(){return this.openFlow("connect",{type:"openkey:auth:request",appName:this.appName})}async signMessage(e){return this.openFlow("sign",{type:"openkey:sign:request",message:e.message,keyId:e.keyId})}async signTypedData(e){return this.openFlow("sign-typed-data",{type:"openkey:signTypedData:request",data:e})}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}}async openFlow(e,n){return new Promise((i,s)=>{let a=`${this.host}/widget/${e}?origin=${encodeURIComponent(window.location.origin)}`;this.usePopup?this.openPopup(a,n,i,s):this.openIframe(a,n,i,s)})}openPopup(e,n,i,s){let a=window.screenX+(window.outerWidth-400)/2,u=window.screenY+(window.outerHeight-600)/2;if(this.popup=window.open(e,"openkey",`width=400,height=600,left=${a},top=${u},popup=true`),!this.popup){s({code:"POPUP_BLOCKED",message:"Popup was blocked. Please allow popups for this site."});return}let p=()=>{window.removeEventListener("message",d),clearInterval(g),clearTimeout(y)},g=setInterval(()=>{this.popup?.closed&&(p(),s({code:"USER_CANCELLED",message:"User closed the popup"}))},500),y=setTimeout(()=>{p(),this.popup?.close(),s({code:"TIMEOUT",message:"Request timed out"})},3e5),d=t=>{if(t.origin!==this.host)return;let r=t.data;if(r.type==="openkey:ready"){this.popup?.postMessage(n,this.host);return}if(r.type==="openkey:close"){p(),this.popup?.close(),s({code:"USER_CANCELLED",message:"User cancelled the request"});return}(r.type==="openkey:auth:response"||r.type==="openkey:sign:response"||r.type==="openkey:signTypedData:response")&&(p(),this.popup?.close(),r.success?r.type==="openkey:auth:response"?i({address:r.address,keyId:r.keyId}):i({signature:r.signature,address:r.address}):s(r.error))};window.addEventListener("message",d)}openIframe(e,n,i,s){let a=document.createElement("div");a.style.cssText=`
1
+ "use strict";var y=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var k=(o,e)=>{for(var s in e)y(o,s,{get:e[s],enumerable:!0})},I=(o,e,s,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of O(e))!U.call(o,r)&&r!==s&&y(o,r,{get:()=>e[r],enumerable:!(n=E(e,r))||n.enumerable});return o};var _=o=>I(y({},"__esModule",{value:!0}),o);var L={};k(L,{OpenKey:()=>l,default:()=>C,getOpenKey:()=>D});module.exports=_(L);var A="https://openkey.so";var h="openkey_oauth";function f(o){return btoa(String.fromCharCode(...o)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function v(){let o=new Uint8Array(32);return crypto.getRandomValues(o),f(o)}async function R(o){let s=new TextEncoder().encode(o),n=await crypto.subtle.digest("SHA-256",s);return f(new Uint8Array(n))}function S(){let o=new Uint8Array(16);return crypto.getRandomValues(o),f(o)}var l=class{host;appName;usePopup;popup=null;iframe=null;constructor(e={}){this.host=e.host||A,this.appName=e.appName||window.location.hostname,this.usePopup=e.usePopup??!0}async connect(){return this.openFlow("connect",{type:"openkey:auth:request",appName:this.appName})}async signMessage(e){return this.openFlow("sign",{type:"openkey:sign:request",message:e.message,keyId:e.keyId})}async signTypedData(e){return this.openFlow("sign-typed-data",{type:"openkey:signTypedData:request",data:e})}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 s=await v(),n=await R(s),r=e.state||S();sessionStorage.setItem(h,JSON.stringify({verifier:s,state:r}));let t=new URL(`${this.host}/api/auth/oauth2/authorize`);return t.searchParams.set("client_id",e.clientId),t.searchParams.set("redirect_uri",e.redirectUri),t.searchParams.set("response_type","code"),t.searchParams.set("scope","openid"),t.searchParams.set("state",r),t.searchParams.set("code_challenge",n),t.searchParams.set("code_challenge_method","S256"),this.openOAuthFlow(t.toString(),r,e.redirectUri)},exchangeCode:async(e,s)=>{let n=sessionStorage.getItem(h);if(!n)throw new Error("No PKCE verifier found. Start a new OAuth flow.");let{verifier:r}=JSON.parse(n),t=await fetch(`${this.host}/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:s.redirectUri,client_id:s.clientId,code_verifier:r})});if(sessionStorage.removeItem(h),!t.ok){let d=await t.json();throw new Error(d.error_description||"Token exchange failed")}return t.json()},verifyState:e=>{let s=sessionStorage.getItem(h);if(!s)return!1;let{state:n}=JSON.parse(s);return n===e},parseCallback:e=>{let s=new URL(e||window.location.href),n=s.searchParams.get("error");if(n)return{error:n,errorDescription:s.searchParams.get("error_description")||void 0};let r=s.searchParams.get("code"),t=s.searchParams.get("state");return!r||!t?{error:"missing_params",errorDescription:"Missing code or state parameter"}:{code:r,state:t}}};openOAuthFlow(e,s,n){return new Promise((r,t)=>{this.usePopup?this.openOAuthPopup(e,s,n,r,t):window.location.href=e})}openOAuthPopup(e,s,n,r,t){let d=window.screenX+(window.outerWidth-400)/2,p=window.screenY+(window.outerHeight-600)/2;if(this.popup=window.open(e,"openkey-oauth",`width=400,height=600,left=${d},top=${p},popup=true`),!this.popup){t({code:"POPUP_BLOCKED",message:"Popup was blocked. Please allow popups or use redirect mode."});return}let g=new URL(n).origin,u=setInterval(()=>{try{if(this.popup?.closed){clearInterval(u),clearTimeout(c),t({code:"USER_CANCELLED",message:"User closed the popup"});return}let i=this.popup?.location?.href;if(i&&i.startsWith(g)){clearInterval(u),clearTimeout(c);let a=new URL(i),P=a.searchParams.get("code"),T=a.searchParams.get("state"),w=a.searchParams.get("error");if(this.popup?.close(),w){t({code:"UNAUTHORIZED",message:a.searchParams.get("error_description")||w});return}if(T!==s){t({code:"STATE_MISMATCH",message:"State mismatch - possible CSRF attack"});return}P?r({code:P,state:T}):t({code:"UNAUTHORIZED",message:"Authorization failed - no code returned"})}}catch{}},100),c=setTimeout(()=>{clearInterval(u),this.popup?.close(),t({code:"TIMEOUT",message:"OAuth flow timed out"})},3e5)}async openFlow(e,s){return new Promise((n,r)=>{let t=`${this.host}/widget/${e}?origin=${encodeURIComponent(window.location.origin)}`;this.usePopup?this.openPopup(t,s,n,r):this.openIframe(t,s,n,r)})}openPopup(e,s,n,r){let t=window.screenX+(window.outerWidth-400)/2,d=window.screenY+(window.outerHeight-600)/2;if(this.popup=window.open(e,"openkey",`width=400,height=600,left=${t},top=${d},popup=true`),!this.popup){r({code:"POPUP_BLOCKED",message:"Popup was blocked. Please allow popups for this site."});return}let p=()=>{window.removeEventListener("message",c),clearInterval(g),clearTimeout(u)},g=setInterval(()=>{this.popup?.closed&&(p(),r({code:"USER_CANCELLED",message:"User closed the popup"}))},500),u=setTimeout(()=>{p(),this.popup?.close(),r({code:"TIMEOUT",message:"Request timed out"})},3e5),c=i=>{if(i.origin!==this.host)return;let a=i.data;if(a.type==="openkey:ready"){this.popup?.postMessage(s,this.host);return}if(a.type==="openkey:close"){p(),this.popup?.close(),r({code:"USER_CANCELLED",message:"User cancelled the request"});return}(a.type==="openkey:auth:response"||a.type==="openkey:sign:response"||a.type==="openkey:signTypedData:response")&&(p(),this.popup?.close(),a.success?a.type==="openkey:auth:response"?n({address:a.address,keyId:a.keyId}):n({signature:a.signature,address:a.address}):r(a.error))};window.addEventListener("message",c)}openIframe(e,s,n,r){let t=document.createElement("div");t.style.cssText=`
2
2
  position: fixed;
3
3
  inset: 0;
4
4
  background: rgba(0, 0, 0, 0.5);
@@ -6,7 +6,7 @@
6
6
  align-items: center;
7
7
  justify-content: center;
8
8
  z-index: 999999;
9
- `;let u=document.createElement("div");u.style.cssText=`
9
+ `;let d=document.createElement("div");d.style.cssText=`
10
10
  width: 400px;
11
11
  height: 600px;
12
12
  background: #1a1a1a;
@@ -17,5 +17,5 @@
17
17
  width: 100%;
18
18
  height: 100%;
19
19
  border: none;
20
- `,u.appendChild(this.iframe),a.appendChild(u),document.body.appendChild(a);let p=()=>{window.removeEventListener("message",y),clearTimeout(g),a.remove(),this.iframe=null};a.addEventListener("click",d=>{d.target===a&&(p(),s({code:"USER_CANCELLED",message:"User cancelled the request"}))});let g=setTimeout(()=>{p(),s({code:"TIMEOUT",message:"Request timed out"})},3e5),y=d=>{if(d.origin!==this.host)return;let t=d.data;if(t.type==="openkey:ready"){this.iframe?.contentWindow?.postMessage(n,this.host);return}if(t.type==="openkey:close"){p(),s({code:"USER_CANCELLED",message:"User cancelled the request"});return}(t.type==="openkey:auth:response"||t.type==="openkey:sign:response"||t.type==="openkey:signTypedData:response")&&(p(),t.success?t.type==="openkey:auth:response"?i({address:t.address,keyId:t.keyId}):i({signature:t.signature,address:t.address}):s(t.error))};window.addEventListener("message",y)}disconnect(){this.popup?.close(),this.popup=null,this.iframe?.parentElement?.parentElement?.remove(),this.iframe=null}},I=c,h=null;function O(o){return(!h||o)&&(h=new c(o)),h}0&&(module.exports={OpenKey,getOpenKey});
20
+ `,d.appendChild(this.iframe),t.appendChild(d),document.body.appendChild(t);let p=()=>{window.removeEventListener("message",u),clearTimeout(g),t.remove(),this.iframe=null};t.addEventListener("click",c=>{c.target===t&&(p(),r({code:"USER_CANCELLED",message:"User cancelled the request"}))});let g=setTimeout(()=>{p(),r({code:"TIMEOUT",message:"Request timed out"})},3e5),u=c=>{if(c.origin!==this.host)return;let i=c.data;if(i.type==="openkey:ready"){this.iframe?.contentWindow?.postMessage(s,this.host);return}if(i.type==="openkey:close"){p(),r({code:"USER_CANCELLED",message:"User cancelled the request"});return}(i.type==="openkey:auth:response"||i.type==="openkey:sign:response"||i.type==="openkey:signTypedData:response")&&(p(),i.success?i.type==="openkey:auth:response"?n({address:i.address,keyId:i.keyId}):n({signature:i.signature,address:i.address}):r(i.error))};window.addEventListener("message",u)}disconnect(){this.popup?.close(),this.popup=null,this.iframe?.parentElement?.parentElement?.remove(),this.iframe=null}},C=l,m=null;function D(o){return(!m||o)&&(m=new l(o)),m}0&&(module.exports={OpenKey,getOpenKey});
21
21
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// OpenKey SDK - Browser client for third-party apps\n// Provides \"Sign with OpenKey\" functionality via popup or iframe\n\nexport interface OpenKeyConfig {\n /** OpenKey host URL (default: https://openkey.so) */\n host?: string;\n /** App identifier for display */\n appName?: string;\n /** Use popup instead of iframe (default: true) */\n usePopup?: boolean;\n}\n\nexport interface SignMessageRequest {\n message: string;\n keyId?: string; // If not provided, user selects\n}\n\nexport interface SignTypedDataRequest {\n domain: {\n name?: string;\n version?: string;\n chainId?: number;\n verifyingContract?: string;\n };\n types: Record<string, Array<{ name: string; type: string }>>;\n primaryType: string;\n message: Record<string, unknown>;\n keyId?: string;\n}\n\nexport interface SignResult {\n signature: string;\n address: string;\n}\n\nexport interface AuthResult {\n address: string;\n keyId: string;\n}\n\nexport interface OpenKeyError {\n code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN';\n message: string;\n}\n\ntype MessageType =\n | { type: 'openkey:auth:request'; appName: string }\n | { type: 'openkey:auth:response'; success: true; address: string; keyId: string }\n | { type: 'openkey:auth:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:sign:request'; message: string; keyId?: string }\n | { type: 'openkey:sign:response'; success: true; signature: string; address: string }\n | { type: 'openkey:sign:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:signTypedData:request'; data: SignTypedDataRequest }\n | { type: 'openkey:signTypedData:response'; success: true; signature: string; address: string }\n | { type: 'openkey:signTypedData:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:ready' }\n | { type: 'openkey:close' };\n\nconst DEFAULT_HOST = 'https://openkey.so';\nconst POPUP_WIDTH = 400;\nconst POPUP_HEIGHT = 600;\nconst DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes\n\nexport class OpenKey {\n private host: string;\n private appName: string;\n private usePopup: boolean;\n private popup: Window | null = null;\n private iframe: HTMLIFrameElement | null = null;\n\n constructor(config: OpenKeyConfig = {}) {\n this.host = config.host || DEFAULT_HOST;\n this.appName = config.appName || window.location.hostname;\n this.usePopup = config.usePopup ?? true;\n }\n\n /**\n * Connect to OpenKey and get user's wallet address\n * Opens auth flow for user to select/create a key\n */\n async connect(): Promise<AuthResult> {\n return this.openFlow<AuthResult>('connect', {\n type: 'openkey:auth:request',\n appName: this.appName,\n });\n }\n\n /**\n * Sign a message with the user's OpenKey wallet\n */\n async signMessage(request: SignMessageRequest): Promise<SignResult> {\n return this.openFlow<SignResult>('sign', {\n type: 'openkey:sign:request',\n message: request.message,\n keyId: request.keyId,\n });\n }\n\n /**\n * Sign typed data (EIP-712) with the user's OpenKey wallet\n */\n async signTypedData(request: SignTypedDataRequest): Promise<SignResult> {\n return this.openFlow<SignResult>('sign-typed-data', {\n type: 'openkey:signTypedData:request',\n data: request,\n });\n }\n\n /**\n * Check if user is already authenticated\n */\n async isConnected(): Promise<boolean> {\n // Check for existing session via API\n try {\n const res = await fetch(`${this.host}/api/auth/session`, {\n credentials: 'include',\n });\n if (!res.ok) return false;\n const data = await res.json();\n return !!data.user;\n } catch {\n return false;\n }\n }\n\n private async openFlow<T>(action: string, message: object): Promise<T> {\n return new Promise((resolve, reject) => {\n const url = `${this.host}/widget/${action}?origin=${encodeURIComponent(window.location.origin)}`;\n\n if (this.usePopup) {\n this.openPopup(url, message, resolve, reject);\n } else {\n this.openIframe(url, message, resolve, reject);\n }\n });\n }\n\n private openPopup<T>(\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Calculate popup position\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n this.popup = window.open(\n url,\n 'openkey',\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!this.popup) {\n reject({\n code: 'POPUP_BLOCKED',\n message: 'Popup was blocked. Please allow popups for this site.',\n });\n return;\n }\n\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearInterval(pollClosed);\n clearTimeout(timeout);\n };\n\n // Poll to detect popup close\n const pollClosed = setInterval(() => {\n if (this.popup?.closed) {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n });\n }\n }, 500);\n\n // Timeout\n const timeout = setTimeout(() => {\n cleanup();\n this.popup?.close();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n }, DEFAULT_TIMEOUT);\n\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n // Widget is ready, send our request\n this.popup?.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n cleanup();\n this.popup?.close();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n return;\n }\n\n // Handle responses\n if (\n data.type === 'openkey:auth:response' ||\n data.type === 'openkey:sign:response' ||\n data.type === 'openkey:signTypedData:response'\n ) {\n cleanup();\n this.popup?.close();\n\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else {\n resolve({ signature: data.signature, address: data.address } as T);\n }\n } else {\n reject(data.error);\n }\n }\n };\n\n window.addEventListener('message', handleMessage);\n }\n\n private openIframe<T>(\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Create overlay\n const overlay = document.createElement('div');\n overlay.style.cssText = `\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 999999;\n `;\n\n // Create iframe container\n const container = document.createElement('div');\n container.style.cssText = `\n width: ${POPUP_WIDTH}px;\n height: ${POPUP_HEIGHT}px;\n background: #1a1a1a;\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);\n `;\n\n // Create iframe\n this.iframe = document.createElement('iframe');\n this.iframe.src = url;\n this.iframe.style.cssText = `\n width: 100%;\n height: 100%;\n border: none;\n `;\n\n container.appendChild(this.iframe);\n overlay.appendChild(container);\n document.body.appendChild(overlay);\n\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearTimeout(timeout);\n overlay.remove();\n this.iframe = null;\n };\n\n // Close on overlay click\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n }\n });\n\n // Timeout\n const timeout = setTimeout(() => {\n cleanup();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n }, DEFAULT_TIMEOUT);\n\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n this.iframe?.contentWindow?.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n return;\n }\n\n // Handle responses\n if (\n data.type === 'openkey:auth:response' ||\n data.type === 'openkey:sign:response' ||\n data.type === 'openkey:signTypedData:response'\n ) {\n cleanup();\n\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else {\n resolve({ signature: data.signature, address: data.address } as T);\n }\n } else {\n reject(data.error);\n }\n }\n };\n\n window.addEventListener('message', handleMessage);\n }\n\n /**\n * Disconnect and close any open flows\n */\n disconnect() {\n this.popup?.close();\n this.popup = null;\n this.iframe?.parentElement?.parentElement?.remove();\n this.iframe = null;\n }\n}\n\n// Default export for convenience\nexport default OpenKey;\n\n// Create singleton instance\nlet defaultInstance: OpenKey | null = null;\n\nexport function getOpenKey(config?: OpenKeyConfig): OpenKey {\n if (!defaultInstance || config) {\n defaultInstance = new OpenKey(config);\n }\n return defaultInstance;\n}\n"],"mappings":"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","names":["index_exports","__export","OpenKey","index_default","getOpenKey","__toCommonJS","DEFAULT_HOST","OpenKey","config","DEFAULT_HOST","request","res","action","message","resolve","reject","url","left","top","cleanup","handleMessage","pollClosed","timeout","event","data","overlay","container","e","index_default","defaultInstance","getOpenKey"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// OpenKey SDK - Browser client for third-party apps\n// Provides \"Sign with OpenKey\" functionality via popup or iframe\n\nexport interface OpenKeyConfig {\n /** OpenKey host URL (default: https://openkey.so) */\n host?: string;\n /** App identifier for display */\n appName?: string;\n /** Use popup instead of iframe (default: true) */\n usePopup?: boolean;\n}\n\nexport interface SignMessageRequest {\n message: string;\n keyId?: string; // If not provided, user selects\n}\n\nexport interface SignTypedDataRequest {\n domain: {\n name?: string;\n version?: string;\n chainId?: number;\n verifyingContract?: string;\n };\n types: Record<string, Array<{ name: string; type: string }>>;\n primaryType: string;\n message: Record<string, unknown>;\n keyId?: string;\n}\n\nexport interface SignResult {\n signature: string;\n address: string;\n}\n\nexport interface AuthResult {\n address: string;\n keyId: string;\n}\n\nexport interface OpenKeyError {\n code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN' | 'STATE_MISMATCH';\n message: string;\n}\n\n// ======= OAuth 2.1 Types =======\n\n/** OAuth configuration for third-party apps */\nexport interface OAuthConfig {\n /** OAuth client_id (registered with OpenKey) */\n clientId: string;\n /** Redirect URI (must match registered URI) */\n redirectUri: string;\n /** State parameter for CSRF protection (auto-generated if not provided) */\n state?: string;\n}\n\n/** Result from OAuth authorization */\nexport interface OAuthResult {\n /** Authorization code (exchange for tokens) */\n code: string;\n /** State parameter (verify matches request) */\n state: string;\n}\n\n/** Response from token exchange */\nexport interface OAuthTokenResponse {\n access_token: string;\n token_type: 'Bearer';\n expires_in: number;\n id_token: string;\n scope: string;\n refresh_token?: string;\n}\n\ntype MessageType =\n | { type: 'openkey:auth:request'; appName: string }\n | { type: 'openkey:auth:response'; success: true; address: string; keyId: string }\n | { type: 'openkey:auth:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:sign:request'; message: string; keyId?: string }\n | { type: 'openkey:sign:response'; success: true; signature: string; address: string }\n | { type: 'openkey:sign:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:signTypedData:request'; data: SignTypedDataRequest }\n | { type: 'openkey:signTypedData:response'; success: true; signature: string; address: string }\n | { type: 'openkey:signTypedData:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:ready' }\n | { type: 'openkey:close' };\n\nconst DEFAULT_HOST = 'https://openkey.so';\nconst POPUP_WIDTH = 400;\nconst POPUP_HEIGHT = 600;\nconst DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes\nconst OAUTH_STORAGE_KEY = 'openkey_oauth';\n\n// ======= PKCE Utilities =======\n\nfunction base64UrlEncode(buffer: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...buffer));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nasync function generateCodeVerifier(): Promise<string> {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(new Uint8Array(digest));\n}\n\nfunction generateState(): string {\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\nexport class OpenKey {\n private host: string;\n private appName: string;\n private usePopup: boolean;\n private popup: Window | null = null;\n private iframe: HTMLIFrameElement | null = null;\n\n constructor(config: OpenKeyConfig = {}) {\n this.host = config.host || DEFAULT_HOST;\n this.appName = config.appName || window.location.hostname;\n this.usePopup = config.usePopup ?? true;\n }\n\n /**\n * Connect to OpenKey and get user's wallet address\n * Opens auth flow for user to select/create a key\n */\n async connect(): Promise<AuthResult> {\n return this.openFlow<AuthResult>('connect', {\n type: 'openkey:auth:request',\n appName: this.appName,\n });\n }\n\n /**\n * Sign a message with the user's OpenKey wallet\n */\n async signMessage(request: SignMessageRequest): Promise<SignResult> {\n return this.openFlow<SignResult>('sign', {\n type: 'openkey:sign:request',\n message: request.message,\n keyId: request.keyId,\n });\n }\n\n /**\n * Sign typed data (EIP-712) with the user's OpenKey wallet\n */\n async signTypedData(request: SignTypedDataRequest): Promise<SignResult> {\n return this.openFlow<SignResult>('sign-typed-data', {\n type: 'openkey:signTypedData:request',\n data: request,\n });\n }\n\n /**\n * Check if user is already authenticated\n */\n async isConnected(): Promise<boolean> {\n // Check for existing session via API\n try {\n const res = await fetch(`${this.host}/api/auth/session`, {\n credentials: 'include',\n });\n if (!res.ok) return false;\n const data = await res.json();\n return !!data.user;\n } catch {\n return false;\n }\n }\n\n // ======= OAuth 2.1 Provider Methods =======\n\n /**\n * OAuth 2.1 methods for third-party app authentication\n * Use this when your app has registered OAuth client credentials with OpenKey\n */\n oauth = {\n /**\n * Start OAuth authorization flow\n * Opens popup/redirect to OpenKey authorization endpoint\n * @returns Promise that resolves with authorization code after user consent\n */\n connect: async (config: OAuthConfig): Promise<OAuthResult> => {\n // Generate PKCE values\n const verifier = await generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n const state = config.state || generateState();\n\n // Store for later token exchange\n sessionStorage.setItem(\n OAUTH_STORAGE_KEY,\n JSON.stringify({ verifier, state })\n );\n\n // Build authorization URL\n const authUrl = new URL(`${this.host}/api/auth/oauth2/authorize`);\n authUrl.searchParams.set('client_id', config.clientId);\n authUrl.searchParams.set('redirect_uri', config.redirectUri);\n authUrl.searchParams.set('response_type', 'code');\n authUrl.searchParams.set('scope', 'openid');\n authUrl.searchParams.set('state', state);\n authUrl.searchParams.set('code_challenge', challenge);\n authUrl.searchParams.set('code_challenge_method', 'S256');\n\n return this.openOAuthFlow(authUrl.toString(), state, config.redirectUri);\n },\n\n /**\n * Exchange authorization code for tokens\n * Call this after receiving the callback with authorization code\n * @param code - Authorization code from callback URL\n * @param config - Same config used for connect()\n * @returns Promise that resolves with access_token, id_token, etc.\n */\n exchangeCode: async (\n code: string,\n config: OAuthConfig\n ): Promise<OAuthTokenResponse> => {\n const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);\n if (!stored) {\n throw new Error('No PKCE verifier found. Start a new OAuth flow.');\n }\n\n const { verifier } = JSON.parse(stored);\n\n const response = await fetch(`${this.host}/api/auth/oauth2/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n code_verifier: verifier,\n }),\n });\n\n // Clear stored verifier\n sessionStorage.removeItem(OAUTH_STORAGE_KEY);\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.error_description || 'Token exchange failed');\n }\n\n return response.json();\n },\n\n /**\n * Verify state parameter from callback\n * Call this before exchangeCode to prevent CSRF attacks\n * @param receivedState - State parameter from callback URL\n * @returns true if state matches, false otherwise\n */\n verifyState: (receivedState: string): boolean => {\n const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);\n if (!stored) return false;\n const { state } = JSON.parse(stored);\n return state === receivedState;\n },\n\n /**\n * Parse authorization response from callback URL\n * @param url - Callback URL (defaults to current window.location.href)\n * @returns Parsed code and state, or error\n */\n parseCallback: (\n url?: string\n ): { code: string; state: string } | { error: string; errorDescription?: string } => {\n const urlObj = new URL(url || window.location.href);\n const error = urlObj.searchParams.get('error');\n\n if (error) {\n return {\n error,\n errorDescription: urlObj.searchParams.get('error_description') || undefined,\n };\n }\n\n const code = urlObj.searchParams.get('code');\n const state = urlObj.searchParams.get('state');\n\n if (!code || !state) {\n return { error: 'missing_params', errorDescription: 'Missing code or state parameter' };\n }\n\n return { code, state };\n },\n };\n\n private openOAuthFlow(\n url: string,\n state: string,\n redirectUri: string\n ): Promise<OAuthResult> {\n return new Promise((resolve, reject) => {\n if (this.usePopup) {\n this.openOAuthPopup(url, state, redirectUri, resolve, reject);\n } else {\n // Full page redirect - won't resolve, page navigates away\n window.location.href = url;\n }\n });\n }\n\n private openOAuthPopup(\n url: string,\n state: string,\n redirectUri: string,\n resolve: (value: OAuthResult) => void,\n reject: (error: OpenKeyError) => void\n ) {\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n this.popup = window.open(\n url,\n 'openkey-oauth',\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!this.popup) {\n reject({\n code: 'POPUP_BLOCKED',\n message: 'Popup was blocked. Please allow popups or use redirect mode.',\n });\n return;\n }\n\n const redirectHost = new URL(redirectUri).origin;\n\n // Poll for redirect back with code\n const pollInterval = setInterval(() => {\n try {\n if (this.popup?.closed) {\n clearInterval(pollInterval);\n clearTimeout(timeout);\n reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n });\n return;\n }\n\n // Check if redirected to callback URL\n const popupUrl = this.popup?.location?.href;\n if (popupUrl && popupUrl.startsWith(redirectHost)) {\n clearInterval(pollInterval);\n clearTimeout(timeout);\n\n const urlObj = new URL(popupUrl);\n const code = urlObj.searchParams.get('code');\n const returnedState = urlObj.searchParams.get('state');\n const error = urlObj.searchParams.get('error');\n\n this.popup?.close();\n\n if (error) {\n reject({\n code: 'UNAUTHORIZED',\n message: urlObj.searchParams.get('error_description') || error,\n });\n return;\n }\n\n if (returnedState !== state) {\n reject({\n code: 'STATE_MISMATCH',\n message: 'State mismatch - possible CSRF attack',\n });\n return;\n }\n\n if (code) {\n resolve({ code, state: returnedState });\n } else {\n reject({\n code: 'UNAUTHORIZED',\n message: 'Authorization failed - no code returned',\n });\n }\n }\n } catch {\n // Cross-origin - popup still on OpenKey domain, continue polling\n }\n }, 100);\n\n // Timeout\n const timeout = setTimeout(() => {\n clearInterval(pollInterval);\n this.popup?.close();\n reject({\n code: 'TIMEOUT',\n message: 'OAuth flow timed out',\n });\n }, DEFAULT_TIMEOUT);\n }\n\n private async openFlow<T>(action: string, message: object): Promise<T> {\n return new Promise((resolve, reject) => {\n const url = `${this.host}/widget/${action}?origin=${encodeURIComponent(window.location.origin)}`;\n\n if (this.usePopup) {\n this.openPopup(url, message, resolve, reject);\n } else {\n this.openIframe(url, message, resolve, reject);\n }\n });\n }\n\n private openPopup<T>(\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Calculate popup position\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n this.popup = window.open(\n url,\n 'openkey',\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!this.popup) {\n reject({\n code: 'POPUP_BLOCKED',\n message: 'Popup was blocked. Please allow popups for this site.',\n });\n return;\n }\n\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearInterval(pollClosed);\n clearTimeout(timeout);\n };\n\n // Poll to detect popup close\n const pollClosed = setInterval(() => {\n if (this.popup?.closed) {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n });\n }\n }, 500);\n\n // Timeout\n const timeout = setTimeout(() => {\n cleanup();\n this.popup?.close();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n }, DEFAULT_TIMEOUT);\n\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n // Widget is ready, send our request\n this.popup?.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n cleanup();\n this.popup?.close();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n return;\n }\n\n // Handle responses\n if (\n data.type === 'openkey:auth:response' ||\n data.type === 'openkey:sign:response' ||\n data.type === 'openkey:signTypedData:response'\n ) {\n cleanup();\n this.popup?.close();\n\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else {\n resolve({ signature: data.signature, address: data.address } as T);\n }\n } else {\n reject(data.error);\n }\n }\n };\n\n window.addEventListener('message', handleMessage);\n }\n\n private openIframe<T>(\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Create overlay\n const overlay = document.createElement('div');\n overlay.style.cssText = `\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 999999;\n `;\n\n // Create iframe container\n const container = document.createElement('div');\n container.style.cssText = `\n width: ${POPUP_WIDTH}px;\n height: ${POPUP_HEIGHT}px;\n background: #1a1a1a;\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);\n `;\n\n // Create iframe\n this.iframe = document.createElement('iframe');\n this.iframe.src = url;\n this.iframe.style.cssText = `\n width: 100%;\n height: 100%;\n border: none;\n `;\n\n container.appendChild(this.iframe);\n overlay.appendChild(container);\n document.body.appendChild(overlay);\n\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearTimeout(timeout);\n overlay.remove();\n this.iframe = null;\n };\n\n // Close on overlay click\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n }\n });\n\n // Timeout\n const timeout = setTimeout(() => {\n cleanup();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n }, DEFAULT_TIMEOUT);\n\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n this.iframe?.contentWindow?.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n return;\n }\n\n // Handle responses\n if (\n data.type === 'openkey:auth:response' ||\n data.type === 'openkey:sign:response' ||\n data.type === 'openkey:signTypedData:response'\n ) {\n cleanup();\n\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else {\n resolve({ signature: data.signature, address: data.address } as T);\n }\n } else {\n reject(data.error);\n }\n }\n };\n\n window.addEventListener('message', handleMessage);\n }\n\n /**\n * Disconnect and close any open flows\n */\n disconnect() {\n this.popup?.close();\n this.popup = null;\n this.iframe?.parentElement?.parentElement?.remove();\n this.iframe = null;\n }\n}\n\n// Default export for convenience\nexport default OpenKey;\n\n// Create singleton instance\nlet defaultInstance: OpenKey | null = null;\n\nexport function getOpenKey(config?: OpenKeyConfig): OpenKey {\n if (!defaultInstance || config) {\n defaultInstance = new OpenKey(config);\n }\n return defaultInstance;\n}\n"],"mappings":"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","names":["index_exports","__export","OpenKey","index_default","getOpenKey","__toCommonJS","DEFAULT_HOST","OAUTH_STORAGE_KEY","base64UrlEncode","buffer","generateCodeVerifier","array","generateCodeChallenge","verifier","data","digest","generateState","OpenKey","config","DEFAULT_HOST","request","res","challenge","state","authUrl","code","stored","response","error","receivedState","url","urlObj","redirectUri","resolve","reject","left","top","redirectHost","pollInterval","timeout","popupUrl","returnedState","action","message","cleanup","handleMessage","pollClosed","event","overlay","container","e","index_default","defaultInstance","getOpenKey"]}
package/dist/index.d.cts CHANGED
@@ -34,9 +34,34 @@ interface AuthResult {
34
34
  keyId: string;
35
35
  }
36
36
  interface OpenKeyError {
37
- code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN';
37
+ code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN' | 'STATE_MISMATCH';
38
38
  message: string;
39
39
  }
40
+ /** OAuth configuration for third-party apps */
41
+ interface OAuthConfig {
42
+ /** OAuth client_id (registered with OpenKey) */
43
+ clientId: string;
44
+ /** Redirect URI (must match registered URI) */
45
+ redirectUri: string;
46
+ /** State parameter for CSRF protection (auto-generated if not provided) */
47
+ state?: string;
48
+ }
49
+ /** Result from OAuth authorization */
50
+ interface OAuthResult {
51
+ /** Authorization code (exchange for tokens) */
52
+ code: string;
53
+ /** State parameter (verify matches request) */
54
+ state: string;
55
+ }
56
+ /** Response from token exchange */
57
+ interface OAuthTokenResponse {
58
+ access_token: string;
59
+ token_type: 'Bearer';
60
+ expires_in: number;
61
+ id_token: string;
62
+ scope: string;
63
+ refresh_token?: string;
64
+ }
40
65
  declare class OpenKey {
41
66
  private host;
42
67
  private appName;
@@ -61,6 +86,47 @@ declare class OpenKey {
61
86
  * Check if user is already authenticated
62
87
  */
63
88
  isConnected(): Promise<boolean>;
89
+ /**
90
+ * OAuth 2.1 methods for third-party app authentication
91
+ * Use this when your app has registered OAuth client credentials with OpenKey
92
+ */
93
+ oauth: {
94
+ /**
95
+ * Start OAuth authorization flow
96
+ * Opens popup/redirect to OpenKey authorization endpoint
97
+ * @returns Promise that resolves with authorization code after user consent
98
+ */
99
+ connect: (config: OAuthConfig) => Promise<OAuthResult>;
100
+ /**
101
+ * Exchange authorization code for tokens
102
+ * Call this after receiving the callback with authorization code
103
+ * @param code - Authorization code from callback URL
104
+ * @param config - Same config used for connect()
105
+ * @returns Promise that resolves with access_token, id_token, etc.
106
+ */
107
+ exchangeCode: (code: string, config: OAuthConfig) => Promise<OAuthTokenResponse>;
108
+ /**
109
+ * Verify state parameter from callback
110
+ * Call this before exchangeCode to prevent CSRF attacks
111
+ * @param receivedState - State parameter from callback URL
112
+ * @returns true if state matches, false otherwise
113
+ */
114
+ verifyState: (receivedState: string) => boolean;
115
+ /**
116
+ * Parse authorization response from callback URL
117
+ * @param url - Callback URL (defaults to current window.location.href)
118
+ * @returns Parsed code and state, or error
119
+ */
120
+ parseCallback: (url?: string) => {
121
+ code: string;
122
+ state: string;
123
+ } | {
124
+ error: string;
125
+ errorDescription?: string;
126
+ };
127
+ };
128
+ private openOAuthFlow;
129
+ private openOAuthPopup;
64
130
  private openFlow;
65
131
  private openPopup;
66
132
  private openIframe;
@@ -72,4 +138,4 @@ declare class OpenKey {
72
138
 
73
139
  declare function getOpenKey(config?: OpenKeyConfig): OpenKey;
74
140
 
75
- export { type AuthResult, OpenKey, type OpenKeyConfig, type OpenKeyError, type SignMessageRequest, type SignResult, type SignTypedDataRequest, OpenKey as default, getOpenKey };
141
+ export { type AuthResult, type OAuthConfig, type OAuthResult, type OAuthTokenResponse, OpenKey, type OpenKeyConfig, type OpenKeyError, type SignMessageRequest, type SignResult, type SignTypedDataRequest, OpenKey as default, getOpenKey };
package/dist/index.d.ts CHANGED
@@ -34,9 +34,34 @@ interface AuthResult {
34
34
  keyId: string;
35
35
  }
36
36
  interface OpenKeyError {
37
- code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN';
37
+ code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN' | 'STATE_MISMATCH';
38
38
  message: string;
39
39
  }
40
+ /** OAuth configuration for third-party apps */
41
+ interface OAuthConfig {
42
+ /** OAuth client_id (registered with OpenKey) */
43
+ clientId: string;
44
+ /** Redirect URI (must match registered URI) */
45
+ redirectUri: string;
46
+ /** State parameter for CSRF protection (auto-generated if not provided) */
47
+ state?: string;
48
+ }
49
+ /** Result from OAuth authorization */
50
+ interface OAuthResult {
51
+ /** Authorization code (exchange for tokens) */
52
+ code: string;
53
+ /** State parameter (verify matches request) */
54
+ state: string;
55
+ }
56
+ /** Response from token exchange */
57
+ interface OAuthTokenResponse {
58
+ access_token: string;
59
+ token_type: 'Bearer';
60
+ expires_in: number;
61
+ id_token: string;
62
+ scope: string;
63
+ refresh_token?: string;
64
+ }
40
65
  declare class OpenKey {
41
66
  private host;
42
67
  private appName;
@@ -61,6 +86,47 @@ declare class OpenKey {
61
86
  * Check if user is already authenticated
62
87
  */
63
88
  isConnected(): Promise<boolean>;
89
+ /**
90
+ * OAuth 2.1 methods for third-party app authentication
91
+ * Use this when your app has registered OAuth client credentials with OpenKey
92
+ */
93
+ oauth: {
94
+ /**
95
+ * Start OAuth authorization flow
96
+ * Opens popup/redirect to OpenKey authorization endpoint
97
+ * @returns Promise that resolves with authorization code after user consent
98
+ */
99
+ connect: (config: OAuthConfig) => Promise<OAuthResult>;
100
+ /**
101
+ * Exchange authorization code for tokens
102
+ * Call this after receiving the callback with authorization code
103
+ * @param code - Authorization code from callback URL
104
+ * @param config - Same config used for connect()
105
+ * @returns Promise that resolves with access_token, id_token, etc.
106
+ */
107
+ exchangeCode: (code: string, config: OAuthConfig) => Promise<OAuthTokenResponse>;
108
+ /**
109
+ * Verify state parameter from callback
110
+ * Call this before exchangeCode to prevent CSRF attacks
111
+ * @param receivedState - State parameter from callback URL
112
+ * @returns true if state matches, false otherwise
113
+ */
114
+ verifyState: (receivedState: string) => boolean;
115
+ /**
116
+ * Parse authorization response from callback URL
117
+ * @param url - Callback URL (defaults to current window.location.href)
118
+ * @returns Parsed code and state, or error
119
+ */
120
+ parseCallback: (url?: string) => {
121
+ code: string;
122
+ state: string;
123
+ } | {
124
+ error: string;
125
+ errorDescription?: string;
126
+ };
127
+ };
128
+ private openOAuthFlow;
129
+ private openOAuthPopup;
64
130
  private openFlow;
65
131
  private openPopup;
66
132
  private openIframe;
@@ -72,4 +138,4 @@ declare class OpenKey {
72
138
 
73
139
  declare function getOpenKey(config?: OpenKeyConfig): OpenKey;
74
140
 
75
- export { type AuthResult, OpenKey, type OpenKeyConfig, type OpenKeyError, type SignMessageRequest, type SignResult, type SignTypedDataRequest, OpenKey as default, getOpenKey };
141
+ export { type AuthResult, type OAuthConfig, type OAuthResult, type OAuthTokenResponse, OpenKey, type OpenKeyConfig, type OpenKeyError, type SignMessageRequest, type SignResult, type SignTypedDataRequest, OpenKey as default, getOpenKey };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- var h="https://openkey.so";var c=class{host;appName;usePopup;popup=null;iframe=null;constructor(e={}){this.host=e.host||h,this.appName=e.appName||window.location.hostname,this.usePopup=e.usePopup??!0}async connect(){return this.openFlow("connect",{type:"openkey:auth:request",appName:this.appName})}async signMessage(e){return this.openFlow("sign",{type:"openkey:sign:request",message:e.message,keyId:e.keyId})}async signTypedData(e){return this.openFlow("sign-typed-data",{type:"openkey:signTypedData:request",data:e})}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}}async openFlow(e,a){return new Promise((i,t)=>{let r=`${this.host}/widget/${e}?origin=${encodeURIComponent(window.location.origin)}`;this.usePopup?this.openPopup(r,a,i,t):this.openIframe(r,a,i,t)})}openPopup(e,a,i,t){let r=window.screenX+(window.outerWidth-400)/2,d=window.screenY+(window.outerHeight-600)/2;if(this.popup=window.open(e,"openkey",`width=400,height=600,left=${r},top=${d},popup=true`),!this.popup){t({code:"POPUP_BLOCKED",message:"Popup was blocked. Please allow popups for this site."});return}let o=()=>{window.removeEventListener("message",p),clearInterval(g),clearTimeout(u)},g=setInterval(()=>{this.popup?.closed&&(o(),t({code:"USER_CANCELLED",message:"User closed the popup"}))},500),u=setTimeout(()=>{o(),this.popup?.close(),t({code:"TIMEOUT",message:"Request timed out"})},3e5),p=s=>{if(s.origin!==this.host)return;let n=s.data;if(n.type==="openkey:ready"){this.popup?.postMessage(a,this.host);return}if(n.type==="openkey:close"){o(),this.popup?.close(),t({code:"USER_CANCELLED",message:"User cancelled the request"});return}(n.type==="openkey:auth:response"||n.type==="openkey:sign:response"||n.type==="openkey:signTypedData:response")&&(o(),this.popup?.close(),n.success?n.type==="openkey:auth:response"?i({address:n.address,keyId:n.keyId}):i({signature:n.signature,address:n.address}):t(n.error))};window.addEventListener("message",p)}openIframe(e,a,i,t){let r=document.createElement("div");r.style.cssText=`
1
+ var w="https://openkey.so";var l="openkey_oauth";function m(a){return btoa(String.fromCharCode(...a)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function E(){let a=new Uint8Array(32);return crypto.getRandomValues(a),m(a)}async function O(a){let s=new TextEncoder().encode(a),n=await crypto.subtle.digest("SHA-256",s);return m(new Uint8Array(n))}function U(){let a=new Uint8Array(16);return crypto.getRandomValues(a),m(a)}var h=class{host;appName;usePopup;popup=null;iframe=null;constructor(t={}){this.host=t.host||w,this.appName=t.appName||window.location.hostname,this.usePopup=t.usePopup??!0}async connect(){return this.openFlow("connect",{type:"openkey:auth:request",appName:this.appName})}async signMessage(t){return this.openFlow("sign",{type:"openkey:sign:request",message:t.message,keyId:t.keyId})}async signTypedData(t){return this.openFlow("sign-typed-data",{type:"openkey:signTypedData:request",data:t})}async isConnected(){try{let t=await fetch(`${this.host}/api/auth/session`,{credentials:"include"});return t.ok?!!(await t.json()).user:!1}catch{return!1}}oauth={connect:async t=>{let s=await E(),n=await O(s),r=t.state||U();sessionStorage.setItem(l,JSON.stringify({verifier:s,state:r}));let e=new URL(`${this.host}/api/auth/oauth2/authorize`);return e.searchParams.set("client_id",t.clientId),e.searchParams.set("redirect_uri",t.redirectUri),e.searchParams.set("response_type","code"),e.searchParams.set("scope","openid"),e.searchParams.set("state",r),e.searchParams.set("code_challenge",n),e.searchParams.set("code_challenge_method","S256"),this.openOAuthFlow(e.toString(),r,t.redirectUri)},exchangeCode:async(t,s)=>{let n=sessionStorage.getItem(l);if(!n)throw new Error("No PKCE verifier found. Start a new OAuth flow.");let{verifier:r}=JSON.parse(n),e=await fetch(`${this.host}/api/auth/oauth2/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:t,redirect_uri:s.redirectUri,client_id:s.clientId,code_verifier:r})});if(sessionStorage.removeItem(l),!e.ok){let d=await e.json();throw new Error(d.error_description||"Token exchange failed")}return e.json()},verifyState:t=>{let s=sessionStorage.getItem(l);if(!s)return!1;let{state:n}=JSON.parse(s);return n===t},parseCallback:t=>{let s=new URL(t||window.location.href),n=s.searchParams.get("error");if(n)return{error:n,errorDescription:s.searchParams.get("error_description")||void 0};let r=s.searchParams.get("code"),e=s.searchParams.get("state");return!r||!e?{error:"missing_params",errorDescription:"Missing code or state parameter"}:{code:r,state:e}}};openOAuthFlow(t,s,n){return new Promise((r,e)=>{this.usePopup?this.openOAuthPopup(t,s,n,r,e):window.location.href=t})}openOAuthPopup(t,s,n,r,e){let d=window.screenX+(window.outerWidth-400)/2,p=window.screenY+(window.outerHeight-600)/2;if(this.popup=window.open(t,"openkey-oauth",`width=400,height=600,left=${d},top=${p},popup=true`),!this.popup){e({code:"POPUP_BLOCKED",message:"Popup was blocked. Please allow popups or use redirect mode."});return}let g=new URL(n).origin,u=setInterval(()=>{try{if(this.popup?.closed){clearInterval(u),clearTimeout(c),e({code:"USER_CANCELLED",message:"User closed the popup"});return}let o=this.popup?.location?.href;if(o&&o.startsWith(g)){clearInterval(u),clearTimeout(c);let i=new URL(o),f=i.searchParams.get("code"),P=i.searchParams.get("state"),T=i.searchParams.get("error");if(this.popup?.close(),T){e({code:"UNAUTHORIZED",message:i.searchParams.get("error_description")||T});return}if(P!==s){e({code:"STATE_MISMATCH",message:"State mismatch - possible CSRF attack"});return}f?r({code:f,state:P}):e({code:"UNAUTHORIZED",message:"Authorization failed - no code returned"})}}catch{}},100),c=setTimeout(()=>{clearInterval(u),this.popup?.close(),e({code:"TIMEOUT",message:"OAuth flow timed out"})},3e5)}async openFlow(t,s){return new Promise((n,r)=>{let e=`${this.host}/widget/${t}?origin=${encodeURIComponent(window.location.origin)}`;this.usePopup?this.openPopup(e,s,n,r):this.openIframe(e,s,n,r)})}openPopup(t,s,n,r){let e=window.screenX+(window.outerWidth-400)/2,d=window.screenY+(window.outerHeight-600)/2;if(this.popup=window.open(t,"openkey",`width=400,height=600,left=${e},top=${d},popup=true`),!this.popup){r({code:"POPUP_BLOCKED",message:"Popup was blocked. Please allow popups for this site."});return}let p=()=>{window.removeEventListener("message",c),clearInterval(g),clearTimeout(u)},g=setInterval(()=>{this.popup?.closed&&(p(),r({code:"USER_CANCELLED",message:"User closed the popup"}))},500),u=setTimeout(()=>{p(),this.popup?.close(),r({code:"TIMEOUT",message:"Request timed out"})},3e5),c=o=>{if(o.origin!==this.host)return;let i=o.data;if(i.type==="openkey:ready"){this.popup?.postMessage(s,this.host);return}if(i.type==="openkey:close"){p(),this.popup?.close(),r({code:"USER_CANCELLED",message:"User cancelled the request"});return}(i.type==="openkey:auth:response"||i.type==="openkey:sign:response"||i.type==="openkey:signTypedData:response")&&(p(),this.popup?.close(),i.success?i.type==="openkey:auth:response"?n({address:i.address,keyId:i.keyId}):n({signature:i.signature,address:i.address}):r(i.error))};window.addEventListener("message",c)}openIframe(t,s,n,r){let e=document.createElement("div");e.style.cssText=`
2
2
  position: fixed;
3
3
  inset: 0;
4
4
  background: rgba(0, 0, 0, 0.5);
@@ -13,9 +13,9 @@ var h="https://openkey.so";var c=class{host;appName;usePopup;popup=null;iframe=n
13
13
  border-radius: 12px;
14
14
  overflow: hidden;
15
15
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
16
- `,this.iframe=document.createElement("iframe"),this.iframe.src=e,this.iframe.style.cssText=`
16
+ `,this.iframe=document.createElement("iframe"),this.iframe.src=t,this.iframe.style.cssText=`
17
17
  width: 100%;
18
18
  height: 100%;
19
19
  border: none;
20
- `,d.appendChild(this.iframe),r.appendChild(d),document.body.appendChild(r);let o=()=>{window.removeEventListener("message",u),clearTimeout(g),r.remove(),this.iframe=null};r.addEventListener("click",p=>{p.target===r&&(o(),t({code:"USER_CANCELLED",message:"User cancelled the request"}))});let g=setTimeout(()=>{o(),t({code:"TIMEOUT",message:"Request timed out"})},3e5),u=p=>{if(p.origin!==this.host)return;let s=p.data;if(s.type==="openkey:ready"){this.iframe?.contentWindow?.postMessage(a,this.host);return}if(s.type==="openkey:close"){o(),t({code:"USER_CANCELLED",message:"User cancelled the request"});return}(s.type==="openkey:auth:response"||s.type==="openkey:sign:response"||s.type==="openkey:signTypedData:response")&&(o(),s.success?s.type==="openkey:auth:response"?i({address:s.address,keyId:s.keyId}):i({signature:s.signature,address:s.address}):t(s.error))};window.addEventListener("message",u)}disconnect(){this.popup?.close(),this.popup=null,this.iframe?.parentElement?.parentElement?.remove(),this.iframe=null}},m=c,l=null;function T(y){return(!l||y)&&(l=new c(y)),l}export{c as OpenKey,m as default,T as getOpenKey};
20
+ `,d.appendChild(this.iframe),e.appendChild(d),document.body.appendChild(e);let p=()=>{window.removeEventListener("message",u),clearTimeout(g),e.remove(),this.iframe=null};e.addEventListener("click",c=>{c.target===e&&(p(),r({code:"USER_CANCELLED",message:"User cancelled the request"}))});let g=setTimeout(()=>{p(),r({code:"TIMEOUT",message:"Request timed out"})},3e5),u=c=>{if(c.origin!==this.host)return;let o=c.data;if(o.type==="openkey:ready"){this.iframe?.contentWindow?.postMessage(s,this.host);return}if(o.type==="openkey:close"){p(),r({code:"USER_CANCELLED",message:"User cancelled the request"});return}(o.type==="openkey:auth:response"||o.type==="openkey:sign:response"||o.type==="openkey:signTypedData:response")&&(p(),o.success?o.type==="openkey:auth:response"?n({address:o.address,keyId:o.keyId}):n({signature:o.signature,address:o.address}):r(o.error))};window.addEventListener("message",u)}disconnect(){this.popup?.close(),this.popup=null,this.iframe?.parentElement?.parentElement?.remove(),this.iframe=null}},k=h,y=null;function I(a){return(!y||a)&&(y=new h(a)),y}export{h as OpenKey,k as default,I as getOpenKey};
21
21
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// OpenKey SDK - Browser client for third-party apps\n// Provides \"Sign with OpenKey\" functionality via popup or iframe\n\nexport interface OpenKeyConfig {\n /** OpenKey host URL (default: https://openkey.so) */\n host?: string;\n /** App identifier for display */\n appName?: string;\n /** Use popup instead of iframe (default: true) */\n usePopup?: boolean;\n}\n\nexport interface SignMessageRequest {\n message: string;\n keyId?: string; // If not provided, user selects\n}\n\nexport interface SignTypedDataRequest {\n domain: {\n name?: string;\n version?: string;\n chainId?: number;\n verifyingContract?: string;\n };\n types: Record<string, Array<{ name: string; type: string }>>;\n primaryType: string;\n message: Record<string, unknown>;\n keyId?: string;\n}\n\nexport interface SignResult {\n signature: string;\n address: string;\n}\n\nexport interface AuthResult {\n address: string;\n keyId: string;\n}\n\nexport interface OpenKeyError {\n code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN';\n message: string;\n}\n\ntype MessageType =\n | { type: 'openkey:auth:request'; appName: string }\n | { type: 'openkey:auth:response'; success: true; address: string; keyId: string }\n | { type: 'openkey:auth:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:sign:request'; message: string; keyId?: string }\n | { type: 'openkey:sign:response'; success: true; signature: string; address: string }\n | { type: 'openkey:sign:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:signTypedData:request'; data: SignTypedDataRequest }\n | { type: 'openkey:signTypedData:response'; success: true; signature: string; address: string }\n | { type: 'openkey:signTypedData:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:ready' }\n | { type: 'openkey:close' };\n\nconst DEFAULT_HOST = 'https://openkey.so';\nconst POPUP_WIDTH = 400;\nconst POPUP_HEIGHT = 600;\nconst DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes\n\nexport class OpenKey {\n private host: string;\n private appName: string;\n private usePopup: boolean;\n private popup: Window | null = null;\n private iframe: HTMLIFrameElement | null = null;\n\n constructor(config: OpenKeyConfig = {}) {\n this.host = config.host || DEFAULT_HOST;\n this.appName = config.appName || window.location.hostname;\n this.usePopup = config.usePopup ?? true;\n }\n\n /**\n * Connect to OpenKey and get user's wallet address\n * Opens auth flow for user to select/create a key\n */\n async connect(): Promise<AuthResult> {\n return this.openFlow<AuthResult>('connect', {\n type: 'openkey:auth:request',\n appName: this.appName,\n });\n }\n\n /**\n * Sign a message with the user's OpenKey wallet\n */\n async signMessage(request: SignMessageRequest): Promise<SignResult> {\n return this.openFlow<SignResult>('sign', {\n type: 'openkey:sign:request',\n message: request.message,\n keyId: request.keyId,\n });\n }\n\n /**\n * Sign typed data (EIP-712) with the user's OpenKey wallet\n */\n async signTypedData(request: SignTypedDataRequest): Promise<SignResult> {\n return this.openFlow<SignResult>('sign-typed-data', {\n type: 'openkey:signTypedData:request',\n data: request,\n });\n }\n\n /**\n * Check if user is already authenticated\n */\n async isConnected(): Promise<boolean> {\n // Check for existing session via API\n try {\n const res = await fetch(`${this.host}/api/auth/session`, {\n credentials: 'include',\n });\n if (!res.ok) return false;\n const data = await res.json();\n return !!data.user;\n } catch {\n return false;\n }\n }\n\n private async openFlow<T>(action: string, message: object): Promise<T> {\n return new Promise((resolve, reject) => {\n const url = `${this.host}/widget/${action}?origin=${encodeURIComponent(window.location.origin)}`;\n\n if (this.usePopup) {\n this.openPopup(url, message, resolve, reject);\n } else {\n this.openIframe(url, message, resolve, reject);\n }\n });\n }\n\n private openPopup<T>(\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Calculate popup position\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n this.popup = window.open(\n url,\n 'openkey',\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!this.popup) {\n reject({\n code: 'POPUP_BLOCKED',\n message: 'Popup was blocked. Please allow popups for this site.',\n });\n return;\n }\n\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearInterval(pollClosed);\n clearTimeout(timeout);\n };\n\n // Poll to detect popup close\n const pollClosed = setInterval(() => {\n if (this.popup?.closed) {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n });\n }\n }, 500);\n\n // Timeout\n const timeout = setTimeout(() => {\n cleanup();\n this.popup?.close();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n }, DEFAULT_TIMEOUT);\n\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n // Widget is ready, send our request\n this.popup?.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n cleanup();\n this.popup?.close();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n return;\n }\n\n // Handle responses\n if (\n data.type === 'openkey:auth:response' ||\n data.type === 'openkey:sign:response' ||\n data.type === 'openkey:signTypedData:response'\n ) {\n cleanup();\n this.popup?.close();\n\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else {\n resolve({ signature: data.signature, address: data.address } as T);\n }\n } else {\n reject(data.error);\n }\n }\n };\n\n window.addEventListener('message', handleMessage);\n }\n\n private openIframe<T>(\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Create overlay\n const overlay = document.createElement('div');\n overlay.style.cssText = `\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 999999;\n `;\n\n // Create iframe container\n const container = document.createElement('div');\n container.style.cssText = `\n width: ${POPUP_WIDTH}px;\n height: ${POPUP_HEIGHT}px;\n background: #1a1a1a;\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);\n `;\n\n // Create iframe\n this.iframe = document.createElement('iframe');\n this.iframe.src = url;\n this.iframe.style.cssText = `\n width: 100%;\n height: 100%;\n border: none;\n `;\n\n container.appendChild(this.iframe);\n overlay.appendChild(container);\n document.body.appendChild(overlay);\n\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearTimeout(timeout);\n overlay.remove();\n this.iframe = null;\n };\n\n // Close on overlay click\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n }\n });\n\n // Timeout\n const timeout = setTimeout(() => {\n cleanup();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n }, DEFAULT_TIMEOUT);\n\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n this.iframe?.contentWindow?.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n return;\n }\n\n // Handle responses\n if (\n data.type === 'openkey:auth:response' ||\n data.type === 'openkey:sign:response' ||\n data.type === 'openkey:signTypedData:response'\n ) {\n cleanup();\n\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else {\n resolve({ signature: data.signature, address: data.address } as T);\n }\n } else {\n reject(data.error);\n }\n }\n };\n\n window.addEventListener('message', handleMessage);\n }\n\n /**\n * Disconnect and close any open flows\n */\n disconnect() {\n this.popup?.close();\n this.popup = null;\n this.iframe?.parentElement?.parentElement?.remove();\n this.iframe = null;\n }\n}\n\n// Default export for convenience\nexport default OpenKey;\n\n// Create singleton instance\nlet defaultInstance: OpenKey | null = null;\n\nexport function getOpenKey(config?: OpenKeyConfig): OpenKey {\n if (!defaultInstance || config) {\n defaultInstance = new OpenKey(config);\n }\n return defaultInstance;\n}\n"],"mappings":"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","names":["DEFAULT_HOST","OpenKey","config","DEFAULT_HOST","request","res","action","message","resolve","reject","url","left","top","cleanup","handleMessage","pollClosed","timeout","event","data","overlay","container","e","index_default","defaultInstance","getOpenKey"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// OpenKey SDK - Browser client for third-party apps\n// Provides \"Sign with OpenKey\" functionality via popup or iframe\n\nexport interface OpenKeyConfig {\n /** OpenKey host URL (default: https://openkey.so) */\n host?: string;\n /** App identifier for display */\n appName?: string;\n /** Use popup instead of iframe (default: true) */\n usePopup?: boolean;\n}\n\nexport interface SignMessageRequest {\n message: string;\n keyId?: string; // If not provided, user selects\n}\n\nexport interface SignTypedDataRequest {\n domain: {\n name?: string;\n version?: string;\n chainId?: number;\n verifyingContract?: string;\n };\n types: Record<string, Array<{ name: string; type: string }>>;\n primaryType: string;\n message: Record<string, unknown>;\n keyId?: string;\n}\n\nexport interface SignResult {\n signature: string;\n address: string;\n}\n\nexport interface AuthResult {\n address: string;\n keyId: string;\n}\n\nexport interface OpenKeyError {\n code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN' | 'STATE_MISMATCH';\n message: string;\n}\n\n// ======= OAuth 2.1 Types =======\n\n/** OAuth configuration for third-party apps */\nexport interface OAuthConfig {\n /** OAuth client_id (registered with OpenKey) */\n clientId: string;\n /** Redirect URI (must match registered URI) */\n redirectUri: string;\n /** State parameter for CSRF protection (auto-generated if not provided) */\n state?: string;\n}\n\n/** Result from OAuth authorization */\nexport interface OAuthResult {\n /** Authorization code (exchange for tokens) */\n code: string;\n /** State parameter (verify matches request) */\n state: string;\n}\n\n/** Response from token exchange */\nexport interface OAuthTokenResponse {\n access_token: string;\n token_type: 'Bearer';\n expires_in: number;\n id_token: string;\n scope: string;\n refresh_token?: string;\n}\n\ntype MessageType =\n | { type: 'openkey:auth:request'; appName: string }\n | { type: 'openkey:auth:response'; success: true; address: string; keyId: string }\n | { type: 'openkey:auth:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:sign:request'; message: string; keyId?: string }\n | { type: 'openkey:sign:response'; success: true; signature: string; address: string }\n | { type: 'openkey:sign:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:signTypedData:request'; data: SignTypedDataRequest }\n | { type: 'openkey:signTypedData:response'; success: true; signature: string; address: string }\n | { type: 'openkey:signTypedData:response'; success: false; error: OpenKeyError }\n | { type: 'openkey:ready' }\n | { type: 'openkey:close' };\n\nconst DEFAULT_HOST = 'https://openkey.so';\nconst POPUP_WIDTH = 400;\nconst POPUP_HEIGHT = 600;\nconst DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes\nconst OAUTH_STORAGE_KEY = 'openkey_oauth';\n\n// ======= PKCE Utilities =======\n\nfunction base64UrlEncode(buffer: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...buffer));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nasync function generateCodeVerifier(): Promise<string> {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(new Uint8Array(digest));\n}\n\nfunction generateState(): string {\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\nexport class OpenKey {\n private host: string;\n private appName: string;\n private usePopup: boolean;\n private popup: Window | null = null;\n private iframe: HTMLIFrameElement | null = null;\n\n constructor(config: OpenKeyConfig = {}) {\n this.host = config.host || DEFAULT_HOST;\n this.appName = config.appName || window.location.hostname;\n this.usePopup = config.usePopup ?? true;\n }\n\n /**\n * Connect to OpenKey and get user's wallet address\n * Opens auth flow for user to select/create a key\n */\n async connect(): Promise<AuthResult> {\n return this.openFlow<AuthResult>('connect', {\n type: 'openkey:auth:request',\n appName: this.appName,\n });\n }\n\n /**\n * Sign a message with the user's OpenKey wallet\n */\n async signMessage(request: SignMessageRequest): Promise<SignResult> {\n return this.openFlow<SignResult>('sign', {\n type: 'openkey:sign:request',\n message: request.message,\n keyId: request.keyId,\n });\n }\n\n /**\n * Sign typed data (EIP-712) with the user's OpenKey wallet\n */\n async signTypedData(request: SignTypedDataRequest): Promise<SignResult> {\n return this.openFlow<SignResult>('sign-typed-data', {\n type: 'openkey:signTypedData:request',\n data: request,\n });\n }\n\n /**\n * Check if user is already authenticated\n */\n async isConnected(): Promise<boolean> {\n // Check for existing session via API\n try {\n const res = await fetch(`${this.host}/api/auth/session`, {\n credentials: 'include',\n });\n if (!res.ok) return false;\n const data = await res.json();\n return !!data.user;\n } catch {\n return false;\n }\n }\n\n // ======= OAuth 2.1 Provider Methods =======\n\n /**\n * OAuth 2.1 methods for third-party app authentication\n * Use this when your app has registered OAuth client credentials with OpenKey\n */\n oauth = {\n /**\n * Start OAuth authorization flow\n * Opens popup/redirect to OpenKey authorization endpoint\n * @returns Promise that resolves with authorization code after user consent\n */\n connect: async (config: OAuthConfig): Promise<OAuthResult> => {\n // Generate PKCE values\n const verifier = await generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n const state = config.state || generateState();\n\n // Store for later token exchange\n sessionStorage.setItem(\n OAUTH_STORAGE_KEY,\n JSON.stringify({ verifier, state })\n );\n\n // Build authorization URL\n const authUrl = new URL(`${this.host}/api/auth/oauth2/authorize`);\n authUrl.searchParams.set('client_id', config.clientId);\n authUrl.searchParams.set('redirect_uri', config.redirectUri);\n authUrl.searchParams.set('response_type', 'code');\n authUrl.searchParams.set('scope', 'openid');\n authUrl.searchParams.set('state', state);\n authUrl.searchParams.set('code_challenge', challenge);\n authUrl.searchParams.set('code_challenge_method', 'S256');\n\n return this.openOAuthFlow(authUrl.toString(), state, config.redirectUri);\n },\n\n /**\n * Exchange authorization code for tokens\n * Call this after receiving the callback with authorization code\n * @param code - Authorization code from callback URL\n * @param config - Same config used for connect()\n * @returns Promise that resolves with access_token, id_token, etc.\n */\n exchangeCode: async (\n code: string,\n config: OAuthConfig\n ): Promise<OAuthTokenResponse> => {\n const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);\n if (!stored) {\n throw new Error('No PKCE verifier found. Start a new OAuth flow.');\n }\n\n const { verifier } = JSON.parse(stored);\n\n const response = await fetch(`${this.host}/api/auth/oauth2/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n code_verifier: verifier,\n }),\n });\n\n // Clear stored verifier\n sessionStorage.removeItem(OAUTH_STORAGE_KEY);\n\n if (!response.ok) {\n const error = await response.json();\n throw new Error(error.error_description || 'Token exchange failed');\n }\n\n return response.json();\n },\n\n /**\n * Verify state parameter from callback\n * Call this before exchangeCode to prevent CSRF attacks\n * @param receivedState - State parameter from callback URL\n * @returns true if state matches, false otherwise\n */\n verifyState: (receivedState: string): boolean => {\n const stored = sessionStorage.getItem(OAUTH_STORAGE_KEY);\n if (!stored) return false;\n const { state } = JSON.parse(stored);\n return state === receivedState;\n },\n\n /**\n * Parse authorization response from callback URL\n * @param url - Callback URL (defaults to current window.location.href)\n * @returns Parsed code and state, or error\n */\n parseCallback: (\n url?: string\n ): { code: string; state: string } | { error: string; errorDescription?: string } => {\n const urlObj = new URL(url || window.location.href);\n const error = urlObj.searchParams.get('error');\n\n if (error) {\n return {\n error,\n errorDescription: urlObj.searchParams.get('error_description') || undefined,\n };\n }\n\n const code = urlObj.searchParams.get('code');\n const state = urlObj.searchParams.get('state');\n\n if (!code || !state) {\n return { error: 'missing_params', errorDescription: 'Missing code or state parameter' };\n }\n\n return { code, state };\n },\n };\n\n private openOAuthFlow(\n url: string,\n state: string,\n redirectUri: string\n ): Promise<OAuthResult> {\n return new Promise((resolve, reject) => {\n if (this.usePopup) {\n this.openOAuthPopup(url, state, redirectUri, resolve, reject);\n } else {\n // Full page redirect - won't resolve, page navigates away\n window.location.href = url;\n }\n });\n }\n\n private openOAuthPopup(\n url: string,\n state: string,\n redirectUri: string,\n resolve: (value: OAuthResult) => void,\n reject: (error: OpenKeyError) => void\n ) {\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n this.popup = window.open(\n url,\n 'openkey-oauth',\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!this.popup) {\n reject({\n code: 'POPUP_BLOCKED',\n message: 'Popup was blocked. Please allow popups or use redirect mode.',\n });\n return;\n }\n\n const redirectHost = new URL(redirectUri).origin;\n\n // Poll for redirect back with code\n const pollInterval = setInterval(() => {\n try {\n if (this.popup?.closed) {\n clearInterval(pollInterval);\n clearTimeout(timeout);\n reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n });\n return;\n }\n\n // Check if redirected to callback URL\n const popupUrl = this.popup?.location?.href;\n if (popupUrl && popupUrl.startsWith(redirectHost)) {\n clearInterval(pollInterval);\n clearTimeout(timeout);\n\n const urlObj = new URL(popupUrl);\n const code = urlObj.searchParams.get('code');\n const returnedState = urlObj.searchParams.get('state');\n const error = urlObj.searchParams.get('error');\n\n this.popup?.close();\n\n if (error) {\n reject({\n code: 'UNAUTHORIZED',\n message: urlObj.searchParams.get('error_description') || error,\n });\n return;\n }\n\n if (returnedState !== state) {\n reject({\n code: 'STATE_MISMATCH',\n message: 'State mismatch - possible CSRF attack',\n });\n return;\n }\n\n if (code) {\n resolve({ code, state: returnedState });\n } else {\n reject({\n code: 'UNAUTHORIZED',\n message: 'Authorization failed - no code returned',\n });\n }\n }\n } catch {\n // Cross-origin - popup still on OpenKey domain, continue polling\n }\n }, 100);\n\n // Timeout\n const timeout = setTimeout(() => {\n clearInterval(pollInterval);\n this.popup?.close();\n reject({\n code: 'TIMEOUT',\n message: 'OAuth flow timed out',\n });\n }, DEFAULT_TIMEOUT);\n }\n\n private async openFlow<T>(action: string, message: object): Promise<T> {\n return new Promise((resolve, reject) => {\n const url = `${this.host}/widget/${action}?origin=${encodeURIComponent(window.location.origin)}`;\n\n if (this.usePopup) {\n this.openPopup(url, message, resolve, reject);\n } else {\n this.openIframe(url, message, resolve, reject);\n }\n });\n }\n\n private openPopup<T>(\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Calculate popup position\n const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;\n const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;\n\n this.popup = window.open(\n url,\n 'openkey',\n `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`\n );\n\n if (!this.popup) {\n reject({\n code: 'POPUP_BLOCKED',\n message: 'Popup was blocked. Please allow popups for this site.',\n });\n return;\n }\n\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearInterval(pollClosed);\n clearTimeout(timeout);\n };\n\n // Poll to detect popup close\n const pollClosed = setInterval(() => {\n if (this.popup?.closed) {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User closed the popup',\n });\n }\n }, 500);\n\n // Timeout\n const timeout = setTimeout(() => {\n cleanup();\n this.popup?.close();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n }, DEFAULT_TIMEOUT);\n\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n // Widget is ready, send our request\n this.popup?.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n cleanup();\n this.popup?.close();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n return;\n }\n\n // Handle responses\n if (\n data.type === 'openkey:auth:response' ||\n data.type === 'openkey:sign:response' ||\n data.type === 'openkey:signTypedData:response'\n ) {\n cleanup();\n this.popup?.close();\n\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else {\n resolve({ signature: data.signature, address: data.address } as T);\n }\n } else {\n reject(data.error);\n }\n }\n };\n\n window.addEventListener('message', handleMessage);\n }\n\n private openIframe<T>(\n url: string,\n message: object,\n resolve: (value: T) => void,\n reject: (error: OpenKeyError) => void\n ) {\n // Create overlay\n const overlay = document.createElement('div');\n overlay.style.cssText = `\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 999999;\n `;\n\n // Create iframe container\n const container = document.createElement('div');\n container.style.cssText = `\n width: ${POPUP_WIDTH}px;\n height: ${POPUP_HEIGHT}px;\n background: #1a1a1a;\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);\n `;\n\n // Create iframe\n this.iframe = document.createElement('iframe');\n this.iframe.src = url;\n this.iframe.style.cssText = `\n width: 100%;\n height: 100%;\n border: none;\n `;\n\n container.appendChild(this.iframe);\n overlay.appendChild(container);\n document.body.appendChild(overlay);\n\n const cleanup = () => {\n window.removeEventListener('message', handleMessage);\n clearTimeout(timeout);\n overlay.remove();\n this.iframe = null;\n };\n\n // Close on overlay click\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n }\n });\n\n // Timeout\n const timeout = setTimeout(() => {\n cleanup();\n reject({\n code: 'TIMEOUT',\n message: 'Request timed out',\n });\n }, DEFAULT_TIMEOUT);\n\n // Listen for messages\n const handleMessage = (event: MessageEvent) => {\n if (event.origin !== this.host) return;\n\n const data = event.data as MessageType;\n\n if (data.type === 'openkey:ready') {\n this.iframe?.contentWindow?.postMessage(message, this.host);\n return;\n }\n\n if (data.type === 'openkey:close') {\n cleanup();\n reject({\n code: 'USER_CANCELLED',\n message: 'User cancelled the request',\n });\n return;\n }\n\n // Handle responses\n if (\n data.type === 'openkey:auth:response' ||\n data.type === 'openkey:sign:response' ||\n data.type === 'openkey:signTypedData:response'\n ) {\n cleanup();\n\n if (data.success) {\n if (data.type === 'openkey:auth:response') {\n resolve({ address: data.address, keyId: data.keyId } as T);\n } else {\n resolve({ signature: data.signature, address: data.address } as T);\n }\n } else {\n reject(data.error);\n }\n }\n };\n\n window.addEventListener('message', handleMessage);\n }\n\n /**\n * Disconnect and close any open flows\n */\n disconnect() {\n this.popup?.close();\n this.popup = null;\n this.iframe?.parentElement?.parentElement?.remove();\n this.iframe = null;\n }\n}\n\n// Default export for convenience\nexport default OpenKey;\n\n// Create singleton instance\nlet defaultInstance: OpenKey | null = null;\n\nexport function getOpenKey(config?: OpenKeyConfig): OpenKey {\n if (!defaultInstance || config) {\n defaultInstance = new OpenKey(config);\n }\n return defaultInstance;\n}\n"],"mappings":"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","names":["DEFAULT_HOST","OAUTH_STORAGE_KEY","base64UrlEncode","buffer","generateCodeVerifier","array","generateCodeChallenge","verifier","data","digest","generateState","OpenKey","config","DEFAULT_HOST","request","res","challenge","state","authUrl","code","stored","response","error","receivedState","url","urlObj","redirectUri","resolve","reject","left","top","redirectHost","pollInterval","timeout","popupUrl","returnedState","action","message","cleanup","handleMessage","pollClosed","event","overlay","container","e","index_default","defaultInstance","getOpenKey"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openkey/sdk",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -12,6 +12,13 @@
12
12
  "types": "./dist/index.d.ts"
13
13
  }
14
14
  },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
15
22
  "scripts": {
16
23
  "dev": "tsup --watch",
17
24
  "build": "tsup",
@@ -1,79 +0,0 @@
1
- $ tsup
2
- ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
3
-
4
- package.json:12:6:
5
- 12 │ "types": "./dist/index.d.ts"
6
- ╵ ~~~~~~~
7
-
8
- The "import" condition comes earlier and will be used for all "import" statements:
9
-
10
- package.json:10:6:
11
- 10 │ "import": "./dist/index.js",
12
- ╵ ~~~~~~~~
13
-
14
- The "require" condition comes earlier and will be used for all "require" calls:
15
-
16
- package.json:11:6:
17
- 11 │ "require": "./dist/index.cjs",
18
- ╵ ~~~~~~~~~
19
-
20
- CLI Building entry: src/index.ts
21
- CLI Using tsconfig: tsconfig.json
22
- CLI tsup v8.5.1
23
- CLI Using tsup config: /Users/samgbafa/Documents/github/tinycloud/openkey/packages/sdk/tsup.config.ts
24
- CLI Target: es2022
25
- CLI Cleaning output folder
26
- ESM Build start
27
- CJS Build start
28
-
29
- WARN ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
30
-
31
- package.json:12:6:
32
-  12 │ "types": "./dist/index.d.ts"
33
- ╵ ~~~~~~~
34
-
35
- The "import" condition comes earlier and will be used for all "import" statements:
36
-
37
- package.json:10:6:
38
-  10 │ "import": "./dist/index.js",
39
- ╵ ~~~~~~~~
40
-
41
- The "require" condition comes earlier and will be used for all "require" calls:
42
-
43
- package.json:11:6:
44
-  11 │ "require": "./dist/index.cjs",
45
- ╵ ~~~~~~~~~
46
-
47
-
48
-
49
-
50
- WARN ▲ [WARNING] The condition "types" here will never be used as it comes after both "import" and "require" [package.json]
51
-
52
- package.json:12:6:
53
-  12 │ "types": "./dist/index.d.ts"
54
- ╵ ~~~~~~~
55
-
56
- The "import" condition comes earlier and will be used for all "import" statements:
57
-
58
- package.json:10:6:
59
-  10 │ "import": "./dist/index.js",
60
- ╵ ~~~~~~~~
61
-
62
- The "require" condition comes earlier and will be used for all "require" calls:
63
-
64
- package.json:11:6:
65
-  11 │ "require": "./dist/index.cjs",
66
- ╵ ~~~~~~~~~
67
-
68
-
69
-
70
- CJS dist/index.cjs 4.24 KB
71
- CJS dist/index.cjs.map 13.89 KB
72
- CJS ⚡️ Build success in 50ms
73
- ESM dist/index.js 3.76 KB
74
- ESM dist/index.js.map 13.74 KB
75
- ESM ⚡️ Build success in 51ms
76
- DTS Build start
77
- DTS ⚡️ Build success in 2139ms
78
- DTS dist/index.d.ts 2.00 KB
79
- DTS dist/index.d.cts 2.00 KB
package/src/index.ts DELETED
@@ -1,369 +0,0 @@
1
- // OpenKey SDK - Browser client for third-party apps
2
- // Provides "Sign with OpenKey" functionality via popup or iframe
3
-
4
- export interface OpenKeyConfig {
5
- /** OpenKey host URL (default: https://openkey.so) */
6
- host?: string;
7
- /** App identifier for display */
8
- appName?: string;
9
- /** Use popup instead of iframe (default: true) */
10
- usePopup?: boolean;
11
- }
12
-
13
- export interface SignMessageRequest {
14
- message: string;
15
- keyId?: string; // If not provided, user selects
16
- }
17
-
18
- export interface SignTypedDataRequest {
19
- domain: {
20
- name?: string;
21
- version?: string;
22
- chainId?: number;
23
- verifyingContract?: string;
24
- };
25
- types: Record<string, Array<{ name: string; type: string }>>;
26
- primaryType: string;
27
- message: Record<string, unknown>;
28
- keyId?: string;
29
- }
30
-
31
- export interface SignResult {
32
- signature: string;
33
- address: string;
34
- }
35
-
36
- export interface AuthResult {
37
- address: string;
38
- keyId: string;
39
- }
40
-
41
- export interface OpenKeyError {
42
- code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN';
43
- message: string;
44
- }
45
-
46
- type MessageType =
47
- | { type: 'openkey:auth:request'; appName: string }
48
- | { type: 'openkey:auth:response'; success: true; address: string; keyId: string }
49
- | { type: 'openkey:auth:response'; success: false; error: OpenKeyError }
50
- | { type: 'openkey:sign:request'; message: string; keyId?: string }
51
- | { type: 'openkey:sign:response'; success: true; signature: string; address: string }
52
- | { type: 'openkey:sign:response'; success: false; error: OpenKeyError }
53
- | { type: 'openkey:signTypedData:request'; data: SignTypedDataRequest }
54
- | { type: 'openkey:signTypedData:response'; success: true; signature: string; address: string }
55
- | { type: 'openkey:signTypedData:response'; success: false; error: OpenKeyError }
56
- | { type: 'openkey:ready' }
57
- | { type: 'openkey:close' };
58
-
59
- const DEFAULT_HOST = 'https://openkey.so';
60
- const POPUP_WIDTH = 400;
61
- const POPUP_HEIGHT = 600;
62
- const DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes
63
-
64
- export class OpenKey {
65
- private host: string;
66
- private appName: string;
67
- private usePopup: boolean;
68
- private popup: Window | null = null;
69
- private iframe: HTMLIFrameElement | null = null;
70
-
71
- constructor(config: OpenKeyConfig = {}) {
72
- this.host = config.host || DEFAULT_HOST;
73
- this.appName = config.appName || window.location.hostname;
74
- this.usePopup = config.usePopup ?? true;
75
- }
76
-
77
- /**
78
- * Connect to OpenKey and get user's wallet address
79
- * Opens auth flow for user to select/create a key
80
- */
81
- async connect(): Promise<AuthResult> {
82
- return this.openFlow<AuthResult>('connect', {
83
- type: 'openkey:auth:request',
84
- appName: this.appName,
85
- });
86
- }
87
-
88
- /**
89
- * Sign a message with the user's OpenKey wallet
90
- */
91
- async signMessage(request: SignMessageRequest): Promise<SignResult> {
92
- return this.openFlow<SignResult>('sign', {
93
- type: 'openkey:sign:request',
94
- message: request.message,
95
- keyId: request.keyId,
96
- });
97
- }
98
-
99
- /**
100
- * Sign typed data (EIP-712) with the user's OpenKey wallet
101
- */
102
- async signTypedData(request: SignTypedDataRequest): Promise<SignResult> {
103
- return this.openFlow<SignResult>('sign-typed-data', {
104
- type: 'openkey:signTypedData:request',
105
- data: request,
106
- });
107
- }
108
-
109
- /**
110
- * Check if user is already authenticated
111
- */
112
- async isConnected(): Promise<boolean> {
113
- // Check for existing session via API
114
- try {
115
- const res = await fetch(`${this.host}/api/auth/session`, {
116
- credentials: 'include',
117
- });
118
- if (!res.ok) return false;
119
- const data = await res.json();
120
- return !!data.user;
121
- } catch {
122
- return false;
123
- }
124
- }
125
-
126
- private async openFlow<T>(action: string, message: object): Promise<T> {
127
- return new Promise((resolve, reject) => {
128
- const url = `${this.host}/widget/${action}?origin=${encodeURIComponent(window.location.origin)}`;
129
-
130
- if (this.usePopup) {
131
- this.openPopup(url, message, resolve, reject);
132
- } else {
133
- this.openIframe(url, message, resolve, reject);
134
- }
135
- });
136
- }
137
-
138
- private openPopup<T>(
139
- url: string,
140
- message: object,
141
- resolve: (value: T) => void,
142
- reject: (error: OpenKeyError) => void
143
- ) {
144
- // Calculate popup position
145
- const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
146
- const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
147
-
148
- this.popup = window.open(
149
- url,
150
- 'openkey',
151
- `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
152
- );
153
-
154
- if (!this.popup) {
155
- reject({
156
- code: 'POPUP_BLOCKED',
157
- message: 'Popup was blocked. Please allow popups for this site.',
158
- });
159
- return;
160
- }
161
-
162
- const cleanup = () => {
163
- window.removeEventListener('message', handleMessage);
164
- clearInterval(pollClosed);
165
- clearTimeout(timeout);
166
- };
167
-
168
- // Poll to detect popup close
169
- const pollClosed = setInterval(() => {
170
- if (this.popup?.closed) {
171
- cleanup();
172
- reject({
173
- code: 'USER_CANCELLED',
174
- message: 'User closed the popup',
175
- });
176
- }
177
- }, 500);
178
-
179
- // Timeout
180
- const timeout = setTimeout(() => {
181
- cleanup();
182
- this.popup?.close();
183
- reject({
184
- code: 'TIMEOUT',
185
- message: 'Request timed out',
186
- });
187
- }, DEFAULT_TIMEOUT);
188
-
189
- // Listen for messages
190
- const handleMessage = (event: MessageEvent) => {
191
- if (event.origin !== this.host) return;
192
-
193
- const data = event.data as MessageType;
194
-
195
- if (data.type === 'openkey:ready') {
196
- // Widget is ready, send our request
197
- this.popup?.postMessage(message, this.host);
198
- return;
199
- }
200
-
201
- if (data.type === 'openkey:close') {
202
- cleanup();
203
- this.popup?.close();
204
- reject({
205
- code: 'USER_CANCELLED',
206
- message: 'User cancelled the request',
207
- });
208
- return;
209
- }
210
-
211
- // Handle responses
212
- if (
213
- data.type === 'openkey:auth:response' ||
214
- data.type === 'openkey:sign:response' ||
215
- data.type === 'openkey:signTypedData:response'
216
- ) {
217
- cleanup();
218
- this.popup?.close();
219
-
220
- if (data.success) {
221
- if (data.type === 'openkey:auth:response') {
222
- resolve({ address: data.address, keyId: data.keyId } as T);
223
- } else {
224
- resolve({ signature: data.signature, address: data.address } as T);
225
- }
226
- } else {
227
- reject(data.error);
228
- }
229
- }
230
- };
231
-
232
- window.addEventListener('message', handleMessage);
233
- }
234
-
235
- private openIframe<T>(
236
- url: string,
237
- message: object,
238
- resolve: (value: T) => void,
239
- reject: (error: OpenKeyError) => void
240
- ) {
241
- // Create overlay
242
- const overlay = document.createElement('div');
243
- overlay.style.cssText = `
244
- position: fixed;
245
- inset: 0;
246
- background: rgba(0, 0, 0, 0.5);
247
- display: flex;
248
- align-items: center;
249
- justify-content: center;
250
- z-index: 999999;
251
- `;
252
-
253
- // Create iframe container
254
- const container = document.createElement('div');
255
- container.style.cssText = `
256
- width: ${POPUP_WIDTH}px;
257
- height: ${POPUP_HEIGHT}px;
258
- background: #1a1a1a;
259
- border-radius: 12px;
260
- overflow: hidden;
261
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
262
- `;
263
-
264
- // Create iframe
265
- this.iframe = document.createElement('iframe');
266
- this.iframe.src = url;
267
- this.iframe.style.cssText = `
268
- width: 100%;
269
- height: 100%;
270
- border: none;
271
- `;
272
-
273
- container.appendChild(this.iframe);
274
- overlay.appendChild(container);
275
- document.body.appendChild(overlay);
276
-
277
- const cleanup = () => {
278
- window.removeEventListener('message', handleMessage);
279
- clearTimeout(timeout);
280
- overlay.remove();
281
- this.iframe = null;
282
- };
283
-
284
- // Close on overlay click
285
- overlay.addEventListener('click', (e) => {
286
- if (e.target === overlay) {
287
- cleanup();
288
- reject({
289
- code: 'USER_CANCELLED',
290
- message: 'User cancelled the request',
291
- });
292
- }
293
- });
294
-
295
- // Timeout
296
- const timeout = setTimeout(() => {
297
- cleanup();
298
- reject({
299
- code: 'TIMEOUT',
300
- message: 'Request timed out',
301
- });
302
- }, DEFAULT_TIMEOUT);
303
-
304
- // Listen for messages
305
- const handleMessage = (event: MessageEvent) => {
306
- if (event.origin !== this.host) return;
307
-
308
- const data = event.data as MessageType;
309
-
310
- if (data.type === 'openkey:ready') {
311
- this.iframe?.contentWindow?.postMessage(message, this.host);
312
- return;
313
- }
314
-
315
- if (data.type === 'openkey:close') {
316
- cleanup();
317
- reject({
318
- code: 'USER_CANCELLED',
319
- message: 'User cancelled the request',
320
- });
321
- return;
322
- }
323
-
324
- // Handle responses
325
- if (
326
- data.type === 'openkey:auth:response' ||
327
- data.type === 'openkey:sign:response' ||
328
- data.type === 'openkey:signTypedData:response'
329
- ) {
330
- cleanup();
331
-
332
- if (data.success) {
333
- if (data.type === 'openkey:auth:response') {
334
- resolve({ address: data.address, keyId: data.keyId } as T);
335
- } else {
336
- resolve({ signature: data.signature, address: data.address } as T);
337
- }
338
- } else {
339
- reject(data.error);
340
- }
341
- }
342
- };
343
-
344
- window.addEventListener('message', handleMessage);
345
- }
346
-
347
- /**
348
- * Disconnect and close any open flows
349
- */
350
- disconnect() {
351
- this.popup?.close();
352
- this.popup = null;
353
- this.iframe?.parentElement?.parentElement?.remove();
354
- this.iframe = null;
355
- }
356
- }
357
-
358
- // Default export for convenience
359
- export default OpenKey;
360
-
361
- // Create singleton instance
362
- let defaultInstance: OpenKey | null = null;
363
-
364
- export function getOpenKey(config?: OpenKeyConfig): OpenKey {
365
- if (!defaultInstance || config) {
366
- defaultInstance = new OpenKey(config);
367
- }
368
- return defaultInstance;
369
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "lib": ["ES2022", "DOM"]
7
- },
8
- "include": ["src/**/*"],
9
- "exclude": ["node_modules", "dist"]
10
- }
package/tsup.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from 'tsup';
2
-
3
- export default defineConfig({
4
- entry: ['src/index.ts'],
5
- format: ['esm', 'cjs'],
6
- dts: true,
7
- clean: true,
8
- sourcemap: true,
9
- minify: true,
10
- });