@formstr/signer 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -4
- package/dist/index.cjs +381 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +374 -12
- package/dist/index.js.map +1 -1
- package/dist/{signer-BepGdtJs.d.cts → signer-oU50RBqS.d.cts} +232 -2
- package/dist/{signer-BepGdtJs.d.ts → signer-oU50RBqS.d.ts} +232 -2
- package/dist/ui/index.cjs +31 -2
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +21 -4
- package/dist/ui/index.d.ts +21 -4
- package/dist/ui/index.js +31 -2
- package/dist/ui/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -356,37 +356,297 @@ function describeIdentifier(value) {
|
|
|
356
356
|
const suffix = value.length > 12 ? "\u2026" : "";
|
|
357
357
|
return `"${prefix}${suffix}" (length=${value.length})`;
|
|
358
358
|
}
|
|
359
|
+
var HEX_PUBKEY_RE = /^[0-9a-f]{64}$/i;
|
|
359
360
|
async function loginWithAndroidSigner(plugin, packageName) {
|
|
360
361
|
if (packageName) {
|
|
361
362
|
await plugin.setPackageName(packageName);
|
|
362
363
|
}
|
|
363
|
-
const { npub, package: pluginPackage } = await plugin.getPublicKey(packageName);
|
|
364
|
+
const { npub: rawIdentifier, package: pluginPackage } = await plugin.getPublicKey(packageName);
|
|
364
365
|
const resolvedPackage = pluginPackage || packageName;
|
|
365
366
|
if (!resolvedPackage) {
|
|
366
367
|
throw new Error(
|
|
367
368
|
"@formstr/signer: android signer did not return a package name and none was supplied"
|
|
368
369
|
);
|
|
369
370
|
}
|
|
371
|
+
const { pubkey, npub } = normalizeNip55Identifier(rawIdentifier);
|
|
372
|
+
return {
|
|
373
|
+
signer: new AndroidSigner(plugin, resolvedPackage, npub, pubkey),
|
|
374
|
+
pubkey,
|
|
375
|
+
npub,
|
|
376
|
+
packageName: resolvedPackage
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function normalizeNip55Identifier(rawIdentifier) {
|
|
380
|
+
if (typeof rawIdentifier === "string" && HEX_PUBKEY_RE.test(rawIdentifier)) {
|
|
381
|
+
const pubkey = rawIdentifier.toLowerCase();
|
|
382
|
+
return { pubkey, npub: nip192.npubEncode(pubkey) };
|
|
383
|
+
}
|
|
370
384
|
let decoded;
|
|
371
385
|
try {
|
|
372
|
-
decoded = nip192.decode(
|
|
386
|
+
decoded = nip192.decode(rawIdentifier);
|
|
373
387
|
} catch (e) {
|
|
374
388
|
throw new Error(
|
|
375
|
-
`@formstr/signer:
|
|
389
|
+
`@formstr/signer: signer returned an undecodable identifier (got ${describeIdentifier(rawIdentifier)}): ${e.message}`
|
|
376
390
|
);
|
|
377
391
|
}
|
|
378
|
-
if (decoded.type
|
|
379
|
-
|
|
380
|
-
`@formstr/signer: android signer returned a non-npub identifier (type=${decoded.type}, got ${describeIdentifier(npub)})`
|
|
381
|
-
);
|
|
392
|
+
if (decoded.type === "npub") {
|
|
393
|
+
return { pubkey: decoded.data, npub: rawIdentifier };
|
|
382
394
|
}
|
|
395
|
+
if (decoded.type === "nprofile") {
|
|
396
|
+
const pubkey = decoded.data.pubkey;
|
|
397
|
+
return { pubkey, npub: nip192.npubEncode(pubkey) };
|
|
398
|
+
}
|
|
399
|
+
throw new Error(
|
|
400
|
+
`@formstr/signer: signer returned a non-pubkey identifier (type=${decoded.type}, got ${describeIdentifier(rawIdentifier)})`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/nip55Web.ts
|
|
405
|
+
import {
|
|
406
|
+
getEventHash as getEventHash2,
|
|
407
|
+
verifyEvent
|
|
408
|
+
} from "nostr-tools";
|
|
409
|
+
var DEFAULT_POLL_INTERVAL_MS = 500;
|
|
410
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
411
|
+
var sentinelCounter = 0;
|
|
412
|
+
function makeSentinel() {
|
|
413
|
+
sentinelCounter += 1;
|
|
414
|
+
return `__formstr_nip55_sentinel_${Date.now()}_${sentinelCounter}__`;
|
|
415
|
+
}
|
|
416
|
+
function abortError() {
|
|
417
|
+
const error = new Error("@formstr/signer: NIP-55 request aborted");
|
|
418
|
+
error.name = "AbortError";
|
|
419
|
+
return error;
|
|
420
|
+
}
|
|
421
|
+
function isNativeShell() {
|
|
422
|
+
const cap = globalThis.Capacitor;
|
|
423
|
+
return typeof cap?.isNativePlatform === "function" && cap.isNativePlatform();
|
|
424
|
+
}
|
|
425
|
+
function browserNip55Transport() {
|
|
383
426
|
return {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
427
|
+
isSupported() {
|
|
428
|
+
return typeof navigator !== "undefined" && !isNativeShell() && /Android/i.test(navigator.userAgent) && typeof navigator.clipboard?.readText === "function";
|
|
429
|
+
},
|
|
430
|
+
open(intent) {
|
|
431
|
+
window.open(intent, "_blank");
|
|
432
|
+
},
|
|
433
|
+
readClipboard() {
|
|
434
|
+
return navigator.clipboard.readText();
|
|
435
|
+
},
|
|
436
|
+
writeClipboard(text) {
|
|
437
|
+
return navigator.clipboard.writeText(text);
|
|
438
|
+
}
|
|
388
439
|
};
|
|
389
440
|
}
|
|
441
|
+
var Nip55WebSigner = class _Nip55WebSigner {
|
|
442
|
+
#transport;
|
|
443
|
+
#pollIntervalMs;
|
|
444
|
+
#timeoutMs;
|
|
445
|
+
#signal;
|
|
446
|
+
#debug;
|
|
447
|
+
#pending = null;
|
|
448
|
+
#pubkey = null;
|
|
449
|
+
constructor(options = {}) {
|
|
450
|
+
this.#transport = options.transport ?? browserNip55Transport();
|
|
451
|
+
this.#pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
452
|
+
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
453
|
+
this.#signal = options.signal;
|
|
454
|
+
this.#debug = options.debug;
|
|
455
|
+
this.#pubkey = options.pubkey ?? null;
|
|
456
|
+
}
|
|
457
|
+
#log(message) {
|
|
458
|
+
this.#debug?.(message);
|
|
459
|
+
}
|
|
460
|
+
/** True when the configured transport can actually open a signer app. */
|
|
461
|
+
isSupported() {
|
|
462
|
+
return this.#transport.isSupported();
|
|
463
|
+
}
|
|
464
|
+
async getPublicKey() {
|
|
465
|
+
if (this.#pubkey !== null) return this.#pubkey;
|
|
466
|
+
this.#checkSupport();
|
|
467
|
+
const raw = await this.#request(_Nip55WebSigner.getPublicKeyIntent());
|
|
468
|
+
const { pubkey } = normalizeNip55Identifier(raw);
|
|
469
|
+
this.#pubkey = pubkey;
|
|
470
|
+
return pubkey;
|
|
471
|
+
}
|
|
472
|
+
async signEvent(event) {
|
|
473
|
+
this.#checkSupport();
|
|
474
|
+
const pubkey = this.#pubkey ?? await this.getPublicKey();
|
|
475
|
+
const unsigned = { ...event, pubkey };
|
|
476
|
+
const draftWithId = { ...unsigned, id: getEventHash2(unsigned) };
|
|
477
|
+
const sig = (await this.#request(_Nip55WebSigner.signEventIntent(draftWithId))).trim();
|
|
478
|
+
if (!/^[0-9a-f]{128}$/i.test(sig)) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
"@formstr/signer: NIP-55 signer did not return a hex signature"
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
const signed = { ...draftWithId, sig: sig.toLowerCase() };
|
|
484
|
+
if (!verifyEvent(signed)) {
|
|
485
|
+
throw new Error("@formstr/signer: NIP-55 signer returned an invalid signature");
|
|
486
|
+
}
|
|
487
|
+
return signed;
|
|
488
|
+
}
|
|
489
|
+
async nip04Encrypt(peerPubkey, plaintext) {
|
|
490
|
+
this.#checkSupport();
|
|
491
|
+
return this.#request(_Nip55WebSigner.nip04EncryptIntent(peerPubkey, plaintext));
|
|
492
|
+
}
|
|
493
|
+
async nip04Decrypt(peerPubkey, ciphertext) {
|
|
494
|
+
this.#checkSupport();
|
|
495
|
+
return this.#request(_Nip55WebSigner.nip04DecryptIntent(peerPubkey, ciphertext));
|
|
496
|
+
}
|
|
497
|
+
async nip44Encrypt(peerPubkey, plaintext) {
|
|
498
|
+
this.#checkSupport();
|
|
499
|
+
return this.#request(_Nip55WebSigner.nip44EncryptIntent(peerPubkey, plaintext));
|
|
500
|
+
}
|
|
501
|
+
async nip44Decrypt(peerPubkey, ciphertext) {
|
|
502
|
+
this.#checkSupport();
|
|
503
|
+
return this.#request(_Nip55WebSigner.nip44DecryptIntent(peerPubkey, ciphertext));
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Cancel any in-flight request and stop its clipboard poll. Subsequent
|
|
507
|
+
* operations still work — this is a teardown of live resources, not a
|
|
508
|
+
* permanent disable (the {@link Signer} calls it when replacing the
|
|
509
|
+
* active signer).
|
|
510
|
+
*/
|
|
511
|
+
close() {
|
|
512
|
+
if (this.#pending) {
|
|
513
|
+
const pending = this.#pending;
|
|
514
|
+
this.#settle();
|
|
515
|
+
pending.reject(abortError());
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
#checkSupport() {
|
|
519
|
+
if (this.#transport.isSupported()) return;
|
|
520
|
+
if (isNativeShell()) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
"@formstr/signer: the browser NIP-55 flow is not for native builds \u2014 use loginWithAndroidSigner() with the Capacitor plugin instead"
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
throw new Error(
|
|
526
|
+
"@formstr/signer: NIP-55 web signing requires an Android browser with clipboard access (a signer app registering the `nostrsigner` scheme must be installed)"
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
/** One poll tick: read the clipboard and settle if the signer answered. */
|
|
530
|
+
#poll = async (pending) => {
|
|
531
|
+
let text;
|
|
532
|
+
try {
|
|
533
|
+
text = await this.#transport.readClipboard();
|
|
534
|
+
} catch (error) {
|
|
535
|
+
this.#log(`clipboard read failed: ${error.message}`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (this.#pending !== pending) return;
|
|
539
|
+
const trimmed = text.trim();
|
|
540
|
+
if (trimmed.length === 0) return;
|
|
541
|
+
if (pending.sentinel !== null && trimmed === pending.sentinel) return;
|
|
542
|
+
this.#log(`clipboard result (${trimmed.length} chars)`);
|
|
543
|
+
this.#settle();
|
|
544
|
+
pending.resolve(trimmed);
|
|
545
|
+
};
|
|
546
|
+
#request(intent) {
|
|
547
|
+
this.#checkAborted();
|
|
548
|
+
this.#cancelPending();
|
|
549
|
+
return new Promise((resolve, reject) => {
|
|
550
|
+
const pending = {
|
|
551
|
+
resolve,
|
|
552
|
+
reject,
|
|
553
|
+
sentinel: null,
|
|
554
|
+
poll: null,
|
|
555
|
+
timer: null,
|
|
556
|
+
onAbort: null
|
|
557
|
+
};
|
|
558
|
+
this.#pending = pending;
|
|
559
|
+
if (this.#signal) {
|
|
560
|
+
const onAbort = () => this.#fail(pending, abortError());
|
|
561
|
+
this.#signal.addEventListener("abort", onAbort);
|
|
562
|
+
pending.onAbort = onAbort;
|
|
563
|
+
}
|
|
564
|
+
if (this.#timeoutMs > 0) {
|
|
565
|
+
pending.timer = setTimeout(() => {
|
|
566
|
+
this.#fail(
|
|
567
|
+
pending,
|
|
568
|
+
new Error(
|
|
569
|
+
`@formstr/signer: NIP-55 request timed out after ${this.#timeoutMs}ms (the signer app never returned a result)`
|
|
570
|
+
)
|
|
571
|
+
);
|
|
572
|
+
}, this.#timeoutMs);
|
|
573
|
+
}
|
|
574
|
+
void (async () => {
|
|
575
|
+
try {
|
|
576
|
+
const sentinel = makeSentinel();
|
|
577
|
+
await this.#transport.writeClipboard(sentinel);
|
|
578
|
+
if (this.#pending === pending) pending.sentinel = sentinel;
|
|
579
|
+
this.#log("planted clipboard sentinel");
|
|
580
|
+
} catch (error) {
|
|
581
|
+
this.#log(`sentinel write failed: ${error.message}`);
|
|
582
|
+
}
|
|
583
|
+
if (this.#pending !== pending) return;
|
|
584
|
+
try {
|
|
585
|
+
this.#log(`opening signer app: ${intent.slice(0, 80)}\u2026`);
|
|
586
|
+
this.#transport.open(intent);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
this.#fail(pending, error);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
pending.poll = setInterval(() => {
|
|
592
|
+
void this.#poll(pending);
|
|
593
|
+
}, this.#pollIntervalMs);
|
|
594
|
+
})();
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Reject the current request. Every caller is cleared on settle — the
|
|
599
|
+
* timeout timer and abort listener are removed, and `open()` is
|
|
600
|
+
* synchronous — so `pending` is always the active request here.
|
|
601
|
+
*/
|
|
602
|
+
#fail(pending, error) {
|
|
603
|
+
this.#settle();
|
|
604
|
+
pending.reject(error);
|
|
605
|
+
}
|
|
606
|
+
#settle() {
|
|
607
|
+
const pending = this.#pending;
|
|
608
|
+
this.#pending = null;
|
|
609
|
+
if (pending?.poll) clearInterval(pending.poll);
|
|
610
|
+
if (pending?.timer) clearTimeout(pending.timer);
|
|
611
|
+
if (pending?.onAbort && this.#signal) {
|
|
612
|
+
this.#signal.removeEventListener("abort", pending.onAbort);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
#cancelPending() {
|
|
616
|
+
if (!this.#pending) return;
|
|
617
|
+
const pending = this.#pending;
|
|
618
|
+
this.#settle();
|
|
619
|
+
pending.reject(new Error("@formstr/signer: NIP-55 request superseded"));
|
|
620
|
+
}
|
|
621
|
+
#checkAborted() {
|
|
622
|
+
if (this.#signal?.aborted) throw abortError();
|
|
623
|
+
}
|
|
624
|
+
static getPublicKeyIntent() {
|
|
625
|
+
return "intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end";
|
|
626
|
+
}
|
|
627
|
+
static signEventIntent(draft) {
|
|
628
|
+
return `intent:${encodeURIComponent(
|
|
629
|
+
JSON.stringify(draft)
|
|
630
|
+
)}#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=sign_event;end`;
|
|
631
|
+
}
|
|
632
|
+
static nip04EncryptIntent(peerPubkey, plaintext) {
|
|
633
|
+
return _Nip55WebSigner.#cryptoIntent("nip04_encrypt", peerPubkey, plaintext);
|
|
634
|
+
}
|
|
635
|
+
static nip04DecryptIntent(peerPubkey, ciphertext) {
|
|
636
|
+
return _Nip55WebSigner.#cryptoIntent("nip04_decrypt", peerPubkey, ciphertext);
|
|
637
|
+
}
|
|
638
|
+
static nip44EncryptIntent(peerPubkey, plaintext) {
|
|
639
|
+
return _Nip55WebSigner.#cryptoIntent("nip44_encrypt", peerPubkey, plaintext);
|
|
640
|
+
}
|
|
641
|
+
static nip44DecryptIntent(peerPubkey, ciphertext) {
|
|
642
|
+
return _Nip55WebSigner.#cryptoIntent("nip44_decrypt", peerPubkey, ciphertext);
|
|
643
|
+
}
|
|
644
|
+
static #cryptoIntent(type, peerPubkey, payload) {
|
|
645
|
+
return `intent:${encodeURIComponent(
|
|
646
|
+
payload
|
|
647
|
+
)}#Intent;scheme=nostrsigner;S.pubKey=${peerPubkey};S.compressionType=none;S.returnType=signature;S.type=${type};end`;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
390
650
|
|
|
391
651
|
// src/core/signer.ts
|
|
392
652
|
var ACCOUNTS_KEY = "accounts";
|
|
@@ -394,6 +654,7 @@ var ACTIVE_KEY = "active-pubkey";
|
|
|
394
654
|
var Signer = class {
|
|
395
655
|
#storage;
|
|
396
656
|
#defaultAndroidPlugin;
|
|
657
|
+
#nip55WebTransport;
|
|
397
658
|
#appMetadata;
|
|
398
659
|
#accounts = [];
|
|
399
660
|
#activePubkey = null;
|
|
@@ -402,6 +663,7 @@ var Signer = class {
|
|
|
402
663
|
constructor(config = {}) {
|
|
403
664
|
this.#storage = config.storage ?? localStorageAdapter(config.storageKeyPrefix);
|
|
404
665
|
this.#defaultAndroidPlugin = config.androidSignerPlugin;
|
|
666
|
+
this.#nip55WebTransport = config.nip55WebTransport;
|
|
405
667
|
this.#appMetadata = {
|
|
406
668
|
name: config.appName,
|
|
407
669
|
url: config.appUrl,
|
|
@@ -435,8 +697,28 @@ var Signer = class {
|
|
|
435
697
|
else this.#accounts.push(account);
|
|
436
698
|
this.#persistAccounts();
|
|
437
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Release the currently-held signer, if it has a `close()`. Called
|
|
702
|
+
* whenever the active signer is replaced or cleared so live resources
|
|
703
|
+
* (bunker subscriptions, `visibilitychange` listeners, in-flight NIP-55
|
|
704
|
+
* requests) don't outlive the session. Errors are swallowed — teardown
|
|
705
|
+
* must never block a login/switch/logout.
|
|
706
|
+
*/
|
|
707
|
+
#closeActiveSigner() {
|
|
708
|
+
const signer = this.#activeSigner;
|
|
709
|
+
if (!signer?.close) return;
|
|
710
|
+
try {
|
|
711
|
+
const result = signer.close();
|
|
712
|
+
if (result && typeof result.catch === "function") {
|
|
713
|
+
result.catch(() => {
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
438
719
|
#setActive(account, signer) {
|
|
439
720
|
const wasDifferent = this.#activePubkey !== null && this.#activePubkey !== account.pubkey;
|
|
721
|
+
if (this.#activeSigner !== signer) this.#closeActiveSigner();
|
|
440
722
|
this.#activePubkey = account.pubkey;
|
|
441
723
|
this.#activeSigner = signer;
|
|
442
724
|
this.#persistActive();
|
|
@@ -628,6 +910,69 @@ var Signer = class {
|
|
|
628
910
|
this.#setActive(account, result.signer);
|
|
629
911
|
return account;
|
|
630
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* Whether `loginWithNip55Web` can run in this environment — a plain
|
|
915
|
+
* Android browser with async clipboard access. False in a Capacitor
|
|
916
|
+
* native shell (use {@link loginWithAndroidSigner} there), and on
|
|
917
|
+
* desktop/iOS/SSR.
|
|
918
|
+
*
|
|
919
|
+
* A **capability** check, not an availability one: there is no web API
|
|
920
|
+
* to detect an installed Android app, so this says nothing about
|
|
921
|
+
* whether a signer app is actually installed. Use it to hide the
|
|
922
|
+
* browser flow where it cannot work, not to promise that it will.
|
|
923
|
+
*/
|
|
924
|
+
supportsNip55Web(transport) {
|
|
925
|
+
const t = transport ?? this.#nip55WebTransport ?? browserNip55Transport();
|
|
926
|
+
return t.isSupported();
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Sign in via a NIP-55 Android external signer (Amber, etc) **from a
|
|
930
|
+
* plain browser**, with no Capacitor/native bridge. Opens the installed
|
|
931
|
+
* signer app through a `nostrsigner` intent and reads the result back
|
|
932
|
+
* from the clipboard once the user returns to the tab. Because the
|
|
933
|
+
* intent names no package, this works with any app that registered the
|
|
934
|
+
* `nostrsigner` scheme — one opens directly, several show the Android
|
|
935
|
+
* "Open with" chooser.
|
|
936
|
+
*
|
|
937
|
+
* Not for native builds — inside a Capacitor shell use
|
|
938
|
+
* {@link loginWithAndroidSigner}, which needs no clipboard and no
|
|
939
|
+
* per-operation approval.
|
|
940
|
+
*
|
|
941
|
+
* Every operation is a separate approval, and a rejection is
|
|
942
|
+
* indistinguishable from the user simply not returning, so callers must
|
|
943
|
+
* impose their own timeout. Prefer NIP-46 when a persistent session is
|
|
944
|
+
* acceptable — the NIP-55 spec recommends it for web clients.
|
|
945
|
+
*
|
|
946
|
+
* @throws if the environment cannot run the flow (not Android, no async
|
|
947
|
+
* clipboard) or the signer returns an unexpected value.
|
|
948
|
+
*/
|
|
949
|
+
async loginWithNip55Web(options = {}) {
|
|
950
|
+
const transport = options.transport ?? this.#nip55WebTransport;
|
|
951
|
+
const pairing = new Nip55WebSigner({
|
|
952
|
+
transport,
|
|
953
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
954
|
+
timeoutMs: options.timeoutMs,
|
|
955
|
+
signal: options.signal,
|
|
956
|
+
debug: options.debug
|
|
957
|
+
});
|
|
958
|
+
const pubkey = await pairing.getPublicKey();
|
|
959
|
+
const signer = new Nip55WebSigner({
|
|
960
|
+
transport,
|
|
961
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
962
|
+
timeoutMs: options.timeoutMs,
|
|
963
|
+
pubkey,
|
|
964
|
+
debug: options.debug
|
|
965
|
+
});
|
|
966
|
+
const npub = nip193.npubEncode(pubkey);
|
|
967
|
+
const account = {
|
|
968
|
+
npub,
|
|
969
|
+
pubkey,
|
|
970
|
+
method: "nip55-web"
|
|
971
|
+
};
|
|
972
|
+
this.#upsertAccount(account);
|
|
973
|
+
this.#setActive(account, signer);
|
|
974
|
+
return account;
|
|
975
|
+
}
|
|
631
976
|
/** Snapshot of every persisted account, in insertion order. */
|
|
632
977
|
listAccounts() {
|
|
633
978
|
return [...this.#accounts];
|
|
@@ -681,6 +1026,10 @@ var Signer = class {
|
|
|
681
1026
|
* {@link loginWithAndroidSigner} performs and that — on Amber —
|
|
682
1027
|
* surfaces as a permission prompt every cold start.
|
|
683
1028
|
*
|
|
1029
|
+
* - `nip55-web`: constructs a {@link Nip55WebSigner} with the stored
|
|
1030
|
+
* `pubkey` cached. Like `android`, this opens no signer app during
|
|
1031
|
+
* unlock; the first sign/encrypt call is what prompts.
|
|
1032
|
+
*
|
|
684
1033
|
* - `ncryptsec`: returns `null`. There is no silent path — the user's
|
|
685
1034
|
* passphrase isn't (and shouldn't be) persisted. The caller must
|
|
686
1035
|
* drive the passphrase prompt and call {@link loginWithNcryptsec}.
|
|
@@ -729,6 +1078,14 @@ var Signer = class {
|
|
|
729
1078
|
this.#setActive(account, signer);
|
|
730
1079
|
return signer;
|
|
731
1080
|
}
|
|
1081
|
+
case "nip55-web": {
|
|
1082
|
+
const signer = new Nip55WebSigner({
|
|
1083
|
+
transport: this.#nip55WebTransport,
|
|
1084
|
+
pubkey: account.pubkey
|
|
1085
|
+
});
|
|
1086
|
+
this.#setActive(account, signer);
|
|
1087
|
+
return signer;
|
|
1088
|
+
}
|
|
732
1089
|
case "ncryptsec":
|
|
733
1090
|
return null;
|
|
734
1091
|
}
|
|
@@ -743,6 +1100,7 @@ var Signer = class {
|
|
|
743
1100
|
async switchAccount(pubkey) {
|
|
744
1101
|
const account = this.#accounts.find((a) => a.pubkey === pubkey);
|
|
745
1102
|
if (!account) throw new Error(`switchAccount: no account for pubkey ${pubkey}`);
|
|
1103
|
+
this.#closeActiveSigner();
|
|
746
1104
|
this.#activePubkey = pubkey;
|
|
747
1105
|
this.#activeSigner = null;
|
|
748
1106
|
this.#persistActive();
|
|
@@ -759,6 +1117,7 @@ var Signer = class {
|
|
|
759
1117
|
this.#accounts = this.#accounts.filter((a) => a.pubkey !== target);
|
|
760
1118
|
this.#persistAccounts();
|
|
761
1119
|
if (this.#activePubkey === target) {
|
|
1120
|
+
this.#closeActiveSigner();
|
|
762
1121
|
this.#activePubkey = null;
|
|
763
1122
|
this.#activeSigner = null;
|
|
764
1123
|
this.#persistActive();
|
|
@@ -785,7 +1144,9 @@ export {
|
|
|
785
1144
|
BunkerSigner,
|
|
786
1145
|
ExtensionSigner,
|
|
787
1146
|
LocalSigner,
|
|
1147
|
+
Nip55WebSigner,
|
|
788
1148
|
Signer,
|
|
1149
|
+
browserNip55Transport,
|
|
789
1150
|
bytesToHex,
|
|
790
1151
|
connectWithBunkerUri,
|
|
791
1152
|
createSigner,
|
|
@@ -796,6 +1157,7 @@ export {
|
|
|
796
1157
|
hexToBytes,
|
|
797
1158
|
initiateNostrConnect,
|
|
798
1159
|
localStorageAdapter,
|
|
799
|
-
loginWithAndroidSigner
|
|
1160
|
+
loginWithAndroidSigner,
|
|
1161
|
+
normalizeNip55Identifier
|
|
800
1162
|
};
|
|
801
1163
|
//# sourceMappingURL=index.js.map
|