@bsv/402-pay 0.1.0

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/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @bsv/402-pay
2
+
3
+ [BRC-121](https://github.com/bitcoin-sv/BRCs/blob/master/payments/0121.md) Simple 402 Payments -- server middleware and client for BSV micropayments over HTTP.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @bsv/402-pay
9
+ ```
10
+
11
+ Peer dependency: `@bsv/sdk >= 2.0.0`
12
+
13
+ ## Server
14
+
15
+ ### Express middleware
16
+
17
+ ```ts
18
+ import express from 'express'
19
+ import { createPaymentMiddleware } from '@bsv/402-pay/server'
20
+
21
+ const app = express()
22
+
23
+ app.use('/articles/:slug', createPaymentMiddleware({
24
+ wallet, // WalletInterface from @bsv/sdk
25
+ calculatePrice: (path) => {
26
+ // Return price in satoshis, or undefined to skip payment
27
+ return 100
28
+ }
29
+ }))
30
+
31
+ app.get('/articles/:slug', (req, res) => {
32
+ // req.payment is set if payment was accepted
33
+ res.send('Paid content here')
34
+ })
35
+ ```
36
+
37
+ ### Low-level validation
38
+
39
+ ```ts
40
+ import { validatePayment, send402 } from '@bsv/402-pay/server'
41
+
42
+ // In any HTTP handler:
43
+ const result = await validatePayment(req, wallet)
44
+ if (!result) {
45
+ send402(res, serverIdentityKey, 100)
46
+ return
47
+ }
48
+ // Payment accepted — serve content
49
+ ```
50
+
51
+ ## Client
52
+
53
+ ### Fetch wrapper
54
+
55
+ ```ts
56
+ import { create402Fetch } from '@bsv/402-pay/client'
57
+
58
+ const fetch402 = create402Fetch({ wallet })
59
+
60
+ // Automatically handles 402 responses with payment
61
+ const response = await fetch402('https://example.com/articles/foo')
62
+ const html = await response.text()
63
+
64
+ // Clear the payment cache
65
+ fetch402.clearCache()
66
+ ```
67
+
68
+ ## Headers
69
+
70
+ | Header | Direction | Description |
71
+ |---|---|---|
72
+ | `x-bsv-sats` | Server → Client | Required satoshi amount |
73
+ | `x-bsv-server` | Server → Client | Server identity public key |
74
+ | `x-bsv-beef` | Client → Server | Base64-encoded BEEF transaction |
75
+ | `x-bsv-sender` | Client → Server | Client identity public key |
76
+ | `x-bsv-nonce` | Client → Server | Base64-encoded derivation prefix |
77
+ | `x-bsv-time` | Client → Server | Unix millisecond timestamp |
78
+ | `x-bsv-vout` | Client → Server | Payment output index |
79
+
80
+ ## Replay Protection
81
+
82
+ Two mechanisms prevent replay attacks:
83
+
84
+ 1. **Timestamp freshness** -- `x-bsv-time` must be within 30 seconds of the server's clock
85
+ 2. **Transaction uniqueness** -- `internalizeAction` returns `isMerge: true` for previously seen transactions
86
+
87
+ ## License
88
+
89
+ See LICENSE
@@ -0,0 +1,25 @@
1
+ import type { WalletInterface } from '@bsv/sdk';
2
+ export interface Payment402Options {
3
+ /** The client's wallet instance */
4
+ wallet: WalletInterface;
5
+ /** Cache timeout in milliseconds for paid content (default: 30 minutes) */
6
+ cacheTimeoutMs?: number;
7
+ }
8
+ /**
9
+ * Creates a fetch wrapper that automatically handles 402 Payment Required responses.
10
+ *
11
+ * When a 402 is received, the wrapper constructs a BRC-121 payment using the
12
+ * provided wallet and retransmits the request with payment headers.
13
+ *
14
+ * Usage:
15
+ * ```ts
16
+ * import { create402Fetch } from '@bsv/402-pay/client'
17
+ *
18
+ * const fetch402 = create402Fetch({ wallet })
19
+ * const response = await fetch402('https://example.com/articles/foo')
20
+ * ```
21
+ */
22
+ export declare function create402Fetch(options: Payment402Options): ((url: string, init?: RequestInit) => Promise<Response>) & {
23
+ clearCache: () => void;
24
+ };
25
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG/C,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,MAAM,EAAE,eAAe,CAAA;IACvB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAQD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,UAY1B,MAAM,SAAQ,WAAW,KAAQ,OAAO,CAAC,QAAQ,CAAC;;EAiGhF"}
package/dist/client.js ADDED
@@ -0,0 +1,111 @@
1
+ import { PublicKey, Utils, Random } from '@bsv/sdk';
2
+ import { BRC29_PROTOCOL_ID, HEADERS } from './constants.js';
3
+ /**
4
+ * Creates a fetch wrapper that automatically handles 402 Payment Required responses.
5
+ *
6
+ * When a 402 is received, the wrapper constructs a BRC-121 payment using the
7
+ * provided wallet and retransmits the request with payment headers.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { create402Fetch } from '@bsv/402-pay/client'
12
+ *
13
+ * const fetch402 = create402Fetch({ wallet })
14
+ * const response = await fetch402('https://example.com/articles/foo')
15
+ * ```
16
+ */
17
+ export function create402Fetch(options) {
18
+ const { wallet, cacheTimeoutMs = 30 * 60 * 1000 } = options;
19
+ const cache = new Map();
20
+ /**
21
+ * Clears the payment cache. Call this when the user clears history
22
+ * or when you want to force re-payment.
23
+ */
24
+ function clearCache() {
25
+ cache.clear();
26
+ }
27
+ async function fetch402(url, init = {}) {
28
+ // Check cache
29
+ const cached = cache.get(url);
30
+ if (cached && (Date.now() - cached.timestamp) < cacheTimeoutMs) {
31
+ return new Response(cached.body, {
32
+ status: cached.response.status,
33
+ headers: cached.response.headers
34
+ });
35
+ }
36
+ // Initial request
37
+ const res = await fetch(url, init);
38
+ if (res.status !== 402) {
39
+ return res;
40
+ }
41
+ // Read 402 headers
42
+ const satsHeader = res.headers.get(HEADERS.SATS);
43
+ const serverHeader = res.headers.get(HEADERS.SERVER);
44
+ if (!satsHeader || !serverHeader)
45
+ return res;
46
+ const satoshis = Number.parseInt(satsHeader);
47
+ if (isNaN(satoshis) || satoshis <= 0)
48
+ return res;
49
+ // Construct payment
50
+ const serverIdentityKey = serverHeader;
51
+ const nonce = Utils.toBase64(Random(8));
52
+ const time = String(Date.now());
53
+ const timeSuffixB64 = Buffer.from(time).toString('base64');
54
+ const originator = new URL(url).origin;
55
+ // Derive recipient public key via BRC-42
56
+ const { publicKey: derivedPubKey } = await wallet.getPublicKey({
57
+ protocolID: BRC29_PROTOCOL_ID,
58
+ keyID: `${nonce} ${timeSuffixB64}`,
59
+ counterparty: serverIdentityKey
60
+ }, originator);
61
+ const pkh = PublicKey.fromString(derivedPubKey).toHash('hex');
62
+ // Get sender identity key
63
+ const { publicKey: senderIdentityKey } = await wallet.getPublicKey({ identityKey: true }, originator);
64
+ // Create payment transaction
65
+ const actionResult = await wallet.createAction({
66
+ description: `Paid Content: ${new URL(url).pathname}`,
67
+ outputs: [{
68
+ satoshis,
69
+ lockingScript: `76a914${pkh}88ac`,
70
+ outputDescription: '402 web payment',
71
+ customInstructions: JSON.stringify({
72
+ derivationPrefix: nonce,
73
+ derivationSuffix: timeSuffixB64,
74
+ serverIdentityKey
75
+ }),
76
+ tags: ['402-payment']
77
+ }],
78
+ labels: ['402-payment'],
79
+ options: { randomizeOutputs: false }
80
+ }, originator);
81
+ const txBase64 = Utils.toBase64(actionResult.tx);
82
+ // Retransmit with payment headers
83
+ const paidRes = await fetch(url, {
84
+ ...init,
85
+ headers: {
86
+ ...init.headers,
87
+ [HEADERS.BEEF]: txBase64,
88
+ [HEADERS.SENDER]: senderIdentityKey,
89
+ [HEADERS.NONCE]: nonce,
90
+ [HEADERS.TIME]: time,
91
+ [HEADERS.VOUT]: '0'
92
+ }
93
+ });
94
+ // Cache successful responses
95
+ if (paidRes.ok) {
96
+ const body = await paidRes.text();
97
+ cache.set(url, {
98
+ response: paidRes,
99
+ body,
100
+ timestamp: Date.now()
101
+ });
102
+ return new Response(body, {
103
+ status: paidRes.status,
104
+ headers: paidRes.headers
105
+ });
106
+ }
107
+ return paidRes;
108
+ }
109
+ return Object.assign(fetch402, { clearCache });
110
+ }
111
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { WalletProtocol } from '@bsv/sdk';
2
+ /** BRC-29 protocol ID for key derivation */
3
+ export declare const BRC29_PROTOCOL_ID: WalletProtocol;
4
+ /** Header prefix for all BRC-121 headers */
5
+ export declare const HEADER_PREFIX = "x-bsv-";
6
+ /** Header names */
7
+ export declare const HEADERS: {
8
+ /** Server → Client: required satoshi amount */
9
+ readonly SATS: "x-bsv-sats";
10
+ /** Server → Client: server identity public key */
11
+ readonly SERVER: "x-bsv-server";
12
+ /** Client → Server: base64-encoded BEEF transaction */
13
+ readonly BEEF: "x-bsv-beef";
14
+ /** Client → Server: client identity public key */
15
+ readonly SENDER: "x-bsv-sender";
16
+ /** Client → Server: base64-encoded derivation prefix */
17
+ readonly NONCE: "x-bsv-nonce";
18
+ /** Client → Server: Unix millisecond timestamp */
19
+ readonly TIME: "x-bsv-time";
20
+ /** Client → Server: output index (decimal string) */
21
+ readonly VOUT: "x-bsv-vout";
22
+ };
23
+ /** Default payment window: 30 seconds */
24
+ export declare const DEFAULT_PAYMENT_WINDOW_MS = 30000;
25
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAE9C,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,EAAE,cAAoC,CAAA;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,aAAa,WAAW,CAAA;AAErC,mBAAmB;AACnB,eAAO,MAAM,OAAO;IAClB,+CAA+C;;IAE/C,kDAAkD;;IAElD,uDAAuD;;IAEvD,kDAAkD;;IAElD,wDAAwD;;IAExD,kDAAkD;;IAElD,qDAAqD;;CAE7C,CAAA;AAEV,yCAAyC;AACzC,eAAO,MAAM,yBAAyB,QAAS,CAAA"}
@@ -0,0 +1,24 @@
1
+ /** BRC-29 protocol ID for key derivation */
2
+ export const BRC29_PROTOCOL_ID = [2, '3241645161d8'];
3
+ /** Header prefix for all BRC-121 headers */
4
+ export const HEADER_PREFIX = 'x-bsv-';
5
+ /** Header names */
6
+ export const HEADERS = {
7
+ /** Server → Client: required satoshi amount */
8
+ SATS: `${HEADER_PREFIX}sats`,
9
+ /** Server → Client: server identity public key */
10
+ SERVER: `${HEADER_PREFIX}server`,
11
+ /** Client → Server: base64-encoded BEEF transaction */
12
+ BEEF: `${HEADER_PREFIX}beef`,
13
+ /** Client → Server: client identity public key */
14
+ SENDER: `${HEADER_PREFIX}sender`,
15
+ /** Client → Server: base64-encoded derivation prefix */
16
+ NONCE: `${HEADER_PREFIX}nonce`,
17
+ /** Client → Server: Unix millisecond timestamp */
18
+ TIME: `${HEADER_PREFIX}time`,
19
+ /** Client → Server: output index (decimal string) */
20
+ VOUT: `${HEADER_PREFIX}vout`
21
+ };
22
+ /** Default payment window: 30 seconds */
23
+ export const DEFAULT_PAYMENT_WINDOW_MS = 30_000;
24
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA,4CAA4C;AAC5C,MAAM,CAAC,MAAM,iBAAiB,GAAmB,CAAC,CAAC,EAAE,cAAc,CAAC,CAAA;AAEpE,4CAA4C;AAC5C,MAAM,CAAC,MAAM,aAAa,GAAG,QAAQ,CAAA;AAErC,mBAAmB;AACnB,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,+CAA+C;IAC/C,IAAI,EAAE,GAAG,aAAa,MAAM;IAC5B,kDAAkD;IAClD,MAAM,EAAE,GAAG,aAAa,QAAQ;IAChC,uDAAuD;IACvD,IAAI,EAAE,GAAG,aAAa,MAAM;IAC5B,kDAAkD;IAClD,MAAM,EAAE,GAAG,aAAa,QAAQ;IAChC,wDAAwD;IACxD,KAAK,EAAE,GAAG,aAAa,OAAO;IAC9B,kDAAkD;IAClD,IAAI,EAAE,GAAG,aAAa,MAAM;IAC5B,qDAAqD;IACrD,IAAI,EAAE,GAAG,aAAa,MAAM;CACpB,CAAA;AAEV,yCAAyC;AACzC,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAA"}
@@ -0,0 +1,6 @@
1
+ export { HEADERS, HEADER_PREFIX, BRC29_PROTOCOL_ID, DEFAULT_PAYMENT_WINDOW_MS } from './constants.js';
2
+ export { createPaymentMiddleware, validatePayment, send402 } from './server.js';
3
+ export type { PaymentMiddlewareOptions, PaymentResult, PaymentRequest, PaymentResponse } from './server.js';
4
+ export { create402Fetch } from './client.js';
5
+ export type { Payment402Options } from './client.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAA;AACrG,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC/E,YAAY,EAAE,wBAAwB,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC3G,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // BRC-121: Simple 402 Payments
2
+ // https://github.com/bitcoin-sv/BRCs/blob/master/payments/0121.md
3
+ export { HEADERS, HEADER_PREFIX, BRC29_PROTOCOL_ID, DEFAULT_PAYMENT_WINDOW_MS } from './constants.js';
4
+ export { createPaymentMiddleware, validatePayment, send402 } from './server.js';
5
+ export { create402Fetch } from './client.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,kEAAkE;AAElE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAA;AACrG,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAE/E,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,51 @@
1
+ import { type WalletInterface } from '@bsv/sdk';
2
+ export interface PaymentResult {
3
+ accepted: boolean;
4
+ satoshisPaid: number;
5
+ senderIdentityKey: string;
6
+ }
7
+ export interface PaymentMiddlewareOptions {
8
+ /** The server's wallet instance */
9
+ wallet: WalletInterface;
10
+ /** Function that returns the price in satoshis for a given request path. Return 0 or undefined to skip payment. */
11
+ calculatePrice: (path: string) => number | undefined;
12
+ /** Payment freshness window in milliseconds (default: 30000) */
13
+ paymentWindowMs?: number;
14
+ }
15
+ /**
16
+ * Generic request/response interface so the middleware is not coupled to Express.
17
+ * Works with Express, Fastify, or any framework that provides headers, path, status, and set.
18
+ */
19
+ export interface PaymentRequest {
20
+ path: string;
21
+ headers: Record<string, string | string[] | undefined>;
22
+ }
23
+ export interface PaymentResponse {
24
+ status(code: number): PaymentResponse;
25
+ set(headers: Record<string, string>): PaymentResponse;
26
+ end(): void;
27
+ }
28
+ /**
29
+ * Sends a 402 Payment Required response with price and server identity headers.
30
+ */
31
+ export declare function send402(res: PaymentResponse, serverIdentityKey: string, sats: number): void;
32
+ /**
33
+ * Validates payment headers on an incoming request.
34
+ * Returns a PaymentResult if the payment is valid, or null if the request should be rejected.
35
+ */
36
+ export declare function validatePayment(req: PaymentRequest, wallet: WalletInterface, paymentWindowMs?: number): Promise<PaymentResult | null>;
37
+ /**
38
+ * Creates an Express-compatible middleware function for BRC-121 payments.
39
+ *
40
+ * Usage:
41
+ * ```ts
42
+ * import { createPaymentMiddleware } from '@bsv/402-pay/server'
43
+ *
44
+ * app.use('/articles/:slug', createPaymentMiddleware({
45
+ * wallet,
46
+ * calculatePrice: (path) => 100
47
+ * }))
48
+ * ```
49
+ */
50
+ export declare function createPaymentMiddleware(options: PaymentMiddlewareOptions): (req: any, res: any, next: any) => Promise<any>;
51
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAe,MAAM,UAAU,CAAA;AAG5D,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,mCAAmC;IACnC,MAAM,EAAE,eAAe,CAAA;IACvB,mHAAmH;IACnH,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACpD,gEAAgE;IAChE,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;CACvD;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAAA;IACrC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,eAAe,CAAA;IACrD,GAAG,IAAI,IAAI,CAAA;CACZ;AAED;;GAEG;AACH,wBAAgB,OAAO,CACrB,GAAG,EAAE,eAAe,EACpB,iBAAiB,EAAE,MAAM,EACzB,IAAI,EAAE,MAAM,GACX,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,eAAe,EACvB,eAAe,GAAE,MAAkC,GAClD,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA2C/B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,IAIzD,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,GAAG,kBA+B5C"}
package/dist/server.js ADDED
@@ -0,0 +1,104 @@
1
+ import { Utils, Beef } from '@bsv/sdk';
2
+ import { HEADERS, DEFAULT_PAYMENT_WINDOW_MS } from './constants.js';
3
+ /**
4
+ * Sends a 402 Payment Required response with price and server identity headers.
5
+ */
6
+ export function send402(res, serverIdentityKey, sats) {
7
+ res.set({
8
+ [HEADERS.SATS]: String(sats),
9
+ [HEADERS.SERVER]: serverIdentityKey
10
+ });
11
+ res.status(402).end();
12
+ }
13
+ /**
14
+ * Validates payment headers on an incoming request.
15
+ * Returns a PaymentResult if the payment is valid, or null if the request should be rejected.
16
+ */
17
+ export async function validatePayment(req, wallet, paymentWindowMs = DEFAULT_PAYMENT_WINDOW_MS) {
18
+ const h = (name) => {
19
+ const v = req.headers[name];
20
+ return Array.isArray(v) ? v[0] : v;
21
+ };
22
+ const sender = h(HEADERS.SENDER);
23
+ const beef = h(HEADERS.BEEF);
24
+ const nonce = h(HEADERS.NONCE);
25
+ const time = h(HEADERS.TIME);
26
+ const vout = h(HEADERS.VOUT);
27
+ if (!sender || !beef || !nonce || !time || !vout)
28
+ return null;
29
+ // Validate timestamp freshness
30
+ const timestamp = Number(time);
31
+ if (isNaN(timestamp) || Math.abs(Date.now() - timestamp) > paymentWindowMs)
32
+ return null;
33
+ const beefArr = Utils.toArray(beef, 'base64');
34
+ Beef.fromBinary(beefArr); // validates structure
35
+ const result = await wallet.internalizeAction({
36
+ tx: beefArr,
37
+ outputs: [{
38
+ outputIndex: Number.parseInt(vout),
39
+ protocol: 'wallet payment',
40
+ paymentRemittance: {
41
+ derivationPrefix: nonce,
42
+ derivationSuffix: Buffer.from(time).toString('base64'),
43
+ senderIdentityKey: sender
44
+ }
45
+ }],
46
+ description: `Payment for ${req.path}`
47
+ });
48
+ // Reject replayed transactions
49
+ if (result.isMerge)
50
+ return null;
51
+ return {
52
+ accepted: true,
53
+ satoshisPaid: 0, // actual amount is validated by the wallet during internalization
54
+ senderIdentityKey: sender
55
+ };
56
+ }
57
+ /**
58
+ * Creates an Express-compatible middleware function for BRC-121 payments.
59
+ *
60
+ * Usage:
61
+ * ```ts
62
+ * import { createPaymentMiddleware } from '@bsv/402-pay/server'
63
+ *
64
+ * app.use('/articles/:slug', createPaymentMiddleware({
65
+ * wallet,
66
+ * calculatePrice: (path) => 100
67
+ * }))
68
+ * ```
69
+ */
70
+ export function createPaymentMiddleware(options) {
71
+ const { wallet, calculatePrice, paymentWindowMs } = options;
72
+ let identityKey = '';
73
+ return async (req, res, next) => {
74
+ try {
75
+ if (!identityKey) {
76
+ const { publicKey } = await wallet.getPublicKey({ identityKey: true });
77
+ identityKey = publicKey;
78
+ }
79
+ const price = calculatePrice(req.path);
80
+ if (!price)
81
+ return next();
82
+ const hasPayment = req.headers[HEADERS.BEEF];
83
+ if (!hasPayment) {
84
+ return send402(res, identityKey, price);
85
+ }
86
+ const result = await validatePayment(req, wallet, paymentWindowMs);
87
+ if (!result) {
88
+ return send402(res, identityKey, price);
89
+ }
90
+ req.payment = { ...result, satoshisPaid: price };
91
+ next();
92
+ }
93
+ catch {
94
+ if (!identityKey) {
95
+ res.status(500).end();
96
+ }
97
+ else {
98
+ const price = calculatePrice(req.path) ?? 100;
99
+ send402(res, identityKey, price);
100
+ }
101
+ }
102
+ };
103
+ }
104
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAC5D,OAAO,EAAE,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAA;AAgCnE;;GAEG;AACH,MAAM,UAAU,OAAO,CACrB,GAAoB,EACpB,iBAAyB,EACzB,IAAY;IAEZ,GAAG,CAAC,GAAG,CAAC;QACN,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC;QAC5B,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,iBAAiB;KACpC,CAAC,CAAA;IACF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAA;AACvB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAmB,EACnB,MAAuB,EACvB,kBAA0B,yBAAyB;IAEnD,MAAM,CAAC,GAAG,CAAC,IAAY,EAAsB,EAAE;QAC7C,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3B,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACpC,CAAC,CAAA;IAED,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IAC9B,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAE5B,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IAE7D,+BAA+B;IAC/B,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC9B,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,eAAe;QAAE,OAAO,IAAI,CAAA;IAEvF,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IAC7C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA,CAAC,sBAAsB;IAE/C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC;QAC5C,EAAE,EAAE,OAAO;QACX,OAAO,EAAE,CAAC;gBACR,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAClC,QAAQ,EAAE,gBAAgB;gBAC1B,iBAAiB,EAAE;oBACjB,gBAAgB,EAAE,KAAK;oBACvB,gBAAgB,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;oBACtD,iBAAiB,EAAE,MAAM;iBAC1B;aACF,CAAC;QACF,WAAW,EAAE,eAAe,GAAG,CAAC,IAAI,EAAE;KACvC,CAA6C,CAAA;IAE9C,+BAA+B;IAC/B,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,IAAI,CAAA;IAE/B,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,YAAY,EAAE,CAAC,EAAE,kEAAkE;QACnF,iBAAiB,EAAE,MAAM;KAC1B,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAiC;IACvE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,GAAG,OAAO,CAAA;IAC3D,IAAI,WAAW,GAAG,EAAE,CAAA;IAEpB,OAAO,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,IAAS,EAAE,EAAE;QAC7C,IAAI,CAAC;YACH,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;gBACtE,WAAW,GAAG,SAAS,CAAA;YACzB,CAAC;YAED,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACtC,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,EAAE,CAAA;YAEzB,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;YAC5C,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,OAAO,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,CAAA;YACzC,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,CAAC,CAAA;YAClE,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,CAAA;YACzC,CAAC;YAED,GAAG,CAAC,OAAO,GAAG,EAAE,GAAG,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,CAAA;YAChD,IAAI,EAAE,CAAA;QACR,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAA;YACvB,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAA;gBAC7C,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,CAAA;YAClC,CAAC;QACH,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@bsv/402-pay",
3
+ "version": "0.1.0",
4
+ "description": "BRC-121 Simple 402 Payments — server middleware and client for BSV micropayments over HTTP",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./server": {
14
+ "types": "./dist/server.d.ts",
15
+ "import": "./dist/server.js"
16
+ },
17
+ "./client": {
18
+ "types": "./dist/client.d.ts",
19
+ "import": "./dist/client.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "keywords": [
31
+ "bsv",
32
+ "402",
33
+ "payment",
34
+ "micropayments",
35
+ "brc-121",
36
+ "middleware"
37
+ ],
38
+ "author": "Deggen <d.kellenschwiler@bsvassociation.org>",
39
+ "license": "SEE LICENSE IN LICENSE",
40
+ "peerDependencies": {
41
+ "@bsv/sdk": ">=2.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@bsv/sdk": "^2.0.13",
45
+ "@types/node": "^20.14.0",
46
+ "typescript": "^5.5.0"
47
+ }
48
+ }