@formstr/signer 0.1.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/dist/index.js ADDED
@@ -0,0 +1,693 @@
1
+ // src/core/signer.ts
2
+ import { getPublicKey as getPublicKey4, nip19 as nip193 } from "nostr-tools";
3
+
4
+ // src/core/storage.ts
5
+ var DEFAULT_PREFIX = "@formstr/signer:";
6
+ function localStorageAdapter(prefix = DEFAULT_PREFIX) {
7
+ const ls = () => {
8
+ try {
9
+ return typeof globalThis !== "undefined" && globalThis.localStorage ? globalThis.localStorage : null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ };
14
+ return {
15
+ get(key) {
16
+ try {
17
+ return ls()?.getItem(prefix + key) ?? null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ },
22
+ set(key, value) {
23
+ try {
24
+ ls()?.setItem(prefix + key, value);
25
+ } catch {
26
+ }
27
+ },
28
+ remove(key) {
29
+ try {
30
+ ls()?.removeItem(prefix + key);
31
+ } catch {
32
+ }
33
+ }
34
+ };
35
+ }
36
+
37
+ // src/core/localSigner.ts
38
+ import {
39
+ finalizeEvent,
40
+ getPublicKey,
41
+ nip04,
42
+ nip44
43
+ } from "nostr-tools";
44
+ var LocalSigner = class {
45
+ #secretKey;
46
+ constructor(secretKey) {
47
+ this.#secretKey = secretKey;
48
+ }
49
+ async getPublicKey() {
50
+ return getPublicKey(this.#secretKey);
51
+ }
52
+ async signEvent(event) {
53
+ return finalizeEvent(event, this.#secretKey);
54
+ }
55
+ async nip04Encrypt(peerPubkey, plaintext) {
56
+ return nip04.encrypt(this.#secretKey, peerPubkey, plaintext);
57
+ }
58
+ async nip04Decrypt(peerPubkey, ciphertext) {
59
+ return nip04.decrypt(this.#secretKey, peerPubkey, ciphertext);
60
+ }
61
+ async nip44Encrypt(peerPubkey, plaintext) {
62
+ const key = nip44.v2.utils.getConversationKey(this.#secretKey, peerPubkey);
63
+ return nip44.v2.encrypt(plaintext, key);
64
+ }
65
+ async nip44Decrypt(peerPubkey, ciphertext) {
66
+ const key = nip44.v2.utils.getConversationKey(this.#secretKey, peerPubkey);
67
+ return nip44.v2.decrypt(ciphertext, key);
68
+ }
69
+ };
70
+
71
+ // src/nip49.ts
72
+ import { generateSecretKey, getPublicKey as getPublicKey2, nip19 } from "nostr-tools";
73
+ import { encrypt as nip49Encrypt, decrypt as nip49Decrypt } from "nostr-tools/nip49";
74
+ function encryptSecretKey(secretKey, passphrase) {
75
+ return nip49Encrypt(secretKey, passphrase);
76
+ }
77
+ function decryptNcryptsec(ncryptsec, passphrase) {
78
+ return nip49Decrypt(ncryptsec, passphrase);
79
+ }
80
+ function generateAccount(passphrase) {
81
+ const secretKey = generateSecretKey();
82
+ const pubkey = getPublicKey2(secretKey);
83
+ const npub = nip19.npubEncode(pubkey);
84
+ const ncryptsec = nip49Encrypt(secretKey, passphrase);
85
+ return { secretKey, pubkey, npub, ncryptsec };
86
+ }
87
+
88
+ // src/nip07.ts
89
+ function getWindowNostr() {
90
+ const nostr = globalThis.nostr;
91
+ if (!nostr) {
92
+ throw new Error(
93
+ "@formstr/signer: NIP-07 extension not found (globalThis.nostr is undefined)"
94
+ );
95
+ }
96
+ return nostr;
97
+ }
98
+ var ExtensionSigner = class {
99
+ async getPublicKey() {
100
+ return getWindowNostr().getPublicKey();
101
+ }
102
+ async signEvent(event) {
103
+ return getWindowNostr().signEvent(event);
104
+ }
105
+ async nip04Encrypt(peerPubkey, plaintext) {
106
+ const ext = getWindowNostr();
107
+ if (!ext.nip04) throw new Error("NIP-07 extension does not expose nip04");
108
+ return ext.nip04.encrypt(peerPubkey, plaintext);
109
+ }
110
+ async nip04Decrypt(peerPubkey, ciphertext) {
111
+ const ext = getWindowNostr();
112
+ if (!ext.nip04) throw new Error("NIP-07 extension does not expose nip04");
113
+ return ext.nip04.decrypt(peerPubkey, ciphertext);
114
+ }
115
+ async nip44Encrypt(peerPubkey, plaintext) {
116
+ const ext = getWindowNostr();
117
+ if (!ext.nip44) throw new Error("NIP-07 extension does not expose nip44");
118
+ return ext.nip44.encrypt(peerPubkey, plaintext);
119
+ }
120
+ async nip44Decrypt(peerPubkey, ciphertext) {
121
+ const ext = getWindowNostr();
122
+ if (!ext.nip44) throw new Error("NIP-07 extension does not expose nip44");
123
+ return ext.nip44.decrypt(peerPubkey, ciphertext);
124
+ }
125
+ };
126
+
127
+ // src/nip46.ts
128
+ import { generateSecretKey as generateSecretKey2, getPublicKey as getPublicKey3 } from "nostr-tools";
129
+ import {
130
+ BunkerSigner as ToolsBunkerSigner,
131
+ createNostrConnectURI,
132
+ parseBunkerInput
133
+ } from "nostr-tools/nip46";
134
+ var BunkerSigner = class {
135
+ #delegate;
136
+ constructor(delegate) {
137
+ this.#delegate = delegate;
138
+ }
139
+ getPublicKey() {
140
+ return this.#delegate.getPublicKey();
141
+ }
142
+ signEvent(event) {
143
+ return this.#delegate.signEvent(event);
144
+ }
145
+ nip04Encrypt(peerPubkey, plaintext) {
146
+ return this.#delegate.nip04Encrypt(peerPubkey, plaintext);
147
+ }
148
+ nip04Decrypt(peerPubkey, ciphertext) {
149
+ return this.#delegate.nip04Decrypt(peerPubkey, ciphertext);
150
+ }
151
+ nip44Encrypt(peerPubkey, plaintext) {
152
+ return this.#delegate.nip44Encrypt(peerPubkey, plaintext);
153
+ }
154
+ nip44Decrypt(peerPubkey, ciphertext) {
155
+ return this.#delegate.nip44Decrypt(peerPubkey, ciphertext);
156
+ }
157
+ async close() {
158
+ return this.#delegate.close();
159
+ }
160
+ };
161
+ async function fetchBunkerRelays(tools) {
162
+ try {
163
+ const resp = await tools.sendRequest("get_relays", []);
164
+ const parsed = JSON.parse(resp);
165
+ if (Array.isArray(parsed)) {
166
+ return parsed.filter((r) => typeof r === "string");
167
+ }
168
+ if (typeof parsed === "object" && parsed !== null) {
169
+ return Object.keys(parsed);
170
+ }
171
+ return null;
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ function relayListsMatch(a, b) {
177
+ if (a.length !== b.length) return false;
178
+ const sa = [...a].sort();
179
+ const sb = [...b].sort();
180
+ for (let i = 0; i < sa.length; i++) if (sa[i] !== sb[i]) return false;
181
+ return true;
182
+ }
183
+ async function resolveRelayChoice(tools, userRelays, onRelayMismatch) {
184
+ if (!onRelayMismatch) return userRelays;
185
+ const bunkerRelays = await fetchBunkerRelays(tools);
186
+ if (!bunkerRelays || relayListsMatch(userRelays, bunkerRelays)) return userRelays;
187
+ const accept = await onRelayMismatch({ userRelays, bunkerRelays });
188
+ return accept ? bunkerRelays : userRelays;
189
+ }
190
+ async function connectWithBunkerUri(uri, options = {}) {
191
+ const pointer = await parseBunkerInput(uri);
192
+ if (!pointer) {
193
+ throw new Error("@formstr/signer: invalid bunker URI");
194
+ }
195
+ if (!pointer.relays?.length) {
196
+ throw new Error("@formstr/signer: bunker URI must include at least one relay");
197
+ }
198
+ const clientSecretKey = options.clientSecretKey ?? generateSecretKey2();
199
+ const tools = ToolsBunkerSigner.fromBunker(clientSecretKey, pointer, {
200
+ pool: options.pool,
201
+ onauth: options.onAuth
202
+ });
203
+ await tools.sendRequest("connect", [
204
+ pointer.pubkey,
205
+ pointer.secret ?? "",
206
+ (options.perms ?? []).join(",")
207
+ ]);
208
+ const pubkey = await tools.getPublicKey();
209
+ const resolvedRelays = await resolveRelayChoice(
210
+ tools,
211
+ pointer.relays,
212
+ options.onRelayMismatch
213
+ );
214
+ return {
215
+ signer: new BunkerSigner(tools),
216
+ pubkey,
217
+ pointer: { ...pointer, relays: resolvedRelays },
218
+ clientSecretKey
219
+ };
220
+ }
221
+ function initiateNostrConnect(options) {
222
+ if (options.relays.length === 0) {
223
+ throw new Error("@formstr/signer: at least one relay is required for nostrconnect");
224
+ }
225
+ const clientSecretKey = options.clientSecretKey ?? generateSecretKey2();
226
+ const clientPubkey = getPublicKey3(clientSecretKey);
227
+ const secret = options.secret ?? Math.random().toString(36).slice(2);
228
+ const uri = createNostrConnectURI({
229
+ clientPubkey,
230
+ relays: options.relays,
231
+ secret,
232
+ perms: options.perms,
233
+ name: options.metadata?.name,
234
+ url: options.metadata?.url,
235
+ image: options.metadata?.image
236
+ });
237
+ const maxWaitOrAbort = options.signal ?? options.timeoutMs ?? 3e5;
238
+ const complete = ToolsBunkerSigner.fromURI(
239
+ clientSecretKey,
240
+ uri,
241
+ { pool: options.pool, onauth: options.onAuth, skipSwitchRelays: true },
242
+ maxWaitOrAbort
243
+ ).then(async (tools) => {
244
+ const pubkey = await tools.getPublicKey();
245
+ const resolvedRelays = await resolveRelayChoice(
246
+ tools,
247
+ options.relays,
248
+ options.onRelayMismatch
249
+ );
250
+ return {
251
+ signer: new BunkerSigner(tools),
252
+ pubkey,
253
+ pointer: { ...tools.bp, relays: resolvedRelays },
254
+ clientSecretKey
255
+ };
256
+ });
257
+ return { uri, clientPubkey, complete };
258
+ }
259
+ var hexAlphabet = "0123456789abcdef";
260
+ function bytesToHex(bytes) {
261
+ let s = "";
262
+ for (const b of bytes) s += hexAlphabet[b >> 4] + hexAlphabet[b & 15];
263
+ return s;
264
+ }
265
+ function hexToBytes(hex) {
266
+ if (hex.length % 2 !== 0) throw new Error("hexToBytes: odd-length hex string");
267
+ const out = new Uint8Array(hex.length / 2);
268
+ for (let i = 0; i < out.length; i++) {
269
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
270
+ }
271
+ return out;
272
+ }
273
+
274
+ // src/nip55.ts
275
+ import { getEventHash, nip19 as nip192 } from "nostr-tools";
276
+ var AndroidSigner = class {
277
+ #plugin;
278
+ #packageName;
279
+ #npub;
280
+ #pubkey;
281
+ constructor(plugin, packageName, npub, pubkey) {
282
+ this.#plugin = plugin;
283
+ this.#packageName = packageName;
284
+ this.#npub = npub;
285
+ this.#pubkey = pubkey;
286
+ }
287
+ async getPublicKey() {
288
+ return this.#pubkey;
289
+ }
290
+ async signEvent(event) {
291
+ const unsigned = { ...event, pubkey: this.#pubkey };
292
+ const eventId = getEventHash(unsigned);
293
+ const result = await this.#plugin.signEvent(
294
+ this.#packageName,
295
+ JSON.stringify(unsigned),
296
+ eventId,
297
+ this.#npub
298
+ );
299
+ return JSON.parse(result.event);
300
+ }
301
+ async nip04Encrypt(peerPubkey, plaintext) {
302
+ const { result } = await this.#plugin.nip04Encrypt(
303
+ this.#packageName,
304
+ plaintext,
305
+ "",
306
+ peerPubkey,
307
+ this.#npub
308
+ );
309
+ return result;
310
+ }
311
+ async nip04Decrypt(peerPubkey, ciphertext) {
312
+ const { result } = await this.#plugin.nip04Decrypt(
313
+ this.#packageName,
314
+ ciphertext,
315
+ "",
316
+ peerPubkey,
317
+ this.#npub
318
+ );
319
+ return result;
320
+ }
321
+ async nip44Encrypt(peerPubkey, plaintext) {
322
+ const { result } = await this.#plugin.nip44Encrypt(
323
+ this.#packageName,
324
+ plaintext,
325
+ "",
326
+ peerPubkey,
327
+ this.#npub
328
+ );
329
+ return result;
330
+ }
331
+ async nip44Decrypt(peerPubkey, ciphertext) {
332
+ const { result } = await this.#plugin.nip44Decrypt(
333
+ this.#packageName,
334
+ ciphertext,
335
+ "",
336
+ peerPubkey,
337
+ this.#npub
338
+ );
339
+ return result;
340
+ }
341
+ };
342
+ async function loginWithAndroidSigner(plugin, packageName) {
343
+ if (packageName) {
344
+ await plugin.setPackageName(packageName);
345
+ }
346
+ const { npub, package: pluginPackage } = await plugin.getPublicKey(packageName);
347
+ const resolvedPackage = pluginPackage || packageName;
348
+ if (!resolvedPackage) {
349
+ throw new Error(
350
+ "@formstr/signer: android signer did not return a package name and none was supplied"
351
+ );
352
+ }
353
+ const decoded = nip192.decode(npub);
354
+ if (decoded.type !== "npub") {
355
+ throw new Error("@formstr/signer: android signer returned a non-npub identifier");
356
+ }
357
+ return {
358
+ signer: new AndroidSigner(plugin, resolvedPackage, npub, decoded.data),
359
+ pubkey: decoded.data,
360
+ npub,
361
+ packageName: resolvedPackage
362
+ };
363
+ }
364
+
365
+ // src/core/signer.ts
366
+ var ACCOUNTS_KEY = "accounts";
367
+ var ACTIVE_KEY = "active-pubkey";
368
+ var Signer = class {
369
+ #storage;
370
+ #defaultAndroidPlugin;
371
+ #appMetadata;
372
+ #accounts = [];
373
+ #activePubkey = null;
374
+ #activeSigner = null;
375
+ #listeners = /* @__PURE__ */ new Set();
376
+ constructor(config = {}) {
377
+ this.#storage = config.storage ?? localStorageAdapter(config.storageKeyPrefix);
378
+ this.#defaultAndroidPlugin = config.androidSignerPlugin;
379
+ this.#appMetadata = {
380
+ name: config.appName,
381
+ url: config.appUrl,
382
+ image: config.appImage
383
+ };
384
+ this.#hydrate();
385
+ }
386
+ #hydrate() {
387
+ try {
388
+ const raw = this.#storage.get(ACCOUNTS_KEY);
389
+ if (raw) {
390
+ const parsed = JSON.parse(raw);
391
+ if (Array.isArray(parsed)) this.#accounts = parsed;
392
+ }
393
+ this.#activePubkey = this.#storage.get(ACTIVE_KEY);
394
+ } catch {
395
+ this.#accounts = [];
396
+ this.#activePubkey = null;
397
+ }
398
+ }
399
+ #persistAccounts() {
400
+ this.#storage.set(ACCOUNTS_KEY, JSON.stringify(this.#accounts));
401
+ }
402
+ #persistActive() {
403
+ if (this.#activePubkey) this.#storage.set(ACTIVE_KEY, this.#activePubkey);
404
+ else this.#storage.remove(ACTIVE_KEY);
405
+ }
406
+ #upsertAccount(account) {
407
+ const idx = this.#accounts.findIndex((a) => a.pubkey === account.pubkey);
408
+ if (idx >= 0) this.#accounts[idx] = account;
409
+ else this.#accounts.push(account);
410
+ this.#persistAccounts();
411
+ }
412
+ #setActive(account, signer) {
413
+ const wasDifferent = this.#activePubkey !== null && this.#activePubkey !== account.pubkey;
414
+ this.#activePubkey = account.pubkey;
415
+ this.#activeSigner = signer;
416
+ this.#persistActive();
417
+ this.#emit({ type: wasDifferent ? "switch" : "login", account });
418
+ }
419
+ #emit(event) {
420
+ for (const cb of this.#listeners) {
421
+ try {
422
+ cb(event);
423
+ } catch {
424
+ }
425
+ }
426
+ }
427
+ /**
428
+ * Generate a brand-new nsec, encrypt it with `passphrase` (NIP-49),
429
+ * persist the resulting `ncryptsec` account, and activate it. Returns
430
+ * the new account's `npub` and `ncryptsec` — the caller must surface
431
+ * the `ncryptsec` to the user **immediately** since it is the only way
432
+ * back into the account on a fresh device.
433
+ *
434
+ * @throws if `passphrase` is empty.
435
+ */
436
+ async createAccount(passphrase) {
437
+ if (!passphrase) throw new Error("createAccount: passphrase required");
438
+ const { secretKey, pubkey, npub, ncryptsec } = generateAccount(passphrase);
439
+ const account = { npub, pubkey, method: "ncryptsec", ncryptsec };
440
+ this.#upsertAccount(account);
441
+ this.#setActive(account, new LocalSigner(secretKey));
442
+ return { npub, ncryptsec };
443
+ }
444
+ /**
445
+ * Decrypt an ncryptsec with the user's passphrase, persist the account
446
+ * (overwriting any previous entry for the same pubkey), and activate it.
447
+ *
448
+ * @throws if either argument is empty, or if the passphrase doesn't
449
+ * decrypt the ncryptsec.
450
+ */
451
+ async loginWithNcryptsec(ncryptsec, passphrase) {
452
+ if (!ncryptsec) throw new Error("loginWithNcryptsec: ncryptsec required");
453
+ if (!passphrase) throw new Error("loginWithNcryptsec: passphrase required");
454
+ const secretKey = decryptNcryptsec(ncryptsec, passphrase);
455
+ const pubkey = getPublicKey4(secretKey);
456
+ const npub = nip193.npubEncode(pubkey);
457
+ const account = { npub, pubkey, method: "ncryptsec", ncryptsec };
458
+ this.#upsertAccount(account);
459
+ this.#setActive(account, new LocalSigner(secretKey));
460
+ return account;
461
+ }
462
+ /**
463
+ * Connect via the NIP-07 browser extension exposed at `window.nostr`.
464
+ * The extension prompts the user for permission on first use.
465
+ *
466
+ * @throws if no extension is installed or the user denies the request.
467
+ */
468
+ async loginWithExtension() {
469
+ const extension = new ExtensionSigner();
470
+ const pubkey = await extension.getPublicKey();
471
+ const npub = nip193.npubEncode(pubkey);
472
+ const account = { npub, pubkey, method: "extension" };
473
+ this.#upsertAccount(account);
474
+ this.#setActive(account, extension);
475
+ return account;
476
+ }
477
+ /**
478
+ * Connect to a NIP-46 remote signer via a `bunker://` URI. Relays are
479
+ * read from the URI itself — no hardcoded fallbacks. Pass a `pool`
480
+ * to reuse an existing relay connection; pass `clientSecretKey` to
481
+ * resume a previous session (the hex from `StoredAccount.nip46`).
482
+ *
483
+ * @throws if the URI is malformed, no relay is reachable, or the
484
+ * remote signer rejects pairing within the implementation's timeout.
485
+ */
486
+ async loginWithBunkerUri(uri, options = {}) {
487
+ const result = await connectWithBunkerUri(uri, options);
488
+ const npub = nip193.npubEncode(result.pubkey);
489
+ const account = {
490
+ npub,
491
+ pubkey: result.pubkey,
492
+ method: "nip46",
493
+ nip46: {
494
+ uri,
495
+ remoteSignerPubkey: result.pointer.pubkey,
496
+ relays: result.pointer.relays,
497
+ clientSecretKey: bytesToHex(result.clientSecretKey)
498
+ }
499
+ };
500
+ this.#upsertAccount(account);
501
+ this.#setActive(account, result.signer);
502
+ return account;
503
+ }
504
+ /**
505
+ * Initiate a NIP-46 `nostrconnect://` pairing. Generates a client
506
+ * keypair, publishes a connect request to the supplied `relays`, and
507
+ * waits for a remote signer to pair. Call `options.onUri(uri)` to
508
+ * render the URI as a QR code; the returned promise resolves once
509
+ * pairing completes. Cancel by aborting `options.signal`.
510
+ *
511
+ * @throws if `relays` is empty, the user aborts, the pairing times
512
+ * out, or no signer responds.
513
+ */
514
+ async loginWithNostrConnect(options) {
515
+ if (options.relays.length === 0) {
516
+ throw new Error("loginWithNostrConnect: at least one relay required");
517
+ }
518
+ const metadata = {
519
+ name: options.metadata?.name ?? this.#appMetadata.name,
520
+ url: options.metadata?.url ?? this.#appMetadata.url,
521
+ image: options.metadata?.image ?? this.#appMetadata.image
522
+ };
523
+ if (!metadata.name) {
524
+ throw new Error(
525
+ "@formstr/signer: loginWithNostrConnect requires an app name. Set `appName` in createSigner() or pass `metadata.name` to loginWithNostrConnect()."
526
+ );
527
+ }
528
+ const init = initiateNostrConnect({
529
+ relays: options.relays,
530
+ metadata,
531
+ perms: options.perms,
532
+ pool: options.pool,
533
+ onAuth: options.onAuth,
534
+ signal: options.signal,
535
+ timeoutMs: options.timeoutMs,
536
+ onRelayMismatch: options.onRelayMismatch
537
+ });
538
+ options.onUri(init.uri);
539
+ const result = await init.complete;
540
+ const npub = nip193.npubEncode(result.pubkey);
541
+ const account = {
542
+ npub,
543
+ pubkey: result.pubkey,
544
+ method: "nip46",
545
+ nip46: {
546
+ uri: init.uri,
547
+ remoteSignerPubkey: result.pointer.pubkey,
548
+ relays: result.pointer.relays,
549
+ clientSecretKey: bytesToHex(result.clientSecretKey)
550
+ }
551
+ };
552
+ this.#upsertAccount(account);
553
+ this.#setActive(account, result.signer);
554
+ return account;
555
+ }
556
+ /**
557
+ * Enumerate NIP-55 signer apps installed on the device, via the
558
+ * configured Android plugin (or `plugin` if supplied). Useful for
559
+ * rendering a "pick your signer" list — the built-in UI does this
560
+ * automatically when the Android tab is selected.
561
+ *
562
+ * Only meaningful inside a Capacitor Android shell. On web/iOS the
563
+ * configured plugin is typically absent and this throws.
564
+ *
565
+ * @throws if no plugin is configured and none is passed in.
566
+ */
567
+ async listAndroidSignerApps(plugin) {
568
+ const p = plugin ?? this.#defaultAndroidPlugin;
569
+ if (!p) {
570
+ throw new Error(
571
+ "@formstr/signer: no Android signer plugin configured (pass `androidSignerPlugin` to createSigner or `plugin` to listAndroidSignerApps)"
572
+ );
573
+ }
574
+ const { apps } = await p.getInstalledSignerApps();
575
+ return apps;
576
+ }
577
+ /**
578
+ * Sign in via a NIP-55 Android external signer (Amber, etc). If
579
+ * `options.packageName` is given, that specific signer app is invoked;
580
+ * otherwise the plugin picks a default (typically the only installed
581
+ * signer, or an OS chooser). Pass `options.plugin` to override the
582
+ * configured default for this call.
583
+ *
584
+ * @throws if no plugin is configured, the signer app cannot be
585
+ * resolved to a package name, or the user denies the request.
586
+ */
587
+ async loginWithAndroidSigner(options = {}) {
588
+ const plugin = options.plugin ?? this.#defaultAndroidPlugin;
589
+ if (!plugin) {
590
+ throw new Error(
591
+ "@formstr/signer: no Android signer plugin configured (pass `androidSignerPlugin` to createSigner or `plugin` to loginWithAndroidSigner)"
592
+ );
593
+ }
594
+ const result = await loginWithAndroidSigner(plugin, options.packageName);
595
+ const account = {
596
+ npub: result.npub,
597
+ pubkey: result.pubkey,
598
+ method: "android",
599
+ androidPackageName: result.packageName
600
+ };
601
+ this.#upsertAccount(account);
602
+ this.#setActive(account, result.signer);
603
+ return account;
604
+ }
605
+ /** Snapshot of every persisted account, in insertion order. */
606
+ listAccounts() {
607
+ return [...this.#accounts];
608
+ }
609
+ /**
610
+ * The currently selected account, or `null` if none. Present even when
611
+ * the account is locked (no active signer yet). Use this to render
612
+ * "logged in as @alice" — pair with {@link getActiveSigner} to decide
613
+ * whether signing is actually available.
614
+ */
615
+ getActiveAccount() {
616
+ if (!this.#activePubkey) return null;
617
+ return this.#accounts.find((a) => a.pubkey === this.#activePubkey) ?? null;
618
+ }
619
+ /**
620
+ * The unlocked signer for the active account, or `null` if locked.
621
+ * After a fresh page load this is `null` for every account type
622
+ * (passphrase / extension grant / signer-app handshake all need to
623
+ * be redone). Calling the matching `loginWith*` method unlocks it.
624
+ */
625
+ getActiveSigner() {
626
+ return this.#activeSigner;
627
+ }
628
+ /**
629
+ * Make `pubkey` the active account. Clears the in-memory signer —
630
+ * the new account starts **locked** even if it was previously
631
+ * unlocked in this session.
632
+ *
633
+ * @throws if `pubkey` does not match any persisted account.
634
+ */
635
+ async switchAccount(pubkey) {
636
+ const account = this.#accounts.find((a) => a.pubkey === pubkey);
637
+ if (!account) throw new Error(`switchAccount: no account for pubkey ${pubkey}`);
638
+ this.#activePubkey = pubkey;
639
+ this.#activeSigner = null;
640
+ this.#persistActive();
641
+ this.#emit({ type: "switch", account });
642
+ }
643
+ /**
644
+ * Remove an account from storage. `pubkey` defaults to the active
645
+ * account. If the active account is removed, the in-memory signer is
646
+ * cleared. No-op if there is nothing to remove.
647
+ */
648
+ async logout(pubkey) {
649
+ const target = pubkey ?? this.#activePubkey;
650
+ if (!target) return;
651
+ this.#accounts = this.#accounts.filter((a) => a.pubkey !== target);
652
+ this.#persistAccounts();
653
+ if (this.#activePubkey === target) {
654
+ this.#activePubkey = null;
655
+ this.#activeSigner = null;
656
+ this.#persistActive();
657
+ }
658
+ this.#emit({ type: "logout", pubkey: target });
659
+ }
660
+ /**
661
+ * Subscribe to account-state changes. Returns an unsubscribe function.
662
+ * Listener errors are swallowed so one bad listener can't break others.
663
+ * See {@link SignerEvent} for the variants.
664
+ */
665
+ onChange(cb) {
666
+ this.#listeners.add(cb);
667
+ return () => {
668
+ this.#listeners.delete(cb);
669
+ };
670
+ }
671
+ };
672
+ function createSigner(config = {}) {
673
+ return new Signer(config);
674
+ }
675
+ export {
676
+ AndroidSigner,
677
+ BunkerSigner,
678
+ ExtensionSigner,
679
+ LocalSigner,
680
+ Signer,
681
+ bytesToHex,
682
+ connectWithBunkerUri,
683
+ createSigner,
684
+ decryptNcryptsec,
685
+ encryptSecretKey,
686
+ generateAccount,
687
+ getWindowNostr,
688
+ hexToBytes,
689
+ initiateNostrConnect,
690
+ localStorageAdapter,
691
+ loginWithAndroidSigner
692
+ };
693
+ //# sourceMappingURL=index.js.map