@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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.3 (unreleased)
4
+
5
+ - **Phones get "Open bWalletX", not a QR.** On a touch device (`isTouchDevice()`), `pairingModal()` no longer shows a QR the user would have to scan with the same phone. It shows **Open bWalletX**, a link to the pairing universal link (`https://www.bwallet.space/pair?…`, the iOS universal link / Android app link the app already handles), plus **Use web.bwalletx.com instead** (copies the link and opens the web wallet to paste it), and the match code. The page keeps waiting on the pairing channel, so the user approves in the wallet and comes back. Desktops still get the QR. Force either with `pairingModal({ mode: 'qr' | 'app' })`.
6
+
7
+ ## 0.5.2 (unreleased)
8
+
9
+ - **Security: framed pages only ask bWalletX itself over XDM.** `findBwalletX` / `discoverBwalletX` in a frame used to post to whatever page framed it and take its answers, so any page that framed your site became "the wallet" (saw every call, could return a fake txid). Now the parent's origin (from `location.ancestorOrigins`, else the referrer) must be one of `BWALLETX_PARENT_ORIGINS` (web.bwalletx.com, bwallet.space, the iOS/Android/Desktop app shells, the Chrome extension); messages go to that exact origin and only its answers count. `probeXdm(ms, allowed)` takes your own list if you host the wallet elsewhere.
10
+ - Docs: verify payments on your server (`verifyPayment`); never trust a txid from the page.
11
+
12
+ ## 0.5.0 (unreleased)
13
+
14
+ - **Site theme in the wallet**: `BwalletXProvider theme="npgx"` (or `declareWalletTheme('npgx')`) declares `<meta name="bwalletx-theme">` while connecting/connected and removes it on disconnect. bWalletX wears that theme for this origin only, from the themes it bundles (brand editions locked to their own sites); only the id is sent, never colours or CSS.
15
+ - Sign-in errors carry `err.stage` (`identity` | `challenge` | `signature` | `verify`) with the original message, so a site can say what actually failed instead of guessing from the text.
16
+ - **Access protocol v1 ($401 identity / $403 token-gated access)**, spec in `docs/ACCESS-PROTOCOL.md`. Standard BRC-100 calls only, so any BRC-100 wallet works.
17
+ - Client: `requestAccess()` signs the site's challenge and proves the keys that hold the token (`proveHoldings()`). Errors are `AccessError` with a protocol code.
18
+ - Server: `createAccessChallenge`, `verifyAccess`, `checkTokenHolding`, plus `oneSatResolver()` (1Sat indexer) and `bitSignResolver()` (identity level / KYC). Fails closed: an indexer outage is `indexer_unavailable`, never a grant.
19
+ - Next.js: `createBwalletXAccess()` gives challenge, verify, status and logout routes, a signed access cookie, and `getAccess()`, which re-checks holdings every `recheckEvery` seconds so selling the tokens ends access.
20
+ - `<bwalletx-signin require-token="bsv21:…" min="100">` runs the access flow and emits `bwalletx-access-granted` / `bwalletx-access-denied`.
21
+ - Protocol videos and posters in `assets/protocol/` (401 green, 402 blue, 403 red), exported as `PROTOCOL_MEDIA`.
22
+
3
23
  ## 0.4.1 (2026-10-12)
4
24
 
5
25
  - React: `BwalletXProvider` and `BwalletxSignin` declare `ReactElement` return types, so they type-check as JSX under React 18 `@types/react`.
package/README.md CHANGED
@@ -99,6 +99,8 @@ function Account() {
99
99
 
100
100
  ## Payments
101
101
 
102
+ Other bCorp apps: use `tip()` for the default Tip button (the same one-sheet tip bWalletX shows on artists, NFTs, chat, Friends and Spaces).
103
+
102
104
  ```ts
103
105
  import { pay, tip, buyToken } from '@bwalletx/connect';
104
106
 
@@ -110,7 +112,9 @@ await buyToken(wallet, { token: '$BOASE', amount: 100, quote: (q) => fetch('/api
110
112
 
111
113
  Each call makes one `createAction`, and nothing is ever retried. Failures throw a `PaymentError` with a `code` (`BAD_REQUEST`, `RESOLVE_FAILED`, `RATE_FAILED`, `WALLET_REJECTED`, `WALLET_ERROR`, `NOT_SUPPORTED`). When `maybeSent` is true, the wallet may already have broadcast the payment, so check before you ask the user again. Browsers often can't resolve paymails themselves because of CORS, so pass `resolvePaymail` to do it on your server.
112
114
 
113
- On the server, check the payment before you deliver anything:
115
+ **Verify payments on your server.** A txid or BEEF that comes back to the page is only what the page was told: a
116
+ browser extension, a modified page or (before 0.5.2) a parent frame posing as the wallet can return a fake one.
117
+ Deliver nothing until your server has checked the payment:
114
118
 
115
119
  ```ts
116
120
  import { verifyPayment } from '@bwalletx/connect/server';
@@ -134,7 +138,7 @@ Every expected output has to be matched by a different output that carries at le
134
138
  - phone pairing (`pairing: pairingModal()`)
135
139
  - any adapters you add in `extra`
136
140
 
137
- `pickWallet(list)` shows the picker. `pairingModal()` returns `PairingOptions` that draw the QR code and match code, so you no longer need your own QR library.
141
+ `pickWallet(list)` shows the picker. `pairingModal()` returns `PairingOptions` that draw the QR code and match code, so you no longer need your own QR library. On phones and tablets it shows an **Open bWalletX** button (the pairing universal link opens the app) and a web.bwalletx.com fallback instead of a QR; pass `mode: 'qr'` or `'app'` to force one.
138
142
 
139
143
  ## Migrating from HandCash Connect
140
144
 
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,118 @@
1
+ import { WalletProtocol } from '@bsv/sdk';
2
+
3
+ /**
4
+ * $401 / $402 / $403 access protocol, version 1. Shared by sites (client + server) and wallets.
5
+ * The full spec is docs/ACCESS-PROTOCOL.md. Pure: no I/O, @bsv/sdk types only.
6
+ *
7
+ * 401 identity "prove who you are (at identity level N, or KYC'd)"
8
+ * 402 payment "pay for this" (the challenge format is reserved; payment itself uses pay())
9
+ * 403 access "prove you hold N of token X (or N basis points of its supply)"
10
+ */
11
+
12
+ declare const ACCESS_VERSION: 1;
13
+ /** Security level 2, a protocol name used for nothing else. Wallets recognise it and show the kind's video. */
14
+ declare const ACCESS_PROTOCOL: WalletProtocol;
15
+ /** How long a challenge is valid. */
16
+ declare const ACCESS_CHALLENGE_LIFETIME_S = 120;
17
+ /** At most this many holding proofs are read from one response. */
18
+ declare const MAX_HOLDING_PROOFS = 50;
19
+ type AccessKind = '401' | '402' | '403';
20
+ interface TokenRequirement {
21
+ /** 'bsv21:<txid>_<vout>' */
22
+ id: string;
23
+ /** Minimum raw (unscaled, integer string) amount. */
24
+ min?: string;
25
+ /** Minimum in basis points of total supply (1 = 0.01%, 10000 = 100%). */
26
+ minBps?: number;
27
+ }
28
+ interface AccessRequirement {
29
+ token?: TokenRequirement;
30
+ /** $401 identity level (bit-sign strands): 1 Basic, 2 Verified, 3 Strong, 4 Sovereign (Veriff). */
31
+ identityLevel?: number;
32
+ /** A live KYC credential is required. */
33
+ kyc?: boolean;
34
+ }
35
+ /** What the wallet signs, as canonical JSON. Times are seconds since the epoch. */
36
+ interface AccessChallenge {
37
+ v: 1;
38
+ kind: AccessKind;
39
+ origin: string;
40
+ nonce: string;
41
+ iat: number;
42
+ exp: number;
43
+ /** The identity key the site expects. A different wallet account is `wrong_account`. */
44
+ sub?: string;
45
+ require: AccessRequirement;
46
+ }
47
+ /** A key that locks some of the required token, proving control by signing the same challenge bytes. */
48
+ interface HoldingProof {
49
+ /** Compressed public key hex of the key locking the outputs. */
50
+ pubkey: string;
51
+ /** DER signature by that key over SHA-256(challenge bytes), as BRC-100 createSignature returns. */
52
+ signature: number[];
53
+ /** Optional hint: outpoints ('txid_vout') this key holds. Servers must not trust it. */
54
+ outpoints?: string[];
55
+ }
56
+ /** What the wallet (via requestAccess) posts back to the site. */
57
+ interface AccessResponse {
58
+ v: 1;
59
+ /** The challenge string exactly as issued. */
60
+ challenge: string;
61
+ identityKey: string;
62
+ /** DER signature: createSignature({ protocolID: ACCESS_PROTOCOL, keyID: nonce, counterparty: 'anyone', data: challenge bytes }). */
63
+ signature: number[];
64
+ holdings?: HoldingProof[];
65
+ }
66
+ type AccessErrorCode = 'not_installed' | 'user_rejected' | 'wrong_account' | 'insufficient_tokens' | 'identity_level_too_low' | 'kyc_required' | 'indexer_unavailable' | 'expired' | 'replay' | 'invalid_signature' | 'bad_request';
67
+ /** The HTTP status a site should answer each code with. */
68
+ declare const ACCESS_ERROR_STATUS: Record<AccessErrorCode, number>;
69
+ interface AccessErrorBody {
70
+ error: AccessErrorCode;
71
+ message: string;
72
+ /** insufficient_tokens: raw amounts; identity_level_too_low: levels. */
73
+ have?: string | number;
74
+ need?: string | number;
75
+ }
76
+ declare class AccessError extends Error {
77
+ readonly code: AccessErrorCode;
78
+ readonly have?: string | number;
79
+ readonly need?: string | number;
80
+ constructor(code: AccessErrorCode, message?: string, extra?: {
81
+ have?: string | number;
82
+ need?: string | number;
83
+ });
84
+ toJSON(): AccessErrorBody;
85
+ }
86
+ /** One video + poster + colour per kind, so wallets and sites show the same thing. Paths are inside the package. */
87
+ declare const PROTOCOL_MEDIA: Record<AccessKind, {
88
+ name: string;
89
+ color: string;
90
+ video: string;
91
+ poster: string;
92
+ }>;
93
+ /** Media URLs on a CDN (unpkg) for a given package version. */
94
+ declare function protocolMediaUrl(kind: AccessKind, which: 'video' | 'poster', version?: string): string;
95
+ /**
96
+ * Canonical JSON: object keys sorted by code unit, no whitespace, undefined members dropped.
97
+ * Only strings, finite numbers, booleans, null, arrays and plain objects are allowed.
98
+ */
99
+ declare function canonicalJson(v: unknown): string;
100
+ /** The bytes every signature in the protocol covers: UTF-8 of the challenge string. */
101
+ declare function challengeBytes(challenge: string): number[];
102
+ /** Throws AccessError('bad_request') if the requirement is malformed. Returns a normalised copy. */
103
+ declare function normaliseRequirement(r: unknown): AccessRequirement;
104
+ /** The kind a requirement implies: any token → 403; otherwise 401. */
105
+ declare function kindFor(r: AccessRequirement): AccessKind;
106
+ /** Parse and validate a challenge string. Throws AccessError('bad_request'). Does not check expiry. */
107
+ declare function parseAccessChallenge(s: unknown): AccessChallenge;
108
+ /** Raw amount needed for a token requirement, given total supply (needed only for minBps). Rounds up. */
109
+ declare function tokenNeedRaw(t: TokenRequirement, supplyRaw?: string): string;
110
+ /** Compare unscaled integer strings. Fails closed on malformed input. */
111
+ declare function atLeastRaw(have: string, need: string): boolean;
112
+ /** A user-readable line for an error code. */
113
+ declare function accessErrorMessage(code: AccessErrorCode, e?: {
114
+ have?: string | number;
115
+ need?: string | number;
116
+ }): string;
117
+
118
+ export { AccessError as A, type HoldingProof as H, MAX_HOLDING_PROOFS as M, PROTOCOL_MEDIA as P, type TokenRequirement as T, type AccessRequirement as a, type AccessResponse as b, type AccessKind as c, ACCESS_PROTOCOL as d, ACCESS_CHALLENGE_LIFETIME_S as e, ACCESS_ERROR_STATUS as f, ACCESS_VERSION as g, type AccessChallenge as h, type AccessErrorBody as i, type AccessErrorCode as j, accessErrorMessage as k, atLeastRaw as l, canonicalJson as m, challengeBytes as n, kindFor as o, normaliseRequirement as p, parseAccessChallenge as q, protocolMediaUrl as r, tokenNeedRaw as t };
@@ -0,0 +1,118 @@
1
+ import { WalletProtocol } from '@bsv/sdk';
2
+
3
+ /**
4
+ * $401 / $402 / $403 access protocol, version 1. Shared by sites (client + server) and wallets.
5
+ * The full spec is docs/ACCESS-PROTOCOL.md. Pure: no I/O, @bsv/sdk types only.
6
+ *
7
+ * 401 identity "prove who you are (at identity level N, or KYC'd)"
8
+ * 402 payment "pay for this" (the challenge format is reserved; payment itself uses pay())
9
+ * 403 access "prove you hold N of token X (or N basis points of its supply)"
10
+ */
11
+
12
+ declare const ACCESS_VERSION: 1;
13
+ /** Security level 2, a protocol name used for nothing else. Wallets recognise it and show the kind's video. */
14
+ declare const ACCESS_PROTOCOL: WalletProtocol;
15
+ /** How long a challenge is valid. */
16
+ declare const ACCESS_CHALLENGE_LIFETIME_S = 120;
17
+ /** At most this many holding proofs are read from one response. */
18
+ declare const MAX_HOLDING_PROOFS = 50;
19
+ type AccessKind = '401' | '402' | '403';
20
+ interface TokenRequirement {
21
+ /** 'bsv21:<txid>_<vout>' */
22
+ id: string;
23
+ /** Minimum raw (unscaled, integer string) amount. */
24
+ min?: string;
25
+ /** Minimum in basis points of total supply (1 = 0.01%, 10000 = 100%). */
26
+ minBps?: number;
27
+ }
28
+ interface AccessRequirement {
29
+ token?: TokenRequirement;
30
+ /** $401 identity level (bit-sign strands): 1 Basic, 2 Verified, 3 Strong, 4 Sovereign (Veriff). */
31
+ identityLevel?: number;
32
+ /** A live KYC credential is required. */
33
+ kyc?: boolean;
34
+ }
35
+ /** What the wallet signs, as canonical JSON. Times are seconds since the epoch. */
36
+ interface AccessChallenge {
37
+ v: 1;
38
+ kind: AccessKind;
39
+ origin: string;
40
+ nonce: string;
41
+ iat: number;
42
+ exp: number;
43
+ /** The identity key the site expects. A different wallet account is `wrong_account`. */
44
+ sub?: string;
45
+ require: AccessRequirement;
46
+ }
47
+ /** A key that locks some of the required token, proving control by signing the same challenge bytes. */
48
+ interface HoldingProof {
49
+ /** Compressed public key hex of the key locking the outputs. */
50
+ pubkey: string;
51
+ /** DER signature by that key over SHA-256(challenge bytes), as BRC-100 createSignature returns. */
52
+ signature: number[];
53
+ /** Optional hint: outpoints ('txid_vout') this key holds. Servers must not trust it. */
54
+ outpoints?: string[];
55
+ }
56
+ /** What the wallet (via requestAccess) posts back to the site. */
57
+ interface AccessResponse {
58
+ v: 1;
59
+ /** The challenge string exactly as issued. */
60
+ challenge: string;
61
+ identityKey: string;
62
+ /** DER signature: createSignature({ protocolID: ACCESS_PROTOCOL, keyID: nonce, counterparty: 'anyone', data: challenge bytes }). */
63
+ signature: number[];
64
+ holdings?: HoldingProof[];
65
+ }
66
+ type AccessErrorCode = 'not_installed' | 'user_rejected' | 'wrong_account' | 'insufficient_tokens' | 'identity_level_too_low' | 'kyc_required' | 'indexer_unavailable' | 'expired' | 'replay' | 'invalid_signature' | 'bad_request';
67
+ /** The HTTP status a site should answer each code with. */
68
+ declare const ACCESS_ERROR_STATUS: Record<AccessErrorCode, number>;
69
+ interface AccessErrorBody {
70
+ error: AccessErrorCode;
71
+ message: string;
72
+ /** insufficient_tokens: raw amounts; identity_level_too_low: levels. */
73
+ have?: string | number;
74
+ need?: string | number;
75
+ }
76
+ declare class AccessError extends Error {
77
+ readonly code: AccessErrorCode;
78
+ readonly have?: string | number;
79
+ readonly need?: string | number;
80
+ constructor(code: AccessErrorCode, message?: string, extra?: {
81
+ have?: string | number;
82
+ need?: string | number;
83
+ });
84
+ toJSON(): AccessErrorBody;
85
+ }
86
+ /** One video + poster + colour per kind, so wallets and sites show the same thing. Paths are inside the package. */
87
+ declare const PROTOCOL_MEDIA: Record<AccessKind, {
88
+ name: string;
89
+ color: string;
90
+ video: string;
91
+ poster: string;
92
+ }>;
93
+ /** Media URLs on a CDN (unpkg) for a given package version. */
94
+ declare function protocolMediaUrl(kind: AccessKind, which: 'video' | 'poster', version?: string): string;
95
+ /**
96
+ * Canonical JSON: object keys sorted by code unit, no whitespace, undefined members dropped.
97
+ * Only strings, finite numbers, booleans, null, arrays and plain objects are allowed.
98
+ */
99
+ declare function canonicalJson(v: unknown): string;
100
+ /** The bytes every signature in the protocol covers: UTF-8 of the challenge string. */
101
+ declare function challengeBytes(challenge: string): number[];
102
+ /** Throws AccessError('bad_request') if the requirement is malformed. Returns a normalised copy. */
103
+ declare function normaliseRequirement(r: unknown): AccessRequirement;
104
+ /** The kind a requirement implies: any token → 403; otherwise 401. */
105
+ declare function kindFor(r: AccessRequirement): AccessKind;
106
+ /** Parse and validate a challenge string. Throws AccessError('bad_request'). Does not check expiry. */
107
+ declare function parseAccessChallenge(s: unknown): AccessChallenge;
108
+ /** Raw amount needed for a token requirement, given total supply (needed only for minBps). Rounds up. */
109
+ declare function tokenNeedRaw(t: TokenRequirement, supplyRaw?: string): string;
110
+ /** Compare unscaled integer strings. Fails closed on malformed input. */
111
+ declare function atLeastRaw(have: string, need: string): boolean;
112
+ /** A user-readable line for an error code. */
113
+ declare function accessErrorMessage(code: AccessErrorCode, e?: {
114
+ have?: string | number;
115
+ need?: string | number;
116
+ }): string;
117
+
118
+ export { AccessError as A, type HoldingProof as H, MAX_HOLDING_PROOFS as M, PROTOCOL_MEDIA as P, type TokenRequirement as T, type AccessRequirement as a, type AccessResponse as b, type AccessKind as c, ACCESS_PROTOCOL as d, ACCESS_CHALLENGE_LIFETIME_S as e, ACCESS_ERROR_STATUS as f, ACCESS_VERSION as g, type AccessChallenge as h, type AccessErrorBody as i, type AccessErrorCode as j, accessErrorMessage as k, atLeastRaw as l, canonicalJson as m, challengeBytes as n, kindFor as o, normaliseRequirement as p, parseAccessChallenge as q, protocolMediaUrl as r, tokenNeedRaw as t };