signet-login 0.7.2 → 0.9.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/dist/modal.js +141 -8
- package/dist/signers.d.ts +47 -1
- package/dist/signers.js +94 -3
- package/dist/signet-login.iife.js +18 -18
- package/dist/signet-login.js +78 -39
- package/dist/storage.d.ts +21 -1
- package/dist/storage.js +37 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.js +8 -0
- package/package.json +1 -1
package/dist/modal.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* top-layer placement, theme-aware colours, no third-party UI deps.
|
|
6
6
|
*/
|
|
7
7
|
import { DEFAULTS } from './types.js';
|
|
8
|
-
import { hasNip07, createNip07Signer, createBunkerSigner, createBunkerSignerFromNostrConnect, buildNostrConnectUri, EphemeralSigner, createLocalSignerFromNsec } from './signers.js';
|
|
8
|
+
import { hasNip07, createNip07Signer, createBunkerSigner, createBunkerSignerFromNostrConnect, buildNostrConnectUri, EphemeralSigner, DeferredBunkerSigner, createLocalSignerFromNsec } from './signers.js';
|
|
9
9
|
import { isAndroid, startAmberSignIn } from './amber.js';
|
|
10
|
+
import { loadOrCreatePersistentClientSk } from './storage.js';
|
|
10
11
|
import { waitForAuthResponse } from 'signet-verify';
|
|
11
12
|
import { schnorr } from '@noble/curves/secp256k1';
|
|
12
13
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
13
14
|
import { startRedirect } from './redirect.js';
|
|
14
15
|
import QRCode from 'qrcode';
|
|
16
|
+
const QR_BUNKER_CONNECT_TIMEOUT_MS = 8000;
|
|
15
17
|
function escapeHtml(str) {
|
|
16
18
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
17
19
|
}
|
|
@@ -39,9 +41,95 @@ function buildModalShell(theme) {
|
|
|
39
41
|
dialog.style.cssText = `border:none;border-radius:16px;padding:32px;max-width:380px;width:90%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,0.3);background:${bg};color:${fg};font-family:system-ui,-apple-system,sans-serif;`;
|
|
40
42
|
document.body.appendChild(dialog);
|
|
41
43
|
dialog.showModal();
|
|
42
|
-
|
|
44
|
+
// Gamepad navigation. A booth/kiosk drives this modal with a gamepad, whose
|
|
45
|
+
// host game dispatches *synthetic* Arrow / Enter / Escape KeyboardEvents on
|
|
46
|
+
// `window` (isTrusted=false). Native <dialog> only moves focus with Tab, and
|
|
47
|
+
// synthetic Enter/Escape don't trigger native button activation or the
|
|
48
|
+
// dialog's cancel — so bridge them here. Real keyboard events (isTrusted)
|
|
49
|
+
// keep their native behaviour; we only fully drive the synthetic ones. Works
|
|
50
|
+
// across every screen because it queries the live buttons each keypress.
|
|
51
|
+
const visibleButtons = () => Array.from(dialog.querySelectorAll('button'))
|
|
52
|
+
.filter((b) => {
|
|
53
|
+
if (b.disabled || !b.isConnected)
|
|
54
|
+
return false;
|
|
55
|
+
const css = window.getComputedStyle(b);
|
|
56
|
+
return css.display !== 'none' && css.visibility !== 'hidden';
|
|
57
|
+
});
|
|
58
|
+
const focusedButton = () => document.activeElement instanceof HTMLButtonElement && dialog.contains(document.activeElement)
|
|
59
|
+
? document.activeElement
|
|
60
|
+
: null;
|
|
61
|
+
// Tracked cursor. We click THIS index on Enter rather than
|
|
62
|
+
// document.activeElement, because a host page's own menu-nav (still mounted
|
|
63
|
+
// behind the dialog) can clear/move DOM focus between keypresses — which made
|
|
64
|
+
// "A = select the highlighted item" click the wrong (first) button. Tracking
|
|
65
|
+
// the index ourselves makes selection reliable regardless of the host.
|
|
66
|
+
let selIndex = 0;
|
|
67
|
+
const showSel = () => {
|
|
68
|
+
const btns = visibleButtons();
|
|
69
|
+
if (btns.length === 0)
|
|
70
|
+
return;
|
|
71
|
+
selIndex = Math.min(Math.max(selIndex, 0), btns.length - 1);
|
|
72
|
+
btns[selIndex].focus();
|
|
73
|
+
};
|
|
74
|
+
const keyNav = (e) => {
|
|
75
|
+
if (!dialog.isConnected || !dialog.open)
|
|
76
|
+
return;
|
|
77
|
+
const tgt = e.target;
|
|
78
|
+
if (tgt && (tgt.tagName === 'INPUT' || tgt.tagName === 'TEXTAREA') && e.isTrusted)
|
|
79
|
+
return; // real typing owns its keys
|
|
80
|
+
const btns = visibleButtons();
|
|
81
|
+
if (btns.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
const key = e.key || e.code;
|
|
84
|
+
const code = e.code || e.key;
|
|
85
|
+
const dir = key === 'ArrowDown' || code === 'ArrowDown' || key === 'ArrowRight' || code === 'ArrowRight' ? 1
|
|
86
|
+
: key === 'ArrowUp' || code === 'ArrowUp' || key === 'ArrowLeft' || code === 'ArrowLeft' ? -1 : 0;
|
|
87
|
+
if (dir) {
|
|
88
|
+
// The modal owns nav while open — stop the host page's listeners from
|
|
89
|
+
// also grabbing the key and stealing focus.
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
e.stopImmediatePropagation();
|
|
92
|
+
// Re-sync to live DOM focus if the host moved it, else step our index.
|
|
93
|
+
const fb = focusedButton();
|
|
94
|
+
const fi = fb ? btns.indexOf(fb) : -1;
|
|
95
|
+
selIndex = ((fi >= 0 ? fi : selIndex) + dir + btns.length) % btns.length;
|
|
96
|
+
btns[selIndex].focus();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Real keyboard (isTrusted): let native Enter/Escape stand AND keep
|
|
100
|
+
// propagating so the focused button's native activation fires. We only
|
|
101
|
+
// fully drive the SYNTHETIC events a gamepad host dispatches on window.
|
|
102
|
+
if (e.isTrusted)
|
|
103
|
+
return;
|
|
104
|
+
if (e.key === 'Enter' || e.key === ' ' || e.code === 'Space' || e.code === 'Enter') {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
e.stopImmediatePropagation();
|
|
107
|
+
btns[Math.min(Math.max(selIndex, 0), btns.length - 1)].click(); // tracked cursor, not activeElement
|
|
108
|
+
}
|
|
109
|
+
else if (e.key === 'Escape' || e.code === 'Escape') {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
e.stopImmediatePropagation();
|
|
112
|
+
// Prefer Back (sub-screen → picker); fall back to Cancel.
|
|
113
|
+
(dialog.querySelector('[data-action="back"]')
|
|
114
|
+
?? dialog.querySelector('[data-action="cancel"],[data-choice="cancel"]'))?.click();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
// Capture phase: run BEFORE the host page's bubble-phase window keydown
|
|
118
|
+
// handlers, so stopImmediatePropagation above actually pre-empts them.
|
|
119
|
+
window.addEventListener('keydown', keyNav, true);
|
|
120
|
+
// Reset the cursor to the first button when a new SCREEN swaps in. childList
|
|
121
|
+
// (no subtree) so frequent status-text updates inside a screen don't reset it.
|
|
122
|
+
const mo = new MutationObserver(() => { selIndex = 0; showSel(); });
|
|
123
|
+
mo.observe(dialog, { childList: true });
|
|
124
|
+
showSel();
|
|
125
|
+
return {
|
|
126
|
+
dialog,
|
|
127
|
+
style,
|
|
128
|
+
cleanupNav: () => { window.removeEventListener('keydown', keyNav, true); mo.disconnect(); },
|
|
129
|
+
};
|
|
43
130
|
}
|
|
44
131
|
function tearDown(refs) {
|
|
132
|
+
refs.cleanupNav?.();
|
|
45
133
|
try {
|
|
46
134
|
refs.dialog.close();
|
|
47
135
|
}
|
|
@@ -233,7 +321,8 @@ async function runRedirectFlow(refs, opts) {
|
|
|
233
321
|
sessionPrivKey,
|
|
234
322
|
expectedOrigin: opts.origin,
|
|
235
323
|
timeout: opts.timeout,
|
|
236
|
-
}).then(
|
|
324
|
+
}).then(rawResult => {
|
|
325
|
+
const result = rawResult;
|
|
237
326
|
const authEvent = {
|
|
238
327
|
id: result.authEvent.id,
|
|
239
328
|
pubkey: result.authEvent.pubkey,
|
|
@@ -246,6 +335,8 @@ async function runRedirectFlow(refs, opts) {
|
|
|
246
335
|
const out = { pubkey: result.pubkey, authEvent };
|
|
247
336
|
if (result.displayName)
|
|
248
337
|
out.displayName = result.displayName;
|
|
338
|
+
if (result.bunkerUri)
|
|
339
|
+
out.bunkerUri = result.bunkerUri;
|
|
249
340
|
settle(out);
|
|
250
341
|
}).catch(err => {
|
|
251
342
|
const status = refs.dialog.querySelector('#signet-login-status');
|
|
@@ -300,7 +391,7 @@ async function runBunkerFlow(refs, opts) {
|
|
|
300
391
|
}
|
|
301
392
|
connectBtn.disabled = true;
|
|
302
393
|
try {
|
|
303
|
-
const signer = await createBunkerSigner({ uri });
|
|
394
|
+
const signer = await createBunkerSigner({ uri, clientSecretKey: loadOrCreatePersistentClientSk() });
|
|
304
395
|
settle(signer);
|
|
305
396
|
}
|
|
306
397
|
catch (err) {
|
|
@@ -323,7 +414,10 @@ async function runBunkerFlow(refs, opts) {
|
|
|
323
414
|
async function runNostrConnectFlow(refs, opts) {
|
|
324
415
|
const dark = isDarkMode(opts.theme);
|
|
325
416
|
const muted = dark ? '#888' : '#666';
|
|
326
|
-
|
|
417
|
+
// Persistent client key so the advertised client pubkey is stable across
|
|
418
|
+
// logins (bunkers auto-approve a bound client pubkey). The connect `secret`
|
|
419
|
+
// stays fresh per handshake — it's a one-time challenge, not an identity.
|
|
420
|
+
const sk = loadOrCreatePersistentClientSk();
|
|
327
421
|
const clientPubkey = bytesToHex(schnorr.getPublicKey(sk));
|
|
328
422
|
const secret = bytesToHex(schnorr.utils.randomPrivateKey()).slice(0, 32);
|
|
329
423
|
const uri = buildNostrConnectUri({
|
|
@@ -558,11 +652,50 @@ export async function showLoginModal(opts) {
|
|
|
558
652
|
return null;
|
|
559
653
|
continue;
|
|
560
654
|
}
|
|
561
|
-
|
|
655
|
+
// Default: auth-only ephemeral signer (identity proof, no live signing).
|
|
656
|
+
let signer = new EphemeralSigner(result.pubkey, result.authEvent);
|
|
657
|
+
let method = 'redirect';
|
|
658
|
+
// Cross-device bunker passthrough: when the signer device hands back a
|
|
659
|
+
// `bunker://` URI (its own NIP-46 server, or an upstream hardware
|
|
660
|
+
// bunker), preserve it as a deferred signer. That keeps login responsive
|
|
661
|
+
// and avoids downgrading to auth-only just because a cold ESP32 / relay
|
|
662
|
+
// is not ready during the approval round trip. The first real signing
|
|
663
|
+
// request awaits the connection and reports auth-only only if it fails.
|
|
664
|
+
if (result.bunkerUri) {
|
|
665
|
+
const clientSecretKey = loadOrCreatePersistentClientSk();
|
|
666
|
+
const expected = result.pubkey;
|
|
667
|
+
const authEvent = result.authEvent;
|
|
668
|
+
const upgrade = createBunkerSigner({
|
|
669
|
+
uri: result.bunkerUri,
|
|
670
|
+
clientSecretKey,
|
|
671
|
+
timeoutMs: QR_BUNKER_CONNECT_TIMEOUT_MS,
|
|
672
|
+
})
|
|
673
|
+
.then((bunkerSigner) => {
|
|
674
|
+
if (bunkerSigner.pubkey.toLowerCase() === expected.toLowerCase()) {
|
|
675
|
+
return bunkerSigner;
|
|
676
|
+
}
|
|
677
|
+
console.warn('[signet-login] QR upgrade: bunker pubkey mismatch — staying auth-only (cannot sign)', { connected: bunkerSigner.pubkey, expected });
|
|
678
|
+
void bunkerSigner.close().catch(() => { });
|
|
679
|
+
return null;
|
|
680
|
+
})
|
|
681
|
+
.catch((err) => {
|
|
682
|
+
console.warn('[signet-login] QR upgrade: createBunkerSigner failed — deferred signer will behave auth-only until reconnect. Reconnect/relay issue or signer device unreachable.', err);
|
|
683
|
+
return null;
|
|
684
|
+
});
|
|
685
|
+
// QR handoffs must not advertise signing until the remote bunker is
|
|
686
|
+
// actually connected. Pallasite starts several background signing
|
|
687
|
+
// tasks as soon as canSignEvents=true; an optimistic cold ESP32
|
|
688
|
+
// handoff strands the UI on "Signing…" before the player can act.
|
|
689
|
+
signer = new DeferredBunkerSigner(expected, authEvent, upgrade, result.bunkerUri, clientSecretKey, false);
|
|
690
|
+
method = 'bunker';
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
console.warn('[signet-login] QR login carried no bunkerUri — auth-only ephemeral (cannot sign). The signer device must have its NIP-46 server enabled to hand back a bunker:// URI.');
|
|
694
|
+
}
|
|
562
695
|
const session = {
|
|
563
696
|
pubkey: result.pubkey,
|
|
564
|
-
method
|
|
565
|
-
signer
|
|
697
|
+
method,
|
|
698
|
+
signer,
|
|
566
699
|
authEvent: result.authEvent,
|
|
567
700
|
};
|
|
568
701
|
if (result.displayName)
|
package/dist/signers.d.ts
CHANGED
|
@@ -85,12 +85,25 @@ export declare function buildNostrConnectUri(input: {
|
|
|
85
85
|
}): string;
|
|
86
86
|
/**
|
|
87
87
|
* Connect a bunker session from a `bunker://` or `nostr+connect://` URI (or a
|
|
88
|
-
* NIP-05 identifier).
|
|
88
|
+
* NIP-05 identifier). Pass `clientSecretKey` to bind a stable client pubkey the
|
|
89
|
+
* signer can auto-approve (see `loadOrCreatePersistentClientSk`); when omitted a
|
|
90
|
+
* fresh ephemeral key is generated, which a per-pubkey-approving bunker will
|
|
91
|
+
* treat as a new, unapproved client.
|
|
89
92
|
*/
|
|
90
93
|
export declare function createBunkerSigner(input: {
|
|
91
94
|
uri: string;
|
|
92
95
|
clientSecretKey?: Uint8Array;
|
|
93
96
|
onauth?: (url: string) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Bound the NIP-46 `connect` + `get_public_key` handshake, in milliseconds.
|
|
99
|
+
* Omit for the interactive paste flow, where a cold remote signer may
|
|
100
|
+
* legitimately take tens of seconds to approve via the `auth_url` callback.
|
|
101
|
+
* Set it for unattended boot-time connects (redirect-bunker auto-pair,
|
|
102
|
+
* session restore) where a non-responding signer must degrade to the
|
|
103
|
+
* auth-only fallback rather than stall. On expiry the half-open signer is
|
|
104
|
+
* closed and the call rejects with `bunker-connect-timeout`.
|
|
105
|
+
*/
|
|
106
|
+
timeoutMs?: number;
|
|
94
107
|
}): Promise<BunkerSignerImpl>;
|
|
95
108
|
/** Generate a 32-byte secret key. */
|
|
96
109
|
export declare function generateSecretKey(): Uint8Array;
|
|
@@ -129,4 +142,37 @@ export declare class EphemeralSigner implements SignetSigner {
|
|
|
129
142
|
signEvent(_template: EventTemplate): Promise<NostrEvent>;
|
|
130
143
|
close(): Promise<void>;
|
|
131
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* A redirect-bunker signer whose bunker connects in the BACKGROUND.
|
|
147
|
+
*
|
|
148
|
+
* `handleRedirectCallback` returns this immediately so the consumer can paint a
|
|
149
|
+
* signed-in UI without waiting on an up-to-8s bunker handshake over flaky
|
|
150
|
+
* relays (the cause of the "blank screen on sign-in"). The authenticated pubkey
|
|
151
|
+
* is known up front; the first `signEvent` / `nip44` call awaits the background
|
|
152
|
+
* connect. If that connect fails, signing rejects with the auth-only error —
|
|
153
|
+
* the session is still valid for identity proof.
|
|
154
|
+
*/
|
|
155
|
+
export declare class DeferredBunkerSigner implements SignetSigner {
|
|
156
|
+
readonly pubkey: string;
|
|
157
|
+
readonly authEvent: SignetAuthEvent;
|
|
158
|
+
/** Resolves to the connected bunker, or null if the connect failed. */
|
|
159
|
+
private readonly upgrade;
|
|
160
|
+
/** Original bunker URI — exposed so persistence can reconnect on reload. */
|
|
161
|
+
readonly bunkerUri?: string | undefined;
|
|
162
|
+
/** Stable client key — exposed so persistence keeps the same NIP-46 client pubkey. */
|
|
163
|
+
readonly clientSecretKey?: Uint8Array | undefined;
|
|
164
|
+
readonly method: "bunker";
|
|
165
|
+
readonly capabilities: SignerCapabilities;
|
|
166
|
+
readonly nip44: SignetSigner['nip44'];
|
|
167
|
+
constructor(pubkey: string, authEvent: SignetAuthEvent,
|
|
168
|
+
/** Resolves to the connected bunker, or null if the connect failed. */
|
|
169
|
+
upgrade: Promise<BunkerSignerImpl | null>,
|
|
170
|
+
/** Original bunker URI — exposed so persistence can reconnect on reload. */
|
|
171
|
+
bunkerUri?: string | undefined,
|
|
172
|
+
/** Stable client key — exposed so persistence keeps the same NIP-46 client pubkey. */
|
|
173
|
+
clientSecretKey?: Uint8Array | undefined, optimisticCapabilities?: boolean);
|
|
174
|
+
private live;
|
|
175
|
+
signEvent(template: EventTemplate): Promise<NostrEvent>;
|
|
176
|
+
close(): Promise<void>;
|
|
177
|
+
}
|
|
132
178
|
export {};
|
package/dist/signers.js
CHANGED
|
@@ -125,9 +125,39 @@ export function buildNostrConnectUri(input) {
|
|
|
125
125
|
params.set('url', input.appUrl);
|
|
126
126
|
return `nostrconnect://${clientPubkeyHex}?${params.toString()}`;
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Race a bunker handshake against a deadline. nostr-tools' `BunkerSigner`
|
|
130
|
+
* `sendRequest` has no per-request timeout — it publishes the request and only
|
|
131
|
+
* settles when a matching response arrives on the subscription. If the remote
|
|
132
|
+
* signer never replies (relay unreachable, or the bunker server is already gone
|
|
133
|
+
* — e.g. signet-app's in-page NIP-46 server after a same-tab redirect navigated
|
|
134
|
+
* it away), `connect()`/`getPublicKey()` hang forever. On timeout we close the
|
|
135
|
+
* half-open signer (releasing its relay subscription) and reject so the caller
|
|
136
|
+
* can fall back. `Promise.race` keeps a rejection handler attached to `p`, so a
|
|
137
|
+
* late rejection from the abandoned handshake won't surface as unhandled.
|
|
138
|
+
*/
|
|
139
|
+
async function raceBunkerHandshake(p, ms, bunker) {
|
|
140
|
+
let timer;
|
|
141
|
+
const timeout = new Promise((_, reject) => {
|
|
142
|
+
timer = setTimeout(() => {
|
|
143
|
+
void bunker.close().catch(() => { });
|
|
144
|
+
reject(new Error('bunker-connect-timeout'));
|
|
145
|
+
}, ms);
|
|
146
|
+
});
|
|
147
|
+
try {
|
|
148
|
+
return await Promise.race([p, timeout]);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if (timer !== undefined)
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
128
155
|
/**
|
|
129
156
|
* Connect a bunker session from a `bunker://` or `nostr+connect://` URI (or a
|
|
130
|
-
* NIP-05 identifier).
|
|
157
|
+
* NIP-05 identifier). Pass `clientSecretKey` to bind a stable client pubkey the
|
|
158
|
+
* signer can auto-approve (see `loadOrCreatePersistentClientSk`); when omitted a
|
|
159
|
+
* fresh ephemeral key is generated, which a per-pubkey-approving bunker will
|
|
160
|
+
* treat as a new, unapproved client.
|
|
131
161
|
*/
|
|
132
162
|
export async function createBunkerSigner(input) {
|
|
133
163
|
const trimmed = input.uri.trim();
|
|
@@ -140,8 +170,13 @@ export async function createBunkerSigner(input) {
|
|
|
140
170
|
if (sk.length !== 32)
|
|
141
171
|
throw new Error('invalid-client-secret-key');
|
|
142
172
|
const bunker = BunkerSigner.fromBunker(sk, pointer, { onauth: input.onauth });
|
|
143
|
-
|
|
144
|
-
|
|
173
|
+
const handshake = (async () => {
|
|
174
|
+
await bunker.connect();
|
|
175
|
+
return bunker.getPublicKey();
|
|
176
|
+
})();
|
|
177
|
+
const pubkey = input.timeoutMs && input.timeoutMs > 0
|
|
178
|
+
? await raceBunkerHandshake(handshake, input.timeoutMs, bunker)
|
|
179
|
+
: await handshake;
|
|
145
180
|
if (!/^[0-9a-f]{64}$/i.test(pubkey)) {
|
|
146
181
|
await bunker.close().catch(() => { });
|
|
147
182
|
throw new Error('invalid-pubkey-from-bunker');
|
|
@@ -238,3 +273,59 @@ export class EphemeralSigner {
|
|
|
238
273
|
// nothing to close
|
|
239
274
|
}
|
|
240
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* A redirect-bunker signer whose bunker connects in the BACKGROUND.
|
|
278
|
+
*
|
|
279
|
+
* `handleRedirectCallback` returns this immediately so the consumer can paint a
|
|
280
|
+
* signed-in UI without waiting on an up-to-8s bunker handshake over flaky
|
|
281
|
+
* relays (the cause of the "blank screen on sign-in"). The authenticated pubkey
|
|
282
|
+
* is known up front; the first `signEvent` / `nip44` call awaits the background
|
|
283
|
+
* connect. If that connect fails, signing rejects with the auth-only error —
|
|
284
|
+
* the session is still valid for identity proof.
|
|
285
|
+
*/
|
|
286
|
+
export class DeferredBunkerSigner {
|
|
287
|
+
constructor(pubkey, authEvent,
|
|
288
|
+
/** Resolves to the connected bunker, or null if the connect failed. */
|
|
289
|
+
upgrade,
|
|
290
|
+
/** Original bunker URI — exposed so persistence can reconnect on reload. */
|
|
291
|
+
bunkerUri,
|
|
292
|
+
/** Stable client key — exposed so persistence keeps the same NIP-46 client pubkey. */
|
|
293
|
+
clientSecretKey, optimisticCapabilities = true) {
|
|
294
|
+
this.pubkey = pubkey;
|
|
295
|
+
this.authEvent = authEvent;
|
|
296
|
+
this.upgrade = upgrade;
|
|
297
|
+
this.bunkerUri = bunkerUri;
|
|
298
|
+
this.clientSecretKey = clientSecretKey;
|
|
299
|
+
this.method = 'bunker';
|
|
300
|
+
this.capabilities = {
|
|
301
|
+
canSignEvents: optimisticCapabilities,
|
|
302
|
+
hasNip44: optimisticCapabilities,
|
|
303
|
+
};
|
|
304
|
+
void this.upgrade.then(signer => {
|
|
305
|
+
if (signer) {
|
|
306
|
+
this.capabilities.canSignEvents = true;
|
|
307
|
+
this.capabilities.hasNip44 = true;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
this.nip44 = {
|
|
311
|
+
encrypt: async (peer, pt) => (await this.live()).nip44.encrypt(peer, pt),
|
|
312
|
+
decrypt: async (peer, ct) => (await this.live()).nip44.decrypt(peer, ct),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async live() {
|
|
316
|
+
const signer = await this.upgrade;
|
|
317
|
+
if (!signer) {
|
|
318
|
+
throw new Error('signer-auth-only: the redirect bunker handoff did not connect, so this ' +
|
|
319
|
+
'session cannot sign. Reconnect the signer or paste a bunker URI to upgrade.');
|
|
320
|
+
}
|
|
321
|
+
return signer;
|
|
322
|
+
}
|
|
323
|
+
async signEvent(template) {
|
|
324
|
+
return (await this.live()).signEvent(template);
|
|
325
|
+
}
|
|
326
|
+
async close() {
|
|
327
|
+
const signer = await this.upgrade.catch(() => null);
|
|
328
|
+
if (signer)
|
|
329
|
+
await signer.close().catch(() => { });
|
|
330
|
+
}
|
|
331
|
+
}
|