@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.mjs CHANGED
@@ -1,16 +1,317 @@
1
+ // ../core/dist/index.js
2
+ var u = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3
+ function g(e) {
4
+ let r = "", n = e.length;
5
+ for (let t = 0; t < n; t += 3) {
6
+ 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;
7
+ 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] : "";
8
+ }
9
+ return r.replace(/\+/g, "-").replace(/\//g, "_");
10
+ }
11
+ function y() {
12
+ let e = new Uint8Array(32);
13
+ return crypto.getRandomValues(e), g(e);
14
+ }
15
+ async function x(e) {
16
+ let n = new TextEncoder().encode(e), t = await crypto.subtle.digest("SHA-256", n);
17
+ return new Uint8Array(t);
18
+ }
19
+ async function k(e, r) {
20
+ let t = await (r ?? x)(e);
21
+ return g(t);
22
+ }
23
+ function m() {
24
+ let e = new Uint8Array(16);
25
+ return crypto.getRandomValues(e), g(e);
26
+ }
27
+ function T(e) {
28
+ 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);
29
+ 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();
30
+ }
31
+
32
+ // src/nostr.ts
33
+ var NOSTR_FRAME_TIMEOUT_MS = 5 * 60 * 1e3;
34
+ var NOSTR_PROTOCOL_VERSION = 1;
35
+ function newRequestId() {
36
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
37
+ return crypto.randomUUID();
38
+ }
39
+ if (typeof crypto === "undefined" || typeof crypto.getRandomValues !== "function") {
40
+ throw new Error("Secure request-id generation is unavailable.");
41
+ }
42
+ const bytes = new Uint32Array(4);
43
+ crypto.getRandomValues(bytes);
44
+ return `nostr-${Array.from(bytes, (value) => value.toString(16)).join("")}`;
45
+ }
46
+ var NostrFrame = class {
47
+ root;
48
+ shadow;
49
+ iframe;
50
+ outer;
51
+ host;
52
+ messageHandler = () => {
53
+ };
54
+ revealed = false;
55
+ requestedHeight = 400;
56
+ viewportHandler = () => {
57
+ this.syncViewport();
58
+ this.applyFrameHeight();
59
+ };
60
+ constructor(host, url) {
61
+ this.host = host;
62
+ this.root = document.createElement("div");
63
+ this.shadow = this.root.attachShadow({ mode: "closed" });
64
+ const style = document.createElement("style");
65
+ style.textContent = `
66
+ :host{all:initial}
67
+ .nf-outer{position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none}
68
+ .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}
69
+ .nf-card{width:1px;height:1px;overflow:hidden}
70
+ .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}
71
+ iframe{border:none;width:1px;height:1px;display:block;transition:height 200ms ease}
72
+ .nf-visible iframe{width:100%;height:400px;max-height:100%}
73
+ @media (prefers-reduced-motion:reduce){iframe{transition:none}}
74
+ `;
75
+ this.outer = document.createElement("div");
76
+ this.outer.className = "nf-outer";
77
+ this.outer.setAttribute("role", "dialog");
78
+ this.outer.setAttribute("aria-modal", "true");
79
+ this.outer.setAttribute("aria-label", "OpenKey approval");
80
+ this.outer.setAttribute("aria-hidden", "true");
81
+ const card = document.createElement("div");
82
+ card.className = "nf-card";
83
+ this.iframe = document.createElement("iframe");
84
+ this.iframe.src = url;
85
+ this.iframe.title = "OpenKey approval";
86
+ this.iframe.setAttribute("allow", "clipboard-write *; publickey-credentials-get *; publickey-credentials-create *");
87
+ this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox");
88
+ card.appendChild(this.iframe);
89
+ this.outer.appendChild(card);
90
+ this.shadow.appendChild(style);
91
+ this.shadow.appendChild(this.outer);
92
+ document.body.appendChild(this.root);
93
+ this.syncViewport();
94
+ window.addEventListener("resize", this.viewportHandler);
95
+ window.visualViewport?.addEventListener("resize", this.viewportHandler);
96
+ window.visualViewport?.addEventListener("scroll", this.viewportHandler);
97
+ }
98
+ onMessage(handler) {
99
+ this.messageHandler = (event) => {
100
+ if (event.origin !== this.host) return;
101
+ if (event.source !== this.iframe.contentWindow) return;
102
+ handler(event.data);
103
+ };
104
+ window.addEventListener("message", this.messageHandler);
105
+ }
106
+ reveal() {
107
+ if (this.revealed) return;
108
+ this.revealed = true;
109
+ this.syncViewport();
110
+ this.outer.classList.add("nf-visible");
111
+ this.outer.setAttribute("aria-hidden", "false");
112
+ this.applyFrameHeight();
113
+ }
114
+ resize(height) {
115
+ if (!Number.isFinite(height) || height <= 0) return;
116
+ this.requestedHeight = height;
117
+ if (this.revealed) this.applyFrameHeight();
118
+ }
119
+ syncViewport() {
120
+ const viewport = window.visualViewport;
121
+ this.outer.style.setProperty("--nf-viewport-top", `${viewport?.offsetTop ?? 0}px`);
122
+ this.outer.style.setProperty("--nf-viewport-left", `${viewport?.offsetLeft ?? 0}px`);
123
+ this.outer.style.setProperty("--nf-viewport-width", `${viewport?.width ?? window.innerWidth}px`);
124
+ this.outer.style.setProperty("--nf-viewport-height", `${viewport?.height ?? window.innerHeight}px`);
125
+ }
126
+ applyFrameHeight() {
127
+ if (!this.revealed) return;
128
+ const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
129
+ const max = Math.max(1, Math.floor(viewportHeight - 32));
130
+ this.iframe.style.height = `${Math.min(this.requestedHeight, max)}px`;
131
+ }
132
+ postMessage(message) {
133
+ this.iframe.contentWindow?.postMessage(message, this.host);
134
+ }
135
+ destroy() {
136
+ window.removeEventListener("message", this.messageHandler);
137
+ window.removeEventListener("resize", this.viewportHandler);
138
+ window.visualViewport?.removeEventListener("resize", this.viewportHandler);
139
+ window.visualViewport?.removeEventListener("scroll", this.viewportHandler);
140
+ this.root.remove();
141
+ }
142
+ };
143
+ var OpenKeyNostr = class {
144
+ constructor(host) {
145
+ this.host = host;
146
+ }
147
+ /**
148
+ * Get-or-create the user's OpenKey-custodied Nostr identity. Always shows
149
+ * a visible consent card (identity creation, or - when `relayUrl` is
150
+ * given - a relay-scoped kind:22242/NIP-42 grant for that exact relay).
151
+ * Never resolves with the secret key; only `keyId`/`pubkey`/`npub`.
152
+ */
153
+ async connect(opts) {
154
+ const url = this.widgetUrl();
155
+ return this.run(
156
+ url,
157
+ { type: "openkey:nostr:connect:request", relayUrl: opts?.relayUrl },
158
+ (data, resolve, reject) => {
159
+ if (data.type !== "openkey:nostr:connect:response") return false;
160
+ if (data.success) resolve({ keyId: data.keyId, pubkey: data.pubkey, npub: data.npub });
161
+ else reject(data.error);
162
+ return true;
163
+ }
164
+ );
165
+ }
166
+ /**
167
+ * Sign a NIP-01 event template with the user's OpenKey Nostr key. Silent
168
+ * (no visible UI) when an active grant already covers this client origin
169
+ * + key + kind (+ exact relay, for kind 22242). Otherwise shows a visible
170
+ * consent card before signing - required for every first kind:9 message to
171
+ * a new origin, per NIP-29 channel-message policy.
172
+ */
173
+ async signEvent(keyId, event) {
174
+ const url = this.widgetUrl();
175
+ return this.run(
176
+ url,
177
+ { type: "openkey:nostr:sign:request", keyId, event },
178
+ (data, resolve, reject) => {
179
+ if (data.type !== "openkey:nostr:sign:response") return false;
180
+ if (data.success) resolve(data.event);
181
+ else reject(data.error);
182
+ return true;
183
+ }
184
+ );
185
+ }
186
+ widgetUrl() {
187
+ const origin = encodeURIComponent(window.location.origin);
188
+ return `${this.host}/widget/embed/nostr/approve?origin=${origin}`;
189
+ }
190
+ run(url, request, onData) {
191
+ return new Promise((resolve, reject) => {
192
+ let settled = false;
193
+ const frame = new NostrFrame(this.host, url);
194
+ const requestId = newRequestId();
195
+ const settle = (fn) => {
196
+ if (settled) return;
197
+ settled = true;
198
+ clearTimeout(timeout);
199
+ fn();
200
+ frame.destroy();
201
+ };
202
+ frame.onMessage((data) => {
203
+ if (!data || typeof data.type !== "string") return;
204
+ if (data.type === "openkey:ready") {
205
+ if (data.protocolVersion !== NOSTR_PROTOCOL_VERSION) return;
206
+ frame.postMessage({ ...request, requestId, protocolVersion: NOSTR_PROTOCOL_VERSION });
207
+ return;
208
+ }
209
+ if (data.requestId !== requestId || data.protocolVersion !== NOSTR_PROTOCOL_VERSION) return;
210
+ if (data.type === "openkey:resize") {
211
+ frame.resize(data.height);
212
+ return;
213
+ }
214
+ if (data.type === "openkey:nostr:show") {
215
+ frame.reveal();
216
+ return;
217
+ }
218
+ if (data.type === "openkey:close") {
219
+ settle(() => reject({ code: "USER_CANCELLED", message: "Request closed" }));
220
+ return;
221
+ }
222
+ onData(data, (v) => settle(() => resolve(v)), (e) => settle(() => reject(e)));
223
+ });
224
+ const timeout = setTimeout(() => {
225
+ settle(() => reject({ code: "TIMEOUT", message: "Nostr request timed out" }));
226
+ }, NOSTR_FRAME_TIMEOUT_MS);
227
+ });
228
+ }
229
+ };
230
+
1
231
  // src/index.ts
2
- import {
3
- generateCodeVerifier,
4
- generateCodeChallenge,
5
- generateState,
6
- buildAuthorizationUrl
7
- } from "@openkey/core";
8
232
  var DEFAULT_HOST = "https://openkey.so";
9
233
  var POPUP_WIDTH = 400;
10
234
  var POPUP_HEIGHT = 600;
11
235
  var DEFAULT_TIMEOUT = 5 * 60 * 1e3;
12
236
  var IFRAME_READY_TIMEOUT = 3e3;
13
237
  var OAUTH_STORAGE_KEY = "openkey_oauth";
238
+ function validateIframeResize(incoming, expected) {
239
+ if (!incoming || typeof incoming !== "object") return null;
240
+ const data = incoming;
241
+ if (data.type !== "openkey:resize") return null;
242
+ if (expected.requestId === null || expected.protocolVersion === null) return null;
243
+ if (typeof data.protocolVersion !== "number" || data.protocolVersion !== expected.protocolVersion) {
244
+ return null;
245
+ }
246
+ if (typeof data.requestId !== "string" || data.requestId !== expected.requestId) {
247
+ return null;
248
+ }
249
+ if (typeof data.height !== "number" || !Number.isFinite(data.height) || data.height <= 0) {
250
+ return null;
251
+ }
252
+ return Math.min(data.height, Math.floor(expected.viewportHeight * 0.85));
253
+ }
254
+ function shouldRouteAuthorizeTinyCloudExternal(state) {
255
+ return state.lastAuth?.keyType === "EXTERNAL";
256
+ }
257
+ var KeyIdTypeMismatchError = class extends Error {
258
+ code;
259
+ requestKeyId;
260
+ lastAuthKeyId;
261
+ requestKeyType;
262
+ lastAuthKeyType;
263
+ constructor(fields) {
264
+ super(fields.message);
265
+ this.name = "KeyIdTypeMismatchError";
266
+ this.code = "KEY_ID_TYPE_MISMATCH";
267
+ this.requestKeyId = fields.requestKeyId;
268
+ this.lastAuthKeyId = fields.lastAuthKeyId;
269
+ this.requestKeyType = fields.requestKeyType;
270
+ this.lastAuthKeyType = fields.lastAuthKeyType;
271
+ }
272
+ };
273
+ function resolveAuthorizeTinyCloudRouting(state) {
274
+ const lastAuthKeyId = state.lastAuth?.keyId ?? null;
275
+ const lastAuthKeyType = state.lastAuth?.keyType ?? null;
276
+ const rawRequestKeyId = state.requestKeyId ?? null;
277
+ const requestKeyId = rawRequestKeyId && rawRequestKeyId.length > 0 ? rawRequestKeyId : null;
278
+ const knownKeys = state.knownKeys ?? [];
279
+ const lookupType = (id) => {
280
+ for (const k2 of knownKeys) {
281
+ if (k2.id === id) return k2.keyType;
282
+ if (k2.address && k2.address.toLowerCase() === id.toLowerCase()) return k2.keyType;
283
+ }
284
+ if (id.startsWith("external:")) return "EXTERNAL";
285
+ return null;
286
+ };
287
+ if (!requestKeyId) {
288
+ const route = lastAuthKeyType === "EXTERNAL" ? "external" : "managed";
289
+ return {
290
+ route,
291
+ resolvedKeyId: lastAuthKeyId,
292
+ resolvedKeyType: lastAuthKeyType
293
+ };
294
+ }
295
+ let requestKeyType = lookupType(requestKeyId);
296
+ if (!requestKeyType && lastAuthKeyId && requestKeyId === lastAuthKeyId) {
297
+ requestKeyType = lastAuthKeyType;
298
+ }
299
+ if (requestKeyType && lastAuthKeyType && requestKeyType !== lastAuthKeyType) {
300
+ throw new KeyIdTypeMismatchError({
301
+ message: `authorizeTinyCloud: request.keyId ${requestKeyId} resolves to ${requestKeyType} but active session key ${lastAuthKeyId} is ${lastAuthKeyType}; refuse to route silently`,
302
+ requestKeyId,
303
+ lastAuthKeyId,
304
+ requestKeyType,
305
+ lastAuthKeyType
306
+ });
307
+ }
308
+ const effectiveType = requestKeyType ?? lastAuthKeyType;
309
+ return {
310
+ route: effectiveType === "EXTERNAL" ? "external" : "managed",
311
+ resolvedKeyId: requestKeyId,
312
+ resolvedKeyType: effectiveType
313
+ };
314
+ }
14
315
  var IframeModal = class {
15
316
  root;
16
317
  shadow;
@@ -19,6 +320,13 @@ var IframeModal = class {
19
320
  onMessage;
20
321
  messageHandler;
21
322
  host;
323
+ // Sol final continuation contract requirement 5: iframe resize traffic
324
+ // MUST correlate to the active request's requestId AND protocolVersion.
325
+ // Set by `setExpectedCorrelation` after the outer flow decides which
326
+ // request this modal is bound to. When both are null, resize messages
327
+ // are dropped (no active request → no resize authority).
328
+ expectedRequestId = null;
329
+ expectedProtocolVersion = null;
22
330
  constructor(opts) {
23
331
  this.host = opts.host;
24
332
  this.onClose = opts.onClose;
@@ -64,7 +372,7 @@ var IframeModal = class {
64
372
  this.iframe = document.createElement("iframe");
65
373
  this.iframe.src = opts.url;
66
374
  this.iframe.setAttribute("allow", "clipboard-write *; publickey-credentials-get *; publickey-credentials-create *");
67
- this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups");
375
+ this.iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox");
68
376
  card.appendChild(this.iframe);
69
377
  backdrop.appendChild(card);
70
378
  this.shadow.appendChild(style);
@@ -74,10 +382,17 @@ var IframeModal = class {
74
382
  if (event.source !== this.iframe.contentWindow) return;
75
383
  const data = event.data;
76
384
  if (data.type === "openkey:resize") {
77
- const h = Math.min(data.height, Math.floor(window.innerHeight * 0.85));
385
+ const h = validateIframeResize(event.data, {
386
+ requestId: this.expectedRequestId,
387
+ protocolVersion: this.expectedProtocolVersion,
388
+ viewportHeight: window.innerHeight
389
+ });
390
+ if (h === null) return;
78
391
  this.iframe.style.height = `${h}px`;
79
392
  return;
80
393
  }
394
+ if (data.type === "openkey:close") {
395
+ }
81
396
  this.onMessage(data);
82
397
  };
83
398
  window.addEventListener("message", this.messageHandler);
@@ -86,6 +401,20 @@ var IframeModal = class {
86
401
  postMessage(message) {
87
402
  this.iframe.contentWindow?.postMessage(message, this.host);
88
403
  }
404
+ /**
405
+ * Sol final continuation contract requirement 5: bind the active
406
+ * request correlation so subsequent resize (and future correlated)
407
+ * messages can be verified against `requestId` + `protocolVersion`.
408
+ *
409
+ * The outer flow calls this immediately BEFORE posting the sign
410
+ * request into the iframe, so a resize that arrives synchronously
411
+ * after ready is correlated correctly. Passing `null` for either
412
+ * argument disables correlation (resize is dropped).
413
+ */
414
+ setExpectedCorrelation(requestId, protocolVersion) {
415
+ this.expectedRequestId = requestId;
416
+ this.expectedProtocolVersion = protocolVersion;
417
+ }
89
418
  destroy() {
90
419
  window.removeEventListener("message", this.messageHandler);
91
420
  this.root.remove();
@@ -219,12 +548,23 @@ var OpenKey = class {
219
548
  lastAuth = null;
220
549
  discoveredProviders = [];
221
550
  sessionToken = null;
551
+ /** Cancels widget flows that could outlive a sign-out on this instance. */
552
+ activeFlowCancellations = /* @__PURE__ */ new Set();
553
+ /**
554
+ * Nostr identity custody + signing (secp256k1 Schnorr / BIP-340). Kept
555
+ * fully separate from the Ethereum flows above: it never reuses
556
+ * `lastAuth`/`sessionToken`, and its widget messages never carry a
557
+ * sessionToken - the OpenKey session and any signing grants stay inside
558
+ * the OpenKey-origin iframe for the whole lifetime of this client.
559
+ */
560
+ nostr;
222
561
  constructor(config = {}) {
223
562
  this.config = config;
224
563
  this.host = config.host || DEFAULT_HOST;
225
564
  this.oauthHost = config.oauthHost || this.deriveOAuthHost(this.host);
226
565
  this.appName = config.appName || window.location.hostname;
227
566
  this.mode = config.mode ?? "iframe";
567
+ this.nostr = new OpenKeyNostr(this.host);
228
568
  if (typeof window !== "undefined") {
229
569
  this.discoveredProviders = [];
230
570
  window.addEventListener("eip6963:announceProvider", (event) => {
@@ -250,6 +590,37 @@ var OpenKey = class {
250
590
  return "https://api.openkey.so";
251
591
  }
252
592
  }
593
+ /**
594
+ * Sol MAJOR-4 (final continuation): fetch the caller's registered keys
595
+ * so `resolveAuthorizeTinyCloudRouting` can resolve an explicit pin
596
+ * whose keyId does not match the active session key. Called only when
597
+ * the caller passes a `requestKeyId` we do not already recognise; the
598
+ * common no-pin path skips the network round-trip.
599
+ *
600
+ * Failures do NOT block routing — we fall through with `knownKeys=[]`
601
+ * so the resolver's lastAuth-based fallback still runs. If the caller
602
+ * pinned a genuinely unknown key that turns out to be external, the
603
+ * widget rejects the server-signing path and the SDK surfaces that
604
+ * error from the finalize call rather than silently routing wrong.
605
+ */
606
+ async fetchKnownKeysIfNeeded(requestKeyId) {
607
+ if (!requestKeyId) return [];
608
+ const la = this.lastAuth;
609
+ if (la?.keyId === requestKeyId) return [];
610
+ if (la?.address && la.address.toLowerCase() === requestKeyId.toLowerCase()) return [];
611
+ if (requestKeyId.startsWith("external:")) return [];
612
+ try {
613
+ const res = await fetch(`${this.oauthHost}/api/keys`, {
614
+ method: "GET",
615
+ credentials: "include"
616
+ });
617
+ if (!res.ok) return [];
618
+ const body = await res.json();
619
+ return Array.isArray(body?.keys) ? body.keys : [];
620
+ } catch {
621
+ return [];
622
+ }
623
+ }
253
624
  /**
254
625
  * Connect to OpenKey and get user's wallet address
255
626
  * Opens auth flow for user to select/create a key
@@ -262,6 +633,55 @@ var OpenKey = class {
262
633
  this.lastAuth = result;
263
634
  return result;
264
635
  }
636
+ /**
637
+ * Signs the active OpenKey account out through the OpenKey-owned widget.
638
+ *
639
+ * The SDK passes its Better Auth bearer through the OpenKey-owned widget
640
+ * protocol so applications do not have to implement revocation themselves.
641
+ * Local SDK session state is cleared before opening the widget, including
642
+ * when remote revocation is unavailable.
643
+ */
644
+ async signOut(opts) {
645
+ const requestId = `ok-signout-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
646
+ const sessionToken = this.sessionToken || void 0;
647
+ const mode = this.resolveMode(opts?.mode) === "redirect" ? "popup" : opts?.mode;
648
+ this.cancelActiveFlows();
649
+ this.lastAuth = null;
650
+ this.sessionToken = null;
651
+ const acknowledgement = await this.openFlow(
652
+ "sign-out",
653
+ {
654
+ type: "openkey:sign-out:request",
655
+ requestId,
656
+ protocolVersion: 1,
657
+ sessionToken
658
+ },
659
+ mode
660
+ );
661
+ if (acknowledgement.requestId !== requestId) {
662
+ throw new Error("OpenKey signOut received an acknowledgement for a different request");
663
+ }
664
+ return acknowledgement;
665
+ }
666
+ /**
667
+ * Better-auth session token relayed during connect(), or null if the
668
+ * flow did not provide one. Use as a bearer token for API calls made
669
+ * on the user's behalf (e.g. TinyCloud auto-sign bootstrap signing).
670
+ */
671
+ getSessionToken() {
672
+ return this.sessionToken;
673
+ }
674
+ /**
675
+ * Endpoint + bearer token for a TinyCloud auto-sign signing strategy.
676
+ * The endpoint is on the OpenKey API host and only signs requests the
677
+ * server-side bootstrap allowlist permits.
678
+ */
679
+ tinycloudSigningOptions() {
680
+ return {
681
+ endpoint: `${this.oauthHost}/api/delegate/sign`,
682
+ token: this.sessionToken
683
+ };
684
+ }
265
685
  /**
266
686
  * Link an external wallet to the user's OpenKey account
267
687
  * Opens link-wallet widget flow
@@ -281,6 +701,208 @@ var OpenKey = class {
281
701
  }
282
702
  return this.signWithOpenKey(request, opts?.mode);
283
703
  }
704
+ /**
705
+ * Versioned TinyCloud authorization protocol (v1).
706
+ *
707
+ * Unlike `signMessage()` (which is contractually byte-exact — the returned
708
+ * signature always verifies against the caller's exact bytes), this method
709
+ * permits the user to narrow the requested capabilities before signing.
710
+ * OpenKey may therefore regenerate the SIWE. The response carries
711
+ * `signedMessage` — the exact bytes the signature actually verifies against.
712
+ *
713
+ * TinyCloud completes the session with `signedMessage`, not the original
714
+ * `siwe` passed in. Consumers MUST use the returned bytes and MUST NOT
715
+ * assume the returned grants are a superset of what they requested — the
716
+ * user is allowed to remove any grant that is not required.
717
+ *
718
+ * Legacy `signMessage()` callers are unaffected: their exact-byte contract
719
+ * remains preserved and OpenKey never rewrites their bytes.
720
+ */
721
+ async authorizeTinyCloud(request, opts) {
722
+ if (request.protocolVersion !== 1) {
723
+ throw new Error(
724
+ `authorizeTinyCloud only supports protocolVersion 1; got ${request.protocolVersion}`
725
+ );
726
+ }
727
+ if (typeof request.siwe !== "string" || !request.siwe) {
728
+ throw new Error("authorizeTinyCloud requires a non-empty SIWE");
729
+ }
730
+ let routing;
731
+ try {
732
+ const knownKeys = await this.fetchKnownKeysIfNeeded(request.keyId);
733
+ routing = resolveAuthorizeTinyCloudRouting({
734
+ lastAuth: this.lastAuth,
735
+ requestKeyId: request.keyId,
736
+ knownKeys
737
+ });
738
+ } catch (e) {
739
+ if (e instanceof KeyIdTypeMismatchError) throw e;
740
+ throw e instanceof Error ? e : new Error("authorizeTinyCloud: failed to resolve target key type");
741
+ }
742
+ if (routing.route === "external") {
743
+ return this.authorizeTinyCloudExternal(request, opts);
744
+ }
745
+ const requestId = `ok-auth-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
746
+ const resolvedKeyId = routing.resolvedKeyId ?? request.keyId ?? this.lastAuth?.keyId;
747
+ const raw = await this.openFlow(
748
+ "sign",
749
+ {
750
+ type: "openkey:sign:request",
751
+ requestId,
752
+ protocolVersion: 1,
753
+ message: request.siwe,
754
+ keyId: resolvedKeyId,
755
+ // Forward JWK so the widget can pass it to /authorize-sign for
756
+ // narrowed-SIWE regeneration.
757
+ jwk: request.jwk,
758
+ // Forward the TinyCloud host so the widget can bind it into the
759
+ // /authorize-sign-prepare context (Sol MAJOR-2).
760
+ host: request.host,
761
+ // Sol MAJOR-2 (envelope): forward the caller-supplied presentation
762
+ // envelope so the widget can render honest provenance and the
763
+ // /authorize-sign-prepare route can bind it into the authorization
764
+ // context. The envelope is display-only; the widget transport
765
+ // validates and size-bounds it before use and NEVER treats it as
766
+ // verified unless the server actually origin-binds a manifest.
767
+ presentation: request.presentation,
768
+ sessionToken: this.sessionToken || void 0
769
+ },
770
+ opts?.mode
771
+ );
772
+ if (typeof raw.signedMessage !== "string" || !raw.signedMessage) {
773
+ throw new Error(
774
+ "authorizeTinyCloud: widget returned no signedMessage \u2014 signature would not correspond to the displayed authorization"
775
+ );
776
+ }
777
+ if (!Array.isArray(raw.selectedActionKeys)) {
778
+ throw new Error(
779
+ "authorizeTinyCloud: widget returned no selectedActionKeys \u2014 cannot verify displayed authorization matches the signed bytes"
780
+ );
781
+ }
782
+ if (!Array.isArray(raw.permissions)) {
783
+ throw new Error(
784
+ "authorizeTinyCloud: widget returned no permissions \u2014 cannot verify displayed authorization matches the signed bytes"
785
+ );
786
+ }
787
+ return {
788
+ protocolVersion: 1,
789
+ address: raw.address,
790
+ signature: raw.signature,
791
+ signedMessage: raw.signedMessage,
792
+ selectedActionKeys: raw.selectedActionKeys,
793
+ permissions: raw.permissions
794
+ };
795
+ }
796
+ /**
797
+ * Sol MAJOR-3 (continuation): authorizeTinyCloud path for external keys.
798
+ * The wallet lives outside OpenKey, so the SDK opens the shared widget
799
+ * for review + preview, then invokes the user's wallet to sign the
800
+ * preview bytes, then finalizes server-side via /authorize-sign.
801
+ *
802
+ * Flow:
803
+ * 1. Open the widget with `externalSign: true` — the widget renders
804
+ * the shared SigningApproval UI, calls /authorize-sign-prepare,
805
+ * calls /authorize-sign-preview after the user approves a
806
+ * selection, and hands back `{ previewApprovalToken,
807
+ * signedMessage, selectedActionIds, address, authorizationContextToken }`.
808
+ * 2. SDK invokes the user's wallet on the returned signedMessage.
809
+ * 3. SDK POSTs /authorize-sign with `externalSignature`.
810
+ *
811
+ * Legacy exact-byte signMessage callers are unaffected — this path is
812
+ * only taken via authorizeTinyCloud (protocolVersion:1) when the target
813
+ * key is EXTERNAL. Sol's rejection required this path to go through
814
+ * the same review UI as managed keys.
815
+ */
816
+ async authorizeTinyCloudExternal(request, opts) {
817
+ const keyId = request.keyId ?? this.lastAuth?.keyId;
818
+ if (!keyId) {
819
+ throw new Error(
820
+ "authorizeTinyCloud (external) requires a keyId \u2014 call connect() first or pass request.keyId"
821
+ );
822
+ }
823
+ const walletAddress = this.lastAuth?.address;
824
+ if (!walletAddress) {
825
+ throw new Error(
826
+ "authorizeTinyCloud (external) requires a connected wallet \u2014 call connect() first"
827
+ );
828
+ }
829
+ const requestId = `ok-ext-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
830
+ const approval = await this.openFlow(
831
+ "sign",
832
+ {
833
+ type: "openkey:sign:request",
834
+ requestId,
835
+ protocolVersion: 1,
836
+ message: request.siwe,
837
+ keyId,
838
+ jwk: request.jwk,
839
+ host: request.host,
840
+ // Sol MAJOR-2 (envelope): forward the caller-supplied presentation
841
+ // envelope. Same rules as the managed path — display-only, size-
842
+ // bounded at the transport, never treated as verified without
843
+ // server-side origin-binding.
844
+ presentation: request.presentation,
845
+ sessionToken: this.sessionToken || void 0,
846
+ // Sol MAJOR-3 (continuation): tell the widget to hand us back
847
+ // the previewApproval instead of asking OpenKey to sign
848
+ // server-side with a managed key. The widget will still render
849
+ // the SAME SigningApproval component; it just diverges at the
850
+ // "sign" step and emits `openkey:externalSign:approve` back
851
+ // through the transport.
852
+ externalSign: true
853
+ },
854
+ opts?.mode
855
+ );
856
+ if (!approval?.previewApprovalToken || !approval?.signedMessage) {
857
+ throw new Error(
858
+ "authorizeTinyCloud (external): widget did not return a previewApprovalToken + signedMessage"
859
+ );
860
+ }
861
+ if (approval.address && approval.address.toLowerCase() !== walletAddress.toLowerCase()) {
862
+ throw new Error(
863
+ `authorizeTinyCloud (external): widget preview address ${approval.address} does not match connected wallet ${walletAddress}`
864
+ );
865
+ }
866
+ const provider = await this.findWalletProvider(walletAddress);
867
+ const hexMessage = this.toHex(approval.signedMessage);
868
+ const walletSignature = await provider.request({
869
+ method: "personal_sign",
870
+ params: [hexMessage, walletAddress]
871
+ });
872
+ const finalizeRes = await fetch(
873
+ `${this.oauthHost}/api/delegate/authorize-sign`,
874
+ {
875
+ method: "POST",
876
+ credentials: "include",
877
+ headers: { "Content-Type": "application/json" },
878
+ body: JSON.stringify({
879
+ authorizationContextToken: approval.authorizationContextToken,
880
+ previewApprovalToken: approval.previewApprovalToken,
881
+ selectedActionIds: approval.selectedActionIds,
882
+ protocolVersion: 1,
883
+ externalSignature: walletSignature
884
+ })
885
+ }
886
+ );
887
+ if (!finalizeRes.ok) {
888
+ const body = await finalizeRes.json().catch(() => ({ error: `HTTP ${finalizeRes.status}` }));
889
+ throw new Error(body.error || `authorize-sign failed (${finalizeRes.status})`);
890
+ }
891
+ const finalize = await finalizeRes.json();
892
+ if (finalize.signedMessage !== approval.signedMessage) {
893
+ throw new Error(
894
+ "authorizeTinyCloud (external): server signed different bytes than the preview showed"
895
+ );
896
+ }
897
+ return {
898
+ protocolVersion: 1,
899
+ address: finalize.address,
900
+ signature: finalize.signature,
901
+ signedMessage: finalize.signedMessage,
902
+ selectedActionKeys: finalize.selectedActionKeys ?? [],
903
+ permissions: finalize.permissions ?? []
904
+ };
905
+ }
284
906
  /**
285
907
  * Sign typed data (EIP-712) with the user's OpenKey wallet
286
908
  * For external keys, routes directly to the user's wallet provider
@@ -291,31 +913,40 @@ var OpenKey = class {
291
913
  }
292
914
  return this.signTypedDataWithOpenKey(request, opts?.mode);
293
915
  }
916
+ providerCandidates() {
917
+ const providers = [];
918
+ if (this.config.externalProvider) {
919
+ providers.push(this.config.externalProvider);
920
+ }
921
+ providers.push(...this.discoveredProviders.map(({ provider }) => provider));
922
+ if (typeof window !== "undefined" && window.ethereum) {
923
+ providers.push(window.ethereum);
924
+ }
925
+ return [...new Set(providers)];
926
+ }
927
+ async providerHasAccount(provider, method, targetAddress) {
928
+ try {
929
+ const accounts = await provider.request({ method });
930
+ return accounts.some((account) => account.toLowerCase() === targetAddress.toLowerCase());
931
+ } catch {
932
+ return false;
933
+ }
934
+ }
294
935
  /**
295
936
  * Find a wallet provider that controls the given address.
296
- * Checks: 1) app-provided externalProvider, 2) EIP-6963 discovered wallets, 3) window.ethereum
937
+ * Checks connected accounts first, then asks the wallet to authorize the
938
+ * app origin if the external key was linked elsewhere.
297
939
  */
298
940
  async findWalletProvider(targetAddress) {
299
- if (this.config.externalProvider) {
300
- return this.config.externalProvider;
301
- }
302
- for (const { provider } of this.discoveredProviders) {
303
- try {
304
- const accounts = await provider.request({ method: "eth_accounts" });
305
- if (accounts.some((a) => a.toLowerCase() === targetAddress.toLowerCase())) {
306
- return provider;
307
- }
308
- } catch {
941
+ const providers = this.providerCandidates();
942
+ for (const provider of providers) {
943
+ if (await this.providerHasAccount(provider, "eth_accounts", targetAddress)) {
944
+ return provider;
309
945
  }
310
946
  }
311
- if (typeof window !== "undefined" && window.ethereum) {
312
- const provider = window.ethereum;
313
- try {
314
- const accounts = await provider.request({ method: "eth_accounts" });
315
- if (accounts.some((a) => a.toLowerCase() === targetAddress.toLowerCase())) {
316
- return provider;
317
- }
318
- } catch {
947
+ for (const provider of providers) {
948
+ if (await this.providerHasAccount(provider, "eth_requestAccounts", targetAddress)) {
949
+ return provider;
319
950
  }
320
951
  }
321
952
  throw new ExternalWalletError("External wallet not found. Connect the wallet that owns this key and try again.");
@@ -406,14 +1037,14 @@ var OpenKey = class {
406
1037
  * @returns Promise that resolves with authorization code after user consent
407
1038
  */
408
1039
  connect: async (config) => {
409
- const verifier = generateCodeVerifier();
410
- const challenge = await generateCodeChallenge(verifier);
411
- const state = config.state || generateState();
1040
+ const verifier = y();
1041
+ const challenge = await k(verifier);
1042
+ const state = config.state || m();
412
1043
  sessionStorage.setItem(
413
1044
  OAUTH_STORAGE_KEY,
414
1045
  JSON.stringify({ verifier, state })
415
1046
  );
416
- const authUrl = buildAuthorizationUrl({
1047
+ const authUrl = T({
417
1048
  host: this.oauthHost,
418
1049
  clientId: config.clientId,
419
1050
  redirectUri: config.redirectUri,
@@ -585,7 +1216,7 @@ var OpenKey = class {
585
1216
  const eoaFlag = action === "connect" && this.hasDetectedEoa() ? "&hasEoa=true" : "";
586
1217
  if (mode === "popup") {
587
1218
  const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
588
- return new Promise((resolve, reject) => this.openPopup(url2, message, resolve, reject));
1219
+ return new Promise((resolve, reject) => this.openPopup(action, url2, message, resolve, reject));
589
1220
  }
590
1221
  if (mode === "redirect") {
591
1222
  const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
@@ -597,10 +1228,14 @@ var OpenKey = class {
597
1228
  return this.openIframeModal(url, action, message, origin);
598
1229
  }
599
1230
  openIframeModal(url, action, message, origin) {
1231
+ const outgoingRequestId = message.requestId;
1232
+ const outgoingProtocolVersion = message.protocolVersion;
1233
+ const isVersionedRequest = typeof outgoingRequestId === "string" && typeof outgoingProtocolVersion === "number" && outgoingProtocolVersion >= 1;
600
1234
  return new Promise((resolve, reject) => {
601
1235
  let readyReceived = false;
602
1236
  let settled = false;
603
1237
  let modal = null;
1238
+ let cancel;
604
1239
  const settle = (fn) => {
605
1240
  if (settled) return;
606
1241
  settled = true;
@@ -611,24 +1246,35 @@ var OpenKey = class {
611
1246
  clearTimeout(overallTimeout);
612
1247
  modal?.destroy();
613
1248
  modal = null;
1249
+ this.activeFlowCancellations.delete(cancel);
614
1250
  };
615
1251
  modal = new IframeModal({
616
1252
  url,
617
1253
  host: this.host,
618
1254
  onClose: () => {
619
1255
  settle(() => {
620
- clearTimeout(readyTimeout);
621
- clearTimeout(overallTimeout);
622
- modal = null;
1256
+ cleanup();
623
1257
  reject({ code: "USER_CANCELLED", message: "User cancelled the request" });
624
1258
  });
625
1259
  },
626
1260
  onMessage: (data) => {
627
1261
  if (data.type === "openkey:ready") {
628
1262
  readyReceived = true;
1263
+ if (isVersionedRequest && modal) {
1264
+ modal.setExpectedCorrelation(
1265
+ outgoingRequestId,
1266
+ outgoingProtocolVersion
1267
+ );
1268
+ }
629
1269
  modal?.postMessage(message);
630
1270
  return;
631
1271
  }
1272
+ if (isVersionedRequest) {
1273
+ const respRequestId = data.requestId;
1274
+ const respVersion = data.protocolVersion;
1275
+ if (respRequestId !== outgoingRequestId) return;
1276
+ if (respVersion !== outgoingProtocolVersion) return;
1277
+ }
632
1278
  if (data.type === "openkey:close") {
633
1279
  settle(() => {
634
1280
  cleanup();
@@ -645,7 +1291,7 @@ var OpenKey = class {
645
1291
  this.handleExternalWalletConnect(resolve, reject);
646
1292
  return;
647
1293
  }
648
- if (data.type === "openkey:auth:response" || data.type === "openkey:sign:response" || data.type === "openkey:signTypedData:response" || data.type === "openkey:link-wallet:response") {
1294
+ if (this.isResponseForAction(action, data)) {
649
1295
  settle(() => {
650
1296
  cleanup();
651
1297
  if (data.success) {
@@ -654,8 +1300,24 @@ var OpenKey = class {
654
1300
  resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
655
1301
  } else if (data.type === "openkey:link-wallet:response") {
656
1302
  resolve({ address: data.address, keyId: data.keyId });
1303
+ } else if (data.type === "openkey:sign-out:response") {
1304
+ resolve({ requestId: data.requestId, revoked: data.revoked });
1305
+ } else if (data.type === "openkey:externalSign:approve") {
1306
+ resolve({
1307
+ authorizationContextToken: data.authorizationContextToken,
1308
+ previewApprovalToken: data.previewApprovalToken,
1309
+ signedMessage: data.signedMessage,
1310
+ selectedActionIds: data.selectedActionIds,
1311
+ address: data.address
1312
+ });
657
1313
  } else {
658
- resolve({ signature: data.signature, address: data.address });
1314
+ resolve({
1315
+ signature: data.signature,
1316
+ address: data.address,
1317
+ signedMessage: data.signedMessage,
1318
+ selectedActionKeys: data.selectedActionKeys,
1319
+ permissions: data.permissions
1320
+ });
659
1321
  }
660
1322
  } else {
661
1323
  reject(data.error);
@@ -670,7 +1332,7 @@ var OpenKey = class {
670
1332
  console.warn("OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP.");
671
1333
  showToast();
672
1334
  const popupUrl = `${this.host}/widget/${action}?origin=${origin}`;
673
- this.openPopup(popupUrl, message, (val) => settle(() => resolve(val)), (err) => settle(() => reject(err)));
1335
+ this.openPopup(action, popupUrl, message, (val) => settle(() => resolve(val)), (err) => settle(() => reject(err)));
674
1336
  }, IFRAME_READY_TIMEOUT);
675
1337
  const overallTimeout = setTimeout(() => {
676
1338
  settle(() => {
@@ -678,8 +1340,24 @@ var OpenKey = class {
678
1340
  reject({ code: "TIMEOUT", message: "Request timed out" });
679
1341
  });
680
1342
  }, DEFAULT_TIMEOUT);
1343
+ cancel = () => {
1344
+ settle(() => {
1345
+ cleanup();
1346
+ reject({ code: "USER_CANCELLED", message: "Superseded by sign-out" });
1347
+ });
1348
+ };
1349
+ this.activeFlowCancellations.add(cancel);
681
1350
  });
682
1351
  }
1352
+ isResponseForAction(action, data) {
1353
+ const type = data.type;
1354
+ if (action === "connect") return type === "openkey:auth:response";
1355
+ if (action === "sign") return type === "openkey:sign:response" || type === "openkey:externalSign:approve";
1356
+ if (action === "sign-typed-data") return type === "openkey:signTypedData:response";
1357
+ if (action === "link-wallet") return type === "openkey:link-wallet:response";
1358
+ if (action === "sign-out") return type === "openkey:sign-out:response";
1359
+ return false;
1360
+ }
683
1361
  handleWalletLinkDelegation(modal) {
684
1362
  const sendResult = (result) => modal.postMessage(result);
685
1363
  new WalletPicker({
@@ -741,15 +1419,16 @@ var OpenKey = class {
741
1419
  }
742
1420
  });
743
1421
  }
744
- openPopup(url, message, resolve, reject) {
1422
+ openPopup(action, url, message, resolve, reject) {
745
1423
  const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
746
1424
  const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
747
- this.popup = window.open(
1425
+ const popup = window.open(
748
1426
  url,
749
- "openkey",
1427
+ `openkey-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
750
1428
  `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
751
1429
  );
752
- if (!this.popup) {
1430
+ this.popup = popup;
1431
+ if (!popup) {
753
1432
  showToast("Popup was blocked. Please allow popups for this site.", "error");
754
1433
  reject({
755
1434
  code: "POPUP_BLOCKED",
@@ -757,74 +1436,129 @@ var OpenKey = class {
757
1436
  });
758
1437
  return;
759
1438
  }
1439
+ let settled = false;
1440
+ let cancel;
760
1441
  const cleanup = () => {
761
1442
  window.removeEventListener("message", handleMessage);
762
1443
  clearInterval(pollClosed);
763
1444
  clearTimeout(timeout);
1445
+ this.activeFlowCancellations.delete(cancel);
1446
+ if (this.popup === popup) this.popup = null;
1447
+ };
1448
+ const settle = (fn) => {
1449
+ if (settled) return;
1450
+ settled = true;
1451
+ cleanup();
1452
+ fn();
764
1453
  };
765
1454
  const pollClosed = setInterval(() => {
766
- if (this.popup?.closed) {
767
- cleanup();
768
- reject({
1455
+ if (popup.closed) {
1456
+ settle(() => reject({
769
1457
  code: "USER_CANCELLED",
770
1458
  message: "User closed the popup"
771
- });
1459
+ }));
772
1460
  }
773
1461
  }, 500);
774
1462
  const timeout = setTimeout(() => {
775
- cleanup();
776
- this.popup?.close();
777
- reject({
778
- code: "TIMEOUT",
779
- message: "Request timed out"
1463
+ settle(() => {
1464
+ popup.close();
1465
+ reject({
1466
+ code: "TIMEOUT",
1467
+ message: "Request timed out"
1468
+ });
780
1469
  });
781
1470
  }, DEFAULT_TIMEOUT);
1471
+ const outgoingRequestId = message.requestId;
1472
+ const outgoingProtocolVersion = message.protocolVersion;
1473
+ const isVersionedRequest = typeof outgoingRequestId === "string" && typeof outgoingProtocolVersion === "number" && outgoingProtocolVersion >= 1;
782
1474
  const handleMessage = (event) => {
783
1475
  if (event.origin !== this.host) return;
784
- if (event.source !== this.popup) return;
1476
+ if (event.source !== popup) return;
785
1477
  const data = event.data;
786
1478
  if (data.type === "openkey:ready") {
787
- this.popup?.postMessage(message, this.host);
1479
+ popup.postMessage(message, this.host);
788
1480
  return;
789
1481
  }
790
1482
  if (data.type === "openkey:close") {
791
- cleanup();
792
- this.popup?.close();
793
- reject({
794
- code: "USER_CANCELLED",
795
- message: "User cancelled the request"
1483
+ if (isVersionedRequest) {
1484
+ const closeRequestId = data.requestId;
1485
+ const closeVersion = data.protocolVersion;
1486
+ if (closeRequestId !== outgoingRequestId) return;
1487
+ if (closeVersion !== outgoingProtocolVersion) return;
1488
+ }
1489
+ settle(() => {
1490
+ popup.close();
1491
+ reject({
1492
+ code: "USER_CANCELLED",
1493
+ message: "User cancelled the request"
1494
+ });
796
1495
  });
797
1496
  return;
798
1497
  }
799
1498
  if (data.type === "openkey:auth:use-external-wallet") {
800
- cleanup();
801
- this.popup?.close();
802
- this.handleExternalWalletConnect(resolve, reject);
1499
+ settle(() => {
1500
+ popup.close();
1501
+ this.handleExternalWalletConnect(resolve, reject);
1502
+ });
803
1503
  return;
804
1504
  }
805
- if (data.type === "openkey:auth:response" || data.type === "openkey:sign:response" || data.type === "openkey:signTypedData:response" || data.type === "openkey:link-wallet:response") {
806
- cleanup();
807
- this.popup?.close();
808
- if (data.success) {
809
- if (data.type === "openkey:auth:response") {
810
- if (data.sessionToken) this.sessionToken = data.sessionToken;
811
- resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
812
- } else if (data.type === "openkey:link-wallet:response") {
813
- resolve({ address: data.address, keyId: data.keyId });
1505
+ if (this.isResponseForAction(action, data)) {
1506
+ if (isVersionedRequest) {
1507
+ const respRequestId = data.requestId;
1508
+ const respVersion = data.protocolVersion;
1509
+ if (respRequestId !== outgoingRequestId) return;
1510
+ if (respVersion !== outgoingProtocolVersion) return;
1511
+ }
1512
+ settle(() => {
1513
+ popup.close();
1514
+ if (data.success) {
1515
+ if (data.type === "openkey:auth:response") {
1516
+ if (data.sessionToken) this.sessionToken = data.sessionToken;
1517
+ resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
1518
+ } else if (data.type === "openkey:link-wallet:response") {
1519
+ resolve({ address: data.address, keyId: data.keyId });
1520
+ } else if (data.type === "openkey:sign-out:response") {
1521
+ resolve({ requestId: data.requestId, revoked: data.revoked });
1522
+ } else if (data.type === "openkey:externalSign:approve") {
1523
+ resolve({
1524
+ authorizationContextToken: data.authorizationContextToken,
1525
+ previewApprovalToken: data.previewApprovalToken,
1526
+ signedMessage: data.signedMessage,
1527
+ selectedActionIds: data.selectedActionIds,
1528
+ address: data.address
1529
+ });
1530
+ } else {
1531
+ resolve({
1532
+ signature: data.signature,
1533
+ address: data.address,
1534
+ signedMessage: data.signedMessage,
1535
+ selectedActionKeys: data.selectedActionKeys,
1536
+ permissions: data.permissions
1537
+ });
1538
+ }
814
1539
  } else {
815
- resolve({ signature: data.signature, address: data.address });
1540
+ reject(data.error);
816
1541
  }
817
- } else {
818
- reject(data.error);
819
- }
1542
+ });
820
1543
  }
821
1544
  };
822
1545
  window.addEventListener("message", handleMessage);
1546
+ cancel = () => {
1547
+ settle(() => {
1548
+ popup.close();
1549
+ reject({ code: "USER_CANCELLED", message: "Superseded by sign-out" });
1550
+ });
1551
+ };
1552
+ this.activeFlowCancellations.add(cancel);
1553
+ }
1554
+ cancelActiveFlows() {
1555
+ for (const cancel of [...this.activeFlowCancellations]) cancel();
823
1556
  }
824
1557
  /**
825
1558
  * Disconnect and close any open flows
826
1559
  */
827
1560
  disconnect() {
1561
+ this.cancelActiveFlows();
828
1562
  this.popup?.close();
829
1563
  this.popup = null;
830
1564
  }
@@ -896,9 +1630,14 @@ function getOpenKey(config) {
896
1630
  return defaultInstance;
897
1631
  }
898
1632
  export {
1633
+ KeyIdTypeMismatchError,
899
1634
  OpenKey,
1635
+ OpenKeyNostr,
900
1636
  OpenKeyProvider,
901
1637
  index_default as default,
902
- getOpenKey
1638
+ getOpenKey,
1639
+ resolveAuthorizeTinyCloudRouting,
1640
+ shouldRouteAuthorizeTinyCloudExternal,
1641
+ validateIframeResize
903
1642
  };
904
1643
  //# sourceMappingURL=index.mjs.map