@bwalletx/connect 0.3.0 → 0.4.1
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 +25 -0
- package/README.md +145 -4
- package/dist/bwalletx-connect.global.js +41 -3
- package/dist/bwalletx-connect.global.js.map +1 -1
- package/dist/chunk-7NAKOJGZ.js +442 -0
- package/dist/chunk-7NAKOJGZ.js.map +1 -0
- package/dist/chunk-FUQ46RJV.js +404 -0
- package/dist/chunk-FUQ46RJV.js.map +1 -0
- package/dist/index.cjs +425 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -62
- package/dist/index.d.ts +23 -62
- package/dist/index.js +35 -1
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +415 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +48 -0
- package/dist/next.d.ts +48 -0
- package/dist/next.js +107 -0
- package/dist/next.js.map +1 -0
- package/dist/react.cjs +586 -36
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +74 -28
- package/dist/react.d.ts +74 -28
- package/dist/react.js +175 -3
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +339 -4
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +204 -2
- package/dist/server.d.ts +204 -2
- package/dist/server.js +36 -107
- package/dist/server.js.map +1 -1
- package/dist/wallets-VOKIKBbO.d.cts +250 -0
- package/dist/wallets-VOKIKBbO.d.ts +250 -0
- package/package.json +38 -13
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.1 (2026-10-12)
|
|
4
|
+
|
|
5
|
+
- React: `BwalletXProvider` and `BwalletxSignin` declare `ReactElement` return types, so they type-check as JSX under React 18 `@types/react`.
|
|
6
|
+
- First public release of the 0.4 line (0.4.0 was staged on npm but never released).
|
|
7
|
+
|
|
8
|
+
## 0.4.0 (2026-10-12)
|
|
9
|
+
|
|
10
|
+
Everything from 0.3 still works the same way. All additions are BRC-100 first: bWalletX is the main target, but any BRC-100 wallet goes through the same interface.
|
|
11
|
+
|
|
12
|
+
- **React** (`@bwalletx/connect/react`): `<BwalletXProvider>` and `useBwalletX()` give you status, identityKey, handle and paymail, connect/disconnect, the BRC-100 `wallet`, and pay/tip/buyToken. Safe to server-render. `<BwalletxSignin>` is unchanged.
|
|
13
|
+
- **Next.js adapter** (`@bwalletx/connect/next`, new): `createBwalletXAuth()` gives you challenge, verify, logout and session route handlers, an HMAC-signed httpOnly session cookie, and `getSession()` for server components. The secret comes from `BWALLETX_SESSION_SECRET` (or `secret`). There is no default, so it throws if the secret is missing.
|
|
14
|
+
- **Nonce stores**: `redisStore` (Upstash, node-redis, ioredis) and `pgStore` (any `query` function), plus `pgNonceTableSql()`. `memoryStore` now warns when it runs in production.
|
|
15
|
+
- **Sessions**: `signSession`, `verifySession`, `serializeCookie` and `readCookie` (WebCrypto, so they work on Edge).
|
|
16
|
+
- **Payments**: `pay` (to a paymail, address or BRC-29 identity key; in sats or USD; optional OP_RETURN memo), `tip`, and a `buyToken` quote hook. Each makes one `createAction` and is never retried. Errors are `PaymentError` with a `code` and a `maybeSent` flag.
|
|
17
|
+
- **Server**: `verifyPayment` checks a txid, raw tx, BEEF or Atomic BEEF against the expected outputs, with optional WhatsOnChain or ARC status checks and SPV.
|
|
18
|
+
- **Account linking**: `linkIdentity`, `signInOrLink`, and `IdentityLinkConflictError` (the Next adapter turns it into a 409).
|
|
19
|
+
- **Wallet picker**: `discoverWallets` finds every BRC-100 wallet that announces itself, plus the in-app wallet, optional localhost desktop wallets and phone pairing. `pickWallet` shows the picker UI. `handcashAdapter` is a transitional slot that wraps an app's own HandCash login and imports no HandCash SDK.
|
|
20
|
+
- **Pairing QR modal**: `pairingModal()` and `qrSvg()`, using the bundled `qrcode-generator` (MIT).
|
|
21
|
+
- Tooling: ESLint (`pnpm lint`) and a CI workflow on pull requests.
|
|
22
|
+
|
|
23
|
+
## 0.3.0
|
|
24
|
+
|
|
25
|
+
- bApp shell: the `bapp.json` manifest, 5-slot bar, top bar, drawer and in-wallet bridge.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @bwalletx/connect
|
|
2
2
|
|
|
3
|
-
Add "Sign in with bWalletX" to a website.
|
|
3
|
+
Add "Sign in with bWalletX" to a website. Built on the open BRC-100 wallet interface, so every BRC-100 wallet works the same way, with bWalletX first.
|
|
4
4
|
|
|
5
5
|
The user's wallet signs a one-time challenge from your server with a key derived from its identity key. Your server checks the signature and signs the user in as that identity key. You don't need an API key or an account with us, and nothing passes through bwalletx.com. The only service of ours involved is the encrypted pairing relay (`relay.bwallet.space`), and only when the user signs in from a phone or the web wallet.
|
|
6
6
|
|
|
@@ -14,6 +14,144 @@ pnpm add @bwalletx/connect @bsv/sdk
|
|
|
14
14
|
|
|
15
15
|
`@bsv/sdk` (2.2 or later, 2.x or 3.x) is a peer dependency.
|
|
16
16
|
|
|
17
|
+
## Next.js quick start (App Router)
|
|
18
|
+
|
|
19
|
+
Works with bWalletX and any other BRC-100 wallet. Set a session secret (32+ characters) in your environment. There is no default: the routes throw without it.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
# .env.local (never commit it)
|
|
23
|
+
BWALLETX_SESSION_SECRET= # e.g. the output of: openssl rand -base64 48
|
|
24
|
+
APP_ORIGIN=https://example.com
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// lib/bwalletx.ts
|
|
29
|
+
import { createBwalletXAuth, pgStore, signInOrLink } from '@bwalletx/connect/next';
|
|
30
|
+
import { query } from './db'; // (sql, params) => Promise<{ rows }>
|
|
31
|
+
|
|
32
|
+
export const auth = createBwalletXAuth({
|
|
33
|
+
origin: process.env.APP_ORIGIN!,
|
|
34
|
+
store: pgStore({ query }), // or redisStore({ client: upstash })
|
|
35
|
+
// Optional: link the identity key to your user table.
|
|
36
|
+
onVerified: async (identityKey) => {
|
|
37
|
+
const r = await signInOrLink(identityKey, {
|
|
38
|
+
findUserByIdentityKey: async (k) => (await query('select user_id from wallet_links where identity_key = $1', [k])).rows[0]?.user_id ?? null,
|
|
39
|
+
attach: async (uid, k) => void (await query('insert into wallet_links (user_id, identity_key) values ($1, $2)', [uid, k])),
|
|
40
|
+
createUser: async () => (await query('insert into users default values returning id', [])).rows[0].id,
|
|
41
|
+
});
|
|
42
|
+
return { uid: r.userId ?? undefined };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// app/api/bwalletx/challenge/route.ts
|
|
49
|
+
import { auth } from '@/lib/bwalletx';
|
|
50
|
+
export const POST = auth.challenge;
|
|
51
|
+
// app/api/bwalletx/verify/route.ts → export const POST = auth.verify;
|
|
52
|
+
// app/api/bwalletx/logout/route.ts → export const POST = auth.logout;
|
|
53
|
+
// app/api/bwalletx/session/route.ts → export const GET = auth.session;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// any server component or route handler
|
|
58
|
+
const session = await auth.getSession(); // { sub: identityKey, uid?, handle?, paymail?, exp } | null
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The cookie (`bwx_session`) is `HttpOnly; SameSite=Lax; Secure` (Secure when the origin is https), HMAC-SHA256 signed, 7 days by default (`maxAge`). Both POST routes check the `Origin` header against `origin`. `signSession` / `verifySession` are also exported from `@bwalletx/connect/server` if you want the token without the adapter.
|
|
62
|
+
|
|
63
|
+
Create the nonce table once (a migration): `pgNonceTableSql()` returns the SQL.
|
|
64
|
+
|
|
65
|
+
## React quick start
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
'use client';
|
|
69
|
+
import { BwalletXProvider, useBwalletX } from '@bwalletx/connect/react';
|
|
70
|
+
|
|
71
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
72
|
+
return (
|
|
73
|
+
<BwalletXProvider
|
|
74
|
+
challengeUrl="/api/bwalletx/challenge"
|
|
75
|
+
verifyUrl="/api/bwalletx/verify"
|
|
76
|
+
logoutUrl="/api/bwalletx/logout"
|
|
77
|
+
sessionUrl="/api/bwalletx/session">
|
|
78
|
+
{children}
|
|
79
|
+
</BwalletXProvider>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Account() {
|
|
84
|
+
const { status, identityKey, handle, connect, disconnect, tip } = useBwalletX();
|
|
85
|
+
if (status !== 'connected') return <button onClick={() => connect()}>Connect wallet</button>;
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<span>{handle ?? identityKey?.slice(0, 10)}</span>
|
|
89
|
+
<button onClick={() => tip({ to: 'author@example.com', usd: 0.05 })}>Tip 5¢</button>
|
|
90
|
+
<button onClick={disconnect}>Sign out</button>
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`useBwalletX()` gives `status` (`idle | restoring | connecting | connected | error`), `identityKey`, `handle`, `paymail`, `wallet` (the BRC-100 `WalletInterface`), `walletId`, `signedIn`, `error`, `wallets`, `connect({ walletId? })`, `disconnect()`, `listWallets()`, `pay`, `tip` and `buyToken`.
|
|
97
|
+
|
|
98
|
+
`connect()` finds wallets, shows the built-in picker when there is more than one (bWalletX first), shows the pairing QR modal for phones, and signs in through your routes when `challengeUrl` and `verifyUrl` are set. Nothing touches `window` during render, so the provider is safe in server-rendered pages. After a reload the session comes back from `sessionUrl`, but `wallet` stays `null` until the user calls `connect()` again.
|
|
99
|
+
|
|
100
|
+
## Payments
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { pay, tip, buyToken } from '@bwalletx/connect';
|
|
104
|
+
|
|
105
|
+
await pay(wallet, { to: 'alice@example.com', sats: 5000, memo: 'Album' }); // paymail
|
|
106
|
+
await pay(wallet, { to: '1Abc...', usd: 1 }); // address; WhatsOnChain rate, or pass usdPerBsv
|
|
107
|
+
const r = await pay(wallet, { to: identityKeyHex, sats: 100 }); // BRC-29; r.remittance goes to the recipient
|
|
108
|
+
await buyToken(wallet, { token: '$BOASE', amount: 100, quote: (q) => fetch('/api/quote', { method: 'POST', body: JSON.stringify(q) }).then((x) => x.json()) });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
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
|
+
|
|
113
|
+
On the server, check the payment before you deliver anything:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { verifyPayment } from '@bwalletx/connect/server';
|
|
117
|
+
|
|
118
|
+
const r = await verifyPayment({
|
|
119
|
+
tx: body.tx ?? body.txid, // Atomic BEEF / BEEF / raw hex, or a txid (fetched from WhatsOnChain)
|
|
120
|
+
expected: [{ address: SELLER_ADDRESS, satoshis: price }], // or { script: hex, satoshis }
|
|
121
|
+
requireSeen: true, // status from WhatsOnChain; or status: 'arc', arcUrl, arcApiKey
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Every expected output has to be matched by a different output that carries at least the expected amount. Otherwise `verifyPayment` throws a `PaymentVerificationError` with a `code` (`OUTPUT_MISSING`, `TX_NOT_FOUND`, `NOT_SEEN`, `REJECTED`, `SPV_FAILED`, `BAD_TX`, `NETWORK`). Pass `spv: chainTracker` to check the merkle proofs in a BEEF as well.
|
|
126
|
+
|
|
127
|
+
## Wallets and the picker
|
|
128
|
+
|
|
129
|
+
`discoverWallets()` lists everything it can find, with bWalletX first:
|
|
130
|
+
|
|
131
|
+
- every wallet that announces itself through BRC-100 discovery (bWalletX, Yours, Metanet and others)
|
|
132
|
+
- the in-app browser's `window.CWI`
|
|
133
|
+
- desktop wallets on localhost ports 3321 and 3301 (only with `probeLocal: true`)
|
|
134
|
+
- phone pairing (`pairing: pairingModal()`)
|
|
135
|
+
- any adapters you add in `extra`
|
|
136
|
+
|
|
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.
|
|
138
|
+
|
|
139
|
+
## Migrating from HandCash Connect
|
|
140
|
+
|
|
141
|
+
1. Keep your HandCash login for now and put it behind the same picker:
|
|
142
|
+
```ts
|
|
143
|
+
import { handcashAdapter } from '@bwalletx/connect';
|
|
144
|
+
const handcash = handcashAdapter({
|
|
145
|
+
login: () => myExistingHandCashLogin(), // resolves { handle, paymail? } once signed in
|
|
146
|
+
pay: (req) => fetch('/api/handcash/pay', { method: 'POST', body: JSON.stringify(req) }).then((r) => r.json()),
|
|
147
|
+
});
|
|
148
|
+
<BwalletXProvider extraWallets={[handcash]} ... />
|
|
149
|
+
```
|
|
150
|
+
**This adapter is transitional.** It wraps your own HandCash code; this package does not import any HandCash SDK. HandCash announced BRC-100 support in October 2026. Once that ships, HandCash shows up as an ordinary BRC-100 wallet and you can delete the adapter.
|
|
151
|
+
2. Let existing users link their wallet. When someone already signed in with HandCash connects bWalletX, call `signInOrLink(identityKey, { currentUserId, ... })` (or `linkIdentity`) in `onVerified`. This links the identity to their existing account instead of creating a new one. If the identity is already linked to someone else, `IdentityLinkConflictError` turns into a 409 response.
|
|
152
|
+
3. Replace HandCash payment calls with `pay` / `tip` / `buyToken`, and check them on the server with `verifyPayment` instead of HandCash payment webhooks.
|
|
153
|
+
4. Remove the adapter when your users have moved over.
|
|
154
|
+
|
|
17
155
|
## The button
|
|
18
156
|
|
|
19
157
|
`<bwalletx-signin>` is the standard "Sign in with bWalletX" button: a web component with no dependencies (Shadow DOM), plus a React wrapper. It is the gold button from bit-sign's sign-in card.
|
|
@@ -108,7 +246,10 @@ interface NonceStore {
|
|
|
108
246
|
|
|
109
247
|
`take` must read and delete in one step. `verifySignIn` calls it before checking anything else, so each nonce gets exactly one attempt.
|
|
110
248
|
|
|
111
|
-
`memoryStore()` keeps challenges in the process. It is lost on restart and isn't shared between processes or serverless instances, so it prints a warning the first time it is used. If you leave out `store`, a shared in-memory store is used and you get the same warning. In production,
|
|
249
|
+
`memoryStore()` keeps challenges in the process. It is lost on restart and isn't shared between processes or serverless instances, so it prints a warning the first time it is used. If you leave out `store`, a shared in-memory store is used and you get the same warning. In production it warns louder. Use one of the built-in shared stores, which need no driver dependency because you pass your own client:
|
|
250
|
+
|
|
251
|
+
- `redisStore({ client })`: Upstash, Vercel KV or node-redis as they are, or ioredis with `flavor: 'ioredis'`. It uses `GETDEL`, so you need Redis 6.2 or later.
|
|
252
|
+
- `pgStore({ query })`: any `(sql, params)` function (node-postgres, postgres.js, Neon). It deletes each nonce with `DELETE ... RETURNING`, and `pgNonceTableSql()` gives you the table.
|
|
112
253
|
|
|
113
254
|
Also rate-limit the challenge route per IP. It is unauthenticated.
|
|
114
255
|
|
|
@@ -238,10 +379,10 @@ API: `validateManifest(json)` → `{ok, manifest, errors}`, `parseManifest`, `lo
|
|
|
238
379
|
|
|
239
380
|
## Limits
|
|
240
381
|
|
|
241
|
-
-
|
|
382
|
+
- Sign-in proves the identity key and nothing else: no handle, paymail or name. Use `onVerified` to look them up and add them yourself.
|
|
242
383
|
- The identity key is the same on every site that uses this method, so two sites can tell it is the same wallet.
|
|
243
384
|
- `relay.bwallet.space` is run by The Bitcoin Corporation. It has no published uptime guarantee and allows about 60 frames a minute per channel.
|
|
244
|
-
- There is no hosted sign-in page or OAuth-style redirect. You run the
|
|
385
|
+
- There is no hosted sign-in page or OAuth-style redirect (auth.bwalletx.com is planned). You run the routes yourself; `@bwalletx/connect/next` gives you ready-made ones.
|
|
245
386
|
|
|
246
387
|
## Testing
|
|
247
388
|
|