@formstr/signer 0.3.0 → 0.3.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 CHANGED
@@ -166,28 +166,56 @@ This is why the transport interface has `readClipboard`/`writeClipboard` but no
166
166
  This flow is for a plain browser. Inside a Capacitor native shell the same
167
167
  device already has the real NIP-55 plugin, which supports every method and
168
168
  needs neither the clipboard nor a per-operation approval — so this path is
169
- disabled there (Capacitor injects a `Capacitor.isNativePlatform()` global,
169
+ hidden there (Capacitor injects a `Capacitor.isNativePlatform()` global,
170
170
  which the package checks without depending on `@capacitor/core`).
171
171
 
172
- Gate your UI on `signer.supportsNip55Web()`:
172
+ Gate your UI on `signer.nip55WebSupport()`:
173
173
 
174
174
  ```ts
175
175
  container.innerHTML = renderLoginHtml({
176
- includeNip55Web: signer.supportsNip55Web(),
176
+ nip55Web: signer.nip55WebSupport(),
177
177
  });
178
178
  ```
179
179
 
180
- In a native build that omits the tab and `loginWithNip55Web()` throws a
181
- message pointing at `loginWithAndroidSigner()`.
180
+ That returns `{ visible, warning?, reason? }`:
182
181
 
183
- `supportsNip55Web()` is a **capability** check, not an availability one:
184
- there is no web API to detect an installed Android app, so it cannot tell
185
- you whether a signer app is actually present. It answers "could this flow
186
- possibly work here", not "will it succeed".
182
+ | Environment | `visible` | `warning` |
183
+ | --- | --- | --- |
184
+ | Android Chrome / Brave | `true` | |
185
+ | Android Firefox / Focus / Klar | `true` | "May not work in Firefox for Android…" |
186
+ | Capacitor native shell | `false` | — |
187
+ | Desktop / iOS | `false` | — |
188
+
189
+ Nothing is hard-blocked: Firefox is shown **with a warning** so users can
190
+ still try, and so they can see why their browser differs from Chrome on the
191
+ same device. `loginWithNip55Web()` in a native build throws a message
192
+ pointing at `loginWithAndroidSigner()`.
193
+
194
+ `supportsNip55Web()` remains as a boolean shorthand for `.visible`.
195
+ Both are **capability** checks, not availability ones: there is no web API
196
+ to detect an installed Android app, so they cannot tell you whether a signer
197
+ app is actually present. They answer "could this flow possibly work here",
198
+ not "will it succeed".
199
+
200
+ ### Firefox for Android
201
+
202
+ Firefox (and Focus/Klar) is the one browser that advertises
203
+ `navigator.clipboard.readText` yet cannot run this flow. It never grants a
204
+ persistent `clipboard-read` permission, so **every** read requires transient
205
+ user activation and is answered with an ephemeral "Paste" menu. A background
206
+ poll therefore cannot succeed.
207
+
208
+ Verified on an emulator (Firefox 125): calling `readText()` directly inside a
209
+ click handler — `hasFocus=true`, `visibilityState=visible`,
210
+ `userActivation.isActive=true` — still rejects with
211
+ `NotAllowedError: Clipboard read operation is not allowed`.
212
+
213
+ Because browser behaviour changes and blocking a whole browser is a worse
214
+ failure than a warning, the option stays visible and attemptable there.
187
215
 
188
216
  ### What to know
189
217
 
190
- - **Android + secure context only.** `supportsNip55Web()` requires an Android user agent, the async clipboard API, and not being in a native shell. `loginWithNip55Web()` throws before persisting anything when unsupported. `http://localhost` counts as a secure context, which is handy for local testing.
218
+ - **Android + secure context only.** `nip55WebSupport().visible` requires an Android user agent, the async clipboard API, and not being in a native shell. `loginWithNip55Web()` throws before persisting anything when not visible. `http://localhost` counts as a secure context, which is handy for local testing.
191
219
  - **Chrome will ask to read the clipboard** the first time; approve it, or every read fails.
192
220
  - **One approval per operation.** There is no background channel, so `getPublicKey`, every `signEvent`, and every `nip04`/`nip44` call re-opens the app. `unlock()` resumes the account from its cached pubkey without opening the app; the first real signing call prompts.
193
221
  - **No rejection signal, but there is a timeout.** NIP-55's reject path is an Android intent extra a browser never sees, so a denial is indistinguishable from the user never returning. The package therefore times requests out (`timeoutMs`, default 120s; `0` disables) and rejects with a clear message. Pass a `signal` to cancel yourself — aborting rejects with `name === 'AbortError'`, matching the NIP-46 flow.
@@ -263,7 +291,7 @@ const detach = attachLoginListeners(container, signer, {
263
291
  // later: detach();
264
292
  ```
265
293
 
266
- The login modal renders one tab per method (Create, Existing key, Extension, Bunker URI, Remote QR, Signer app, Android). The Android tab is always rendered but its list of installed signers is fetched lazily on activation via `signer.listAndroidSignerApps()` — it errors clearly if no Android plugin is configured (e.g. when running on web). The Signer app tab drives `loginWithNip55Web()` and needs no plugin; pass `renderLoginHtml({ includeNip55Web: signer.supportsNip55Web() })` to hide it in native builds.
294
+ The login modal renders one tab per method (Create, Existing key, Extension, Bunker URI, Remote QR, Signer app, Android). The Android tab is always rendered but its list of installed signers is fetched lazily on activation via `signer.listAndroidSignerApps()` — it errors clearly if no Android plugin is configured (e.g. when running on web). The Signer app tab drives `loginWithNip55Web()` and needs no plugin; pass `renderLoginHtml({ nip55Web: signer.nip55WebSupport() })` to hide it in native builds and to show Firefox's warning.
267
295
 
268
296
  ## Errors
269
297
 
@@ -372,5 +400,6 @@ The UI ships with these class names. Override in your own CSS.
372
400
  | `.nostr-signer__button--primary` | primary button |
373
401
  | `.nostr-signer__button--secondary` | secondary button |
374
402
  | `.nostr-signer__error` | error message |
403
+ | `.nostr-signer__warn` | advisory (non-blocking) warning |
375
404
  | `.nostr-signer__status` | status / loading message |
376
405
  | `.nostr-signer__hint` | hint text |
package/dist/index.cjs CHANGED
@@ -454,10 +454,36 @@ function isNativeShell() {
454
454
  const cap = globalThis.Capacitor;
455
455
  return typeof cap?.isNativePlatform === "function" && cap.isNativePlatform();
456
456
  }
457
+ function isGeckoBrowser() {
458
+ const ua = navigator.userAgent;
459
+ return /Firefox\/|FxiOS\/|Focus\/|Gecko\//.test(ua);
460
+ }
457
461
  function browserNip55Transport() {
458
462
  return {
463
+ supportStatus() {
464
+ if (typeof navigator === "undefined") {
465
+ return { visible: false, reason: "not-android" };
466
+ }
467
+ if (isNativeShell()) {
468
+ return { visible: false, reason: "native" };
469
+ }
470
+ if (!/Android/i.test(navigator.userAgent)) {
471
+ return { visible: false, reason: "not-android" };
472
+ }
473
+ if (typeof navigator.clipboard?.readText !== "function") {
474
+ return { visible: false, reason: "no-clipboard" };
475
+ }
476
+ if (isGeckoBrowser()) {
477
+ return {
478
+ visible: true,
479
+ reason: "firefox",
480
+ warning: "May not work in Firefox for Android \u2014 the clipboard usually cannot be read automatically. If it hangs, try Chrome, Brave, or the Android app."
481
+ };
482
+ }
483
+ return { visible: true };
484
+ },
459
485
  isSupported() {
460
- return typeof navigator !== "undefined" && !isNativeShell() && /Android/i.test(navigator.userAgent) && typeof navigator.clipboard?.readText === "function";
486
+ return this.supportStatus().visible;
461
487
  },
462
488
  open(intent) {
463
489
  window.open(intent, "_blank");
@@ -493,6 +519,16 @@ var Nip55WebSigner = class _Nip55WebSigner {
493
519
  isSupported() {
494
520
  return this.#transport.isSupported();
495
521
  }
522
+ /**
523
+ * Whether to offer the option and whether it works. Prefer this over
524
+ * {@link isSupported} so a host can surface `message` — notably the
525
+ * Firefox case, which is shown but cannot complete.
526
+ */
527
+ supportStatus() {
528
+ const status = this.#transport.supportStatus;
529
+ if (status) return status.call(this.#transport);
530
+ return { visible: this.#transport.isSupported() };
531
+ }
496
532
  async getPublicKey() {
497
533
  if (this.#pubkey !== null) return this.#pubkey;
498
534
  this.#checkSupport();
@@ -548,8 +584,9 @@ var Nip55WebSigner = class _Nip55WebSigner {
548
584
  }
549
585
  }
550
586
  #checkSupport() {
551
- if (this.#transport.isSupported()) return;
552
- if (isNativeShell()) {
587
+ const { visible, reason } = this.supportStatus();
588
+ if (visible) return;
589
+ if (reason === "native") {
553
590
  throw new Error(
554
591
  "@formstr/signer: the browser NIP-55 flow is not for native builds \u2014 use loginWithAndroidSigner() with the Capacitor plugin instead"
555
592
  );
@@ -954,8 +991,23 @@ var Signer = class {
954
991
  * browser flow where it cannot work, not to promise that it will.
955
992
  */
956
993
  supportsNip55Web(transport) {
994
+ return this.nip55WebSupport(transport).visible;
995
+ }
996
+ /**
997
+ * Whether to offer the browser NIP-55 option, plus an advisory `warning`
998
+ * where it is known to be flaky. Prefer this in UI code.
999
+ *
1000
+ * Nothing here is a hard block. `visible: false` covers only environments
1001
+ * where the mechanism cannot exist — a Capacitor native shell (the plugin
1002
+ * path is better) and non-Android platforms (no `nostrsigner` handler).
1003
+ * Firefox for Android is `visible: true` **with a warning**, because it
1004
+ * advertises `readText` but never grants a persistent permission, so the
1005
+ * poll loop usually cannot complete. Users are still allowed to try.
1006
+ */
1007
+ nip55WebSupport(transport) {
957
1008
  const t = transport ?? this.#nip55WebTransport ?? browserNip55Transport();
958
- return t.isSupported();
1009
+ if (t.supportStatus) return t.supportStatus();
1010
+ return { visible: t.isSupported() };
959
1011
  }
960
1012
  /**
961
1013
  * Sign in via a NIP-55 Android external signer (Amber, etc) **from a
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/core/signer.ts","../src/core/storage.ts","../src/core/localSigner.ts","../src/nip49.ts","../src/nip07.ts","../src/nip46.ts","../src/nip55.ts","../src/nip55Web.ts"],"sourcesContent":["export { Signer, createSigner } from './core/signer.js';\nexport { LocalSigner } from './core/localSigner.js';\nexport { localStorageAdapter } from './core/storage.js';\nexport type { StorageAdapter } from './core/storage.js';\nexport type {\n SignerConfig,\n ActiveSigner,\n StoredAccount,\n LoginMethod,\n SignerEvent,\n NostrConnectOptions,\n BunkerLoginOptions,\n Nip55WebLoginOptions,\n RelayMismatchInfo,\n RelayMismatchHandler,\n UnlockOptions,\n} from './core/types.js';\nexport {\n encryptSecretKey,\n decryptNcryptsec,\n generateAccount,\n type GeneratedAccount,\n} from './nip49.js';\nexport { ExtensionSigner, getWindowNostr, type WindowNostr } from './nip07.js';\nexport {\n AndroidSigner,\n loginWithAndroidSigner,\n normalizeNip55Identifier,\n type AndroidSignerPlugin,\n type AndroidSignerAppInfo,\n type AndroidLoginOptions,\n type AndroidLoginResult,\n} from './nip55.js';\nexport {\n Nip55WebSigner,\n browserNip55Transport,\n type Nip55WebOptions,\n type Nip55WebTransport,\n} from './nip55Web.js';\nexport {\n BunkerSigner,\n connectWithBunkerUri,\n initiateNostrConnect,\n bytesToHex,\n hexToBytes,\n type BunkerPointer,\n type BunkerLoginOptions as BunkerConnectOptions,\n type BunkerConnectResult,\n type NostrConnectInitOptions,\n type NostrConnectInitiation,\n} from './nip46.js';\n","import { getPublicKey, nip19 } from 'nostr-tools';\nimport { BunkerSigner as ToolsBunkerSigner } from 'nostr-tools/nip46';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type {\n ActiveSigner,\n BunkerLoginOptions,\n Nip55WebLoginOptions,\n NostrConnectOptions,\n SignerConfig,\n SignerEvent,\n StoredAccount,\n UnlockOptions,\n} from './types.js';\nimport { localStorageAdapter, type StorageAdapter } from './storage.js';\nimport { LocalSigner } from './localSigner.js';\nimport { decryptNcryptsec, generateAccount } from '../nip49.js';\nimport { ExtensionSigner } from '../nip07.js';\nimport {\n BunkerSigner,\n bytesToHex,\n connectWithBunkerUri,\n hexToBytes,\n initiateNostrConnect,\n} from '../nip46.js';\nimport {\n AndroidSigner,\n loginWithAndroidSigner as connectWithAndroidSigner,\n type AndroidLoginOptions,\n type AndroidSignerAppInfo,\n type AndroidSignerPlugin,\n} from '../nip55.js';\nimport {\n Nip55WebSigner,\n browserNip55Transport,\n type Nip55WebTransport,\n} from '../nip55Web.js';\n\nconst ACCOUNTS_KEY = 'accounts';\nconst ACTIVE_KEY = 'active-pubkey';\n\n/**\n * Multi-account Nostr signer with persistence.\n *\n * **Hydration.** The constructor reads previously-saved accounts from\n * the configured storage adapter. Every hydrated account starts\n * **locked**: present in `listAccounts()` and (if it was the active one\n * before) reachable via `getActiveAccount()`, but `getActiveSigner()`\n * returns `null` until the user re-authenticates. The matching\n * `loginWith*` method unlocks the active account.\n *\n * **Locked vs unlocked.** Use `getActiveAccount()` to render UI (\"logged\n * in as @alice\") and `getActiveSigner()` to decide whether the user can\n * actually sign. The pattern is \"show the account always, gate signing\n * on the signer.\"\n *\n * **Events.** Subscribe via `onChange()` to re-render when an account\n * is added, switched, or removed. See {@link SignerEvent}.\n */\nexport class Signer {\n readonly #storage: StorageAdapter;\n readonly #defaultAndroidPlugin: AndroidSignerPlugin | undefined;\n readonly #nip55WebTransport: Nip55WebTransport | undefined;\n readonly #appMetadata: { name?: string; url?: string; image?: string };\n #accounts: StoredAccount[] = [];\n #activePubkey: string | null = null;\n #activeSigner: ActiveSigner | null = null;\n #listeners = new Set<(event: SignerEvent) => void>();\n\n constructor(config: SignerConfig = {}) {\n this.#storage = config.storage ?? localStorageAdapter(config.storageKeyPrefix);\n this.#defaultAndroidPlugin = config.androidSignerPlugin;\n this.#nip55WebTransport = config.nip55WebTransport;\n this.#appMetadata = {\n name: config.appName,\n url: config.appUrl,\n image: config.appImage,\n };\n this.#hydrate();\n }\n\n #hydrate(): void {\n try {\n const raw = this.#storage.get(ACCOUNTS_KEY);\n if (raw) {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed)) this.#accounts = parsed as StoredAccount[];\n }\n this.#activePubkey = this.#storage.get(ACTIVE_KEY);\n } catch {\n this.#accounts = [];\n this.#activePubkey = null;\n }\n }\n\n #persistAccounts(): void {\n this.#storage.set(ACCOUNTS_KEY, JSON.stringify(this.#accounts));\n }\n\n #persistActive(): void {\n if (this.#activePubkey) this.#storage.set(ACTIVE_KEY, this.#activePubkey);\n else this.#storage.remove(ACTIVE_KEY);\n }\n\n #upsertAccount(account: StoredAccount): void {\n const idx = this.#accounts.findIndex(a => a.pubkey === account.pubkey);\n if (idx >= 0) this.#accounts[idx] = account;\n else this.#accounts.push(account);\n this.#persistAccounts();\n }\n\n /**\n * Release the currently-held signer, if it has a `close()`. Called\n * whenever the active signer is replaced or cleared so live resources\n * (bunker subscriptions, `visibilitychange` listeners, in-flight NIP-55\n * requests) don't outlive the session. Errors are swallowed — teardown\n * must never block a login/switch/logout.\n */\n #closeActiveSigner(): void {\n const signer = this.#activeSigner;\n if (!signer?.close) return;\n try {\n const result = signer.close();\n // close() may be async (BunkerSigner tears down relay subscriptions).\n if (result && typeof (result as Promise<void>).catch === 'function') {\n (result as Promise<void>).catch(() => {\n // best-effort teardown\n });\n }\n } catch {\n // best-effort teardown — never block a login/switch/logout\n }\n }\n\n #setActive(account: StoredAccount, signer: ActiveSigner): void {\n const wasDifferent = this.#activePubkey !== null && this.#activePubkey !== account.pubkey;\n // Close the outgoing signer only when it is actually being replaced —\n // re-asserting the same account (e.g. unlock) would otherwise tear\n // down the very signer we are handing back.\n if (this.#activeSigner !== signer) this.#closeActiveSigner();\n this.#activePubkey = account.pubkey;\n this.#activeSigner = signer;\n this.#persistActive();\n this.#emit({ type: wasDifferent ? 'switch' : 'login', account });\n }\n\n #emit(event: SignerEvent): void {\n for (const cb of this.#listeners) {\n try {\n cb(event);\n } catch {\n // listener errors are swallowed so one bad listener can't break others\n }\n }\n }\n\n /**\n * Generate a brand-new nsec, encrypt it with `passphrase` (NIP-49),\n * persist the resulting `ncryptsec` account, and activate it. Returns\n * the new account's `npub` and `ncryptsec` — the caller must surface\n * the `ncryptsec` to the user **immediately** since it is the only way\n * back into the account on a fresh device.\n *\n * @throws if `passphrase` is empty.\n */\n async createAccount(passphrase: string): Promise<{ npub: string; ncryptsec: string }> {\n if (!passphrase) throw new Error('createAccount: passphrase required');\n const { secretKey, pubkey, npub, ncryptsec } = generateAccount(passphrase);\n const account: StoredAccount = { npub, pubkey, method: 'ncryptsec', ncryptsec };\n this.#upsertAccount(account);\n this.#setActive(account, new LocalSigner(secretKey));\n return { npub, ncryptsec };\n }\n\n /**\n * Decrypt an ncryptsec with the user's passphrase, persist the account\n * (overwriting any previous entry for the same pubkey), and activate it.\n *\n * @throws if either argument is empty, or if the passphrase doesn't\n * decrypt the ncryptsec.\n */\n async loginWithNcryptsec(ncryptsec: string, passphrase: string): Promise<StoredAccount> {\n if (!ncryptsec) throw new Error('loginWithNcryptsec: ncryptsec required');\n if (!passphrase) throw new Error('loginWithNcryptsec: passphrase required');\n const secretKey = decryptNcryptsec(ncryptsec, passphrase);\n const pubkey = getPublicKey(secretKey);\n const npub = nip19.npubEncode(pubkey);\n const account: StoredAccount = { npub, pubkey, method: 'ncryptsec', ncryptsec };\n this.#upsertAccount(account);\n this.#setActive(account, new LocalSigner(secretKey));\n return account;\n }\n\n /**\n * Connect via the NIP-07 browser extension exposed at `window.nostr`.\n * The extension prompts the user for permission on first use.\n *\n * @throws if no extension is installed or the user denies the request.\n */\n async loginWithExtension(): Promise<StoredAccount> {\n const extension = new ExtensionSigner();\n const pubkey = await extension.getPublicKey();\n const npub = nip19.npubEncode(pubkey);\n const account: StoredAccount = { npub, pubkey, method: 'extension' };\n this.#upsertAccount(account);\n this.#setActive(account, extension);\n return account;\n }\n\n /**\n * Connect to a NIP-46 remote signer via a `bunker://` URI. Relays are\n * read from the URI itself — no hardcoded fallbacks. Pass a `pool`\n * to reuse an existing relay connection; pass `clientSecretKey` to\n * resume a previous session (the hex from `StoredAccount.nip46`).\n *\n * @throws if the URI is malformed, no relay is reachable, or the\n * remote signer rejects pairing within the implementation's timeout.\n */\n async loginWithBunkerUri(\n uri: string,\n options: BunkerLoginOptions = {},\n ): Promise<StoredAccount> {\n const result = await connectWithBunkerUri(uri, options);\n const npub = nip19.npubEncode(result.pubkey);\n const account: StoredAccount = {\n npub,\n pubkey: result.pubkey,\n method: 'nip46',\n nip46: {\n uri,\n remoteSignerPubkey: result.pointer.pubkey,\n relays: result.pointer.relays,\n clientSecretKey: bytesToHex(result.clientSecretKey),\n },\n };\n this.#upsertAccount(account);\n this.#setActive(account, result.signer);\n return account;\n }\n\n /**\n * Initiate a NIP-46 `nostrconnect://` pairing. Generates a client\n * keypair, publishes a connect request to the supplied `relays`, and\n * waits for a remote signer to pair. Call `options.onUri(uri)` to\n * render the URI as a QR code; the returned promise resolves once\n * pairing completes. Cancel by aborting `options.signal`.\n *\n * @throws if `relays` is empty, the user aborts, the pairing times\n * out, or no signer responds.\n */\n async loginWithNostrConnect(options: NostrConnectOptions): Promise<StoredAccount> {\n if (options.relays.length === 0) {\n throw new Error('loginWithNostrConnect: at least one relay required');\n }\n // Merge per-call metadata over SignerConfig defaults (appName/url/image).\n // `name` is required — Amber (and likely other NIP-55 signer apps) gate\n // the consent UI on having a recognizable client identity in the URI.\n // Without one they will receive the request but never surface\n // approve/deny buttons, leaving the pairing silently stuck.\n const metadata = {\n name: options.metadata?.name ?? this.#appMetadata.name,\n url: options.metadata?.url ?? this.#appMetadata.url,\n image: options.metadata?.image ?? this.#appMetadata.image,\n };\n if (!metadata.name) {\n throw new Error(\n '@formstr/signer: loginWithNostrConnect requires an app name. Set `appName` in createSigner() or pass `metadata.name` to loginWithNostrConnect().',\n );\n }\n const init = initiateNostrConnect({\n relays: options.relays,\n metadata,\n perms: options.perms,\n pool: options.pool,\n onAuth: options.onAuth,\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n onRelayMismatch: options.onRelayMismatch,\n });\n options.onUri(init.uri);\n const result = await init.complete;\n const npub = nip19.npubEncode(result.pubkey);\n const account: StoredAccount = {\n npub,\n pubkey: result.pubkey,\n method: 'nip46',\n nip46: {\n uri: init.uri,\n remoteSignerPubkey: result.pointer.pubkey,\n relays: result.pointer.relays,\n clientSecretKey: bytesToHex(result.clientSecretKey),\n },\n };\n this.#upsertAccount(account);\n this.#setActive(account, result.signer);\n return account;\n }\n\n /**\n * Enumerate NIP-55 signer apps installed on the device, via the\n * configured Android plugin (or `plugin` if supplied). Useful for\n * rendering a \"pick your signer\" list — the built-in UI does this\n * automatically when the Android tab is selected.\n *\n * Only meaningful inside a Capacitor Android shell. On web/iOS the\n * configured plugin is typically absent and this throws.\n *\n * @throws if no plugin is configured and none is passed in.\n */\n async listAndroidSignerApps(\n plugin?: AndroidSignerPlugin,\n ): Promise<AndroidSignerAppInfo[]> {\n const p = plugin ?? this.#defaultAndroidPlugin;\n if (!p) {\n throw new Error(\n '@formstr/signer: no Android signer plugin configured (pass `androidSignerPlugin` to createSigner or `plugin` to listAndroidSignerApps)',\n );\n }\n const { apps } = await p.getInstalledSignerApps();\n return apps;\n }\n\n /**\n * Sign in via a NIP-55 Android external signer (Amber, etc). If\n * `options.packageName` is given, that specific signer app is invoked;\n * otherwise the plugin picks a default (typically the only installed\n * signer, or an OS chooser). Pass `options.plugin` to override the\n * configured default for this call.\n *\n * @throws if no plugin is configured, the signer app cannot be\n * resolved to a package name, or the user denies the request.\n */\n async loginWithAndroidSigner(options: AndroidLoginOptions = {}): Promise<StoredAccount> {\n const plugin = options.plugin ?? this.#defaultAndroidPlugin;\n if (!plugin) {\n throw new Error(\n '@formstr/signer: no Android signer plugin configured (pass `androidSignerPlugin` to createSigner or `plugin` to loginWithAndroidSigner)',\n );\n }\n const result = await connectWithAndroidSigner(plugin, options.packageName);\n const account: StoredAccount = {\n npub: result.npub,\n pubkey: result.pubkey,\n method: 'android',\n androidPackageName: result.packageName,\n };\n this.#upsertAccount(account);\n this.#setActive(account, result.signer);\n return account;\n }\n\n /**\n * Whether `loginWithNip55Web` can run in this environment — a plain\n * Android browser with async clipboard access. False in a Capacitor\n * native shell (use {@link loginWithAndroidSigner} there), and on\n * desktop/iOS/SSR.\n *\n * A **capability** check, not an availability one: there is no web API\n * to detect an installed Android app, so this says nothing about\n * whether a signer app is actually installed. Use it to hide the\n * browser flow where it cannot work, not to promise that it will.\n */\n supportsNip55Web(transport?: Nip55WebTransport): boolean {\n const t = transport ?? this.#nip55WebTransport ?? browserNip55Transport();\n return t.isSupported();\n }\n\n /**\n * Sign in via a NIP-55 Android external signer (Amber, etc) **from a\n * plain browser**, with no Capacitor/native bridge. Opens the installed\n * signer app through a `nostrsigner` intent and reads the result back\n * from the clipboard once the user returns to the tab. Because the\n * intent names no package, this works with any app that registered the\n * `nostrsigner` scheme — one opens directly, several show the Android\n * \"Open with\" chooser.\n *\n * Not for native builds — inside a Capacitor shell use\n * {@link loginWithAndroidSigner}, which needs no clipboard and no\n * per-operation approval.\n *\n * Every operation is a separate approval, and a rejection is\n * indistinguishable from the user simply not returning, so callers must\n * impose their own timeout. Prefer NIP-46 when a persistent session is\n * acceptable — the NIP-55 spec recommends it for web clients.\n *\n * @throws if the environment cannot run the flow (not Android, no async\n * clipboard) or the signer returns an unexpected value.\n */\n async loginWithNip55Web(options: Nip55WebLoginOptions = {}): Promise<StoredAccount> {\n const transport = options.transport ?? this.#nip55WebTransport;\n // Pair with a throwaway signer so `options.signal` cancels only the\n // pairing attempt. The signal belongs to the login call, not to the\n // account — retaining it would poison the signer, since hosts commonly\n // abort the login controller once the modal closes on success.\n const pairing = new Nip55WebSigner({\n transport,\n pollIntervalMs: options.pollIntervalMs,\n timeoutMs: options.timeoutMs,\n signal: options.signal,\n debug: options.debug,\n });\n // getPublicKey throws for an unsupported environment (not Android / no\n // async clipboard) or an aborted pairing, before anything is persisted.\n const pubkey = await pairing.getPublicKey();\n const signer = new Nip55WebSigner({\n transport,\n pollIntervalMs: options.pollIntervalMs,\n timeoutMs: options.timeoutMs,\n pubkey,\n debug: options.debug,\n });\n const npub = nip19.npubEncode(pubkey);\n const account: StoredAccount = {\n npub,\n pubkey,\n method: 'nip55-web',\n };\n this.#upsertAccount(account);\n this.#setActive(account, signer);\n return account;\n }\n\n /** Snapshot of every persisted account, in insertion order. */\n listAccounts(): StoredAccount[] {\n return [...this.#accounts];\n }\n\n /**\n * The currently selected account, or `null` if none. Present even when\n * the account is locked (no active signer yet). Use this to render\n * \"logged in as @alice\" — pair with {@link getActiveSigner} to decide\n * whether signing is actually available.\n */\n getActiveAccount(): StoredAccount | null {\n if (!this.#activePubkey) return null;\n return this.#accounts.find(a => a.pubkey === this.#activePubkey) ?? null;\n }\n\n /**\n * The unlocked signer for the active account, or `null` if locked.\n * After a fresh page load this is `null` for every account type\n * (passphrase / extension grant / signer-app handshake all need to\n * be redone). Calling the matching `loginWith*` method unlocks it.\n */\n getActiveSigner(): ActiveSigner | null {\n return this.#activeSigner;\n }\n\n /**\n * Silently unlock the active account from persisted state — no user\n * prompt, no fresh pairing. The package already keeps everything it\n * needs to reconstruct the runtime signer on disk; this method is the\n * way to actually use that on cold start instead of re-running each\n * method's first-time login flow.\n *\n * Behavior by method:\n *\n * - `extension`: constructs an {@link ExtensionSigner}, which just\n * proxies to `window.nostr`. No setup roundtrip — individual\n * operations may still prompt depending on the extension's own\n * permission state, but unlock itself does not.\n *\n * - `nip46`: reuses the stored `clientSecretKey` to construct a\n * {@link BunkerSigner} against the stored bunker pubkey + relays\n * via `BunkerSigner.fromBunker`. Deliberately skips the `connect`\n * request — the remote signer (Amber etc.) approved this client\n * pubkey on first pairing and re-sending `connect` is what surfaces\n * a fresh approval prompt every cold start. Requires `options.pool`\n * so the BunkerSigner has somewhere to listen for responses.\n * The cached user pubkey is fed into the wrapper so a follow-up\n * `getPublicKey()` is a memory read, not a relay request.\n *\n * - `android`: constructs an {@link AndroidSigner} directly from the\n * stored `androidPackageName` + `pubkey` + `npub`. Skips the\n * `getPublicKey` content-provider roundtrip that\n * {@link loginWithAndroidSigner} performs and that — on Amber —\n * surfaces as a permission prompt every cold start.\n *\n * - `nip55-web`: constructs a {@link Nip55WebSigner} with the stored\n * `pubkey` cached. Like `android`, this opens no signer app during\n * unlock; the first sign/encrypt call is what prompts.\n *\n * - `ncryptsec`: returns `null`. There is no silent path — the user's\n * passphrase isn't (and shouldn't be) persisted. The caller must\n * drive the passphrase prompt and call {@link loginWithNcryptsec}.\n *\n * Returns `null` (without emitting any event or mutating state) when\n * there is no active account, when the account is missing fields\n * required to unlock, when `nip46` is the method but no `pool` was\n * supplied, or when `android` is the method but no plugin is\n * configured. On success emits the same `login` / `switch` event the\n * corresponding `loginWith*` would.\n */\n async unlock(options: UnlockOptions = {}): Promise<ActiveSigner | null> {\n const account = this.getActiveAccount();\n if (!account) return null;\n\n switch (account.method) {\n case 'extension': {\n const signer = new ExtensionSigner();\n this.#setActive(account, signer);\n return signer;\n }\n\n case 'nip46': {\n if (!account.nip46) return null;\n if (!options.pool) return null;\n const { remoteSignerPubkey, relays, clientSecretKey } = account.nip46;\n if (!remoteSignerPubkey || !relays.length || !clientSecretKey) {\n return null;\n }\n const tools = ToolsBunkerSigner.fromBunker(\n hexToBytes(clientSecretKey),\n { pubkey: remoteSignerPubkey, relays, secret: null },\n { pool: options.pool },\n );\n // No tools.connect() — the bunker already approved this client\n // on first pairing; re-sending `connect` is what triggers the\n // cold-start approval prompt we are trying to avoid.\n const signer = new BunkerSigner(tools, account.pubkey);\n this.#setActive(account, signer);\n return signer;\n }\n\n case 'android': {\n if (!account.androidPackageName) return null;\n if (!this.#defaultAndroidPlugin) return null;\n const signer = new AndroidSigner(\n this.#defaultAndroidPlugin,\n account.androidPackageName,\n account.npub,\n account.pubkey,\n );\n this.#setActive(account, signer);\n return signer;\n }\n\n case 'nip55-web': {\n // Resume with the cached pubkey so unlocking does not itself open\n // the signer app; only the next sign/encrypt call does.\n const signer = new Nip55WebSigner({\n transport: this.#nip55WebTransport,\n pubkey: account.pubkey,\n });\n this.#setActive(account, signer);\n return signer;\n }\n\n case 'ncryptsec':\n // No silent path — passphrase is not (and must not be) persisted.\n return null;\n }\n }\n\n /**\n * Make `pubkey` the active account. Clears the in-memory signer —\n * the new account starts **locked** even if it was previously\n * unlocked in this session.\n *\n * @throws if `pubkey` does not match any persisted account.\n */\n async switchAccount(pubkey: string): Promise<void> {\n const account = this.#accounts.find(a => a.pubkey === pubkey);\n if (!account) throw new Error(`switchAccount: no account for pubkey ${pubkey}`);\n this.#closeActiveSigner();\n this.#activePubkey = pubkey;\n this.#activeSigner = null;\n this.#persistActive();\n this.#emit({ type: 'switch', account });\n }\n\n /**\n * Remove an account from storage. `pubkey` defaults to the active\n * account. If the active account is removed, the in-memory signer is\n * cleared. No-op if there is nothing to remove.\n */\n async logout(pubkey?: string): Promise<void> {\n const target = pubkey ?? this.#activePubkey;\n if (!target) return;\n this.#accounts = this.#accounts.filter(a => a.pubkey !== target);\n this.#persistAccounts();\n if (this.#activePubkey === target) {\n this.#closeActiveSigner();\n this.#activePubkey = null;\n this.#activeSigner = null;\n this.#persistActive();\n }\n this.#emit({ type: 'logout', pubkey: target });\n }\n\n /**\n * Subscribe to account-state changes. Returns an unsubscribe function.\n * Listener errors are swallowed so one bad listener can't break others.\n * See {@link SignerEvent} for the variants.\n */\n onChange(cb: (event: SignerEvent) => void): () => void {\n this.#listeners.add(cb);\n return () => {\n this.#listeners.delete(cb);\n };\n }\n}\n\n/** Convenience wrapper around `new Signer(config)`. */\nexport function createSigner(config: SignerConfig = {}): Signer {\n return new Signer(config);\n}\n","export interface StorageAdapter {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\nconst DEFAULT_PREFIX = '@formstr/signer:';\n\nexport function localStorageAdapter(prefix: string = DEFAULT_PREFIX): StorageAdapter {\n const ls = (): Storage | null => {\n try {\n return typeof globalThis !== 'undefined' && globalThis.localStorage\n ? globalThis.localStorage\n : null;\n } catch {\n return null;\n }\n };\n return {\n get(key) {\n try {\n return ls()?.getItem(prefix + key) ?? null;\n } catch {\n return null;\n }\n },\n set(key, value) {\n try {\n ls()?.setItem(prefix + key, value);\n } catch {\n // swallow quota / privacy-mode errors\n }\n },\n remove(key) {\n try {\n ls()?.removeItem(prefix + key);\n } catch {\n // swallow\n }\n },\n };\n}\n","import {\n finalizeEvent,\n getPublicKey,\n nip04,\n nip44,\n type Event as NostrEvent,\n type EventTemplate,\n} from 'nostr-tools';\nimport type { ActiveSigner } from './types.js';\n\n/**\n * ActiveSigner backed by a raw secret key held in memory.\n * The secret key never leaves this object — there is no getter for it.\n */\nexport class LocalSigner implements ActiveSigner {\n readonly #secretKey: Uint8Array;\n\n constructor(secretKey: Uint8Array) {\n this.#secretKey = secretKey;\n }\n\n async getPublicKey(): Promise<string> {\n return getPublicKey(this.#secretKey);\n }\n\n async signEvent(event: EventTemplate): Promise<NostrEvent> {\n return finalizeEvent(event, this.#secretKey);\n }\n\n async nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n return nip04.encrypt(this.#secretKey, peerPubkey, plaintext);\n }\n\n async nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n return nip04.decrypt(this.#secretKey, peerPubkey, ciphertext);\n }\n\n async nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const key = nip44.v2.utils.getConversationKey(this.#secretKey, peerPubkey);\n return nip44.v2.encrypt(plaintext, key);\n }\n\n async nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const key = nip44.v2.utils.getConversationKey(this.#secretKey, peerPubkey);\n return nip44.v2.decrypt(ciphertext, key);\n }\n}\n","import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';\nimport { encrypt as nip49Encrypt, decrypt as nip49Decrypt } from 'nostr-tools/nip49';\n\nexport function encryptSecretKey(secretKey: Uint8Array, passphrase: string): string {\n return nip49Encrypt(secretKey, passphrase);\n}\n\nexport function decryptNcryptsec(ncryptsec: string, passphrase: string): Uint8Array {\n return nip49Decrypt(ncryptsec, passphrase);\n}\n\nexport interface GeneratedAccount {\n secretKey: Uint8Array;\n pubkey: string;\n npub: string;\n ncryptsec: string;\n}\n\nexport function generateAccount(passphrase: string): GeneratedAccount {\n const secretKey = generateSecretKey();\n const pubkey = getPublicKey(secretKey);\n const npub = nip19.npubEncode(pubkey);\n const ncryptsec = nip49Encrypt(secretKey, passphrase);\n return { secretKey, pubkey, npub, ncryptsec };\n}\n","import type { Event as NostrEvent, EventTemplate } from 'nostr-tools';\nimport type { ActiveSigner } from './core/types.js';\n\nexport interface WindowNostr {\n getPublicKey(): Promise<string>;\n signEvent(event: EventTemplate): Promise<NostrEvent>;\n getRelays?(): Promise<Record<string, { read: boolean; write: boolean }>>;\n nip04?: {\n encrypt(peerPubkey: string, plaintext: string): Promise<string>;\n decrypt(peerPubkey: string, ciphertext: string): Promise<string>;\n };\n nip44?: {\n encrypt(peerPubkey: string, plaintext: string): Promise<string>;\n decrypt(peerPubkey: string, ciphertext: string): Promise<string>;\n };\n}\n\nexport function getWindowNostr(): WindowNostr {\n const nostr = (globalThis as { nostr?: WindowNostr }).nostr;\n if (!nostr) {\n throw new Error(\n '@formstr/signer: NIP-07 extension not found (globalThis.nostr is undefined)',\n );\n }\n return nostr;\n}\n\nexport class ExtensionSigner implements ActiveSigner {\n async getPublicKey(): Promise<string> {\n return getWindowNostr().getPublicKey();\n }\n\n async signEvent(event: EventTemplate): Promise<NostrEvent> {\n return getWindowNostr().signEvent(event);\n }\n\n async nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const ext = getWindowNostr();\n if (!ext.nip04) throw new Error('NIP-07 extension does not expose nip04');\n return ext.nip04.encrypt(peerPubkey, plaintext);\n }\n\n async nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const ext = getWindowNostr();\n if (!ext.nip04) throw new Error('NIP-07 extension does not expose nip04');\n return ext.nip04.decrypt(peerPubkey, ciphertext);\n }\n\n async nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const ext = getWindowNostr();\n if (!ext.nip44) throw new Error('NIP-07 extension does not expose nip44');\n return ext.nip44.encrypt(peerPubkey, plaintext);\n }\n\n async nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const ext = getWindowNostr();\n if (!ext.nip44) throw new Error('NIP-07 extension does not expose nip44');\n return ext.nip44.decrypt(peerPubkey, ciphertext);\n }\n}\n","import { generateSecretKey, getPublicKey } from 'nostr-tools';\nimport {\n BunkerSigner as ToolsBunkerSigner,\n createNostrConnectURI,\n parseBunkerInput,\n type BunkerPointer,\n} from 'nostr-tools/nip46';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Event as NostrEvent, EventTemplate } from 'nostr-tools';\nimport type { ActiveSigner, RelayMismatchHandler } from './core/types.js';\n\nexport type { BunkerPointer };\n\n/**\n * Thin wrapper around nostr-tools' BunkerSigner that exposes only the\n * ActiveSigner surface. We keep this layer so callers depend on a stable\n * interface even if we ever swap the underlying implementation.\n *\n * Optionally accepts a `cachedUserPubkey`. When supplied, {@link getPublicKey}\n * returns it without a bunker roundtrip. The user's signer pubkey is fixed\n * for a given paired account, so caching it after the initial `connect` —\n * or feeding it back in from persisted storage on unlock — avoids both a\n * network hop and a potential approval prompt on every cold start. Without\n * a cached value we fall back to asking the bunker, matching the prior\n * behavior.\n */\nexport class BunkerSigner implements ActiveSigner {\n readonly #delegate: ToolsBunkerSigner;\n readonly #cachedUserPubkey: string | null;\n\n constructor(delegate: ToolsBunkerSigner, cachedUserPubkey?: string) {\n this.#delegate = delegate;\n this.#cachedUserPubkey = cachedUserPubkey ?? null;\n }\n\n getPublicKey(): Promise<string> {\n if (this.#cachedUserPubkey !== null) {\n return Promise.resolve(this.#cachedUserPubkey);\n }\n return this.#delegate.getPublicKey();\n }\n signEvent(event: EventTemplate): Promise<NostrEvent> {\n return this.#delegate.signEvent(event);\n }\n nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n return this.#delegate.nip04Encrypt(peerPubkey, plaintext);\n }\n nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n return this.#delegate.nip04Decrypt(peerPubkey, ciphertext);\n }\n nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n return this.#delegate.nip44Encrypt(peerPubkey, plaintext);\n }\n nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n return this.#delegate.nip44Decrypt(peerPubkey, ciphertext);\n }\n async close(): Promise<void> {\n return this.#delegate.close();\n }\n}\n\nexport interface BunkerLoginOptions {\n /** Custom pool, e.g. for tests. Defaults to a new SimplePool inside nostr-tools. */\n pool?: AbstractSimplePool;\n /** Called when the remote signer needs the user to visit an auth URL. */\n onAuth?: (url: string) => void;\n /** Optional client session keypair (hex bytes). Auto-generated if omitted. */\n clientSecretKey?: Uint8Array;\n /** Notified when the bunker's preferred relays differ from the URI's. */\n onRelayMismatch?: RelayMismatchHandler;\n /**\n * NIP-46 permissions to request as the 3rd `connect` param\n * (e.g. `['sign_event:1', 'nip44_encrypt']`). When omitted, the\n * connect request carries no perms — bunker UIs may then skip the\n * approval prompt entirely, leaving the user with nothing to tap.\n */\n perms?: string[];\n}\n\nasync function fetchBunkerRelays(tools: ToolsBunkerSigner): Promise<string[] | null> {\n try {\n const resp = await tools.sendRequest('get_relays', []);\n const parsed = JSON.parse(resp) as unknown;\n if (Array.isArray(parsed)) {\n return parsed.filter((r): r is string => typeof r === 'string');\n }\n if (typeof parsed === 'object' && parsed !== null) {\n return Object.keys(parsed as Record<string, unknown>);\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction relayListsMatch(a: string[], b: string[]): boolean {\n if (a.length !== b.length) return false;\n const sa = [...a].sort();\n const sb = [...b].sort();\n for (let i = 0; i < sa.length; i++) if (sa[i] !== sb[i]) return false;\n return true;\n}\n\nasync function resolveRelayChoice(\n tools: ToolsBunkerSigner,\n userRelays: string[],\n onRelayMismatch: RelayMismatchHandler | undefined,\n): Promise<string[]> {\n if (!onRelayMismatch) return userRelays;\n const bunkerRelays = await fetchBunkerRelays(tools);\n if (!bunkerRelays || relayListsMatch(userRelays, bunkerRelays)) return userRelays;\n const accept = await onRelayMismatch({ userRelays, bunkerRelays });\n return accept ? bunkerRelays : userRelays;\n}\n\nexport interface BunkerConnectResult {\n signer: BunkerSigner;\n pubkey: string;\n pointer: BunkerPointer;\n clientSecretKey: Uint8Array;\n}\n\n/**\n * Connect to a remote signer via a bunker:// URI (or a NIP-05 identifier).\n * Relays come from the URI — there is no fallback default list.\n */\nexport async function connectWithBunkerUri(\n uri: string,\n options: BunkerLoginOptions = {},\n): Promise<BunkerConnectResult> {\n const pointer = await parseBunkerInput(uri);\n if (!pointer) {\n throw new Error('@formstr/signer: invalid bunker URI');\n }\n if (!pointer.relays?.length) {\n throw new Error('@formstr/signer: bunker URI must include at least one relay');\n }\n const clientSecretKey = options.clientSecretKey ?? generateSecretKey();\n const tools = ToolsBunkerSigner.fromBunker(clientSecretKey, pointer, {\n pool: options.pool,\n onauth: options.onAuth,\n });\n // nostr-tools' BunkerSigner.connect() hardcodes only [pubkey, secret],\n // dropping the optional 3rd `perms` arg defined by NIP-46. Without it\n // bunker UIs (Amber, etc.) have no permissions to authorize and may\n // skip the approval prompt entirely. We send the request directly.\n await tools.sendRequest('connect', [\n pointer.pubkey,\n pointer.secret ?? '',\n (options.perms ?? []).join(','),\n ]);\n const pubkey = await tools.getPublicKey();\n const resolvedRelays = await resolveRelayChoice(\n tools,\n pointer.relays,\n options.onRelayMismatch,\n );\n return {\n signer: new BunkerSigner(tools, pubkey),\n pubkey,\n pointer: { ...pointer, relays: resolvedRelays },\n clientSecretKey,\n };\n}\n\nexport interface NostrConnectInitOptions {\n /** User-supplied relays. The whole point of the strict-relay rule. */\n relays: string[];\n metadata?: { name?: string; url?: string; image?: string };\n /** Permissions to request (NIP-46 perms list, e.g. [\"sign_event:1\",\"nip44_encrypt\"]). */\n perms?: string[];\n pool?: AbstractSimplePool;\n onAuth?: (url: string) => void;\n /** Override the auto-generated client session keypair. */\n clientSecretKey?: Uint8Array;\n /** Override the auto-generated URI secret. */\n secret?: string;\n /** Abort the pairing wait. */\n signal?: AbortSignal;\n /** Max wait for pairing in ms (default 5 minutes). */\n timeoutMs?: number;\n /** Notified when the bunker's preferred relays differ from the user's. */\n onRelayMismatch?: RelayMismatchHandler;\n}\n\nexport interface NostrConnectInitiation {\n uri: string;\n clientPubkey: string;\n complete: Promise<BunkerConnectResult>;\n}\n\n/**\n * Generate a nostrconnect:// URI and wait for the remote signer to pair.\n * The caller displays the URI (typically as a QR code), and the returned\n * `complete` promise resolves once the signer connects back.\n */\nexport function initiateNostrConnect(options: NostrConnectInitOptions): NostrConnectInitiation {\n if (options.relays.length === 0) {\n throw new Error('@formstr/signer: at least one relay is required for nostrconnect');\n }\n const clientSecretKey = options.clientSecretKey ?? generateSecretKey();\n const clientPubkey = getPublicKey(clientSecretKey);\n const secret = options.secret ?? Math.random().toString(36).slice(2);\n const uri = createNostrConnectURI({\n clientPubkey,\n relays: options.relays,\n secret,\n perms: options.perms,\n name: options.metadata?.name,\n url: options.metadata?.url,\n image: options.metadata?.image,\n });\n const maxWaitOrAbort: number | AbortSignal =\n options.signal ?? options.timeoutMs ?? 300_000;\n // skipSwitchRelays:true — keep the caller-supplied relays authoritative,\n // never silently swap to whatever the bunker prefers.\n const complete = ToolsBunkerSigner.fromURI(\n clientSecretKey,\n uri,\n { pool: options.pool, onauth: options.onAuth, skipSwitchRelays: true },\n maxWaitOrAbort,\n ).then(async (tools) => {\n const pubkey = await tools.getPublicKey();\n const resolvedRelays = await resolveRelayChoice(\n tools,\n options.relays,\n options.onRelayMismatch,\n );\n return {\n signer: new BunkerSigner(tools, pubkey),\n pubkey,\n pointer: { ...tools.bp, relays: resolvedRelays },\n clientSecretKey,\n };\n });\n return { uri, clientPubkey, complete };\n}\n\nconst hexAlphabet = '0123456789abcdef';\n\nexport function bytesToHex(bytes: Uint8Array): string {\n let s = '';\n for (const b of bytes) s += hexAlphabet[b >> 4] + hexAlphabet[b & 0xf];\n return s;\n}\n\nexport function hexToBytes(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error('hexToBytes: odd-length hex string');\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { getEventHash, nip19, type Event as NostrEvent, type EventTemplate } from 'nostr-tools';\nimport type { ActiveSigner } from './core/types.js';\n\n/**\n * Subset of `nostr-signer-capacitor-plugin`'s exported `NostrSignerPlugin`\n * that we depend on. Signatures intentionally mirror that library\n * (positional args, per-call `packageName`) so the real plugin is\n * structurally assignable here — and any mock written against this\n * interface is a faithful stand-in. The conformance is enforced by a\n * compile-time guard in `tests/helpers/mockAndroidPlugin.ts`.\n */\nexport interface AndroidSignerAppInfo {\n name: string;\n packageName: string;\n iconUrl?: string;\n}\n\nexport interface AndroidSignerPlugin {\n setPackageName(packageName: string): Promise<void>;\n getInstalledSignerApps(): Promise<{ apps: AndroidSignerAppInfo[] }>;\n getPublicKey(\n packageName?: string,\n permissions?: string,\n ): Promise<{ npub: string; package: string }>;\n signEvent(\n packageName: string,\n eventJson: string,\n id: string,\n npub: string,\n ): Promise<{ signature: string; id: string; event: string }>;\n nip04Encrypt(\n packageName: string,\n plainText: string,\n id: string,\n pubKey: string,\n npub: string,\n ): Promise<{ result: string; id: string }>;\n nip04Decrypt(\n packageName: string,\n encryptedText: string,\n id: string,\n pubKey: string,\n npub: string,\n ): Promise<{ result: string; id: string }>;\n nip44Encrypt(\n packageName: string,\n plainText: string,\n id: string,\n pubKey: string,\n npub: string,\n ): Promise<{ result: string; id: string }>;\n nip44Decrypt(\n packageName: string,\n encryptedText: string,\n id: string,\n pubKey: string,\n npub: string,\n ): Promise<{ result: string; id: string }>;\n}\n\nexport interface AndroidLoginOptions {\n /** The Android package name of the external signer app (e.g. com.greenart7c3.nostrsigner). */\n packageName?: string;\n /** Override the plugin for this call. Falls back to SignerConfig.androidSignerPlugin. */\n plugin?: AndroidSignerPlugin;\n}\n\nexport class AndroidSigner implements ActiveSigner {\n readonly #plugin: AndroidSignerPlugin;\n readonly #packageName: string;\n readonly #npub: string;\n readonly #pubkey: string;\n\n constructor(\n plugin: AndroidSignerPlugin,\n packageName: string,\n npub: string,\n pubkey: string,\n ) {\n this.#plugin = plugin;\n this.#packageName = packageName;\n this.#npub = npub;\n this.#pubkey = pubkey;\n }\n\n async getPublicKey(): Promise<string> {\n return this.#pubkey;\n }\n\n async signEvent(event: EventTemplate): Promise<NostrEvent> {\n const unsigned = { ...event, pubkey: this.#pubkey };\n const eventId = getEventHash(unsigned);\n const result = await this.#plugin.signEvent(\n this.#packageName,\n JSON.stringify(unsigned),\n eventId,\n this.#npub,\n );\n return JSON.parse(result.event) as NostrEvent;\n }\n\n async nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const { result } = await this.#plugin.nip04Encrypt(\n this.#packageName,\n plaintext,\n '',\n peerPubkey,\n this.#npub,\n );\n return result;\n }\n\n async nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const { result } = await this.#plugin.nip04Decrypt(\n this.#packageName,\n ciphertext,\n '',\n peerPubkey,\n this.#npub,\n );\n return result;\n }\n\n async nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const { result } = await this.#plugin.nip44Encrypt(\n this.#packageName,\n plaintext,\n '',\n peerPubkey,\n this.#npub,\n );\n return result;\n }\n\n async nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const { result } = await this.#plugin.nip44Decrypt(\n this.#packageName,\n ciphertext,\n '',\n peerPubkey,\n this.#npub,\n );\n return result;\n }\n}\n\nexport interface AndroidLoginResult {\n signer: AndroidSigner;\n pubkey: string;\n npub: string;\n packageName: string;\n}\n\n/**\n * Render a debuggable summary of what the Android signer plugin returned\n * where an npub was expected. Includes type and length, plus a truncated\n * prefix that preserves the bech32 HRP (so callers can tell `nsec1…` /\n * `nprofile1…` / a raw hex pubkey apart) without leaking the full secret\n * material that an erroneous `nsec` response would carry.\n */\nfunction describeIdentifier(value: unknown): string {\n if (value === null) return 'null';\n if (value === undefined) return 'undefined';\n if (typeof value !== 'string') {\n return `<${typeof value}>`;\n }\n if (value.length === 0) return 'empty string';\n const prefix = value.slice(0, 12);\n const suffix = value.length > 12 ? '…' : '';\n return `\"${prefix}${suffix}\" (length=${value.length})`;\n}\n\n/**\n * Lowercase 32-byte hex pubkey shape. NIP-55 nominally returns an `npub`\n * in the `signature` extra, but recent Amber builds put the raw hex\n * pubkey there instead — we accept either form and normalize internally.\n */\nconst HEX_PUBKEY_RE = /^[0-9a-f]{64}$/i;\n\nexport async function loginWithAndroidSigner(\n plugin: AndroidSignerPlugin,\n packageName?: string,\n): Promise<AndroidLoginResult> {\n if (packageName) {\n await plugin.setPackageName(packageName);\n }\n const { npub: rawIdentifier, package: pluginPackage } =\n await plugin.getPublicKey(packageName);\n const resolvedPackage = pluginPackage || packageName;\n if (!resolvedPackage) {\n throw new Error(\n '@formstr/signer: android signer did not return a package name and none was supplied',\n );\n }\n const { pubkey, npub } = normalizeNip55Identifier(rawIdentifier);\n return {\n signer: new AndroidSigner(plugin, resolvedPackage, npub, pubkey),\n pubkey,\n npub,\n packageName: resolvedPackage,\n };\n}\n\n/**\n * Normalize whatever a NIP-55 transport handed us in the pubkey slot into a\n * `{ pubkey, npub }` pair. Accepts:\n * - a 32-byte lowercase hex pubkey (newer Amber builds return this),\n * - a bech32 `npub1…` (the original NIP-55 spec shape),\n * - a bech32 `nprofile1…`, which the web/clipboard transport may return.\n * Throws a debuggable error for anything else, including the preview of\n * what was actually received so the caller can triage.\n *\n * Shared by the Capacitor (`loginWithAndroidSigner`) and pure-web\n * (`nip55Web.ts`) flows — both transports are NIP-55 and must agree on\n * how identifiers normalize.\n */\nexport function normalizeNip55Identifier(\n rawIdentifier: unknown,\n): { pubkey: string; npub: string } {\n if (typeof rawIdentifier === 'string' && HEX_PUBKEY_RE.test(rawIdentifier)) {\n const pubkey = rawIdentifier.toLowerCase();\n return { pubkey, npub: nip19.npubEncode(pubkey) };\n }\n // Wrap nip19.decode so a bech32 failure (\"Data must be at least 6\n // characters long\", \"Invalid checksum\", ...) surfaces what the transport\n // actually returned. Without this, callers see an opaque bech32 crash\n // and can't tell whether the signer sent back an empty string, an nsec,\n // or something else entirely.\n let decoded: ReturnType<typeof nip19.decode>;\n try {\n decoded = nip19.decode(rawIdentifier as string);\n } catch (e) {\n // nostr-tools' nip19 decoder always throws Error instances on bech32\n // failures. Pass the message straight through.\n throw new Error(\n `@formstr/signer: signer returned an undecodable identifier (got ${describeIdentifier(rawIdentifier)}): ${(e as Error).message}`,\n );\n }\n if (decoded.type === 'npub') {\n return { pubkey: decoded.data, npub: rawIdentifier as string };\n }\n if (decoded.type === 'nprofile') {\n const pubkey = decoded.data.pubkey;\n return { pubkey, npub: nip19.npubEncode(pubkey) };\n }\n throw new Error(\n `@formstr/signer: signer returned a non-pubkey identifier (type=${decoded.type}, got ${describeIdentifier(rawIdentifier)})`,\n );\n}\n","import {\n getEventHash,\n verifyEvent,\n type Event as NostrEvent,\n type EventTemplate,\n} from 'nostr-tools';\nimport type { ActiveSigner } from './core/types.js';\nimport { normalizeNip55Identifier } from './nip55.js';\n\n/**\n * NIP-55 over the web, without a Capacitor/native bridge.\n *\n * A plain browser page cannot call an Android signer in the background, so\n * this transport hands the OS a `nostrsigner` intent and then reads the\n * result back from the **clipboard** once the user returns to the tab. It\n * is the mechanism used by applesauce's `AmberClipboardSigner` and\n * gitworkshop's \"Use Amber\" button; no package is named in the intent, so\n * Android resolves it against whatever app registered the scheme — one\n * installed signer opens directly, several show the \"Open with\" chooser.\n *\n * The alternative NIP-55 web path — `?callbackUrl=` — is not used here: it\n * navigates the page away and back, which cannot be hidden behind a\n * promise-returning {@link ActiveSigner}. Use NIP-46 when a persistent\n * session is wanted; the spec recommends it for web clients.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/55.md\n */\nexport interface Nip55WebTransport {\n /**\n * Whether this environment can run the intent + clipboard flow at all.\n * The package's browser transport requires an Android user agent and the\n * async clipboard API; anything else must fail loudly rather than open a\n * dead intent.\n */\n isSupported(): boolean;\n /** Hand the intent URI to the OS (a browser typically `window.open`s it). */\n open(intent: string): void;\n /** Read the current clipboard text. */\n readClipboard(): Promise<string>;\n /**\n * Overwrite the clipboard. Used to plant a sentinel before opening the\n * signer, so the result can be told apart from whatever the user had\n * copied earlier. Best-effort — a failure just means we fall back to\n * accepting the first non-empty read.\n */\n writeClipboard(text: string): Promise<void>;\n}\n\nexport interface Nip55WebOptions {\n /**\n * Environment bridge. Defaults to a browser implementation\n * ({@link browserNip55Transport}). Tests inject a fake.\n */\n transport?: Nip55WebTransport;\n /**\n * Cached user pubkey. When supplied, {@link Nip55WebSigner.getPublicKey}\n * returns it without a signer roundtrip — used on cold start to avoid a\n * fresh `get_public_key` approval prompt for an already-paired account.\n */\n pubkey?: string;\n /**\n * How often to poll the clipboard while waiting for the signer app to\n * return a result. Default 500ms.\n *\n * Polling, not events: on Android Chrome the return from the signer app\n * fires **no** `visibilitychange`/`focus` event at all (the tab was never\n * reported hidden), so an event-driven read simply never happens.\n * `setInterval` keeps running while the tab is backgrounded, so polling\n * is what actually observes the result.\n */\n pollIntervalMs?: number;\n /**\n * Max time to wait for the signer app to return a result. A rejection is\n * invisible over NIP-55 web (the app just never calls back), so without\n * this a denied or abandoned request hangs forever. Default 120000ms;\n * `0` disables the timeout.\n */\n timeoutMs?: number;\n /**\n * Lifetime abort. Aborts the in-flight request and makes every future\n * request reject immediately (rejections carry `name === 'AbortError'`,\n * matching the NIP-46 flow).\n *\n * This is a property of the *signer*, not of one attempt. Do **not**\n * pass a controller that is aborted once a login modal closes — that\n * would leave the signer permanently unusable. To cancel a single\n * pairing attempt, use {@link Signer.loginWithNip55Web}'s `signal`.\n */\n signal?: AbortSignal;\n /**\n * Diagnostic sink for the intent/clipboard round-trip. The flow is hard\n * to observe (it leaves the page and returns), so a host can pass a\n * logger — e.g. to an on-screen list — to see what actually happened.\n */\n debug?: (message: string) => void;\n}\n\ninterface PendingRequest {\n resolve(value: string): void;\n reject(error: unknown): void;\n /** The value planted before opening the signer, if the write succeeded. */\n sentinel: string | null;\n /** Interval id for the clipboard poll; cleared on settle. */\n poll: ReturnType<typeof setInterval> | null;\n /** Request timeout; cleared on settle. `null` when disabled. */\n timer: ReturnType<typeof setTimeout> | null;\n /** Abort listener; removed on settle. `null` when no signal was given. */\n onAbort: (() => void) | null;\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 500;\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\nlet sentinelCounter = 0;\n\n/**\n * A clipboard value guaranteed not to collide with a real signer result.\n * Written before the intent so polling can tell \"the app has answered\"\n * apart from \"the user's clipboard still holds yesterday's copy\".\n */\nfunction makeSentinel(): string {\n sentinelCounter += 1;\n return `__formstr_nip55_sentinel_${Date.now()}_${sentinelCounter}__`;\n}\n\nfunction abortError(): Error {\n const error = new Error('@formstr/signer: NIP-55 request aborted');\n error.name = 'AbortError';\n return error;\n}\n\n/** Capacitor's injected global, when running inside a native shell. */\ninterface CapacitorGlobalLike {\n isNativePlatform?: () => boolean;\n}\n\n/**\n * True when the page is running inside a Capacitor native shell (the\n * Android/iOS app) rather than a plain browser. Capacitor injects a\n * `Capacitor` global into the WebView, so this needs no dependency on\n * `@capacitor/core` — which the package deliberately does not pull in.\n */\nfunction isNativeShell(): boolean {\n const cap = (globalThis as { Capacitor?: CapacitorGlobalLike }).Capacitor;\n return typeof cap?.isNativePlatform === 'function' && cap.isNativePlatform();\n}\n\n/**\n * Browser transport: `window.open` for the intent, `navigator.clipboard`\n * for the result. Globals are read lazily so importing this module stays\n * safe in Node.\n *\n * This transport is for a **plain browser only**. Inside a Capacitor\n * native shell the same device has the real NIP-55 plugin, which supports\n * every method and does not need the clipboard or a per-operation\n * approval, so this flow is not offered there — a native host should use\n * `loginWithAndroidSigner` instead.\n */\nexport function browserNip55Transport(): Nip55WebTransport {\n return {\n isSupported() {\n return (\n typeof navigator !== 'undefined' &&\n !isNativeShell() &&\n /Android/i.test(navigator.userAgent) &&\n typeof navigator.clipboard?.readText === 'function'\n );\n },\n open(intent) {\n window.open(intent, '_blank');\n },\n readClipboard() {\n return navigator.clipboard.readText();\n },\n writeClipboard(text) {\n return navigator.clipboard.writeText(text);\n },\n };\n}\n\n/**\n * {@link ActiveSigner} backed by any installed NIP-55 signer app, used from\n * a plain (Android) browser via intents + clipboard.\n *\n * Every operation is a separate user approval — there is no background\n * channel and no way to learn that the user rejected a request, so callers\n * must impose their own timeout (the flow simply never resolves otherwise).\n */\nexport class Nip55WebSigner implements ActiveSigner {\n readonly #transport: Nip55WebTransport;\n readonly #pollIntervalMs: number;\n readonly #timeoutMs: number;\n readonly #signal: AbortSignal | undefined;\n readonly #debug: ((message: string) => void) | undefined;\n #pending: PendingRequest | null = null;\n #pubkey: string | null = null;\n\n constructor(options: Nip55WebOptions = {}) {\n this.#transport = options.transport ?? browserNip55Transport();\n this.#pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.#signal = options.signal;\n this.#debug = options.debug;\n this.#pubkey = options.pubkey ?? null;\n }\n\n #log(message: string): void {\n this.#debug?.(message);\n }\n\n /** True when the configured transport can actually open a signer app. */\n isSupported(): boolean {\n return this.#transport.isSupported();\n }\n\n async getPublicKey(): Promise<string> {\n if (this.#pubkey !== null) return this.#pubkey;\n this.#checkSupport();\n const raw = await this.#request(Nip55WebSigner.getPublicKeyIntent());\n const { pubkey } = normalizeNip55Identifier(raw);\n this.#pubkey = pubkey;\n return pubkey;\n }\n\n async signEvent(event: EventTemplate): Promise<NostrEvent> {\n this.#checkSupport();\n const pubkey = this.#pubkey ?? (await this.getPublicKey());\n const unsigned = { ...event, pubkey };\n const draftWithId = { ...unsigned, id: getEventHash(unsigned) };\n const sig = (\n await this.#request(Nip55WebSigner.signEventIntent(draftWithId))\n ).trim();\n if (!/^[0-9a-f]{128}$/i.test(sig)) {\n throw new Error(\n '@formstr/signer: NIP-55 signer did not return a hex signature',\n );\n }\n const signed: NostrEvent = { ...draftWithId, sig: sig.toLowerCase() };\n if (!verifyEvent(signed)) {\n throw new Error('@formstr/signer: NIP-55 signer returned an invalid signature');\n }\n return signed;\n }\n\n async nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n this.#checkSupport();\n return this.#request(Nip55WebSigner.nip04EncryptIntent(peerPubkey, plaintext));\n }\n\n async nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n this.#checkSupport();\n return this.#request(Nip55WebSigner.nip04DecryptIntent(peerPubkey, ciphertext));\n }\n\n async nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n this.#checkSupport();\n return this.#request(Nip55WebSigner.nip44EncryptIntent(peerPubkey, plaintext));\n }\n\n async nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n this.#checkSupport();\n return this.#request(Nip55WebSigner.nip44DecryptIntent(peerPubkey, ciphertext));\n }\n\n /**\n * Cancel any in-flight request and stop its clipboard poll. Subsequent\n * operations still work — this is a teardown of live resources, not a\n * permanent disable (the {@link Signer} calls it when replacing the\n * active signer).\n */\n close(): void {\n if (this.#pending) {\n const pending = this.#pending;\n this.#settle();\n pending.reject(abortError());\n }\n }\n\n #checkSupport(): void {\n if (this.#transport.isSupported()) return;\n if (isNativeShell()) {\n throw new Error(\n '@formstr/signer: the browser NIP-55 flow is not for native builds — use loginWithAndroidSigner() with the Capacitor plugin instead',\n );\n }\n throw new Error(\n '@formstr/signer: NIP-55 web signing requires an Android browser with clipboard access (a signer app registering the `nostrsigner` scheme must be installed)',\n );\n }\n\n /** One poll tick: read the clipboard and settle if the signer answered. */\n #poll = async (pending: PendingRequest): Promise<void> => {\n let text: string;\n try {\n text = await this.#transport.readClipboard();\n } catch (error) {\n // NotAllowedError until the page regains focus; keep polling.\n this.#log(`clipboard read failed: ${(error as Error).message}`);\n return;\n }\n // A read can resume after the request was superseded/settled while it\n // was in flight; the interval is cleared then, but this tick is already\n // running. Drop it rather than resolving the wrong request.\n if (this.#pending !== pending) return;\n const trimmed = text.trim();\n if (trimmed.length === 0) return;\n // With a sentinel we require a *change*; without one (the write failed)\n // accept any non-empty value, matching the pre-sentinel behaviour.\n if (pending.sentinel !== null && trimmed === pending.sentinel) return;\n this.#log(`clipboard result (${trimmed.length} chars)`);\n this.#settle();\n pending.resolve(trimmed);\n };\n\n #request(intent: string): Promise<string> {\n this.#checkAborted();\n this.#cancelPending();\n return new Promise<string>((resolve, reject) => {\n const pending: PendingRequest = {\n resolve,\n reject,\n sentinel: null,\n poll: null,\n timer: null,\n onAbort: null,\n };\n this.#pending = pending;\n if (this.#signal) {\n const onAbort = () => this.#fail(pending, abortError());\n this.#signal.addEventListener('abort', onAbort);\n pending.onAbort = onAbort;\n }\n if (this.#timeoutMs > 0) {\n pending.timer = setTimeout(() => {\n this.#fail(\n pending,\n new Error(\n `@formstr/signer: NIP-55 request timed out after ${this.#timeoutMs}ms (the signer app never returned a result)`,\n ),\n );\n }, this.#timeoutMs);\n }\n\n // Plant a sentinel before opening the signer. Without it, a stale\n // clipboard from an earlier approval would look like a fresh result\n // and resolve immediately. Best-effort: if the write is refused we\n // proceed without it rather than failing the whole flow.\n void (async () => {\n try {\n const sentinel = makeSentinel();\n await this.#transport.writeClipboard(sentinel);\n if (this.#pending === pending) pending.sentinel = sentinel;\n this.#log('planted clipboard sentinel');\n } catch (error) {\n this.#log(`sentinel write failed: ${(error as Error).message}`);\n }\n if (this.#pending !== pending) return;\n try {\n this.#log(`opening signer app: ${intent.slice(0, 80)}…`);\n this.#transport.open(intent);\n } catch (error) {\n this.#fail(pending, error);\n return;\n }\n // Start polling only after the intent is out; the sentinel write and\n // open are skipped on the very first ticks otherwise.\n pending.poll = setInterval(() => {\n void this.#poll(pending);\n }, this.#pollIntervalMs);\n })();\n });\n }\n\n /**\n * Reject the current request. Every caller is cleared on settle — the\n * timeout timer and abort listener are removed, and `open()` is\n * synchronous — so `pending` is always the active request here.\n */\n #fail(pending: PendingRequest, error: unknown): void {\n this.#settle();\n pending.reject(error);\n }\n\n #settle(): void {\n const pending = this.#pending;\n this.#pending = null;\n if (pending?.poll) clearInterval(pending.poll);\n if (pending?.timer) clearTimeout(pending.timer);\n if (pending?.onAbort && this.#signal) {\n this.#signal.removeEventListener('abort', pending.onAbort);\n }\n }\n\n #cancelPending(): void {\n if (!this.#pending) return;\n const pending = this.#pending;\n this.#settle();\n pending.reject(new Error('@formstr/signer: NIP-55 request superseded'));\n }\n\n #checkAborted(): void {\n if (this.#signal?.aborted) throw abortError();\n }\n\n static getPublicKeyIntent(): string {\n return 'intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end';\n }\n\n static signEventIntent(draft: object): string {\n return `intent:${encodeURIComponent(\n JSON.stringify(draft),\n )}#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=sign_event;end`;\n }\n\n static nip04EncryptIntent(peerPubkey: string, plaintext: string): string {\n return Nip55WebSigner.#cryptoIntent('nip04_encrypt', peerPubkey, plaintext);\n }\n\n static nip04DecryptIntent(peerPubkey: string, ciphertext: string): string {\n return Nip55WebSigner.#cryptoIntent('nip04_decrypt', peerPubkey, ciphertext);\n }\n\n static nip44EncryptIntent(peerPubkey: string, plaintext: string): string {\n return Nip55WebSigner.#cryptoIntent('nip44_encrypt', peerPubkey, plaintext);\n }\n\n static nip44DecryptIntent(peerPubkey: string, ciphertext: string): string {\n return Nip55WebSigner.#cryptoIntent('nip44_decrypt', peerPubkey, ciphertext);\n }\n\n static #cryptoIntent(\n type: 'nip04_encrypt' | 'nip04_decrypt' | 'nip44_encrypt' | 'nip44_decrypt',\n peerPubkey: string,\n payload: string,\n ): string {\n return `intent:${encodeURIComponent(\n payload,\n )}#Intent;scheme=nostrsigner;S.pubKey=${peerPubkey};S.compressionType=none;S.returnType=signature;S.type=${type};end`;\n }\n}\n"],"mappings":"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","names":["import_nostr_tools","import_nip46","import_nostr_tools","nip49Encrypt","nip49Decrypt","import_nostr_tools","ToolsBunkerSigner","import_nostr_tools","import_nostr_tools","ToolsBunkerSigner"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/core/signer.ts","../src/core/storage.ts","../src/core/localSigner.ts","../src/nip49.ts","../src/nip07.ts","../src/nip46.ts","../src/nip55.ts","../src/nip55Web.ts"],"sourcesContent":["export { Signer, createSigner } from './core/signer.js';\nexport { LocalSigner } from './core/localSigner.js';\nexport { localStorageAdapter } from './core/storage.js';\nexport type { StorageAdapter } from './core/storage.js';\nexport type {\n SignerConfig,\n ActiveSigner,\n StoredAccount,\n LoginMethod,\n SignerEvent,\n NostrConnectOptions,\n BunkerLoginOptions,\n Nip55WebLoginOptions,\n RelayMismatchInfo,\n RelayMismatchHandler,\n UnlockOptions,\n} from './core/types.js';\nexport {\n encryptSecretKey,\n decryptNcryptsec,\n generateAccount,\n type GeneratedAccount,\n} from './nip49.js';\nexport { ExtensionSigner, getWindowNostr, type WindowNostr } from './nip07.js';\nexport {\n AndroidSigner,\n loginWithAndroidSigner,\n normalizeNip55Identifier,\n type AndroidSignerPlugin,\n type AndroidSignerAppInfo,\n type AndroidLoginOptions,\n type AndroidLoginResult,\n} from './nip55.js';\nexport {\n Nip55WebSigner,\n browserNip55Transport,\n type Nip55WebOptions,\n type Nip55WebSupport,\n type Nip55WebSupportReason,\n type Nip55WebTransport,\n} from './nip55Web.js';\nexport {\n BunkerSigner,\n connectWithBunkerUri,\n initiateNostrConnect,\n bytesToHex,\n hexToBytes,\n type BunkerPointer,\n type BunkerLoginOptions as BunkerConnectOptions,\n type BunkerConnectResult,\n type NostrConnectInitOptions,\n type NostrConnectInitiation,\n} from './nip46.js';\n","import { getPublicKey, nip19 } from 'nostr-tools';\nimport { BunkerSigner as ToolsBunkerSigner } from 'nostr-tools/nip46';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type {\n ActiveSigner,\n BunkerLoginOptions,\n Nip55WebLoginOptions,\n NostrConnectOptions,\n SignerConfig,\n SignerEvent,\n StoredAccount,\n UnlockOptions,\n} from './types.js';\nimport { localStorageAdapter, type StorageAdapter } from './storage.js';\nimport { LocalSigner } from './localSigner.js';\nimport { decryptNcryptsec, generateAccount } from '../nip49.js';\nimport { ExtensionSigner } from '../nip07.js';\nimport {\n BunkerSigner,\n bytesToHex,\n connectWithBunkerUri,\n hexToBytes,\n initiateNostrConnect,\n} from '../nip46.js';\nimport {\n AndroidSigner,\n loginWithAndroidSigner as connectWithAndroidSigner,\n type AndroidLoginOptions,\n type AndroidSignerAppInfo,\n type AndroidSignerPlugin,\n} from '../nip55.js';\nimport {\n Nip55WebSigner,\n browserNip55Transport,\n type Nip55WebSupport,\n type Nip55WebTransport,\n} from '../nip55Web.js';\n\nconst ACCOUNTS_KEY = 'accounts';\nconst ACTIVE_KEY = 'active-pubkey';\n\n/**\n * Multi-account Nostr signer with persistence.\n *\n * **Hydration.** The constructor reads previously-saved accounts from\n * the configured storage adapter. Every hydrated account starts\n * **locked**: present in `listAccounts()` and (if it was the active one\n * before) reachable via `getActiveAccount()`, but `getActiveSigner()`\n * returns `null` until the user re-authenticates. The matching\n * `loginWith*` method unlocks the active account.\n *\n * **Locked vs unlocked.** Use `getActiveAccount()` to render UI (\"logged\n * in as @alice\") and `getActiveSigner()` to decide whether the user can\n * actually sign. The pattern is \"show the account always, gate signing\n * on the signer.\"\n *\n * **Events.** Subscribe via `onChange()` to re-render when an account\n * is added, switched, or removed. See {@link SignerEvent}.\n */\nexport class Signer {\n readonly #storage: StorageAdapter;\n readonly #defaultAndroidPlugin: AndroidSignerPlugin | undefined;\n readonly #nip55WebTransport: Nip55WebTransport | undefined;\n readonly #appMetadata: { name?: string; url?: string; image?: string };\n #accounts: StoredAccount[] = [];\n #activePubkey: string | null = null;\n #activeSigner: ActiveSigner | null = null;\n #listeners = new Set<(event: SignerEvent) => void>();\n\n constructor(config: SignerConfig = {}) {\n this.#storage = config.storage ?? localStorageAdapter(config.storageKeyPrefix);\n this.#defaultAndroidPlugin = config.androidSignerPlugin;\n this.#nip55WebTransport = config.nip55WebTransport;\n this.#appMetadata = {\n name: config.appName,\n url: config.appUrl,\n image: config.appImage,\n };\n this.#hydrate();\n }\n\n #hydrate(): void {\n try {\n const raw = this.#storage.get(ACCOUNTS_KEY);\n if (raw) {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed)) this.#accounts = parsed as StoredAccount[];\n }\n this.#activePubkey = this.#storage.get(ACTIVE_KEY);\n } catch {\n this.#accounts = [];\n this.#activePubkey = null;\n }\n }\n\n #persistAccounts(): void {\n this.#storage.set(ACCOUNTS_KEY, JSON.stringify(this.#accounts));\n }\n\n #persistActive(): void {\n if (this.#activePubkey) this.#storage.set(ACTIVE_KEY, this.#activePubkey);\n else this.#storage.remove(ACTIVE_KEY);\n }\n\n #upsertAccount(account: StoredAccount): void {\n const idx = this.#accounts.findIndex(a => a.pubkey === account.pubkey);\n if (idx >= 0) this.#accounts[idx] = account;\n else this.#accounts.push(account);\n this.#persistAccounts();\n }\n\n /**\n * Release the currently-held signer, if it has a `close()`. Called\n * whenever the active signer is replaced or cleared so live resources\n * (bunker subscriptions, `visibilitychange` listeners, in-flight NIP-55\n * requests) don't outlive the session. Errors are swallowed — teardown\n * must never block a login/switch/logout.\n */\n #closeActiveSigner(): void {\n const signer = this.#activeSigner;\n if (!signer?.close) return;\n try {\n const result = signer.close();\n // close() may be async (BunkerSigner tears down relay subscriptions).\n if (result && typeof (result as Promise<void>).catch === 'function') {\n (result as Promise<void>).catch(() => {\n // best-effort teardown\n });\n }\n } catch {\n // best-effort teardown — never block a login/switch/logout\n }\n }\n\n #setActive(account: StoredAccount, signer: ActiveSigner): void {\n const wasDifferent = this.#activePubkey !== null && this.#activePubkey !== account.pubkey;\n // Close the outgoing signer only when it is actually being replaced —\n // re-asserting the same account (e.g. unlock) would otherwise tear\n // down the very signer we are handing back.\n if (this.#activeSigner !== signer) this.#closeActiveSigner();\n this.#activePubkey = account.pubkey;\n this.#activeSigner = signer;\n this.#persistActive();\n this.#emit({ type: wasDifferent ? 'switch' : 'login', account });\n }\n\n #emit(event: SignerEvent): void {\n for (const cb of this.#listeners) {\n try {\n cb(event);\n } catch {\n // listener errors are swallowed so one bad listener can't break others\n }\n }\n }\n\n /**\n * Generate a brand-new nsec, encrypt it with `passphrase` (NIP-49),\n * persist the resulting `ncryptsec` account, and activate it. Returns\n * the new account's `npub` and `ncryptsec` — the caller must surface\n * the `ncryptsec` to the user **immediately** since it is the only way\n * back into the account on a fresh device.\n *\n * @throws if `passphrase` is empty.\n */\n async createAccount(passphrase: string): Promise<{ npub: string; ncryptsec: string }> {\n if (!passphrase) throw new Error('createAccount: passphrase required');\n const { secretKey, pubkey, npub, ncryptsec } = generateAccount(passphrase);\n const account: StoredAccount = { npub, pubkey, method: 'ncryptsec', ncryptsec };\n this.#upsertAccount(account);\n this.#setActive(account, new LocalSigner(secretKey));\n return { npub, ncryptsec };\n }\n\n /**\n * Decrypt an ncryptsec with the user's passphrase, persist the account\n * (overwriting any previous entry for the same pubkey), and activate it.\n *\n * @throws if either argument is empty, or if the passphrase doesn't\n * decrypt the ncryptsec.\n */\n async loginWithNcryptsec(ncryptsec: string, passphrase: string): Promise<StoredAccount> {\n if (!ncryptsec) throw new Error('loginWithNcryptsec: ncryptsec required');\n if (!passphrase) throw new Error('loginWithNcryptsec: passphrase required');\n const secretKey = decryptNcryptsec(ncryptsec, passphrase);\n const pubkey = getPublicKey(secretKey);\n const npub = nip19.npubEncode(pubkey);\n const account: StoredAccount = { npub, pubkey, method: 'ncryptsec', ncryptsec };\n this.#upsertAccount(account);\n this.#setActive(account, new LocalSigner(secretKey));\n return account;\n }\n\n /**\n * Connect via the NIP-07 browser extension exposed at `window.nostr`.\n * The extension prompts the user for permission on first use.\n *\n * @throws if no extension is installed or the user denies the request.\n */\n async loginWithExtension(): Promise<StoredAccount> {\n const extension = new ExtensionSigner();\n const pubkey = await extension.getPublicKey();\n const npub = nip19.npubEncode(pubkey);\n const account: StoredAccount = { npub, pubkey, method: 'extension' };\n this.#upsertAccount(account);\n this.#setActive(account, extension);\n return account;\n }\n\n /**\n * Connect to a NIP-46 remote signer via a `bunker://` URI. Relays are\n * read from the URI itself — no hardcoded fallbacks. Pass a `pool`\n * to reuse an existing relay connection; pass `clientSecretKey` to\n * resume a previous session (the hex from `StoredAccount.nip46`).\n *\n * @throws if the URI is malformed, no relay is reachable, or the\n * remote signer rejects pairing within the implementation's timeout.\n */\n async loginWithBunkerUri(\n uri: string,\n options: BunkerLoginOptions = {},\n ): Promise<StoredAccount> {\n const result = await connectWithBunkerUri(uri, options);\n const npub = nip19.npubEncode(result.pubkey);\n const account: StoredAccount = {\n npub,\n pubkey: result.pubkey,\n method: 'nip46',\n nip46: {\n uri,\n remoteSignerPubkey: result.pointer.pubkey,\n relays: result.pointer.relays,\n clientSecretKey: bytesToHex(result.clientSecretKey),\n },\n };\n this.#upsertAccount(account);\n this.#setActive(account, result.signer);\n return account;\n }\n\n /**\n * Initiate a NIP-46 `nostrconnect://` pairing. Generates a client\n * keypair, publishes a connect request to the supplied `relays`, and\n * waits for a remote signer to pair. Call `options.onUri(uri)` to\n * render the URI as a QR code; the returned promise resolves once\n * pairing completes. Cancel by aborting `options.signal`.\n *\n * @throws if `relays` is empty, the user aborts, the pairing times\n * out, or no signer responds.\n */\n async loginWithNostrConnect(options: NostrConnectOptions): Promise<StoredAccount> {\n if (options.relays.length === 0) {\n throw new Error('loginWithNostrConnect: at least one relay required');\n }\n // Merge per-call metadata over SignerConfig defaults (appName/url/image).\n // `name` is required — Amber (and likely other NIP-55 signer apps) gate\n // the consent UI on having a recognizable client identity in the URI.\n // Without one they will receive the request but never surface\n // approve/deny buttons, leaving the pairing silently stuck.\n const metadata = {\n name: options.metadata?.name ?? this.#appMetadata.name,\n url: options.metadata?.url ?? this.#appMetadata.url,\n image: options.metadata?.image ?? this.#appMetadata.image,\n };\n if (!metadata.name) {\n throw new Error(\n '@formstr/signer: loginWithNostrConnect requires an app name. Set `appName` in createSigner() or pass `metadata.name` to loginWithNostrConnect().',\n );\n }\n const init = initiateNostrConnect({\n relays: options.relays,\n metadata,\n perms: options.perms,\n pool: options.pool,\n onAuth: options.onAuth,\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n onRelayMismatch: options.onRelayMismatch,\n });\n options.onUri(init.uri);\n const result = await init.complete;\n const npub = nip19.npubEncode(result.pubkey);\n const account: StoredAccount = {\n npub,\n pubkey: result.pubkey,\n method: 'nip46',\n nip46: {\n uri: init.uri,\n remoteSignerPubkey: result.pointer.pubkey,\n relays: result.pointer.relays,\n clientSecretKey: bytesToHex(result.clientSecretKey),\n },\n };\n this.#upsertAccount(account);\n this.#setActive(account, result.signer);\n return account;\n }\n\n /**\n * Enumerate NIP-55 signer apps installed on the device, via the\n * configured Android plugin (or `plugin` if supplied). Useful for\n * rendering a \"pick your signer\" list — the built-in UI does this\n * automatically when the Android tab is selected.\n *\n * Only meaningful inside a Capacitor Android shell. On web/iOS the\n * configured plugin is typically absent and this throws.\n *\n * @throws if no plugin is configured and none is passed in.\n */\n async listAndroidSignerApps(\n plugin?: AndroidSignerPlugin,\n ): Promise<AndroidSignerAppInfo[]> {\n const p = plugin ?? this.#defaultAndroidPlugin;\n if (!p) {\n throw new Error(\n '@formstr/signer: no Android signer plugin configured (pass `androidSignerPlugin` to createSigner or `plugin` to listAndroidSignerApps)',\n );\n }\n const { apps } = await p.getInstalledSignerApps();\n return apps;\n }\n\n /**\n * Sign in via a NIP-55 Android external signer (Amber, etc). If\n * `options.packageName` is given, that specific signer app is invoked;\n * otherwise the plugin picks a default (typically the only installed\n * signer, or an OS chooser). Pass `options.plugin` to override the\n * configured default for this call.\n *\n * @throws if no plugin is configured, the signer app cannot be\n * resolved to a package name, or the user denies the request.\n */\n async loginWithAndroidSigner(options: AndroidLoginOptions = {}): Promise<StoredAccount> {\n const plugin = options.plugin ?? this.#defaultAndroidPlugin;\n if (!plugin) {\n throw new Error(\n '@formstr/signer: no Android signer plugin configured (pass `androidSignerPlugin` to createSigner or `plugin` to loginWithAndroidSigner)',\n );\n }\n const result = await connectWithAndroidSigner(plugin, options.packageName);\n const account: StoredAccount = {\n npub: result.npub,\n pubkey: result.pubkey,\n method: 'android',\n androidPackageName: result.packageName,\n };\n this.#upsertAccount(account);\n this.#setActive(account, result.signer);\n return account;\n }\n\n /**\n * Whether `loginWithNip55Web` can run in this environment — a plain\n * Android browser with async clipboard access. False in a Capacitor\n * native shell (use {@link loginWithAndroidSigner} there), and on\n * desktop/iOS/SSR.\n *\n * A **capability** check, not an availability one: there is no web API\n * to detect an installed Android app, so this says nothing about\n * whether a signer app is actually installed. Use it to hide the\n * browser flow where it cannot work, not to promise that it will.\n */\n supportsNip55Web(transport?: Nip55WebTransport): boolean {\n return this.nip55WebSupport(transport).visible;\n }\n\n /**\n * Whether to offer the browser NIP-55 option, plus an advisory `warning`\n * where it is known to be flaky. Prefer this in UI code.\n *\n * Nothing here is a hard block. `visible: false` covers only environments\n * where the mechanism cannot exist — a Capacitor native shell (the plugin\n * path is better) and non-Android platforms (no `nostrsigner` handler).\n * Firefox for Android is `visible: true` **with a warning**, because it\n * advertises `readText` but never grants a persistent permission, so the\n * poll loop usually cannot complete. Users are still allowed to try.\n */\n nip55WebSupport(transport?: Nip55WebTransport): Nip55WebSupport {\n const t = transport ?? this.#nip55WebTransport ?? browserNip55Transport();\n if (t.supportStatus) return t.supportStatus();\n return { visible: t.isSupported() };\n }\n\n /**\n * Sign in via a NIP-55 Android external signer (Amber, etc) **from a\n * plain browser**, with no Capacitor/native bridge. Opens the installed\n * signer app through a `nostrsigner` intent and reads the result back\n * from the clipboard once the user returns to the tab. Because the\n * intent names no package, this works with any app that registered the\n * `nostrsigner` scheme — one opens directly, several show the Android\n * \"Open with\" chooser.\n *\n * Not for native builds — inside a Capacitor shell use\n * {@link loginWithAndroidSigner}, which needs no clipboard and no\n * per-operation approval.\n *\n * Every operation is a separate approval, and a rejection is\n * indistinguishable from the user simply not returning, so callers must\n * impose their own timeout. Prefer NIP-46 when a persistent session is\n * acceptable — the NIP-55 spec recommends it for web clients.\n *\n * @throws if the environment cannot run the flow (not Android, no async\n * clipboard) or the signer returns an unexpected value.\n */\n async loginWithNip55Web(options: Nip55WebLoginOptions = {}): Promise<StoredAccount> {\n const transport = options.transport ?? this.#nip55WebTransport;\n // Pair with a throwaway signer so `options.signal` cancels only the\n // pairing attempt. The signal belongs to the login call, not to the\n // account — retaining it would poison the signer, since hosts commonly\n // abort the login controller once the modal closes on success.\n const pairing = new Nip55WebSigner({\n transport,\n pollIntervalMs: options.pollIntervalMs,\n timeoutMs: options.timeoutMs,\n signal: options.signal,\n debug: options.debug,\n });\n // getPublicKey throws for an unsupported environment (not Android / no\n // async clipboard) or an aborted pairing, before anything is persisted.\n const pubkey = await pairing.getPublicKey();\n const signer = new Nip55WebSigner({\n transport,\n pollIntervalMs: options.pollIntervalMs,\n timeoutMs: options.timeoutMs,\n pubkey,\n debug: options.debug,\n });\n const npub = nip19.npubEncode(pubkey);\n const account: StoredAccount = {\n npub,\n pubkey,\n method: 'nip55-web',\n };\n this.#upsertAccount(account);\n this.#setActive(account, signer);\n return account;\n }\n\n /** Snapshot of every persisted account, in insertion order. */\n listAccounts(): StoredAccount[] {\n return [...this.#accounts];\n }\n\n /**\n * The currently selected account, or `null` if none. Present even when\n * the account is locked (no active signer yet). Use this to render\n * \"logged in as @alice\" — pair with {@link getActiveSigner} to decide\n * whether signing is actually available.\n */\n getActiveAccount(): StoredAccount | null {\n if (!this.#activePubkey) return null;\n return this.#accounts.find(a => a.pubkey === this.#activePubkey) ?? null;\n }\n\n /**\n * The unlocked signer for the active account, or `null` if locked.\n * After a fresh page load this is `null` for every account type\n * (passphrase / extension grant / signer-app handshake all need to\n * be redone). Calling the matching `loginWith*` method unlocks it.\n */\n getActiveSigner(): ActiveSigner | null {\n return this.#activeSigner;\n }\n\n /**\n * Silently unlock the active account from persisted state — no user\n * prompt, no fresh pairing. The package already keeps everything it\n * needs to reconstruct the runtime signer on disk; this method is the\n * way to actually use that on cold start instead of re-running each\n * method's first-time login flow.\n *\n * Behavior by method:\n *\n * - `extension`: constructs an {@link ExtensionSigner}, which just\n * proxies to `window.nostr`. No setup roundtrip — individual\n * operations may still prompt depending on the extension's own\n * permission state, but unlock itself does not.\n *\n * - `nip46`: reuses the stored `clientSecretKey` to construct a\n * {@link BunkerSigner} against the stored bunker pubkey + relays\n * via `BunkerSigner.fromBunker`. Deliberately skips the `connect`\n * request — the remote signer (Amber etc.) approved this client\n * pubkey on first pairing and re-sending `connect` is what surfaces\n * a fresh approval prompt every cold start. Requires `options.pool`\n * so the BunkerSigner has somewhere to listen for responses.\n * The cached user pubkey is fed into the wrapper so a follow-up\n * `getPublicKey()` is a memory read, not a relay request.\n *\n * - `android`: constructs an {@link AndroidSigner} directly from the\n * stored `androidPackageName` + `pubkey` + `npub`. Skips the\n * `getPublicKey` content-provider roundtrip that\n * {@link loginWithAndroidSigner} performs and that — on Amber —\n * surfaces as a permission prompt every cold start.\n *\n * - `nip55-web`: constructs a {@link Nip55WebSigner} with the stored\n * `pubkey` cached. Like `android`, this opens no signer app during\n * unlock; the first sign/encrypt call is what prompts.\n *\n * - `ncryptsec`: returns `null`. There is no silent path — the user's\n * passphrase isn't (and shouldn't be) persisted. The caller must\n * drive the passphrase prompt and call {@link loginWithNcryptsec}.\n *\n * Returns `null` (without emitting any event or mutating state) when\n * there is no active account, when the account is missing fields\n * required to unlock, when `nip46` is the method but no `pool` was\n * supplied, or when `android` is the method but no plugin is\n * configured. On success emits the same `login` / `switch` event the\n * corresponding `loginWith*` would.\n */\n async unlock(options: UnlockOptions = {}): Promise<ActiveSigner | null> {\n const account = this.getActiveAccount();\n if (!account) return null;\n\n switch (account.method) {\n case 'extension': {\n const signer = new ExtensionSigner();\n this.#setActive(account, signer);\n return signer;\n }\n\n case 'nip46': {\n if (!account.nip46) return null;\n if (!options.pool) return null;\n const { remoteSignerPubkey, relays, clientSecretKey } = account.nip46;\n if (!remoteSignerPubkey || !relays.length || !clientSecretKey) {\n return null;\n }\n const tools = ToolsBunkerSigner.fromBunker(\n hexToBytes(clientSecretKey),\n { pubkey: remoteSignerPubkey, relays, secret: null },\n { pool: options.pool },\n );\n // No tools.connect() — the bunker already approved this client\n // on first pairing; re-sending `connect` is what triggers the\n // cold-start approval prompt we are trying to avoid.\n const signer = new BunkerSigner(tools, account.pubkey);\n this.#setActive(account, signer);\n return signer;\n }\n\n case 'android': {\n if (!account.androidPackageName) return null;\n if (!this.#defaultAndroidPlugin) return null;\n const signer = new AndroidSigner(\n this.#defaultAndroidPlugin,\n account.androidPackageName,\n account.npub,\n account.pubkey,\n );\n this.#setActive(account, signer);\n return signer;\n }\n\n case 'nip55-web': {\n // Resume with the cached pubkey so unlocking does not itself open\n // the signer app; only the next sign/encrypt call does.\n const signer = new Nip55WebSigner({\n transport: this.#nip55WebTransport,\n pubkey: account.pubkey,\n });\n this.#setActive(account, signer);\n return signer;\n }\n\n case 'ncryptsec':\n // No silent path — passphrase is not (and must not be) persisted.\n return null;\n }\n }\n\n /**\n * Make `pubkey` the active account. Clears the in-memory signer —\n * the new account starts **locked** even if it was previously\n * unlocked in this session.\n *\n * @throws if `pubkey` does not match any persisted account.\n */\n async switchAccount(pubkey: string): Promise<void> {\n const account = this.#accounts.find(a => a.pubkey === pubkey);\n if (!account) throw new Error(`switchAccount: no account for pubkey ${pubkey}`);\n this.#closeActiveSigner();\n this.#activePubkey = pubkey;\n this.#activeSigner = null;\n this.#persistActive();\n this.#emit({ type: 'switch', account });\n }\n\n /**\n * Remove an account from storage. `pubkey` defaults to the active\n * account. If the active account is removed, the in-memory signer is\n * cleared. No-op if there is nothing to remove.\n */\n async logout(pubkey?: string): Promise<void> {\n const target = pubkey ?? this.#activePubkey;\n if (!target) return;\n this.#accounts = this.#accounts.filter(a => a.pubkey !== target);\n this.#persistAccounts();\n if (this.#activePubkey === target) {\n this.#closeActiveSigner();\n this.#activePubkey = null;\n this.#activeSigner = null;\n this.#persistActive();\n }\n this.#emit({ type: 'logout', pubkey: target });\n }\n\n /**\n * Subscribe to account-state changes. Returns an unsubscribe function.\n * Listener errors are swallowed so one bad listener can't break others.\n * See {@link SignerEvent} for the variants.\n */\n onChange(cb: (event: SignerEvent) => void): () => void {\n this.#listeners.add(cb);\n return () => {\n this.#listeners.delete(cb);\n };\n }\n}\n\n/** Convenience wrapper around `new Signer(config)`. */\nexport function createSigner(config: SignerConfig = {}): Signer {\n return new Signer(config);\n}\n","export interface StorageAdapter {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\nconst DEFAULT_PREFIX = '@formstr/signer:';\n\nexport function localStorageAdapter(prefix: string = DEFAULT_PREFIX): StorageAdapter {\n const ls = (): Storage | null => {\n try {\n return typeof globalThis !== 'undefined' && globalThis.localStorage\n ? globalThis.localStorage\n : null;\n } catch {\n return null;\n }\n };\n return {\n get(key) {\n try {\n return ls()?.getItem(prefix + key) ?? null;\n } catch {\n return null;\n }\n },\n set(key, value) {\n try {\n ls()?.setItem(prefix + key, value);\n } catch {\n // swallow quota / privacy-mode errors\n }\n },\n remove(key) {\n try {\n ls()?.removeItem(prefix + key);\n } catch {\n // swallow\n }\n },\n };\n}\n","import {\n finalizeEvent,\n getPublicKey,\n nip04,\n nip44,\n type Event as NostrEvent,\n type EventTemplate,\n} from 'nostr-tools';\nimport type { ActiveSigner } from './types.js';\n\n/**\n * ActiveSigner backed by a raw secret key held in memory.\n * The secret key never leaves this object — there is no getter for it.\n */\nexport class LocalSigner implements ActiveSigner {\n readonly #secretKey: Uint8Array;\n\n constructor(secretKey: Uint8Array) {\n this.#secretKey = secretKey;\n }\n\n async getPublicKey(): Promise<string> {\n return getPublicKey(this.#secretKey);\n }\n\n async signEvent(event: EventTemplate): Promise<NostrEvent> {\n return finalizeEvent(event, this.#secretKey);\n }\n\n async nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n return nip04.encrypt(this.#secretKey, peerPubkey, plaintext);\n }\n\n async nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n return nip04.decrypt(this.#secretKey, peerPubkey, ciphertext);\n }\n\n async nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const key = nip44.v2.utils.getConversationKey(this.#secretKey, peerPubkey);\n return nip44.v2.encrypt(plaintext, key);\n }\n\n async nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const key = nip44.v2.utils.getConversationKey(this.#secretKey, peerPubkey);\n return nip44.v2.decrypt(ciphertext, key);\n }\n}\n","import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';\nimport { encrypt as nip49Encrypt, decrypt as nip49Decrypt } from 'nostr-tools/nip49';\n\nexport function encryptSecretKey(secretKey: Uint8Array, passphrase: string): string {\n return nip49Encrypt(secretKey, passphrase);\n}\n\nexport function decryptNcryptsec(ncryptsec: string, passphrase: string): Uint8Array {\n return nip49Decrypt(ncryptsec, passphrase);\n}\n\nexport interface GeneratedAccount {\n secretKey: Uint8Array;\n pubkey: string;\n npub: string;\n ncryptsec: string;\n}\n\nexport function generateAccount(passphrase: string): GeneratedAccount {\n const secretKey = generateSecretKey();\n const pubkey = getPublicKey(secretKey);\n const npub = nip19.npubEncode(pubkey);\n const ncryptsec = nip49Encrypt(secretKey, passphrase);\n return { secretKey, pubkey, npub, ncryptsec };\n}\n","import type { Event as NostrEvent, EventTemplate } from 'nostr-tools';\nimport type { ActiveSigner } from './core/types.js';\n\nexport interface WindowNostr {\n getPublicKey(): Promise<string>;\n signEvent(event: EventTemplate): Promise<NostrEvent>;\n getRelays?(): Promise<Record<string, { read: boolean; write: boolean }>>;\n nip04?: {\n encrypt(peerPubkey: string, plaintext: string): Promise<string>;\n decrypt(peerPubkey: string, ciphertext: string): Promise<string>;\n };\n nip44?: {\n encrypt(peerPubkey: string, plaintext: string): Promise<string>;\n decrypt(peerPubkey: string, ciphertext: string): Promise<string>;\n };\n}\n\nexport function getWindowNostr(): WindowNostr {\n const nostr = (globalThis as { nostr?: WindowNostr }).nostr;\n if (!nostr) {\n throw new Error(\n '@formstr/signer: NIP-07 extension not found (globalThis.nostr is undefined)',\n );\n }\n return nostr;\n}\n\nexport class ExtensionSigner implements ActiveSigner {\n async getPublicKey(): Promise<string> {\n return getWindowNostr().getPublicKey();\n }\n\n async signEvent(event: EventTemplate): Promise<NostrEvent> {\n return getWindowNostr().signEvent(event);\n }\n\n async nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const ext = getWindowNostr();\n if (!ext.nip04) throw new Error('NIP-07 extension does not expose nip04');\n return ext.nip04.encrypt(peerPubkey, plaintext);\n }\n\n async nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const ext = getWindowNostr();\n if (!ext.nip04) throw new Error('NIP-07 extension does not expose nip04');\n return ext.nip04.decrypt(peerPubkey, ciphertext);\n }\n\n async nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const ext = getWindowNostr();\n if (!ext.nip44) throw new Error('NIP-07 extension does not expose nip44');\n return ext.nip44.encrypt(peerPubkey, plaintext);\n }\n\n async nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const ext = getWindowNostr();\n if (!ext.nip44) throw new Error('NIP-07 extension does not expose nip44');\n return ext.nip44.decrypt(peerPubkey, ciphertext);\n }\n}\n","import { generateSecretKey, getPublicKey } from 'nostr-tools';\nimport {\n BunkerSigner as ToolsBunkerSigner,\n createNostrConnectURI,\n parseBunkerInput,\n type BunkerPointer,\n} from 'nostr-tools/nip46';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Event as NostrEvent, EventTemplate } from 'nostr-tools';\nimport type { ActiveSigner, RelayMismatchHandler } from './core/types.js';\n\nexport type { BunkerPointer };\n\n/**\n * Thin wrapper around nostr-tools' BunkerSigner that exposes only the\n * ActiveSigner surface. We keep this layer so callers depend on a stable\n * interface even if we ever swap the underlying implementation.\n *\n * Optionally accepts a `cachedUserPubkey`. When supplied, {@link getPublicKey}\n * returns it without a bunker roundtrip. The user's signer pubkey is fixed\n * for a given paired account, so caching it after the initial `connect` —\n * or feeding it back in from persisted storage on unlock — avoids both a\n * network hop and a potential approval prompt on every cold start. Without\n * a cached value we fall back to asking the bunker, matching the prior\n * behavior.\n */\nexport class BunkerSigner implements ActiveSigner {\n readonly #delegate: ToolsBunkerSigner;\n readonly #cachedUserPubkey: string | null;\n\n constructor(delegate: ToolsBunkerSigner, cachedUserPubkey?: string) {\n this.#delegate = delegate;\n this.#cachedUserPubkey = cachedUserPubkey ?? null;\n }\n\n getPublicKey(): Promise<string> {\n if (this.#cachedUserPubkey !== null) {\n return Promise.resolve(this.#cachedUserPubkey);\n }\n return this.#delegate.getPublicKey();\n }\n signEvent(event: EventTemplate): Promise<NostrEvent> {\n return this.#delegate.signEvent(event);\n }\n nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n return this.#delegate.nip04Encrypt(peerPubkey, plaintext);\n }\n nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n return this.#delegate.nip04Decrypt(peerPubkey, ciphertext);\n }\n nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n return this.#delegate.nip44Encrypt(peerPubkey, plaintext);\n }\n nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n return this.#delegate.nip44Decrypt(peerPubkey, ciphertext);\n }\n async close(): Promise<void> {\n return this.#delegate.close();\n }\n}\n\nexport interface BunkerLoginOptions {\n /** Custom pool, e.g. for tests. Defaults to a new SimplePool inside nostr-tools. */\n pool?: AbstractSimplePool;\n /** Called when the remote signer needs the user to visit an auth URL. */\n onAuth?: (url: string) => void;\n /** Optional client session keypair (hex bytes). Auto-generated if omitted. */\n clientSecretKey?: Uint8Array;\n /** Notified when the bunker's preferred relays differ from the URI's. */\n onRelayMismatch?: RelayMismatchHandler;\n /**\n * NIP-46 permissions to request as the 3rd `connect` param\n * (e.g. `['sign_event:1', 'nip44_encrypt']`). When omitted, the\n * connect request carries no perms — bunker UIs may then skip the\n * approval prompt entirely, leaving the user with nothing to tap.\n */\n perms?: string[];\n}\n\nasync function fetchBunkerRelays(tools: ToolsBunkerSigner): Promise<string[] | null> {\n try {\n const resp = await tools.sendRequest('get_relays', []);\n const parsed = JSON.parse(resp) as unknown;\n if (Array.isArray(parsed)) {\n return parsed.filter((r): r is string => typeof r === 'string');\n }\n if (typeof parsed === 'object' && parsed !== null) {\n return Object.keys(parsed as Record<string, unknown>);\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction relayListsMatch(a: string[], b: string[]): boolean {\n if (a.length !== b.length) return false;\n const sa = [...a].sort();\n const sb = [...b].sort();\n for (let i = 0; i < sa.length; i++) if (sa[i] !== sb[i]) return false;\n return true;\n}\n\nasync function resolveRelayChoice(\n tools: ToolsBunkerSigner,\n userRelays: string[],\n onRelayMismatch: RelayMismatchHandler | undefined,\n): Promise<string[]> {\n if (!onRelayMismatch) return userRelays;\n const bunkerRelays = await fetchBunkerRelays(tools);\n if (!bunkerRelays || relayListsMatch(userRelays, bunkerRelays)) return userRelays;\n const accept = await onRelayMismatch({ userRelays, bunkerRelays });\n return accept ? bunkerRelays : userRelays;\n}\n\nexport interface BunkerConnectResult {\n signer: BunkerSigner;\n pubkey: string;\n pointer: BunkerPointer;\n clientSecretKey: Uint8Array;\n}\n\n/**\n * Connect to a remote signer via a bunker:// URI (or a NIP-05 identifier).\n * Relays come from the URI — there is no fallback default list.\n */\nexport async function connectWithBunkerUri(\n uri: string,\n options: BunkerLoginOptions = {},\n): Promise<BunkerConnectResult> {\n const pointer = await parseBunkerInput(uri);\n if (!pointer) {\n throw new Error('@formstr/signer: invalid bunker URI');\n }\n if (!pointer.relays?.length) {\n throw new Error('@formstr/signer: bunker URI must include at least one relay');\n }\n const clientSecretKey = options.clientSecretKey ?? generateSecretKey();\n const tools = ToolsBunkerSigner.fromBunker(clientSecretKey, pointer, {\n pool: options.pool,\n onauth: options.onAuth,\n });\n // nostr-tools' BunkerSigner.connect() hardcodes only [pubkey, secret],\n // dropping the optional 3rd `perms` arg defined by NIP-46. Without it\n // bunker UIs (Amber, etc.) have no permissions to authorize and may\n // skip the approval prompt entirely. We send the request directly.\n await tools.sendRequest('connect', [\n pointer.pubkey,\n pointer.secret ?? '',\n (options.perms ?? []).join(','),\n ]);\n const pubkey = await tools.getPublicKey();\n const resolvedRelays = await resolveRelayChoice(\n tools,\n pointer.relays,\n options.onRelayMismatch,\n );\n return {\n signer: new BunkerSigner(tools, pubkey),\n pubkey,\n pointer: { ...pointer, relays: resolvedRelays },\n clientSecretKey,\n };\n}\n\nexport interface NostrConnectInitOptions {\n /** User-supplied relays. The whole point of the strict-relay rule. */\n relays: string[];\n metadata?: { name?: string; url?: string; image?: string };\n /** Permissions to request (NIP-46 perms list, e.g. [\"sign_event:1\",\"nip44_encrypt\"]). */\n perms?: string[];\n pool?: AbstractSimplePool;\n onAuth?: (url: string) => void;\n /** Override the auto-generated client session keypair. */\n clientSecretKey?: Uint8Array;\n /** Override the auto-generated URI secret. */\n secret?: string;\n /** Abort the pairing wait. */\n signal?: AbortSignal;\n /** Max wait for pairing in ms (default 5 minutes). */\n timeoutMs?: number;\n /** Notified when the bunker's preferred relays differ from the user's. */\n onRelayMismatch?: RelayMismatchHandler;\n}\n\nexport interface NostrConnectInitiation {\n uri: string;\n clientPubkey: string;\n complete: Promise<BunkerConnectResult>;\n}\n\n/**\n * Generate a nostrconnect:// URI and wait for the remote signer to pair.\n * The caller displays the URI (typically as a QR code), and the returned\n * `complete` promise resolves once the signer connects back.\n */\nexport function initiateNostrConnect(options: NostrConnectInitOptions): NostrConnectInitiation {\n if (options.relays.length === 0) {\n throw new Error('@formstr/signer: at least one relay is required for nostrconnect');\n }\n const clientSecretKey = options.clientSecretKey ?? generateSecretKey();\n const clientPubkey = getPublicKey(clientSecretKey);\n const secret = options.secret ?? Math.random().toString(36).slice(2);\n const uri = createNostrConnectURI({\n clientPubkey,\n relays: options.relays,\n secret,\n perms: options.perms,\n name: options.metadata?.name,\n url: options.metadata?.url,\n image: options.metadata?.image,\n });\n const maxWaitOrAbort: number | AbortSignal =\n options.signal ?? options.timeoutMs ?? 300_000;\n // skipSwitchRelays:true — keep the caller-supplied relays authoritative,\n // never silently swap to whatever the bunker prefers.\n const complete = ToolsBunkerSigner.fromURI(\n clientSecretKey,\n uri,\n { pool: options.pool, onauth: options.onAuth, skipSwitchRelays: true },\n maxWaitOrAbort,\n ).then(async (tools) => {\n const pubkey = await tools.getPublicKey();\n const resolvedRelays = await resolveRelayChoice(\n tools,\n options.relays,\n options.onRelayMismatch,\n );\n return {\n signer: new BunkerSigner(tools, pubkey),\n pubkey,\n pointer: { ...tools.bp, relays: resolvedRelays },\n clientSecretKey,\n };\n });\n return { uri, clientPubkey, complete };\n}\n\nconst hexAlphabet = '0123456789abcdef';\n\nexport function bytesToHex(bytes: Uint8Array): string {\n let s = '';\n for (const b of bytes) s += hexAlphabet[b >> 4] + hexAlphabet[b & 0xf];\n return s;\n}\n\nexport function hexToBytes(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error('hexToBytes: odd-length hex string');\n const out = new Uint8Array(hex.length / 2);\n for (let i = 0; i < out.length; i++) {\n out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n }\n return out;\n}\n","import { getEventHash, nip19, type Event as NostrEvent, type EventTemplate } from 'nostr-tools';\nimport type { ActiveSigner } from './core/types.js';\n\n/**\n * Subset of `nostr-signer-capacitor-plugin`'s exported `NostrSignerPlugin`\n * that we depend on. Signatures intentionally mirror that library\n * (positional args, per-call `packageName`) so the real plugin is\n * structurally assignable here — and any mock written against this\n * interface is a faithful stand-in. The conformance is enforced by a\n * compile-time guard in `tests/helpers/mockAndroidPlugin.ts`.\n */\nexport interface AndroidSignerAppInfo {\n name: string;\n packageName: string;\n iconUrl?: string;\n}\n\nexport interface AndroidSignerPlugin {\n setPackageName(packageName: string): Promise<void>;\n getInstalledSignerApps(): Promise<{ apps: AndroidSignerAppInfo[] }>;\n getPublicKey(\n packageName?: string,\n permissions?: string,\n ): Promise<{ npub: string; package: string }>;\n signEvent(\n packageName: string,\n eventJson: string,\n id: string,\n npub: string,\n ): Promise<{ signature: string; id: string; event: string }>;\n nip04Encrypt(\n packageName: string,\n plainText: string,\n id: string,\n pubKey: string,\n npub: string,\n ): Promise<{ result: string; id: string }>;\n nip04Decrypt(\n packageName: string,\n encryptedText: string,\n id: string,\n pubKey: string,\n npub: string,\n ): Promise<{ result: string; id: string }>;\n nip44Encrypt(\n packageName: string,\n plainText: string,\n id: string,\n pubKey: string,\n npub: string,\n ): Promise<{ result: string; id: string }>;\n nip44Decrypt(\n packageName: string,\n encryptedText: string,\n id: string,\n pubKey: string,\n npub: string,\n ): Promise<{ result: string; id: string }>;\n}\n\nexport interface AndroidLoginOptions {\n /** The Android package name of the external signer app (e.g. com.greenart7c3.nostrsigner). */\n packageName?: string;\n /** Override the plugin for this call. Falls back to SignerConfig.androidSignerPlugin. */\n plugin?: AndroidSignerPlugin;\n}\n\nexport class AndroidSigner implements ActiveSigner {\n readonly #plugin: AndroidSignerPlugin;\n readonly #packageName: string;\n readonly #npub: string;\n readonly #pubkey: string;\n\n constructor(\n plugin: AndroidSignerPlugin,\n packageName: string,\n npub: string,\n pubkey: string,\n ) {\n this.#plugin = plugin;\n this.#packageName = packageName;\n this.#npub = npub;\n this.#pubkey = pubkey;\n }\n\n async getPublicKey(): Promise<string> {\n return this.#pubkey;\n }\n\n async signEvent(event: EventTemplate): Promise<NostrEvent> {\n const unsigned = { ...event, pubkey: this.#pubkey };\n const eventId = getEventHash(unsigned);\n const result = await this.#plugin.signEvent(\n this.#packageName,\n JSON.stringify(unsigned),\n eventId,\n this.#npub,\n );\n return JSON.parse(result.event) as NostrEvent;\n }\n\n async nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const { result } = await this.#plugin.nip04Encrypt(\n this.#packageName,\n plaintext,\n '',\n peerPubkey,\n this.#npub,\n );\n return result;\n }\n\n async nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const { result } = await this.#plugin.nip04Decrypt(\n this.#packageName,\n ciphertext,\n '',\n peerPubkey,\n this.#npub,\n );\n return result;\n }\n\n async nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n const { result } = await this.#plugin.nip44Encrypt(\n this.#packageName,\n plaintext,\n '',\n peerPubkey,\n this.#npub,\n );\n return result;\n }\n\n async nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n const { result } = await this.#plugin.nip44Decrypt(\n this.#packageName,\n ciphertext,\n '',\n peerPubkey,\n this.#npub,\n );\n return result;\n }\n}\n\nexport interface AndroidLoginResult {\n signer: AndroidSigner;\n pubkey: string;\n npub: string;\n packageName: string;\n}\n\n/**\n * Render a debuggable summary of what the Android signer plugin returned\n * where an npub was expected. Includes type and length, plus a truncated\n * prefix that preserves the bech32 HRP (so callers can tell `nsec1…` /\n * `nprofile1…` / a raw hex pubkey apart) without leaking the full secret\n * material that an erroneous `nsec` response would carry.\n */\nfunction describeIdentifier(value: unknown): string {\n if (value === null) return 'null';\n if (value === undefined) return 'undefined';\n if (typeof value !== 'string') {\n return `<${typeof value}>`;\n }\n if (value.length === 0) return 'empty string';\n const prefix = value.slice(0, 12);\n const suffix = value.length > 12 ? '…' : '';\n return `\"${prefix}${suffix}\" (length=${value.length})`;\n}\n\n/**\n * Lowercase 32-byte hex pubkey shape. NIP-55 nominally returns an `npub`\n * in the `signature` extra, but recent Amber builds put the raw hex\n * pubkey there instead — we accept either form and normalize internally.\n */\nconst HEX_PUBKEY_RE = /^[0-9a-f]{64}$/i;\n\nexport async function loginWithAndroidSigner(\n plugin: AndroidSignerPlugin,\n packageName?: string,\n): Promise<AndroidLoginResult> {\n if (packageName) {\n await plugin.setPackageName(packageName);\n }\n const { npub: rawIdentifier, package: pluginPackage } =\n await plugin.getPublicKey(packageName);\n const resolvedPackage = pluginPackage || packageName;\n if (!resolvedPackage) {\n throw new Error(\n '@formstr/signer: android signer did not return a package name and none was supplied',\n );\n }\n const { pubkey, npub } = normalizeNip55Identifier(rawIdentifier);\n return {\n signer: new AndroidSigner(plugin, resolvedPackage, npub, pubkey),\n pubkey,\n npub,\n packageName: resolvedPackage,\n };\n}\n\n/**\n * Normalize whatever a NIP-55 transport handed us in the pubkey slot into a\n * `{ pubkey, npub }` pair. Accepts:\n * - a 32-byte lowercase hex pubkey (newer Amber builds return this),\n * - a bech32 `npub1…` (the original NIP-55 spec shape),\n * - a bech32 `nprofile1…`, which the web/clipboard transport may return.\n * Throws a debuggable error for anything else, including the preview of\n * what was actually received so the caller can triage.\n *\n * Shared by the Capacitor (`loginWithAndroidSigner`) and pure-web\n * (`nip55Web.ts`) flows — both transports are NIP-55 and must agree on\n * how identifiers normalize.\n */\nexport function normalizeNip55Identifier(\n rawIdentifier: unknown,\n): { pubkey: string; npub: string } {\n if (typeof rawIdentifier === 'string' && HEX_PUBKEY_RE.test(rawIdentifier)) {\n const pubkey = rawIdentifier.toLowerCase();\n return { pubkey, npub: nip19.npubEncode(pubkey) };\n }\n // Wrap nip19.decode so a bech32 failure (\"Data must be at least 6\n // characters long\", \"Invalid checksum\", ...) surfaces what the transport\n // actually returned. Without this, callers see an opaque bech32 crash\n // and can't tell whether the signer sent back an empty string, an nsec,\n // or something else entirely.\n let decoded: ReturnType<typeof nip19.decode>;\n try {\n decoded = nip19.decode(rawIdentifier as string);\n } catch (e) {\n // nostr-tools' nip19 decoder always throws Error instances on bech32\n // failures. Pass the message straight through.\n throw new Error(\n `@formstr/signer: signer returned an undecodable identifier (got ${describeIdentifier(rawIdentifier)}): ${(e as Error).message}`,\n );\n }\n if (decoded.type === 'npub') {\n return { pubkey: decoded.data, npub: rawIdentifier as string };\n }\n if (decoded.type === 'nprofile') {\n const pubkey = decoded.data.pubkey;\n return { pubkey, npub: nip19.npubEncode(pubkey) };\n }\n throw new Error(\n `@formstr/signer: signer returned a non-pubkey identifier (type=${decoded.type}, got ${describeIdentifier(rawIdentifier)})`,\n );\n}\n","import {\n getEventHash,\n verifyEvent,\n type Event as NostrEvent,\n type EventTemplate,\n} from 'nostr-tools';\nimport type { ActiveSigner } from './core/types.js';\nimport { normalizeNip55Identifier } from './nip55.js';\n\n/**\n * NIP-55 over the web, without a Capacitor/native bridge.\n *\n * A plain browser page cannot call an Android signer in the background, so\n * this transport hands the OS a `nostrsigner` intent and then reads the\n * result back from the **clipboard** once the user returns to the tab. It\n * is the mechanism used by applesauce's `AmberClipboardSigner` and\n * gitworkshop's \"Use Amber\" button; no package is named in the intent, so\n * Android resolves it against whatever app registered the scheme — one\n * installed signer opens directly, several show the \"Open with\" chooser.\n *\n * The alternative NIP-55 web path — `?callbackUrl=` — is not used here: it\n * navigates the page away and back, which cannot be hidden behind a\n * promise-returning {@link ActiveSigner}. Use NIP-46 when a persistent\n * session is wanted; the spec recommends it for web clients.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/55.md\n */\n/** Why the browser NIP-55 flow cannot run here. */\nexport type Nip55WebSupportReason =\n | 'native'\n | 'not-android'\n | 'no-clipboard'\n | 'firefox';\n\n/**\n * Whether the browser NIP-55 option should be shown, plus an advisory\n * warning where it is known to be flaky.\n *\n * Visibility and warnings are deliberately separate, and nothing is\n * hard-blocked. The option is hidden only where the mechanism cannot exist\n * at all — a Capacitor native shell (the plugin path is strictly better)\n * and non-Android platforms (the `nostrsigner` intent cannot resolve).\n *\n * Firefox for Android is **shown with a warning** rather than blocked. It\n * advertises `readText` but never grants a persistent permission, so every\n * read needs transient activation and the poll loop cannot complete —\n * verified on an emulator (Firefox 125). We still let the user try, because\n * blocking a browser outright is a worse failure than a warning, and\n * browser behaviour changes.\n */\nexport interface Nip55WebSupport {\n /** Render the option at all. False in native shells and on non-Android. */\n visible: boolean;\n /**\n * Advisory, non-blocking message to show beside the option. The flow\n * remains attemptable; this only sets expectations.\n */\n warning?: string;\n /** Machine-readable reason for the warning / hidden state. */\n reason?: Nip55WebSupportReason;\n}\n\nexport interface Nip55WebTransport {\n /**\n * Whether this environment can run the intent + clipboard flow at all.\n * The package's browser transport requires an Android user agent and the\n * async clipboard API; anything else must fail loudly rather than open a\n * dead intent.\n */\n isSupported(): boolean;\n /**\n * Richer form of {@link isSupported}: whether to *offer* the option and\n * whether it actually works. Hosts should prefer this so they can show an\n * explanatory hint (e.g. \"does not work on Firefox\") rather than hide a\n * capability that other browsers on the same device do have.\n */\n supportStatus?(): Nip55WebSupport;\n /** Hand the intent URI to the OS (a browser typically `window.open`s it). */\n open(intent: string): void;\n /** Read the current clipboard text. */\n readClipboard(): Promise<string>;\n /**\n * Overwrite the clipboard. Used to plant a sentinel before opening the\n * signer, so the result can be told apart from whatever the user had\n * copied earlier. Best-effort — a failure just means we fall back to\n * accepting the first non-empty read.\n */\n writeClipboard(text: string): Promise<void>;\n}\n\nexport interface Nip55WebOptions {\n /**\n * Environment bridge. Defaults to a browser implementation\n * ({@link browserNip55Transport}). Tests inject a fake.\n */\n transport?: Nip55WebTransport;\n /**\n * Cached user pubkey. When supplied, {@link Nip55WebSigner.getPublicKey}\n * returns it without a signer roundtrip — used on cold start to avoid a\n * fresh `get_public_key` approval prompt for an already-paired account.\n */\n pubkey?: string;\n /**\n * How often to poll the clipboard while waiting for the signer app to\n * return a result. Default 500ms.\n *\n * Polling, not events: on Android Chrome the return from the signer app\n * fires **no** `visibilitychange`/`focus` event at all (the tab was never\n * reported hidden), so an event-driven read simply never happens.\n * `setInterval` keeps running while the tab is backgrounded, so polling\n * is what actually observes the result.\n */\n pollIntervalMs?: number;\n /**\n * Max time to wait for the signer app to return a result. A rejection is\n * invisible over NIP-55 web (the app just never calls back), so without\n * this a denied or abandoned request hangs forever. Default 120000ms;\n * `0` disables the timeout.\n */\n timeoutMs?: number;\n /**\n * Lifetime abort. Aborts the in-flight request and makes every future\n * request reject immediately (rejections carry `name === 'AbortError'`,\n * matching the NIP-46 flow).\n *\n * This is a property of the *signer*, not of one attempt. Do **not**\n * pass a controller that is aborted once a login modal closes — that\n * would leave the signer permanently unusable. To cancel a single\n * pairing attempt, use {@link Signer.loginWithNip55Web}'s `signal`.\n */\n signal?: AbortSignal;\n /**\n * Diagnostic sink for the intent/clipboard round-trip. The flow is hard\n * to observe (it leaves the page and returns), so a host can pass a\n * logger — e.g. to an on-screen list — to see what actually happened.\n */\n debug?: (message: string) => void;\n}\n\ninterface PendingRequest {\n resolve(value: string): void;\n reject(error: unknown): void;\n /** The value planted before opening the signer, if the write succeeded. */\n sentinel: string | null;\n /** Interval id for the clipboard poll; cleared on settle. */\n poll: ReturnType<typeof setInterval> | null;\n /** Request timeout; cleared on settle. `null` when disabled. */\n timer: ReturnType<typeof setTimeout> | null;\n /** Abort listener; removed on settle. `null` when no signal was given. */\n onAbort: (() => void) | null;\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 500;\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\nlet sentinelCounter = 0;\n\n/**\n * A clipboard value guaranteed not to collide with a real signer result.\n * Written before the intent so polling can tell \"the app has answered\"\n * apart from \"the user's clipboard still holds yesterday's copy\".\n */\nfunction makeSentinel(): string {\n sentinelCounter += 1;\n return `__formstr_nip55_sentinel_${Date.now()}_${sentinelCounter}__`;\n}\n\nfunction abortError(): Error {\n const error = new Error('@formstr/signer: NIP-55 request aborted');\n error.name = 'AbortError';\n return error;\n}\n\n/** Capacitor's injected global, when running inside a native shell. */\ninterface CapacitorGlobalLike {\n isNativePlatform?: () => boolean;\n}\n\n/**\n * True when the page is running inside a Capacitor native shell (the\n * Android/iOS app) rather than a plain browser. Capacitor injects a\n * `Capacitor` global into the WebView, so this needs no dependency on\n * `@capacitor/core` — which the package deliberately does not pull in.\n */\nfunction isNativeShell(): boolean {\n const cap = (globalThis as { Capacitor?: CapacitorGlobalLike }).Capacitor;\n return typeof cap?.isNativePlatform === 'function' && cap.isNativePlatform();\n}\n\n/**\n * True for Gecko-based Android browsers (Firefox/Fenix, Focus, Klar).\n *\n * These are the one environment that reports `navigator.clipboard.readText`\n * yet cannot run this flow. Firefox for Android never grants a persistent\n * `clipboard-read` permission: every read needs transient user activation\n * and is answered with an ephemeral \"Paste\" menu, so a background poll can\n * only ever fail (verified on an emulator, Firefox 125 — even directly\n * inside a click handler, with focus and user activation, `readText()`\n * rejects with \"Clipboard read operation is not allowed\").\n *\n * UA sniffing is used because there is no synchronous feature test for\n * \"does reading require activation\". `Gecko/` is checked alongside the\n * product tokens so Focus/Klar are caught too; Chromium UAs only ever say\n * \"like Gecko\", never \"Gecko/\".\n *\n * `navigator` is guaranteed by the caller — `supportStatus` returns before\n * this in a DOM-less environment.\n */\nfunction isGeckoBrowser(): boolean {\n const ua = navigator.userAgent;\n return /Firefox\\/|FxiOS\\/|Focus\\/|Gecko\\//.test(ua);\n}\n\n/**\n * Browser transport: `window.open` for the intent, `navigator.clipboard`\n * for the result. Globals are read lazily so importing this module stays\n * safe in Node.\n *\n * This transport is for a **plain browser only**. Inside a Capacitor\n * native shell the same device has the real NIP-55 plugin, which supports\n * every method and does not need the clipboard or a per-operation\n * approval, so this flow is not offered there — a native host should use\n * `loginWithAndroidSigner` instead.\n */\nexport function browserNip55Transport(): Nip55WebTransport {\n return {\n supportStatus() {\n // Server-side rendering / plain Node: no DOM, so nothing to offer.\n if (typeof navigator === 'undefined') {\n return { visible: false, reason: 'not-android' as const };\n }\n // A Capacitor shell has the real NIP-55 plugin, which is strictly\n // better (every method, no clipboard, no per-operation approval), so\n // the browser flow is not offered there at all.\n if (isNativeShell()) {\n return { visible: false, reason: 'native' as const };\n }\n if (!/Android/i.test(navigator.userAgent)) {\n return { visible: false, reason: 'not-android' as const };\n }\n if (typeof navigator.clipboard?.readText !== 'function') {\n return { visible: false, reason: 'no-clipboard' as const };\n }\n if (isGeckoBrowser()) {\n // Shown, but flagged: every read needs transient activation, so the\n // poll loop cannot complete. Still attemptable on purpose.\n return {\n visible: true,\n reason: 'firefox' as const,\n warning:\n 'May not work in Firefox for Android — the clipboard usually cannot be read automatically. If it hangs, try Chrome, Brave, or the Android app.',\n };\n }\n return { visible: true };\n },\n isSupported() {\n // \"Has the API surface\", not \"is guaranteed to work\": Firefox returns\n // true here and is allowed to try, with a warning from supportStatus().\n return this.supportStatus!().visible;\n },\n open(intent) {\n window.open(intent, '_blank');\n },\n readClipboard() {\n return navigator.clipboard.readText();\n },\n writeClipboard(text) {\n return navigator.clipboard.writeText(text);\n },\n };\n}\n\n/**\n * {@link ActiveSigner} backed by any installed NIP-55 signer app, used from\n * a plain (Android) browser via intents + clipboard.\n *\n * Every operation is a separate user approval — there is no background\n * channel and no way to learn that the user rejected a request, so callers\n * must impose their own timeout (the flow simply never resolves otherwise).\n */\nexport class Nip55WebSigner implements ActiveSigner {\n readonly #transport: Nip55WebTransport;\n readonly #pollIntervalMs: number;\n readonly #timeoutMs: number;\n readonly #signal: AbortSignal | undefined;\n readonly #debug: ((message: string) => void) | undefined;\n #pending: PendingRequest | null = null;\n #pubkey: string | null = null;\n\n constructor(options: Nip55WebOptions = {}) {\n this.#transport = options.transport ?? browserNip55Transport();\n this.#pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.#signal = options.signal;\n this.#debug = options.debug;\n this.#pubkey = options.pubkey ?? null;\n }\n\n #log(message: string): void {\n this.#debug?.(message);\n }\n\n /** True when the configured transport can actually open a signer app. */\n isSupported(): boolean {\n return this.#transport.isSupported();\n }\n\n /**\n * Whether to offer the option and whether it works. Prefer this over\n * {@link isSupported} so a host can surface `message` — notably the\n * Firefox case, which is shown but cannot complete.\n */\n supportStatus(): Nip55WebSupport {\n const status = this.#transport.supportStatus;\n if (status) return status.call(this.#transport);\n // A custom transport without `supportStatus` only gets a boolean, so we\n // can't add a warning; just mirror visibility.\n return { visible: this.#transport.isSupported() };\n }\n\n async getPublicKey(): Promise<string> {\n if (this.#pubkey !== null) return this.#pubkey;\n this.#checkSupport();\n const raw = await this.#request(Nip55WebSigner.getPublicKeyIntent());\n const { pubkey } = normalizeNip55Identifier(raw);\n this.#pubkey = pubkey;\n return pubkey;\n }\n\n async signEvent(event: EventTemplate): Promise<NostrEvent> {\n this.#checkSupport();\n const pubkey = this.#pubkey ?? (await this.getPublicKey());\n const unsigned = { ...event, pubkey };\n const draftWithId = { ...unsigned, id: getEventHash(unsigned) };\n const sig = (\n await this.#request(Nip55WebSigner.signEventIntent(draftWithId))\n ).trim();\n if (!/^[0-9a-f]{128}$/i.test(sig)) {\n throw new Error(\n '@formstr/signer: NIP-55 signer did not return a hex signature',\n );\n }\n const signed: NostrEvent = { ...draftWithId, sig: sig.toLowerCase() };\n if (!verifyEvent(signed)) {\n throw new Error('@formstr/signer: NIP-55 signer returned an invalid signature');\n }\n return signed;\n }\n\n async nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n this.#checkSupport();\n return this.#request(Nip55WebSigner.nip04EncryptIntent(peerPubkey, plaintext));\n }\n\n async nip04Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n this.#checkSupport();\n return this.#request(Nip55WebSigner.nip04DecryptIntent(peerPubkey, ciphertext));\n }\n\n async nip44Encrypt(peerPubkey: string, plaintext: string): Promise<string> {\n this.#checkSupport();\n return this.#request(Nip55WebSigner.nip44EncryptIntent(peerPubkey, plaintext));\n }\n\n async nip44Decrypt(peerPubkey: string, ciphertext: string): Promise<string> {\n this.#checkSupport();\n return this.#request(Nip55WebSigner.nip44DecryptIntent(peerPubkey, ciphertext));\n }\n\n /**\n * Cancel any in-flight request and stop its clipboard poll. Subsequent\n * operations still work — this is a teardown of live resources, not a\n * permanent disable (the {@link Signer} calls it when replacing the\n * active signer).\n */\n close(): void {\n if (this.#pending) {\n const pending = this.#pending;\n this.#settle();\n pending.reject(abortError());\n }\n }\n\n #checkSupport(): void {\n const { visible, reason } = this.supportStatus();\n // A warning (e.g. Firefox) does not block — the user is allowed to try.\n if (visible) return;\n if (reason === 'native') {\n throw new Error(\n '@formstr/signer: the browser NIP-55 flow is not for native builds — use loginWithAndroidSigner() with the Capacitor plugin instead',\n );\n }\n throw new Error(\n '@formstr/signer: NIP-55 web signing requires an Android browser with clipboard access (a signer app registering the `nostrsigner` scheme must be installed)',\n );\n }\n\n /** One poll tick: read the clipboard and settle if the signer answered. */\n #poll = async (pending: PendingRequest): Promise<void> => {\n let text: string;\n try {\n text = await this.#transport.readClipboard();\n } catch (error) {\n // NotAllowedError until the page regains focus; keep polling.\n this.#log(`clipboard read failed: ${(error as Error).message}`);\n return;\n }\n // A read can resume after the request was superseded/settled while it\n // was in flight; the interval is cleared then, but this tick is already\n // running. Drop it rather than resolving the wrong request.\n if (this.#pending !== pending) return;\n const trimmed = text.trim();\n if (trimmed.length === 0) return;\n // With a sentinel we require a *change*; without one (the write failed)\n // accept any non-empty value, matching the pre-sentinel behaviour.\n if (pending.sentinel !== null && trimmed === pending.sentinel) return;\n this.#log(`clipboard result (${trimmed.length} chars)`);\n this.#settle();\n pending.resolve(trimmed);\n };\n\n #request(intent: string): Promise<string> {\n this.#checkAborted();\n this.#cancelPending();\n return new Promise<string>((resolve, reject) => {\n const pending: PendingRequest = {\n resolve,\n reject,\n sentinel: null,\n poll: null,\n timer: null,\n onAbort: null,\n };\n this.#pending = pending;\n if (this.#signal) {\n const onAbort = () => this.#fail(pending, abortError());\n this.#signal.addEventListener('abort', onAbort);\n pending.onAbort = onAbort;\n }\n if (this.#timeoutMs > 0) {\n pending.timer = setTimeout(() => {\n this.#fail(\n pending,\n new Error(\n `@formstr/signer: NIP-55 request timed out after ${this.#timeoutMs}ms (the signer app never returned a result)`,\n ),\n );\n }, this.#timeoutMs);\n }\n\n // Plant a sentinel before opening the signer. Without it, a stale\n // clipboard from an earlier approval would look like a fresh result\n // and resolve immediately. Best-effort: if the write is refused we\n // proceed without it rather than failing the whole flow.\n void (async () => {\n try {\n const sentinel = makeSentinel();\n await this.#transport.writeClipboard(sentinel);\n if (this.#pending === pending) pending.sentinel = sentinel;\n this.#log('planted clipboard sentinel');\n } catch (error) {\n this.#log(`sentinel write failed: ${(error as Error).message}`);\n }\n if (this.#pending !== pending) return;\n try {\n this.#log(`opening signer app: ${intent.slice(0, 80)}…`);\n this.#transport.open(intent);\n } catch (error) {\n this.#fail(pending, error);\n return;\n }\n // Start polling only after the intent is out; the sentinel write and\n // open are skipped on the very first ticks otherwise.\n pending.poll = setInterval(() => {\n void this.#poll(pending);\n }, this.#pollIntervalMs);\n })();\n });\n }\n\n /**\n * Reject the current request. Every caller is cleared on settle — the\n * timeout timer and abort listener are removed, and `open()` is\n * synchronous — so `pending` is always the active request here.\n */\n #fail(pending: PendingRequest, error: unknown): void {\n this.#settle();\n pending.reject(error);\n }\n\n #settle(): void {\n const pending = this.#pending;\n this.#pending = null;\n if (pending?.poll) clearInterval(pending.poll);\n if (pending?.timer) clearTimeout(pending.timer);\n if (pending?.onAbort && this.#signal) {\n this.#signal.removeEventListener('abort', pending.onAbort);\n }\n }\n\n #cancelPending(): void {\n if (!this.#pending) return;\n const pending = this.#pending;\n this.#settle();\n pending.reject(new Error('@formstr/signer: NIP-55 request superseded'));\n }\n\n #checkAborted(): void {\n if (this.#signal?.aborted) throw abortError();\n }\n\n static getPublicKeyIntent(): string {\n return 'intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end';\n }\n\n static signEventIntent(draft: object): string {\n return `intent:${encodeURIComponent(\n JSON.stringify(draft),\n )}#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=sign_event;end`;\n }\n\n static nip04EncryptIntent(peerPubkey: string, plaintext: string): string {\n return Nip55WebSigner.#cryptoIntent('nip04_encrypt', peerPubkey, plaintext);\n }\n\n static nip04DecryptIntent(peerPubkey: string, ciphertext: string): string {\n return Nip55WebSigner.#cryptoIntent('nip04_decrypt', peerPubkey, ciphertext);\n }\n\n static nip44EncryptIntent(peerPubkey: string, plaintext: string): string {\n return Nip55WebSigner.#cryptoIntent('nip44_encrypt', peerPubkey, plaintext);\n }\n\n static nip44DecryptIntent(peerPubkey: string, ciphertext: string): string {\n return Nip55WebSigner.#cryptoIntent('nip44_decrypt', peerPubkey, ciphertext);\n }\n\n static #cryptoIntent(\n type: 'nip04_encrypt' | 'nip04_decrypt' | 'nip44_encrypt' | 'nip44_decrypt',\n peerPubkey: string,\n payload: string,\n ): string {\n return `intent:${encodeURIComponent(\n payload,\n )}#Intent;scheme=nostrsigner;S.pubKey=${peerPubkey};S.compressionType=none;S.returnType=signature;S.type=${type};end`;\n }\n}\n"],"mappings":"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","names":["import_nostr_tools","import_nip46","import_nostr_tools","nip49Encrypt","nip49Decrypt","import_nostr_tools","ToolsBunkerSigner","import_nostr_tools","import_nostr_tools","ToolsBunkerSigner"]}