@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.
- package/CHANGELOG.md +20 -0
- package/README.md +6 -2
- package/assets/protocol/401.jpg +0 -0
- package/assets/protocol/401.mp4 +0 -0
- package/assets/protocol/402.jpg +0 -0
- package/assets/protocol/402.mp4 +0 -0
- package/assets/protocol/403.jpg +0 -0
- package/assets/protocol/403.mp4 +0 -0
- package/dist/access-Dp0eKIOc.d.cts +118 -0
- package/dist/access-Dp0eKIOc.d.ts +118 -0
- package/dist/bwalletx-connect.global.js +20 -17
- package/dist/bwalletx-connect.global.js.map +1 -1
- package/dist/{chunk-NNMUSWPG.js → chunk-IUDD7YTQ.js} +200 -10
- package/dist/chunk-IUDD7YTQ.js.map +1 -0
- package/dist/chunk-MWD4BCO5.js +182 -0
- package/dist/chunk-MWD4BCO5.js.map +1 -0
- package/dist/{chunk-FUQ46RJV.js → chunk-PI42H3XY.js} +65 -10
- package/dist/chunk-PI42H3XY.js.map +1 -0
- package/dist/{chunk-7NAKOJGZ.js → chunk-QRXFL66W.js} +220 -10
- package/dist/chunk-QRXFL66W.js.map +1 -0
- package/dist/element.cjs +290 -14
- package/dist/element.cjs.map +1 -1
- package/dist/element.js +2 -1
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +437 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.js +36 -4
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +377 -9
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +52 -2
- package/dist/next.d.ts +52 -2
- package/dist/next.js +92 -1
- package/dist/next.js.map +1 -1
- package/dist/react.cjs +561 -226
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +5 -0
- package/dist/react.d.ts +5 -0
- package/dist/react.js +12 -3
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +406 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +111 -1
- package/dist/server.d.ts +111 -1
- package/dist/server.js +47 -1
- package/package.json +12 -11
- package/dist/chunk-7NAKOJGZ.js.map +0 -1
- package/dist/chunk-FUQ46RJV.js.map +0 -1
- package/dist/chunk-NNMUSWPG.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/access.ts"],"sourcesContent":["/**\n * $401 / $402 / $403 access protocol, version 1. Shared by sites (client + server) and wallets.\n * The full spec is docs/ACCESS-PROTOCOL.md. Pure: no I/O, @bsv/sdk types only.\n *\n * 401 identity \"prove who you are (at identity level N, or KYC'd)\"\n * 402 payment \"pay for this\" (the challenge format is reserved; payment itself uses pay())\n * 403 access \"prove you hold N of token X (or N basis points of its supply)\"\n */\nimport type { WalletProtocol } from '@bsv/sdk';\n\nexport const ACCESS_VERSION = 1 as const;\n/** Security level 2, a protocol name used for nothing else. Wallets recognise it and show the kind's video. */\nexport const ACCESS_PROTOCOL: WalletProtocol = [2, 'bwalletx access'];\n/** How long a challenge is valid. */\nexport const ACCESS_CHALLENGE_LIFETIME_S = 120;\n/** At most this many holding proofs are read from one response. */\nexport const MAX_HOLDING_PROOFS = 50;\n\nexport type AccessKind = '401' | '402' | '403';\n\nexport interface TokenRequirement {\n /** 'bsv21:<txid>_<vout>' */\n id: string;\n /** Minimum raw (unscaled, integer string) amount. */\n min?: string;\n /** Minimum in basis points of total supply (1 = 0.01%, 10000 = 100%). */\n minBps?: number;\n}\n\nexport interface AccessRequirement {\n token?: TokenRequirement;\n /** $401 identity level (bit-sign strands): 1 Basic, 2 Verified, 3 Strong, 4 Sovereign (Veriff). */\n identityLevel?: number;\n /** A live KYC credential is required. */\n kyc?: boolean;\n}\n\n/** What the wallet signs, as canonical JSON. Times are seconds since the epoch. */\nexport interface AccessChallenge {\n v: 1;\n kind: AccessKind;\n origin: string;\n nonce: string;\n iat: number;\n exp: number;\n /** The identity key the site expects. A different wallet account is `wrong_account`. */\n sub?: string;\n require: AccessRequirement;\n}\n\n/** A key that locks some of the required token, proving control by signing the same challenge bytes. */\nexport interface HoldingProof {\n /** Compressed public key hex of the key locking the outputs. */\n pubkey: string;\n /** DER signature by that key over SHA-256(challenge bytes), as BRC-100 createSignature returns. */\n signature: number[];\n /** Optional hint: outpoints ('txid_vout') this key holds. Servers must not trust it. */\n outpoints?: string[];\n}\n\n/** What the wallet (via requestAccess) posts back to the site. */\nexport interface AccessResponse {\n v: 1;\n /** The challenge string exactly as issued. */\n challenge: string;\n identityKey: string;\n /** DER signature: createSignature({ protocolID: ACCESS_PROTOCOL, keyID: nonce, counterparty: 'anyone', data: challenge bytes }). */\n signature: number[];\n holdings?: HoldingProof[];\n}\n\nexport type AccessErrorCode =\n | 'not_installed'\n | 'user_rejected'\n | 'wrong_account'\n | 'insufficient_tokens'\n | 'identity_level_too_low'\n | 'kyc_required'\n | 'indexer_unavailable'\n | 'expired'\n | 'replay'\n | 'invalid_signature'\n | 'bad_request';\n\n/** The HTTP status a site should answer each code with. */\nexport const ACCESS_ERROR_STATUS: Record<AccessErrorCode, number> = {\n not_installed: 424,\n user_rejected: 403,\n wrong_account: 409,\n insufficient_tokens: 403,\n identity_level_too_low: 403,\n kyc_required: 403,\n indexer_unavailable: 503,\n expired: 401,\n replay: 401,\n invalid_signature: 401,\n bad_request: 400,\n};\n\nexport interface AccessErrorBody {\n error: AccessErrorCode;\n message: string;\n /** insufficient_tokens: raw amounts; identity_level_too_low: levels. */\n have?: string | number;\n need?: string | number;\n}\n\nexport class AccessError extends Error {\n readonly code: AccessErrorCode;\n readonly have?: string | number;\n readonly need?: string | number;\n constructor(code: AccessErrorCode, message?: string, extra: { have?: string | number; need?: string | number } = {}) {\n super(message ?? code);\n this.name = 'AccessError';\n this.code = code;\n this.have = extra.have;\n this.need = extra.need;\n }\n toJSON(): AccessErrorBody {\n const b: AccessErrorBody = { error: this.code, message: this.message };\n if (this.have !== undefined) b.have = this.have;\n if (this.need !== undefined) b.need = this.need;\n return b;\n }\n}\n\n/** One video + poster + colour per kind, so wallets and sites show the same thing. Paths are inside the package. */\nexport const PROTOCOL_MEDIA: Record<AccessKind, { name: string; color: string; video: string; poster: string }> = {\n '401': { name: 'Identity', color: '#22c55e', video: 'assets/protocol/401.mp4', poster: 'assets/protocol/401.jpg' },\n '402': { name: 'Payment', color: '#3b82f6', video: 'assets/protocol/402.mp4', poster: 'assets/protocol/402.jpg' },\n '403': { name: 'Access', color: '#ef4444', video: 'assets/protocol/403.mp4', poster: 'assets/protocol/403.jpg' },\n};\n\n/** Media URLs on a CDN (unpkg) for a given package version. */\nexport function protocolMediaUrl(kind: AccessKind, which: 'video' | 'poster', version = 'latest'): string {\n return `https://unpkg.com/@bwalletx/connect@${version}/${PROTOCOL_MEDIA[kind][which]}`;\n}\n\n/**\n * Canonical JSON: object keys sorted by code unit, no whitespace, undefined members dropped.\n * Only strings, finite numbers, booleans, null, arrays and plain objects are allowed.\n */\nexport function canonicalJson(v: unknown): string {\n if (v === null) return 'null';\n if (typeof v === 'string' || typeof v === 'boolean') return JSON.stringify(v);\n if (typeof v === 'number') {\n if (!Number.isFinite(v)) throw new Error('canonicalJson: non-finite number');\n return JSON.stringify(v);\n }\n if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x)).join(',')}]`;\n if (typeof v === 'object') {\n const o = v as Record<string, unknown>;\n const keys = Object.keys(o).filter((k) => o[k] !== undefined).sort();\n return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(',')}}`;\n }\n throw new Error(`canonicalJson: unsupported ${typeof v}`);\n}\n\n/** The bytes every signature in the protocol covers: UTF-8 of the challenge string. */\nexport function challengeBytes(challenge: string): number[] {\n return Array.from(new TextEncoder().encode(challenge));\n}\n\nconst TOKEN_ID = /^bsv21:[0-9a-f]{64}_\\d{1,6}$/;\nconst RAW = /^[1-9]\\d{0,38}$/;\n\n/** Throws AccessError('bad_request') if the requirement is malformed. Returns a normalised copy. */\nexport function normaliseRequirement(r: unknown): AccessRequirement {\n if (!r || typeof r !== 'object') throw new AccessError('bad_request', 'require must be an object');\n const req = r as Record<string, unknown>;\n const out: AccessRequirement = {};\n if (req.token !== undefined) {\n const t = req.token as Record<string, unknown>;\n if (!t || typeof t !== 'object' || typeof t.id !== 'string') throw new AccessError('bad_request', 'require.token.id is required');\n const id = t.id.trim().toLowerCase();\n if (!TOKEN_ID.test(id)) throw new AccessError('bad_request', \"require.token.id must be 'bsv21:<txid>_<vout>'\");\n const hasMin = t.min !== undefined;\n const hasBps = t.minBps !== undefined;\n if (hasMin === hasBps) throw new AccessError('bad_request', 'require.token needs exactly one of min or minBps');\n out.token = { id };\n if (hasMin) {\n 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)');\n out.token.min = t.min;\n } else {\n if (!Number.isInteger(t.minBps) || (t.minBps as number) < 1 || (t.minBps as number) > 10000) {\n throw new AccessError('bad_request', 'require.token.minBps must be an integer 1..10000');\n }\n out.token.minBps = t.minBps as number;\n }\n }\n if (req.identityLevel !== undefined) {\n if (!Number.isInteger(req.identityLevel) || (req.identityLevel as number) < 1 || (req.identityLevel as number) > 10) {\n throw new AccessError('bad_request', 'require.identityLevel must be an integer 1..10');\n }\n out.identityLevel = req.identityLevel as number;\n }\n if (req.kyc !== undefined) {\n if (typeof req.kyc !== 'boolean') throw new AccessError('bad_request', 'require.kyc must be a boolean');\n if (req.kyc) out.kyc = true;\n }\n if (!out.token && !out.identityLevel && !out.kyc) throw new AccessError('bad_request', 'require is empty');\n return out;\n}\n\n/** The kind a requirement implies: any token → 403; otherwise 401. */\nexport function kindFor(r: AccessRequirement): AccessKind {\n return r.token ? '403' : '401';\n}\n\n/** Parse and validate a challenge string. Throws AccessError('bad_request'). Does not check expiry. */\nexport function parseAccessChallenge(s: unknown): AccessChallenge {\n if (typeof s !== 'string' || s.length > 4096) throw new AccessError('bad_request', 'challenge must be a string');\n let c: AccessChallenge;\n try {\n c = JSON.parse(s) as AccessChallenge;\n } catch {\n throw new AccessError('bad_request', 'challenge is not JSON');\n }\n if (!c || typeof c !== 'object' || c.v !== 1) throw new AccessError('bad_request', 'unsupported challenge version');\n if (c.kind !== '401' && c.kind !== '402' && c.kind !== '403') throw new AccessError('bad_request', 'bad kind');\n if (typeof c.origin !== 'string' || typeof c.nonce !== 'string' || !/^[A-Za-z0-9_-]{16,64}$/.test(c.nonce)) {\n throw new AccessError('bad_request', 'bad origin or nonce');\n }\n if (!Number.isInteger(c.iat) || !Number.isInteger(c.exp) || c.exp <= c.iat) throw new AccessError('bad_request', 'bad iat/exp');\n if (c.sub !== undefined && (typeof c.sub !== 'string' || !/^0[23][0-9a-f]{64}$/.test(c.sub))) throw new AccessError('bad_request', 'bad sub');\n c.require = normaliseRequirement(c.require);\n if (canonicalJson(c) !== s) throw new AccessError('bad_request', 'challenge is not canonical');\n return c;\n}\n\n/** Raw amount needed for a token requirement, given total supply (needed only for minBps). Rounds up. */\nexport function tokenNeedRaw(t: TokenRequirement, supplyRaw?: string): string {\n if (t.min) return t.min;\n if (supplyRaw === undefined || !/^\\d+$/.test(supplyRaw)) throw new AccessError('indexer_unavailable', 'token supply unknown');\n const n = BigInt(supplyRaw) * BigInt(t.minBps!);\n const need = (n + 9999n) / 10000n;\n return (need < 1n ? 1n : need).toString();\n}\n\n/** Compare unscaled integer strings. Fails closed on malformed input. */\nexport function atLeastRaw(have: string, need: string): boolean {\n try {\n return BigInt(have) >= BigInt(need);\n } catch {\n return false;\n }\n}\n\n/** A user-readable line for an error code. */\nexport function accessErrorMessage(code: AccessErrorCode, e: { have?: string | number; need?: string | number } = {}): string {\n switch (code) {\n case 'not_installed': return 'No bWalletX (or other BRC-100 wallet) was found.';\n case 'user_rejected': return 'The request was declined in the wallet.';\n case 'wrong_account': return 'The wallet is signed in to a different account than this site expects.';\n case 'insufficient_tokens': return `Not enough tokens: holding ${e.have ?? 0}, need ${e.need ?? '?'}.`;\n case 'identity_level_too_low': return `Identity level ${e.have ?? 0} is below the required level ${e.need ?? '?'}.`;\n case 'kyc_required': return 'A verified identity (KYC) is required.';\n case 'indexer_unavailable': return 'Holdings could not be checked right now. Try again shortly.';\n case 'expired': return 'The request expired. Try again.';\n case 'replay': return 'That request was already used. Try again.';\n case 'invalid_signature': return 'The wallet signature did not verify.';\n case 'bad_request': return 'The request was malformed.';\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BWALLETX_RDNS,
|
|
3
3
|
RDNS_EXTENSION,
|
|
4
|
+
WEB_WALLET_URL,
|
|
4
5
|
pairBwalletX,
|
|
5
6
|
restorePairing
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IUDD7YTQ.js";
|
|
7
8
|
|
|
8
9
|
// src/pay.ts
|
|
9
10
|
import { P2PKH, PublicKey, Transaction, Utils } from "@bsv/sdk";
|
|
@@ -279,6 +280,9 @@ var CSS = `
|
|
|
279
280
|
.bwx-row{display:flex;gap:8px;margin-top:14px}
|
|
280
281
|
.bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
|
|
281
282
|
.bwx-btn:hover{border-color:#e8c766}
|
|
283
|
+
a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}
|
|
284
|
+
.bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}
|
|
285
|
+
.bwx-stack{display:flex;flex-direction:column;gap:8px}
|
|
282
286
|
.bwx-list{display:flex;flex-direction:column;gap:8px}
|
|
283
287
|
.bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
|
|
284
288
|
.bwx-opt img{width:24px;height:24px;border-radius:6px}
|
|
@@ -309,6 +313,18 @@ function qrSvg(text) {
|
|
|
309
313
|
q.make();
|
|
310
314
|
return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
|
|
311
315
|
}
|
|
316
|
+
function isTouchDevice(env = globalThis) {
|
|
317
|
+
const nav = env.navigator;
|
|
318
|
+
if (!nav) return false;
|
|
319
|
+
const ua = nav.userAgent ?? "";
|
|
320
|
+
if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;
|
|
321
|
+
if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;
|
|
322
|
+
try {
|
|
323
|
+
return !!env.matchMedia?.("(pointer: coarse)").matches && !env.matchMedia?.("(hover: hover)").matches;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
312
328
|
function pairingModal(o = {}) {
|
|
313
329
|
const ac = new AbortController();
|
|
314
330
|
let ui = null;
|
|
@@ -325,20 +341,37 @@ function pairingModal(o = {}) {
|
|
|
325
341
|
if (typeof document === "undefined") return;
|
|
326
342
|
close();
|
|
327
343
|
ui = mount();
|
|
344
|
+
const app = o.mode === "app" || (o.mode ?? "auto") === "auto" && isTouchDevice();
|
|
328
345
|
const h = el("h2", "", o.title ?? "Connect bWalletX");
|
|
346
|
+
const cancel = el("button", "bwx-btn", "Cancel");
|
|
347
|
+
cancel.onclick = () => {
|
|
348
|
+
o.onCancel?.();
|
|
349
|
+
ac.abort();
|
|
350
|
+
};
|
|
351
|
+
codeEl = el("div", "bwx-code", "");
|
|
352
|
+
codeEl.setAttribute("aria-live", "polite");
|
|
353
|
+
if (app) {
|
|
354
|
+
const p2 = el("p", "", "Open bWalletX, approve, then come back to this page.");
|
|
355
|
+
const stack = el("div", "bwx-stack");
|
|
356
|
+
const open = el("a", "bwx-btn bwx-primary", "Open bWalletX");
|
|
357
|
+
open.href = link;
|
|
358
|
+
open.rel = "noopener";
|
|
359
|
+
const web = el("button", "bwx-btn", "Use web.bwalletx.com instead");
|
|
360
|
+
web.onclick = () => {
|
|
361
|
+
void navigator.clipboard?.writeText(link).catch(() => void 0);
|
|
362
|
+
window.open(WEB_WALLET_URL, "_blank", "noopener");
|
|
363
|
+
p2.textContent = "Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.";
|
|
364
|
+
};
|
|
365
|
+
stack.append(open, web, cancel);
|
|
366
|
+
ui.box.append(h, p2, codeEl, stack);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
329
369
|
const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
|
|
330
370
|
const qr = el("div", "bwx-qr");
|
|
331
371
|
qr.innerHTML = qrSvg(link);
|
|
332
|
-
codeEl = el("div", "bwx-code", "");
|
|
333
|
-
codeEl.setAttribute("aria-live", "polite");
|
|
334
372
|
const row = el("div", "bwx-row");
|
|
335
373
|
const copy = el("button", "bwx-btn", "Copy link");
|
|
336
374
|
copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => copy.textContent = "Copied");
|
|
337
|
-
const cancel = el("button", "bwx-btn", "Cancel");
|
|
338
|
-
cancel.onclick = () => {
|
|
339
|
-
o.onCancel?.();
|
|
340
|
-
ac.abort();
|
|
341
|
-
};
|
|
342
375
|
row.append(copy, cancel);
|
|
343
376
|
ui.box.append(h, p, qr, codeEl, row);
|
|
344
377
|
},
|
|
@@ -383,6 +416,25 @@ function pickWallet(options, o = {}) {
|
|
|
383
416
|
});
|
|
384
417
|
}
|
|
385
418
|
|
|
419
|
+
// src/theme.ts
|
|
420
|
+
var WALLET_THEME_META = "bwalletx-theme";
|
|
421
|
+
var ID = /^[a-z0-9-]{1,32}$/;
|
|
422
|
+
function declareWalletTheme(theme) {
|
|
423
|
+
if (typeof document === "undefined") return;
|
|
424
|
+
const id = (theme ?? "").trim().toLowerCase();
|
|
425
|
+
let meta = document.head.querySelector(`meta[name="${WALLET_THEME_META}"]`);
|
|
426
|
+
if (!id || !ID.test(id)) {
|
|
427
|
+
meta?.remove();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (!meta) {
|
|
431
|
+
meta = document.createElement("meta");
|
|
432
|
+
meta.name = WALLET_THEME_META;
|
|
433
|
+
document.head.appendChild(meta);
|
|
434
|
+
}
|
|
435
|
+
if (meta.content !== id) meta.content = id;
|
|
436
|
+
}
|
|
437
|
+
|
|
386
438
|
export {
|
|
387
439
|
BRC29_PROTOCOL,
|
|
388
440
|
PaymentError,
|
|
@@ -398,7 +450,10 @@ export {
|
|
|
398
450
|
handcashAdapter,
|
|
399
451
|
discoverWallets,
|
|
400
452
|
qrSvg,
|
|
453
|
+
isTouchDevice,
|
|
401
454
|
pairingModal,
|
|
402
|
-
pickWallet
|
|
455
|
+
pickWallet,
|
|
456
|
+
WALLET_THEME_META,
|
|
457
|
+
declareWalletTheme
|
|
403
458
|
};
|
|
404
|
-
//# sourceMappingURL=chunk-
|
|
459
|
+
//# sourceMappingURL=chunk-PI42H3XY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pay.ts","../src/wallets.ts","../src/modal.ts","../src/theme.ts"],"sourcesContent":["/**\n * Browser-side payment helpers on top of the BRC-100 wallet's createAction. Works with any\n * BRC-100 wallet. Each helper asks the wallet once; it never retries a spend. If a call fails\n * the error says whether the wallet may have sent anything (`maybeSent`).\n */\nimport { P2PKH, PublicKey, Transaction, Utils, type WalletInterface } from '@bsv/sdk';\n\n/** BRC-29 payment protocol id. */\nexport const BRC29_PROTOCOL: [2, string] = [2, '3241645161d8'];\n\nexport type PayTo = string; // paymail (name@domain), P2PKH address, or 66-hex identity key\n\nexport interface PayOutput {\n /** Locking script hex. */\n script: string;\n satoshis: number;\n description?: string;\n}\n\nexport interface PayRequest {\n to: PayTo;\n /** Amount in satoshis. Give this or `usd`. */\n sats?: number;\n /** Amount in US dollars, converted at `usdPerBsv` (or the live WhatsOnChain rate). */\n usd?: number;\n /** Shown in the wallet and, with `memoOnChain`, written in an OP_RETURN output. */\n memo?: string;\n memoOnChain?: boolean;\n /** Labels for the wallet's history. */\n labels?: string[];\n}\n\nexport interface PayOptions {\n /** Fixed rate for `usd`. Otherwise fetched from WhatsOnChain. */\n usdPerBsv?: number | (() => Promise<number>);\n /** Resolve a paymail to outputs. Default: bsvalias P2P payment destination (needs CORS on the paymail host). */\n resolvePaymail?: (paymail: string, sats: number) => Promise<{ outputs: PayOutput[]; reference?: string; submit?: (rawTxHex: string) => Promise<void> }>;\n fetch?: typeof fetch;\n}\n\nexport interface Brc29Remittance {\n senderIdentityKey: string;\n derivationPrefix: string;\n derivationSuffix: string;\n outputIndex: number;\n}\n\nexport interface PayResult {\n txid: string;\n /** Atomic BEEF (BRC-95) if the wallet returned it. Send it to your server's verifyPayment. */\n tx?: number[];\n sats: number;\n outputs: PayOutput[];\n /** For an identity-key payment: what the recipient needs to internalize the output (BRC-29). Deliver it to them. */\n remittance?: Brc29Remittance;\n}\n\nexport type PaymentErrorCode = 'BAD_REQUEST' | 'RESOLVE_FAILED' | 'RATE_FAILED' | 'WALLET_REJECTED' | 'WALLET_ERROR' | 'NOT_SUPPORTED';\n\nexport class PaymentError extends Error {\n constructor(\n readonly code: PaymentErrorCode,\n message: string,\n /** true when the wallet was asked to spend and the outcome is unknown. Check before trying again. */\n readonly maybeSent = false,\n readonly cause?: unknown,\n ) {\n super(message);\n this.name = 'PaymentError';\n }\n}\n\n/** A wallet that is not BRC-100 (the HandCash transitional adapter) can still pay through this. */\nexport interface LegacyPayer {\n pay(req: PayRequest & { sats: number }): Promise<PayResult>;\n}\nexport type PaySource = WalletInterface | LegacyPayer;\n\nconst isLegacy = (s: PaySource): s is LegacyPayer => typeof (s as LegacyPayer).pay === 'function' && typeof (s as WalletInterface).createAction !== 'function';\n\nexport const isIdentityKeyHex = (s: string) => /^0[23][0-9a-fA-F]{64}$/.test(s);\nexport const isPaymail = (s: string) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s);\n\nfunction randomB64(n: number) {\n const b = new Uint8Array(n);\n globalThis.crypto.getRandomValues(b);\n return Utils.toBase64(Array.from(b));\n}\n\nfunction opReturn(text: string): string {\n const d = Utils.toArray(text, 'utf8');\n const len = d.length;\n const push = len < 0x4c ? [len] : len <= 0xff ? [0x4c, len] : [0x4d, len & 0xff, len >> 8];\n return Utils.toHex([0x00, 0x6a, ...push, ...d]);\n}\n\nconst clip = (s: string, n: number) => (s.length > n ? s.slice(0, n - 1) + '…' : s);\n/** createAction descriptions must be 5–50 bytes. */\nconst describe = (s: string) => clip(s.length < 5 ? `${s} payment`.trim() : s, 50);\n\nasync function usdRate(o: PayOptions): Promise<number> {\n if (typeof o.usdPerBsv === 'number') return o.usdPerBsv;\n try {\n if (typeof o.usdPerBsv === 'function') return await o.usdPerBsv();\n const r = await (o.fetch ?? fetch)('https://api.whatsonchain.com/v1/bsv/main/exchangerate');\n const j = (await r.json()) as { rate?: number };\n if (!r.ok || !(Number(j.rate) > 0)) throw new Error(`status ${r.status}`);\n return Number(j.rate);\n } catch (e) {\n throw new PaymentError('RATE_FAILED', `Could not get the BSV/USD rate: ${(e as Error).message}`, false, e);\n }\n}\n\n/** Satoshis for a request (sats as given, or usd converted, rounded up). */\nexport async function amountInSats(req: Pick<PayRequest, 'sats' | 'usd'>, o: PayOptions = {}): Promise<number> {\n if (req.sats != null) {\n if (!Number.isSafeInteger(req.sats) || req.sats <= 0) throw new PaymentError('BAD_REQUEST', 'sats must be a positive integer');\n return req.sats;\n }\n if (req.usd != null) {\n if (!(req.usd > 0)) throw new PaymentError('BAD_REQUEST', 'usd must be positive');\n return Math.max(1, Math.ceil((req.usd / (await usdRate(o))) * 1e8));\n }\n throw new PaymentError('BAD_REQUEST', 'Give sats or usd');\n}\n\n/** Default paymail resolver: bsvalias P2P Payment Destination (BRC-? / capability 2a40af698840). */\nexport async function resolvePaymailP2P(paymail: string, sats: number, f: typeof fetch = fetch) {\n const [alias, domain] = paymail.split('@');\n const fail = (m: string, e?: unknown) => new PaymentError('RESOLVE_FAILED', `Could not resolve ${paymail}: ${m}`, false, e);\n let caps: Record<string, string>;\n try {\n const r = await f(`https://${domain}/.well-known/bsvalias`);\n caps = ((await r.json()) as { capabilities: Record<string, string> }).capabilities;\n } catch (e) {\n throw fail('no bsvalias document (or CORS blocked it; resolve on your server and pass resolvePaymail)', e);\n }\n const fill = (u: string) => u.replace('{alias}', encodeURIComponent(alias)).replace('{domain.tld}', domain);\n const dest = caps?.['2a40af698840'];\n if (!dest) throw fail('it does not offer P2P payment destinations');\n let body: { outputs?: { script: string; satoshis: number }[]; reference?: string };\n try {\n const r = await f(fill(dest), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ satoshis: sats }) });\n if (!r.ok) throw new Error(`status ${r.status}`);\n body = await r.json();\n } catch (e) {\n throw fail((e as Error).message, e);\n }\n if (!body.outputs?.length) throw fail('no outputs returned');\n const receive = caps['5f1323cddf31'];\n return {\n outputs: body.outputs.map((o) => ({ script: o.script, satoshis: o.satoshis })),\n reference: body.reference,\n submit: receive\n ? async (hex: string) => {\n await f(fill(receive), {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ hex, reference: body.reference, metadata: {} }),\n });\n }\n : undefined,\n };\n}\n\nasync function runAction(wallet: WalletInterface, description: string, outputs: PayOutput[], labels?: string[]) {\n try {\n return await wallet.createAction({\n description: describe(description),\n outputs: outputs.map((o) => ({ lockingScript: o.script, satoshis: o.satoshis, outputDescription: describe(o.description ?? 'payment') })),\n labels: labels?.length ? labels : undefined,\n options: { randomizeOutputs: false },\n });\n } catch (e) {\n const msg = (e as Error)?.message ?? String(e);\n const rejected = /denied|reject|cancel|abort|user/i.test(msg) || (e as { code?: unknown })?.code === 4001;\n // Not retried: a lost reply could mean the wallet already broadcast it.\n throw new PaymentError(rejected ? 'WALLET_REJECTED' : 'WALLET_ERROR', rejected ? 'The payment was declined in the wallet.' : `The wallet could not make the payment: ${msg}`, !rejected, e);\n }\n}\n\n/** Pay a paymail, address or identity key. One wallet prompt; never retried. */\nexport async function pay(source: PaySource, req: PayRequest, o: PayOptions = {}): Promise<PayResult> {\n const to = (req.to ?? '').trim();\n if (!to) throw new PaymentError('BAD_REQUEST', 'Give a recipient (to)');\n if (req.memo && req.memo.length > 1000) throw new PaymentError('BAD_REQUEST', 'memo is too long');\n const sats = await amountInSats(req, o);\n if (isLegacy(source)) return source.pay({ ...req, to, sats });\n\n let outputs: PayOutput[];\n let remittance: Brc29Remittance | undefined;\n let submit: ((hex: string) => Promise<void>) | undefined;\n if (isIdentityKeyHex(to)) {\n const derivationPrefix = randomB64(8);\n const derivationSuffix = randomB64(8);\n let pk: string;\n let me: string;\n try {\n ({ publicKey: pk } = await source.getPublicKey({ protocolID: BRC29_PROTOCOL, keyID: `${derivationPrefix} ${derivationSuffix}`, counterparty: to.toLowerCase() }));\n ({ publicKey: me } = await source.getPublicKey({ identityKey: true }));\n } catch (e) {\n throw new PaymentError('WALLET_ERROR', `The wallet could not derive the payment key: ${(e as Error).message}`, false, e);\n }\n outputs = [{ script: new P2PKH().lock(PublicKey.fromString(pk).toHash() as number[]).toHex(), satoshis: sats, description: 'payment' }];\n remittance = { senderIdentityKey: me, derivationPrefix, derivationSuffix, outputIndex: 0 };\n } else if (isPaymail(to)) {\n const r = await (o.resolvePaymail ?? ((p, s) => resolvePaymailP2P(p, s, o.fetch ?? fetch)))(to, sats);\n outputs = r.outputs;\n submit = r.submit;\n } else {\n try {\n outputs = [{ script: new P2PKH().lock(to).toHex(), satoshis: sats, description: 'payment' }];\n } catch {\n throw new PaymentError('BAD_REQUEST', `Not a paymail, address or identity key: ${to}`);\n }\n }\n if (req.memo && req.memoOnChain) outputs = [...outputs, { script: opReturn(req.memo), satoshis: 0, description: 'memo' }];\n\n const r = await runAction(source, req.memo || `Pay ${clip(to, 30)}`, outputs, req.labels);\n if (!r.txid) throw new PaymentError('WALLET_ERROR', 'The wallet did not return a txid', true);\n const tx = r.tx ? Array.from(r.tx) : undefined;\n if (submit && tx) {\n // Tell the paymail host about the tx. Best effort: the payment itself is already made.\n await submit(Transaction.fromAtomicBEEF(tx).toHex()).catch(() => {});\n }\n return { txid: r.txid, tx, sats, outputs, remittance };\n}\n\n/** pay() labelled as a tip. */\nexport function tip(source: PaySource, req: PayRequest, o: PayOptions = {}): Promise<PayResult> {\n return pay(source, { memo: 'Tip', ...req, labels: [...(req.labels ?? []), 'tip'] }, o);\n}\n\nexport interface TokenQuote {\n /** Outputs to fund, as your server priced them (e.g. payment to the seller + fee). */\n outputs: PayOutput[];\n description?: string;\n /** Called with the result after the wallet signs; e.g. POST txid + tx to your server for verifyPayment. */\n onPaid?: (r: PayResult) => Promise<void> | void;\n}\n\nexport interface BuyTokenRequest {\n token: string;\n amount: number | string;\n /** The hook point: return the outputs for this purchase (usually from your server). */\n quote: (req: { token: string; amount: number | string }) => Promise<TokenQuote>;\n labels?: string[];\n}\n\n/** Buy a token: your `quote` hook prices it, the wallet pays once, `onPaid` delivers it. */\nexport async function buyToken(source: PaySource, req: BuyTokenRequest): Promise<PayResult> {\n if (isLegacy(source)) throw new PaymentError('NOT_SUPPORTED', 'This wallet cannot buy tokens. Use a BRC-100 wallet such as bWalletX.');\n const q = await req.quote({ token: req.token, amount: req.amount });\n if (!q?.outputs?.length) throw new PaymentError('BAD_REQUEST', 'The quote returned no outputs');\n const sats = q.outputs.reduce((n, x) => n + x.satoshis, 0);\n const r = await runAction(source, q.description ?? `Buy ${clip(req.token, 30)}`, q.outputs, [...(req.labels ?? []), 'token-buy']);\n if (!r.txid) throw new PaymentError('WALLET_ERROR', 'The wallet did not return a txid', true);\n const res: PayResult = { txid: r.txid, tx: r.tx ? Array.from(r.tx) : undefined, sats, outputs: q.outputs };\n await q.onPaid?.(res);\n return res;\n}\n","/**\n * Every wallet the user could connect with, behind one interface. BRC-100 first: bWalletX, then any\n * other wallet that announces itself (Yours, Metanet, ...), the in-app browser, optional localhost\n * desktop wallets, phone pairing, and app-supplied adapters (e.g. the transitional HandCash one).\n */\nimport { WalletClient, type WalletInterface } from '@bsv/sdk';\nimport { BWALLETX_RDNS, RDNS_EXTENSION } from './discover.js';\nimport { pairBwalletX, restorePairing, type PairingOptions } from './pair.js';\nimport type { PaySource } from './pay.js';\n\nexport type WalletKind = 'brc100' | 'pairing' | 'handcash' | 'custom';\n\nexport interface ConnectedWallet {\n /** The BRC-100 wallet. null for a non-BRC-100 adapter (HandCash today). */\n wallet: WalletInterface | null;\n /** What pay/tip use: the BRC-100 wallet, or the adapter's own payer. */\n payer: PaySource | null;\n /** The identity public key (hex) when the wallet has one. */\n identityKey: string | null;\n handle?: string;\n paymail?: string;\n /** Which option connected. */\n optionId: string;\n kind: WalletKind;\n /** True for adapters kept only until the vendor ships BRC-100. */\n transitional?: boolean;\n}\n\nexport interface WalletOption {\n id: string;\n name: string;\n icon?: string;\n kind: WalletKind;\n rdns?: string;\n /** True for bWalletX (extension, in-app or paired). */\n isBwalletX: boolean;\n connect(): Promise<ConnectedWallet>;\n}\n\nasync function brc100Connected(wallet: WalletInterface, o: Pick<WalletOption, 'id' | 'kind'>): Promise<ConnectedWallet> {\n await wallet.waitForAuthentication({}).catch(() => {});\n const { publicKey } = await wallet.getPublicKey({ identityKey: true });\n return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };\n}\n\n/** Wrap any BRC-100 wallet as a picker option. */\nexport function brc100Option(wallet: WalletInterface, info: { id: string; name: string; icon?: string; rdns?: string; isBwalletX?: boolean }): WalletOption {\n const opt: WalletOption = {\n id: info.id,\n name: info.name,\n icon: info.icon,\n rdns: info.rdns,\n kind: 'brc100',\n isBwalletX: info.isBwalletX ?? false,\n connect: () => brc100Connected(wallet, opt),\n };\n return opt;\n}\n\n/** Phone / web.bwalletx.com pairing as an option (shows a QR through `pairing`). Reuses a stored pairing first. */\nexport function pairingOption(pairing: PairingOptions | (() => PairingOptions)): WalletOption {\n const opt: WalletOption = {\n id: 'bwalletx-pairing',\n name: 'bWalletX on your phone',\n kind: 'pairing',\n isBwalletX: true,\n async connect() {\n const p = typeof pairing === 'function' ? pairing() : pairing;\n const w = (await restorePairing(p)) ?? (await pairBwalletX(p));\n return brc100Connected(w, opt);\n },\n };\n return opt;\n}\n\nexport interface HandCashAdapterOptions {\n /**\n * Your existing HandCash Connect login (redirect/popup + your server route). Resolve with the\n * profile once the user is signed in. This package does not import any HandCash SDK.\n */\n login(): Promise<{ handle: string; paymail?: string; identityKey?: string }>;\n /** Your existing HandCash pay call (usually a server route using the auth token). Optional. */\n pay?: Extract<PaySource, { pay: unknown }>['pay'];\n icon?: string;\n}\n\n/**\n * TRANSITIONAL. Keeps existing HandCash users working behind the same picker until HandCash's\n * BRC-100 support ships; then remove this and HandCash appears as an ordinary BRC-100 wallet.\n */\nexport function handcashAdapter(o: HandCashAdapterOptions): WalletOption {\n const opt: WalletOption = {\n id: 'handcash',\n name: 'HandCash',\n icon: o.icon,\n kind: 'handcash',\n isBwalletX: false,\n async connect() {\n const p = await o.login();\n return {\n wallet: null,\n payer: o.pay ? { pay: o.pay } : null,\n identityKey: p.identityKey ?? null,\n handle: p.handle,\n paymail: p.paymail ?? `${p.handle.replace(/^\\$/, '')}@handcash.io`,\n optionId: opt.id,\n kind: 'handcash',\n transitional: true,\n };\n },\n };\n return opt;\n}\n\nexport interface DiscoverWalletsOptions {\n /** How long to collect BRC-100 announcements, ms. Default 1200. */\n ms?: number;\n /**\n * Also probe desktop wallets on localhost (Metanet / json-api on 3321, Cicada on 3301).\n * Off by default: some browsers prompt for local-network access.\n */\n probeLocal?: boolean;\n /** Add the phone pairing option (needs a way to show the QR, e.g. pairingModal()). */\n pairing?: PairingOptions | (() => PairingOptions);\n /** App-supplied options appended at the end (e.g. handcashAdapter(...)). */\n extra?: WalletOption[];\n}\n\nconst NAMES: Record<string, string> = { [RDNS_EXTENSION]: 'bWalletX', 'space.bwallet.mobile': 'bWalletX' };\n\nasync function probe(substrate: 'json-api' | 'Cicada', ms: number): Promise<WalletInterface | null> {\n try {\n const w = new WalletClient(substrate);\n const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise<boolean>((r) => setTimeout(() => r(false), ms))]);\n return ok ? w : null;\n } catch {\n return null;\n }\n}\n\n/** All wallets we can see, bWalletX first. SSR-safe: resolves [] (plus `extra`) outside a browser. */\nexport async function discoverWallets(o: DiscoverWalletsOptions = {}): Promise<WalletOption[]> {\n const out: WalletOption[] = [];\n const ms = o.ms ?? 1200;\n if (typeof window !== 'undefined') {\n const seen = new Map<string, WalletOption>();\n const onAnnounce = (e: Event) => {\n const d = (e as CustomEvent).detail as { info?: { rdns?: string; name?: string; icon?: string; uuid?: string }; wallet?: WalletInterface } | undefined;\n if (!d?.wallet) return;\n const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;\n if (seen.has(rdns)) return;\n const isB = (BWALLETX_RDNS as readonly string[]).includes(rdns);\n seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB }));\n };\n window.addEventListener('brc100:announceWallet', onAnnounce);\n window.dispatchEvent(new Event('brc100:requestWallet'));\n const local = o.probeLocal ? Promise.all([probe('json-api', ms), probe('Cicada', ms)]) : Promise.resolve([null, null]);\n await new Promise((r) => setTimeout(r, ms));\n window.removeEventListener('brc100:announceWallet', onAnnounce);\n out.push(...seen.values());\n const cwi = (window as unknown as { CWI?: WalletInterface }).CWI;\n if (cwi && ![...seen.values()].some((x) => x.rdns === 'space.bwallet.mobile')) {\n out.push(brc100Option(cwi, { id: 'window.CWI', name: 'This app’s wallet', isBwalletX: false }));\n }\n const [json, cicada] = await local;\n if (json) out.push(brc100Option(json, { id: 'localhost:3321', name: 'Desktop wallet (localhost:3321)' }));\n if (cicada) out.push(brc100Option(cicada, { id: 'localhost:3301', name: 'Desktop wallet (localhost:3301)' }));\n }\n if (o.pairing) out.push(pairingOption(o.pairing));\n out.push(...(o.extra ?? []));\n // bWalletX first, transitional last; otherwise keep discovery order.\n const rank = (w: WalletOption) => (w.isBwalletX ? 0 : w.kind === 'handcash' ? 2 : 1);\n return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);\n}\n","/**\n * Built-in UI: the pairing QR modal and the wallet picker. Plain DOM, no framework, only created\n * when called in a browser.\n */\nimport qrcode from 'qrcode-generator';\nimport type { PairingOptions } from './pair.js';\nimport type { WalletOption } from './wallets.js';\nimport { WEB_WALLET_URL } from './element.js';\n\nconst CSS = `\n.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}\n.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)}\n.bwx-box h2{margin:0 0 6px;font-size:18px;color:#e8c766}\n.bwx-box p{margin:0 0 14px;color:#bdb6a3;font-size:14px}\n.bwx-qr{background:#fff;border-radius:12px;padding:12px;display:flex;justify-content:center}\n.bwx-qr svg{width:100%;height:auto;max-width:260px}\n.bwx-code{margin:14px 0 0;text-align:center;font-size:28px;letter-spacing:.3em;color:#e8c766;font-variant-numeric:tabular-nums}\n.bwx-row{display:flex;gap:8px;margin-top:14px}\n.bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}\n.bwx-btn:hover{border-color:#e8c766}\na.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}\n.bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}\n.bwx-stack{display:flex;flex-direction:column;gap:8px}\n.bwx-list{display:flex;flex-direction:column;gap:8px}\n.bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}\n.bwx-opt img{width:24px;height:24px;border-radius:6px}\n.bwx-tag{margin-left:auto;font-size:11px;color:#8d866f}\n`;\n\nfunction mount(): { root: HTMLDivElement; box: HTMLDivElement; close: () => void } {\n const root = document.createElement('div');\n root.className = 'bwx-ov';\n root.setAttribute('role', 'dialog');\n root.setAttribute('aria-modal', 'true');\n const style = document.createElement('style');\n style.textContent = CSS;\n const box = document.createElement('div');\n box.className = 'bwx-box';\n root.append(style, box);\n document.body.appendChild(root);\n return { root, box, close: () => root.remove() };\n}\n\nfunction el<K extends keyof HTMLElementTagNameMap>(tag: K, cls?: string, text?: string) {\n const e = document.createElement(tag);\n if (cls) e.className = cls;\n if (text != null) e.textContent = text;\n return e;\n}\n\n/** An SVG QR code for `text` (error correction M). */\nexport function qrSvg(text: string): string {\n const q = qrcode(0, 'M');\n q.addData(text);\n q.make();\n return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });\n}\n\n\n/**\n * True on a phone or tablet: a coarse pointer with no hover, or a mobile user agent (iPadOS reports\n * a Mac UA, so touch points count too). Scanning a QR shown on the same screen is impossible there.\n */\nexport function isTouchDevice(env: { navigator?: Navigator; matchMedia?: (q: string) => MediaQueryList } = globalThis as never): boolean {\n const nav = env.navigator;\n if (!nav) return false;\n const ua = nav.userAgent ?? '';\n if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;\n if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;\n try {\n return !!env.matchMedia?.('(pointer: coarse)').matches && !env.matchMedia?.('(hover: hover)').matches;\n } catch {\n return false;\n }\n}\n\nexport interface PairingModalOptions {\n title?: string;\n /**\n * 'qr' shows the pairing QR (desktop). 'app' hands off to the bWalletX app on this device: the\n * pairing link is a universal link / Android app link that opens the app, and the bwallet.space\n * page behind it explains what to do when the app is not installed. Default 'auto': 'app' on touch\n * devices, 'qr' elsewhere.\n */\n mode?: 'auto' | 'qr' | 'app';\n /** Called when the user closes the modal (the pairing is then aborted). */\n onCancel?: () => void;\n}\n\n/**\n * PairingOptions that show a QR modal. Pass to signInWithBwalletX({ pairing: pairingModal() }) or\n * connectBwalletX. The modal opens when the link is ready and shows the match code. Call `close()`\n * once pairing settles (useBwalletX does this for you); Cancel aborts the pairing.\n */\nexport function pairingModal(o: PairingModalOptions = {}): PairingOptions & { close(): void } {\n const ac = new AbortController();\n let ui: ReturnType<typeof mount> | null = null;\n let codeEl: HTMLDivElement | null = null;\n const close = () => {\n ui?.close();\n ui = null;\n };\n ac.signal.addEventListener('abort', close);\n return {\n signal: ac.signal,\n close,\n onLink(link) {\n if (typeof document === 'undefined') return;\n close();\n ui = mount();\n const app = o.mode === 'app' || ((o.mode ?? 'auto') === 'auto' && isTouchDevice());\n const h = el('h2', '', o.title ?? 'Connect bWalletX');\n const cancel = el('button', 'bwx-btn', 'Cancel');\n cancel.onclick = () => {\n o.onCancel?.();\n ac.abort();\n };\n codeEl = el('div', 'bwx-code', '');\n codeEl.setAttribute('aria-live', 'polite');\n if (app) {\n // Same device: no QR to scan. The link opens the bWalletX app (universal link); approve there,\n // then come back to this tab, which is still waiting on the pairing channel.\n const p = el('p', '', 'Open bWalletX, approve, then come back to this page.');\n const stack = el('div', 'bwx-stack');\n const open = el('a', 'bwx-btn bwx-primary', 'Open bWalletX');\n open.href = link;\n open.rel = 'noopener';\n const web = el('button', 'bwx-btn', 'Use web.bwalletx.com instead');\n web.onclick = () => {\n void navigator.clipboard?.writeText(link).catch(() => undefined);\n window.open(WEB_WALLET_URL, '_blank', 'noopener');\n p.textContent = 'Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.';\n };\n stack.append(open, web, cancel);\n ui.box.append(h, p, codeEl, stack);\n return;\n }\n const p = el('p', '', 'Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.');\n const qr = el('div', 'bwx-qr');\n qr.innerHTML = qrSvg(link);\n const row = el('div', 'bwx-row');\n const copy = el('button', 'bwx-btn', 'Copy link');\n copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => (copy.textContent = 'Copied'));\n row.append(copy, cancel);\n ui.box.append(h, p, qr, codeEl, row);\n },\n onCode(code) {\n if (codeEl) {\n codeEl.textContent = code;\n const p = ui?.box.querySelector('p');\n if (p) p.textContent = 'Check bWalletX shows the same code, then tap Connect.';\n }\n },\n };\n}\n\n/** Show the picker; resolves with the chosen option, or null if closed. */\nexport function pickWallet(options: WalletOption[], o: { title?: string } = {}): Promise<WalletOption | null> {\n if (typeof document === 'undefined') return Promise.resolve(null);\n return new Promise((resolve) => {\n const ui = mount();\n const done = (w: WalletOption | null) => {\n ui.close();\n resolve(w);\n };\n ui.box.append(el('h2', '', o.title ?? 'Choose a wallet'));\n if (!options.length) ui.box.append(el('p', '', 'No wallet found. Get bWalletX at bwalletx.com.'));\n const list = el('div', 'bwx-list');\n for (const w of options) {\n const b = el('button', 'bwx-btn bwx-opt');\n if (w.icon) {\n const img = el('img');\n img.src = w.icon;\n img.alt = '';\n b.append(img);\n }\n b.append(el('span', '', w.name));\n if (w.kind === 'handcash') b.append(el('span', 'bwx-tag', 'until BRC-100'));\n b.onclick = () => done(w);\n list.append(b);\n }\n const cancel = el('button', 'bwx-btn', 'Cancel');\n cancel.onclick = () => done(null);\n ui.root.onclick = (e) => e.target === ui.root && done(null);\n ui.box.append(list, el('div', 'bwx-row'));\n (ui.box.lastChild as HTMLElement).append(cancel);\n });\n}\n","/**\n * Site theme for the wallet. A site may ask bWalletX to wear one of the themes the wallet already bundles\n * (e.g. 'npgx') while the site is connected and is the active tab; the wallet puts the user's own theme back on\n * any other site or when this declaration is removed. Only an id is declared: the wallet decides whether this\n * origin may wear it (brand editions are locked to their own sites) and never loads colours, CSS or URLs from here.\n */\nexport const WALLET_THEME_META = 'bwalletx-theme';\n\nconst ID = /^[a-z0-9-]{1,32}$/;\n\n/** Declare (or with null, withdraw) the theme the wallet should wear for this page. No-op outside a browser. */\nexport function declareWalletTheme(theme: string | null | undefined): void {\n if (typeof document === 'undefined') return;\n const id = (theme ?? '').trim().toLowerCase();\n let meta = document.head.querySelector<HTMLMetaElement>(`meta[name=\"${WALLET_THEME_META}\"]`);\n if (!id || !ID.test(id)) {\n meta?.remove();\n return;\n }\n if (!meta) {\n meta = document.createElement('meta');\n meta.name = WALLET_THEME_META;\n document.head.appendChild(meta);\n }\n if (meta.content !== id) meta.content = id;\n}\n"],"mappings":"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","names":["r","p"]}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACCESS_CHALLENGE_LIFETIME_S,
|
|
3
|
+
ACCESS_PROTOCOL,
|
|
4
|
+
AccessError,
|
|
5
|
+
MAX_HOLDING_PROOFS,
|
|
6
|
+
atLeastRaw,
|
|
7
|
+
canonicalJson,
|
|
8
|
+
challengeBytes,
|
|
9
|
+
kindFor,
|
|
10
|
+
normaliseRequirement,
|
|
11
|
+
parseAccessChallenge,
|
|
12
|
+
tokenNeedRaw
|
|
13
|
+
} from "./chunk-MWD4BCO5.js";
|
|
14
|
+
|
|
1
15
|
// src/server.ts
|
|
2
|
-
import { ProtoWallet, PublicKey } from "@bsv/sdk";
|
|
16
|
+
import { ProtoWallet as ProtoWallet2, PublicKey as PublicKey2 } from "@bsv/sdk";
|
|
3
17
|
|
|
4
18
|
// src/nonce-stores.ts
|
|
5
19
|
var ttlSeconds = (c) => Math.max(1, Math.ceil((c.expiresAt - Date.now()) / 1e3));
|
|
@@ -305,6 +319,196 @@ async function signInOrLink(identityKey, o) {
|
|
|
305
319
|
return { status: "created", userId, identityKey: key };
|
|
306
320
|
}
|
|
307
321
|
|
|
322
|
+
// src/access-server.ts
|
|
323
|
+
import { ProtoWallet, PublicKey, Signature } from "@bsv/sdk";
|
|
324
|
+
var bare = (tokenId) => tokenId.replace(/^bsv21:/, "");
|
|
325
|
+
function oneSatResolver(o = {}) {
|
|
326
|
+
const base = (o.base ?? "https://api.1sat.app/1sat").replace(/\/$/, "");
|
|
327
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
328
|
+
const t = o.timeoutMs ?? 12e3;
|
|
329
|
+
const call = async (path, init) => {
|
|
330
|
+
const res = await f(`${base}${path}`, { ...init, signal: AbortSignal.timeout(t), cache: "no-store" });
|
|
331
|
+
if (!res.ok) throw new Error(`1sat ${res.status} for ${path}`);
|
|
332
|
+
return res.json();
|
|
333
|
+
};
|
|
334
|
+
return {
|
|
335
|
+
async tokenBalance(tokenId, addresses) {
|
|
336
|
+
const sorted = [...new Set(addresses)].sort();
|
|
337
|
+
let total = 0n;
|
|
338
|
+
for (let i = 0; i < sorted.length; i += 100) {
|
|
339
|
+
const j = await call(`/bsv21/${bare(tokenId)}/p2pkh/balance`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: { "content-type": "application/json" },
|
|
342
|
+
body: JSON.stringify(sorted.slice(i, i + 100))
|
|
343
|
+
});
|
|
344
|
+
const b = j?.balance ?? 0;
|
|
345
|
+
const s = typeof b === "number" ? String(Math.trunc(b)) : String(b).split(".")[0] || "0";
|
|
346
|
+
if (!/^\d+$/.test(s)) throw new Error("1sat returned a bad balance");
|
|
347
|
+
total += BigInt(s);
|
|
348
|
+
}
|
|
349
|
+
return total.toString();
|
|
350
|
+
},
|
|
351
|
+
async tokenSupply(tokenId) {
|
|
352
|
+
const j = await call(`/bsv21/${bare(tokenId)}`);
|
|
353
|
+
const tok = (j?.token ?? j) || {};
|
|
354
|
+
const v = tok.amt ?? tok.max ?? tok.supply;
|
|
355
|
+
const s = v === void 0 || v === null ? "" : String(v).split(".")[0];
|
|
356
|
+
if (!/^\d+$/.test(s) || s === "0") throw new Error("1sat did not report a supply");
|
|
357
|
+
return s;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function bitSignResolver(o = {}) {
|
|
362
|
+
const base = (o.base ?? "https://bit-sign.online").replace(/\/$/, "");
|
|
363
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
364
|
+
const tokens = o.tokens ?? oneSatResolver({ fetch: f });
|
|
365
|
+
return {
|
|
366
|
+
tokenBalance: (id, a) => tokens.tokenBalance(id, a),
|
|
367
|
+
tokenSupply: tokens.tokenSupply ? (id) => tokens.tokenSupply(id) : void 0,
|
|
368
|
+
async identity(identityKey, response) {
|
|
369
|
+
const res = await f(`${base}/api/access/verify`, {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: { "content-type": "application/json" },
|
|
372
|
+
body: JSON.stringify({ response, only: "identity" }),
|
|
373
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? 12e3),
|
|
374
|
+
cache: "no-store"
|
|
375
|
+
});
|
|
376
|
+
const j = await res.json().catch(() => null);
|
|
377
|
+
if (!j?.identity || j.identityKey !== identityKey) throw new Error(`bit-sign ${res.status}`);
|
|
378
|
+
return { level: Number(j.identity.level) || 0, kyc: j.identity.kyc === true };
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function normalOrigin(origin) {
|
|
383
|
+
const u = new URL(origin);
|
|
384
|
+
if (u.origin === "null" || u.origin !== origin.replace(/\/$/, "")) throw new Error(`origin must be scheme://host[:port], got ${origin}`);
|
|
385
|
+
return u.origin;
|
|
386
|
+
}
|
|
387
|
+
function randomNonce() {
|
|
388
|
+
const b = new Uint8Array(18);
|
|
389
|
+
globalThis.crypto.getRandomValues(b);
|
|
390
|
+
let s = "";
|
|
391
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
392
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
393
|
+
}
|
|
394
|
+
async function createAccessChallenge(o) {
|
|
395
|
+
const origin = normalOrigin(o.origin);
|
|
396
|
+
const require2 = normaliseRequirement(o.require);
|
|
397
|
+
let sub;
|
|
398
|
+
if (o.identityKey !== void 0 && o.identityKey !== null && o.identityKey !== "") {
|
|
399
|
+
if (!isIdentityKey(o.identityKey)) throw new AccessError("bad_request", "identityKey must be a compressed public key");
|
|
400
|
+
sub = o.identityKey.toLowerCase();
|
|
401
|
+
}
|
|
402
|
+
const iat = Math.floor((o.now ?? Date.now()) / 1e3);
|
|
403
|
+
const exp = iat + (o.lifetimeS ?? ACCESS_CHALLENGE_LIFETIME_S);
|
|
404
|
+
const nonce = randomNonce();
|
|
405
|
+
const c = { v: 1, kind: o.kind ?? kindFor(require2), origin, nonce, iat, exp, sub, require: require2 };
|
|
406
|
+
const challenge = canonicalJson(c);
|
|
407
|
+
await o.store.put(nonce, { identityKey: sub ?? "", origin, message: challenge, expiresAt: exp * 1e3 });
|
|
408
|
+
return { challenge, nonce, kind: c.kind, protocolID: ACCESS_PROTOCOL, keyID: nonce, exp };
|
|
409
|
+
}
|
|
410
|
+
var isByteArray = (s) => Array.isArray(s) && s.length >= 8 && s.length <= 80 && s.every((b) => Number.isInteger(b) && b >= 0 && b <= 255);
|
|
411
|
+
function verifyHoldingProof(challenge, p) {
|
|
412
|
+
const proof = p;
|
|
413
|
+
if (!proof || typeof proof.pubkey !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(proof.pubkey) || !isByteArray(proof.signature)) return null;
|
|
414
|
+
try {
|
|
415
|
+
const pub = PublicKey.fromString(proof.pubkey);
|
|
416
|
+
const sig = Signature.fromDER(proof.signature);
|
|
417
|
+
return pub.verify(challengeBytes(challenge), sig) ? pub.toAddress() : null;
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function verifyAccess(o) {
|
|
423
|
+
const r = o.response;
|
|
424
|
+
if (!r || typeof r !== "object" || r.v !== 1) throw new AccessError("bad_request", "response must be { v: 1, challenge, identityKey, signature }");
|
|
425
|
+
const c = parseAccessChallenge(r.challenge);
|
|
426
|
+
const nowS = Math.floor((o.now ?? Date.now()) / 1e3);
|
|
427
|
+
const pending = await o.store.take(c.nonce);
|
|
428
|
+
if (!pending) throw new AccessError(nowS >= c.exp ? "expired" : "replay");
|
|
429
|
+
if (nowS >= c.exp || (o.now ?? Date.now()) > pending.expiresAt) throw new AccessError("expired");
|
|
430
|
+
if (pending.message !== r.challenge) throw new AccessError("replay", "challenge does not match the one issued");
|
|
431
|
+
let origin;
|
|
432
|
+
try {
|
|
433
|
+
origin = normalOrigin(o.origin);
|
|
434
|
+
} catch {
|
|
435
|
+
throw new AccessError("bad_request", "server origin is misconfigured");
|
|
436
|
+
}
|
|
437
|
+
if (c.origin !== origin || pending.origin !== origin) throw new AccessError("bad_request", "origin mismatch");
|
|
438
|
+
if (o.require && canonicalJson(normaliseRequirement(o.require)) !== canonicalJson(c.require)) {
|
|
439
|
+
throw new AccessError("bad_request", "challenge requirement does not match this gate");
|
|
440
|
+
}
|
|
441
|
+
if (!isIdentityKey(r.identityKey)) throw new AccessError("bad_request", "identityKey must be a compressed public key");
|
|
442
|
+
const identityKey = r.identityKey.toLowerCase();
|
|
443
|
+
if (c.sub && c.sub !== identityKey) throw new AccessError("wrong_account");
|
|
444
|
+
if (!isByteArray(r.signature)) throw new AccessError("invalid_signature");
|
|
445
|
+
let valid = false;
|
|
446
|
+
try {
|
|
447
|
+
({ valid } = await new ProtoWallet("anyone").verifySignature({
|
|
448
|
+
protocolID: ACCESS_PROTOCOL,
|
|
449
|
+
keyID: c.nonce,
|
|
450
|
+
counterparty: identityKey,
|
|
451
|
+
data: challengeBytes(r.challenge),
|
|
452
|
+
signature: r.signature
|
|
453
|
+
}));
|
|
454
|
+
} catch {
|
|
455
|
+
valid = false;
|
|
456
|
+
}
|
|
457
|
+
if (!valid) throw new AccessError("invalid_signature");
|
|
458
|
+
const grant = { ok: true, identityKey, kind: c.kind, require: c.require, addresses: [], have: {} };
|
|
459
|
+
if (c.require.identityLevel || c.require.kyc) {
|
|
460
|
+
if (!o.resolver.identity) throw new AccessError("indexer_unavailable", "no identity resolver configured");
|
|
461
|
+
let id;
|
|
462
|
+
try {
|
|
463
|
+
id = await o.resolver.identity(identityKey, r);
|
|
464
|
+
} catch (e) {
|
|
465
|
+
throw new AccessError("indexer_unavailable", `identity lookup failed: ${e.message}`);
|
|
466
|
+
}
|
|
467
|
+
grant.have.identityLevel = id.level;
|
|
468
|
+
grant.have.kyc = id.kyc;
|
|
469
|
+
if (c.require.kyc && !id.kyc) throw new AccessError("kyc_required");
|
|
470
|
+
if (c.require.identityLevel && id.level < c.require.identityLevel) {
|
|
471
|
+
throw new AccessError("identity_level_too_low", void 0, { have: id.level, need: c.require.identityLevel });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (c.require.token) {
|
|
475
|
+
const proofs = Array.isArray(r.holdings) ? r.holdings.slice(0, MAX_HOLDING_PROOFS) : [];
|
|
476
|
+
const addresses = /* @__PURE__ */ new Set();
|
|
477
|
+
for (const p of proofs) {
|
|
478
|
+
const a = verifyHoldingProof(r.challenge, p);
|
|
479
|
+
if (a) addresses.add(a);
|
|
480
|
+
}
|
|
481
|
+
grant.addresses = [...addresses].sort();
|
|
482
|
+
const check = await checkTokenHolding(c.require, grant.addresses, o.resolver);
|
|
483
|
+
grant.have.tokenRaw = check.have;
|
|
484
|
+
grant.have.needRaw = check.need;
|
|
485
|
+
}
|
|
486
|
+
return grant;
|
|
487
|
+
}
|
|
488
|
+
async function checkTokenHolding(require2, addresses, resolver) {
|
|
489
|
+
const t = require2.token;
|
|
490
|
+
if (!t) return { have: "0", need: "0" };
|
|
491
|
+
let supply;
|
|
492
|
+
if (t.minBps) {
|
|
493
|
+
if (!resolver.tokenSupply) throw new AccessError("indexer_unavailable", "no supply resolver configured");
|
|
494
|
+
try {
|
|
495
|
+
supply = await resolver.tokenSupply(t.id);
|
|
496
|
+
} catch (e) {
|
|
497
|
+
throw new AccessError("indexer_unavailable", `supply lookup failed: ${e.message}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const need = tokenNeedRaw(t, supply);
|
|
501
|
+
if (!addresses.length) throw new AccessError("insufficient_tokens", void 0, { have: "0", need });
|
|
502
|
+
let have;
|
|
503
|
+
try {
|
|
504
|
+
have = await resolver.tokenBalance(t.id, addresses);
|
|
505
|
+
} catch (e) {
|
|
506
|
+
throw new AccessError("indexer_unavailable", `balance lookup failed: ${e.message}`);
|
|
507
|
+
}
|
|
508
|
+
if (!atLeastRaw(have, need)) throw new AccessError("insufficient_tokens", void 0, { have, need });
|
|
509
|
+
return { have, need };
|
|
510
|
+
}
|
|
511
|
+
|
|
308
512
|
// src/server.ts
|
|
309
513
|
var LOGIN_PROTOCOL = [2, "bwallet sign in"];
|
|
310
514
|
var CHALLENGE_LIFETIME_MS = 2 * 60 * 1e3;
|
|
@@ -336,13 +540,13 @@ var storeOrDefault = (s) => s ?? (defaultStore ??= memoryStore());
|
|
|
336
540
|
function isIdentityKey(x) {
|
|
337
541
|
if (typeof x !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(x)) return false;
|
|
338
542
|
try {
|
|
339
|
-
|
|
543
|
+
PublicKey2.fromString(x);
|
|
340
544
|
return true;
|
|
341
545
|
} catch {
|
|
342
546
|
return false;
|
|
343
547
|
}
|
|
344
548
|
}
|
|
345
|
-
function
|
|
549
|
+
function normalOrigin2(origin) {
|
|
346
550
|
const u = new URL(origin);
|
|
347
551
|
if (u.origin === "null" || u.origin !== origin.replace(/\/$/, "")) {
|
|
348
552
|
throw new Error(`origin must be scheme://host[:port], got ${origin}`);
|
|
@@ -360,7 +564,7 @@ function loginMessage(origin, nonce, expiresAt) {
|
|
|
360
564
|
`Expires: ${new Date(expiresAt).toISOString()}`
|
|
361
565
|
].join("\n");
|
|
362
566
|
}
|
|
363
|
-
function
|
|
567
|
+
function randomNonce2() {
|
|
364
568
|
const b = new Uint8Array(16);
|
|
365
569
|
globalThis.crypto.getRandomValues(b);
|
|
366
570
|
let s = "";
|
|
@@ -369,8 +573,8 @@ function randomNonce() {
|
|
|
369
573
|
}
|
|
370
574
|
async function createChallenge(opts) {
|
|
371
575
|
if (!isIdentityKey(opts.identityKey)) throw new Error("identityKey must be a compressed public key (66 hex chars)");
|
|
372
|
-
const origin =
|
|
373
|
-
const nonce =
|
|
576
|
+
const origin = normalOrigin2(opts.origin);
|
|
577
|
+
const nonce = randomNonce2();
|
|
374
578
|
const expiresAt = Date.now() + CHALLENGE_LIFETIME_MS;
|
|
375
579
|
const message = loginMessage(origin, nonce, expiresAt);
|
|
376
580
|
await storeOrDefault(opts.store).put(nonce, { identityKey: opts.identityKey.toLowerCase(), origin, message, expiresAt });
|
|
@@ -382,7 +586,7 @@ async function verifySignIn(opts) {
|
|
|
382
586
|
if (!c || Date.now() > c.expiresAt) return null;
|
|
383
587
|
let origin;
|
|
384
588
|
try {
|
|
385
|
-
origin =
|
|
589
|
+
origin = normalOrigin2(opts.origin);
|
|
386
590
|
} catch {
|
|
387
591
|
return null;
|
|
388
592
|
}
|
|
@@ -395,7 +599,7 @@ async function verifySignIn(opts) {
|
|
|
395
599
|
return null;
|
|
396
600
|
}
|
|
397
601
|
try {
|
|
398
|
-
const { valid } = await new
|
|
602
|
+
const { valid } = await new ProtoWallet2("anyone").verifySignature({
|
|
399
603
|
protocolID: LOGIN_PROTOCOL,
|
|
400
604
|
keyID: opts.nonce,
|
|
401
605
|
counterparty: identityKey,
|
|
@@ -409,7 +613,7 @@ async function verifySignIn(opts) {
|
|
|
409
613
|
}
|
|
410
614
|
function originAllowed(requestOrigin, origin) {
|
|
411
615
|
try {
|
|
412
|
-
return !!requestOrigin && requestOrigin ===
|
|
616
|
+
return !!requestOrigin && requestOrigin === normalOrigin2(origin);
|
|
413
617
|
} catch {
|
|
414
618
|
return false;
|
|
415
619
|
}
|
|
@@ -430,6 +634,12 @@ export {
|
|
|
430
634
|
IdentityLinkConflictError,
|
|
431
635
|
linkIdentity,
|
|
432
636
|
signInOrLink,
|
|
637
|
+
oneSatResolver,
|
|
638
|
+
bitSignResolver,
|
|
639
|
+
createAccessChallenge,
|
|
640
|
+
verifyHoldingProof,
|
|
641
|
+
verifyAccess,
|
|
642
|
+
checkTokenHolding,
|
|
433
643
|
LOGIN_PROTOCOL,
|
|
434
644
|
CHALLENGE_LIFETIME_MS,
|
|
435
645
|
memoryStore,
|
|
@@ -439,4 +649,4 @@ export {
|
|
|
439
649
|
verifySignIn,
|
|
440
650
|
originAllowed
|
|
441
651
|
};
|
|
442
|
-
//# sourceMappingURL=chunk-
|
|
652
|
+
//# sourceMappingURL=chunk-QRXFL66W.js.map
|