@bwalletx/connect 0.4.1 → 0.5.3

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +6 -2
  3. package/assets/protocol/401.jpg +0 -0
  4. package/assets/protocol/401.mp4 +0 -0
  5. package/assets/protocol/402.jpg +0 -0
  6. package/assets/protocol/402.mp4 +0 -0
  7. package/assets/protocol/403.jpg +0 -0
  8. package/assets/protocol/403.mp4 +0 -0
  9. package/dist/access-Dp0eKIOc.d.cts +118 -0
  10. package/dist/access-Dp0eKIOc.d.ts +118 -0
  11. package/dist/bwalletx-connect.global.js +20 -17
  12. package/dist/bwalletx-connect.global.js.map +1 -1
  13. package/dist/{chunk-NNMUSWPG.js → chunk-IUDD7YTQ.js} +200 -10
  14. package/dist/chunk-IUDD7YTQ.js.map +1 -0
  15. package/dist/chunk-MWD4BCO5.js +182 -0
  16. package/dist/chunk-MWD4BCO5.js.map +1 -0
  17. package/dist/{chunk-FUQ46RJV.js → chunk-PI42H3XY.js} +65 -10
  18. package/dist/chunk-PI42H3XY.js.map +1 -0
  19. package/dist/{chunk-7NAKOJGZ.js → chunk-QRXFL66W.js} +220 -10
  20. package/dist/chunk-QRXFL66W.js.map +1 -0
  21. package/dist/element.cjs +290 -14
  22. package/dist/element.cjs.map +1 -1
  23. package/dist/element.js +2 -1
  24. package/dist/element.js.map +1 -1
  25. package/dist/index.cjs +437 -32
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +71 -1
  28. package/dist/index.d.ts +71 -1
  29. package/dist/index.js +36 -4
  30. package/dist/index.js.map +1 -1
  31. package/dist/next.cjs +377 -9
  32. package/dist/next.cjs.map +1 -1
  33. package/dist/next.d.cts +52 -2
  34. package/dist/next.d.ts +52 -2
  35. package/dist/next.js +92 -1
  36. package/dist/next.js.map +1 -1
  37. package/dist/react.cjs +561 -226
  38. package/dist/react.cjs.map +1 -1
  39. package/dist/react.d.cts +5 -0
  40. package/dist/react.d.ts +5 -0
  41. package/dist/react.js +12 -3
  42. package/dist/react.js.map +1 -1
  43. package/dist/server.cjs +406 -9
  44. package/dist/server.cjs.map +1 -1
  45. package/dist/server.d.cts +111 -1
  46. package/dist/server.d.ts +111 -1
  47. package/dist/server.js +47 -1
  48. package/package.json +12 -11
  49. package/dist/chunk-7NAKOJGZ.js.map +0 -1
  50. package/dist/chunk-FUQ46RJV.js.map +0 -1
  51. package/dist/chunk-NNMUSWPG.js.map +0 -1
package/dist/react.cjs CHANGED
@@ -407,7 +407,27 @@ async function clearPairing(opts = {}) {
407
407
  });
408
408
  }
409
409
 
410
+ // src/theme.ts
411
+ var WALLET_THEME_META = "bwalletx-theme";
412
+ var ID = /^[a-z0-9-]{1,32}$/;
413
+ function declareWalletTheme(theme) {
414
+ if (typeof document === "undefined") return;
415
+ const id = (theme ?? "").trim().toLowerCase();
416
+ let meta = document.head.querySelector(`meta[name="${WALLET_THEME_META}"]`);
417
+ if (!id || !ID.test(id)) {
418
+ meta?.remove();
419
+ return;
420
+ }
421
+ if (!meta) {
422
+ meta = document.createElement("meta");
423
+ meta.name = WALLET_THEME_META;
424
+ document.head.appendChild(meta);
425
+ }
426
+ if (meta.content !== id) meta.content = id;
427
+ }
428
+
410
429
  // src/discover.ts
430
+ var import_sdk3 = require("@bsv/sdk");
411
431
  var RDNS_EXTENSION = "com.bwalletx.extension";
412
432
  var RDNS_MOBILE = "space.bwallet.mobile";
413
433
  var BWALLETX_RDNS = [RDNS_EXTENSION, RDNS_MOBILE];
@@ -432,10 +452,64 @@ function discoverBwalletX(ms = 1200) {
432
452
  window.dispatchEvent(new Event("brc100:requestWallet"));
433
453
  setTimeout(() => {
434
454
  const cwi = window.CWI;
435
- finish(cwi ? { wallet: cwi, method: "in-app", rdns: null } : null);
455
+ if (cwi) return finish({ wallet: cwi, method: "in-app", rdns: null });
456
+ if (!isFramed()) return finish(null);
457
+ void probeXdm(ms).then((w) => finish(w ? { wallet: w, method: "in-app", rdns: null } : null));
436
458
  }, ms);
437
459
  });
438
460
  }
461
+ var isFramed = () => {
462
+ try {
463
+ return window.parent !== window;
464
+ } catch {
465
+ return true;
466
+ }
467
+ };
468
+ var BWALLETX_PARENT_ORIGINS = [
469
+ "https://web.bwalletx.com",
470
+ "https://desktop.bwalletx.com",
471
+ "https://bwalletx.com",
472
+ "https://www.bwallet.space",
473
+ "https://bwallet.space",
474
+ "capacitor://localhost",
475
+ // iOS app shell
476
+ "https://localhost",
477
+ // Android app shell
478
+ "tauri://localhost",
479
+ // bWalletX Desktop (macOS)
480
+ "https://tauri.localhost",
481
+ // bWalletX Desktop (Windows)
482
+ "chrome-extension://bombmmcjghjdlbmcclnhjodgcelnehgl"
483
+ // bWalletX for Chrome (fixed id)
484
+ ];
485
+ var parentOrigin = () => {
486
+ try {
487
+ const anc = window.location.ancestorOrigins;
488
+ if (anc && anc.length > 0) return anc[0] ?? null;
489
+ if (!document.referrer) return null;
490
+ const u = new URL(document.referrer);
491
+ return `${u.protocol}//${u.host}`;
492
+ } catch {
493
+ return null;
494
+ }
495
+ };
496
+ async function probeXdm(ms = 1200, allowed = BWALLETX_PARENT_ORIGINS) {
497
+ const origin = parentOrigin();
498
+ if (!origin || !allowed.includes(origin)) return null;
499
+ try {
500
+ const w = new import_sdk3.WalletClient(new import_sdk3.XDM(origin));
501
+ const ok = await Promise.race([
502
+ w.isAuthenticated({}).then(
503
+ () => true,
504
+ () => false
505
+ ),
506
+ new Promise((r) => setTimeout(() => r(false), ms))
507
+ ]);
508
+ return ok ? w : null;
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
439
513
 
440
514
  // src/signin.ts
441
515
  var LOGIN_PROTOCOL = [2, "bwallet sign in"];
@@ -456,19 +530,442 @@ async function signInWithBwalletX(opts) {
456
530
  const { wallet, method } = await connectBwalletX(opts);
457
531
  if (method !== "pairing") await wallet.waitForAuthentication({}).catch(() => {
458
532
  });
459
- const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
460
- const c = await opts.challenge(identityKey);
461
- const { signature } = await wallet.createSignature({
462
- protocolID: c.protocolID ?? LOGIN_PROTOCOL,
463
- keyID: c.keyID,
464
- counterparty: "anyone",
465
- data: Array.from(new TextEncoder().encode(c.message))
466
- });
533
+ const { publicKey: identityKey } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
534
+ const c = await atStage("challenge", () => opts.challenge(identityKey));
535
+ const { signature } = await atStage(
536
+ "signature",
537
+ () => wallet.createSignature({
538
+ protocolID: c.protocolID ?? LOGIN_PROTOCOL,
539
+ keyID: c.keyID,
540
+ counterparty: "anyone",
541
+ data: Array.from(new TextEncoder().encode(c.message))
542
+ })
543
+ );
467
544
  return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
468
545
  }
546
+ async function atStage(stage, run) {
547
+ try {
548
+ return await run();
549
+ } catch (e) {
550
+ const err = e instanceof Error ? e : new Error(String(e));
551
+ if (!err.stage) Object.assign(err, { stage });
552
+ throw err;
553
+ }
554
+ }
469
555
 
470
556
  // src/modal.ts
471
557
  var import_qrcode_generator = __toESM(require("qrcode-generator"), 1);
558
+
559
+ // src/access-client.ts
560
+ var import_sdk4 = require("@bsv/sdk");
561
+
562
+ // src/access.ts
563
+ var ACCESS_PROTOCOL = [2, "bwalletx access"];
564
+ var MAX_HOLDING_PROOFS = 50;
565
+ var AccessError = class extends Error {
566
+ code;
567
+ have;
568
+ need;
569
+ constructor(code, message, extra = {}) {
570
+ super(message ?? code);
571
+ this.name = "AccessError";
572
+ this.code = code;
573
+ this.have = extra.have;
574
+ this.need = extra.need;
575
+ }
576
+ toJSON() {
577
+ const b = { error: this.code, message: this.message };
578
+ if (this.have !== void 0) b.have = this.have;
579
+ if (this.need !== void 0) b.need = this.need;
580
+ return b;
581
+ }
582
+ };
583
+ function canonicalJson(v) {
584
+ if (v === null) return "null";
585
+ if (typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
586
+ if (typeof v === "number") {
587
+ if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
588
+ return JSON.stringify(v);
589
+ }
590
+ if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x)).join(",")}]`;
591
+ if (typeof v === "object") {
592
+ const o = v;
593
+ const keys = Object.keys(o).filter((k) => o[k] !== void 0).sort();
594
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
595
+ }
596
+ throw new Error(`canonicalJson: unsupported ${typeof v}`);
597
+ }
598
+ function challengeBytes(challenge) {
599
+ return Array.from(new TextEncoder().encode(challenge));
600
+ }
601
+ var TOKEN_ID = /^bsv21:[0-9a-f]{64}_\d{1,6}$/;
602
+ var RAW = /^[1-9]\d{0,38}$/;
603
+ function normaliseRequirement(r) {
604
+ if (!r || typeof r !== "object") throw new AccessError("bad_request", "require must be an object");
605
+ const req = r;
606
+ const out = {};
607
+ if (req.token !== void 0) {
608
+ const t = req.token;
609
+ if (!t || typeof t !== "object" || typeof t.id !== "string") throw new AccessError("bad_request", "require.token.id is required");
610
+ const id = t.id.trim().toLowerCase();
611
+ if (!TOKEN_ID.test(id)) throw new AccessError("bad_request", "require.token.id must be 'bsv21:<txid>_<vout>'");
612
+ const hasMin = t.min !== void 0;
613
+ const hasBps = t.minBps !== void 0;
614
+ if (hasMin === hasBps) throw new AccessError("bad_request", "require.token needs exactly one of min or minBps");
615
+ out.token = { id };
616
+ if (hasMin) {
617
+ if (typeof t.min !== "string" || !RAW.test(t.min)) throw new AccessError("bad_request", "require.token.min must be a positive integer string (raw units)");
618
+ out.token.min = t.min;
619
+ } else {
620
+ if (!Number.isInteger(t.minBps) || t.minBps < 1 || t.minBps > 1e4) {
621
+ throw new AccessError("bad_request", "require.token.minBps must be an integer 1..10000");
622
+ }
623
+ out.token.minBps = t.minBps;
624
+ }
625
+ }
626
+ if (req.identityLevel !== void 0) {
627
+ if (!Number.isInteger(req.identityLevel) || req.identityLevel < 1 || req.identityLevel > 10) {
628
+ throw new AccessError("bad_request", "require.identityLevel must be an integer 1..10");
629
+ }
630
+ out.identityLevel = req.identityLevel;
631
+ }
632
+ if (req.kyc !== void 0) {
633
+ if (typeof req.kyc !== "boolean") throw new AccessError("bad_request", "require.kyc must be a boolean");
634
+ if (req.kyc) out.kyc = true;
635
+ }
636
+ if (!out.token && !out.identityLevel && !out.kyc) throw new AccessError("bad_request", "require is empty");
637
+ return out;
638
+ }
639
+ function parseAccessChallenge(s) {
640
+ if (typeof s !== "string" || s.length > 4096) throw new AccessError("bad_request", "challenge must be a string");
641
+ let c;
642
+ try {
643
+ c = JSON.parse(s);
644
+ } catch {
645
+ throw new AccessError("bad_request", "challenge is not JSON");
646
+ }
647
+ if (!c || typeof c !== "object" || c.v !== 1) throw new AccessError("bad_request", "unsupported challenge version");
648
+ if (c.kind !== "401" && c.kind !== "402" && c.kind !== "403") throw new AccessError("bad_request", "bad kind");
649
+ if (typeof c.origin !== "string" || typeof c.nonce !== "string" || !/^[A-Za-z0-9_-]{16,64}$/.test(c.nonce)) {
650
+ throw new AccessError("bad_request", "bad origin or nonce");
651
+ }
652
+ if (!Number.isInteger(c.iat) || !Number.isInteger(c.exp) || c.exp <= c.iat) throw new AccessError("bad_request", "bad iat/exp");
653
+ if (c.sub !== void 0 && (typeof c.sub !== "string" || !/^0[23][0-9a-f]{64}$/.test(c.sub))) throw new AccessError("bad_request", "bad sub");
654
+ c.require = normaliseRequirement(c.require);
655
+ if (canonicalJson(c) !== s) throw new AccessError("bad_request", "challenge is not canonical");
656
+ return c;
657
+ }
658
+
659
+ // src/access-client.ts
660
+ var ONESAT = [0, "onesat"];
661
+ var LEGACY_ONESAT = [0, "p 1sat"];
662
+ function toAccessError(e) {
663
+ if (e instanceof AccessError) return e;
664
+ const err = e;
665
+ const msg = String(err?.message ?? e ?? "");
666
+ if (err?.code === "NOT_FOUND") return new AccessError("not_installed", msg);
667
+ if (/reject|denied|declin|cancel|user/i.test(msg) || err?.code === "ERR_USER_REJECTED" || err?.status === 4001) {
668
+ return new AccessError("user_rejected", msg);
669
+ }
670
+ return new AccessError("bad_request", msg || "access failed");
671
+ }
672
+ function throwIfAccessError(body) {
673
+ const b = body;
674
+ if (b && typeof b.error === "string") throw new AccessError(b.error, b.message, { have: b.have, need: b.need });
675
+ }
676
+ function hash160FromP2pkh(script) {
677
+ const m = /^76a914([0-9a-f]{40})88ac$/i.exec(script.slice(-50));
678
+ return m ? m[1].toLowerCase() : null;
679
+ }
680
+ async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
681
+ const bare = tokenId.replace(/^bsv21:/, "");
682
+ const list = await wallet.listOutputs({ basket, include: "locking scripts", includeCustomInstructions: true, limit: 1e4 });
683
+ const byKey = /* @__PURE__ */ new Map();
684
+ const data = challengeBytes(challenge);
685
+ for (const out of list.outputs) {
686
+ if (byKey.size >= MAX_HOLDING_PROOFS) break;
687
+ let ci = {};
688
+ try {
689
+ ci = JSON.parse(out.customInstructions ?? "{}");
690
+ } catch {
691
+ continue;
692
+ }
693
+ if (ci.id !== bare || !ci.keyID) continue;
694
+ const want = out.lockingScript ? hash160FromP2pkh(out.lockingScript) : null;
695
+ const counterparty = ci.counterparty ?? "self";
696
+ for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
697
+ const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
698
+ const h = import_sdk4.Utils.toHex(import_sdk4.Hash.hash160(import_sdk4.PublicKey.fromString(publicKey).encode(true)));
699
+ if (want && h !== want) continue;
700
+ const existing = byKey.get(publicKey);
701
+ if (existing) {
702
+ existing.outpoints.push(out.outpoint.replace(".", "_"));
703
+ break;
704
+ }
705
+ const { signature } = await wallet.createSignature({ protocolID, keyID: ci.keyID, counterparty, data });
706
+ byKey.set(publicKey, { pubkey: publicKey, signature: Array.from(signature), outpoints: [out.outpoint.replace(".", "_")] });
707
+ break;
708
+ }
709
+ }
710
+ return [...byKey.values()];
711
+ }
712
+ async function requestAccess(o) {
713
+ let wallet;
714
+ try {
715
+ ({ wallet } = await connectBwalletX({ wallet: o.wallet, pairing: o.pairing, discoverMs: o.discoverMs }));
716
+ } catch (e) {
717
+ throw e?.code === "NOT_FOUND" ? new AccessError("not_installed") : toAccessError(e);
718
+ }
719
+ try {
720
+ const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
721
+ const { challenge } = await o.challenge(identityKey);
722
+ const c = parseAccessChallenge(challenge);
723
+ if (c.sub && c.sub !== identityKey.toLowerCase()) throw new AccessError("wrong_account");
724
+ if (Date.now() / 1e3 >= c.exp) throw new AccessError("expired");
725
+ const { signature } = await wallet.createSignature({
726
+ protocolID: ACCESS_PROTOCOL,
727
+ keyID: c.nonce,
728
+ counterparty: "anyone",
729
+ data: challengeBytes(challenge)
730
+ });
731
+ const response = { v: 1, challenge, identityKey, signature: Array.from(signature) };
732
+ if (c.require.token) response.holdings = await proveHoldings(wallet, challenge, c.require.token.id, o.tokenBasket);
733
+ let verified;
734
+ if (o.verify) {
735
+ verified = await o.verify(response);
736
+ throwIfAccessError(verified);
737
+ }
738
+ return { response, verified, wallet };
739
+ } catch (e) {
740
+ throw toAccessError(e);
741
+ }
742
+ }
743
+
744
+ // src/element.ts
745
+ var SIGNIN_TAG = "bwalletx-signin";
746
+ var GET_URL = "https://bwalletx.com/get";
747
+ var WEB_WALLET_URL = "https://web.bwalletx.com";
748
+ var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect" };
749
+ function signinText(label = "signin", brand = "bWalletX") {
750
+ return `${VERBS[label] ?? VERBS.signin} ${brand}`;
751
+ }
752
+ var uid = 0;
753
+ function markSvg(px = 20) {
754
+ const n = ++uid;
755
+ return `<svg viewBox="23 8 74 100" width="${Math.round(px * 0.74)}" height="${px}" aria-hidden="true" focusable="false"><defs><linearGradient id="bwxg${n}" gradientUnits="userSpaceOnUse" x1="0" y1="12" x2="0" y2="105"><stop offset="0" stop-color="#FFE58A"/><stop offset=".55" stop-color="#FFD24D"/><stop offset="1" stop-color="#C98F00"/></linearGradient><mask id="bwxh${n}"><rect width="140" height="140" fill="#fff"/><circle cx="60" cy="72" r="15" fill="#000"/></mask></defs><g fill="url(#bwxg${n})" mask="url(#bwxh${n})"><polygon points="45,12 45,76 27,76 27,30"/><circle cx="60" cy="72" r="33"/></g></svg>`;
756
+ }
757
+ var SIGNIN_CSS = `
758
+ :host{display:inline-block;vertical-align:middle;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
759
+ :host([full-width]){display:block;width:100%}
760
+ :host([hidden]){display:none}
761
+ button{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:12px;width:100%;cursor:pointer;
762
+ padding:16px 20px;border-radius:12px;font-size:16px;font-weight:700;line-height:1.2;text-align:left;
763
+ background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;
764
+ box-shadow:0 10px 15px -3px rgba(245,158,11,.2),0 4px 6px -4px rgba(245,158,11,.2);
765
+ transition:background .15s,transform .1s,box-shadow .15s,border-color .15s;-webkit-tap-highlight-color:transparent}
766
+ button:hover{background:linear-gradient(180deg,#fde68a,#fbbf24)}
767
+ button:active{transform:translateY(1px);background:linear-gradient(180deg,#fbbf24,#d97706)}
768
+ button:focus-visible{outline:2px solid #b45309;outline-offset:3px}
769
+ button:disabled{opacity:.6;cursor:not-allowed;transform:none}
770
+ button[aria-busy=true]{cursor:wait}
771
+ .mark{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
772
+ .txt{display:flex;flex-direction:column;align-items:flex-start;min-width:0}
773
+ .txt b{font-weight:700;white-space:nowrap}
774
+ .txt small{font-size:11px;font-weight:500;color:rgba(0,0,0,.65);white-space:nowrap}
775
+ .spin{width:16px;height:16px;border-radius:50%;border:2px solid rgba(255,210,77,.35);border-top-color:#ffd24d;animation:r .8s linear infinite}
776
+ @keyframes r{to{transform:rotate(360deg)}}
777
+ @media (prefers-reduced-motion:reduce){button{transition:none}button:active{transform:none}.spin{animation:none;border-color:#ffd24d;border-right-color:transparent}}
778
+ /* sizes */
779
+ :host([size=md]) button{padding:10px 16px;border-radius:10px;font-size:15px;gap:10px}
780
+ :host([size=md]) .mark{width:24px;height:24px}
781
+ :host([size=md]) .txt small{font-size:10.5px}
782
+ :host([size=sm]) button{padding:6px 12px 6px 6px;border-radius:8px;font-size:13px;gap:8px;min-height:36px}
783
+ :host([size=sm]) .mark{width:22px;height:22px}
784
+ :host([size=icon]) button{width:48px;height:48px;padding:0;border-radius:50%;gap:0}
785
+ :host([size=icon]) .mark{width:34px;height:34px}
786
+ /* themes */
787
+ :host([theme=dark]) button{background:#0a0a0a;color:#fafafa;border:1px solid #3f3f46;box-shadow:none}
788
+ :host([theme=dark]) button:hover{background:#18181b;border-color:rgba(251,191,36,.6)}
789
+ :host([theme=dark]) button:active{background:#000}
790
+ :host([theme=dark]) button:focus-visible{outline-color:#fcd34d}
791
+ :host([theme=dark]) .mark{box-shadow:inset 0 0 0 1px #3f3f46}
792
+ :host([theme=dark]) .txt small{color:rgba(255,255,255,.65)}
793
+ :host([theme=light]) button{background:#fff;color:#09090b;border:1px solid #d4d4d8;box-shadow:0 1px 2px rgba(0,0,0,.06)}
794
+ :host([theme=light]) button:hover{background:#fafafa;border-color:#f59e0b}
795
+ :host([theme=light]) button:active{background:#f4f4f5}
796
+ :host([theme=light]) .txt small{color:#52525b}
797
+ /* not found */
798
+ .nf{margin-top:8px;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
799
+ .nf a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
800
+ .nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
801
+ :host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
802
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
803
+ `;
804
+ function b64url2(bytes) {
805
+ let s = "";
806
+ for (const b of bytes) s += String.fromCharCode(b);
807
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
808
+ }
809
+ async function buildAuthorizeUrl(p) {
810
+ const rnd = (n) => b64url2(crypto.getRandomValues(new Uint8Array(n)));
811
+ const verifier = rnd(32);
812
+ const state = rnd(16);
813
+ const nonce = rnd(16);
814
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
815
+ const u = new URL(p.authorizeUrl, typeof location !== "undefined" ? location.href : void 0);
816
+ const set = (k, v) => {
817
+ if (v) u.searchParams.set(k, v);
818
+ };
819
+ set("response_type", "code");
820
+ set("client_id", p.clientId);
821
+ set("redirect_uri", p.redirectUri);
822
+ set("scope", p.scope || "openid profile");
823
+ set("state", state);
824
+ set("nonce", nonce);
825
+ set("code_challenge", b64url2(digest));
826
+ set("code_challenge_method", "S256");
827
+ try {
828
+ sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
829
+ } catch {
830
+ }
831
+ return u.toString();
832
+ }
833
+ async function postJson(url, body) {
834
+ const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
835
+ const data = await res.json().catch(() => ({}));
836
+ if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
837
+ return data;
838
+ }
839
+ async function postAccess(url, body) {
840
+ const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
841
+ return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
842
+ }
843
+ var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
844
+ function createClass() {
845
+ return class BwalletxSigninElement extends HTMLElement {
846
+ static get observedAttributes() {
847
+ return OBSERVED;
848
+ }
849
+ #root;
850
+ #notFound = false;
851
+ #busy = false;
852
+ constructor() {
853
+ super();
854
+ this.#root = this.attachShadow({ mode: "open" });
855
+ }
856
+ connectedCallback() {
857
+ this.#render();
858
+ }
859
+ attributeChangedCallback() {
860
+ if (this.isConnected) this.#render();
861
+ }
862
+ /** The inner <button>, for focus() and tests. */
863
+ get button() {
864
+ return this.#root.querySelector("button");
865
+ }
866
+ get notFound() {
867
+ return this.#notFound;
868
+ }
869
+ focus(opts) {
870
+ this.button?.focus(opts);
871
+ }
872
+ #attr(name) {
873
+ return this.getAttribute(name);
874
+ }
875
+ #render() {
876
+ const size = this.#attr("size") ?? "lg";
877
+ const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
878
+ const text = signinText(this.#attr("label") ?? "signin", brand);
879
+ const subAttr = this.#attr("subtitle");
880
+ const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
881
+ const subtitle = subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
882
+ const loading = this.hasAttribute("loading") || this.#busy;
883
+ const disabled = this.hasAttribute("disabled");
884
+ const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
885
+ const aria = this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
886
+ const getUrl = this.#attr("get-url") ?? GET_URL;
887
+ const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
888
+ const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
889
+ const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
890
+ const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : markSvg(markPx);
891
+ const nf = this.#notFound ? `<p class="nf" part="not-found" role="status">bWalletX wasn't found in this browser. <a href="${esc(getUrl)}" target="_blank" rel="noopener">Get ${brand}</a> or <a href="${esc(webUrl)}" target="_blank" rel="noopener">use the web wallet</a>.</p>` : "";
892
+ this.#root.innerHTML = `<style>${SIGNIN_CSS}</style><button type="button" part="button" aria-label="${esc(aria)}" title="${esc(subtitle ? `${text} (${subtitle})` : text)}"${disabled ? " disabled" : ""}${loading ? ' aria-busy="true"' : ""}><span class="mark" part="mark">${mark}</span>${label}</button>${loading ? '<span class="sr" role="status">Connecting to bWalletX\u2026</span>' : ""}${nf}`;
893
+ this.button.addEventListener("click", () => void this.#click());
894
+ }
895
+ #emit(type, detail, cancelable = false) {
896
+ return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
897
+ }
898
+ #setBusy(on) {
899
+ this.#busy = on;
900
+ this.#render();
901
+ }
902
+ async #click() {
903
+ if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy) return;
904
+ if (!this.#emit("bwalletx-signin", { element: this }, true)) return;
905
+ const href = this.#attr("href");
906
+ const authorizeUrl = this.#attr("authorize-url");
907
+ if (authorizeUrl) {
908
+ location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
909
+ return;
910
+ }
911
+ if (href) {
912
+ location.assign(href);
913
+ return;
914
+ }
915
+ const challengeUrl = this.#attr("challenge-url");
916
+ if (!challengeUrl) return;
917
+ this.#notFound = false;
918
+ this.#setBusy(true);
919
+ try {
920
+ const ms = Number(this.#attr("discover-ms")) || 1200;
921
+ const found = await discoverBwalletX(ms);
922
+ if (!found) {
923
+ this.#notFound = true;
924
+ this.#emit("bwalletx-not-found");
925
+ if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
926
+ return;
927
+ }
928
+ const requireToken = this.#attr("require-token");
929
+ if (requireToken) {
930
+ const verifyUrl2 = this.#attr("verify-url");
931
+ const min = this.#attr("min");
932
+ const minBps = this.#attr("min-bps");
933
+ const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
934
+ try {
935
+ const a = await requestAccess({
936
+ wallet: found.wallet,
937
+ challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
938
+ verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
939
+ });
940
+ this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
941
+ } catch (e) {
942
+ const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
943
+ this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
944
+ }
945
+ return;
946
+ }
947
+ const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
948
+ const verifyUrl = this.#attr("verify-url");
949
+ const response = verifyUrl ? await postJson(verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }) : null;
950
+ this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: found.method, nonce: r.nonce, signature: r.signature, response });
951
+ } catch (error) {
952
+ this.#emit("bwalletx-error", { error });
953
+ } finally {
954
+ this.#setBusy(false);
955
+ }
956
+ }
957
+ };
958
+ }
959
+ function defineBwalletxSignin(tag = SIGNIN_TAG) {
960
+ if (typeof window === "undefined" || typeof customElements === "undefined") return void 0;
961
+ const existing = customElements.get(tag);
962
+ if (existing) return existing;
963
+ const C = createClass();
964
+ customElements.define(tag, C);
965
+ return C;
966
+ }
967
+
968
+ // src/modal.ts
472
969
  var CSS = `
473
970
  .bwx-ov{position:fixed;inset:0;z-index:2147483000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;font:15px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
474
971
  .bwx-box{background:#0b0b0b;color:#f4f1e8;border:1px solid #3a3220;border-radius:16px;width:min(360px,100%);padding:20px;box-shadow:0 20px 60px rgba(0,0,0,.5)}
@@ -480,6 +977,9 @@ var CSS = `
480
977
  .bwx-row{display:flex;gap:8px;margin-top:14px}
481
978
  .bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
482
979
  .bwx-btn:hover{border-color:#e8c766}
980
+ a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}
981
+ .bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}
982
+ .bwx-stack{display:flex;flex-direction:column;gap:8px}
483
983
  .bwx-list{display:flex;flex-direction:column;gap:8px}
484
984
  .bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
485
985
  .bwx-opt img{width:24px;height:24px;border-radius:6px}
@@ -510,6 +1010,18 @@ function qrSvg(text) {
510
1010
  q.make();
511
1011
  return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
512
1012
  }
1013
+ function isTouchDevice(env = globalThis) {
1014
+ const nav = env.navigator;
1015
+ if (!nav) return false;
1016
+ const ua = nav.userAgent ?? "";
1017
+ if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;
1018
+ if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;
1019
+ try {
1020
+ return !!env.matchMedia?.("(pointer: coarse)").matches && !env.matchMedia?.("(hover: hover)").matches;
1021
+ } catch {
1022
+ return false;
1023
+ }
1024
+ }
513
1025
  function pairingModal(o = {}) {
514
1026
  const ac = new AbortController();
515
1027
  let ui = null;
@@ -526,20 +1038,37 @@ function pairingModal(o = {}) {
526
1038
  if (typeof document === "undefined") return;
527
1039
  close();
528
1040
  ui = mount();
1041
+ const app = o.mode === "app" || (o.mode ?? "auto") === "auto" && isTouchDevice();
529
1042
  const h = el("h2", "", o.title ?? "Connect bWalletX");
1043
+ const cancel = el("button", "bwx-btn", "Cancel");
1044
+ cancel.onclick = () => {
1045
+ o.onCancel?.();
1046
+ ac.abort();
1047
+ };
1048
+ codeEl = el("div", "bwx-code", "");
1049
+ codeEl.setAttribute("aria-live", "polite");
1050
+ if (app) {
1051
+ const p2 = el("p", "", "Open bWalletX, approve, then come back to this page.");
1052
+ const stack = el("div", "bwx-stack");
1053
+ const open = el("a", "bwx-btn bwx-primary", "Open bWalletX");
1054
+ open.href = link;
1055
+ open.rel = "noopener";
1056
+ const web = el("button", "bwx-btn", "Use web.bwalletx.com instead");
1057
+ web.onclick = () => {
1058
+ void navigator.clipboard?.writeText(link).catch(() => void 0);
1059
+ window.open(WEB_WALLET_URL, "_blank", "noopener");
1060
+ p2.textContent = "Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.";
1061
+ };
1062
+ stack.append(open, web, cancel);
1063
+ ui.box.append(h, p2, codeEl, stack);
1064
+ return;
1065
+ }
530
1066
  const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
531
1067
  const qr = el("div", "bwx-qr");
532
1068
  qr.innerHTML = qrSvg(link);
533
- codeEl = el("div", "bwx-code", "");
534
- codeEl.setAttribute("aria-live", "polite");
535
1069
  const row = el("div", "bwx-row");
536
1070
  const copy = el("button", "bwx-btn", "Copy link");
537
1071
  copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => copy.textContent = "Copied");
538
- const cancel = el("button", "bwx-btn", "Cancel");
539
- cancel.onclick = () => {
540
- o.onCancel?.();
541
- ac.abort();
542
- };
543
1072
  row.append(copy, cancel);
544
1073
  ui.box.append(h, p, qr, codeEl, row);
545
1074
  },
@@ -585,7 +1114,7 @@ function pickWallet(options, o = {}) {
585
1114
  }
586
1115
 
587
1116
  // src/wallets.ts
588
- var import_sdk3 = require("@bsv/sdk");
1117
+ var import_sdk5 = require("@bsv/sdk");
589
1118
  async function brc100Connected(wallet, o) {
590
1119
  await wallet.waitForAuthentication({}).catch(() => {
591
1120
  });
@@ -644,7 +1173,7 @@ function handcashAdapter(o) {
644
1173
  var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
645
1174
  async function probe(substrate, ms) {
646
1175
  try {
647
- const w = new import_sdk3.WalletClient(substrate);
1176
+ const w = new import_sdk5.WalletClient(substrate);
648
1177
  const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
649
1178
  return ok ? w : null;
650
1179
  } catch {
@@ -685,7 +1214,7 @@ async function discoverWallets(o = {}) {
685
1214
  }
686
1215
 
687
1216
  // src/pay.ts
688
- var import_sdk4 = require("@bsv/sdk");
1217
+ var import_sdk6 = require("@bsv/sdk");
689
1218
  var BRC29_PROTOCOL = [2, "3241645161d8"];
690
1219
  var PaymentError = class extends Error {
691
1220
  constructor(code, message, maybeSent = false, cause) {
@@ -705,13 +1234,13 @@ var isPaymail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
705
1234
  function randomB64(n) {
706
1235
  const b = new Uint8Array(n);
707
1236
  globalThis.crypto.getRandomValues(b);
708
- return import_sdk4.Utils.toBase64(Array.from(b));
1237
+ return import_sdk6.Utils.toBase64(Array.from(b));
709
1238
  }
710
1239
  function opReturn(text) {
711
- const d = import_sdk4.Utils.toArray(text, "utf8");
1240
+ const d = import_sdk6.Utils.toArray(text, "utf8");
712
1241
  const len = d.length;
713
1242
  const push = len < 76 ? [len] : len <= 255 ? [76, len] : [77, len & 255, len >> 8];
714
- return import_sdk4.Utils.toHex([0, 106, ...push, ...d]);
1243
+ return import_sdk6.Utils.toHex([0, 106, ...push, ...d]);
715
1244
  }
716
1245
  var clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
717
1246
  var describe = (s) => clip(s.length < 5 ? `${s} payment`.trim() : s, 50);
@@ -807,7 +1336,7 @@ async function pay(source, req, o = {}) {
807
1336
  } catch (e) {
808
1337
  throw new PaymentError("WALLET_ERROR", `The wallet could not derive the payment key: ${e.message}`, false, e);
809
1338
  }
810
- outputs = [{ script: new import_sdk4.P2PKH().lock(import_sdk4.PublicKey.fromString(pk).toHash()).toHex(), satoshis: sats, description: "payment" }];
1339
+ outputs = [{ script: new import_sdk6.P2PKH().lock(import_sdk6.PublicKey.fromString(pk).toHash()).toHex(), satoshis: sats, description: "payment" }];
811
1340
  remittance = { senderIdentityKey: me, derivationPrefix, derivationSuffix, outputIndex: 0 };
812
1341
  } else if (isPaymail(to)) {
813
1342
  const r2 = await (o.resolvePaymail ?? ((p, s) => resolvePaymailP2P(p, s, o.fetch ?? fetch)))(to, sats);
@@ -815,7 +1344,7 @@ async function pay(source, req, o = {}) {
815
1344
  submit = r2.submit;
816
1345
  } else {
817
1346
  try {
818
- outputs = [{ script: new import_sdk4.P2PKH().lock(to).toHex(), satoshis: sats, description: "payment" }];
1347
+ outputs = [{ script: new import_sdk6.P2PKH().lock(to).toHex(), satoshis: sats, description: "payment" }];
819
1348
  } catch {
820
1349
  throw new PaymentError("BAD_REQUEST", `Not a paymail, address or identity key: ${to}`);
821
1350
  }
@@ -825,7 +1354,7 @@ async function pay(source, req, o = {}) {
825
1354
  if (!r.txid) throw new PaymentError("WALLET_ERROR", "The wallet did not return a txid", true);
826
1355
  const tx = r.tx ? Array.from(r.tx) : void 0;
827
1356
  if (submit && tx) {
828
- await submit(import_sdk4.Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {
1357
+ await submit(import_sdk6.Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {
829
1358
  });
830
1359
  }
831
1360
  return { txid: r.txid, tx, sats, outputs, remittance };
@@ -845,206 +1374,6 @@ async function buyToken(source, req) {
845
1374
  return res;
846
1375
  }
847
1376
 
848
- // src/element.ts
849
- var SIGNIN_TAG = "bwalletx-signin";
850
- var GET_URL = "https://bwalletx.com/get";
851
- var WEB_WALLET_URL = "https://web.bwalletx.com";
852
- var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect" };
853
- function signinText(label = "signin", brand = "bWalletX") {
854
- return `${VERBS[label] ?? VERBS.signin} ${brand}`;
855
- }
856
- var uid = 0;
857
- function markSvg(px = 20) {
858
- const n = ++uid;
859
- return `<svg viewBox="23 8 74 100" width="${Math.round(px * 0.74)}" height="${px}" aria-hidden="true" focusable="false"><defs><linearGradient id="bwxg${n}" gradientUnits="userSpaceOnUse" x1="0" y1="12" x2="0" y2="105"><stop offset="0" stop-color="#FFE58A"/><stop offset=".55" stop-color="#FFD24D"/><stop offset="1" stop-color="#C98F00"/></linearGradient><mask id="bwxh${n}"><rect width="140" height="140" fill="#fff"/><circle cx="60" cy="72" r="15" fill="#000"/></mask></defs><g fill="url(#bwxg${n})" mask="url(#bwxh${n})"><polygon points="45,12 45,76 27,76 27,30"/><circle cx="60" cy="72" r="33"/></g></svg>`;
860
- }
861
- var SIGNIN_CSS = `
862
- :host{display:inline-block;vertical-align:middle;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
863
- :host([full-width]){display:block;width:100%}
864
- :host([hidden]){display:none}
865
- button{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:12px;width:100%;cursor:pointer;
866
- padding:16px 20px;border-radius:12px;font-size:16px;font-weight:700;line-height:1.2;text-align:left;
867
- background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;
868
- box-shadow:0 10px 15px -3px rgba(245,158,11,.2),0 4px 6px -4px rgba(245,158,11,.2);
869
- transition:background .15s,transform .1s,box-shadow .15s,border-color .15s;-webkit-tap-highlight-color:transparent}
870
- button:hover{background:linear-gradient(180deg,#fde68a,#fbbf24)}
871
- button:active{transform:translateY(1px);background:linear-gradient(180deg,#fbbf24,#d97706)}
872
- button:focus-visible{outline:2px solid #b45309;outline-offset:3px}
873
- button:disabled{opacity:.6;cursor:not-allowed;transform:none}
874
- button[aria-busy=true]{cursor:wait}
875
- .mark{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
876
- .txt{display:flex;flex-direction:column;align-items:flex-start;min-width:0}
877
- .txt b{font-weight:700;white-space:nowrap}
878
- .txt small{font-size:11px;font-weight:500;color:rgba(0,0,0,.65);white-space:nowrap}
879
- .spin{width:16px;height:16px;border-radius:50%;border:2px solid rgba(255,210,77,.35);border-top-color:#ffd24d;animation:r .8s linear infinite}
880
- @keyframes r{to{transform:rotate(360deg)}}
881
- @media (prefers-reduced-motion:reduce){button{transition:none}button:active{transform:none}.spin{animation:none;border-color:#ffd24d;border-right-color:transparent}}
882
- /* sizes */
883
- :host([size=md]) button{padding:10px 16px;border-radius:10px;font-size:15px;gap:10px}
884
- :host([size=md]) .mark{width:24px;height:24px}
885
- :host([size=md]) .txt small{font-size:10.5px}
886
- :host([size=sm]) button{padding:6px 12px 6px 6px;border-radius:8px;font-size:13px;gap:8px;min-height:36px}
887
- :host([size=sm]) .mark{width:22px;height:22px}
888
- :host([size=icon]) button{width:48px;height:48px;padding:0;border-radius:50%;gap:0}
889
- :host([size=icon]) .mark{width:34px;height:34px}
890
- /* themes */
891
- :host([theme=dark]) button{background:#0a0a0a;color:#fafafa;border:1px solid #3f3f46;box-shadow:none}
892
- :host([theme=dark]) button:hover{background:#18181b;border-color:rgba(251,191,36,.6)}
893
- :host([theme=dark]) button:active{background:#000}
894
- :host([theme=dark]) button:focus-visible{outline-color:#fcd34d}
895
- :host([theme=dark]) .mark{box-shadow:inset 0 0 0 1px #3f3f46}
896
- :host([theme=dark]) .txt small{color:rgba(255,255,255,.65)}
897
- :host([theme=light]) button{background:#fff;color:#09090b;border:1px solid #d4d4d8;box-shadow:0 1px 2px rgba(0,0,0,.06)}
898
- :host([theme=light]) button:hover{background:#fafafa;border-color:#f59e0b}
899
- :host([theme=light]) button:active{background:#f4f4f5}
900
- :host([theme=light]) .txt small{color:#52525b}
901
- /* not found */
902
- .nf{margin-top:8px;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
903
- .nf a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
904
- .nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
905
- :host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
906
- .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
907
- `;
908
- function b64url2(bytes) {
909
- let s = "";
910
- for (const b of bytes) s += String.fromCharCode(b);
911
- return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
912
- }
913
- async function buildAuthorizeUrl(p) {
914
- const rnd = (n) => b64url2(crypto.getRandomValues(new Uint8Array(n)));
915
- const verifier = rnd(32);
916
- const state = rnd(16);
917
- const nonce = rnd(16);
918
- const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
919
- const u = new URL(p.authorizeUrl, typeof location !== "undefined" ? location.href : void 0);
920
- const set = (k, v) => {
921
- if (v) u.searchParams.set(k, v);
922
- };
923
- set("response_type", "code");
924
- set("client_id", p.clientId);
925
- set("redirect_uri", p.redirectUri);
926
- set("scope", p.scope || "openid profile");
927
- set("state", state);
928
- set("nonce", nonce);
929
- set("code_challenge", b64url2(digest));
930
- set("code_challenge_method", "S256");
931
- try {
932
- sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
933
- } catch {
934
- }
935
- return u.toString();
936
- }
937
- async function postJson(url, body) {
938
- const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
939
- const data = await res.json().catch(() => ({}));
940
- if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
941
- return data;
942
- }
943
- var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
944
- function createClass() {
945
- return class BwalletxSigninElement extends HTMLElement {
946
- static get observedAttributes() {
947
- return OBSERVED;
948
- }
949
- #root;
950
- #notFound = false;
951
- #busy = false;
952
- constructor() {
953
- super();
954
- this.#root = this.attachShadow({ mode: "open" });
955
- }
956
- connectedCallback() {
957
- this.#render();
958
- }
959
- attributeChangedCallback() {
960
- if (this.isConnected) this.#render();
961
- }
962
- /** The inner <button>, for focus() and tests. */
963
- get button() {
964
- return this.#root.querySelector("button");
965
- }
966
- get notFound() {
967
- return this.#notFound;
968
- }
969
- focus(opts) {
970
- this.button?.focus(opts);
971
- }
972
- #attr(name) {
973
- return this.getAttribute(name);
974
- }
975
- #render() {
976
- const size = this.#attr("size") ?? "lg";
977
- const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
978
- const text = signinText(this.#attr("label") ?? "signin", brand);
979
- const subAttr = this.#attr("subtitle");
980
- const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
981
- const subtitle = subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
982
- const loading = this.hasAttribute("loading") || this.#busy;
983
- const disabled = this.hasAttribute("disabled");
984
- const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
985
- const aria = this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
986
- const getUrl = this.#attr("get-url") ?? GET_URL;
987
- const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
988
- const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
989
- const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
990
- const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : markSvg(markPx);
991
- const nf = this.#notFound ? `<p class="nf" part="not-found" role="status">bWalletX wasn't found in this browser. <a href="${esc(getUrl)}" target="_blank" rel="noopener">Get ${brand}</a> or <a href="${esc(webUrl)}" target="_blank" rel="noopener">use the web wallet</a>.</p>` : "";
992
- this.#root.innerHTML = `<style>${SIGNIN_CSS}</style><button type="button" part="button" aria-label="${esc(aria)}" title="${esc(subtitle ? `${text} (${subtitle})` : text)}"${disabled ? " disabled" : ""}${loading ? ' aria-busy="true"' : ""}><span class="mark" part="mark">${mark}</span>${label}</button>${loading ? '<span class="sr" role="status">Connecting to bWalletX\u2026</span>' : ""}${nf}`;
993
- this.button.addEventListener("click", () => void this.#click());
994
- }
995
- #emit(type, detail, cancelable = false) {
996
- return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
997
- }
998
- #setBusy(on) {
999
- this.#busy = on;
1000
- this.#render();
1001
- }
1002
- async #click() {
1003
- if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy) return;
1004
- if (!this.#emit("bwalletx-signin", { element: this }, true)) return;
1005
- const href = this.#attr("href");
1006
- const authorizeUrl = this.#attr("authorize-url");
1007
- if (authorizeUrl) {
1008
- location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
1009
- return;
1010
- }
1011
- if (href) {
1012
- location.assign(href);
1013
- return;
1014
- }
1015
- const challengeUrl = this.#attr("challenge-url");
1016
- if (!challengeUrl) return;
1017
- this.#notFound = false;
1018
- this.#setBusy(true);
1019
- try {
1020
- const ms = Number(this.#attr("discover-ms")) || 1200;
1021
- const found = await discoverBwalletX(ms);
1022
- if (!found) {
1023
- this.#notFound = true;
1024
- this.#emit("bwalletx-not-found");
1025
- return;
1026
- }
1027
- const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
1028
- const verifyUrl = this.#attr("verify-url");
1029
- const response = verifyUrl ? await postJson(verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }) : null;
1030
- this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: found.method, nonce: r.nonce, signature: r.signature, response });
1031
- } catch (error) {
1032
- this.#emit("bwalletx-error", { error });
1033
- } finally {
1034
- this.#setBusy(false);
1035
- }
1036
- }
1037
- };
1038
- }
1039
- function defineBwalletxSignin(tag = SIGNIN_TAG) {
1040
- if (typeof window === "undefined" || typeof customElements === "undefined") return void 0;
1041
- const existing = customElements.get(tag);
1042
- if (existing) return existing;
1043
- const C = createClass();
1044
- customElements.define(tag, C);
1045
- return C;
1046
- }
1047
-
1048
1377
  // src/react.ts
1049
1378
  function BwalletxSignin(props) {
1050
1379
  const ref = (0, import_react.useRef)(null);
@@ -1124,6 +1453,12 @@ function BwalletXProvider(p) {
1124
1453
  props.current.onError?.(err);
1125
1454
  return err;
1126
1455
  }, []);
1456
+ const themeOn = !!p.theme && (state.status === "connecting" || state.status === "connected");
1457
+ (0, import_react.useEffect)(() => {
1458
+ if (!themeOn) return;
1459
+ declareWalletTheme(p.theme);
1460
+ return () => declareWalletTheme(null);
1461
+ }, [themeOn, p.theme]);
1127
1462
  (0, import_react.useEffect)(() => {
1128
1463
  const url = p.sessionUrl;
1129
1464
  if (!url) return;
@@ -1174,7 +1509,7 @@ function BwalletXProvider(p) {
1174
1509
  wallet: cw.wallet,
1175
1510
  challenge: (identityKey) => postJson2(c.challengeUrl, { identityKey })
1176
1511
  });
1177
- const v = await postJson2(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature });
1512
+ const v = await atStage("verify", () => postJson2(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }));
1178
1513
  handle = v.handle ?? handle;
1179
1514
  paymail = v.paymail ?? paymail;
1180
1515
  signedIn = true;