@openkey/sdk 0.9.0 → 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 +314 -1
- package/dist/index.d.ts +314 -1
- package/dist/index.js +774 -51
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +766 -55
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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;
|
|
@@ -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,36 @@ 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
|
+
}
|
|
265
666
|
/**
|
|
266
667
|
* Better-auth session token relayed during connect(), or null if the
|
|
267
668
|
* flow did not provide one. Use as a bearer token for API calls made
|
|
@@ -300,6 +701,208 @@ var OpenKey = class {
|
|
|
300
701
|
}
|
|
301
702
|
return this.signWithOpenKey(request, opts?.mode);
|
|
302
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
|
+
}
|
|
303
906
|
/**
|
|
304
907
|
* Sign typed data (EIP-712) with the user's OpenKey wallet
|
|
305
908
|
* For external keys, routes directly to the user's wallet provider
|
|
@@ -434,14 +1037,14 @@ var OpenKey = class {
|
|
|
434
1037
|
* @returns Promise that resolves with authorization code after user consent
|
|
435
1038
|
*/
|
|
436
1039
|
connect: async (config) => {
|
|
437
|
-
const verifier =
|
|
438
|
-
const challenge = await
|
|
439
|
-
const state = config.state ||
|
|
1040
|
+
const verifier = y();
|
|
1041
|
+
const challenge = await k(verifier);
|
|
1042
|
+
const state = config.state || m();
|
|
440
1043
|
sessionStorage.setItem(
|
|
441
1044
|
OAUTH_STORAGE_KEY,
|
|
442
1045
|
JSON.stringify({ verifier, state })
|
|
443
1046
|
);
|
|
444
|
-
const authUrl =
|
|
1047
|
+
const authUrl = T({
|
|
445
1048
|
host: this.oauthHost,
|
|
446
1049
|
clientId: config.clientId,
|
|
447
1050
|
redirectUri: config.redirectUri,
|
|
@@ -613,7 +1216,7 @@ var OpenKey = class {
|
|
|
613
1216
|
const eoaFlag = action === "connect" && this.hasDetectedEoa() ? "&hasEoa=true" : "";
|
|
614
1217
|
if (mode === "popup") {
|
|
615
1218
|
const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
|
|
616
|
-
return new Promise((resolve, reject) => this.openPopup(url2, message, resolve, reject));
|
|
1219
|
+
return new Promise((resolve, reject) => this.openPopup(action, url2, message, resolve, reject));
|
|
617
1220
|
}
|
|
618
1221
|
if (mode === "redirect") {
|
|
619
1222
|
const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
|
|
@@ -625,10 +1228,14 @@ var OpenKey = class {
|
|
|
625
1228
|
return this.openIframeModal(url, action, message, origin);
|
|
626
1229
|
}
|
|
627
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;
|
|
628
1234
|
return new Promise((resolve, reject) => {
|
|
629
1235
|
let readyReceived = false;
|
|
630
1236
|
let settled = false;
|
|
631
1237
|
let modal = null;
|
|
1238
|
+
let cancel;
|
|
632
1239
|
const settle = (fn) => {
|
|
633
1240
|
if (settled) return;
|
|
634
1241
|
settled = true;
|
|
@@ -639,24 +1246,35 @@ var OpenKey = class {
|
|
|
639
1246
|
clearTimeout(overallTimeout);
|
|
640
1247
|
modal?.destroy();
|
|
641
1248
|
modal = null;
|
|
1249
|
+
this.activeFlowCancellations.delete(cancel);
|
|
642
1250
|
};
|
|
643
1251
|
modal = new IframeModal({
|
|
644
1252
|
url,
|
|
645
1253
|
host: this.host,
|
|
646
1254
|
onClose: () => {
|
|
647
1255
|
settle(() => {
|
|
648
|
-
|
|
649
|
-
clearTimeout(overallTimeout);
|
|
650
|
-
modal = null;
|
|
1256
|
+
cleanup();
|
|
651
1257
|
reject({ code: "USER_CANCELLED", message: "User cancelled the request" });
|
|
652
1258
|
});
|
|
653
1259
|
},
|
|
654
1260
|
onMessage: (data) => {
|
|
655
1261
|
if (data.type === "openkey:ready") {
|
|
656
1262
|
readyReceived = true;
|
|
1263
|
+
if (isVersionedRequest && modal) {
|
|
1264
|
+
modal.setExpectedCorrelation(
|
|
1265
|
+
outgoingRequestId,
|
|
1266
|
+
outgoingProtocolVersion
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
657
1269
|
modal?.postMessage(message);
|
|
658
1270
|
return;
|
|
659
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
|
+
}
|
|
660
1278
|
if (data.type === "openkey:close") {
|
|
661
1279
|
settle(() => {
|
|
662
1280
|
cleanup();
|
|
@@ -673,7 +1291,7 @@ var OpenKey = class {
|
|
|
673
1291
|
this.handleExternalWalletConnect(resolve, reject);
|
|
674
1292
|
return;
|
|
675
1293
|
}
|
|
676
|
-
if (
|
|
1294
|
+
if (this.isResponseForAction(action, data)) {
|
|
677
1295
|
settle(() => {
|
|
678
1296
|
cleanup();
|
|
679
1297
|
if (data.success) {
|
|
@@ -682,8 +1300,24 @@ var OpenKey = class {
|
|
|
682
1300
|
resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
|
|
683
1301
|
} else if (data.type === "openkey:link-wallet:response") {
|
|
684
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
|
+
});
|
|
685
1313
|
} else {
|
|
686
|
-
resolve({
|
|
1314
|
+
resolve({
|
|
1315
|
+
signature: data.signature,
|
|
1316
|
+
address: data.address,
|
|
1317
|
+
signedMessage: data.signedMessage,
|
|
1318
|
+
selectedActionKeys: data.selectedActionKeys,
|
|
1319
|
+
permissions: data.permissions
|
|
1320
|
+
});
|
|
687
1321
|
}
|
|
688
1322
|
} else {
|
|
689
1323
|
reject(data.error);
|
|
@@ -698,7 +1332,7 @@ var OpenKey = class {
|
|
|
698
1332
|
console.warn("OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP.");
|
|
699
1333
|
showToast();
|
|
700
1334
|
const popupUrl = `${this.host}/widget/${action}?origin=${origin}`;
|
|
701
|
-
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)));
|
|
702
1336
|
}, IFRAME_READY_TIMEOUT);
|
|
703
1337
|
const overallTimeout = setTimeout(() => {
|
|
704
1338
|
settle(() => {
|
|
@@ -706,8 +1340,24 @@ var OpenKey = class {
|
|
|
706
1340
|
reject({ code: "TIMEOUT", message: "Request timed out" });
|
|
707
1341
|
});
|
|
708
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);
|
|
709
1350
|
});
|
|
710
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
|
+
}
|
|
711
1361
|
handleWalletLinkDelegation(modal) {
|
|
712
1362
|
const sendResult = (result) => modal.postMessage(result);
|
|
713
1363
|
new WalletPicker({
|
|
@@ -769,15 +1419,16 @@ var OpenKey = class {
|
|
|
769
1419
|
}
|
|
770
1420
|
});
|
|
771
1421
|
}
|
|
772
|
-
openPopup(url, message, resolve, reject) {
|
|
1422
|
+
openPopup(action, url, message, resolve, reject) {
|
|
773
1423
|
const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
|
|
774
1424
|
const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
|
|
775
|
-
|
|
1425
|
+
const popup = window.open(
|
|
776
1426
|
url,
|
|
777
|
-
|
|
1427
|
+
`openkey-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
778
1428
|
`width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
|
|
779
1429
|
);
|
|
780
|
-
|
|
1430
|
+
this.popup = popup;
|
|
1431
|
+
if (!popup) {
|
|
781
1432
|
showToast("Popup was blocked. Please allow popups for this site.", "error");
|
|
782
1433
|
reject({
|
|
783
1434
|
code: "POPUP_BLOCKED",
|
|
@@ -785,74 +1436,129 @@ var OpenKey = class {
|
|
|
785
1436
|
});
|
|
786
1437
|
return;
|
|
787
1438
|
}
|
|
1439
|
+
let settled = false;
|
|
1440
|
+
let cancel;
|
|
788
1441
|
const cleanup = () => {
|
|
789
1442
|
window.removeEventListener("message", handleMessage);
|
|
790
1443
|
clearInterval(pollClosed);
|
|
791
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();
|
|
792
1453
|
};
|
|
793
1454
|
const pollClosed = setInterval(() => {
|
|
794
|
-
if (
|
|
795
|
-
|
|
796
|
-
reject({
|
|
1455
|
+
if (popup.closed) {
|
|
1456
|
+
settle(() => reject({
|
|
797
1457
|
code: "USER_CANCELLED",
|
|
798
1458
|
message: "User closed the popup"
|
|
799
|
-
});
|
|
1459
|
+
}));
|
|
800
1460
|
}
|
|
801
1461
|
}, 500);
|
|
802
1462
|
const timeout = setTimeout(() => {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1463
|
+
settle(() => {
|
|
1464
|
+
popup.close();
|
|
1465
|
+
reject({
|
|
1466
|
+
code: "TIMEOUT",
|
|
1467
|
+
message: "Request timed out"
|
|
1468
|
+
});
|
|
808
1469
|
});
|
|
809
1470
|
}, DEFAULT_TIMEOUT);
|
|
1471
|
+
const outgoingRequestId = message.requestId;
|
|
1472
|
+
const outgoingProtocolVersion = message.protocolVersion;
|
|
1473
|
+
const isVersionedRequest = typeof outgoingRequestId === "string" && typeof outgoingProtocolVersion === "number" && outgoingProtocolVersion >= 1;
|
|
810
1474
|
const handleMessage = (event) => {
|
|
811
1475
|
if (event.origin !== this.host) return;
|
|
812
|
-
if (event.source !==
|
|
1476
|
+
if (event.source !== popup) return;
|
|
813
1477
|
const data = event.data;
|
|
814
1478
|
if (data.type === "openkey:ready") {
|
|
815
|
-
|
|
1479
|
+
popup.postMessage(message, this.host);
|
|
816
1480
|
return;
|
|
817
1481
|
}
|
|
818
1482
|
if (data.type === "openkey:close") {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
+
});
|
|
824
1495
|
});
|
|
825
1496
|
return;
|
|
826
1497
|
}
|
|
827
1498
|
if (data.type === "openkey:auth:use-external-wallet") {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1499
|
+
settle(() => {
|
|
1500
|
+
popup.close();
|
|
1501
|
+
this.handleExternalWalletConnect(resolve, reject);
|
|
1502
|
+
});
|
|
831
1503
|
return;
|
|
832
1504
|
}
|
|
833
|
-
if (
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
+
}
|
|
842
1539
|
} else {
|
|
843
|
-
|
|
1540
|
+
reject(data.error);
|
|
844
1541
|
}
|
|
845
|
-
}
|
|
846
|
-
reject(data.error);
|
|
847
|
-
}
|
|
1542
|
+
});
|
|
848
1543
|
}
|
|
849
1544
|
};
|
|
850
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();
|
|
851
1556
|
}
|
|
852
1557
|
/**
|
|
853
1558
|
* Disconnect and close any open flows
|
|
854
1559
|
*/
|
|
855
1560
|
disconnect() {
|
|
1561
|
+
this.cancelActiveFlows();
|
|
856
1562
|
this.popup?.close();
|
|
857
1563
|
this.popup = null;
|
|
858
1564
|
}
|
|
@@ -924,9 +1630,14 @@ function getOpenKey(config) {
|
|
|
924
1630
|
return defaultInstance;
|
|
925
1631
|
}
|
|
926
1632
|
export {
|
|
1633
|
+
KeyIdTypeMismatchError,
|
|
927
1634
|
OpenKey,
|
|
1635
|
+
OpenKeyNostr,
|
|
928
1636
|
OpenKeyProvider,
|
|
929
1637
|
index_default as default,
|
|
930
|
-
getOpenKey
|
|
1638
|
+
getOpenKey,
|
|
1639
|
+
resolveAuthorizeTinyCloudRouting,
|
|
1640
|
+
shouldRouteAuthorizeTinyCloudExternal,
|
|
1641
|
+
validateIframeResize
|
|
931
1642
|
};
|
|
932
1643
|
//# sourceMappingURL=index.mjs.map
|