@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/index.cjs CHANGED
@@ -30,23 +30,32 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ ACCESS_ERROR_STATUS: () => ACCESS_ERROR_STATUS,
34
+ ACCESS_PROTOCOL: () => ACCESS_PROTOCOL,
35
+ ACCESS_VERSION: () => ACCESS_VERSION,
36
+ AccessError: () => AccessError,
33
37
  BRC29_PROTOCOL: () => BRC29_PROTOCOL,
34
38
  BWALLETX_RDNS: () => BWALLETX_RDNS,
35
39
  GET_URL: () => GET_URL,
36
40
  LOGIN_PROTOCOL: () => LOGIN_PROTOCOL,
37
41
  LOGO_URL: () => LOGO_URL,
42
+ PROTOCOL_MEDIA: () => PROTOCOL_MEDIA,
38
43
  PaymentError: () => PaymentError,
39
44
  RDNS_EXTENSION: () => RDNS_EXTENSION,
40
45
  RDNS_MOBILE: () => RDNS_MOBILE,
41
46
  SIGNIN_CSS: () => SIGNIN_CSS,
42
47
  SIGNIN_TAG: () => SIGNIN_TAG,
48
+ WALLET_THEME_META: () => WALLET_THEME_META,
43
49
  WEB_WALLET_URL: () => WEB_WALLET_URL,
50
+ accessErrorMessage: () => accessErrorMessage,
44
51
  amountInSats: () => amountInSats,
45
52
  brc100Option: () => brc100Option,
46
53
  buildAuthorizeUrl: () => buildAuthorizeUrl,
47
54
  buyToken: () => buyToken,
55
+ canonicalJson: () => canonicalJson,
48
56
  clearPairing: () => clearPairing,
49
57
  connectBwalletX: () => connectBwalletX,
58
+ declareWalletTheme: () => declareWalletTheme,
50
59
  defineBwalletxSignin: () => defineBwalletxSignin,
51
60
  discoverBwalletX: () => discoverBwalletX,
52
61
  discoverWallets: () => discoverWallets,
@@ -55,23 +64,30 @@ __export(src_exports, {
55
64
  inOrder: () => inOrder,
56
65
  isIdentityKeyHex: () => isIdentityKeyHex,
57
66
  isPaymail: () => isPaymail,
67
+ isTouchDevice: () => isTouchDevice,
58
68
  markSvg: () => markSvg,
59
69
  pairBwalletX: () => pairBwalletX,
60
70
  pairingModal: () => pairingModal,
61
71
  pairingOption: () => pairingOption,
62
72
  pay: () => pay,
63
73
  pickWallet: () => pickWallet,
74
+ protocolMediaUrl: () => protocolMediaUrl,
75
+ proveHoldings: () => proveHoldings,
64
76
  qrSvg: () => qrSvg,
65
77
  renderButton: () => renderButton,
78
+ requestAccess: () => requestAccess,
66
79
  resolvePaymailP2P: () => resolvePaymailP2P,
67
80
  restorePairing: () => restorePairing,
68
81
  signInWithBwalletX: () => signInWithBwalletX,
69
82
  signinText: () => signinText,
70
- tip: () => tip
83
+ throwIfAccessError: () => throwIfAccessError,
84
+ tip: () => tip,
85
+ toAccessError: () => toAccessError
71
86
  });
72
87
  module.exports = __toCommonJS(src_exports);
73
88
 
74
89
  // src/discover.ts
90
+ var import_sdk = require("@bsv/sdk");
75
91
  var RDNS_EXTENSION = "com.bwalletx.extension";
76
92
  var RDNS_MOBILE = "space.bwallet.mobile";
77
93
  var BWALLETX_RDNS = [RDNS_EXTENSION, RDNS_MOBILE];
@@ -96,16 +112,70 @@ function discoverBwalletX(ms = 1200) {
96
112
  window.dispatchEvent(new Event("brc100:requestWallet"));
97
113
  setTimeout(() => {
98
114
  const cwi = window.CWI;
99
- finish(cwi ? { wallet: cwi, method: "in-app", rdns: null } : null);
115
+ if (cwi) return finish({ wallet: cwi, method: "in-app", rdns: null });
116
+ if (!isFramed()) return finish(null);
117
+ void probeXdm(ms).then((w) => finish(w ? { wallet: w, method: "in-app", rdns: null } : null));
100
118
  }, ms);
101
119
  });
102
120
  }
103
121
  async function findBwalletX(ms = 1200) {
104
122
  return (await discoverBwalletX(ms))?.wallet ?? null;
105
123
  }
124
+ var isFramed = () => {
125
+ try {
126
+ return window.parent !== window;
127
+ } catch {
128
+ return true;
129
+ }
130
+ };
131
+ var BWALLETX_PARENT_ORIGINS = [
132
+ "https://web.bwalletx.com",
133
+ "https://desktop.bwalletx.com",
134
+ "https://bwalletx.com",
135
+ "https://www.bwallet.space",
136
+ "https://bwallet.space",
137
+ "capacitor://localhost",
138
+ // iOS app shell
139
+ "https://localhost",
140
+ // Android app shell
141
+ "tauri://localhost",
142
+ // bWalletX Desktop (macOS)
143
+ "https://tauri.localhost",
144
+ // bWalletX Desktop (Windows)
145
+ "chrome-extension://bombmmcjghjdlbmcclnhjodgcelnehgl"
146
+ // bWalletX for Chrome (fixed id)
147
+ ];
148
+ var parentOrigin = () => {
149
+ try {
150
+ const anc = window.location.ancestorOrigins;
151
+ if (anc && anc.length > 0) return anc[0] ?? null;
152
+ if (!document.referrer) return null;
153
+ const u = new URL(document.referrer);
154
+ return `${u.protocol}//${u.host}`;
155
+ } catch {
156
+ return null;
157
+ }
158
+ };
159
+ async function probeXdm(ms = 1200, allowed = BWALLETX_PARENT_ORIGINS) {
160
+ const origin = parentOrigin();
161
+ if (!origin || !allowed.includes(origin)) return null;
162
+ try {
163
+ const w = new import_sdk.WalletClient(new import_sdk.XDM(origin));
164
+ const ok = await Promise.race([
165
+ w.isAuthenticated({}).then(
166
+ () => true,
167
+ () => false
168
+ ),
169
+ new Promise((r) => setTimeout(() => r(false), ms))
170
+ ]);
171
+ return ok ? w : null;
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
106
176
 
107
177
  // src/pair.ts
108
- var import_sdk2 = require("@bsv/sdk");
178
+ var import_sdk3 = require("@bsv/sdk");
109
179
 
110
180
  // src/in-order.ts
111
181
  function inOrder(handle, onError) {
@@ -117,7 +187,7 @@ function inOrder(handle, onError) {
117
187
  }
118
188
 
119
189
  // src/protocol.ts
120
- var import_sdk = require("@bsv/sdk");
190
+ var import_sdk2 = require("@bsv/sdk");
121
191
  var PAIR_VERSION = "1";
122
192
  var PAIR_INFO = "bwallet-pair-v1";
123
193
  var PAIR_HOST = "www.bwallet.space";
@@ -150,7 +220,7 @@ var relaySocketUrl = (relay, channel, role, expiry) => {
150
220
  return `${local ? "ws" : "wss"}://${relay}/v1/c/${channel}?${q}`;
151
221
  };
152
222
  async function deriveSession(mine, theirs, channel) {
153
- const point = mine.deriveSharedSecret(import_sdk.PublicKey.fromString(theirs));
223
+ const point = mine.deriveSharedSecret(import_sdk2.PublicKey.fromString(theirs));
154
224
  const ikm = new Uint8Array(point.encode(true));
155
225
  const base = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
156
226
  const bits = new Uint8Array(
@@ -392,7 +462,7 @@ function pairBwalletX(opts) {
392
462
  const relay = opts.relay ?? DEFAULT_RELAY;
393
463
  const origin = opts.origin ?? (typeof location !== "undefined" ? location.origin : "");
394
464
  if (!origin) return Promise.reject(new Error("pairBwalletX needs options.origin outside a browser"));
395
- const siteKey = import_sdk2.PrivateKey.fromRandom();
465
+ const siteKey = import_sdk3.PrivateKey.fromRandom();
396
466
  const channel = newChannel();
397
467
  const expiry = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S;
398
468
  return new Promise((resolve, reject) => {
@@ -456,7 +526,7 @@ async function restorePairing(opts = {}) {
456
526
  const key = opts.storageKey ?? DEFAULT_KEY;
457
527
  const p = parseStored(await Promise.resolve(storage.getItem(key)).catch(() => null));
458
528
  if (!p) return null;
459
- const { key: aes } = await deriveSession(import_sdk2.PrivateKey.fromHex(p.s), p.k, p.c);
529
+ const { key: aes } = await deriveSession(import_sdk3.PrivateKey.fromHex(p.s), p.k, p.c);
460
530
  const sealer = new Sealer(aes, "site");
461
531
  sealer.restore({ sent: p.sent, lastSeen: p.lastSeen });
462
532
  return new Link(p, sealer, storage, key, opts.socket ?? defaultSocket(), opts.callTimeoutMs ?? 12e4).wallet();
@@ -486,16 +556,28 @@ async function signInWithBwalletX(opts) {
486
556
  const { wallet, method } = await connectBwalletX(opts);
487
557
  if (method !== "pairing") await wallet.waitForAuthentication({}).catch(() => {
488
558
  });
489
- const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
490
- const c = await opts.challenge(identityKey);
491
- const { signature } = await wallet.createSignature({
492
- protocolID: c.protocolID ?? LOGIN_PROTOCOL,
493
- keyID: c.keyID,
494
- counterparty: "anyone",
495
- data: Array.from(new TextEncoder().encode(c.message))
496
- });
559
+ const { publicKey: identityKey } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
560
+ const c = await atStage("challenge", () => opts.challenge(identityKey));
561
+ const { signature } = await atStage(
562
+ "signature",
563
+ () => wallet.createSignature({
564
+ protocolID: c.protocolID ?? LOGIN_PROTOCOL,
565
+ keyID: c.keyID,
566
+ counterparty: "anyone",
567
+ data: Array.from(new TextEncoder().encode(c.message))
568
+ })
569
+ );
497
570
  return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
498
571
  }
572
+ async function atStage(stage, run) {
573
+ try {
574
+ return await run();
575
+ } catch (e) {
576
+ const err = e instanceof Error ? e : new Error(String(e));
577
+ if (!err.stage) Object.assign(err, { stage });
578
+ throw err;
579
+ }
580
+ }
499
581
 
500
582
  // src/button.ts
501
583
  var CSS = `
@@ -541,6 +623,239 @@ function renderButton(el2, opts = {}) {
541
623
  return b;
542
624
  }
543
625
 
626
+ // src/access-client.ts
627
+ var import_sdk4 = require("@bsv/sdk");
628
+
629
+ // src/access.ts
630
+ var ACCESS_VERSION = 1;
631
+ var ACCESS_PROTOCOL = [2, "bwalletx access"];
632
+ var MAX_HOLDING_PROOFS = 50;
633
+ var ACCESS_ERROR_STATUS = {
634
+ not_installed: 424,
635
+ user_rejected: 403,
636
+ wrong_account: 409,
637
+ insufficient_tokens: 403,
638
+ identity_level_too_low: 403,
639
+ kyc_required: 403,
640
+ indexer_unavailable: 503,
641
+ expired: 401,
642
+ replay: 401,
643
+ invalid_signature: 401,
644
+ bad_request: 400
645
+ };
646
+ var AccessError = class extends Error {
647
+ code;
648
+ have;
649
+ need;
650
+ constructor(code, message, extra = {}) {
651
+ super(message ?? code);
652
+ this.name = "AccessError";
653
+ this.code = code;
654
+ this.have = extra.have;
655
+ this.need = extra.need;
656
+ }
657
+ toJSON() {
658
+ const b = { error: this.code, message: this.message };
659
+ if (this.have !== void 0) b.have = this.have;
660
+ if (this.need !== void 0) b.need = this.need;
661
+ return b;
662
+ }
663
+ };
664
+ var PROTOCOL_MEDIA = {
665
+ "401": { name: "Identity", color: "#22c55e", video: "assets/protocol/401.mp4", poster: "assets/protocol/401.jpg" },
666
+ "402": { name: "Payment", color: "#3b82f6", video: "assets/protocol/402.mp4", poster: "assets/protocol/402.jpg" },
667
+ "403": { name: "Access", color: "#ef4444", video: "assets/protocol/403.mp4", poster: "assets/protocol/403.jpg" }
668
+ };
669
+ function protocolMediaUrl(kind, which, version = "latest") {
670
+ return `https://unpkg.com/@bwalletx/connect@${version}/${PROTOCOL_MEDIA[kind][which]}`;
671
+ }
672
+ function canonicalJson(v) {
673
+ if (v === null) return "null";
674
+ if (typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
675
+ if (typeof v === "number") {
676
+ if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
677
+ return JSON.stringify(v);
678
+ }
679
+ if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x)).join(",")}]`;
680
+ if (typeof v === "object") {
681
+ const o = v;
682
+ const keys = Object.keys(o).filter((k) => o[k] !== void 0).sort();
683
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
684
+ }
685
+ throw new Error(`canonicalJson: unsupported ${typeof v}`);
686
+ }
687
+ function challengeBytes(challenge) {
688
+ return Array.from(new TextEncoder().encode(challenge));
689
+ }
690
+ var TOKEN_ID = /^bsv21:[0-9a-f]{64}_\d{1,6}$/;
691
+ var RAW = /^[1-9]\d{0,38}$/;
692
+ function normaliseRequirement(r) {
693
+ if (!r || typeof r !== "object") throw new AccessError("bad_request", "require must be an object");
694
+ const req = r;
695
+ const out = {};
696
+ if (req.token !== void 0) {
697
+ const t = req.token;
698
+ if (!t || typeof t !== "object" || typeof t.id !== "string") throw new AccessError("bad_request", "require.token.id is required");
699
+ const id = t.id.trim().toLowerCase();
700
+ if (!TOKEN_ID.test(id)) throw new AccessError("bad_request", "require.token.id must be 'bsv21:<txid>_<vout>'");
701
+ const hasMin = t.min !== void 0;
702
+ const hasBps = t.minBps !== void 0;
703
+ if (hasMin === hasBps) throw new AccessError("bad_request", "require.token needs exactly one of min or minBps");
704
+ out.token = { id };
705
+ if (hasMin) {
706
+ 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)");
707
+ out.token.min = t.min;
708
+ } else {
709
+ if (!Number.isInteger(t.minBps) || t.minBps < 1 || t.minBps > 1e4) {
710
+ throw new AccessError("bad_request", "require.token.minBps must be an integer 1..10000");
711
+ }
712
+ out.token.minBps = t.minBps;
713
+ }
714
+ }
715
+ if (req.identityLevel !== void 0) {
716
+ if (!Number.isInteger(req.identityLevel) || req.identityLevel < 1 || req.identityLevel > 10) {
717
+ throw new AccessError("bad_request", "require.identityLevel must be an integer 1..10");
718
+ }
719
+ out.identityLevel = req.identityLevel;
720
+ }
721
+ if (req.kyc !== void 0) {
722
+ if (typeof req.kyc !== "boolean") throw new AccessError("bad_request", "require.kyc must be a boolean");
723
+ if (req.kyc) out.kyc = true;
724
+ }
725
+ if (!out.token && !out.identityLevel && !out.kyc) throw new AccessError("bad_request", "require is empty");
726
+ return out;
727
+ }
728
+ function parseAccessChallenge(s) {
729
+ if (typeof s !== "string" || s.length > 4096) throw new AccessError("bad_request", "challenge must be a string");
730
+ let c;
731
+ try {
732
+ c = JSON.parse(s);
733
+ } catch {
734
+ throw new AccessError("bad_request", "challenge is not JSON");
735
+ }
736
+ if (!c || typeof c !== "object" || c.v !== 1) throw new AccessError("bad_request", "unsupported challenge version");
737
+ if (c.kind !== "401" && c.kind !== "402" && c.kind !== "403") throw new AccessError("bad_request", "bad kind");
738
+ if (typeof c.origin !== "string" || typeof c.nonce !== "string" || !/^[A-Za-z0-9_-]{16,64}$/.test(c.nonce)) {
739
+ throw new AccessError("bad_request", "bad origin or nonce");
740
+ }
741
+ if (!Number.isInteger(c.iat) || !Number.isInteger(c.exp) || c.exp <= c.iat) throw new AccessError("bad_request", "bad iat/exp");
742
+ 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");
743
+ c.require = normaliseRequirement(c.require);
744
+ if (canonicalJson(c) !== s) throw new AccessError("bad_request", "challenge is not canonical");
745
+ return c;
746
+ }
747
+ function accessErrorMessage(code, e = {}) {
748
+ switch (code) {
749
+ case "not_installed":
750
+ return "No bWalletX (or other BRC-100 wallet) was found.";
751
+ case "user_rejected":
752
+ return "The request was declined in the wallet.";
753
+ case "wrong_account":
754
+ return "The wallet is signed in to a different account than this site expects.";
755
+ case "insufficient_tokens":
756
+ return `Not enough tokens: holding ${e.have ?? 0}, need ${e.need ?? "?"}.`;
757
+ case "identity_level_too_low":
758
+ return `Identity level ${e.have ?? 0} is below the required level ${e.need ?? "?"}.`;
759
+ case "kyc_required":
760
+ return "A verified identity (KYC) is required.";
761
+ case "indexer_unavailable":
762
+ return "Holdings could not be checked right now. Try again shortly.";
763
+ case "expired":
764
+ return "The request expired. Try again.";
765
+ case "replay":
766
+ return "That request was already used. Try again.";
767
+ case "invalid_signature":
768
+ return "The wallet signature did not verify.";
769
+ case "bad_request":
770
+ return "The request was malformed.";
771
+ }
772
+ }
773
+
774
+ // src/access-client.ts
775
+ var ONESAT = [0, "onesat"];
776
+ var LEGACY_ONESAT = [0, "p 1sat"];
777
+ function toAccessError(e) {
778
+ if (e instanceof AccessError) return e;
779
+ const err = e;
780
+ const msg = String(err?.message ?? e ?? "");
781
+ if (err?.code === "NOT_FOUND") return new AccessError("not_installed", msg);
782
+ if (/reject|denied|declin|cancel|user/i.test(msg) || err?.code === "ERR_USER_REJECTED" || err?.status === 4001) {
783
+ return new AccessError("user_rejected", msg);
784
+ }
785
+ return new AccessError("bad_request", msg || "access failed");
786
+ }
787
+ function throwIfAccessError(body) {
788
+ const b = body;
789
+ if (b && typeof b.error === "string") throw new AccessError(b.error, b.message, { have: b.have, need: b.need });
790
+ }
791
+ function hash160FromP2pkh(script) {
792
+ const m = /^76a914([0-9a-f]{40})88ac$/i.exec(script.slice(-50));
793
+ return m ? m[1].toLowerCase() : null;
794
+ }
795
+ async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
796
+ const bare = tokenId.replace(/^bsv21:/, "");
797
+ const list = await wallet.listOutputs({ basket, include: "locking scripts", includeCustomInstructions: true, limit: 1e4 });
798
+ const byKey = /* @__PURE__ */ new Map();
799
+ const data = challengeBytes(challenge);
800
+ for (const out of list.outputs) {
801
+ if (byKey.size >= MAX_HOLDING_PROOFS) break;
802
+ let ci = {};
803
+ try {
804
+ ci = JSON.parse(out.customInstructions ?? "{}");
805
+ } catch {
806
+ continue;
807
+ }
808
+ if (ci.id !== bare || !ci.keyID) continue;
809
+ const want = out.lockingScript ? hash160FromP2pkh(out.lockingScript) : null;
810
+ const counterparty = ci.counterparty ?? "self";
811
+ for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
812
+ const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
813
+ const h = import_sdk4.Utils.toHex(import_sdk4.Hash.hash160(import_sdk4.PublicKey.fromString(publicKey).encode(true)));
814
+ if (want && h !== want) continue;
815
+ const existing = byKey.get(publicKey);
816
+ if (existing) {
817
+ existing.outpoints.push(out.outpoint.replace(".", "_"));
818
+ break;
819
+ }
820
+ const { signature } = await wallet.createSignature({ protocolID, keyID: ci.keyID, counterparty, data });
821
+ byKey.set(publicKey, { pubkey: publicKey, signature: Array.from(signature), outpoints: [out.outpoint.replace(".", "_")] });
822
+ break;
823
+ }
824
+ }
825
+ return [...byKey.values()];
826
+ }
827
+ async function requestAccess(o) {
828
+ let wallet;
829
+ try {
830
+ ({ wallet } = await connectBwalletX({ wallet: o.wallet, pairing: o.pairing, discoverMs: o.discoverMs }));
831
+ } catch (e) {
832
+ throw e?.code === "NOT_FOUND" ? new AccessError("not_installed") : toAccessError(e);
833
+ }
834
+ try {
835
+ const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
836
+ const { challenge } = await o.challenge(identityKey);
837
+ const c = parseAccessChallenge(challenge);
838
+ if (c.sub && c.sub !== identityKey.toLowerCase()) throw new AccessError("wrong_account");
839
+ if (Date.now() / 1e3 >= c.exp) throw new AccessError("expired");
840
+ const { signature } = await wallet.createSignature({
841
+ protocolID: ACCESS_PROTOCOL,
842
+ keyID: c.nonce,
843
+ counterparty: "anyone",
844
+ data: challengeBytes(challenge)
845
+ });
846
+ const response = { v: 1, challenge, identityKey, signature: Array.from(signature) };
847
+ if (c.require.token) response.holdings = await proveHoldings(wallet, challenge, c.require.token.id, o.tokenBasket);
848
+ let verified;
849
+ if (o.verify) {
850
+ verified = await o.verify(response);
851
+ throwIfAccessError(verified);
852
+ }
853
+ return { response, verified, wallet };
854
+ } catch (e) {
855
+ throw toAccessError(e);
856
+ }
857
+ }
858
+
544
859
  // src/element.ts
545
860
  var SIGNIN_TAG = "bwalletx-signin";
546
861
  var GET_URL = "https://bwalletx.com/get";
@@ -636,6 +951,10 @@ async function postJson(url, body) {
636
951
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
637
952
  return data;
638
953
  }
954
+ async function postAccess(url, body) {
955
+ const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
956
+ return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
957
+ }
639
958
  var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
640
959
  function createClass() {
641
960
  return class BwalletxSigninElement extends HTMLElement {
@@ -718,6 +1037,26 @@ function createClass() {
718
1037
  if (!found) {
719
1038
  this.#notFound = true;
720
1039
  this.#emit("bwalletx-not-found");
1040
+ if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
1041
+ return;
1042
+ }
1043
+ const requireToken = this.#attr("require-token");
1044
+ if (requireToken) {
1045
+ const verifyUrl2 = this.#attr("verify-url");
1046
+ const min = this.#attr("min");
1047
+ const minBps = this.#attr("min-bps");
1048
+ const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
1049
+ try {
1050
+ const a = await requestAccess({
1051
+ wallet: found.wallet,
1052
+ challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
1053
+ verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
1054
+ });
1055
+ this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
1056
+ } catch (e) {
1057
+ const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
1058
+ this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
1059
+ }
721
1060
  return;
722
1061
  }
723
1062
  const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
@@ -742,7 +1081,7 @@ function defineBwalletxSignin(tag = SIGNIN_TAG) {
742
1081
  }
743
1082
 
744
1083
  // src/pay.ts
745
- var import_sdk3 = require("@bsv/sdk");
1084
+ var import_sdk5 = require("@bsv/sdk");
746
1085
  var BRC29_PROTOCOL = [2, "3241645161d8"];
747
1086
  var PaymentError = class extends Error {
748
1087
  constructor(code, message, maybeSent = false, cause) {
@@ -762,13 +1101,13 @@ var isPaymail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
762
1101
  function randomB64(n) {
763
1102
  const b = new Uint8Array(n);
764
1103
  globalThis.crypto.getRandomValues(b);
765
- return import_sdk3.Utils.toBase64(Array.from(b));
1104
+ return import_sdk5.Utils.toBase64(Array.from(b));
766
1105
  }
767
1106
  function opReturn(text) {
768
- const d = import_sdk3.Utils.toArray(text, "utf8");
1107
+ const d = import_sdk5.Utils.toArray(text, "utf8");
769
1108
  const len = d.length;
770
1109
  const push = len < 76 ? [len] : len <= 255 ? [76, len] : [77, len & 255, len >> 8];
771
- return import_sdk3.Utils.toHex([0, 106, ...push, ...d]);
1110
+ return import_sdk5.Utils.toHex([0, 106, ...push, ...d]);
772
1111
  }
773
1112
  var clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
774
1113
  var describe = (s) => clip(s.length < 5 ? `${s} payment`.trim() : s, 50);
@@ -864,7 +1203,7 @@ async function pay(source, req, o = {}) {
864
1203
  } catch (e) {
865
1204
  throw new PaymentError("WALLET_ERROR", `The wallet could not derive the payment key: ${e.message}`, false, e);
866
1205
  }
867
- outputs = [{ script: new import_sdk3.P2PKH().lock(import_sdk3.PublicKey.fromString(pk).toHash()).toHex(), satoshis: sats, description: "payment" }];
1206
+ outputs = [{ script: new import_sdk5.P2PKH().lock(import_sdk5.PublicKey.fromString(pk).toHash()).toHex(), satoshis: sats, description: "payment" }];
868
1207
  remittance = { senderIdentityKey: me, derivationPrefix, derivationSuffix, outputIndex: 0 };
869
1208
  } else if (isPaymail(to)) {
870
1209
  const r2 = await (o.resolvePaymail ?? ((p, s) => resolvePaymailP2P(p, s, o.fetch ?? fetch)))(to, sats);
@@ -872,7 +1211,7 @@ async function pay(source, req, o = {}) {
872
1211
  submit = r2.submit;
873
1212
  } else {
874
1213
  try {
875
- outputs = [{ script: new import_sdk3.P2PKH().lock(to).toHex(), satoshis: sats, description: "payment" }];
1214
+ outputs = [{ script: new import_sdk5.P2PKH().lock(to).toHex(), satoshis: sats, description: "payment" }];
876
1215
  } catch {
877
1216
  throw new PaymentError("BAD_REQUEST", `Not a paymail, address or identity key: ${to}`);
878
1217
  }
@@ -882,7 +1221,7 @@ async function pay(source, req, o = {}) {
882
1221
  if (!r.txid) throw new PaymentError("WALLET_ERROR", "The wallet did not return a txid", true);
883
1222
  const tx = r.tx ? Array.from(r.tx) : void 0;
884
1223
  if (submit && tx) {
885
- await submit(import_sdk3.Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {
1224
+ await submit(import_sdk5.Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {
886
1225
  });
887
1226
  }
888
1227
  return { txid: r.txid, tx, sats, outputs, remittance };
@@ -903,7 +1242,7 @@ async function buyToken(source, req) {
903
1242
  }
904
1243
 
905
1244
  // src/wallets.ts
906
- var import_sdk4 = require("@bsv/sdk");
1245
+ var import_sdk6 = require("@bsv/sdk");
907
1246
  async function brc100Connected(wallet, o) {
908
1247
  await wallet.waitForAuthentication({}).catch(() => {
909
1248
  });
@@ -962,7 +1301,7 @@ function handcashAdapter(o) {
962
1301
  var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
963
1302
  async function probe(substrate, ms) {
964
1303
  try {
965
- const w = new import_sdk4.WalletClient(substrate);
1304
+ const w = new import_sdk6.WalletClient(substrate);
966
1305
  const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
967
1306
  return ok ? w : null;
968
1307
  } catch {
@@ -1015,6 +1354,9 @@ var CSS2 = `
1015
1354
  .bwx-row{display:flex;gap:8px;margin-top:14px}
1016
1355
  .bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
1017
1356
  .bwx-btn:hover{border-color:#e8c766}
1357
+ a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}
1358
+ .bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}
1359
+ .bwx-stack{display:flex;flex-direction:column;gap:8px}
1018
1360
  .bwx-list{display:flex;flex-direction:column;gap:8px}
1019
1361
  .bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
1020
1362
  .bwx-opt img{width:24px;height:24px;border-radius:6px}
@@ -1045,6 +1387,18 @@ function qrSvg(text) {
1045
1387
  q.make();
1046
1388
  return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
1047
1389
  }
1390
+ function isTouchDevice(env = globalThis) {
1391
+ const nav = env.navigator;
1392
+ if (!nav) return false;
1393
+ const ua = nav.userAgent ?? "";
1394
+ if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;
1395
+ if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;
1396
+ try {
1397
+ return !!env.matchMedia?.("(pointer: coarse)").matches && !env.matchMedia?.("(hover: hover)").matches;
1398
+ } catch {
1399
+ return false;
1400
+ }
1401
+ }
1048
1402
  function pairingModal(o = {}) {
1049
1403
  const ac = new AbortController();
1050
1404
  let ui = null;
@@ -1061,20 +1415,37 @@ function pairingModal(o = {}) {
1061
1415
  if (typeof document === "undefined") return;
1062
1416
  close();
1063
1417
  ui = mount();
1418
+ const app = o.mode === "app" || (o.mode ?? "auto") === "auto" && isTouchDevice();
1064
1419
  const h = el("h2", "", o.title ?? "Connect bWalletX");
1420
+ const cancel = el("button", "bwx-btn", "Cancel");
1421
+ cancel.onclick = () => {
1422
+ o.onCancel?.();
1423
+ ac.abort();
1424
+ };
1425
+ codeEl = el("div", "bwx-code", "");
1426
+ codeEl.setAttribute("aria-live", "polite");
1427
+ if (app) {
1428
+ const p2 = el("p", "", "Open bWalletX, approve, then come back to this page.");
1429
+ const stack = el("div", "bwx-stack");
1430
+ const open = el("a", "bwx-btn bwx-primary", "Open bWalletX");
1431
+ open.href = link;
1432
+ open.rel = "noopener";
1433
+ const web = el("button", "bwx-btn", "Use web.bwalletx.com instead");
1434
+ web.onclick = () => {
1435
+ void navigator.clipboard?.writeText(link).catch(() => void 0);
1436
+ window.open(WEB_WALLET_URL, "_blank", "noopener");
1437
+ p2.textContent = "Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.";
1438
+ };
1439
+ stack.append(open, web, cancel);
1440
+ ui.box.append(h, p2, codeEl, stack);
1441
+ return;
1442
+ }
1065
1443
  const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
1066
1444
  const qr = el("div", "bwx-qr");
1067
1445
  qr.innerHTML = qrSvg(link);
1068
- codeEl = el("div", "bwx-code", "");
1069
- codeEl.setAttribute("aria-live", "polite");
1070
1446
  const row = el("div", "bwx-row");
1071
1447
  const copy = el("button", "bwx-btn", "Copy link");
1072
1448
  copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => copy.textContent = "Copied");
1073
- const cancel = el("button", "bwx-btn", "Cancel");
1074
- cancel.onclick = () => {
1075
- o.onCancel?.();
1076
- ac.abort();
1077
- };
1078
1449
  row.append(copy, cancel);
1079
1450
  ui.box.append(h, p, qr, codeEl, row);
1080
1451
  },
@@ -1118,25 +1489,53 @@ function pickWallet(options, o = {}) {
1118
1489
  ui.box.lastChild.append(cancel);
1119
1490
  });
1120
1491
  }
1492
+
1493
+ // src/theme.ts
1494
+ var WALLET_THEME_META = "bwalletx-theme";
1495
+ var ID = /^[a-z0-9-]{1,32}$/;
1496
+ function declareWalletTheme(theme) {
1497
+ if (typeof document === "undefined") return;
1498
+ const id = (theme ?? "").trim().toLowerCase();
1499
+ let meta = document.head.querySelector(`meta[name="${WALLET_THEME_META}"]`);
1500
+ if (!id || !ID.test(id)) {
1501
+ meta?.remove();
1502
+ return;
1503
+ }
1504
+ if (!meta) {
1505
+ meta = document.createElement("meta");
1506
+ meta.name = WALLET_THEME_META;
1507
+ document.head.appendChild(meta);
1508
+ }
1509
+ if (meta.content !== id) meta.content = id;
1510
+ }
1121
1511
  // Annotate the CommonJS export names for ESM import in node:
1122
1512
  0 && (module.exports = {
1513
+ ACCESS_ERROR_STATUS,
1514
+ ACCESS_PROTOCOL,
1515
+ ACCESS_VERSION,
1516
+ AccessError,
1123
1517
  BRC29_PROTOCOL,
1124
1518
  BWALLETX_RDNS,
1125
1519
  GET_URL,
1126
1520
  LOGIN_PROTOCOL,
1127
1521
  LOGO_URL,
1522
+ PROTOCOL_MEDIA,
1128
1523
  PaymentError,
1129
1524
  RDNS_EXTENSION,
1130
1525
  RDNS_MOBILE,
1131
1526
  SIGNIN_CSS,
1132
1527
  SIGNIN_TAG,
1528
+ WALLET_THEME_META,
1133
1529
  WEB_WALLET_URL,
1530
+ accessErrorMessage,
1134
1531
  amountInSats,
1135
1532
  brc100Option,
1136
1533
  buildAuthorizeUrl,
1137
1534
  buyToken,
1535
+ canonicalJson,
1138
1536
  clearPairing,
1139
1537
  connectBwalletX,
1538
+ declareWalletTheme,
1140
1539
  defineBwalletxSignin,
1141
1540
  discoverBwalletX,
1142
1541
  discoverWallets,
@@ -1145,18 +1544,24 @@ function pickWallet(options, o = {}) {
1145
1544
  inOrder,
1146
1545
  isIdentityKeyHex,
1147
1546
  isPaymail,
1547
+ isTouchDevice,
1148
1548
  markSvg,
1149
1549
  pairBwalletX,
1150
1550
  pairingModal,
1151
1551
  pairingOption,
1152
1552
  pay,
1153
1553
  pickWallet,
1554
+ protocolMediaUrl,
1555
+ proveHoldings,
1154
1556
  qrSvg,
1155
1557
  renderButton,
1558
+ requestAccess,
1156
1559
  resolvePaymailP2P,
1157
1560
  restorePairing,
1158
1561
  signInWithBwalletX,
1159
1562
  signinText,
1160
- tip
1563
+ throwIfAccessError,
1564
+ tip,
1565
+ toAccessError
1161
1566
  });
1162
1567
  //# sourceMappingURL=index.cjs.map