Skip to content

Commit a76db01

Browse files
authored
Harden and standardize Auth and Payment Express middleware (#361)
* Harden and standardize Express middleware packages * Resolve Sonar findings * Raise middleware patch coverage * Cover middleware security branches
1 parent 6c772b3 commit a76db01

59 files changed

Lines changed: 4291 additions & 2616 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/architecture/layers.md‎

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
id: architecture-layers
33
title: Stack Layers
44
kind: meta
5-
version: "n/a"
6-
last_updated: "2026-05-01"
7-
last_verified: "2026-05-01"
5+
version: 'n/a'
6+
last_updated: '2026-05-01'
7+
last_verified: '2026-05-01'
88
review_cadence_days: 30
99
status: stable
10-
tags: ["architecture", "layers"]
10+
tags: ['architecture', 'layers']
1111
---
1212

1313
# Stack Layers
@@ -20,14 +20,14 @@ The stack is organized in three primary layers with supporting components that c
2020

2121
These packages are not a fourth vertical layer. They sit alongside the main stack and usually connect through the BRC-100 wallet interface, `@bsv/sdk` primitives, or direct service APIs.
2222

23-
| Capability | Packages | What it provides | Connects through |
24-
|------------|----------|------------------|------------------|
25-
| Data overlays | @bsv/overlay, @bsv/overlay-express, @bsv/overlay-topics | Topic managers, lookup services, SHIP/SLAP discovery, and shared on-chain context | `@bsv/sdk` transactions, overlay services |
26-
| P2P messaging and payments | @bsv/message-box-client, @bsv/authsocket, @bsv/paymail | Store-and-forward encrypted messages, live authenticated channels, BRC-29 payment derivation | BRC-100 wallet, BRC-103/104 auth |
27-
| Identity | @bsv/auth-express-middleware, @bsv/authsocket | BRC-31 HTTP handshake and peer identity framework | BRC-100 wallet, BRC-103/104 peer framework |
28-
| Monetization | @bsv/402-pay, @bsv/payment-express-middleware | HTTP 402 payment-gated APIs and micropayment middleware | BRC-100 wallet, BRC-121 HTTP 402 |
29-
| Tokens | @bsv/btms, @bsv/btms-permission-module | Token metadata, wallet permission display context, and BRC-48 PushDrop flows | BRC-100 wallet, overlay topics |
30-
| Storage | @bsv/overlay-topics, UHRP | Universal Hash Resolution for content-addressed files | UHRP servers, BRC-26 |
23+
| Capability | Packages | What it provides | Connects through |
24+
| -------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
25+
| Data overlays | @bsv/overlay, @bsv/overlay-express, @bsv/overlay-topics | Topic managers, lookup services, SHIP/SLAP discovery, and shared on-chain context | `@bsv/sdk` transactions, overlay services |
26+
| P2P messaging and payments | @bsv/message-box-client, @bsv/authsocket, @bsv/paymail | Store-and-forward encrypted messages, live authenticated channels, BRC-29 payment derivation | BRC-100 wallet, BRC-103/104 auth |
27+
| Identity | @bsv/auth-express-middleware, @bsv/authsocket | BRC-31 HTTP handshake and peer identity framework | BRC-100 wallet, BRC-103/104 peer framework |
28+
| Monetization | @bsv/402-pay, @bsv/payment-express-middleware | BRC-121 HTTP 402 plus the distinct legacy authenticated payment flow | BRC-100 wallet; BRC-121 for `402-pay`; BRC-103/104 + BRC-29 for PaymentExpress |
29+
| Tokens | @bsv/btms, @bsv/btms-permission-module | Token metadata, wallet permission display context, and BRC-48 PushDrop flows | BRC-100 wallet, overlay topics |
30+
| Storage | @bsv/overlay-topics, UHRP | Universal Hash Resolution for content-addressed files | UHRP servers, BRC-26 |
3131

3232
## Foundation: @bsv/sdk
3333

@@ -57,17 +57,17 @@ Any BRC-100-compliant wallet can be swapped below this boundary without changing
5757

5858
`@bsv/wallet-toolbox` is not a single monolithic wallet. It is a set of composable modules:
5959

60-
| Module | Role |
61-
|--------|------|
62-
| `WalletStorageManager` | Orchestrates persistence providers (active + backup + incremental sync) |
63-
| `KnexWalletStorage` | SQL persistence (SQLite, MySQL, PostgreSQL) |
64-
| `IndexedDBWalletStorage` | Browser persistence |
65-
| `RemoteWalletStorage` | Remote storage over HTTPS (for relay setups) |
66-
| `Monitor` | Background daemon: confirms transactions, acquires Merkle proofs, rebroadcasts stalled txs |
67-
| `PrivilegedKeyManager` | Handles the privileged-mode keyring for identity operations |
68-
| `ShamirWalletManager` | Shamir secret sharing for key backup |
69-
| `WalletSigner` | Bridges BRC-100 `signAction` to `@bsv/sdk` signing internals |
70-
| `Services` | Container wiring ARC, Chaintracks, WhatsOnChain |
60+
| Module | Role |
61+
| ------------------------ | ------------------------------------------------------------------------------------------ |
62+
| `WalletStorageManager` | Orchestrates persistence providers (active + backup + incremental sync) |
63+
| `KnexWalletStorage` | SQL persistence (SQLite, MySQL, PostgreSQL) |
64+
| `IndexedDBWalletStorage` | Browser persistence |
65+
| `RemoteWalletStorage` | Remote storage over HTTPS (for relay setups) |
66+
| `Monitor` | Background daemon: confirms transactions, acquires Merkle proofs, rebroadcasts stalled txs |
67+
| `PrivilegedKeyManager` | Handles the privileged-mode keyring for identity operations |
68+
| `ShamirWalletManager` | Shamir secret sharing for key backup |
69+
| `WalletSigner` | Bridges BRC-100 `signAction` to `@bsv/sdk` signing internals |
70+
| `Services` | Container wiring ARC, Chaintracks, WhatsOnChain |
7171

7272
Wallet builders compose these into a BRC-100 `Wallet`. The resulting object is the same interface that `@bsv/simple` and other app-layer packages use.
7373

‎docs/guides/http-402-payments.md‎

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
id: guide-http-402
3-
title: "HTTP 402 Payment Gating"
3+
title: 'HTTP 402 Payment Gating'
44
kind: guide
5-
version: "1.0.1"
6-
last_updated: "2026-07-26"
7-
last_verified: "2026-07-26"
5+
version: '1.0.1'
6+
last_updated: '2026-07-26'
7+
last_verified: '2026-07-26'
88
review_cadence_days: 30
99
status: stable
1010
tags: [guide, payments, http-402, brc-121, monetization]
@@ -20,6 +20,7 @@ tags: [guide, payments, http-402, brc-121, monetization]
2020
## What you'll build
2121

2222
A complete end-to-end payment system:
23+
2324
- **Server**: Express middleware that gates endpoints by payment, validates transactions
2425
- **Client**: Auto-paying fetch wrapper that transparently handles 402 responses
2526
- **Flow**: Client requests resource → server responds 402 with payment details → client signs & pays → access granted
@@ -85,31 +86,31 @@ async function setupServer() {
8586
network: 'main',
8687
storageUrl: 'https://store-us-1.bsvb.tech'
8788
})
88-
89+
8990
// 2. Create payment middleware
9091
const paymentMiddleware = createPaymentMiddleware({
9192
wallet,
92-
calculatePrice: (path) => {
93+
calculatePrice: path => {
9394
// Dynamic pricing by route
94-
if (path === '/api/premium') return 1000 // 1000 satoshis
95-
if (path === '/api/free') return 0 // Free
96-
return 100 // Default 100 satoshis
95+
if (path === '/api/premium') return 1000 // 1000 satoshis
96+
if (path === '/api/free') return 0 // Free
97+
return 100 // Default 100 satoshis
9798
}
9899
})
99-
100+
100101
// 3. Build Express app
101102
const app = express()
102103
app.use(bodyParser.json())
103104
app.use(paymentMiddleware)
104-
105+
105106
// 4. Define routes
106107
app.get('/api/free', (req, res) => {
107108
res.json({
108109
message: 'Free content',
109110
paid: false
110111
})
111112
})
112-
113+
113114
app.get('/api/premium', (req, res) => {
114115
res.json({
115116
message: 'Premium content — paid!',
@@ -118,15 +119,15 @@ async function setupServer() {
118119
sender: req.payment?.senderIdentityKey
119120
})
120121
})
121-
122+
122123
return app
123124
}
124125

125126
// Start server
126127
async function main() {
127128
const app = await setupServer()
128129
const port = process.env.PORT || 3000
129-
130+
130131
app.listen(port, () => {
131132
console.log(`Payment server running on http://localhost:${port}`)
132133
console.log('- GET /api/free: No payment required')
@@ -138,6 +139,7 @@ main().catch(console.error)
138139
```
139140

140141
The middleware chain:
142+
141143
1. **Payment middleware** checks if payment is required and validates provided payment headers
142144
2. **Route handler** receives the request only if payment was accepted or the route is free
143145

@@ -146,6 +148,7 @@ The middleware chain:
146148
When client requests a paid endpoint without payment, the server responds with 402 and payment details.
147149

148150
The payment middleware automatically:
151+
149152
- Calculates required satoshis via `calculatePrice()`
150153
- Returns 402 status if unpaid
151154
- Includes headers:
@@ -166,18 +169,19 @@ dotenv.config()
166169
export async function setupPaymentClient() {
167170
// Initialize wallet for signing payments
168171
const wallet = new WalletClient()
169-
172+
170173
// Create a fetch wrapper that auto-handles 402
171174
const fetch402 = create402Fetch({
172175
wallet,
173-
cacheTimeoutMs: 30 * 60 * 1000 // Cache paid content for 30 minutes
176+
cacheTimeoutMs: 30 * 60 * 1000 // Cache paid content for 30 minutes
174177
})
175-
178+
176179
return fetch402
177180
}
178181
```
179182

180183
`create402Fetch()` returns a fetch-compatible function that:
184+
181185
- Catches 402 responses
182186
- Signs a payment transaction
183187
- Retries with payment headers
@@ -191,7 +195,7 @@ Create `requests.ts` in client:
191195
export async function accessFreeContent(fetch402: any) {
192196
const response = await fetch402('http://localhost:3000/api/free')
193197
const data = await response.json()
194-
198+
195199
console.log('Free content:', data)
196200
return data
197201
}
@@ -200,7 +204,7 @@ export async function accessPremiumContent(fetch402: any) {
200204
// Automatically handles 402 and pays
201205
const response = await fetch402('http://localhost:3000/api/premium')
202206
const data = await response.json()
203-
207+
204208
console.log('Premium content:', data)
205209
return data
206210
}
@@ -213,6 +217,7 @@ export async function clearPaymentCache(fetch402: any) {
213217
```
214218

215219
When client calls `fetch402('/api/premium')`:
220+
216221
1. Sends request (no payment)
217222
2. Receives 402 with `x-bsv-sats: 1000` and `x-bsv-server`
218223
3. Generates a fresh nonce and timestamp, then derives the payment address with BRC-29
@@ -235,20 +240,16 @@ export async function manualPayment(
235240
serverIdentityKey: string
236241
) {
237242
// Build payment headers manually
238-
const headers = await constructPaymentHeaders(
239-
wallet,
240-
serverUrl,
241-
satoshis,
242-
serverIdentityKey
243-
)
244-
243+
const headers = await constructPaymentHeaders(wallet, serverUrl, satoshis, serverIdentityKey)
244+
245245
// Use with custom fetch
246246
const response = await fetch(serverUrl, { headers })
247247
return response
248248
}
249249
```
250250

251251
Manual header construction gives you:
252+
252253
- `x-bsv-beef`: Base64-encoded transaction proof
253254
- `x-bsv-sender`: Your identity public key
254255
- `x-bsv-nonce`: Derivation prefix (random 8 bytes)
@@ -272,35 +273,35 @@ async function main() {
272273
network: 'main',
273274
storageUrl: 'https://store-us-1.bsvb.tech'
274275
})
275-
276+
276277
// Setup middleware
277278
const paymentMiddleware = createPaymentMiddleware({
278279
wallet,
279-
calculatePrice: (path) => {
280+
calculatePrice: path => {
280281
if (path === '/api/premium') return 1000
281282
if (path === '/api/free') return 0
282283
return 100
283284
}
284285
})
285-
286+
286287
// Create app
287288
const app = express()
288289
app.use(bodyParser.json())
289290
app.use(paymentMiddleware)
290-
291+
291292
// Routes
292293
app.get('/api/free', (req, res) => {
293294
res.json({ message: 'Free content', paid: false })
294295
})
295-
296+
296297
app.get('/api/premium', (req, res) => {
297298
res.json({
298299
message: 'Premium content',
299300
paid: true,
300301
satoshisPaid: req.payment?.satoshisPaid || 0
301302
})
302303
})
303-
304+
304305
// Start
305306
app.listen(3000, () => {
306307
console.log('Server on http://localhost:3000')
@@ -319,23 +320,23 @@ import { WalletClient } from '@bsv/sdk'
319320
async function main() {
320321
// Setup wallet
321322
const wallet = new WalletClient()
322-
323+
323324
// Create auto-paying fetch
324325
const fetch402 = create402Fetch({
325326
wallet,
326327
cacheTimeoutMs: 30 * 60 * 1000
327328
})
328-
329+
329330
console.log('Accessing free content...')
330331
let response = await fetch402('http://localhost:3000/api/free')
331332
let data = await response.json()
332333
console.log('Free:', data)
333-
334+
334335
console.log('\nAccessing premium content (will auto-pay 1000 sats)...')
335336
response = await fetch402('http://localhost:3000/api/premium')
336337
data = await response.json()
337338
console.log('Premium:', data)
338-
339+
339340
console.log('\nAccessing premium again (cached, no new payment)...')
340341
response = await fetch402('http://localhost:3000/api/premium')
341342
data = await response.json()
@@ -378,7 +379,11 @@ npx ts-node main.ts
378379
→ If cache timeout is very long, client may use stale payments for different resources. Use moderate timeout (30 min is safe)
379380

380381
**"Replay protection"**
381-
→ Server wallet rejects duplicate payment internalization. Each payment must use a fresh nonce and transaction.
382+
→ For `@bsv/402-pay`, freshness fields bind the proof but the wallet must also
383+
avoid newly accepting the same transaction twice. For
384+
`@bsv/payment-express-middleware`, inject a durable atomic
385+
transaction-ID replay store in multi-process deployments; its derivation
386+
prefix is not a single-use replay database.
382387

383388
## What to read next
384389

0 commit comments

Comments
 (0)