@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.js
CHANGED
|
@@ -20,19 +20,336 @@ 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) {
|
|
277
|
+
if (data.requestId !== void 0) return null;
|
|
278
|
+
} else {
|
|
279
|
+
if (expected.requestId === null || expected.protocolVersion === null) return null;
|
|
280
|
+
if (typeof data.protocolVersion !== "number" || data.protocolVersion !== expected.protocolVersion) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
if (typeof data.requestId !== "string" || data.requestId !== expected.requestId) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (typeof data.height !== "number" || !Number.isFinite(data.height) || data.height <= 0) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
return Math.min(data.height, Math.floor(expected.viewportHeight * 0.85));
|
|
291
|
+
}
|
|
292
|
+
function shouldRouteAuthorizeTinyCloudExternal(state) {
|
|
293
|
+
return state.lastAuth?.keyType === "EXTERNAL";
|
|
294
|
+
}
|
|
295
|
+
var KeyIdTypeMismatchError = class extends Error {
|
|
296
|
+
code;
|
|
297
|
+
requestKeyId;
|
|
298
|
+
lastAuthKeyId;
|
|
299
|
+
requestKeyType;
|
|
300
|
+
lastAuthKeyType;
|
|
301
|
+
constructor(fields) {
|
|
302
|
+
super(fields.message);
|
|
303
|
+
this.name = "KeyIdTypeMismatchError";
|
|
304
|
+
this.code = "KEY_ID_TYPE_MISMATCH";
|
|
305
|
+
this.requestKeyId = fields.requestKeyId;
|
|
306
|
+
this.lastAuthKeyId = fields.lastAuthKeyId;
|
|
307
|
+
this.requestKeyType = fields.requestKeyType;
|
|
308
|
+
this.lastAuthKeyType = fields.lastAuthKeyType;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
function resolveAuthorizeTinyCloudRouting(state) {
|
|
312
|
+
const lastAuthKeyId = state.lastAuth?.keyId ?? null;
|
|
313
|
+
const lastAuthKeyType = state.lastAuth?.keyType ?? null;
|
|
314
|
+
const rawRequestKeyId = state.requestKeyId ?? null;
|
|
315
|
+
const requestKeyId = rawRequestKeyId && rawRequestKeyId.length > 0 ? rawRequestKeyId : null;
|
|
316
|
+
const knownKeys = state.knownKeys ?? [];
|
|
317
|
+
const lookupType = (id) => {
|
|
318
|
+
for (const k2 of knownKeys) {
|
|
319
|
+
if (k2.id === id) return k2.keyType;
|
|
320
|
+
if (k2.address && k2.address.toLowerCase() === id.toLowerCase()) return k2.keyType;
|
|
321
|
+
}
|
|
322
|
+
if (id.startsWith("external:")) return "EXTERNAL";
|
|
323
|
+
return null;
|
|
324
|
+
};
|
|
325
|
+
if (!requestKeyId) {
|
|
326
|
+
const route = lastAuthKeyType === "EXTERNAL" ? "external" : "managed";
|
|
327
|
+
return {
|
|
328
|
+
route,
|
|
329
|
+
resolvedKeyId: lastAuthKeyId,
|
|
330
|
+
resolvedKeyType: lastAuthKeyType
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
let requestKeyType = lookupType(requestKeyId);
|
|
334
|
+
if (!requestKeyType && lastAuthKeyId && requestKeyId === lastAuthKeyId) {
|
|
335
|
+
requestKeyType = lastAuthKeyType;
|
|
336
|
+
}
|
|
337
|
+
if (requestKeyType && lastAuthKeyType && requestKeyType !== lastAuthKeyType) {
|
|
338
|
+
throw new KeyIdTypeMismatchError({
|
|
339
|
+
message: `authorizeTinyCloud: request.keyId ${requestKeyId} resolves to ${requestKeyType} but active session key ${lastAuthKeyId} is ${lastAuthKeyType}; refuse to route silently`,
|
|
340
|
+
requestKeyId,
|
|
341
|
+
lastAuthKeyId,
|
|
342
|
+
requestKeyType,
|
|
343
|
+
lastAuthKeyType
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const effectiveType = requestKeyType ?? lastAuthKeyType;
|
|
347
|
+
return {
|
|
348
|
+
route: effectiveType === "EXTERNAL" ? "external" : "managed",
|
|
349
|
+
resolvedKeyId: requestKeyId,
|
|
350
|
+
resolvedKeyType: effectiveType
|
|
351
|
+
};
|
|
352
|
+
}
|
|
36
353
|
var IframeModal = class {
|
|
37
354
|
root;
|
|
38
355
|
shadow;
|
|
@@ -41,6 +358,13 @@ var IframeModal = class {
|
|
|
41
358
|
onMessage;
|
|
42
359
|
messageHandler;
|
|
43
360
|
host;
|
|
361
|
+
// Sol final continuation contract requirement 5: when the modal carries a
|
|
362
|
+
// versioned request, iframe resize traffic MUST correlate to its
|
|
363
|
+
// requestId AND protocolVersion. Set by `setExpectedCorrelation` as soon
|
|
364
|
+
// as the modal is created. When both are null the flow is unversioned and
|
|
365
|
+
// only uncorrelated resizes from this iframe are accepted (TC-647).
|
|
366
|
+
expectedRequestId = null;
|
|
367
|
+
expectedProtocolVersion = null;
|
|
44
368
|
constructor(opts) {
|
|
45
369
|
this.host = opts.host;
|
|
46
370
|
this.onClose = opts.onClose;
|
|
@@ -96,10 +420,17 @@ var IframeModal = class {
|
|
|
96
420
|
if (event.source !== this.iframe.contentWindow) return;
|
|
97
421
|
const data = event.data;
|
|
98
422
|
if (data.type === "openkey:resize") {
|
|
99
|
-
const h =
|
|
423
|
+
const h = validateIframeResize(event.data, {
|
|
424
|
+
requestId: this.expectedRequestId,
|
|
425
|
+
protocolVersion: this.expectedProtocolVersion,
|
|
426
|
+
viewportHeight: window.innerHeight
|
|
427
|
+
});
|
|
428
|
+
if (h === null) return;
|
|
100
429
|
this.iframe.style.height = `${h}px`;
|
|
101
430
|
return;
|
|
102
431
|
}
|
|
432
|
+
if (data.type === "openkey:close") {
|
|
433
|
+
}
|
|
103
434
|
this.onMessage(data);
|
|
104
435
|
};
|
|
105
436
|
window.addEventListener("message", this.messageHandler);
|
|
@@ -108,6 +439,21 @@ var IframeModal = class {
|
|
|
108
439
|
postMessage(message) {
|
|
109
440
|
this.iframe.contentWindow?.postMessage(message, this.host);
|
|
110
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Sol final continuation contract requirement 5: bind the active
|
|
444
|
+
* request correlation so subsequent resize (and future correlated)
|
|
445
|
+
* messages can be verified against `requestId` + `protocolVersion`.
|
|
446
|
+
*
|
|
447
|
+
* The outer flow calls this right after creating the modal for a
|
|
448
|
+
* versioned request, before the iframe can send anything, so no
|
|
449
|
+
* uncorrelated resize is ever accepted for that request. Passing `null`
|
|
450
|
+
* for both arguments means the flow is unversioned; passing `null` for
|
|
451
|
+
* only one drops every resize.
|
|
452
|
+
*/
|
|
453
|
+
setExpectedCorrelation(requestId, protocolVersion) {
|
|
454
|
+
this.expectedRequestId = requestId;
|
|
455
|
+
this.expectedProtocolVersion = protocolVersion;
|
|
456
|
+
}
|
|
111
457
|
destroy() {
|
|
112
458
|
window.removeEventListener("message", this.messageHandler);
|
|
113
459
|
this.root.remove();
|
|
@@ -241,12 +587,23 @@ var OpenKey = class {
|
|
|
241
587
|
lastAuth = null;
|
|
242
588
|
discoveredProviders = [];
|
|
243
589
|
sessionToken = null;
|
|
590
|
+
/** Cancels widget flows that could outlive a sign-out on this instance. */
|
|
591
|
+
activeFlowCancellations = /* @__PURE__ */ new Set();
|
|
592
|
+
/**
|
|
593
|
+
* Nostr identity custody + signing (secp256k1 Schnorr / BIP-340). Kept
|
|
594
|
+
* fully separate from the Ethereum flows above: it never reuses
|
|
595
|
+
* `lastAuth`/`sessionToken`, and its widget messages never carry a
|
|
596
|
+
* sessionToken - the OpenKey session and any signing grants stay inside
|
|
597
|
+
* the OpenKey-origin iframe for the whole lifetime of this client.
|
|
598
|
+
*/
|
|
599
|
+
nostr;
|
|
244
600
|
constructor(config = {}) {
|
|
245
601
|
this.config = config;
|
|
246
602
|
this.host = config.host || DEFAULT_HOST;
|
|
247
603
|
this.oauthHost = config.oauthHost || this.deriveOAuthHost(this.host);
|
|
248
604
|
this.appName = config.appName || window.location.hostname;
|
|
249
605
|
this.mode = config.mode ?? "iframe";
|
|
606
|
+
this.nostr = new OpenKeyNostr(this.host);
|
|
250
607
|
if (typeof window !== "undefined") {
|
|
251
608
|
this.discoveredProviders = [];
|
|
252
609
|
window.addEventListener("eip6963:announceProvider", (event) => {
|
|
@@ -272,6 +629,37 @@ var OpenKey = class {
|
|
|
272
629
|
return "https://api.openkey.so";
|
|
273
630
|
}
|
|
274
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Sol MAJOR-4 (final continuation): fetch the caller's registered keys
|
|
634
|
+
* so `resolveAuthorizeTinyCloudRouting` can resolve an explicit pin
|
|
635
|
+
* whose keyId does not match the active session key. Called only when
|
|
636
|
+
* the caller passes a `requestKeyId` we do not already recognise; the
|
|
637
|
+
* common no-pin path skips the network round-trip.
|
|
638
|
+
*
|
|
639
|
+
* Failures do NOT block routing — we fall through with `knownKeys=[]`
|
|
640
|
+
* so the resolver's lastAuth-based fallback still runs. If the caller
|
|
641
|
+
* pinned a genuinely unknown key that turns out to be external, the
|
|
642
|
+
* widget rejects the server-signing path and the SDK surfaces that
|
|
643
|
+
* error from the finalize call rather than silently routing wrong.
|
|
644
|
+
*/
|
|
645
|
+
async fetchKnownKeysIfNeeded(requestKeyId) {
|
|
646
|
+
if (!requestKeyId) return [];
|
|
647
|
+
const la = this.lastAuth;
|
|
648
|
+
if (la?.keyId === requestKeyId) return [];
|
|
649
|
+
if (la?.address && la.address.toLowerCase() === requestKeyId.toLowerCase()) return [];
|
|
650
|
+
if (requestKeyId.startsWith("external:")) return [];
|
|
651
|
+
try {
|
|
652
|
+
const res = await fetch(`${this.oauthHost}/api/keys`, {
|
|
653
|
+
method: "GET",
|
|
654
|
+
credentials: "include"
|
|
655
|
+
});
|
|
656
|
+
if (!res.ok) return [];
|
|
657
|
+
const body = await res.json();
|
|
658
|
+
return Array.isArray(body?.keys) ? body.keys : [];
|
|
659
|
+
} catch {
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
275
663
|
/**
|
|
276
664
|
* Connect to OpenKey and get user's wallet address
|
|
277
665
|
* Opens auth flow for user to select/create a key
|
|
@@ -284,6 +672,36 @@ var OpenKey = class {
|
|
|
284
672
|
this.lastAuth = result;
|
|
285
673
|
return result;
|
|
286
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Signs the active OpenKey account out through the OpenKey-owned widget.
|
|
677
|
+
*
|
|
678
|
+
* The SDK passes its Better Auth bearer through the OpenKey-owned widget
|
|
679
|
+
* protocol so applications do not have to implement revocation themselves.
|
|
680
|
+
* Local SDK session state is cleared before opening the widget, including
|
|
681
|
+
* when remote revocation is unavailable.
|
|
682
|
+
*/
|
|
683
|
+
async signOut(opts) {
|
|
684
|
+
const requestId = `ok-signout-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
685
|
+
const sessionToken = this.sessionToken || void 0;
|
|
686
|
+
const mode = this.resolveMode(opts?.mode) === "redirect" ? "popup" : opts?.mode;
|
|
687
|
+
this.cancelActiveFlows();
|
|
688
|
+
this.lastAuth = null;
|
|
689
|
+
this.sessionToken = null;
|
|
690
|
+
const acknowledgement = await this.openFlow(
|
|
691
|
+
"sign-out",
|
|
692
|
+
{
|
|
693
|
+
type: "openkey:sign-out:request",
|
|
694
|
+
requestId,
|
|
695
|
+
protocolVersion: 1,
|
|
696
|
+
sessionToken
|
|
697
|
+
},
|
|
698
|
+
mode
|
|
699
|
+
);
|
|
700
|
+
if (acknowledgement.requestId !== requestId) {
|
|
701
|
+
throw new Error("OpenKey signOut received an acknowledgement for a different request");
|
|
702
|
+
}
|
|
703
|
+
return acknowledgement;
|
|
704
|
+
}
|
|
287
705
|
/**
|
|
288
706
|
* Better-auth session token relayed during connect(), or null if the
|
|
289
707
|
* flow did not provide one. Use as a bearer token for API calls made
|
|
@@ -322,6 +740,208 @@ var OpenKey = class {
|
|
|
322
740
|
}
|
|
323
741
|
return this.signWithOpenKey(request, opts?.mode);
|
|
324
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Versioned TinyCloud authorization protocol (v1).
|
|
745
|
+
*
|
|
746
|
+
* Unlike `signMessage()` (which is contractually byte-exact — the returned
|
|
747
|
+
* signature always verifies against the caller's exact bytes), this method
|
|
748
|
+
* permits the user to narrow the requested capabilities before signing.
|
|
749
|
+
* OpenKey may therefore regenerate the SIWE. The response carries
|
|
750
|
+
* `signedMessage` — the exact bytes the signature actually verifies against.
|
|
751
|
+
*
|
|
752
|
+
* TinyCloud completes the session with `signedMessage`, not the original
|
|
753
|
+
* `siwe` passed in. Consumers MUST use the returned bytes and MUST NOT
|
|
754
|
+
* assume the returned grants are a superset of what they requested — the
|
|
755
|
+
* user is allowed to remove any grant that is not required.
|
|
756
|
+
*
|
|
757
|
+
* Legacy `signMessage()` callers are unaffected: their exact-byte contract
|
|
758
|
+
* remains preserved and OpenKey never rewrites their bytes.
|
|
759
|
+
*/
|
|
760
|
+
async authorizeTinyCloud(request, opts) {
|
|
761
|
+
if (request.protocolVersion !== 1) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`authorizeTinyCloud only supports protocolVersion 1; got ${request.protocolVersion}`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (typeof request.siwe !== "string" || !request.siwe) {
|
|
767
|
+
throw new Error("authorizeTinyCloud requires a non-empty SIWE");
|
|
768
|
+
}
|
|
769
|
+
let routing;
|
|
770
|
+
try {
|
|
771
|
+
const knownKeys = await this.fetchKnownKeysIfNeeded(request.keyId);
|
|
772
|
+
routing = resolveAuthorizeTinyCloudRouting({
|
|
773
|
+
lastAuth: this.lastAuth,
|
|
774
|
+
requestKeyId: request.keyId,
|
|
775
|
+
knownKeys
|
|
776
|
+
});
|
|
777
|
+
} catch (e) {
|
|
778
|
+
if (e instanceof KeyIdTypeMismatchError) throw e;
|
|
779
|
+
throw e instanceof Error ? e : new Error("authorizeTinyCloud: failed to resolve target key type");
|
|
780
|
+
}
|
|
781
|
+
if (routing.route === "external") {
|
|
782
|
+
return this.authorizeTinyCloudExternal(request, opts);
|
|
783
|
+
}
|
|
784
|
+
const requestId = `ok-auth-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
785
|
+
const resolvedKeyId = routing.resolvedKeyId ?? request.keyId ?? this.lastAuth?.keyId;
|
|
786
|
+
const raw = await this.openFlow(
|
|
787
|
+
"sign",
|
|
788
|
+
{
|
|
789
|
+
type: "openkey:sign:request",
|
|
790
|
+
requestId,
|
|
791
|
+
protocolVersion: 1,
|
|
792
|
+
message: request.siwe,
|
|
793
|
+
keyId: resolvedKeyId,
|
|
794
|
+
// Forward JWK so the widget can pass it to /authorize-sign for
|
|
795
|
+
// narrowed-SIWE regeneration.
|
|
796
|
+
jwk: request.jwk,
|
|
797
|
+
// Forward the TinyCloud host so the widget can bind it into the
|
|
798
|
+
// /authorize-sign-prepare context (Sol MAJOR-2).
|
|
799
|
+
host: request.host,
|
|
800
|
+
// Sol MAJOR-2 (envelope): forward the caller-supplied presentation
|
|
801
|
+
// envelope so the widget can render honest provenance and the
|
|
802
|
+
// /authorize-sign-prepare route can bind it into the authorization
|
|
803
|
+
// context. The envelope is display-only; the widget transport
|
|
804
|
+
// validates and size-bounds it before use and NEVER treats it as
|
|
805
|
+
// verified unless the server actually origin-binds a manifest.
|
|
806
|
+
presentation: request.presentation,
|
|
807
|
+
sessionToken: this.sessionToken || void 0
|
|
808
|
+
},
|
|
809
|
+
opts?.mode
|
|
810
|
+
);
|
|
811
|
+
if (typeof raw.signedMessage !== "string" || !raw.signedMessage) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
"authorizeTinyCloud: widget returned no signedMessage \u2014 signature would not correspond to the displayed authorization"
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
if (!Array.isArray(raw.selectedActionKeys)) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
"authorizeTinyCloud: widget returned no selectedActionKeys \u2014 cannot verify displayed authorization matches the signed bytes"
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
if (!Array.isArray(raw.permissions)) {
|
|
822
|
+
throw new Error(
|
|
823
|
+
"authorizeTinyCloud: widget returned no permissions \u2014 cannot verify displayed authorization matches the signed bytes"
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
protocolVersion: 1,
|
|
828
|
+
address: raw.address,
|
|
829
|
+
signature: raw.signature,
|
|
830
|
+
signedMessage: raw.signedMessage,
|
|
831
|
+
selectedActionKeys: raw.selectedActionKeys,
|
|
832
|
+
permissions: raw.permissions
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Sol MAJOR-3 (continuation): authorizeTinyCloud path for external keys.
|
|
837
|
+
* The wallet lives outside OpenKey, so the SDK opens the shared widget
|
|
838
|
+
* for review + preview, then invokes the user's wallet to sign the
|
|
839
|
+
* preview bytes, then finalizes server-side via /authorize-sign.
|
|
840
|
+
*
|
|
841
|
+
* Flow:
|
|
842
|
+
* 1. Open the widget with `externalSign: true` — the widget renders
|
|
843
|
+
* the shared SigningApproval UI, calls /authorize-sign-prepare,
|
|
844
|
+
* calls /authorize-sign-preview after the user approves a
|
|
845
|
+
* selection, and hands back `{ previewApprovalToken,
|
|
846
|
+
* signedMessage, selectedActionIds, address, authorizationContextToken }`.
|
|
847
|
+
* 2. SDK invokes the user's wallet on the returned signedMessage.
|
|
848
|
+
* 3. SDK POSTs /authorize-sign with `externalSignature`.
|
|
849
|
+
*
|
|
850
|
+
* Legacy exact-byte signMessage callers are unaffected — this path is
|
|
851
|
+
* only taken via authorizeTinyCloud (protocolVersion:1) when the target
|
|
852
|
+
* key is EXTERNAL. Sol's rejection required this path to go through
|
|
853
|
+
* the same review UI as managed keys.
|
|
854
|
+
*/
|
|
855
|
+
async authorizeTinyCloudExternal(request, opts) {
|
|
856
|
+
const keyId = request.keyId ?? this.lastAuth?.keyId;
|
|
857
|
+
if (!keyId) {
|
|
858
|
+
throw new Error(
|
|
859
|
+
"authorizeTinyCloud (external) requires a keyId \u2014 call connect() first or pass request.keyId"
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
const walletAddress = this.lastAuth?.address;
|
|
863
|
+
if (!walletAddress) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
"authorizeTinyCloud (external) requires a connected wallet \u2014 call connect() first"
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
const requestId = `ok-ext-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
869
|
+
const approval = await this.openFlow(
|
|
870
|
+
"sign",
|
|
871
|
+
{
|
|
872
|
+
type: "openkey:sign:request",
|
|
873
|
+
requestId,
|
|
874
|
+
protocolVersion: 1,
|
|
875
|
+
message: request.siwe,
|
|
876
|
+
keyId,
|
|
877
|
+
jwk: request.jwk,
|
|
878
|
+
host: request.host,
|
|
879
|
+
// Sol MAJOR-2 (envelope): forward the caller-supplied presentation
|
|
880
|
+
// envelope. Same rules as the managed path — display-only, size-
|
|
881
|
+
// bounded at the transport, never treated as verified without
|
|
882
|
+
// server-side origin-binding.
|
|
883
|
+
presentation: request.presentation,
|
|
884
|
+
sessionToken: this.sessionToken || void 0,
|
|
885
|
+
// Sol MAJOR-3 (continuation): tell the widget to hand us back
|
|
886
|
+
// the previewApproval instead of asking OpenKey to sign
|
|
887
|
+
// server-side with a managed key. The widget will still render
|
|
888
|
+
// the SAME SigningApproval component; it just diverges at the
|
|
889
|
+
// "sign" step and emits `openkey:externalSign:approve` back
|
|
890
|
+
// through the transport.
|
|
891
|
+
externalSign: true
|
|
892
|
+
},
|
|
893
|
+
opts?.mode
|
|
894
|
+
);
|
|
895
|
+
if (!approval?.previewApprovalToken || !approval?.signedMessage) {
|
|
896
|
+
throw new Error(
|
|
897
|
+
"authorizeTinyCloud (external): widget did not return a previewApprovalToken + signedMessage"
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
if (approval.address && approval.address.toLowerCase() !== walletAddress.toLowerCase()) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
`authorizeTinyCloud (external): widget preview address ${approval.address} does not match connected wallet ${walletAddress}`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
const provider = await this.findWalletProvider(walletAddress);
|
|
906
|
+
const hexMessage = this.toHex(approval.signedMessage);
|
|
907
|
+
const walletSignature = await provider.request({
|
|
908
|
+
method: "personal_sign",
|
|
909
|
+
params: [hexMessage, walletAddress]
|
|
910
|
+
});
|
|
911
|
+
const finalizeRes = await fetch(
|
|
912
|
+
`${this.oauthHost}/api/delegate/authorize-sign`,
|
|
913
|
+
{
|
|
914
|
+
method: "POST",
|
|
915
|
+
credentials: "include",
|
|
916
|
+
headers: { "Content-Type": "application/json" },
|
|
917
|
+
body: JSON.stringify({
|
|
918
|
+
authorizationContextToken: approval.authorizationContextToken,
|
|
919
|
+
previewApprovalToken: approval.previewApprovalToken,
|
|
920
|
+
selectedActionIds: approval.selectedActionIds,
|
|
921
|
+
protocolVersion: 1,
|
|
922
|
+
externalSignature: walletSignature
|
|
923
|
+
})
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
if (!finalizeRes.ok) {
|
|
927
|
+
const body = await finalizeRes.json().catch(() => ({ error: `HTTP ${finalizeRes.status}` }));
|
|
928
|
+
throw new Error(body.error || `authorize-sign failed (${finalizeRes.status})`);
|
|
929
|
+
}
|
|
930
|
+
const finalize = await finalizeRes.json();
|
|
931
|
+
if (finalize.signedMessage !== approval.signedMessage) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
"authorizeTinyCloud (external): server signed different bytes than the preview showed"
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
protocolVersion: 1,
|
|
938
|
+
address: finalize.address,
|
|
939
|
+
signature: finalize.signature,
|
|
940
|
+
signedMessage: finalize.signedMessage,
|
|
941
|
+
selectedActionKeys: finalize.selectedActionKeys ?? [],
|
|
942
|
+
permissions: finalize.permissions ?? []
|
|
943
|
+
};
|
|
944
|
+
}
|
|
325
945
|
/**
|
|
326
946
|
* Sign typed data (EIP-712) with the user's OpenKey wallet
|
|
327
947
|
* For external keys, routes directly to the user's wallet provider
|
|
@@ -456,14 +1076,14 @@ var OpenKey = class {
|
|
|
456
1076
|
* @returns Promise that resolves with authorization code after user consent
|
|
457
1077
|
*/
|
|
458
1078
|
connect: async (config) => {
|
|
459
|
-
const verifier = (
|
|
460
|
-
const challenge = await (
|
|
461
|
-
const state = config.state || (
|
|
1079
|
+
const verifier = y();
|
|
1080
|
+
const challenge = await k(verifier);
|
|
1081
|
+
const state = config.state || m();
|
|
462
1082
|
sessionStorage.setItem(
|
|
463
1083
|
OAUTH_STORAGE_KEY,
|
|
464
1084
|
JSON.stringify({ verifier, state })
|
|
465
1085
|
);
|
|
466
|
-
const authUrl = (
|
|
1086
|
+
const authUrl = T({
|
|
467
1087
|
host: this.oauthHost,
|
|
468
1088
|
clientId: config.clientId,
|
|
469
1089
|
redirectUri: config.redirectUri,
|
|
@@ -635,7 +1255,7 @@ var OpenKey = class {
|
|
|
635
1255
|
const eoaFlag = action === "connect" && this.hasDetectedEoa() ? "&hasEoa=true" : "";
|
|
636
1256
|
if (mode === "popup") {
|
|
637
1257
|
const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
|
|
638
|
-
return new Promise((resolve, reject) => this.openPopup(url2, message, resolve, reject));
|
|
1258
|
+
return new Promise((resolve, reject) => this.openPopup(action, url2, message, resolve, reject));
|
|
639
1259
|
}
|
|
640
1260
|
if (mode === "redirect") {
|
|
641
1261
|
const url2 = `${this.host}/widget/${action}?origin=${origin}${eoaFlag}`;
|
|
@@ -647,10 +1267,14 @@ var OpenKey = class {
|
|
|
647
1267
|
return this.openIframeModal(url, action, message, origin);
|
|
648
1268
|
}
|
|
649
1269
|
openIframeModal(url, action, message, origin) {
|
|
1270
|
+
const outgoingRequestId = message.requestId;
|
|
1271
|
+
const outgoingProtocolVersion = message.protocolVersion;
|
|
1272
|
+
const isVersionedRequest = typeof outgoingRequestId === "string" && typeof outgoingProtocolVersion === "number" && outgoingProtocolVersion >= 1;
|
|
650
1273
|
return new Promise((resolve, reject) => {
|
|
651
1274
|
let readyReceived = false;
|
|
652
1275
|
let settled = false;
|
|
653
1276
|
let modal = null;
|
|
1277
|
+
let cancel;
|
|
654
1278
|
const settle = (fn) => {
|
|
655
1279
|
if (settled) return;
|
|
656
1280
|
settled = true;
|
|
@@ -661,15 +1285,14 @@ var OpenKey = class {
|
|
|
661
1285
|
clearTimeout(overallTimeout);
|
|
662
1286
|
modal?.destroy();
|
|
663
1287
|
modal = null;
|
|
1288
|
+
this.activeFlowCancellations.delete(cancel);
|
|
664
1289
|
};
|
|
665
1290
|
modal = new IframeModal({
|
|
666
1291
|
url,
|
|
667
1292
|
host: this.host,
|
|
668
1293
|
onClose: () => {
|
|
669
1294
|
settle(() => {
|
|
670
|
-
|
|
671
|
-
clearTimeout(overallTimeout);
|
|
672
|
-
modal = null;
|
|
1295
|
+
cleanup();
|
|
673
1296
|
reject({ code: "USER_CANCELLED", message: "User cancelled the request" });
|
|
674
1297
|
});
|
|
675
1298
|
},
|
|
@@ -679,6 +1302,12 @@ var OpenKey = class {
|
|
|
679
1302
|
modal?.postMessage(message);
|
|
680
1303
|
return;
|
|
681
1304
|
}
|
|
1305
|
+
if (isVersionedRequest) {
|
|
1306
|
+
const respRequestId = data.requestId;
|
|
1307
|
+
const respVersion = data.protocolVersion;
|
|
1308
|
+
if (respRequestId !== outgoingRequestId) return;
|
|
1309
|
+
if (respVersion !== outgoingProtocolVersion) return;
|
|
1310
|
+
}
|
|
682
1311
|
if (data.type === "openkey:close") {
|
|
683
1312
|
settle(() => {
|
|
684
1313
|
cleanup();
|
|
@@ -695,7 +1324,7 @@ var OpenKey = class {
|
|
|
695
1324
|
this.handleExternalWalletConnect(resolve, reject);
|
|
696
1325
|
return;
|
|
697
1326
|
}
|
|
698
|
-
if (
|
|
1327
|
+
if (this.isResponseForAction(action, data)) {
|
|
699
1328
|
settle(() => {
|
|
700
1329
|
cleanup();
|
|
701
1330
|
if (data.success) {
|
|
@@ -704,8 +1333,24 @@ var OpenKey = class {
|
|
|
704
1333
|
resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
|
|
705
1334
|
} else if (data.type === "openkey:link-wallet:response") {
|
|
706
1335
|
resolve({ address: data.address, keyId: data.keyId });
|
|
1336
|
+
} else if (data.type === "openkey:sign-out:response") {
|
|
1337
|
+
resolve({ requestId: data.requestId, revoked: data.revoked });
|
|
1338
|
+
} else if (data.type === "openkey:externalSign:approve") {
|
|
1339
|
+
resolve({
|
|
1340
|
+
authorizationContextToken: data.authorizationContextToken,
|
|
1341
|
+
previewApprovalToken: data.previewApprovalToken,
|
|
1342
|
+
signedMessage: data.signedMessage,
|
|
1343
|
+
selectedActionIds: data.selectedActionIds,
|
|
1344
|
+
address: data.address
|
|
1345
|
+
});
|
|
707
1346
|
} else {
|
|
708
|
-
resolve({
|
|
1347
|
+
resolve({
|
|
1348
|
+
signature: data.signature,
|
|
1349
|
+
address: data.address,
|
|
1350
|
+
signedMessage: data.signedMessage,
|
|
1351
|
+
selectedActionKeys: data.selectedActionKeys,
|
|
1352
|
+
permissions: data.permissions
|
|
1353
|
+
});
|
|
709
1354
|
}
|
|
710
1355
|
} else {
|
|
711
1356
|
reject(data.error);
|
|
@@ -714,13 +1359,19 @@ var OpenKey = class {
|
|
|
714
1359
|
}
|
|
715
1360
|
}
|
|
716
1361
|
});
|
|
1362
|
+
if (isVersionedRequest) {
|
|
1363
|
+
modal.setExpectedCorrelation(
|
|
1364
|
+
outgoingRequestId,
|
|
1365
|
+
outgoingProtocolVersion
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
717
1368
|
const readyTimeout = setTimeout(() => {
|
|
718
1369
|
if (readyReceived || settled) return;
|
|
719
1370
|
cleanup();
|
|
720
1371
|
console.warn("OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP.");
|
|
721
1372
|
showToast();
|
|
722
1373
|
const popupUrl = `${this.host}/widget/${action}?origin=${origin}`;
|
|
723
|
-
this.openPopup(popupUrl, message, (val) => settle(() => resolve(val)), (err) => settle(() => reject(err)));
|
|
1374
|
+
this.openPopup(action, popupUrl, message, (val) => settle(() => resolve(val)), (err) => settle(() => reject(err)));
|
|
724
1375
|
}, IFRAME_READY_TIMEOUT);
|
|
725
1376
|
const overallTimeout = setTimeout(() => {
|
|
726
1377
|
settle(() => {
|
|
@@ -728,8 +1379,24 @@ var OpenKey = class {
|
|
|
728
1379
|
reject({ code: "TIMEOUT", message: "Request timed out" });
|
|
729
1380
|
});
|
|
730
1381
|
}, DEFAULT_TIMEOUT);
|
|
1382
|
+
cancel = () => {
|
|
1383
|
+
settle(() => {
|
|
1384
|
+
cleanup();
|
|
1385
|
+
reject({ code: "USER_CANCELLED", message: "Superseded by sign-out" });
|
|
1386
|
+
});
|
|
1387
|
+
};
|
|
1388
|
+
this.activeFlowCancellations.add(cancel);
|
|
731
1389
|
});
|
|
732
1390
|
}
|
|
1391
|
+
isResponseForAction(action, data) {
|
|
1392
|
+
const type = data.type;
|
|
1393
|
+
if (action === "connect") return type === "openkey:auth:response";
|
|
1394
|
+
if (action === "sign") return type === "openkey:sign:response" || type === "openkey:externalSign:approve";
|
|
1395
|
+
if (action === "sign-typed-data") return type === "openkey:signTypedData:response";
|
|
1396
|
+
if (action === "link-wallet") return type === "openkey:link-wallet:response";
|
|
1397
|
+
if (action === "sign-out") return type === "openkey:sign-out:response";
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
733
1400
|
handleWalletLinkDelegation(modal) {
|
|
734
1401
|
const sendResult = (result) => modal.postMessage(result);
|
|
735
1402
|
new WalletPicker({
|
|
@@ -791,15 +1458,16 @@ var OpenKey = class {
|
|
|
791
1458
|
}
|
|
792
1459
|
});
|
|
793
1460
|
}
|
|
794
|
-
openPopup(url, message, resolve, reject) {
|
|
1461
|
+
openPopup(action, url, message, resolve, reject) {
|
|
795
1462
|
const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
|
|
796
1463
|
const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
|
|
797
|
-
|
|
1464
|
+
const popup = window.open(
|
|
798
1465
|
url,
|
|
799
|
-
|
|
1466
|
+
`openkey-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
800
1467
|
`width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
|
|
801
1468
|
);
|
|
802
|
-
|
|
1469
|
+
this.popup = popup;
|
|
1470
|
+
if (!popup) {
|
|
803
1471
|
showToast("Popup was blocked. Please allow popups for this site.", "error");
|
|
804
1472
|
reject({
|
|
805
1473
|
code: "POPUP_BLOCKED",
|
|
@@ -807,74 +1475,129 @@ var OpenKey = class {
|
|
|
807
1475
|
});
|
|
808
1476
|
return;
|
|
809
1477
|
}
|
|
1478
|
+
let settled = false;
|
|
1479
|
+
let cancel;
|
|
810
1480
|
const cleanup = () => {
|
|
811
1481
|
window.removeEventListener("message", handleMessage);
|
|
812
1482
|
clearInterval(pollClosed);
|
|
813
1483
|
clearTimeout(timeout);
|
|
1484
|
+
this.activeFlowCancellations.delete(cancel);
|
|
1485
|
+
if (this.popup === popup) this.popup = null;
|
|
1486
|
+
};
|
|
1487
|
+
const settle = (fn) => {
|
|
1488
|
+
if (settled) return;
|
|
1489
|
+
settled = true;
|
|
1490
|
+
cleanup();
|
|
1491
|
+
fn();
|
|
814
1492
|
};
|
|
815
1493
|
const pollClosed = setInterval(() => {
|
|
816
|
-
if (
|
|
817
|
-
|
|
818
|
-
reject({
|
|
1494
|
+
if (popup.closed) {
|
|
1495
|
+
settle(() => reject({
|
|
819
1496
|
code: "USER_CANCELLED",
|
|
820
1497
|
message: "User closed the popup"
|
|
821
|
-
});
|
|
1498
|
+
}));
|
|
822
1499
|
}
|
|
823
1500
|
}, 500);
|
|
824
1501
|
const timeout = setTimeout(() => {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1502
|
+
settle(() => {
|
|
1503
|
+
popup.close();
|
|
1504
|
+
reject({
|
|
1505
|
+
code: "TIMEOUT",
|
|
1506
|
+
message: "Request timed out"
|
|
1507
|
+
});
|
|
830
1508
|
});
|
|
831
1509
|
}, DEFAULT_TIMEOUT);
|
|
1510
|
+
const outgoingRequestId = message.requestId;
|
|
1511
|
+
const outgoingProtocolVersion = message.protocolVersion;
|
|
1512
|
+
const isVersionedRequest = typeof outgoingRequestId === "string" && typeof outgoingProtocolVersion === "number" && outgoingProtocolVersion >= 1;
|
|
832
1513
|
const handleMessage = (event) => {
|
|
833
1514
|
if (event.origin !== this.host) return;
|
|
834
|
-
if (event.source !==
|
|
1515
|
+
if (event.source !== popup) return;
|
|
835
1516
|
const data = event.data;
|
|
836
1517
|
if (data.type === "openkey:ready") {
|
|
837
|
-
|
|
1518
|
+
popup.postMessage(message, this.host);
|
|
838
1519
|
return;
|
|
839
1520
|
}
|
|
840
1521
|
if (data.type === "openkey:close") {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1522
|
+
if (isVersionedRequest) {
|
|
1523
|
+
const closeRequestId = data.requestId;
|
|
1524
|
+
const closeVersion = data.protocolVersion;
|
|
1525
|
+
if (closeRequestId !== outgoingRequestId) return;
|
|
1526
|
+
if (closeVersion !== outgoingProtocolVersion) return;
|
|
1527
|
+
}
|
|
1528
|
+
settle(() => {
|
|
1529
|
+
popup.close();
|
|
1530
|
+
reject({
|
|
1531
|
+
code: "USER_CANCELLED",
|
|
1532
|
+
message: "User cancelled the request"
|
|
1533
|
+
});
|
|
846
1534
|
});
|
|
847
1535
|
return;
|
|
848
1536
|
}
|
|
849
1537
|
if (data.type === "openkey:auth:use-external-wallet") {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1538
|
+
settle(() => {
|
|
1539
|
+
popup.close();
|
|
1540
|
+
this.handleExternalWalletConnect(resolve, reject);
|
|
1541
|
+
});
|
|
853
1542
|
return;
|
|
854
1543
|
}
|
|
855
|
-
if (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
if (
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1544
|
+
if (this.isResponseForAction(action, data)) {
|
|
1545
|
+
if (isVersionedRequest) {
|
|
1546
|
+
const respRequestId = data.requestId;
|
|
1547
|
+
const respVersion = data.protocolVersion;
|
|
1548
|
+
if (respRequestId !== outgoingRequestId) return;
|
|
1549
|
+
if (respVersion !== outgoingProtocolVersion) return;
|
|
1550
|
+
}
|
|
1551
|
+
settle(() => {
|
|
1552
|
+
popup.close();
|
|
1553
|
+
if (data.success) {
|
|
1554
|
+
if (data.type === "openkey:auth:response") {
|
|
1555
|
+
if (data.sessionToken) this.sessionToken = data.sessionToken;
|
|
1556
|
+
resolve({ address: data.address, keyId: data.keyId, keyType: data.keyType || "MANAGED" });
|
|
1557
|
+
} else if (data.type === "openkey:link-wallet:response") {
|
|
1558
|
+
resolve({ address: data.address, keyId: data.keyId });
|
|
1559
|
+
} else if (data.type === "openkey:sign-out:response") {
|
|
1560
|
+
resolve({ requestId: data.requestId, revoked: data.revoked });
|
|
1561
|
+
} else if (data.type === "openkey:externalSign:approve") {
|
|
1562
|
+
resolve({
|
|
1563
|
+
authorizationContextToken: data.authorizationContextToken,
|
|
1564
|
+
previewApprovalToken: data.previewApprovalToken,
|
|
1565
|
+
signedMessage: data.signedMessage,
|
|
1566
|
+
selectedActionIds: data.selectedActionIds,
|
|
1567
|
+
address: data.address
|
|
1568
|
+
});
|
|
1569
|
+
} else {
|
|
1570
|
+
resolve({
|
|
1571
|
+
signature: data.signature,
|
|
1572
|
+
address: data.address,
|
|
1573
|
+
signedMessage: data.signedMessage,
|
|
1574
|
+
selectedActionKeys: data.selectedActionKeys,
|
|
1575
|
+
permissions: data.permissions
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
864
1578
|
} else {
|
|
865
|
-
|
|
1579
|
+
reject(data.error);
|
|
866
1580
|
}
|
|
867
|
-
}
|
|
868
|
-
reject(data.error);
|
|
869
|
-
}
|
|
1581
|
+
});
|
|
870
1582
|
}
|
|
871
1583
|
};
|
|
872
1584
|
window.addEventListener("message", handleMessage);
|
|
1585
|
+
cancel = () => {
|
|
1586
|
+
settle(() => {
|
|
1587
|
+
popup.close();
|
|
1588
|
+
reject({ code: "USER_CANCELLED", message: "Superseded by sign-out" });
|
|
1589
|
+
});
|
|
1590
|
+
};
|
|
1591
|
+
this.activeFlowCancellations.add(cancel);
|
|
1592
|
+
}
|
|
1593
|
+
cancelActiveFlows() {
|
|
1594
|
+
for (const cancel of [...this.activeFlowCancellations]) cancel();
|
|
873
1595
|
}
|
|
874
1596
|
/**
|
|
875
1597
|
* Disconnect and close any open flows
|
|
876
1598
|
*/
|
|
877
1599
|
disconnect() {
|
|
1600
|
+
this.cancelActiveFlows();
|
|
878
1601
|
this.popup?.close();
|
|
879
1602
|
this.popup = null;
|
|
880
1603
|
}
|
|
@@ -947,8 +1670,13 @@ function getOpenKey(config) {
|
|
|
947
1670
|
}
|
|
948
1671
|
// Annotate the CommonJS export names for ESM import in node:
|
|
949
1672
|
0 && (module.exports = {
|
|
1673
|
+
KeyIdTypeMismatchError,
|
|
950
1674
|
OpenKey,
|
|
1675
|
+
OpenKeyNostr,
|
|
951
1676
|
OpenKeyProvider,
|
|
952
|
-
getOpenKey
|
|
1677
|
+
getOpenKey,
|
|
1678
|
+
resolveAuthorizeTinyCloudRouting,
|
|
1679
|
+
shouldRouteAuthorizeTinyCloudExternal,
|
|
1680
|
+
validateIframeResize
|
|
953
1681
|
});
|
|
954
1682
|
//# sourceMappingURL=index.js.map
|