@1shotapi/ows-signer 0.4.4 → 0.4.6
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/html/signer.css +14 -0
- package/package.json +1 -1
- package/src/ui.js +94 -11
package/html/signer.css
CHANGED
|
@@ -123,6 +123,20 @@ body.ows-ceremony-open {
|
|
|
123
123
|
padding-bottom: max(16px, env(safe-area-inset-bottom, 0px));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
.ows-signer-ui--key-entry .ows-ceremony-body {
|
|
127
|
+
flex: 1 1 auto;
|
|
128
|
+
min-height: 0;
|
|
129
|
+
overflow-y: auto;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ows-signer-ui--key-entry .ows-password {
|
|
133
|
+
margin-top: 12px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.ows-signer-ui--key-entry .ows-password + .ows-ceremony-text {
|
|
137
|
+
margin-top: 8px;
|
|
138
|
+
}
|
|
139
|
+
|
|
126
140
|
.ows-signer-ui:empty {
|
|
127
141
|
display: none;
|
|
128
142
|
}
|
package/package.json
CHANGED
package/src/ui.js
CHANGED
|
@@ -3,8 +3,9 @@ import { debugLog, describeCeremonyEnvironment } from "./debug.js";
|
|
|
3
3
|
import { getTrustedParentOrigin } from "./state.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Clickjack / obscured-frame gate. True when the signer document is visible
|
|
7
|
+
* and focused. Reveal snapshots this at the Confirm or passphrase click
|
|
8
|
+
* ({@link canRevealPrivateKey}); clipboard writes check it again on Copy.
|
|
8
9
|
* @returns {boolean}
|
|
9
10
|
*/
|
|
10
11
|
export function isSignerDocumentInteractive() {
|
|
@@ -16,6 +17,46 @@ export function isSignerDocumentInteractive() {
|
|
|
16
17
|
return true;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Key hex may be shown when the document is visible and either focused now
|
|
22
|
+
* or this ceremony's Confirm / passphrase click happened while it was.
|
|
23
|
+
* The passkey sheet blurs the iframe before reveal, so focus at reveal time
|
|
24
|
+
* alone rejects a ceremony the user just confirmed.
|
|
25
|
+
* Clipboard writes do not use this helper.
|
|
26
|
+
*
|
|
27
|
+
* @param {{
|
|
28
|
+
* visibilityState: string,
|
|
29
|
+
* hasFocus: boolean,
|
|
30
|
+
* confirmedWhileInteractive: boolean,
|
|
31
|
+
* }} state
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function canRevealPrivateKey(state) {
|
|
35
|
+
if (state.visibilityState !== "visible") return false;
|
|
36
|
+
return Boolean(state.hasFocus || state.confirmedWhileInteractive);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set on a Confirm or passphrase click that ran while the signer document
|
|
41
|
+
* was visible and focused. Consumed by {@link showPrivateKey}.
|
|
42
|
+
* @type {boolean}
|
|
43
|
+
*/
|
|
44
|
+
let revealConfirmedWhileInteractive = false;
|
|
45
|
+
|
|
46
|
+
function noteRevealInteraction() {
|
|
47
|
+
revealConfirmedWhileInteractive = isSignerDocumentInteractive();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function clearRevealInteraction() {
|
|
51
|
+
revealConfirmedWhileInteractive = false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function takeRevealInteraction() {
|
|
55
|
+
const confirmed = revealConfirmedWhileInteractive;
|
|
56
|
+
revealConfirmedWhileInteractive = false;
|
|
57
|
+
return confirmed;
|
|
58
|
+
}
|
|
59
|
+
|
|
19
60
|
/** @type {HTMLElement | null} */
|
|
20
61
|
let root = null;
|
|
21
62
|
|
|
@@ -112,6 +153,7 @@ export class CeremonyDeniedError extends Error {
|
|
|
112
153
|
*/
|
|
113
154
|
export function cancelPendingCeremonyConfirm() {
|
|
114
155
|
if (!pendingConfirmReject) return;
|
|
156
|
+
clearRevealInteraction();
|
|
115
157
|
const reject = pendingConfirmReject;
|
|
116
158
|
pendingConfirmReject = null;
|
|
117
159
|
clearUi();
|
|
@@ -190,6 +232,7 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
190
232
|
return Promise.reject(new Error("uiNotInitialized"));
|
|
191
233
|
}
|
|
192
234
|
cancelPendingCeremonyConfirm();
|
|
235
|
+
clearRevealInteraction();
|
|
193
236
|
clearUi();
|
|
194
237
|
root.className = "ows-signer-ui ows-signer-ui--ceremony";
|
|
195
238
|
setCeremonyOpen(true);
|
|
@@ -246,6 +289,7 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
246
289
|
};
|
|
247
290
|
|
|
248
291
|
const settleFailure = (error) => {
|
|
292
|
+
clearRevealInteraction();
|
|
249
293
|
if (pendingConfirmReject !== reject) return;
|
|
250
294
|
finishWaiting();
|
|
251
295
|
clearUi();
|
|
@@ -255,6 +299,9 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
255
299
|
confirmBtn.addEventListener("click", () => {
|
|
256
300
|
confirmBtn.disabled = true;
|
|
257
301
|
denyBtn.disabled = true;
|
|
302
|
+
// Snapshot focus before credentials.get(). The passkey sheet blurs
|
|
303
|
+
// this iframe, so reveal cannot rely on hasFocus() afterwards.
|
|
304
|
+
noteRevealInteraction();
|
|
258
305
|
try {
|
|
259
306
|
// Start WebAuthn in the same turn as the click (no prior await).
|
|
260
307
|
// Keep copy visible until the ceremony settles.
|
|
@@ -266,6 +313,7 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
266
313
|
});
|
|
267
314
|
|
|
268
315
|
denyBtn.addEventListener("click", () => {
|
|
316
|
+
clearRevealInteraction();
|
|
269
317
|
finishWaiting();
|
|
270
318
|
clearUi();
|
|
271
319
|
reject(new CeremonyDeniedError());
|
|
@@ -280,8 +328,10 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
280
328
|
* Show the private key hex with Copy + Done. Resolves when the user clicks Done
|
|
281
329
|
* (or the UI is cleared), so Branding can keep the ceremony panel open.
|
|
282
330
|
*
|
|
283
|
-
*
|
|
284
|
-
* or
|
|
331
|
+
* The document must be visible. Focus is required unless this ceremony's
|
|
332
|
+
* Confirm or passphrase click already happened while the signer was
|
|
333
|
+
* interactive — the passkey sheet blurs the iframe before reveal.
|
|
334
|
+
* Clipboard writes still require a focused document at the Copy click.
|
|
285
335
|
*
|
|
286
336
|
* @param {Uint8Array} privateKey
|
|
287
337
|
* @returns {Promise<void>}
|
|
@@ -290,7 +340,24 @@ export function showPrivateKey(privateKey) {
|
|
|
290
340
|
if (!root) {
|
|
291
341
|
return Promise.reject(new Error("uiNotInitialized"));
|
|
292
342
|
}
|
|
293
|
-
|
|
343
|
+
try {
|
|
344
|
+
window.focus();
|
|
345
|
+
} catch {
|
|
346
|
+
// Nested iframe focus can be ignored after the passkey sheet.
|
|
347
|
+
}
|
|
348
|
+
const visibilityState =
|
|
349
|
+
typeof document !== "undefined" ? document.visibilityState : "hidden";
|
|
350
|
+
const hasFocus =
|
|
351
|
+
typeof document !== "undefined" &&
|
|
352
|
+
(typeof document.hasFocus !== "function" || document.hasFocus());
|
|
353
|
+
const confirmedWhileInteractive = takeRevealInteraction();
|
|
354
|
+
if (
|
|
355
|
+
!canRevealPrivateKey({
|
|
356
|
+
visibilityState,
|
|
357
|
+
hasFocus,
|
|
358
|
+
confirmedWhileInteractive,
|
|
359
|
+
})
|
|
360
|
+
) {
|
|
294
361
|
return Promise.reject(new Error("signerNotInteractive"));
|
|
295
362
|
}
|
|
296
363
|
cancelPendingCeremonyConfirm();
|
|
@@ -334,6 +401,7 @@ export function promptPassphrase(passwordText, buttonText, minPasswordLength) {
|
|
|
334
401
|
return Promise.reject(new Error("uiNotInitialized"));
|
|
335
402
|
}
|
|
336
403
|
cancelPendingCeremonyConfirm();
|
|
404
|
+
clearRevealInteraction();
|
|
337
405
|
clearUi();
|
|
338
406
|
const safePlaceholder = sanitizeDisplayText(passwordText, {
|
|
339
407
|
maxLength: MAX_PASSPHRASE_PROMPT_LENGTH,
|
|
@@ -352,9 +420,12 @@ export function promptPassphrase(passwordText, buttonText, minPasswordLength) {
|
|
|
352
420
|
button.textContent = safeButton || "Continue";
|
|
353
421
|
button.addEventListener("click", () => {
|
|
354
422
|
if (input.value.length < minPasswordLength) {
|
|
423
|
+
clearRevealInteraction();
|
|
355
424
|
reject(new Error("passwordTooShort"));
|
|
356
425
|
return;
|
|
357
426
|
}
|
|
427
|
+
// recoverKey shows the key after this click's awaits, once focus may be gone.
|
|
428
|
+
noteRevealInteraction();
|
|
358
429
|
resolve(input.value);
|
|
359
430
|
clearUi();
|
|
360
431
|
});
|
|
@@ -374,14 +445,24 @@ export function promptPrivateKey() {
|
|
|
374
445
|
}
|
|
375
446
|
cancelPendingCeremonyConfirm();
|
|
376
447
|
clearUi();
|
|
448
|
+
root.className =
|
|
449
|
+
"ows-signer-ui ows-signer-ui--ceremony ows-signer-ui--key-entry";
|
|
450
|
+
setCeremonyOpen(true);
|
|
377
451
|
return new Promise((resolve, reject) => {
|
|
452
|
+
const grab = document.createElement("div");
|
|
453
|
+
grab.className = "ows-ceremony-grab";
|
|
454
|
+
grab.setAttribute("aria-hidden", "true");
|
|
455
|
+
|
|
378
456
|
const header = document.createElement("h2");
|
|
379
457
|
header.className = "ows-ceremony-header";
|
|
380
458
|
header.textContent = "Import private key";
|
|
381
|
-
|
|
382
|
-
body
|
|
383
|
-
body.
|
|
384
|
-
|
|
459
|
+
|
|
460
|
+
const body = document.createElement("div");
|
|
461
|
+
body.className = "ows-ceremony-body";
|
|
462
|
+
const text = document.createElement("p");
|
|
463
|
+
text.className = "ows-ceremony-text";
|
|
464
|
+
text.textContent =
|
|
465
|
+
"Paste your EVM private key to recover your 1Shot wallet accounts.";
|
|
385
466
|
const input = document.createElement("input");
|
|
386
467
|
input.type = "password";
|
|
387
468
|
input.className = "ows-password";
|
|
@@ -392,6 +473,8 @@ export function promptPrivateKey() {
|
|
|
392
473
|
errorEl.className = "ows-ceremony-text";
|
|
393
474
|
errorEl.style.color = "tomato";
|
|
394
475
|
errorEl.hidden = true;
|
|
476
|
+
body.append(text, input, errorEl);
|
|
477
|
+
|
|
395
478
|
const confirmBtn = document.createElement("button");
|
|
396
479
|
confirmBtn.type = "button";
|
|
397
480
|
confirmBtn.className = "ows-confirm";
|
|
@@ -401,7 +484,7 @@ export function promptPrivateKey() {
|
|
|
401
484
|
denyBtn.className = "ows-deny";
|
|
402
485
|
denyBtn.textContent = "Cancel";
|
|
403
486
|
const actions = document.createElement("div");
|
|
404
|
-
actions.className = "ows-actions";
|
|
487
|
+
actions.className = "ows-ceremony-actions";
|
|
405
488
|
actions.append(confirmBtn, denyBtn);
|
|
406
489
|
|
|
407
490
|
const finish = (fn) => {
|
|
@@ -422,7 +505,7 @@ export function promptPrivateKey() {
|
|
|
422
505
|
finish(() => reject(new CeremonyDeniedError()));
|
|
423
506
|
});
|
|
424
507
|
|
|
425
|
-
root.append(header, body,
|
|
508
|
+
root.append(grab, header, body, actions);
|
|
426
509
|
input.focus();
|
|
427
510
|
});
|
|
428
511
|
}
|