@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/README.md +26 -0
- package/dist/index.d.mts +333 -2
- package/dist/index.d.ts +333 -2
- package/dist/index.js +822 -71
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +814 -75
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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 =
|
|
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
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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 =
|
|
410
|
-
const challenge = await
|
|
411
|
-
const state = config.state ||
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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({
|
|
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
|
-
|
|
1425
|
+
const popup = window.open(
|
|
748
1426
|
url,
|
|
749
|
-
|
|
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
|
-
|
|
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 (
|
|
767
|
-
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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 !==
|
|
1476
|
+
if (event.source !== popup) return;
|
|
785
1477
|
const data = event.data;
|
|
786
1478
|
if (data.type === "openkey:ready") {
|
|
787
|
-
|
|
1479
|
+
popup.postMessage(message, this.host);
|
|
788
1480
|
return;
|
|
789
1481
|
}
|
|
790
1482
|
if (data.type === "openkey:close") {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1499
|
+
settle(() => {
|
|
1500
|
+
popup.close();
|
|
1501
|
+
this.handleExternalWalletConnect(resolve, reject);
|
|
1502
|
+
});
|
|
803
1503
|
return;
|
|
804
1504
|
}
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
if (
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
1540
|
+
reject(data.error);
|
|
816
1541
|
}
|
|
817
|
-
}
|
|
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
|