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