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