@paynodelabs/sdk-js 2.2.3 → 2.4.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 +81 -73
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +40 -17
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/merchant/index.d.ts +37 -0
- package/dist/merchant/index.d.ts.map +1 -0
- package/dist/merchant/index.js +116 -0
- package/dist/merchant/middleware.d.ts +14 -0
- package/dist/merchant/middleware.d.ts.map +1 -0
- package/dist/merchant/middleware.js +76 -0
- package/dist/merchant/types.d.ts +27 -0
- package/dist/merchant/types.d.ts.map +1 -0
- package/dist/merchant/types.js +2 -0
- package/dist/middleware/x402.d.ts.map +1 -1
- package/dist/middleware/x402.js +23 -16
- package/dist/types/x402.d.ts +19 -2
- package/dist/types/x402.d.ts.map +1 -1
- package/dist/utils/payload.d.ts.map +1 -1
- package/dist/utils/payload.js +11 -3
- package/dist/utils/signature.d.ts +13 -0
- package/dist/utils/signature.d.ts.map +1 -0
- package/dist/utils/signature.js +42 -0
- package/dist/utils/verifier.d.ts.map +1 -1
- package/dist/utils/verifier.js +19 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
# PayNode
|
|
1
|
+
# PayNode Master SDK (JS/TS)
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> Compatibility policy & legacy header aliases are documented in [COMPATIBILITY.md](./COMPATIBILITY.md).
|
|
2
5
|
|
|
3
6
|
[](https://docs.paynode.dev)
|
|
4
7
|
[](https://www.npmjs.com/package/@paynodelabs/sdk-js)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
The **Master SDK** for the [PayNode Protocol](https://paynode.dev). This JavaScript/TypeScript implementation serves as the **Reference Implementation** for the x402 V2 protocol. All future SDKs (Python, Go, Rust, etc.) are built following the architectural patterns and logic established in this codebase.
|
|
11
|
+
|
|
12
|
+
The current baseline and cleanup requirements for turning `sdk-js` into the formal multi-language source of truth are documented in [`meta/MULTI_LANGUAGE_BASELINE.md`](../../meta/MULTI_LANGUAGE_BASELINE.md). The protocol source of truth lives in [`meta/SPEC.md`](../../meta/SPEC.md).
|
|
5
13
|
|
|
6
|
-
|
|
14
|
+
PayNode is a stateless, non-custodial M2M payment gateway that standardizes the HTTP 402 "Payment Required" flow for AI Agents, enabling seamless USDC settlement on Base L2.
|
|
7
15
|
|
|
8
|
-
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🏗️ Reference Implementation Status
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
As the **Master SDK**, `sdk-js` defines the gold standard for:
|
|
21
|
+
- **x402 V2 Handshake**: The JSON-based negotiation protocol between Agents and Merchants.
|
|
22
|
+
- **Dual-Mode Settlement**: Seamless switching between high-speed off-chain signatures (EIP-3009) and robust on-chain receipts.
|
|
23
|
+
- **Proof-of-Intent**: Standardized cryptographic signatures for M2M commerce.
|
|
24
|
+
- **Stateless Verification**: Logic for verifying payments without requiring complex backend databases.
|
|
25
|
+
|
|
26
|
+
> [!TIP]
|
|
27
|
+
> Developers building PayNode SDKs for other languages should treat this repository as the **Source of Truth** for internal logic and protocol compliance.
|
|
28
|
+
|
|
29
|
+
---
|
|
12
30
|
|
|
13
31
|
## ⚡ Quick Start
|
|
14
32
|
|
|
@@ -19,102 +37,92 @@ npm install @paynodelabs/sdk-js ethers
|
|
|
19
37
|
```
|
|
20
38
|
|
|
21
39
|
### Agent Client (Payer)
|
|
40
|
+
The `PayNodeAgentClient` automatically identifies 402 challenges and executes the best settlement path (On-chain or EIP-3009).
|
|
22
41
|
|
|
23
42
|
```typescript
|
|
24
43
|
import { PayNodeAgentClient } from "@paynodelabs/sdk-js";
|
|
25
44
|
|
|
26
|
-
|
|
45
|
+
// Initialize with Private Key and Base RPC
|
|
46
|
+
const client = new PayNodeAgentClient("YOUR_PRIVATE_KEY", ["https://mainnet.base.org"]);
|
|
27
47
|
|
|
28
48
|
async function main() {
|
|
29
|
-
// Automatically handles 402 challenges,
|
|
49
|
+
// Automatically handles 402 challenges, settles USDC, and retries the request
|
|
30
50
|
const response = await client.requestGate("https://api.merchant.com/premium-data");
|
|
31
|
-
|
|
51
|
+
const result = await response.json();
|
|
52
|
+
console.log("Success:", result);
|
|
32
53
|
}
|
|
33
54
|
main();
|
|
34
55
|
```
|
|
35
56
|
|
|
36
|
-
###
|
|
37
|
-
|
|
38
|
-
- **Double-Spend Protection**:
|
|
39
|
-
- **L1 (Memory)**: High-speed local replay protection via `IdempotencyStore`.
|
|
40
|
-
- **L2 (RPC)**: Real-time on-chain `authorizationState` verification.
|
|
41
|
-
- **Empty-Wallet Proof**: Integrated `balanceOf` probes to block malicious agents using empty wallets to generate valid signatures.
|
|
42
|
-
- **EIP-3009 Support**: Sign payments off-chain using `TransferWithAuthorization`.
|
|
43
|
-
- **X402 V2 Protocol**: JSON-based handshake for structured agent interaction.
|
|
44
|
-
- **Dual Flow**: Automatic switch between V1 (on-chain receipts) and V2 (off-chain signatures).
|
|
45
|
-
|
|
46
|
-
## 🗺️ Roadmap
|
|
47
|
-
- **TRON Support**: USDT (TRC-20) payment integration.
|
|
48
|
-
- **Solana Support**: SPL USDC/USDT payment integration.
|
|
49
|
-
- **Cross-chain**: Universal settlement via bridges.
|
|
50
|
-
|
|
51
|
-
## 🚀 Run the Demo
|
|
52
|
-
|
|
53
|
-
The SDK includes a full merchant/agent demonstration in the `examples/` directory.
|
|
57
|
+
### Merchant Gateway (Seller)
|
|
58
|
+
Starting from **v2.3.0**, the SDK provides a unified middleware for **PayNode Market Proxy** requests. It handles discovery probes, signature verification, and body unwrap in a single line.
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
```typescript
|
|
61
|
+
import { PayNodeMerchant } from "@paynodelabs/sdk-js";
|
|
62
|
+
import express from "express";
|
|
63
|
+
|
|
64
|
+
const app = express();
|
|
65
|
+
const merchant = new PayNodeMerchant({
|
|
66
|
+
sharedSecret: "YOUR_MARKET_SECRET" // Obtainable from PayNode Market Hub
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Unified Middleware (Manifest + Auth + Discovery)
|
|
70
|
+
app.post("/api/v1/tools", merchant.middleware({
|
|
71
|
+
manifest: {
|
|
72
|
+
slug: "gpt-researcher",
|
|
73
|
+
name: "GPT Researcher",
|
|
74
|
+
description: "Research endpoint",
|
|
75
|
+
price_per_call: "0.05",
|
|
76
|
+
currency: "USDC"
|
|
77
|
+
},
|
|
78
|
+
price: "0.05" // Also set price directly — manifest.price_per_call is for Market Hub sync only
|
|
79
|
+
}), (req, res) => {
|
|
80
|
+
// req.body is automatically unwrapped for valid Market Proxy requests
|
|
81
|
+
res.json({ result: "Data for " + req.body.query });
|
|
82
|
+
}
|
|
83
|
+
);
|
|
68
84
|
```
|
|
69
85
|
|
|
70
|
-
|
|
86
|
+
---
|
|
71
87
|
|
|
72
|
-
|
|
73
|
-
npx ts-node examples/express-server.ts
|
|
74
|
-
```
|
|
88
|
+
## 🚀 Key Features
|
|
75
89
|
|
|
76
|
-
|
|
90
|
+
- **Zero-Wait Checkout**: Off-chain EIP-3009 signatures allow **<50ms** checkout times by bypassing block confirmation delays.
|
|
91
|
+
- **Autonomous Recovery**: Built-in RPC failover and retry logic for high-availability agent environments.
|
|
92
|
+
- **Double-Spend Protection**: Memory-efficient `IdempotencyStore` for high-frequency replay protection.
|
|
93
|
+
- **EIP-2612 & EIP-3009**: Native support for gasless approvals and off-chain transfers.
|
|
94
|
+
- **Market Proxy Middleware**: Drop-in Express support for PayNode Market discovery, authentication, and payload unwrap.
|
|
77
95
|
|
|
78
|
-
|
|
96
|
+
---
|
|
79
97
|
|
|
80
|
-
|
|
81
|
-
npx ts-node examples/agent-client.ts
|
|
82
|
-
```
|
|
98
|
+
## 🧭 Roadmap & Ecosystem
|
|
83
99
|
|
|
84
|
-
|
|
100
|
+
As the lead SDK, this package drives the protocol forward. Current initiatives:
|
|
101
|
+
- 🐍 **Python SDK**: Porting `sdk-js` logic to Python for LangChain/AutoGPT integration.
|
|
102
|
+
- 🦀 **Rust SDK**: High-performance implementation for edge compute.
|
|
103
|
+
- 📡 **Cross-chain Settlement**: Integration with Solana (USDC) and TRON (USDT).
|
|
85
104
|
|
|
86
105
|
---
|
|
87
106
|
|
|
88
|
-
##
|
|
107
|
+
## 🛠️ Development & Testing
|
|
89
108
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
2. **Login to NPM** (if not already):
|
|
97
|
-
```bash
|
|
98
|
-
npm login
|
|
99
|
-
```
|
|
100
|
-
3. **Publish**:
|
|
101
|
-
```bash
|
|
102
|
-
npm publish --access public
|
|
103
|
-
```
|
|
109
|
+
### Run the Demo
|
|
110
|
+
1. **Setup Env**: `cp .env.example .env` and set `CLIENT_PRIVATE_KEY`. If you want to run the merchant demo too, also set `MERCHANT_ADDRESS`.
|
|
111
|
+
2. **Mint Test USDC**: `npx ts-node examples/mint-test-tokens.ts` (Base Sepolia).
|
|
112
|
+
3. **Start Merchant**: `npm run example:server`
|
|
113
|
+
4. **Run Agent**: `npx ts-node examples/agent-client.ts`
|
|
104
114
|
|
|
105
115
|
---
|
|
106
116
|
|
|
107
|
-
## Versioning
|
|
108
|
-
|
|
109
|
-
PayNode uses two distinct versioning schemes:
|
|
117
|
+
## 📖 Versioning
|
|
110
118
|
|
|
111
|
-
|
|
|
112
|
-
| :--- | :--- |
|
|
113
|
-
| **Protocol
|
|
114
|
-
| **SDK
|
|
119
|
+
| Component | Current Version |
|
|
120
|
+
| :--- | :--- |
|
|
121
|
+
| **Protocol (x402)** | `v2` |
|
|
122
|
+
| **SDK Implementation** | `v2.4.0` |
|
|
115
123
|
|
|
116
|
-
> [!
|
|
117
|
-
> Protocol
|
|
124
|
+
> [!IMPORTANT]
|
|
125
|
+
> Protocol and SDK package versioning are not the same thing. Treat the protocol as `x402 v2`, and treat `2.4.0` as the current JS SDK implementation version.
|
|
118
126
|
|
|
119
127
|
---
|
|
120
128
|
|
package/dist/client.d.ts
CHANGED
|
@@ -2,15 +2,22 @@ import { ExactEVMPayload } from './types/x402';
|
|
|
2
2
|
export interface RequestOptions extends RequestInit {
|
|
3
3
|
json?: any;
|
|
4
4
|
}
|
|
5
|
+
export interface AgentClientOptions {
|
|
6
|
+
rpcUrls?: string | string[];
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
quiet?: boolean;
|
|
9
|
+
}
|
|
5
10
|
export declare class PayNodeAgentClient {
|
|
6
11
|
private wallet;
|
|
7
12
|
private provider;
|
|
8
13
|
private rpcUrls;
|
|
9
14
|
private maxRetries;
|
|
15
|
+
private options;
|
|
10
16
|
private nonceLock;
|
|
11
17
|
private ERC20_ABI;
|
|
12
18
|
private ROUTER_ABI;
|
|
13
19
|
constructor(privateKey: string, rpcUrls?: string | string[], maxRetries?: number);
|
|
20
|
+
constructor(privateKey: string, options?: AgentClientOptions);
|
|
14
21
|
private _fetchWithRetry;
|
|
15
22
|
requestGate(url: string, options?: RequestOptions): Promise<Response>;
|
|
16
23
|
private _handleX402V2;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,eAAe,EAEhB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,eAAe,EAEhB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAKD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,SAAS,CAAoC;IAErD,OAAO,CAAC,SAAS,CAMf;IAEF,OAAO,CAAC,UAAU,CAAsB;gBAE5B,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM;gBACpE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB;YAuB9C,eAAe;IAoCvB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YAuEjE,aAAa;IAoKrB,6BAA6B,CACjC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC9B,OAAO,CAAC,eAAe,CAAC;IA+CrB,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBpH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA8BrJ,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAE,MAAa,EAAE,OAAO,GAAE,MAAY;;;;;;YAyChH,UAAU;CAYzB"}
|
package/dist/client.js
CHANGED
|
@@ -10,6 +10,7 @@ class PayNodeAgentClient {
|
|
|
10
10
|
provider;
|
|
11
11
|
rpcUrls;
|
|
12
12
|
maxRetries;
|
|
13
|
+
options;
|
|
13
14
|
nonceLock = Promise.resolve();
|
|
14
15
|
ERC20_ABI = [
|
|
15
16
|
"function approve(address spender, uint256 value) public returns (bool)",
|
|
@@ -19,9 +20,17 @@ class PayNodeAgentClient {
|
|
|
19
20
|
"function nonces(address owner) view returns (uint256)"
|
|
20
21
|
];
|
|
21
22
|
ROUTER_ABI = constants_1.PAYNODE_ROUTER_ABI;
|
|
22
|
-
constructor(privateKey,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
constructor(privateKey, arg2, arg3) {
|
|
24
|
+
if (typeof arg2 === 'object' && !Array.isArray(arg2)) {
|
|
25
|
+
this.options = { ...arg2 };
|
|
26
|
+
this.rpcUrls = Array.isArray(this.options.rpcUrls) ? this.options.rpcUrls : (this.options.rpcUrls ? [this.options.rpcUrls] : constants_1.BASE_RPC_URLS);
|
|
27
|
+
this.maxRetries = this.options.maxRetries ?? 3;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
this.rpcUrls = Array.isArray(arg2) ? arg2 : (arg2 ? [arg2] : constants_1.BASE_RPC_URLS);
|
|
31
|
+
this.maxRetries = arg3 ?? 3;
|
|
32
|
+
this.options = { rpcUrls: this.rpcUrls, maxRetries: this.maxRetries };
|
|
33
|
+
}
|
|
25
34
|
const configs = this.rpcUrls.map((url, index) => ({
|
|
26
35
|
provider: new ethers_1.ethers.JsonRpcProvider(url, undefined, { staticNetwork: true }),
|
|
27
36
|
priority: index,
|
|
@@ -44,7 +53,8 @@ class PayNodeAgentClient {
|
|
|
44
53
|
}
|
|
45
54
|
if (attempt < this.maxRetries - 1) {
|
|
46
55
|
const backoffMs = Math.pow(2, attempt) * 1000;
|
|
47
|
-
|
|
56
|
+
if (!this.options.quiet)
|
|
57
|
+
console.warn(`⚠️ [PayNode-JS] ${response.status} received. Retrying in ${backoffMs}ms...`);
|
|
48
58
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
49
59
|
continue;
|
|
50
60
|
}
|
|
@@ -54,7 +64,8 @@ class PayNodeAgentClient {
|
|
|
54
64
|
lastError = error;
|
|
55
65
|
if (attempt < this.maxRetries - 1) {
|
|
56
66
|
const backoffMs = Math.pow(2, attempt) * 1000;
|
|
57
|
-
|
|
67
|
+
if (!this.options.quiet)
|
|
68
|
+
console.warn(`⚠️ [PayNode-JS] Request failed: ${error.message}. Retrying in ${backoffMs}ms...`);
|
|
58
69
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
59
70
|
}
|
|
60
71
|
}
|
|
@@ -79,7 +90,8 @@ class PayNodeAgentClient {
|
|
|
79
90
|
try {
|
|
80
91
|
let response = await this._fetchWithRetry(url, fetchOptions);
|
|
81
92
|
if (response.status === 402) {
|
|
82
|
-
|
|
93
|
+
if (!this.options.quiet)
|
|
94
|
+
console.log(`💡 [PayNode-JS] 402 Payment Required detected. Analyzing protocol version...`);
|
|
83
95
|
const contentType = response.headers.get('content-type');
|
|
84
96
|
const b64Required = response.headers.get('PAYMENT-REQUIRED') || response.headers.get('X-402-Required');
|
|
85
97
|
const orderId = response.headers.get('X-402-Order-Id');
|
|
@@ -93,7 +105,8 @@ class PayNodeAgentClient {
|
|
|
93
105
|
headerBody = JSON.parse(decoded);
|
|
94
106
|
}
|
|
95
107
|
catch (e) {
|
|
96
|
-
|
|
108
|
+
if (!this.options.quiet)
|
|
109
|
+
console.debug('⚠️ [PayNode-JS] Failed to parse PAYMENT-REQUIRED header:', e);
|
|
97
110
|
}
|
|
98
111
|
}
|
|
99
112
|
if (contentType && contentType.includes('application/json')) {
|
|
@@ -107,7 +120,8 @@ class PayNodeAgentClient {
|
|
|
107
120
|
body = { ...body, ...headerBody };
|
|
108
121
|
}
|
|
109
122
|
if (body && body.x402Version === 2) {
|
|
110
|
-
|
|
123
|
+
if (!this.options.quiet)
|
|
124
|
+
console.log(`🚀 [PayNode-JS] x402 v2 detected. Handling autonomous payment...`);
|
|
111
125
|
if (orderId && !body.orderId)
|
|
112
126
|
body.orderId = orderId;
|
|
113
127
|
return await this._handleX402V2(url, fetchOptions, body);
|
|
@@ -119,7 +133,8 @@ class PayNodeAgentClient {
|
|
|
119
133
|
catch (error) {
|
|
120
134
|
if (error instanceof errors_1.PayNodeException || error?.name === "PayNodeException")
|
|
121
135
|
throw error;
|
|
122
|
-
|
|
136
|
+
if (!this.options.quiet)
|
|
137
|
+
console.error(`❌ [PayNode-JS] Critical error in requestGate:`, error);
|
|
123
138
|
throw new errors_1.PayNodeException(errors_1.ErrorCode.RpcError, undefined, error);
|
|
124
139
|
}
|
|
125
140
|
}
|
|
@@ -132,7 +147,8 @@ class PayNodeAgentClient {
|
|
|
132
147
|
if (!requirement) {
|
|
133
148
|
throw new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed, `No compatible payment requirement found for network ${caip2ChainId}`);
|
|
134
149
|
}
|
|
135
|
-
|
|
150
|
+
if (!this.options.quiet)
|
|
151
|
+
console.log(`💡 [PayNode-JS] Selected payment method: ${requirement.type || 'onchain'} on ${requirement.network}`);
|
|
136
152
|
// 🛡️ Token Whitelist Check (Case-insensitive)
|
|
137
153
|
const chainTokens = constants_1.ACCEPTED_TOKENS[chainId]?.map(t => t.toLowerCase());
|
|
138
154
|
if (chainTokens && !chainTokens.includes(requirement.asset.toLowerCase())) {
|
|
@@ -154,6 +170,7 @@ class PayNodeAgentClient {
|
|
|
154
170
|
resource: requirements.resource,
|
|
155
171
|
accepted: {
|
|
156
172
|
scheme: requirement.scheme,
|
|
173
|
+
type: 'eip3009',
|
|
157
174
|
network: requirement.network,
|
|
158
175
|
amount: requirement.amount,
|
|
159
176
|
asset: requirement.asset,
|
|
@@ -163,7 +180,7 @@ class PayNodeAgentClient {
|
|
|
163
180
|
},
|
|
164
181
|
payload: authorization,
|
|
165
182
|
_paynode: {
|
|
166
|
-
|
|
183
|
+
sdkVersion: constants_1.SDK_VERSION,
|
|
167
184
|
type: 'eip3009',
|
|
168
185
|
orderId: orderId
|
|
169
186
|
}
|
|
@@ -175,7 +192,8 @@ class PayNodeAgentClient {
|
|
|
175
192
|
if (!routerAddr) {
|
|
176
193
|
throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, "On-chain payment required but no router address provided.");
|
|
177
194
|
}
|
|
178
|
-
|
|
195
|
+
if (!this.options.quiet)
|
|
196
|
+
console.log(`⚡ [PayNode-JS] Executing on-chain payment to ${requirement.payTo}...`);
|
|
179
197
|
const amount = BigInt(requirement.amount);
|
|
180
198
|
const tokenContract = new ethers_1.ethers.Contract(requirement.asset, this.ERC20_ABI, this.wallet);
|
|
181
199
|
const allowance = await tokenContract.allowance(this.wallet.address, routerAddr);
|
|
@@ -185,7 +203,8 @@ class PayNodeAgentClient {
|
|
|
185
203
|
txHash = await this.pay(routerAddr, requirement.asset, requirement.payTo, amount, orderId);
|
|
186
204
|
}
|
|
187
205
|
catch (e) {
|
|
188
|
-
|
|
206
|
+
if (!this.options.quiet)
|
|
207
|
+
console.warn(`⚠️ [PayNode-JS] Direct pay failed (possibly allowance race), falling back to permit:`, e);
|
|
189
208
|
txHash = await this.payWithPermit(routerAddr, requirement.asset, requirement.payTo, amount, orderId, requirement.extra?.version || '2');
|
|
190
209
|
}
|
|
191
210
|
}
|
|
@@ -197,6 +216,7 @@ class PayNodeAgentClient {
|
|
|
197
216
|
resource: requirements.resource,
|
|
198
217
|
accepted: {
|
|
199
218
|
scheme: requirement.scheme,
|
|
219
|
+
type: 'onchain',
|
|
200
220
|
network: requirement.network,
|
|
201
221
|
amount: requirement.amount,
|
|
202
222
|
asset: requirement.asset,
|
|
@@ -207,7 +227,7 @@ class PayNodeAgentClient {
|
|
|
207
227
|
},
|
|
208
228
|
payload: { txHash },
|
|
209
229
|
_paynode: {
|
|
210
|
-
|
|
230
|
+
sdkVersion: constants_1.SDK_VERSION,
|
|
211
231
|
type: 'onchain',
|
|
212
232
|
orderId: orderId
|
|
213
233
|
}
|
|
@@ -248,14 +268,17 @@ class PayNodeAgentClient {
|
|
|
248
268
|
}
|
|
249
269
|
const settleData = JSON.parse(decoded);
|
|
250
270
|
if (settleData.success) {
|
|
251
|
-
|
|
271
|
+
if (!this.options.quiet)
|
|
272
|
+
console.log(`✅ [PayNode-JS] Settlement confirmed: ${settleData.transaction}`);
|
|
252
273
|
}
|
|
253
274
|
else {
|
|
254
|
-
|
|
275
|
+
if (!this.options.quiet)
|
|
276
|
+
console.warn(`⚠️ [PayNode-JS] Settlement failed: ${settleData.errorReason || 'Unknown error'}`);
|
|
255
277
|
}
|
|
256
278
|
}
|
|
257
279
|
catch (e) {
|
|
258
|
-
|
|
280
|
+
if (!this.options.quiet)
|
|
281
|
+
console.warn(`⚠️ [PayNode-JS] Failed to parse settlement response:`, e);
|
|
259
282
|
}
|
|
260
283
|
}
|
|
261
284
|
return retryResponse;
|
package/dist/constants.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export declare const BASE_USDC_ADDRESS_SANDBOX = "0x65c088EfBDB0E03185Dbe8e258Ad
|
|
|
6
6
|
export declare const PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
|
|
7
7
|
export declare const PROTOCOL_FEE_BPS = 100;
|
|
8
8
|
export declare const MIN_PAYMENT_AMOUNT: bigint;
|
|
9
|
-
export declare const
|
|
9
|
+
export declare const PROTOCOL_VERSION = 2;
|
|
10
|
+
export declare const SDK_VERSION = "2.4.0";
|
|
10
11
|
export declare const BASE_RPC_URLS: string[];
|
|
11
12
|
export declare const BASE_RPC_URLS_SANDBOX: string[];
|
|
12
13
|
export declare const ACCEPTED_TOKENS: Record<number, string[]>;
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,eAAO,MAAM,sBAAsB,+CAA+C,CAAC;AACnF,eAAO,MAAM,8BAA8B,+CAA+C,CAAC;AAC3F,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,yBAAyB,+CAA+C,CAAC;AACtF,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,kBAAkB,QAAe,CAAC;AAC/C,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC,eAAO,MAAM,aAAa,UAAmF,CAAC;AAC9G,eAAO,MAAM,qBAAqB,UAA0E,CAAC;AAE7G,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAGpD,CAAC;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA25K,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,eAAO,MAAM,sBAAsB,+CAA+C,CAAC;AACnF,eAAO,MAAM,8BAA8B,+CAA+C,CAAC;AAC3F,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,yBAAyB,+CAA+C,CAAC;AACtF,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,kBAAkB,QAAe,CAAC;AAC/C,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAClC,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC,eAAO,MAAM,aAAa,UAAmF,CAAC;AAC9G,eAAO,MAAM,qBAAqB,UAA0E,CAAC;AAE7G,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAGpD,CAAC;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA25K,CAAC"}
|
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PAYNODE_ROUTER_ABI = exports.ACCEPTED_TOKENS = exports.BASE_RPC_URLS_SANDBOX = exports.BASE_RPC_URLS = exports.SDK_VERSION = exports.MIN_PAYMENT_AMOUNT = exports.PROTOCOL_FEE_BPS = exports.PROTOCOL_TREASURY = exports.BASE_USDC_ADDRESS_SANDBOX = exports.BASE_USDC_ADDRESS = exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = exports.PAYNODE_ROUTER_ADDRESS = void 0;
|
|
3
|
+
exports.PAYNODE_ROUTER_ABI = exports.ACCEPTED_TOKENS = exports.BASE_RPC_URLS_SANDBOX = exports.BASE_RPC_URLS = exports.SDK_VERSION = exports.PROTOCOL_VERSION = exports.MIN_PAYMENT_AMOUNT = exports.PROTOCOL_FEE_BPS = exports.PROTOCOL_TREASURY = exports.BASE_USDC_ADDRESS_SANDBOX = exports.BASE_USDC_ADDRESS = exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = exports.PAYNODE_ROUTER_ADDRESS = void 0;
|
|
4
4
|
/** Generated by meta/scripts/sync-config.py */
|
|
5
5
|
exports.PAYNODE_ROUTER_ADDRESS = "0x4A73696ccF76E7381b044cB95127B3784369Ed63";
|
|
6
6
|
exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F";
|
|
@@ -9,7 +9,8 @@ exports.BASE_USDC_ADDRESS_SANDBOX = "0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0"
|
|
|
9
9
|
exports.PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
|
|
10
10
|
exports.PROTOCOL_FEE_BPS = 100;
|
|
11
11
|
exports.MIN_PAYMENT_AMOUNT = BigInt(1000);
|
|
12
|
-
exports.
|
|
12
|
+
exports.PROTOCOL_VERSION = 2;
|
|
13
|
+
exports.SDK_VERSION = "2.4.0";
|
|
13
14
|
exports.BASE_RPC_URLS = ["https://mainnet.base.org", "https://base.meowrpc.com", "https://1rpc.io/base"];
|
|
14
15
|
exports.BASE_RPC_URLS_SANDBOX = ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"];
|
|
15
16
|
exports.ACCEPTED_TOKENS = {
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -26,3 +26,5 @@ __exportStar(require("./utils/payload"), exports);
|
|
|
26
26
|
__exportStar(require("./types/x402"), exports);
|
|
27
27
|
var ethers_1 = require("ethers");
|
|
28
28
|
Object.defineProperty(exports, "ethers", { enumerable: true, get: function () { return ethers_1.ethers; } });
|
|
29
|
+
__exportStar(require("./merchant"), exports);
|
|
30
|
+
__exportStar(require("./merchant/types"), exports);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MerchantConfig, MerchantMiddlewareOptions, ApiManifest } from './types';
|
|
2
|
+
import { PayNodeMiddlewareOptions } from '../middleware/x402';
|
|
3
|
+
/**
|
|
4
|
+
* PayNodeMerchant: The high-level SDK class for Merchant Integration.
|
|
5
|
+
*/
|
|
6
|
+
export declare class PayNodeMerchant {
|
|
7
|
+
private config;
|
|
8
|
+
constructor(config: MerchantConfig);
|
|
9
|
+
/**
|
|
10
|
+
* Registers or syncs the API manifest with the PayNode Market.
|
|
11
|
+
* This ensures the market shows the correct price and input schema.
|
|
12
|
+
*/
|
|
13
|
+
sync(manifest: ApiManifest): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Returns a unified middleware that handles:
|
|
16
|
+
* 1. Market Proxy (Strict Signature Check + Body Unwrap)
|
|
17
|
+
* 2. Auto-Discovery (Market Sync Probe)
|
|
18
|
+
* 3. (Optional) Direct X402 payment
|
|
19
|
+
*/
|
|
20
|
+
middleware(options?: MerchantMiddlewareOptions & Partial<PayNodeMiddlewareOptions>): (req: import("express").Request | any, res: import("express").Response | any, next: import("express").NextFunction) => Promise<any>;
|
|
21
|
+
/**
|
|
22
|
+
* Manual verification for Next.js or other non-Express environments.
|
|
23
|
+
* Extracts headers and verifies signature. Returns the unwrapped body and context.
|
|
24
|
+
*/
|
|
25
|
+
verify(req: any): Promise<{
|
|
26
|
+
isValid: boolean;
|
|
27
|
+
error: string;
|
|
28
|
+
body?: undefined;
|
|
29
|
+
paynodeContext?: undefined;
|
|
30
|
+
} | {
|
|
31
|
+
isValid: boolean;
|
|
32
|
+
body: any;
|
|
33
|
+
paynodeContext: any;
|
|
34
|
+
error?: undefined;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/merchant/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,yBAAyB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEjF,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAG9D;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAiB;gBAEnB,MAAM,EAAE,cAAc;IAOlC;;;OAGG;IACG,IAAI,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IA4BnD;;;;;OAKG;IACH,UAAU,CAAC,OAAO,GAAE,yBAAyB,GAAG,OAAO,CAAC,wBAAwB,CAAM;IAOtF;;;OAGG;IACG,MAAM,CAAC,GAAG,EAAE,GAAG;;;;;;;;;;;CAiDtB"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PayNodeMerchant = void 0;
|
|
4
|
+
const middleware_1 = require("./middleware");
|
|
5
|
+
const signature_1 = require("../utils/signature");
|
|
6
|
+
/**
|
|
7
|
+
* PayNodeMerchant: The high-level SDK class for Merchant Integration.
|
|
8
|
+
*/
|
|
9
|
+
class PayNodeMerchant {
|
|
10
|
+
config;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = {
|
|
13
|
+
marketUrl: 'https://mk.paynode.dev',
|
|
14
|
+
...config
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Registers or syncs the API manifest with the PayNode Market.
|
|
19
|
+
* This ensures the market shows the correct price and input schema.
|
|
20
|
+
*/
|
|
21
|
+
async sync(manifest) {
|
|
22
|
+
if (!this.config.quiet)
|
|
23
|
+
console.log(`[PayNode-SDK] Syncing API manifest for ${manifest.slug} to ${this.config.marketUrl}`);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`${this.config.marketUrl}/api/v1/merchant/apis`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
...manifest,
|
|
30
|
+
gateway_url: manifest.slug
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
const result = await response.json();
|
|
34
|
+
if (response.ok && result.success) {
|
|
35
|
+
if (!this.config.quiet)
|
|
36
|
+
console.log(`[PayNode-SDK] Successfully synced ${manifest.slug}. Status: ${result.api_id}`);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
if (!this.config.quiet)
|
|
41
|
+
console.warn(`[PayNode-SDK] Sync failed for ${manifest.slug}: ${result.error || response.statusText}`);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (!this.config.quiet)
|
|
47
|
+
console.error(`[PayNode-SDK] Network error during sync for ${manifest.slug}:`, err.message);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns a unified middleware that handles:
|
|
53
|
+
* 1. Market Proxy (Strict Signature Check + Body Unwrap)
|
|
54
|
+
* 2. Auto-Discovery (Market Sync Probe)
|
|
55
|
+
* 3. (Optional) Direct X402 payment
|
|
56
|
+
*/
|
|
57
|
+
middleware(options = {}) {
|
|
58
|
+
return (0, middleware_1.createMerchantMiddleware)(this.config, {
|
|
59
|
+
price: options.manifest?.price_per_call || '0.01',
|
|
60
|
+
...options
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Manual verification for Next.js or other non-Express environments.
|
|
65
|
+
* Extracts headers and verifies signature. Returns the unwrapped body and context.
|
|
66
|
+
*/
|
|
67
|
+
async verify(req) {
|
|
68
|
+
const getHeader = (name) => {
|
|
69
|
+
if (typeof req.getHeader === 'function')
|
|
70
|
+
return req.getHeader(name);
|
|
71
|
+
if (typeof req.get === 'function')
|
|
72
|
+
return req.get(name);
|
|
73
|
+
if (req.headers?.get && typeof req.headers.get === 'function')
|
|
74
|
+
return req.headers.get(name);
|
|
75
|
+
if (req.headers)
|
|
76
|
+
return req.headers[name.toLowerCase()] || req.headers[name];
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
const signature = getHeader('X-PayNode-Signature');
|
|
80
|
+
const timestamp = getHeader('X-PayNode-Timestamp');
|
|
81
|
+
const requestId = getHeader('X-PayNode-Request-Id') || getHeader('X-402-Order-Id') || getHeader('PAYMENT-SIGNATURE'); // Try fallback
|
|
82
|
+
const isValid = (0, signature_1.verifyMarketSignature)({
|
|
83
|
+
signature,
|
|
84
|
+
orderId: requestId,
|
|
85
|
+
timestamp,
|
|
86
|
+
sharedSecret: this.config.sharedSecret,
|
|
87
|
+
});
|
|
88
|
+
if (!isValid) {
|
|
89
|
+
return { isValid: false, error: 'Invalid PayNode Market Signature' };
|
|
90
|
+
}
|
|
91
|
+
// Handle Body Unwrap if it's a JSON body
|
|
92
|
+
let body = {};
|
|
93
|
+
try {
|
|
94
|
+
if (typeof req.json === 'function') {
|
|
95
|
+
body = await req.json();
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
body = req.body;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e) { }
|
|
102
|
+
let paynodeContext = { orderId: requestId };
|
|
103
|
+
if (body && body.payload && typeof body.payload === 'object') {
|
|
104
|
+
paynodeContext = {
|
|
105
|
+
...paynodeContext,
|
|
106
|
+
txHash: getHeader('X-PayNode-Transaction-Hash') || body.tx_hash,
|
|
107
|
+
amount: getHeader('X-PayNode-Amount') || body.amount,
|
|
108
|
+
network: getHeader('X-PayNode-Network') || body.network,
|
|
109
|
+
chainId: getHeader('X-PayNode-Chain-Id') || body.chain_id?.toString(),
|
|
110
|
+
};
|
|
111
|
+
body = body.payload;
|
|
112
|
+
}
|
|
113
|
+
return { isValid: true, body, paynodeContext };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.PayNodeMerchant = PayNodeMerchant;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { MerchantConfig, MerchantMiddlewareOptions } from './types';
|
|
3
|
+
import { PayNodeMiddlewareOptions } from '../middleware/x402';
|
|
4
|
+
/**
|
|
5
|
+
* Unified PayNode Merchant Middleware
|
|
6
|
+
* Handles:
|
|
7
|
+
* 1. Market Proxy (Strict HMAC Signature + Body Unwrapping)
|
|
8
|
+
* 2. Discovery Probes (Auto-respond with API Manifest)
|
|
9
|
+
*
|
|
10
|
+
* Note: Standalone direct X402 payment flow should be handled
|
|
11
|
+
* via x402Gate component.
|
|
12
|
+
*/
|
|
13
|
+
export declare const createMerchantMiddleware: (config: MerchantConfig, options: MerchantMiddlewareOptions & PayNodeMiddlewareOptions) => (req: Request | any, res: Response | any, next: NextFunction) => Promise<any>;
|
|
14
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/merchant/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AAEpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,QAAQ,cAAc,EAAE,SAAS,yBAAyB,GAAG,wBAAwB,MAG9G,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBAiE1E,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMerchantMiddleware = void 0;
|
|
4
|
+
const signature_1 = require("../utils/signature");
|
|
5
|
+
/**
|
|
6
|
+
* Unified PayNode Merchant Middleware
|
|
7
|
+
* Handles:
|
|
8
|
+
* 1. Market Proxy (Strict HMAC Signature + Body Unwrapping)
|
|
9
|
+
* 2. Discovery Probes (Auto-respond with API Manifest)
|
|
10
|
+
*
|
|
11
|
+
* Note: Standalone direct X402 payment flow should be handled
|
|
12
|
+
* via x402Gate component.
|
|
13
|
+
*/
|
|
14
|
+
const createMerchantMiddleware = (config, options) => {
|
|
15
|
+
const { manifest } = options;
|
|
16
|
+
return async (req, res, next) => {
|
|
17
|
+
// 1. Check for Market Proxy Headers
|
|
18
|
+
const signature = req.header('X-PayNode-Signature');
|
|
19
|
+
const timestamp = req.header('X-PayNode-Timestamp');
|
|
20
|
+
const requestId = req.header('X-PayNode-Request-Id') || req.header('X-402-Order-Id');
|
|
21
|
+
const isDiscovery = req.header('X-PayNode-Discovery') === 'true';
|
|
22
|
+
if (signature && requestId && timestamp) {
|
|
23
|
+
// ✅ Verify Signature from PayNode Market
|
|
24
|
+
const isValid = (0, signature_1.verifyMarketSignature)({
|
|
25
|
+
signature,
|
|
26
|
+
orderId: requestId,
|
|
27
|
+
timestamp,
|
|
28
|
+
sharedSecret: config.sharedSecret,
|
|
29
|
+
});
|
|
30
|
+
if (!isValid) {
|
|
31
|
+
if (!config.quiet)
|
|
32
|
+
console.error(`[PayNode-SDK] Invalid Market Proxy Signature for request ${requestId}`);
|
|
33
|
+
return res.status(401).json({ error: 'unauthorized', message: 'PayNode Market Signature verification failed.' });
|
|
34
|
+
}
|
|
35
|
+
// --- Scene A: Discovery Probe ---
|
|
36
|
+
if (isDiscovery) {
|
|
37
|
+
return res.status(200).json({
|
|
38
|
+
status: 'DISCOVERED',
|
|
39
|
+
x402Version: 2,
|
|
40
|
+
manifest: manifest || {},
|
|
41
|
+
last_synced: new Date().toISOString()
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// --- Scene B: Proxy Flow - Body Unwrapping ---
|
|
45
|
+
// The Market Proxy wraps original body in { payload: { ... } }
|
|
46
|
+
if (req.body && req.body.payload && typeof req.body.payload === 'object') {
|
|
47
|
+
const metadata = { ...req.body };
|
|
48
|
+
delete metadata.payload;
|
|
49
|
+
// Enrich request context with Proxy details
|
|
50
|
+
req.paynode = {
|
|
51
|
+
orderId: requestId,
|
|
52
|
+
txHash: req.header('X-PayNode-Transaction-Hash') || req.body.tx_hash,
|
|
53
|
+
amount: req.header('X-PayNode-Amount') || req.body.amount,
|
|
54
|
+
network: req.header('X-PayNode-Network') || req.body.network,
|
|
55
|
+
chainId: req.header('X-PayNode-Chain-Id') || req.body.chain_id?.toString(),
|
|
56
|
+
proxyMetadata: metadata
|
|
57
|
+
};
|
|
58
|
+
// Transparently Unwrap Body
|
|
59
|
+
req.body = req.body.payload;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Direct call via Proxy (unlikely for POST, but possible for some flows)
|
|
63
|
+
req.paynode = { orderId: requestId };
|
|
64
|
+
}
|
|
65
|
+
return next();
|
|
66
|
+
}
|
|
67
|
+
// 2. Scene C: Direct Agent Call (Rejected)
|
|
68
|
+
// PayNodeMerchant component REQUIRES Market Proxy to ensure protocol consistency and fee collection.
|
|
69
|
+
// Use x402Gate directly for standalone/direct 402 integration.
|
|
70
|
+
return res.status(403).json({
|
|
71
|
+
error: 'forbidden',
|
|
72
|
+
message: 'PayNode Market Auth required. This API must be accessed via PayNode Market Proxy for verification.'
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
exports.createMerchantMiddleware = createMerchantMiddleware;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface MerchantConfig {
|
|
2
|
+
sharedSecret: string;
|
|
3
|
+
marketUrl?: string;
|
|
4
|
+
quiet?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ApiManifest {
|
|
7
|
+
slug: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
price_per_call: string;
|
|
11
|
+
currency?: string;
|
|
12
|
+
network?: 'mainnet' | 'testnet';
|
|
13
|
+
input_schema?: Record<string, any>;
|
|
14
|
+
sample_response?: Record<string, any>;
|
|
15
|
+
}
|
|
16
|
+
export interface PayNodeRequestContext {
|
|
17
|
+
orderId: string;
|
|
18
|
+
txHash?: string;
|
|
19
|
+
payer?: string;
|
|
20
|
+
amount?: string;
|
|
21
|
+
network?: string;
|
|
22
|
+
chainId?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface MerchantMiddlewareOptions {
|
|
25
|
+
manifest?: Partial<ApiManifest>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/merchant/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CACjC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAgBxD,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,KAAK,MAAM,CAAC;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,QAAQ,GAAI,SAAS,wBAAwB,MA6B1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBAkK1E,CAAC"}
|
package/dist/middleware/x402.js
CHANGED
|
@@ -40,7 +40,7 @@ const x402Gate = (options) => {
|
|
|
40
40
|
return req.headers[name.toLowerCase()] || req.headers[name];
|
|
41
41
|
return null;
|
|
42
42
|
};
|
|
43
|
-
const v2PayloadHeader = getHeader('PAYMENT-SIGNATURE') || getHeader('X-402-Payload');
|
|
43
|
+
const v2PayloadHeader = getHeader('PAYMENT-SIGNATURE') || getHeader('X-402-Payload'); // COMPAT: X-402-Payload is a legacy alias for PAYMENT-SIGNATURE
|
|
44
44
|
let orderId = getHeader('X-402-Order-Id');
|
|
45
45
|
if (!orderId) {
|
|
46
46
|
orderId = (options.generateOrderId || defaultOrderIdGen)(req);
|
|
@@ -63,23 +63,29 @@ const x402Gate = (options) => {
|
|
|
63
63
|
inferredType = "onchain";
|
|
64
64
|
}
|
|
65
65
|
unifiedPayload = {
|
|
66
|
-
|
|
66
|
+
x402Version: constants_1.PROTOCOL_VERSION,
|
|
67
67
|
type: parsed._paynode?.type || inferredType,
|
|
68
68
|
orderId: internalOrderId,
|
|
69
69
|
router: parsed.accepted?.router,
|
|
70
|
-
payload: parsed.payload
|
|
70
|
+
payload: parsed.payload,
|
|
71
|
+
_paynode: {
|
|
72
|
+
sdkVersion: constants_1.SDK_VERSION
|
|
73
|
+
}
|
|
71
74
|
};
|
|
72
75
|
orderId = internalOrderId;
|
|
73
76
|
}
|
|
74
|
-
else if (typeof parsed.version === 'string'
|
|
75
|
-
// Legacy PayNode format
|
|
76
|
-
unifiedPayload =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
else if (typeof (parsed.version || parsed.x402Version) === 'string' || typeof parsed.version === 'string') {
|
|
78
|
+
// Legacy PayNode format or old x402 V2 drafts
|
|
79
|
+
unifiedPayload = {
|
|
80
|
+
x402Version: constants_1.PROTOCOL_VERSION,
|
|
81
|
+
type: parsed.type || (parsed.payload?.txHash ? "onchain" : "eip3009"),
|
|
82
|
+
orderId: parsed.orderId || parsed.order_id || orderId || `legacy_${Date.now()}`,
|
|
83
|
+
payload: parsed.payload,
|
|
84
|
+
_paynode: {
|
|
85
|
+
sdkVersion: constants_1.SDK_VERSION
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
orderId = unifiedPayload.orderId;
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
catch (e) {
|
|
@@ -104,7 +110,7 @@ const x402Gate = (options) => {
|
|
|
104
110
|
const b64Response = Buffer.from(JSON.stringify(settleResponse)).toString('base64');
|
|
105
111
|
if (res.set) {
|
|
106
112
|
res.set('PAYMENT-RESPONSE', b64Response);
|
|
107
|
-
res.set('X-PAYMENT-RESPONSE', b64Response); //
|
|
113
|
+
res.set('X-PAYMENT-RESPONSE', b64Response); // COMPAT (legacy): deprecated alias for PAYMENT-RESPONSE
|
|
108
114
|
}
|
|
109
115
|
req.paynode = { unifiedPayload, orderId };
|
|
110
116
|
return next();
|
|
@@ -120,7 +126,7 @@ const x402Gate = (options) => {
|
|
|
120
126
|
const b64Response = Buffer.from(JSON.stringify(settleResponse)).toString('base64');
|
|
121
127
|
if (res.set) {
|
|
122
128
|
res.set('PAYMENT-RESPONSE', b64Response);
|
|
123
|
-
res.set('X-PAYMENT-RESPONSE', b64Response); //
|
|
129
|
+
res.set('X-PAYMENT-RESPONSE', b64Response); // COMPAT (legacy): deprecated alias for PAYMENT-RESPONSE
|
|
124
130
|
}
|
|
125
131
|
return res.status(403).json({
|
|
126
132
|
error: "Forbidden",
|
|
@@ -131,13 +137,14 @@ const x402Gate = (options) => {
|
|
|
131
137
|
}
|
|
132
138
|
// No valid payment found, return 402 with appropriate headers
|
|
133
139
|
const v2Response = {
|
|
134
|
-
x402Version:
|
|
140
|
+
x402Version: constants_1.PROTOCOL_VERSION,
|
|
135
141
|
error: "Payment Required by PayNode",
|
|
136
142
|
resource: {
|
|
137
143
|
url: req.protocol + '://' + req.get('host') + (req.originalUrl || req.url),
|
|
138
144
|
description: options.description || "Protected Resource",
|
|
139
145
|
mimeType: getHeader('accept') || "application/json"
|
|
140
146
|
},
|
|
147
|
+
orderId: orderId || undefined,
|
|
141
148
|
accepts: [
|
|
142
149
|
{
|
|
143
150
|
scheme: "exact",
|
|
@@ -167,7 +174,7 @@ const x402Gate = (options) => {
|
|
|
167
174
|
const b64Required = Buffer.from(JSON.stringify(v2Response)).toString('base64');
|
|
168
175
|
if (res.set) {
|
|
169
176
|
res.set('PAYMENT-REQUIRED', b64Required);
|
|
170
|
-
res.set('X-402-Required', b64Required);
|
|
177
|
+
res.set('X-402-Required', b64Required); // COMPAT (legacy): deprecated alias for PAYMENT-REQUIRED
|
|
171
178
|
res.set('X-402-Order-Id', orderId);
|
|
172
179
|
}
|
|
173
180
|
return res.status(402).json(v2Response);
|
package/dist/types/x402.d.ts
CHANGED
|
@@ -4,6 +4,19 @@ export interface ResourceInfo {
|
|
|
4
4
|
description?: string;
|
|
5
5
|
mimeType?: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* PaymentRequirements describes a single payment option returned
|
|
9
|
+
* in the `accepts[]` array of a 402 challenge.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: The `orderId` (legacy) is DEPRECATED. All SDKs MUST read
|
|
12
|
+
* and write the request ID exclusively via the
|
|
13
|
+
* `X-402-Order-Id` HTTP header. This field exists only for
|
|
14
|
+
* backward compatibility with paynode-js <= 2.2.x and must
|
|
15
|
+
* not be used by new implementations.
|
|
16
|
+
*
|
|
17
|
+
* Protocol: x402 v2
|
|
18
|
+
* Canonical orderId transport: X-402-Order-Id header
|
|
19
|
+
*/
|
|
7
20
|
export interface PaymentRequirements {
|
|
8
21
|
scheme: string;
|
|
9
22
|
type?: "onchain" | "eip3009";
|
|
@@ -12,6 +25,7 @@ export interface PaymentRequirements {
|
|
|
12
25
|
asset: string;
|
|
13
26
|
payTo: string;
|
|
14
27
|
router?: string;
|
|
28
|
+
/** @deprecated Use X-402-Order-Id header exclusively. Legacy alias only. */
|
|
15
29
|
orderId?: string;
|
|
16
30
|
maxTimeoutSeconds: number;
|
|
17
31
|
extra?: Record<string, any>;
|
|
@@ -37,7 +51,7 @@ export interface PaymentPayload {
|
|
|
37
51
|
};
|
|
38
52
|
extensions?: Record<string, any>;
|
|
39
53
|
_paynode?: {
|
|
40
|
-
|
|
54
|
+
sdkVersion: string;
|
|
41
55
|
type: "onchain" | "eip3009";
|
|
42
56
|
orderId: string;
|
|
43
57
|
};
|
|
@@ -67,7 +81,7 @@ export interface VerifyResponse {
|
|
|
67
81
|
payer?: string;
|
|
68
82
|
}
|
|
69
83
|
export interface UnifiedPaymentPayload {
|
|
70
|
-
|
|
84
|
+
x402Version: X402Version;
|
|
71
85
|
type: "onchain" | "eip3009";
|
|
72
86
|
orderId: string;
|
|
73
87
|
router?: string;
|
|
@@ -76,6 +90,9 @@ export interface UnifiedPaymentPayload {
|
|
|
76
90
|
signature?: string;
|
|
77
91
|
authorization?: any;
|
|
78
92
|
} | ExactEVMPayload;
|
|
93
|
+
_paynode?: {
|
|
94
|
+
sdkVersion: string;
|
|
95
|
+
};
|
|
79
96
|
}
|
|
80
97
|
export interface SupportedKind {
|
|
81
98
|
x402Version: X402Version;
|
package/dist/types/x402.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/types/x402.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC;AAE5B,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,eAAe,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE;QACT,
|
|
1
|
+
{"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/types/x402.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAGtD,qBAAa,iBAAiB;IAC5B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,qBAAqB;CA8CtF"}
|
package/dist/utils/payload.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.X402PayloadHelper = void 0;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
4
5
|
class X402PayloadHelper {
|
|
5
6
|
/**
|
|
6
7
|
* Normalizes a raw payment payload (usually from PAYMENT-SIGNATURE or X-402-Payload headers)
|
|
@@ -29,18 +30,25 @@ class X402PayloadHelper {
|
|
|
29
30
|
inferredType = "onchain";
|
|
30
31
|
}
|
|
31
32
|
return {
|
|
32
|
-
|
|
33
|
+
x402Version: 2,
|
|
33
34
|
type: parsed._paynode?.type || inferredType,
|
|
34
35
|
orderId: parsed._paynode?.orderId || fallbackOrderId || "",
|
|
35
36
|
router: parsed.accepted?.router || parsed.router,
|
|
36
|
-
payload: parsed.payload
|
|
37
|
+
payload: parsed.payload,
|
|
38
|
+
_paynode: {
|
|
39
|
+
sdkVersion: constants_1.SDK_VERSION
|
|
40
|
+
}
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
// 2. Handle Legacy or already Unified Format
|
|
40
44
|
if (typeof parsed.version === 'string' && (parsed.version.startsWith("2.2") || parsed.version.startsWith("2.3"))) {
|
|
41
45
|
return {
|
|
42
46
|
...parsed,
|
|
43
|
-
|
|
47
|
+
x402Version: parsed.x402Version || 2,
|
|
48
|
+
orderId: parsed.orderId || parsed.order_id || fallbackOrderId || "",
|
|
49
|
+
_paynode: parsed._paynode || {
|
|
50
|
+
sdkVersion: parsed.version
|
|
51
|
+
}
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
// 3. Fallback for raw internal format
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SignatureContext {
|
|
2
|
+
signature: string;
|
|
3
|
+
orderId: string;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
sharedSecret: string;
|
|
6
|
+
now?: number;
|
|
7
|
+
driftWindow?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Verifies the HMAC-SHA256 signature from PayNode Market Proxy
|
|
11
|
+
*/
|
|
12
|
+
export declare function verifyMarketSignature(context: SignatureContext): boolean;
|
|
13
|
+
//# sourceMappingURL=signature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/utils/signature.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAuCxE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.verifyMarketSignature = verifyMarketSignature;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
/**
|
|
9
|
+
* Verifies the HMAC-SHA256 signature from PayNode Market Proxy
|
|
10
|
+
*/
|
|
11
|
+
function verifyMarketSignature(context) {
|
|
12
|
+
const { signature, orderId, timestamp, sharedSecret } = context;
|
|
13
|
+
if (!signature || !orderId || !timestamp || !sharedSecret) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
// Check for timestamp drift (default 5 minutes to prevent replay)
|
|
17
|
+
const tsDate = new Date(timestamp);
|
|
18
|
+
const checkTime = context.now || Date.now();
|
|
19
|
+
const driftWindow = context.driftWindow || 5 * 60 * 1000;
|
|
20
|
+
// Accept both ISO string and milliseconds
|
|
21
|
+
const tsMs = isNaN(tsDate.getTime()) ? parseInt(timestamp) : tsDate.getTime();
|
|
22
|
+
if (isNaN(tsMs))
|
|
23
|
+
return false;
|
|
24
|
+
const drift = Math.abs(checkTime - tsMs);
|
|
25
|
+
if (drift > driftWindow) {
|
|
26
|
+
if (driftWindow > 0) {
|
|
27
|
+
console.warn(`[PayNode-SDK] Signature timestamp drift too high: ${drift}ms`);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const expectedSig = crypto_1.default
|
|
32
|
+
.createHmac('sha256', sharedSecret)
|
|
33
|
+
.update(`${orderId}:${timestamp}`)
|
|
34
|
+
.digest('hex');
|
|
35
|
+
// Use constant-time comparison to prevent timing attacks
|
|
36
|
+
try {
|
|
37
|
+
return crypto_1.default.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSig, 'hex'));
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AAEzE,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAc;gBAExB,MAAM,EAAE,qBAAqB;IAyCzC,OAAO,CAAC,MAAM,CAAC,UAAU,CAEvB;IAEI,MAAM,CACV,cAAc,EAAE,qBAAqB,EACrC,QAAQ,EAAE,eAAe,EACzB,KAAK,CAAC,EAAE,GAAG,GACV,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IA8BpE,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAqFlI;;;OAGG;IACG,+BAA+B,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;KACjC,EACD,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC9B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CA+G3E"}
|
package/dist/utils/verifier.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.PayNodeVerifier = void 0;
|
|
4
7
|
const ethers_1 = require("ethers");
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
5
9
|
const errors_1 = require("../errors");
|
|
6
10
|
const idempotency_1 = require("./idempotency");
|
|
7
11
|
const constants_1 = require("../constants");
|
|
@@ -108,10 +112,16 @@ class PayNodeVerifier {
|
|
|
108
112
|
const parsed = router.parseLog(log);
|
|
109
113
|
if (parsed && parsed.name === 'PaymentReceived') {
|
|
110
114
|
const { merchant, token, amount, orderId, payer } = parsed.args;
|
|
111
|
-
const
|
|
112
|
-
const
|
|
115
|
+
const merchantBuf = Buffer.from(merchant.toLowerCase().replace('0x', ''), 'hex');
|
|
116
|
+
const expectedMerchantBuf = Buffer.from(expected.merchantAddress.toLowerCase().replace('0x', ''), 'hex');
|
|
117
|
+
const isMerchantMatch = merchantBuf.length === expectedMerchantBuf.length && crypto_1.default.timingSafeEqual(merchantBuf, expectedMerchantBuf);
|
|
118
|
+
const tokenBuf = Buffer.from(token.toLowerCase().replace('0x', ''), 'hex');
|
|
119
|
+
const expectedTokenBuf = Buffer.from(expected.tokenAddress.toLowerCase().replace('0x', ''), 'hex');
|
|
120
|
+
const isTokenMatch = tokenBuf.length === expectedTokenBuf.length && crypto_1.default.timingSafeEqual(tokenBuf, expectedTokenBuf);
|
|
113
121
|
const isAmountMatch = BigInt(amount) >= BigInt(expected.amount);
|
|
114
|
-
const
|
|
122
|
+
const orderIdBuf = Buffer.from(orderId.replace('0x', ''), 'hex');
|
|
123
|
+
const targetOrderIdBuf = Buffer.from(targetOrderId.replace('0x', ''), 'hex');
|
|
124
|
+
const isOrderMatch = orderIdBuf.length === targetOrderIdBuf.length && crypto_1.default.timingSafeEqual(orderIdBuf, targetOrderIdBuf);
|
|
115
125
|
if (isMerchantMatch && isTokenMatch && isAmountMatch) {
|
|
116
126
|
if (isOrderMatch) {
|
|
117
127
|
validEventFound = true;
|
|
@@ -169,7 +179,9 @@ class PayNodeVerifier {
|
|
|
169
179
|
const expectedValue = BigInt(expected.value);
|
|
170
180
|
const payloadValue = BigInt(value);
|
|
171
181
|
// 2. 基础字段与金额校验 (防粉尘攻击)
|
|
172
|
-
|
|
182
|
+
const toBuf = Buffer.from(to.toLowerCase().replace('0x', ''), 'hex');
|
|
183
|
+
const expectedToBuf = Buffer.from(expected.to.toLowerCase().replace('0x', ''), 'hex');
|
|
184
|
+
if (toBuf.length !== expectedToBuf.length || !crypto_1.default.timingSafeEqual(toBuf, expectedToBuf)) {
|
|
173
185
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Recipient mismatch") };
|
|
174
186
|
}
|
|
175
187
|
if (payloadValue < expectedValue) {
|
|
@@ -202,7 +214,9 @@ class PayNodeVerifier {
|
|
|
202
214
|
]
|
|
203
215
|
};
|
|
204
216
|
const recoveredAddress = ethers_1.ethers.verifyTypedData(domain, types, authorization, signature);
|
|
205
|
-
|
|
217
|
+
const recoveredBuf = Buffer.from(recoveredAddress.toLowerCase().replace('0x', ''), 'hex');
|
|
218
|
+
const fromBuf = Buffer.from(from.toLowerCase().replace('0x', ''), 'hex');
|
|
219
|
+
if (recoveredBuf.length !== fromBuf.length || !crypto_1.default.timingSafeEqual(recoveredBuf, fromBuf)) {
|
|
206
220
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Invalid signature: recovered address mismatch") };
|
|
207
221
|
}
|
|
208
222
|
// 4. 内存幂等性校验 (防高频重放)
|