@thecryptodonkey/toll-booth 3.7.0 → 3.8.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.
@@ -1,7 +1,52 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { DEFAULT_USDC_ASSETS } from './x402-types.js';
2
+ import { DEFAULT_USDC_ASSETS, X402_VERSION } from './x402-types.js';
3
+ /**
4
+ * Normalise an inbound x402 v2 PAYMENT-SIGNATURE payload to the flat
5
+ * internal X402Payment format used by the facilitator interface.
6
+ */
7
+ function normaliseV2Payload(wire) {
8
+ const auth = wire.payload?.authorization;
9
+ if (!auth)
10
+ return undefined;
11
+ const amount = Number(auth.value);
12
+ if (!Number.isFinite(amount))
13
+ return undefined;
14
+ return {
15
+ signature: wire.payload.signature,
16
+ sender: auth.from,
17
+ amount,
18
+ network: wire.accepted?.network ?? '',
19
+ nonce: auth.nonce,
20
+ };
21
+ }
22
+ /**
23
+ * Try to parse the payment from either the v2 PAYMENT-SIGNATURE header
24
+ * (base64-encoded JSON) or the legacy X-Payment header (raw JSON).
25
+ */
26
+ function parsePayment(req) {
27
+ // v2: PAYMENT-SIGNATURE (base64-encoded JSON)
28
+ const sigHeader = req.headers['payment-signature'];
29
+ if (sigHeader && sigHeader.length <= 8192) {
30
+ try {
31
+ const decoded = JSON.parse(Buffer.from(sigHeader, 'base64').toString());
32
+ if (decoded.x402Version >= 2)
33
+ return normaliseV2Payload(decoded);
34
+ }
35
+ catch { /* fall through to legacy */ }
36
+ }
37
+ // Legacy: X-Payment (raw JSON)
38
+ const raw = req.headers['x-payment'];
39
+ if (!raw || raw.length > 4096)
40
+ return undefined;
41
+ try {
42
+ return JSON.parse(raw);
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
3
48
  export function createX402Rail(config) {
4
- const { receiverAddress, network, asset = DEFAULT_USDC_ASSETS[network], facilitator, creditMode = true, facilitatorUrl, storage, } = config;
49
+ const { receiverAddress, network, asset = DEFAULT_USDC_ASSETS[network], facilitator, creditMode = true, facilitatorUrl, maxTimeoutSeconds = 3600, storage, } = config;
5
50
  return {
6
51
  type: 'x402',
7
52
  creditSupported: true,
@@ -9,13 +54,33 @@ export function createX402Rail(config) {
9
54
  return price.usd !== undefined;
10
55
  },
11
56
  detect(req) {
12
- return req.headers['x-payment'] !== undefined;
57
+ return req.headers['payment-signature'] !== undefined
58
+ || req.headers['x-payment'] !== undefined;
13
59
  },
14
- async challenge(_route, price) {
60
+ async challenge(route, price) {
61
+ const requirements = {
62
+ x402Version: X402_VERSION,
63
+ accepts: [{
64
+ scheme: 'exact',
65
+ network,
66
+ amount: String(price.usd),
67
+ asset: asset ?? '',
68
+ payTo: receiverAddress,
69
+ maxTimeoutSeconds,
70
+ extra: {
71
+ ...(facilitatorUrl && { facilitatorUrl }),
72
+ },
73
+ }],
74
+ resource: { url: route },
75
+ };
76
+ const encoded = Buffer.from(JSON.stringify(requirements)).toString('base64');
15
77
  return {
16
- headers: { 'X-Payment-Required': 'x402' },
78
+ headers: {
79
+ 'Payment-Required': encoded,
80
+ },
17
81
  body: {
18
82
  x402: {
83
+ version: X402_VERSION,
19
84
  receiver: receiverAddress,
20
85
  network,
21
86
  asset,
@@ -26,20 +91,12 @@ export function createX402Rail(config) {
26
91
  };
27
92
  },
28
93
  async verify(req) {
29
- const raw = req.headers['x-payment'];
30
- if (!raw || raw.length > 4096) {
31
- return { authenticated: false, paymentId: '', mode: 'per-request', currency: 'usd' };
32
- }
33
- let payload;
34
- try {
35
- payload = JSON.parse(raw);
36
- }
37
- catch {
94
+ const payload = parsePayment(req);
95
+ if (!payload) {
38
96
  return { authenticated: false, paymentId: '', mode: 'per-request', currency: 'usd' };
39
97
  }
40
98
  // Validate required fields before passing to facilitator
41
- if (typeof payload !== 'object' || payload === null ||
42
- typeof payload.signature !== 'string' || !payload.signature ||
99
+ if (typeof payload.signature !== 'string' || !payload.signature ||
43
100
  typeof payload.sender !== 'string' || !payload.sender ||
44
101
  typeof payload.amount !== 'number' || !Number.isFinite(payload.amount) || payload.amount <= 0 ||
45
102
  typeof payload.network !== 'string' || !payload.network ||
@@ -1,3 +1,4 @@
1
+ /** Internal normalised payment (flat structure passed to facilitator). */
1
2
  export interface X402Payment {
2
3
  signature: string;
3
4
  sender: string;
@@ -21,9 +22,49 @@ export interface X402RailConfig {
21
22
  facilitator: X402Facilitator;
22
23
  creditMode?: boolean;
23
24
  facilitatorUrl?: string;
25
+ /** Max seconds before payment authorisation expires. Default 3600. */
26
+ maxTimeoutSeconds?: number;
24
27
  /** Storage backend — required for credit mode to persist balances. Injected by Booth. */
25
28
  storage?: import('../storage/interface.js').StorageBackend;
26
29
  }
27
30
  /** Default USDC contract addresses by network */
28
31
  export declare const DEFAULT_USDC_ASSETS: Record<string, string>;
32
+ export declare const X402_VERSION = 2;
33
+ export interface X402PaymentRequirements {
34
+ scheme: string;
35
+ network: string;
36
+ amount: string;
37
+ asset: string;
38
+ payTo: string;
39
+ maxTimeoutSeconds: number;
40
+ extra: Record<string, unknown>;
41
+ }
42
+ export interface X402Resource {
43
+ url: string;
44
+ description?: string;
45
+ mimeType?: string;
46
+ }
47
+ /** PAYMENT-REQUIRED header (base64-encoded JSON). */
48
+ export interface X402ChallengeWire {
49
+ x402Version: number;
50
+ accepts: X402PaymentRequirements[];
51
+ resource?: X402Resource;
52
+ }
53
+ /** PAYMENT-SIGNATURE header (base64-encoded JSON). */
54
+ export interface X402PaymentWire {
55
+ x402Version: number;
56
+ resource?: X402Resource;
57
+ accepted?: X402PaymentRequirements;
58
+ payload: {
59
+ signature: string;
60
+ authorization: {
61
+ from: string;
62
+ to: string;
63
+ value: string;
64
+ validAfter: string;
65
+ validBefore: string;
66
+ nonce: string;
67
+ };
68
+ };
69
+ }
29
70
  //# sourceMappingURL=x402-types.d.ts.map
@@ -4,4 +4,6 @@ export const DEFAULT_USDC_ASSETS = {
4
4
  'base-sepolia': '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
5
5
  'polygon': '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
6
6
  };
7
+ // ── x402 v2 wire format types ──────────────────────────────────────
8
+ export const X402_VERSION = 2;
7
9
  //# sourceMappingURL=x402-types.js.map
package/dist/index.d.ts CHANGED
@@ -25,8 +25,8 @@ export { normalisePricing, normalisePricingTable, isTieredPricing } from './core
25
25
  export { createL402Rail } from './core/l402-rail.js';
26
26
  export type { L402RailConfig } from './core/l402-rail.js';
27
27
  export { createX402Rail } from './core/x402-rail.js';
28
- export type { X402RailConfig, X402Facilitator, X402Payment, X402VerifyResult } from './core/x402-types.js';
29
- export { DEFAULT_USDC_ASSETS } from './core/x402-types.js';
28
+ export type { X402RailConfig, X402Facilitator, X402Payment, X402VerifyResult, X402ChallengeWire, X402PaymentWire, X402PaymentRequirements } from './core/x402-types.js';
29
+ export { DEFAULT_USDC_ASSETS, X402_VERSION } from './core/x402-types.js';
30
30
  export { createXCashuRail } from './core/xcashu-rail.js';
31
31
  export type { XCashuConfig } from './types.js';
32
32
  export { meltToLightning } from './core/melt-to-lightning.js';
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ export { createWebStandardMiddleware, createWebStandardInvoiceStatusHandler, cre
16
16
  export { normalisePricing, normalisePricingTable, isTieredPricing } from './core/payment-rail.js';
17
17
  export { createL402Rail } from './core/l402-rail.js';
18
18
  export { createX402Rail } from './core/x402-rail.js';
19
- export { DEFAULT_USDC_ASSETS } from './core/x402-types.js';
19
+ export { DEFAULT_USDC_ASSETS, X402_VERSION } from './core/x402-types.js';
20
20
  export { createXCashuRail } from './core/xcashu-rail.js';
21
21
  export { meltToLightning } from './core/melt-to-lightning.js';
22
22
  // Utilities
package/dist/macaroon.js CHANGED
@@ -1,5 +1,14 @@
1
+ import { randomBytes } from 'node:crypto';
1
2
  import { newMacaroon, importMacaroon } from 'macaroon';
2
3
  const LOCATION = 'toll-booth';
4
+ /**
5
+ * Aperture-compatible L402 identifier layout (version 0):
6
+ * [0..1] uint16 big-endian version (0)
7
+ * [2..33] 32-byte payment hash
8
+ * [34..65] 32-byte random token ID
9
+ */
10
+ const L402_ID_VERSION = 0;
11
+ const L402_ID_SIZE = 66;
3
12
  const KNOWN_CAVEATS = new Set(['payment_hash', 'credit_balance', 'currency', 'route', 'expires', 'ip']);
4
13
  /** Caveat keys that encode monetary value and must not be set via the caveats parameter. */
5
14
  const RESERVED_CAVEAT_KEYS = new Set(['payment_hash', 'credit_balance', 'currency']);
@@ -16,7 +25,7 @@ const MAX_CUSTOM_CAVEATS = 16;
16
25
  export function mintMacaroon(rootKey, paymentHash, creditBalanceSats, caveats, currency) {
17
26
  const keyBytes = hexToBytes(rootKey);
18
27
  const m = newMacaroon({
19
- identifier: paymentHash,
28
+ identifier: encodeL402Identifier(paymentHash),
20
29
  location: LOCATION,
21
30
  rootKey: keyBytes,
22
31
  version: 2,
@@ -68,13 +77,15 @@ export function verifyMacaroon(rootKey, macaroonBase64, context) {
68
77
  return 'duplicate caveat';
69
78
  return null;
70
79
  }, []);
71
- // Use the immutable identifier as the authoritative payment hash —
72
- // it is set at mint time and covered by the root signature.
73
- const json = m.exportJSON();
74
- const identifier = json.i;
80
+ // Decode the aperture-compatible binary identifier to extract the
81
+ // payment hash. The identifier is set at mint time and covered by
82
+ // the root signature, so it cannot be tampered with.
83
+ const paymentHash = decodeL402Identifier(m.identifier);
84
+ if (!paymentHash)
85
+ return { valid: false };
75
86
  const caveats = parseCaveats(macaroonBase64);
76
87
  // Cross-check: the payment_hash caveat must match the identifier
77
- if (caveats.payment_hash && caveats.payment_hash !== identifier) {
88
+ if (caveats.payment_hash && caveats.payment_hash !== paymentHash) {
78
89
  return { valid: false };
79
90
  }
80
91
  if (context) {
@@ -108,7 +119,7 @@ export function verifyMacaroon(rootKey, macaroonBase64, context) {
108
119
  const currency = caveats.currency ?? 'sat';
109
120
  return {
110
121
  valid: true,
111
- paymentHash: identifier,
122
+ paymentHash,
112
123
  creditBalance,
113
124
  currency,
114
125
  customCaveats: Object.keys(customCaveats).length > 0 ? customCaveats : undefined,
@@ -153,6 +164,22 @@ export function parseCaveats(macaroonBase64) {
153
164
  }
154
165
  return result;
155
166
  }
167
+ function encodeL402Identifier(paymentHash) {
168
+ const id = new Uint8Array(L402_ID_SIZE);
169
+ id[0] = (L402_ID_VERSION >> 8) & 0xff;
170
+ id[1] = L402_ID_VERSION & 0xff;
171
+ id.set(hexToBytes(paymentHash), 2);
172
+ id.set(randomBytes(32), 34);
173
+ return id;
174
+ }
175
+ function decodeL402Identifier(id) {
176
+ if (id.length < L402_ID_SIZE)
177
+ return undefined;
178
+ const version = (id[0] << 8) | id[1];
179
+ if (version !== L402_ID_VERSION)
180
+ return undefined;
181
+ return bytesToHex(id.slice(2, 34));
182
+ }
156
183
  function hexToBytes(hex) {
157
184
  const bytes = new Uint8Array(hex.length / 2);
158
185
  for (let i = 0; i < hex.length; i += 2) {
@@ -160,6 +187,9 @@ function hexToBytes(hex) {
160
187
  }
161
188
  return bytes;
162
189
  }
190
+ function bytesToHex(bytes) {
191
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
192
+ }
163
193
  function uint8ToBase64(bytes) {
164
194
  return Buffer.from(bytes).toString('base64');
165
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thecryptodonkey/toll-booth",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "type": "module",
5
5
  "description": "Monetise any API with HTTP 402 payments. Payment-rail agnostic middleware for Express, Hono, Deno, Bun, and Workers.",
6
6
  "license": "MIT",