@openkey/sdk 0.9.0 → 0.10.1
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 +325 -1
- package/dist/index.d.ts +325 -1
- package/dist/index.js +779 -51
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +771 -55
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,321 @@
|
|
|
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) {
|
|
243
|
+
if (data.requestId !== void 0) return null;
|
|
244
|
+
} else {
|
|
245
|
+
if (expected.requestId === null || expected.protocolVersion === null) return null;
|
|
246
|
+
if (typeof data.protocolVersion !== "number" || data.protocolVersion !== expected.protocolVersion) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
if (typeof data.requestId !== "string" || data.requestId !== expected.requestId) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (typeof data.height !== "number" || !Number.isFinite(data.height) || data.height <= 0) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
return Math.min(data.height, Math.floor(expected.viewportHeight * 0.85));
|
|
257
|
+
}
|
|
258
|
+
function shouldRouteAuthorizeTinyCloudExternal(state) {
|
|
259
|
+
return state.lastAuth?.keyType === "EXTERNAL";
|
|
260
|
+
}
|
|
261
|
+
var KeyIdTypeMismatchError = class extends Error {
|
|
262
|
+
code;
|
|
263
|
+
requestKeyId;
|
|
264
|
+
lastAuthKeyId;
|
|
265
|
+
requestKeyType;
|
|
266
|
+
lastAuthKeyType;
|
|
267
|
+
constructor(fields) {
|
|
268
|
+
super(fields.message);
|
|
269
|
+
this.name = "KeyIdTypeMismatchError";
|
|
270
|
+
this.code = "KEY_ID_TYPE_MISMATCH";
|
|
271
|
+
this.requestKeyId = fields.requestKeyId;
|
|
272
|
+
this.lastAuthKeyId = fields.lastAuthKeyId;
|
|
273
|
+
this.requestKeyType = fields.requestKeyType;
|
|
274
|
+
this.lastAuthKeyType = fields.lastAuthKeyType;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
function resolveAuthorizeTinyCloudRouting(state) {
|
|
278
|
+
const lastAuthKeyId = state.lastAuth?.keyId ?? null;
|
|
279
|
+
const lastAuthKeyType = state.lastAuth?.keyType ?? null;
|
|
280
|
+
const rawRequestKeyId = state.requestKeyId ?? null;
|
|
281
|
+
const requestKeyId = rawRequestKeyId && rawRequestKeyId.length > 0 ? rawRequestKeyId : null;
|
|
282
|
+
const knownKeys = state.knownKeys ?? [];
|
|
283
|
+
const lookupType = (id) => {
|
|
284
|
+
for (const k2 of knownKeys) {
|
|
285
|
+
if (k2.id === id) return k2.keyType;
|
|
286
|
+
if (k2.address && k2.address.toLowerCase() === id.toLowerCase()) return k2.keyType;
|
|
287
|
+
}
|
|
288
|
+
if (id.startsWith("external:")) return "EXTERNAL";
|
|
289
|
+
return null;
|
|
290
|
+
};
|
|
291
|
+
if (!requestKeyId) {
|
|
292
|
+
const route = lastAuthKeyType === "EXTERNAL" ? "external" : "managed";
|
|
293
|
+
return {
|
|
294
|
+
route,
|
|
295
|
+
resolvedKeyId: lastAuthKeyId,
|
|
296
|
+
resolvedKeyType: lastAuthKeyType
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
let requestKeyType = lookupType(requestKeyId);
|
|
300
|
+
if (!requestKeyType && lastAuthKeyId && requestKeyId === lastAuthKeyId) {
|
|
301
|
+
requestKeyType = lastAuthKeyType;
|
|
302
|
+
}
|
|
303
|
+
if (requestKeyType && lastAuthKeyType && requestKeyType !== lastAuthKeyType) {
|
|
304
|
+
throw new KeyIdTypeMismatchError({
|
|
305
|
+
message: `authorizeTinyCloud: request.keyId ${requestKeyId} resolves to ${requestKeyType} but active session key ${lastAuthKeyId} is ${lastAuthKeyType}; refuse to route silently`,
|
|
306
|
+
requestKeyId,
|
|
307
|
+
lastAuthKeyId,
|
|
308
|
+
requestKeyType,
|
|
309
|
+
lastAuthKeyType
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const effectiveType = requestKeyType ?? lastAuthKeyType;
|
|
313
|
+
return {
|
|
314
|
+
route: effectiveType === "EXTERNAL" ? "external" : "managed",
|
|
315
|
+
resolvedKeyId: requestKeyId,
|
|
316
|
+
resolvedKeyType: effectiveType
|
|
317
|
+
};
|
|
318
|
+
}
|
|
14
319
|
var IframeModal = class {
|
|
15
320
|
root;
|
|
16
321
|
shadow;
|
|
@@ -19,6 +324,13 @@ var IframeModal = class {
|
|
|
19
324
|
onMessage;
|
|
20
325
|
messageHandler;
|
|
21
326
|
host;
|
|
327
|
+
// Sol final continuation contract requirement 5: when the modal carries a
|
|
328
|
+
// versioned request, iframe resize traffic MUST correlate to its
|
|
329
|
+
// requestId AND protocolVersion. Set by `setExpectedCorrelation` as soon
|
|
330
|
+
// as the modal is created. When both are null the flow is unversioned and
|
|
331
|
+
// only uncorrelated resizes from this iframe are accepted (TC-647).
|
|
332
|
+
expectedRequestId = null;
|
|
333
|
+
expectedProtocolVersion = null;
|
|
22
334
|
constructor(opts) {
|
|
23
335
|
this.host = opts.host;
|
|
24
336
|
this.onClose = opts.onClose;
|
|
@@ -74,10 +386,17 @@ var IframeModal = class {
|
|
|
74
386
|
if (event.source !== this.iframe.contentWindow) return;
|
|
75
387
|
const data = event.data;
|
|
76
388
|
if (data.type === "openkey:resize") {
|
|
77
|
-
const h =
|
|
389
|
+
const h = validateIframeResize(event.data, {
|
|
390
|
+
requestId: this.expectedRequestId,
|
|
391
|
+
protocolVersion: this.expectedProtocolVersion,
|
|
392
|
+
viewportHeight: window.innerHeight
|
|
393
|
+
});
|
|
394
|
+
if (h === null) return;
|
|
78
395
|
this.iframe.style.height = `${h}px`;
|
|
79
396
|
return;
|
|
80
397
|
}
|
|
398
|
+
if (data.type === "openkey:close") {
|
|
399
|
+
}
|
|
81
400
|
this.onMessage(data);
|
|
82
401
|
};
|
|
83
402
|
window.addEventListener("message", this.messageHandler);
|
|
@@ -86,6 +405,21 @@ var IframeModal = class {
|
|
|
86
405
|
postMessage(message) {
|
|
87
406
|
this.iframe.contentWindow?.postMessage(message, this.host);
|
|
88
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Sol final continuation contract requirement 5: bind the active
|
|
410
|
+
* request correlation so subsequent resize (and future correlated)
|
|
411
|
+
* messages can be verified against `requestId` + `protocolVersion`.
|
|
412
|
+
*
|
|
413
|
+
* The outer flow calls this right after creating the modal for a
|
|
414
|
+
* versioned request, before the iframe can send anything, so no
|
|
415
|
+
* uncorrelated resize is ever accepted for that request. Passing `null`
|
|
416
|
+
* for both arguments means the flow is unversioned; passing `null` for
|
|
417
|
+
* only one drops every resize.
|
|
418
|
+
*/
|
|
419
|
+
setExpectedCorrelation(requestId, protocolVersion) {
|
|
420
|
+
this.expectedRequestId = requestId;
|
|
421
|
+
this.expectedProtocolVersion = protocolVersion;
|
|
422
|
+
}
|
|
89
423
|
destroy() {
|
|
90
424
|
window.removeEventListener("message", this.messageHandler);
|
|
91
425
|
this.root.remove();
|
|
@@ -219,12 +553,23 @@ var OpenKey = class {
|
|
|
219
553
|
lastAuth = null;
|
|
220
554
|
discoveredProviders = [];
|
|
221
555
|
sessionToken = null;
|
|
556
|
+
/** Cancels widget flows that could outlive a sign-out on this instance. */
|
|
557
|
+
activeFlowCancellations = /* @__PURE__ */ new Set();
|
|
558
|
+
/**
|
|
559
|
+
* Nostr identity custody + signing (secp256k1 Schnorr / BIP-340). Kept
|
|
560
|
+
* fully separate from the Ethereum flows above: it never reuses
|
|
561
|
+
* `lastAuth`/`sessionToken`, and its widget messages never carry a
|
|
562
|
+
* sessionToken - the OpenKey session and any signing grants stay inside
|
|
563
|
+
* the OpenKey-origin iframe for the whole lifetime of this client.
|
|
564
|
+
*/
|
|
565
|
+
nostr;
|
|
222
566
|
constructor(config = {}) {
|
|
223
567
|
this.config = config;
|
|
224
568
|
this.host = config.host || DEFAULT_HOST;
|
|
225
569
|
this.oauthHost = config.oauthHost || this.deriveOAuthHost(this.host);
|
|
226
570
|
this.appName = config.appName || window.location.hostname;
|
|
227
571
|
this.mode = config.mode ?? "iframe";
|
|
572
|
+
this.nostr = new OpenKeyNostr(this.host);
|
|
228
573
|
if (typeof window !== "undefined") {
|
|
229
574
|
this.discoveredProviders = [];
|
|
230
575
|
window.addEventListener("eip6963:announceProvider", (event) => {
|
|
@@ -250,6 +595,37 @@ var OpenKey = class {
|
|
|
250
595
|
return "https://api.openkey.so";
|
|
251
596
|
}
|
|
252
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Sol MAJOR-4 (final continuation): fetch the caller's registered keys
|
|
600
|
+
* so `resolveAuthorizeTinyCloudRouting` can resolve an explicit pin
|
|
601
|
+
* whose keyId does not match the active session key. Called only when
|
|
602
|
+
* the caller passes a `requestKeyId` we do not already recognise; the
|
|
603
|
+
* common no-pin path skips the network round-trip.
|
|
604
|
+
*
|
|
605
|
+
* Failures do NOT block routing — we fall through with `knownKeys=[]`
|
|
606
|
+
* so the resolver's lastAuth-based fallback still runs. If the caller
|
|
607
|
+
* pinned a genuinely unknown key that turns out to be external, the
|
|
608
|
+
* widget rejects the server-signing path and the SDK surfaces that
|
|
609
|
+
* error from the finalize call rather than silently routing wrong.
|
|
610
|
+
*/
|
|
611
|
+
async fetchKnownKeysIfNeeded(requestKeyId) {
|
|
612
|
+
if (!requestKeyId) return [];
|
|
613
|
+
const la = this.lastAuth;
|
|
614
|
+
if (la?.keyId === requestKeyId) return [];
|
|
615
|
+
if (la?.address && la.address.toLowerCase() === requestKeyId.toLowerCase()) return [];
|
|
616
|
+
if (requestKeyId.startsWith("external:")) return [];
|
|
617
|
+
try {
|
|
618
|
+
const res = await fetch(`${this.oauthHost}/api/keys`, {
|
|
619
|
+
method: "GET",
|
|
620
|
+
credentials: "include"
|
|
621
|
+
});
|
|
622
|
+
if (!res.ok) return [];
|
|
623
|
+
const body = await res.json();
|
|
624
|
+
return Array.isArray(body?.keys) ? body.keys : [];
|
|
625
|
+
} catch {
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
253
629
|
/**
|
|
254
630
|
* Connect to OpenKey and get user's wallet address
|
|
255
631
|
* Opens auth flow for user to select/create a key
|
|
@@ -262,6 +638,36 @@ var OpenKey = class {
|
|
|
262
638
|
this.lastAuth = result;
|
|
263
639
|
return result;
|
|
264
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Signs the active OpenKey account out through the OpenKey-owned widget.
|
|
643
|
+
*
|
|
644
|
+
* The SDK passes its Better Auth bearer through the OpenKey-owned widget
|
|
645
|
+
* protocol so applications do not have to implement revocation themselves.
|
|
646
|
+
* Local SDK session state is cleared before opening the widget, including
|
|
647
|
+
* when remote revocation is unavailable.
|
|
648
|
+
*/
|
|
649
|
+
async signOut(opts) {
|
|
650
|
+
const requestId = `ok-signout-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
651
|
+
const sessionToken = this.sessionToken || void 0;
|
|
652
|
+
const mode = this.resolveMode(opts?.mode) === "redirect" ? "popup" : opts?.mode;
|
|
653
|
+
this.cancelActiveFlows();
|
|
654
|
+
this.lastAuth = null;
|
|
655
|
+
this.sessionToken = null;
|
|
656
|
+
const acknowledgement = await this.openFlow(
|
|
657
|
+
"sign-out",
|
|
658
|
+
{
|
|
659
|
+
type: "openkey:sign-out:request",
|
|
660
|
+
requestId,
|
|
661
|
+
protocolVersion: 1,
|
|
662
|
+
sessionToken
|
|
663
|
+
},
|
|
664
|
+
mode
|
|
665
|
+
);
|
|
666
|
+
if (acknowledgement.requestId !== requestId) {
|
|
667
|
+
throw new Error("OpenKey signOut received an acknowledgement for a different request");
|
|
668
|
+
}
|
|
669
|
+
return acknowledgement;
|
|
670
|
+
}
|
|
265
671
|
/**
|
|
266
672
|
* Better-auth session token relayed during connect(), or null if the
|
|
267
673
|
* flow did not provide one. Use as a bearer token for API calls made
|
|
@@ -300,6 +706,208 @@ var OpenKey = class {
|
|
|
300
706
|
}
|
|
301
707
|
return this.signWithOpenKey(request, opts?.mode);
|
|
302
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* Versioned TinyCloud authorization protocol (v1).
|
|
711
|
+
*
|
|
712
|
+
* Unlike `signMessage()` (which is contractually byte-exact — the returned
|
|
713
|
+
* signature always verifies against the caller's exact bytes), this method
|
|
714
|
+
* permits the user to narrow the requested capabilities before signing.
|
|
715
|
+
* OpenKey may therefore regenerate the SIWE. The response carries
|
|
716
|
+
* `signedMessage` — the exact bytes the signature actually verifies against.
|
|
717
|
+
*
|
|
718
|
+
* TinyCloud completes the session with `signedMessage`, not the original
|
|
719
|
+
* `siwe` passed in. Consumers MUST use the returned bytes and MUST NOT
|
|
720
|
+
* assume the returned grants are a superset of what they requested — the
|
|
721
|
+
* user is allowed to remove any grant that is not required.
|
|
722
|
+
*
|
|
723
|
+
* Legacy `signMessage()` callers are unaffected: their exact-byte contract
|
|
724
|
+
* remains preserved and OpenKey never rewrites their bytes.
|
|
725
|
+
*/
|
|
726
|
+
async authorizeTinyCloud(request, opts) {
|
|
727
|
+
if (request.protocolVersion !== 1) {
|
|
728
|
+
throw new Error(
|
|
729
|
+
`authorizeTinyCloud only supports protocolVersion 1; got ${request.protocolVersion}`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (typeof request.siwe !== "string" || !request.siwe) {
|
|
733
|
+
throw new Error("authorizeTinyCloud requires a non-empty SIWE");
|
|
734
|
+
}
|
|
735
|
+
let routing;
|
|
736
|
+
try {
|
|
737
|
+
const knownKeys = await this.fetchKnownKeysIfNeeded(request.keyId);
|
|
738
|
+
routing = resolveAuthorizeTinyCloudRouting({
|
|
739
|
+
lastAuth: this.lastAuth,
|
|
740
|
+
requestKeyId: request.keyId,
|
|
741
|
+
knownKeys
|
|
742
|
+
});
|
|
743
|
+
} catch (e) {
|
|
744
|
+
if (e instanceof KeyIdTypeMismatchError) throw e;
|
|
745
|
+
throw e instanceof Error ? e : new Error("authorizeTinyCloud: failed to resolve target key type");
|
|
746
|
+
}
|
|
747
|
+
if (routing.route === "external") {
|
|
748
|
+
return this.authorizeTinyCloudExternal(request, opts);
|
|
749
|
+
}
|
|
750
|
+
const requestId = `ok-auth-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
751
|
+
const resolvedKeyId = routing.resolvedKeyId ?? request.keyId ?? this.lastAuth?.keyId;
|
|
752
|
+
const raw = await this.openFlow(
|
|
753
|
+
"sign",
|
|
754
|
+
{
|
|
755
|
+
type: "openkey:sign:request",
|
|
756
|
+
requestId,
|
|
757
|
+
protocolVersion: 1,
|
|
758
|
+
message: request.siwe,
|
|
759
|
+
keyId: resolvedKeyId,
|
|
760
|
+
// Forward JWK so the widget can pass it to /authorize-sign for
|
|
761
|
+
// narrowed-SIWE regeneration.
|
|
762
|
+
jwk: request.jwk,
|
|
763
|
+
// Forward the TinyCloud host so the widget can bind it into the
|
|
764
|
+
// /authorize-sign-prepare context (Sol MAJOR-2).
|
|
765
|
+
host: request.host,
|
|
766
|
+
// Sol MAJOR-2 (envelope): forward the caller-supplied presentation
|
|
767
|
+
// envelope so the widget can render honest provenance and the
|
|
768
|
+
// /authorize-sign-prepare route can bind it into the authorization
|
|
769
|
+
// context. The envelope is display-only; the widget transport
|
|
770
|
+
// validates and size-bounds it before use and NEVER treats it as
|
|
771
|
+
// verified unless the server actually origin-binds a manifest.
|
|
772
|
+
presentation: request.presentation,
|
|
773
|
+
sessionToken: this.sessionToken || void 0
|
|
774
|
+
},
|
|
775
|
+
opts?.mode
|
|
776
|
+
);
|
|
777
|
+
if (typeof raw.signedMessage !== "string" || !raw.signedMessage) {
|
|
778
|
+
throw new Error(
|
|
779
|
+
"authorizeTinyCloud: widget returned no signedMessage \u2014 signature would not correspond to the displayed authorization"
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (!Array.isArray(raw.selectedActionKeys)) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
"authorizeTinyCloud: widget returned no selectedActionKeys \u2014 cannot verify displayed authorization matches the signed bytes"
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
if (!Array.isArray(raw.permissions)) {
|
|
788
|
+
throw new Error(
|
|
789
|
+
"authorizeTinyCloud: widget returned no permissions \u2014 cannot verify displayed authorization matches the signed bytes"
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
protocolVersion: 1,
|
|
794
|
+
address: raw.address,
|
|
795
|
+
signature: raw.signature,
|
|
796
|
+
signedMessage: raw.signedMessage,
|
|
797
|
+
selectedActionKeys: raw.selectedActionKeys,
|
|
798
|
+
permissions: raw.permissions
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Sol MAJOR-3 (continuation): authorizeTinyCloud path for external keys.
|
|
803
|
+
* The wallet lives outside OpenKey, so the SDK opens the shared widget
|
|
804
|
+
* for review + preview, then invokes the user's wallet to sign the
|
|
805
|
+
* preview bytes, then finalizes server-side via /authorize-sign.
|
|
806
|
+
*
|
|
807
|
+
* Flow:
|
|
808
|
+
* 1. Open the widget with `externalSign: true` — the widget renders
|
|
809
|
+
* the shared SigningApproval UI, calls /authorize-sign-prepare,
|
|
810
|
+
* calls /authorize-sign-preview after the user approves a
|
|
811
|
+
* selection, and hands back `{ previewApprovalToken,
|
|
812
|
+
* signedMessage, selectedActionIds, address, authorizationContextToken }`.
|
|
813
|
+
* 2. SDK invokes the user's wallet on the returned signedMessage.
|
|
814
|
+
* 3. SDK POSTs /authorize-sign with `externalSignature`.
|
|
815
|
+
*
|
|
816
|
+
* Legacy exact-byte signMessage callers are unaffected — this path is
|
|
817
|
+
* only taken via authorizeTinyCloud (protocolVersion:1) when the target
|
|
818
|
+
* key is EXTERNAL. Sol's rejection required this path to go through
|
|
819
|
+
* the same review UI as managed keys.
|
|
820
|
+
*/
|
|
821
|
+
async authorizeTinyCloudExternal(request, opts) {
|
|
822
|
+
const keyId = request.keyId ?? this.lastAuth?.keyId;
|
|
823
|
+
if (!keyId) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
"authorizeTinyCloud (external) requires a keyId \u2014 call connect() first or pass request.keyId"
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const walletAddress = this.lastAuth?.address;
|
|
829
|
+
if (!walletAddress) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
"authorizeTinyCloud (external) requires a connected wallet \u2014 call connect() first"
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
const requestId = `ok-ext-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
835
|
+
const approval = await this.openFlow(
|
|
836
|
+
"sign",
|
|
837
|
+
{
|
|
838
|
+
type: "openkey:sign:request",
|
|
839
|
+
requestId,
|
|
840
|
+
protocolVersion: 1,
|
|
841
|
+
message: request.siwe,
|
|
842
|
+
keyId,
|
|
843
|
+
jwk: request.jwk,
|
|
844
|
+
host: request.host,
|
|
845
|
+
// Sol MAJOR-2 (envelope): forward the caller-supplied presentation
|
|
846
|
+
// envelope. Same rules as the managed path — display-only, size-
|
|
847
|
+
// bounded at the transport, never treated as verified without
|
|
848
|
+
// server-side origin-binding.
|
|
849
|
+
presentation: request.presentation,
|
|
850
|
+
sessionToken: this.sessionToken || void 0,
|
|
851
|
+
// Sol MAJOR-3 (continuation): tell the widget to hand us back
|
|
852
|
+
// the previewApproval instead of asking OpenKey to sign
|
|
853
|
+
// server-side with a managed key. The widget will still render
|
|
854
|
+
// the SAME SigningApproval component; it just diverges at the
|
|
855
|
+
// "sign" step and emits `openkey:externalSign:approve` back
|
|
856
|
+
// through the transport.
|
|
857
|
+
externalSign: true
|
|
858
|
+
},
|
|
859
|
+
opts?.mode
|
|
860
|
+
);
|
|
861
|
+
if (!approval?.previewApprovalToken || !approval?.signedMessage) {
|
|
862
|
+
throw new Error(
|
|
863
|
+
"authorizeTinyCloud (external): widget did not return a previewApprovalToken + signedMessage"
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
if (approval.address && approval.address.toLowerCase() !== walletAddress.toLowerCase()) {
|
|
867
|
+
throw new Error(
|
|
868
|
+
`authorizeTinyCloud (external): widget preview address ${approval.address} does not match connected wallet ${walletAddress}`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
const provider = await this.findWalletProvider(walletAddress);
|
|
872
|
+
const hexMessage = this.toHex(approval.signedMessage);
|
|
873
|
+
const walletSignature = await provider.request({
|
|
874
|
+
method: "personal_sign",
|
|
875
|
+
params: [hexMessage, walletAddress]
|
|
876
|
+
});
|
|
877
|
+
const finalizeRes = await fetch(
|
|
878
|
+
`${this.oauthHost}/api/delegate/authorize-sign`,
|
|
879
|
+
{
|
|
880
|
+
method: "POST",
|
|
881
|
+
credentials: "include",
|
|
882
|
+
headers: { "Content-Type": "application/json" },
|
|
883
|
+
body: JSON.stringify({
|
|
884
|
+
authorizationContextToken: approval.authorizationContextToken,
|
|
885
|
+
previewApprovalToken: approval.previewApprovalToken,
|
|
886
|
+
selectedActionIds: approval.selectedActionIds,
|
|
887
|
+
protocolVersion: 1,
|
|
888
|
+
externalSignature: walletSignature
|
|
889
|
+
})
|
|
890
|
+
}
|
|
891
|
+
);
|
|
892
|
+
if (!finalizeRes.ok) {
|
|
893
|
+
const body = await finalizeRes.json().catch(() => ({ error: `HTTP ${finalizeRes.status}` }));
|
|
894
|
+
throw new Error(body.error || `authorize-sign failed (${finalizeRes.status})`);
|
|
895
|
+
}
|
|
896
|
+
const finalize = await finalizeRes.json();
|
|
897
|
+
if (finalize.signedMessage !== approval.signedMessage) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
"authorizeTinyCloud (external): server signed different bytes than the preview showed"
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
protocolVersion: 1,
|
|
904
|
+
address: finalize.address,
|
|
905
|
+
signature: finalize.signature,
|
|
906
|
+
signedMessage: finalize.signedMessage,
|
|
907
|
+
selectedActionKeys: finalize.selectedActionKeys ?? [],
|
|
908
|
+
permissions: finalize.permissions ?? []
|
|
909
|
+
};
|
|
910
|
+
}
|
|
303
911
|
/**
|
|
304
912
|
* Sign typed data (EIP-712) with the user's OpenKey wallet
|
|
305
913
|
* For external keys, routes directly to the user's wallet provider
|
|
@@ -434,14 +1042,14 @@ var OpenKey = class {
|
|
|
434
1042
|
* @returns Promise that resolves with authorization code after user consent
|
|
435
1043
|
*/
|
|
436
1044
|
connect: async (config) => {
|
|
437
|
-
const verifier =
|
|
438
|
-
const challenge = await
|
|
439
|
-
const state = config.state ||
|
|
1045
|
+
const verifier = y();
|
|
1046
|
+
const challenge = await k(verifier);
|
|
1047
|
+
const state = config.state || m();
|
|
440
1048
|
sessionStorage.setItem(
|
|
441
1049
|
OAUTH_STORAGE_KEY,
|
|
442
1050
|
JSON.stringify({ verifier, state })
|
|
443
1051
|
);
|
|
444
|
-
const authUrl =
|
|
1052
|
+
const authUrl = T({
|
|
445
1053
|
host: this.oauthHost,
|
|
446
1054
|
clientId: config.clientId,
|
|
447
1055
|
redirectUri: config.redirectUri,
|
|
@@ -613,7 +1221,7 @@ var OpenKey = class {
|
|
|
613
1221
|
const eoaFlag = action === "connect" && this.hasDetectedEoa() ? "&hasEoa=true" : "";
|
|
614
1222
|
if (mode === "popup") {
|
|
615
1223
|
const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
|
|
616
|
-
return new Promise((resolve, reject) => this.openPopup(url2, message, resolve, reject));
|
|
1224
|
+
return new Promise((resolve, reject) => this.openPopup(action, url2, message, resolve, reject));
|
|
617
1225
|
}
|
|
618
1226
|
if (mode === "redirect") {
|
|
619
1227
|
const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
|
|
@@ -625,10 +1233,14 @@ var OpenKey = class {
|
|
|
625
1233
|
return this.openIframeModal(url, action, message, origin);
|
|
626
1234
|
}
|
|
627
1235
|
openIframeModal(url, action, message, origin) {
|
|
1236
|
+
const outgoingRequestId = message.requestId;
|
|
1237
|
+
const outgoingProtocolVersion = message.protocolVersion;
|
|
1238
|
+
const isVersionedRequest = typeof outgoingRequestId === "string" && typeof outgoingProtocolVersion === "number" && outgoingProtocolVersion >= 1;
|
|
628
1239
|
return new Promise((resolve, reject) => {
|
|
629
1240
|
let readyReceived = false;
|
|
630
1241
|
let settled = false;
|
|
631
1242
|
let modal = null;
|
|
1243
|
+
let cancel;
|
|
632
1244
|
const settle = (fn) => {
|
|
633
1245
|
if (settled) return;
|
|
634
1246
|
settled = true;
|
|
@@ -639,15 +1251,14 @@ var OpenKey = class {
|
|
|
639
1251
|
clearTimeout(overallTimeout);
|
|
640
1252
|
modal?.destroy();
|
|
641
1253
|
modal = null;
|
|
1254
|
+
this.activeFlowCancellations.delete(cancel);
|
|
642
1255
|
};
|
|
643
1256
|
modal = new IframeModal({
|
|
644
1257
|
url,
|
|
645
1258
|
host: this.host,
|
|
646
1259
|
onClose: () => {
|
|
647
1260
|
settle(() => {
|
|
648
|
-
|
|
649
|
-
clearTimeout(overallTimeout);
|
|
650
|
-
modal = null;
|
|
1261
|
+
cleanup();
|
|
651
1262
|
reject({ code: "USER_CANCELLED", message: "User cancelled the request" });
|
|
652
1263
|
});
|
|
653
1264
|
},
|
|
@@ -657,6 +1268,12 @@ var OpenKey = class {
|
|
|
657
1268
|
modal?.postMessage(message);
|
|
658
1269
|
return;
|
|
659
1270
|
}
|
|
1271
|
+
if (isVersionedRequest) {
|
|
1272
|
+
const respRequestId = data.requestId;
|
|
1273
|
+
const respVersion = data.protocolVersion;
|
|
1274
|
+
if (respRequestId !== outgoingRequestId) return;
|
|
1275
|
+
if (respVersion !== outgoingProtocolVersion) return;
|
|
1276
|
+
}
|
|
660
1277
|
if (data.type === "openkey:close") {
|
|
661
1278
|
settle(() => {
|
|
662
1279
|
cleanup();
|
|
@@ -673,7 +1290,7 @@ var OpenKey = class {
|
|
|
673
1290
|
this.handleExternalWalletConnect(resolve, reject);
|
|
674
1291
|
return;
|
|
675
1292
|
}
|
|
676
|
-
if (
|
|
1293
|
+
if (this.isResponseForAction(action, data)) {
|
|
677
1294
|
settle(() => {
|
|
678
1295
|
cleanup();
|
|
679
1296
|
if (data.success) {
|
|
@@ -682,8 +1299,24 @@ var OpenKey = class {
|
|
|
682
1299
|
resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
|
|
683
1300
|
} else if (data.type === "openkey:link-wallet:response") {
|
|
684
1301
|
resolve({ address: data.address, keyId: data.keyId });
|
|
1302
|
+
} else if (data.type === "openkey:sign-out:response") {
|
|
1303
|
+
resolve({ requestId: data.requestId, revoked: data.revoked });
|
|
1304
|
+
} else if (data.type === "openkey:externalSign:approve") {
|
|
1305
|
+
resolve({
|
|
1306
|
+
authorizationContextToken: data.authorizationContextToken,
|
|
1307
|
+
previewApprovalToken: data.previewApprovalToken,
|
|
1308
|
+
signedMessage: data.signedMessage,
|
|
1309
|
+
selectedActionIds: data.selectedActionIds,
|
|
1310
|
+
address: data.address
|
|
1311
|
+
});
|
|
685
1312
|
} else {
|
|
686
|
-
resolve({
|
|
1313
|
+
resolve({
|
|
1314
|
+
signature: data.signature,
|
|
1315
|
+
address: data.address,
|
|
1316
|
+
signedMessage: data.signedMessage,
|
|
1317
|
+
selectedActionKeys: data.selectedActionKeys,
|
|
1318
|
+
permissions: data.permissions
|
|
1319
|
+
});
|
|
687
1320
|
}
|
|
688
1321
|
} else {
|
|
689
1322
|
reject(data.error);
|
|
@@ -692,13 +1325,19 @@ var OpenKey = class {
|
|
|
692
1325
|
}
|
|
693
1326
|
}
|
|
694
1327
|
});
|
|
1328
|
+
if (isVersionedRequest) {
|
|
1329
|
+
modal.setExpectedCorrelation(
|
|
1330
|
+
outgoingRequestId,
|
|
1331
|
+
outgoingProtocolVersion
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
695
1334
|
const readyTimeout = setTimeout(() => {
|
|
696
1335
|
if (readyReceived || settled) return;
|
|
697
1336
|
cleanup();
|
|
698
1337
|
console.warn("OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP.");
|
|
699
1338
|
showToast();
|
|
700
1339
|
const popupUrl = `${this.host}/widget/${action}?origin=${origin}`;
|
|
701
|
-
this.openPopup(popupUrl, message, (val) => settle(() => resolve(val)), (err) => settle(() => reject(err)));
|
|
1340
|
+
this.openPopup(action, popupUrl, message, (val) => settle(() => resolve(val)), (err) => settle(() => reject(err)));
|
|
702
1341
|
}, IFRAME_READY_TIMEOUT);
|
|
703
1342
|
const overallTimeout = setTimeout(() => {
|
|
704
1343
|
settle(() => {
|
|
@@ -706,8 +1345,24 @@ var OpenKey = class {
|
|
|
706
1345
|
reject({ code: "TIMEOUT", message: "Request timed out" });
|
|
707
1346
|
});
|
|
708
1347
|
}, DEFAULT_TIMEOUT);
|
|
1348
|
+
cancel = () => {
|
|
1349
|
+
settle(() => {
|
|
1350
|
+
cleanup();
|
|
1351
|
+
reject({ code: "USER_CANCELLED", message: "Superseded by sign-out" });
|
|
1352
|
+
});
|
|
1353
|
+
};
|
|
1354
|
+
this.activeFlowCancellations.add(cancel);
|
|
709
1355
|
});
|
|
710
1356
|
}
|
|
1357
|
+
isResponseForAction(action, data) {
|
|
1358
|
+
const type = data.type;
|
|
1359
|
+
if (action === "connect") return type === "openkey:auth:response";
|
|
1360
|
+
if (action === "sign") return type === "openkey:sign:response" || type === "openkey:externalSign:approve";
|
|
1361
|
+
if (action === "sign-typed-data") return type === "openkey:signTypedData:response";
|
|
1362
|
+
if (action === "link-wallet") return type === "openkey:link-wallet:response";
|
|
1363
|
+
if (action === "sign-out") return type === "openkey:sign-out:response";
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
711
1366
|
handleWalletLinkDelegation(modal) {
|
|
712
1367
|
const sendResult = (result) => modal.postMessage(result);
|
|
713
1368
|
new WalletPicker({
|
|
@@ -769,15 +1424,16 @@ var OpenKey = class {
|
|
|
769
1424
|
}
|
|
770
1425
|
});
|
|
771
1426
|
}
|
|
772
|
-
openPopup(url, message, resolve, reject) {
|
|
1427
|
+
openPopup(action, url, message, resolve, reject) {
|
|
773
1428
|
const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
|
|
774
1429
|
const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
|
|
775
|
-
|
|
1430
|
+
const popup = window.open(
|
|
776
1431
|
url,
|
|
777
|
-
|
|
1432
|
+
`openkey-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
778
1433
|
`width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
|
|
779
1434
|
);
|
|
780
|
-
|
|
1435
|
+
this.popup = popup;
|
|
1436
|
+
if (!popup) {
|
|
781
1437
|
showToast("Popup was blocked. Please allow popups for this site.", "error");
|
|
782
1438
|
reject({
|
|
783
1439
|
code: "POPUP_BLOCKED",
|
|
@@ -785,74 +1441,129 @@ var OpenKey = class {
|
|
|
785
1441
|
});
|
|
786
1442
|
return;
|
|
787
1443
|
}
|
|
1444
|
+
let settled = false;
|
|
1445
|
+
let cancel;
|
|
788
1446
|
const cleanup = () => {
|
|
789
1447
|
window.removeEventListener("message", handleMessage);
|
|
790
1448
|
clearInterval(pollClosed);
|
|
791
1449
|
clearTimeout(timeout);
|
|
1450
|
+
this.activeFlowCancellations.delete(cancel);
|
|
1451
|
+
if (this.popup === popup) this.popup = null;
|
|
1452
|
+
};
|
|
1453
|
+
const settle = (fn) => {
|
|
1454
|
+
if (settled) return;
|
|
1455
|
+
settled = true;
|
|
1456
|
+
cleanup();
|
|
1457
|
+
fn();
|
|
792
1458
|
};
|
|
793
1459
|
const pollClosed = setInterval(() => {
|
|
794
|
-
if (
|
|
795
|
-
|
|
796
|
-
reject({
|
|
1460
|
+
if (popup.closed) {
|
|
1461
|
+
settle(() => reject({
|
|
797
1462
|
code: "USER_CANCELLED",
|
|
798
1463
|
message: "User closed the popup"
|
|
799
|
-
});
|
|
1464
|
+
}));
|
|
800
1465
|
}
|
|
801
1466
|
}, 500);
|
|
802
1467
|
const timeout = setTimeout(() => {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1468
|
+
settle(() => {
|
|
1469
|
+
popup.close();
|
|
1470
|
+
reject({
|
|
1471
|
+
code: "TIMEOUT",
|
|
1472
|
+
message: "Request timed out"
|
|
1473
|
+
});
|
|
808
1474
|
});
|
|
809
1475
|
}, DEFAULT_TIMEOUT);
|
|
1476
|
+
const outgoingRequestId = message.requestId;
|
|
1477
|
+
const outgoingProtocolVersion = message.protocolVersion;
|
|
1478
|
+
const isVersionedRequest = typeof outgoingRequestId === "string" && typeof outgoingProtocolVersion === "number" && outgoingProtocolVersion >= 1;
|
|
810
1479
|
const handleMessage = (event) => {
|
|
811
1480
|
if (event.origin !== this.host) return;
|
|
812
|
-
if (event.source !==
|
|
1481
|
+
if (event.source !== popup) return;
|
|
813
1482
|
const data = event.data;
|
|
814
1483
|
if (data.type === "openkey:ready") {
|
|
815
|
-
|
|
1484
|
+
popup.postMessage(message, this.host);
|
|
816
1485
|
return;
|
|
817
1486
|
}
|
|
818
1487
|
if (data.type === "openkey:close") {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1488
|
+
if (isVersionedRequest) {
|
|
1489
|
+
const closeRequestId = data.requestId;
|
|
1490
|
+
const closeVersion = data.protocolVersion;
|
|
1491
|
+
if (closeRequestId !== outgoingRequestId) return;
|
|
1492
|
+
if (closeVersion !== outgoingProtocolVersion) return;
|
|
1493
|
+
}
|
|
1494
|
+
settle(() => {
|
|
1495
|
+
popup.close();
|
|
1496
|
+
reject({
|
|
1497
|
+
code: "USER_CANCELLED",
|
|
1498
|
+
message: "User cancelled the request"
|
|
1499
|
+
});
|
|
824
1500
|
});
|
|
825
1501
|
return;
|
|
826
1502
|
}
|
|
827
1503
|
if (data.type === "openkey:auth:use-external-wallet") {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1504
|
+
settle(() => {
|
|
1505
|
+
popup.close();
|
|
1506
|
+
this.handleExternalWalletConnect(resolve, reject);
|
|
1507
|
+
});
|
|
831
1508
|
return;
|
|
832
1509
|
}
|
|
833
|
-
if (
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1510
|
+
if (this.isResponseForAction(action, data)) {
|
|
1511
|
+
if (isVersionedRequest) {
|
|
1512
|
+
const respRequestId = data.requestId;
|
|
1513
|
+
const respVersion = data.protocolVersion;
|
|
1514
|
+
if (respRequestId !== outgoingRequestId) return;
|
|
1515
|
+
if (respVersion !== outgoingProtocolVersion) return;
|
|
1516
|
+
}
|
|
1517
|
+
settle(() => {
|
|
1518
|
+
popup.close();
|
|
1519
|
+
if (data.success) {
|
|
1520
|
+
if (data.type === "openkey:auth:response") {
|
|
1521
|
+
if (data.sessionToken) this.sessionToken = data.sessionToken;
|
|
1522
|
+
resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
|
|
1523
|
+
} else if (data.type === "openkey:link-wallet:response") {
|
|
1524
|
+
resolve({ address: data.address, keyId: data.keyId });
|
|
1525
|
+
} else if (data.type === "openkey:sign-out:response") {
|
|
1526
|
+
resolve({ requestId: data.requestId, revoked: data.revoked });
|
|
1527
|
+
} else if (data.type === "openkey:externalSign:approve") {
|
|
1528
|
+
resolve({
|
|
1529
|
+
authorizationContextToken: data.authorizationContextToken,
|
|
1530
|
+
previewApprovalToken: data.previewApprovalToken,
|
|
1531
|
+
signedMessage: data.signedMessage,
|
|
1532
|
+
selectedActionIds: data.selectedActionIds,
|
|
1533
|
+
address: data.address
|
|
1534
|
+
});
|
|
1535
|
+
} else {
|
|
1536
|
+
resolve({
|
|
1537
|
+
signature: data.signature,
|
|
1538
|
+
address: data.address,
|
|
1539
|
+
signedMessage: data.signedMessage,
|
|
1540
|
+
selectedActionKeys: data.selectedActionKeys,
|
|
1541
|
+
permissions: data.permissions
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
842
1544
|
} else {
|
|
843
|
-
|
|
1545
|
+
reject(data.error);
|
|
844
1546
|
}
|
|
845
|
-
}
|
|
846
|
-
reject(data.error);
|
|
847
|
-
}
|
|
1547
|
+
});
|
|
848
1548
|
}
|
|
849
1549
|
};
|
|
850
1550
|
window.addEventListener("message", handleMessage);
|
|
1551
|
+
cancel = () => {
|
|
1552
|
+
settle(() => {
|
|
1553
|
+
popup.close();
|
|
1554
|
+
reject({ code: "USER_CANCELLED", message: "Superseded by sign-out" });
|
|
1555
|
+
});
|
|
1556
|
+
};
|
|
1557
|
+
this.activeFlowCancellations.add(cancel);
|
|
1558
|
+
}
|
|
1559
|
+
cancelActiveFlows() {
|
|
1560
|
+
for (const cancel of [...this.activeFlowCancellations]) cancel();
|
|
851
1561
|
}
|
|
852
1562
|
/**
|
|
853
1563
|
* Disconnect and close any open flows
|
|
854
1564
|
*/
|
|
855
1565
|
disconnect() {
|
|
1566
|
+
this.cancelActiveFlows();
|
|
856
1567
|
this.popup?.close();
|
|
857
1568
|
this.popup = null;
|
|
858
1569
|
}
|
|
@@ -924,9 +1635,14 @@ function getOpenKey(config) {
|
|
|
924
1635
|
return defaultInstance;
|
|
925
1636
|
}
|
|
926
1637
|
export {
|
|
1638
|
+
KeyIdTypeMismatchError,
|
|
927
1639
|
OpenKey,
|
|
1640
|
+
OpenKeyNostr,
|
|
928
1641
|
OpenKeyProvider,
|
|
929
1642
|
index_default as default,
|
|
930
|
-
getOpenKey
|
|
1643
|
+
getOpenKey,
|
|
1644
|
+
resolveAuthorizeTinyCloudRouting,
|
|
1645
|
+
shouldRouteAuthorizeTinyCloudExternal,
|
|
1646
|
+
validateIframeResize
|
|
931
1647
|
};
|
|
932
1648
|
//# sourceMappingURL=index.mjs.map
|