@openkey/sdk 0.8.7 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -20,19 +20,332 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ KeyIdTypeMismatchError: () => KeyIdTypeMismatchError,
23
24
  OpenKey: () => OpenKey,
25
+ OpenKeyNostr: () => OpenKeyNostr,
24
26
  OpenKeyProvider: () => OpenKeyProvider,
25
27
  default: () => index_default,
26
- getOpenKey: () => getOpenKey
28
+ getOpenKey: () => getOpenKey,
29
+ resolveAuthorizeTinyCloudRouting: () => resolveAuthorizeTinyCloudRouting,
30
+ shouldRouteAuthorizeTinyCloudExternal: () => shouldRouteAuthorizeTinyCloudExternal,
31
+ validateIframeResize: () => validateIframeResize
27
32
  });
28
33
  module.exports = __toCommonJS(index_exports);
29
- var import_core = require("@openkey/core");
34
+
35
+ // ../core/dist/index.js
36
+ var u = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
37
+ function g(e) {
38
+ let r = "", n = e.length;
39
+ for (let t = 0; t < n; t += 3) {
40
+ let c = e[t], d = t + 1 < n ? e[t + 1] : 0, i = t + 2 < n ? e[t + 2] : 0, o = c << 16 | d << 8 | i;
41
+ r += u[o >> 18 & 63], r += u[o >> 12 & 63], r += t + 1 < n ? u[o >> 6 & 63] : "", r += t + 2 < n ? u[o & 63] : "";
42
+ }
43
+ return r.replace(/\+/g, "-").replace(/\//g, "_");
44
+ }
45
+ function y() {
46
+ let e = new Uint8Array(32);
47
+ return crypto.getRandomValues(e), g(e);
48
+ }
49
+ async function x(e) {
50
+ let n = new TextEncoder().encode(e), t = await crypto.subtle.digest("SHA-256", n);
51
+ return new Uint8Array(t);
52
+ }
53
+ async function k(e, r) {
54
+ let t = await (r ?? x)(e);
55
+ return g(t);
56
+ }
57
+ function m() {
58
+ let e = new Uint8Array(16);
59
+ return crypto.getRandomValues(e), g(e);
60
+ }
61
+ function T(e) {
62
+ let { host: r, clientId: n, redirectUri: t, codeChallenge: c, state: d, scopes: i, authorizePath: o = "/api/auth/oauth2/authorize" } = e, s = new URL(o, r);
63
+ return s.searchParams.set("client_id", n), s.searchParams.set("redirect_uri", t), s.searchParams.set("response_type", "code"), s.searchParams.set("scope", i.join(" ")), s.searchParams.set("state", d), s.searchParams.set("code_challenge", c), s.searchParams.set("code_challenge_method", "S256"), s.toString();
64
+ }
65
+
66
+ // src/nostr.ts
67
+ var NOSTR_FRAME_TIMEOUT_MS = 5 * 60 * 1e3;
68
+ var NOSTR_PROTOCOL_VERSION = 1;
69
+ function newRequestId() {
70
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
71
+ return crypto.randomUUID();
72
+ }
73
+ if (typeof crypto === "undefined" || typeof crypto.getRandomValues !== "function") {
74
+ throw new Error("Secure request-id generation is unavailable.");
75
+ }
76
+ const bytes = new Uint32Array(4);
77
+ crypto.getRandomValues(bytes);
78
+ return `nostr-${Array.from(bytes, (value) => value.toString(16)).join("")}`;
79
+ }
80
+ var NostrFrame = class {
81
+ root;
82
+ shadow;
83
+ iframe;
84
+ outer;
85
+ host;
86
+ messageHandler = () => {
87
+ };
88
+ revealed = false;
89
+ requestedHeight = 400;
90
+ viewportHandler = () => {
91
+ this.syncViewport();
92
+ this.applyFrameHeight();
93
+ };
94
+ constructor(host, url) {
95
+ this.host = host;
96
+ this.root = document.createElement("div");
97
+ this.shadow = this.root.attachShadow({ mode: "closed" });
98
+ const style = document.createElement("style");
99
+ style.textContent = `
100
+ :host{all:initial}
101
+ .nf-outer{position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none}
102
+ .nf-outer.nf-visible{position:fixed;top:var(--nf-viewport-top,0px);left:var(--nf-viewport-left,0px);width:var(--nf-viewport-width,100vw);height:var(--nf-viewport-height,100dvh);box-sizing:border-box;padding:max(16px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));overflow:auto;overscroll-behavior:contain;opacity:1;pointer-events:auto;background:rgba(0,0,0,0.42);z-index:999999;display:flex;align-items:center;justify-content:center}
103
+ .nf-card{width:1px;height:1px;overflow:hidden}
104
+ .nf-visible .nf-card{background:#fafafa;width:min(400px,100%);height:auto;max-height:100%;border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);overflow:auto;flex:0 1 auto}
105
+ iframe{border:none;width:1px;height:1px;display:block;transition:height 200ms ease}
106
+ .nf-visible iframe{width:100%;height:400px;max-height:100%}
107
+ @media (prefers-reduced-motion:reduce){iframe{transition:none}}
108
+ `;
109
+ this.outer = document.createElement("div");
110
+ this.outer.className = "nf-outer";
111
+ this.outer.setAttribute("role", "dialog");
112
+ this.outer.setAttribute("aria-modal", "true");
113
+ this.outer.setAttribute("aria-label", "OpenKey approval");
114
+ this.outer.setAttribute("aria-hidden", "true");
115
+ const card = document.createElement("div");
116
+ card.className = "nf-card";
117
+ this.iframe = document.createElement("iframe");
118
+ this.iframe.src = url;
119
+ this.iframe.title = "OpenKey approval";
120
+ this.iframe.setAttribute("allow", "clipboard-write *; publickey-credentials-get *; publickey-credentials-create *");
121
+ this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox");
122
+ card.appendChild(this.iframe);
123
+ this.outer.appendChild(card);
124
+ this.shadow.appendChild(style);
125
+ this.shadow.appendChild(this.outer);
126
+ document.body.appendChild(this.root);
127
+ this.syncViewport();
128
+ window.addEventListener("resize", this.viewportHandler);
129
+ window.visualViewport?.addEventListener("resize", this.viewportHandler);
130
+ window.visualViewport?.addEventListener("scroll", this.viewportHandler);
131
+ }
132
+ onMessage(handler) {
133
+ this.messageHandler = (event) => {
134
+ if (event.origin !== this.host) return;
135
+ if (event.source !== this.iframe.contentWindow) return;
136
+ handler(event.data);
137
+ };
138
+ window.addEventListener("message", this.messageHandler);
139
+ }
140
+ reveal() {
141
+ if (this.revealed) return;
142
+ this.revealed = true;
143
+ this.syncViewport();
144
+ this.outer.classList.add("nf-visible");
145
+ this.outer.setAttribute("aria-hidden", "false");
146
+ this.applyFrameHeight();
147
+ }
148
+ resize(height) {
149
+ if (!Number.isFinite(height) || height <= 0) return;
150
+ this.requestedHeight = height;
151
+ if (this.revealed) this.applyFrameHeight();
152
+ }
153
+ syncViewport() {
154
+ const viewport = window.visualViewport;
155
+ this.outer.style.setProperty("--nf-viewport-top", `${viewport?.offsetTop ?? 0}px`);
156
+ this.outer.style.setProperty("--nf-viewport-left", `${viewport?.offsetLeft ?? 0}px`);
157
+ this.outer.style.setProperty("--nf-viewport-width", `${viewport?.width ?? window.innerWidth}px`);
158
+ this.outer.style.setProperty("--nf-viewport-height", `${viewport?.height ?? window.innerHeight}px`);
159
+ }
160
+ applyFrameHeight() {
161
+ if (!this.revealed) return;
162
+ const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
163
+ const max = Math.max(1, Math.floor(viewportHeight - 32));
164
+ this.iframe.style.height = `${Math.min(this.requestedHeight, max)}px`;
165
+ }
166
+ postMessage(message) {
167
+ this.iframe.contentWindow?.postMessage(message, this.host);
168
+ }
169
+ destroy() {
170
+ window.removeEventListener("message", this.messageHandler);
171
+ window.removeEventListener("resize", this.viewportHandler);
172
+ window.visualViewport?.removeEventListener("resize", this.viewportHandler);
173
+ window.visualViewport?.removeEventListener("scroll", this.viewportHandler);
174
+ this.root.remove();
175
+ }
176
+ };
177
+ var OpenKeyNostr = class {
178
+ constructor(host) {
179
+ this.host = host;
180
+ }
181
+ /**
182
+ * Get-or-create the user's OpenKey-custodied Nostr identity. Always shows
183
+ * a visible consent card (identity creation, or - when `relayUrl` is
184
+ * given - a relay-scoped kind:22242/NIP-42 grant for that exact relay).
185
+ * Never resolves with the secret key; only `keyId`/`pubkey`/`npub`.
186
+ */
187
+ async connect(opts) {
188
+ const url = this.widgetUrl();
189
+ return this.run(
190
+ url,
191
+ { type: "openkey:nostr:connect:request", relayUrl: opts?.relayUrl },
192
+ (data, resolve, reject) => {
193
+ if (data.type !== "openkey:nostr:connect:response") return false;
194
+ if (data.success) resolve({ keyId: data.keyId, pubkey: data.pubkey, npub: data.npub });
195
+ else reject(data.error);
196
+ return true;
197
+ }
198
+ );
199
+ }
200
+ /**
201
+ * Sign a NIP-01 event template with the user's OpenKey Nostr key. Silent
202
+ * (no visible UI) when an active grant already covers this client origin
203
+ * + key + kind (+ exact relay, for kind 22242). Otherwise shows a visible
204
+ * consent card before signing - required for every first kind:9 message to
205
+ * a new origin, per NIP-29 channel-message policy.
206
+ */
207
+ async signEvent(keyId, event) {
208
+ const url = this.widgetUrl();
209
+ return this.run(
210
+ url,
211
+ { type: "openkey:nostr:sign:request", keyId, event },
212
+ (data, resolve, reject) => {
213
+ if (data.type !== "openkey:nostr:sign:response") return false;
214
+ if (data.success) resolve(data.event);
215
+ else reject(data.error);
216
+ return true;
217
+ }
218
+ );
219
+ }
220
+ widgetUrl() {
221
+ const origin = encodeURIComponent(window.location.origin);
222
+ return `${this.host}/widget/embed/nostr/approve?origin=${origin}`;
223
+ }
224
+ run(url, request, onData) {
225
+ return new Promise((resolve, reject) => {
226
+ let settled = false;
227
+ const frame = new NostrFrame(this.host, url);
228
+ const requestId = newRequestId();
229
+ const settle = (fn) => {
230
+ if (settled) return;
231
+ settled = true;
232
+ clearTimeout(timeout);
233
+ fn();
234
+ frame.destroy();
235
+ };
236
+ frame.onMessage((data) => {
237
+ if (!data || typeof data.type !== "string") return;
238
+ if (data.type === "openkey:ready") {
239
+ if (data.protocolVersion !== NOSTR_PROTOCOL_VERSION) return;
240
+ frame.postMessage({ ...request, requestId, protocolVersion: NOSTR_PROTOCOL_VERSION });
241
+ return;
242
+ }
243
+ if (data.requestId !== requestId || data.protocolVersion !== NOSTR_PROTOCOL_VERSION) return;
244
+ if (data.type === "openkey:resize") {
245
+ frame.resize(data.height);
246
+ return;
247
+ }
248
+ if (data.type === "openkey:nostr:show") {
249
+ frame.reveal();
250
+ return;
251
+ }
252
+ if (data.type === "openkey:close") {
253
+ settle(() => reject({ code: "USER_CANCELLED", message: "Request closed" }));
254
+ return;
255
+ }
256
+ onData(data, (v) => settle(() => resolve(v)), (e) => settle(() => reject(e)));
257
+ });
258
+ const timeout = setTimeout(() => {
259
+ settle(() => reject({ code: "TIMEOUT", message: "Nostr request timed out" }));
260
+ }, NOSTR_FRAME_TIMEOUT_MS);
261
+ });
262
+ }
263
+ };
264
+
265
+ // src/index.ts
30
266
  var DEFAULT_HOST = "https://openkey.so";
31
267
  var POPUP_WIDTH = 400;
32
268
  var POPUP_HEIGHT = 600;
33
269
  var DEFAULT_TIMEOUT = 5 * 60 * 1e3;
34
270
  var IFRAME_READY_TIMEOUT = 3e3;
35
271
  var OAUTH_STORAGE_KEY = "openkey_oauth";
272
+ function validateIframeResize(incoming, expected) {
273
+ if (!incoming || typeof incoming !== "object") return null;
274
+ const data = incoming;
275
+ if (data.type !== "openkey:resize") return null;
276
+ if (expected.requestId === null || expected.protocolVersion === null) return null;
277
+ if (typeof data.protocolVersion !== "number" || data.protocolVersion !== expected.protocolVersion) {
278
+ return null;
279
+ }
280
+ if (typeof data.requestId !== "string" || data.requestId !== expected.requestId) {
281
+ return null;
282
+ }
283
+ if (typeof data.height !== "number" || !Number.isFinite(data.height) || data.height <= 0) {
284
+ return null;
285
+ }
286
+ return Math.min(data.height, Math.floor(expected.viewportHeight * 0.85));
287
+ }
288
+ function shouldRouteAuthorizeTinyCloudExternal(state) {
289
+ return state.lastAuth?.keyType === "EXTERNAL";
290
+ }
291
+ var KeyIdTypeMismatchError = class extends Error {
292
+ code;
293
+ requestKeyId;
294
+ lastAuthKeyId;
295
+ requestKeyType;
296
+ lastAuthKeyType;
297
+ constructor(fields) {
298
+ super(fields.message);
299
+ this.name = "KeyIdTypeMismatchError";
300
+ this.code = "KEY_ID_TYPE_MISMATCH";
301
+ this.requestKeyId = fields.requestKeyId;
302
+ this.lastAuthKeyId = fields.lastAuthKeyId;
303
+ this.requestKeyType = fields.requestKeyType;
304
+ this.lastAuthKeyType = fields.lastAuthKeyType;
305
+ }
306
+ };
307
+ function resolveAuthorizeTinyCloudRouting(state) {
308
+ const lastAuthKeyId = state.lastAuth?.keyId ?? null;
309
+ const lastAuthKeyType = state.lastAuth?.keyType ?? null;
310
+ const rawRequestKeyId = state.requestKeyId ?? null;
311
+ const requestKeyId = rawRequestKeyId && rawRequestKeyId.length > 0 ? rawRequestKeyId : null;
312
+ const knownKeys = state.knownKeys ?? [];
313
+ const lookupType = (id) => {
314
+ for (const k2 of knownKeys) {
315
+ if (k2.id === id) return k2.keyType;
316
+ if (k2.address && k2.address.toLowerCase() === id.toLowerCase()) return k2.keyType;
317
+ }
318
+ if (id.startsWith("external:")) return "EXTERNAL";
319
+ return null;
320
+ };
321
+ if (!requestKeyId) {
322
+ const route = lastAuthKeyType === "EXTERNAL" ? "external" : "managed";
323
+ return {
324
+ route,
325
+ resolvedKeyId: lastAuthKeyId,
326
+ resolvedKeyType: lastAuthKeyType
327
+ };
328
+ }
329
+ let requestKeyType = lookupType(requestKeyId);
330
+ if (!requestKeyType && lastAuthKeyId && requestKeyId === lastAuthKeyId) {
331
+ requestKeyType = lastAuthKeyType;
332
+ }
333
+ if (requestKeyType && lastAuthKeyType && requestKeyType !== lastAuthKeyType) {
334
+ throw new KeyIdTypeMismatchError({
335
+ message: `authorizeTinyCloud: request.keyId ${requestKeyId} resolves to ${requestKeyType} but active session key ${lastAuthKeyId} is ${lastAuthKeyType}; refuse to route silently`,
336
+ requestKeyId,
337
+ lastAuthKeyId,
338
+ requestKeyType,
339
+ lastAuthKeyType
340
+ });
341
+ }
342
+ const effectiveType = requestKeyType ?? lastAuthKeyType;
343
+ return {
344
+ route: effectiveType === "EXTERNAL" ? "external" : "managed",
345
+ resolvedKeyId: requestKeyId,
346
+ resolvedKeyType: effectiveType
347
+ };
348
+ }
36
349
  var IframeModal = class {
37
350
  root;
38
351
  shadow;
@@ -41,6 +354,13 @@ var IframeModal = class {
41
354
  onMessage;
42
355
  messageHandler;
43
356
  host;
357
+ // Sol final continuation contract requirement 5: iframe resize traffic
358
+ // MUST correlate to the active request's requestId AND protocolVersion.
359
+ // Set by `setExpectedCorrelation` after the outer flow decides which
360
+ // request this modal is bound to. When both are null, resize messages
361
+ // are dropped (no active request → no resize authority).
362
+ expectedRequestId = null;
363
+ expectedProtocolVersion = null;
44
364
  constructor(opts) {
45
365
  this.host = opts.host;
46
366
  this.onClose = opts.onClose;
@@ -86,7 +406,7 @@ var IframeModal = class {
86
406
  this.iframe = document.createElement("iframe");
87
407
  this.iframe.src = opts.url;
88
408
  this.iframe.setAttribute("allow", "clipboard-write *; publickey-credentials-get *; publickey-credentials-create *");
89
- this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups");
409
+ this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox");
90
410
  card.appendChild(this.iframe);
91
411
  backdrop.appendChild(card);
92
412
  this.shadow.appendChild(style);
@@ -96,10 +416,17 @@ var IframeModal = class {
96
416
  if (event.source !== this.iframe.contentWindow) return;
97
417
  const data = event.data;
98
418
  if (data.type === "openkey:resize") {
99
- const h = Math.min(data.height, Math.floor(window.innerHeight * 0.85));
419
+ const h = validateIframeResize(event.data, {
420
+ requestId: this.expectedRequestId,
421
+ protocolVersion: this.expectedProtocolVersion,
422
+ viewportHeight: window.innerHeight
423
+ });
424
+ if (h === null) return;
100
425
  this.iframe.style.height = `${h}px`;
101
426
  return;
102
427
  }
428
+ if (data.type === "openkey:close") {
429
+ }
103
430
  this.onMessage(data);
104
431
  };
105
432
  window.addEventListener("message", this.messageHandler);
@@ -108,6 +435,20 @@ var IframeModal = class {
108
435
  postMessage(message) {
109
436
  this.iframe.contentWindow?.postMessage(message, this.host);
110
437
  }
438
+ /**
439
+ * Sol final continuation contract requirement 5: bind the active
440
+ * request correlation so subsequent resize (and future correlated)
441
+ * messages can be verified against `requestId` + `protocolVersion`.
442
+ *
443
+ * The outer flow calls this immediately BEFORE posting the sign
444
+ * request into the iframe, so a resize that arrives synchronously
445
+ * after ready is correlated correctly. Passing `null` for either
446
+ * argument disables correlation (resize is dropped).
447
+ */
448
+ setExpectedCorrelation(requestId, protocolVersion) {
449
+ this.expectedRequestId = requestId;
450
+ this.expectedProtocolVersion = protocolVersion;
451
+ }
111
452
  destroy() {
112
453
  window.removeEventListener("message", this.messageHandler);
113
454
  this.root.remove();
@@ -241,12 +582,23 @@ var OpenKey = class {
241
582
  lastAuth = null;
242
583
  discoveredProviders = [];
243
584
  sessionToken = null;
585
+ /** Cancels widget flows that could outlive a sign-out on this instance. */
586
+ activeFlowCancellations = /* @__PURE__ */ new Set();
587
+ /**
588
+ * Nostr identity custody + signing (secp256k1 Schnorr / BIP-340). Kept
589
+ * fully separate from the Ethereum flows above: it never reuses
590
+ * `lastAuth`/`sessionToken`, and its widget messages never carry a
591
+ * sessionToken - the OpenKey session and any signing grants stay inside
592
+ * the OpenKey-origin iframe for the whole lifetime of this client.
593
+ */
594
+ nostr;
244
595
  constructor(config = {}) {
245
596
  this.config = config;
246
597
  this.host = config.host || DEFAULT_HOST;
247
598
  this.oauthHost = config.oauthHost || this.deriveOAuthHost(this.host);
248
599
  this.appName = config.appName || window.location.hostname;
249
600
  this.mode = config.mode ?? "iframe";
601
+ this.nostr = new OpenKeyNostr(this.host);
250
602
  if (typeof window !== "undefined") {
251
603
  this.discoveredProviders = [];
252
604
  window.addEventListener("eip6963:announceProvider", (event) => {
@@ -272,6 +624,37 @@ var OpenKey = class {
272
624
  return "https://api.openkey.so";
273
625
  }
274
626
  }
627
+ /**
628
+ * Sol MAJOR-4 (final continuation): fetch the caller's registered keys
629
+ * so `resolveAuthorizeTinyCloudRouting` can resolve an explicit pin
630
+ * whose keyId does not match the active session key. Called only when
631
+ * the caller passes a `requestKeyId` we do not already recognise; the
632
+ * common no-pin path skips the network round-trip.
633
+ *
634
+ * Failures do NOT block routing — we fall through with `knownKeys=[]`
635
+ * so the resolver's lastAuth-based fallback still runs. If the caller
636
+ * pinned a genuinely unknown key that turns out to be external, the
637
+ * widget rejects the server-signing path and the SDK surfaces that
638
+ * error from the finalize call rather than silently routing wrong.
639
+ */
640
+ async fetchKnownKeysIfNeeded(requestKeyId) {
641
+ if (!requestKeyId) return [];
642
+ const la = this.lastAuth;
643
+ if (la?.keyId === requestKeyId) return [];
644
+ if (la?.address && la.address.toLowerCase() === requestKeyId.toLowerCase()) return [];
645
+ if (requestKeyId.startsWith("external:")) return [];
646
+ try {
647
+ const res = await fetch(`${this.oauthHost}/api/keys`, {
648
+ method: "GET",
649
+ credentials: "include"
650
+ });
651
+ if (!res.ok) return [];
652
+ const body = await res.json();
653
+ return Array.isArray(body?.keys) ? body.keys : [];
654
+ } catch {
655
+ return [];
656
+ }
657
+ }
275
658
  /**
276
659
  * Connect to OpenKey and get user's wallet address
277
660
  * Opens auth flow for user to select/create a key
@@ -284,6 +667,55 @@ var OpenKey = class {
284
667
  this.lastAuth = result;
285
668
  return result;
286
669
  }
670
+ /**
671
+ * Signs the active OpenKey account out through the OpenKey-owned widget.
672
+ *
673
+ * The SDK passes its Better Auth bearer through the OpenKey-owned widget
674
+ * protocol so applications do not have to implement revocation themselves.
675
+ * Local SDK session state is cleared before opening the widget, including
676
+ * when remote revocation is unavailable.
677
+ */
678
+ async signOut(opts) {
679
+ const requestId = `ok-signout-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
680
+ const sessionToken = this.sessionToken || void 0;
681
+ const mode = this.resolveMode(opts?.mode) === "redirect" ? "popup" : opts?.mode;
682
+ this.cancelActiveFlows();
683
+ this.lastAuth = null;
684
+ this.sessionToken = null;
685
+ const acknowledgement = await this.openFlow(
686
+ "sign-out",
687
+ {
688
+ type: "openkey:sign-out:request",
689
+ requestId,
690
+ protocolVersion: 1,
691
+ sessionToken
692
+ },
693
+ mode
694
+ );
695
+ if (acknowledgement.requestId !== requestId) {
696
+ throw new Error("OpenKey signOut received an acknowledgement for a different request");
697
+ }
698
+ return acknowledgement;
699
+ }
700
+ /**
701
+ * Better-auth session token relayed during connect(), or null if the
702
+ * flow did not provide one. Use as a bearer token for API calls made
703
+ * on the user's behalf (e.g. TinyCloud auto-sign bootstrap signing).
704
+ */
705
+ getSessionToken() {
706
+ return this.sessionToken;
707
+ }
708
+ /**
709
+ * Endpoint + bearer token for a TinyCloud auto-sign signing strategy.
710
+ * The endpoint is on the OpenKey API host and only signs requests the
711
+ * server-side bootstrap allowlist permits.
712
+ */
713
+ tinycloudSigningOptions() {
714
+ return {
715
+ endpoint: `${this.oauthHost}/api/delegate/sign`,
716
+ token: this.sessionToken
717
+ };
718
+ }
287
719
  /**
288
720
  * Link an external wallet to the user's OpenKey account
289
721
  * Opens link-wallet widget flow
@@ -303,6 +735,208 @@ var OpenKey = class {
303
735
  }
304
736
  return this.signWithOpenKey(request, opts?.mode);
305
737
  }
738
+ /**
739
+ * Versioned TinyCloud authorization protocol (v1).
740
+ *
741
+ * Unlike `signMessage()` (which is contractually byte-exact — the returned
742
+ * signature always verifies against the caller's exact bytes), this method
743
+ * permits the user to narrow the requested capabilities before signing.
744
+ * OpenKey may therefore regenerate the SIWE. The response carries
745
+ * `signedMessage` — the exact bytes the signature actually verifies against.
746
+ *
747
+ * TinyCloud completes the session with `signedMessage`, not the original
748
+ * `siwe` passed in. Consumers MUST use the returned bytes and MUST NOT
749
+ * assume the returned grants are a superset of what they requested — the
750
+ * user is allowed to remove any grant that is not required.
751
+ *
752
+ * Legacy `signMessage()` callers are unaffected: their exact-byte contract
753
+ * remains preserved and OpenKey never rewrites their bytes.
754
+ */
755
+ async authorizeTinyCloud(request, opts) {
756
+ if (request.protocolVersion !== 1) {
757
+ throw new Error(
758
+ `authorizeTinyCloud only supports protocolVersion 1; got ${request.protocolVersion}`
759
+ );
760
+ }
761
+ if (typeof request.siwe !== "string" || !request.siwe) {
762
+ throw new Error("authorizeTinyCloud requires a non-empty SIWE");
763
+ }
764
+ let routing;
765
+ try {
766
+ const knownKeys = await this.fetchKnownKeysIfNeeded(request.keyId);
767
+ routing = resolveAuthorizeTinyCloudRouting({
768
+ lastAuth: this.lastAuth,
769
+ requestKeyId: request.keyId,
770
+ knownKeys
771
+ });
772
+ } catch (e) {
773
+ if (e instanceof KeyIdTypeMismatchError) throw e;
774
+ throw e instanceof Error ? e : new Error("authorizeTinyCloud: failed to resolve target key type");
775
+ }
776
+ if (routing.route === "external") {
777
+ return this.authorizeTinyCloudExternal(request, opts);
778
+ }
779
+ const requestId = `ok-auth-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
780
+ const resolvedKeyId = routing.resolvedKeyId ?? request.keyId ?? this.lastAuth?.keyId;
781
+ const raw = await this.openFlow(
782
+ "sign",
783
+ {
784
+ type: "openkey:sign:request",
785
+ requestId,
786
+ protocolVersion: 1,
787
+ message: request.siwe,
788
+ keyId: resolvedKeyId,
789
+ // Forward JWK so the widget can pass it to /authorize-sign for
790
+ // narrowed-SIWE regeneration.
791
+ jwk: request.jwk,
792
+ // Forward the TinyCloud host so the widget can bind it into the
793
+ // /authorize-sign-prepare context (Sol MAJOR-2).
794
+ host: request.host,
795
+ // Sol MAJOR-2 (envelope): forward the caller-supplied presentation
796
+ // envelope so the widget can render honest provenance and the
797
+ // /authorize-sign-prepare route can bind it into the authorization
798
+ // context. The envelope is display-only; the widget transport
799
+ // validates and size-bounds it before use and NEVER treats it as
800
+ // verified unless the server actually origin-binds a manifest.
801
+ presentation: request.presentation,
802
+ sessionToken: this.sessionToken || void 0
803
+ },
804
+ opts?.mode
805
+ );
806
+ if (typeof raw.signedMessage !== "string" || !raw.signedMessage) {
807
+ throw new Error(
808
+ "authorizeTinyCloud: widget returned no signedMessage \u2014 signature would not correspond to the displayed authorization"
809
+ );
810
+ }
811
+ if (!Array.isArray(raw.selectedActionKeys)) {
812
+ throw new Error(
813
+ "authorizeTinyCloud: widget returned no selectedActionKeys \u2014 cannot verify displayed authorization matches the signed bytes"
814
+ );
815
+ }
816
+ if (!Array.isArray(raw.permissions)) {
817
+ throw new Error(
818
+ "authorizeTinyCloud: widget returned no permissions \u2014 cannot verify displayed authorization matches the signed bytes"
819
+ );
820
+ }
821
+ return {
822
+ protocolVersion: 1,
823
+ address: raw.address,
824
+ signature: raw.signature,
825
+ signedMessage: raw.signedMessage,
826
+ selectedActionKeys: raw.selectedActionKeys,
827
+ permissions: raw.permissions
828
+ };
829
+ }
830
+ /**
831
+ * Sol MAJOR-3 (continuation): authorizeTinyCloud path for external keys.
832
+ * The wallet lives outside OpenKey, so the SDK opens the shared widget
833
+ * for review + preview, then invokes the user's wallet to sign the
834
+ * preview bytes, then finalizes server-side via /authorize-sign.
835
+ *
836
+ * Flow:
837
+ * 1. Open the widget with `externalSign: true` — the widget renders
838
+ * the shared SigningApproval UI, calls /authorize-sign-prepare,
839
+ * calls /authorize-sign-preview after the user approves a
840
+ * selection, and hands back `{ previewApprovalToken,
841
+ * signedMessage, selectedActionIds, address, authorizationContextToken }`.
842
+ * 2. SDK invokes the user's wallet on the returned signedMessage.
843
+ * 3. SDK POSTs /authorize-sign with `externalSignature`.
844
+ *
845
+ * Legacy exact-byte signMessage callers are unaffected — this path is
846
+ * only taken via authorizeTinyCloud (protocolVersion:1) when the target
847
+ * key is EXTERNAL. Sol's rejection required this path to go through
848
+ * the same review UI as managed keys.
849
+ */
850
+ async authorizeTinyCloudExternal(request, opts) {
851
+ const keyId = request.keyId ?? this.lastAuth?.keyId;
852
+ if (!keyId) {
853
+ throw new Error(
854
+ "authorizeTinyCloud (external) requires a keyId \u2014 call connect() first or pass request.keyId"
855
+ );
856
+ }
857
+ const walletAddress = this.lastAuth?.address;
858
+ if (!walletAddress) {
859
+ throw new Error(
860
+ "authorizeTinyCloud (external) requires a connected wallet \u2014 call connect() first"
861
+ );
862
+ }
863
+ const requestId = `ok-ext-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
864
+ const approval = await this.openFlow(
865
+ "sign",
866
+ {
867
+ type: "openkey:sign:request",
868
+ requestId,
869
+ protocolVersion: 1,
870
+ message: request.siwe,
871
+ keyId,
872
+ jwk: request.jwk,
873
+ host: request.host,
874
+ // Sol MAJOR-2 (envelope): forward the caller-supplied presentation
875
+ // envelope. Same rules as the managed path — display-only, size-
876
+ // bounded at the transport, never treated as verified without
877
+ // server-side origin-binding.
878
+ presentation: request.presentation,
879
+ sessionToken: this.sessionToken || void 0,
880
+ // Sol MAJOR-3 (continuation): tell the widget to hand us back
881
+ // the previewApproval instead of asking OpenKey to sign
882
+ // server-side with a managed key. The widget will still render
883
+ // the SAME SigningApproval component; it just diverges at the
884
+ // "sign" step and emits `openkey:externalSign:approve` back
885
+ // through the transport.
886
+ externalSign: true
887
+ },
888
+ opts?.mode
889
+ );
890
+ if (!approval?.previewApprovalToken || !approval?.signedMessage) {
891
+ throw new Error(
892
+ "authorizeTinyCloud (external): widget did not return a previewApprovalToken + signedMessage"
893
+ );
894
+ }
895
+ if (approval.address && approval.address.toLowerCase() !== walletAddress.toLowerCase()) {
896
+ throw new Error(
897
+ `authorizeTinyCloud (external): widget preview address ${approval.address} does not match connected wallet ${walletAddress}`
898
+ );
899
+ }
900
+ const provider = await this.findWalletProvider(walletAddress);
901
+ const hexMessage = this.toHex(approval.signedMessage);
902
+ const walletSignature = await provider.request({
903
+ method: "personal_sign",
904
+ params: [hexMessage, walletAddress]
905
+ });
906
+ const finalizeRes = await fetch(
907
+ `${this.oauthHost}/api/delegate/authorize-sign`,
908
+ {
909
+ method: "POST",
910
+ credentials: "include",
911
+ headers: { "Content-Type": "application/json" },
912
+ body: JSON.stringify({
913
+ authorizationContextToken: approval.authorizationContextToken,
914
+ previewApprovalToken: approval.previewApprovalToken,
915
+ selectedActionIds: approval.selectedActionIds,
916
+ protocolVersion: 1,
917
+ externalSignature: walletSignature
918
+ })
919
+ }
920
+ );
921
+ if (!finalizeRes.ok) {
922
+ const body = await finalizeRes.json().catch(() => ({ error: `HTTP ${finalizeRes.status}` }));
923
+ throw new Error(body.error || `authorize-sign failed (${finalizeRes.status})`);
924
+ }
925
+ const finalize = await finalizeRes.json();
926
+ if (finalize.signedMessage !== approval.signedMessage) {
927
+ throw new Error(
928
+ "authorizeTinyCloud (external): server signed different bytes than the preview showed"
929
+ );
930
+ }
931
+ return {
932
+ protocolVersion: 1,
933
+ address: finalize.address,
934
+ signature: finalize.signature,
935
+ signedMessage: finalize.signedMessage,
936
+ selectedActionKeys: finalize.selectedActionKeys ?? [],
937
+ permissions: finalize.permissions ?? []
938
+ };
939
+ }
306
940
  /**
307
941
  * Sign typed data (EIP-712) with the user's OpenKey wallet
308
942
  * For external keys, routes directly to the user's wallet provider
@@ -313,31 +947,40 @@ var OpenKey = class {
313
947
  }
314
948
  return this.signTypedDataWithOpenKey(request, opts?.mode);
315
949
  }
950
+ providerCandidates() {
951
+ const providers = [];
952
+ if (this.config.externalProvider) {
953
+ providers.push(this.config.externalProvider);
954
+ }
955
+ providers.push(...this.discoveredProviders.map(({ provider }) => provider));
956
+ if (typeof window !== "undefined" && window.ethereum) {
957
+ providers.push(window.ethereum);
958
+ }
959
+ return [...new Set(providers)];
960
+ }
961
+ async providerHasAccount(provider, method, targetAddress) {
962
+ try {
963
+ const accounts = await provider.request({ method });
964
+ return accounts.some((account) => account.toLowerCase() === targetAddress.toLowerCase());
965
+ } catch {
966
+ return false;
967
+ }
968
+ }
316
969
  /**
317
970
  * Find a wallet provider that controls the given address.
318
- * Checks: 1) app-provided externalProvider, 2) EIP-6963 discovered wallets, 3) window.ethereum
971
+ * Checks connected accounts first, then asks the wallet to authorize the
972
+ * app origin if the external key was linked elsewhere.
319
973
  */
320
974
  async findWalletProvider(targetAddress) {
321
- if (this.config.externalProvider) {
322
- return this.config.externalProvider;
323
- }
324
- for (const { provider } of this.discoveredProviders) {
325
- try {
326
- const accounts = await provider.request({ method: "eth_accounts" });
327
- if (accounts.some((a) => a.toLowerCase() === targetAddress.toLowerCase())) {
328
- return provider;
329
- }
330
- } catch {
975
+ const providers = this.providerCandidates();
976
+ for (const provider of providers) {
977
+ if (await this.providerHasAccount(provider, "eth_accounts", targetAddress)) {
978
+ return provider;
331
979
  }
332
980
  }
333
- if (typeof window !== "undefined" && window.ethereum) {
334
- const provider = window.ethereum;
335
- try {
336
- const accounts = await provider.request({ method: "eth_accounts" });
337
- if (accounts.some((a) => a.toLowerCase() === targetAddress.toLowerCase())) {
338
- return provider;
339
- }
340
- } catch {
981
+ for (const provider of providers) {
982
+ if (await this.providerHasAccount(provider, "eth_requestAccounts", targetAddress)) {
983
+ return provider;
341
984
  }
342
985
  }
343
986
  throw new ExternalWalletError("External wallet not found. Connect the wallet that owns this key and try again.");
@@ -428,14 +1071,14 @@ var OpenKey = class {
428
1071
  * @returns Promise that resolves with authorization code after user consent
429
1072
  */
430
1073
  connect: async (config) => {
431
- const verifier = (0, import_core.generateCodeVerifier)();
432
- const challenge = await (0, import_core.generateCodeChallenge)(verifier);
433
- const state = config.state || (0, import_core.generateState)();
1074
+ const verifier = y();
1075
+ const challenge = await k(verifier);
1076
+ const state = config.state || m();
434
1077
  sessionStorage.setItem(
435
1078
  OAUTH_STORAGE_KEY,
436
1079
  JSON.stringify({ verifier, state })
437
1080
  );
438
- const authUrl = (0, import_core.buildAuthorizationUrl)({
1081
+ const authUrl = T({
439
1082
  host: this.oauthHost,
440
1083
  clientId: config.clientId,
441
1084
  redirectUri: config.redirectUri,
@@ -607,7 +1250,7 @@ var OpenKey = class {
607
1250
  const eoaFlag = action === "connect" && this.hasDetectedEoa() ? "&hasEoa=true" : "";
608
1251
  if (mode === "popup") {
609
1252
  const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
610
- return new Promise((resolve, reject) => this.openPopup(url2, message, resolve, reject));
1253
+ return new Promise((resolve, reject) => this.openPopup(action, url2, message, resolve, reject));
611
1254
  }
612
1255
  if (mode === "redirect") {
613
1256
  const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
@@ -619,10 +1262,14 @@ var OpenKey = class {
619
1262
  return this.openIframeModal(url, action, message, origin);
620
1263
  }
621
1264
  openIframeModal(url, action, message, origin) {
1265
+ const outgoingRequestId = message.requestId;
1266
+ const outgoingProtocolVersion = message.protocolVersion;
1267
+ const isVersionedRequest = typeof outgoingRequestId === "string" && typeof outgoingProtocolVersion === "number" && outgoingProtocolVersion >= 1;
622
1268
  return new Promise((resolve, reject) => {
623
1269
  let readyReceived = false;
624
1270
  let settled = false;
625
1271
  let modal = null;
1272
+ let cancel;
626
1273
  const settle = (fn) => {
627
1274
  if (settled) return;
628
1275
  settled = true;
@@ -633,24 +1280,35 @@ var OpenKey = class {
633
1280
  clearTimeout(overallTimeout);
634
1281
  modal?.destroy();
635
1282
  modal = null;
1283
+ this.activeFlowCancellations.delete(cancel);
636
1284
  };
637
1285
  modal = new IframeModal({
638
1286
  url,
639
1287
  host: this.host,
640
1288
  onClose: () => {
641
1289
  settle(() => {
642
- clearTimeout(readyTimeout);
643
- clearTimeout(overallTimeout);
644
- modal = null;
1290
+ cleanup();
645
1291
  reject({ code: "USER_CANCELLED", message: "User cancelled the request" });
646
1292
  });
647
1293
  },
648
1294
  onMessage: (data) => {
649
1295
  if (data.type === "openkey:ready") {
650
1296
  readyReceived = true;
1297
+ if (isVersionedRequest && modal) {
1298
+ modal.setExpectedCorrelation(
1299
+ outgoingRequestId,
1300
+ outgoingProtocolVersion
1301
+ );
1302
+ }
651
1303
  modal?.postMessage(message);
652
1304
  return;
653
1305
  }
1306
+ if (isVersionedRequest) {
1307
+ const respRequestId = data.requestId;
1308
+ const respVersion = data.protocolVersion;
1309
+ if (respRequestId !== outgoingRequestId) return;
1310
+ if (respVersion !== outgoingProtocolVersion) return;
1311
+ }
654
1312
  if (data.type === "openkey:close") {
655
1313
  settle(() => {
656
1314
  cleanup();
@@ -667,7 +1325,7 @@ var OpenKey = class {
667
1325
  this.handleExternalWalletConnect(resolve, reject);
668
1326
  return;
669
1327
  }
670
- if (data.type === "openkey:auth:response" || data.type === "openkey:sign:response" || data.type === "openkey:signTypedData:response" || data.type === "openkey:link-wallet:response") {
1328
+ if (this.isResponseForAction(action, data)) {
671
1329
  settle(() => {
672
1330
  cleanup();
673
1331
  if (data.success) {
@@ -676,8 +1334,24 @@ var OpenKey = class {
676
1334
  resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
677
1335
  } else if (data.type === "openkey:link-wallet:response") {
678
1336
  resolve({ address: data.address, keyId: data.keyId });
1337
+ } else if (data.type === "openkey:sign-out:response") {
1338
+ resolve({ requestId: data.requestId, revoked: data.revoked });
1339
+ } else if (data.type === "openkey:externalSign:approve") {
1340
+ resolve({
1341
+ authorizationContextToken: data.authorizationContextToken,
1342
+ previewApprovalToken: data.previewApprovalToken,
1343
+ signedMessage: data.signedMessage,
1344
+ selectedActionIds: data.selectedActionIds,
1345
+ address: data.address
1346
+ });
679
1347
  } else {
680
- resolve({ signature: data.signature, address: data.address });
1348
+ resolve({
1349
+ signature: data.signature,
1350
+ address: data.address,
1351
+ signedMessage: data.signedMessage,
1352
+ selectedActionKeys: data.selectedActionKeys,
1353
+ permissions: data.permissions
1354
+ });
681
1355
  }
682
1356
  } else {
683
1357
  reject(data.error);
@@ -692,7 +1366,7 @@ var OpenKey = class {
692
1366
  console.warn("OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP.");
693
1367
  showToast();
694
1368
  const popupUrl = `${this.host}/widget/${action}?origin=${origin}`;
695
- this.openPopup(popupUrl, message, (val) => settle(() => resolve(val)), (err) => settle(() => reject(err)));
1369
+ this.openPopup(action, popupUrl, message, (val) => settle(() => resolve(val)), (err) => settle(() => reject(err)));
696
1370
  }, IFRAME_READY_TIMEOUT);
697
1371
  const overallTimeout = setTimeout(() => {
698
1372
  settle(() => {
@@ -700,8 +1374,24 @@ var OpenKey = class {
700
1374
  reject({ code: "TIMEOUT", message: "Request timed out" });
701
1375
  });
702
1376
  }, DEFAULT_TIMEOUT);
1377
+ cancel = () => {
1378
+ settle(() => {
1379
+ cleanup();
1380
+ reject({ code: "USER_CANCELLED", message: "Superseded by sign-out" });
1381
+ });
1382
+ };
1383
+ this.activeFlowCancellations.add(cancel);
703
1384
  });
704
1385
  }
1386
+ isResponseForAction(action, data) {
1387
+ const type = data.type;
1388
+ if (action === "connect") return type === "openkey:auth:response";
1389
+ if (action === "sign") return type === "openkey:sign:response" || type === "openkey:externalSign:approve";
1390
+ if (action === "sign-typed-data") return type === "openkey:signTypedData:response";
1391
+ if (action === "link-wallet") return type === "openkey:link-wallet:response";
1392
+ if (action === "sign-out") return type === "openkey:sign-out:response";
1393
+ return false;
1394
+ }
705
1395
  handleWalletLinkDelegation(modal) {
706
1396
  const sendResult = (result) => modal.postMessage(result);
707
1397
  new WalletPicker({
@@ -763,15 +1453,16 @@ var OpenKey = class {
763
1453
  }
764
1454
  });
765
1455
  }
766
- openPopup(url, message, resolve, reject) {
1456
+ openPopup(action, url, message, resolve, reject) {
767
1457
  const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
768
1458
  const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
769
- this.popup = window.open(
1459
+ const popup = window.open(
770
1460
  url,
771
- "openkey",
1461
+ `openkey-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
772
1462
  `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
773
1463
  );
774
- if (!this.popup) {
1464
+ this.popup = popup;
1465
+ if (!popup) {
775
1466
  showToast("Popup was blocked. Please allow popups for this site.", "error");
776
1467
  reject({
777
1468
  code: "POPUP_BLOCKED",
@@ -779,74 +1470,129 @@ var OpenKey = class {
779
1470
  });
780
1471
  return;
781
1472
  }
1473
+ let settled = false;
1474
+ let cancel;
782
1475
  const cleanup = () => {
783
1476
  window.removeEventListener("message", handleMessage);
784
1477
  clearInterval(pollClosed);
785
1478
  clearTimeout(timeout);
1479
+ this.activeFlowCancellations.delete(cancel);
1480
+ if (this.popup === popup) this.popup = null;
1481
+ };
1482
+ const settle = (fn) => {
1483
+ if (settled) return;
1484
+ settled = true;
1485
+ cleanup();
1486
+ fn();
786
1487
  };
787
1488
  const pollClosed = setInterval(() => {
788
- if (this.popup?.closed) {
789
- cleanup();
790
- reject({
1489
+ if (popup.closed) {
1490
+ settle(() => reject({
791
1491
  code: "USER_CANCELLED",
792
1492
  message: "User closed the popup"
793
- });
1493
+ }));
794
1494
  }
795
1495
  }, 500);
796
1496
  const timeout = setTimeout(() => {
797
- cleanup();
798
- this.popup?.close();
799
- reject({
800
- code: "TIMEOUT",
801
- message: "Request timed out"
1497
+ settle(() => {
1498
+ popup.close();
1499
+ reject({
1500
+ code: "TIMEOUT",
1501
+ message: "Request timed out"
1502
+ });
802
1503
  });
803
1504
  }, DEFAULT_TIMEOUT);
1505
+ const outgoingRequestId = message.requestId;
1506
+ const outgoingProtocolVersion = message.protocolVersion;
1507
+ const isVersionedRequest = typeof outgoingRequestId === "string" && typeof outgoingProtocolVersion === "number" && outgoingProtocolVersion >= 1;
804
1508
  const handleMessage = (event) => {
805
1509
  if (event.origin !== this.host) return;
806
- if (event.source !== this.popup) return;
1510
+ if (event.source !== popup) return;
807
1511
  const data = event.data;
808
1512
  if (data.type === "openkey:ready") {
809
- this.popup?.postMessage(message, this.host);
1513
+ popup.postMessage(message, this.host);
810
1514
  return;
811
1515
  }
812
1516
  if (data.type === "openkey:close") {
813
- cleanup();
814
- this.popup?.close();
815
- reject({
816
- code: "USER_CANCELLED",
817
- message: "User cancelled the request"
1517
+ if (isVersionedRequest) {
1518
+ const closeRequestId = data.requestId;
1519
+ const closeVersion = data.protocolVersion;
1520
+ if (closeRequestId !== outgoingRequestId) return;
1521
+ if (closeVersion !== outgoingProtocolVersion) return;
1522
+ }
1523
+ settle(() => {
1524
+ popup.close();
1525
+ reject({
1526
+ code: "USER_CANCELLED",
1527
+ message: "User cancelled the request"
1528
+ });
818
1529
  });
819
1530
  return;
820
1531
  }
821
1532
  if (data.type === "openkey:auth:use-external-wallet") {
822
- cleanup();
823
- this.popup?.close();
824
- this.handleExternalWalletConnect(resolve, reject);
1533
+ settle(() => {
1534
+ popup.close();
1535
+ this.handleExternalWalletConnect(resolve, reject);
1536
+ });
825
1537
  return;
826
1538
  }
827
- if (data.type === "openkey:auth:response" || data.type === "openkey:sign:response" || data.type === "openkey:signTypedData:response" || data.type === "openkey:link-wallet:response") {
828
- cleanup();
829
- this.popup?.close();
830
- if (data.success) {
831
- if (data.type === "openkey:auth:response") {
832
- if (data.sessionToken) this.sessionToken = data.sessionToken;
833
- resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
834
- } else if (data.type === "openkey:link-wallet:response") {
835
- resolve({ address: data.address, keyId: data.keyId });
1539
+ if (this.isResponseForAction(action, data)) {
1540
+ if (isVersionedRequest) {
1541
+ const respRequestId = data.requestId;
1542
+ const respVersion = data.protocolVersion;
1543
+ if (respRequestId !== outgoingRequestId) return;
1544
+ if (respVersion !== outgoingProtocolVersion) return;
1545
+ }
1546
+ settle(() => {
1547
+ popup.close();
1548
+ if (data.success) {
1549
+ if (data.type === "openkey:auth:response") {
1550
+ if (data.sessionToken) this.sessionToken = data.sessionToken;
1551
+ resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
1552
+ } else if (data.type === "openkey:link-wallet:response") {
1553
+ resolve({ address: data.address, keyId: data.keyId });
1554
+ } else if (data.type === "openkey:sign-out:response") {
1555
+ resolve({ requestId: data.requestId, revoked: data.revoked });
1556
+ } else if (data.type === "openkey:externalSign:approve") {
1557
+ resolve({
1558
+ authorizationContextToken: data.authorizationContextToken,
1559
+ previewApprovalToken: data.previewApprovalToken,
1560
+ signedMessage: data.signedMessage,
1561
+ selectedActionIds: data.selectedActionIds,
1562
+ address: data.address
1563
+ });
1564
+ } else {
1565
+ resolve({
1566
+ signature: data.signature,
1567
+ address: data.address,
1568
+ signedMessage: data.signedMessage,
1569
+ selectedActionKeys: data.selectedActionKeys,
1570
+ permissions: data.permissions
1571
+ });
1572
+ }
836
1573
  } else {
837
- resolve({ signature: data.signature, address: data.address });
1574
+ reject(data.error);
838
1575
  }
839
- } else {
840
- reject(data.error);
841
- }
1576
+ });
842
1577
  }
843
1578
  };
844
1579
  window.addEventListener("message", handleMessage);
1580
+ cancel = () => {
1581
+ settle(() => {
1582
+ popup.close();
1583
+ reject({ code: "USER_CANCELLED", message: "Superseded by sign-out" });
1584
+ });
1585
+ };
1586
+ this.activeFlowCancellations.add(cancel);
1587
+ }
1588
+ cancelActiveFlows() {
1589
+ for (const cancel of [...this.activeFlowCancellations]) cancel();
845
1590
  }
846
1591
  /**
847
1592
  * Disconnect and close any open flows
848
1593
  */
849
1594
  disconnect() {
1595
+ this.cancelActiveFlows();
850
1596
  this.popup?.close();
851
1597
  this.popup = null;
852
1598
  }
@@ -919,8 +1665,13 @@ function getOpenKey(config) {
919
1665
  }
920
1666
  // Annotate the CommonJS export names for ESM import in node:
921
1667
  0 && (module.exports = {
1668
+ KeyIdTypeMismatchError,
922
1669
  OpenKey,
1670
+ OpenKeyNostr,
923
1671
  OpenKeyProvider,
924
- getOpenKey
1672
+ getOpenKey,
1673
+ resolveAuthorizeTinyCloudRouting,
1674
+ shouldRouteAuthorizeTinyCloudExternal,
1675
+ validateIframeResize
925
1676
  });
926
1677
  //# sourceMappingURL=index.js.map