ap2-payment-handler 1.0.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 +133 -0
- package/dist/__tests__/handler.test.d.ts +2 -0
- package/dist/__tests__/handler.test.d.ts.map +1 -0
- package/dist/__tests__/handler.test.js +251 -0
- package/dist/handler.d.ts +14 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +87 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +61 -0
- package/dist/x402.d.ts +31 -0
- package/dist/x402.d.ts.map +1 -0
- package/dist/x402.js +74 -0
- package/package.json +35 -0
- package/src/__tests__/handler.test.ts +278 -0
- package/src/handler.ts +107 -0
- package/src/index.ts +4 -0
- package/src/types.ts +53 -0
- package/src/validation.ts +61 -0
- package/src/x402.ts +86 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# ap2-payment-handler
|
|
2
|
+
|
|
3
|
+
> Non-custodial crypto payment handler for the AP2 Agent Payment Protocol. The first open-source, zero-escrow AP2 implementation.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/ap2-payment-handler)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## What is AP2?
|
|
9
|
+
|
|
10
|
+
AP2 (Agent Payment Protocol) is Google's emerging standard for agentic commerce — enabling AI agents to negotiate and execute payments autonomously on behalf of users. This package implements the AP2 mandate lifecycle with a non-custodial, crypto-native approach.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install ap2-payment-handler
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { AP2PaymentHandler } from 'ap2-payment-handler';
|
|
22
|
+
|
|
23
|
+
const handler = new AP2PaymentHandler({
|
|
24
|
+
supportedMethods: ['usdc_base', 'x402'],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Create an intent mandate (agent-initiated)
|
|
28
|
+
const mandate = handler.createIntentMandate({
|
|
29
|
+
agentId: 'my-agent-001',
|
|
30
|
+
merchantId: 'merchant-xyz',
|
|
31
|
+
maxAmount: 10.0,
|
|
32
|
+
currency: 'USDC',
|
|
33
|
+
ttl: Date.now() + 5 * 60_000, // 5 minutes
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Process payment
|
|
37
|
+
const response = await handler.handle({
|
|
38
|
+
mandate,
|
|
39
|
+
preferredMethod: 'usdc_base',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (response.success) {
|
|
43
|
+
console.log('Payment initiated:', response.transactionId);
|
|
44
|
+
console.log('Audit trail:', response.paymentMandate?.auditTrail);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Cart Payments
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const cartMandate = handler.createCartMandate({
|
|
52
|
+
agentId: 'my-agent-001',
|
|
53
|
+
lineItems: [
|
|
54
|
+
{ name: 'API Credits', price: 5.0, qty: 2 },
|
|
55
|
+
{ name: 'Priority Queue', price: 3.0, qty: 1 },
|
|
56
|
+
],
|
|
57
|
+
total: 13.0,
|
|
58
|
+
currency: 'USDC',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const response = await handler.handle({
|
|
62
|
+
mandate: cartMandate,
|
|
63
|
+
preferredMethod: 'usdc_base',
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## HTTP 402 Payment Required (x402)
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { X402Bridge } from 'ap2-payment-handler';
|
|
71
|
+
|
|
72
|
+
const bridge = new X402Bridge();
|
|
73
|
+
|
|
74
|
+
// When your agent receives a 402 response
|
|
75
|
+
const paymentResponse = await bridge.handleResponse({
|
|
76
|
+
status: 402,
|
|
77
|
+
headers: response.headers,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (paymentResponse) {
|
|
81
|
+
// Build proof and retry request
|
|
82
|
+
const proof = await bridge.buildPaymentProof({
|
|
83
|
+
amount: '5.00',
|
|
84
|
+
currency: 'USDC',
|
|
85
|
+
address: '0x...',
|
|
86
|
+
agentId: 'my-agent-001',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Supported Payment Methods
|
|
92
|
+
|
|
93
|
+
| Method | Network | Description |
|
|
94
|
+
|--------|---------|-------------|
|
|
95
|
+
| `usdc_base` | Base (Coinbase L2) | USDC on Base — low fees, fast finality |
|
|
96
|
+
| `x402` | Any | HTTP 402 Payment Required flow |
|
|
97
|
+
| `usdc_arbitrum` | Arbitrum | USDC on Arbitrum One |
|
|
98
|
+
| `usdc_optimism` | Optimism | USDC on Optimism |
|
|
99
|
+
|
|
100
|
+
## Non-Custodial Design
|
|
101
|
+
|
|
102
|
+
This package is **zero-escrow** by design:
|
|
103
|
+
|
|
104
|
+
- **No private keys stored** — signing is delegated to your wallet/signer
|
|
105
|
+
- **No funds held** — payment proofs reference on-chain transactions
|
|
106
|
+
- **Full audit trail** — every mandate lifecycle step is logged
|
|
107
|
+
- **EIP-712 compatible** — structured signing for human-readable approval
|
|
108
|
+
|
|
109
|
+
All `PaymentMandate` objects carry `isNonCustodial: true` as a type-level guarantee.
|
|
110
|
+
|
|
111
|
+
## API Reference
|
|
112
|
+
|
|
113
|
+
### `AP2PaymentHandler`
|
|
114
|
+
|
|
115
|
+
- `constructor({ supportedMethods })` — initialize with supported payment methods
|
|
116
|
+
- `handle(request)` — process an AP2 payment request
|
|
117
|
+
- `createIntentMandate(params)` — create an agent-initiated intent mandate
|
|
118
|
+
- `createCartMandate(params)` — create a cart mandate from line items
|
|
119
|
+
- `getSupportedMethods()` — list configured methods
|
|
120
|
+
|
|
121
|
+
### `X402Bridge`
|
|
122
|
+
|
|
123
|
+
- `parsePaymentRequired(headers)` — extract payment details from 402 headers
|
|
124
|
+
- `buildPaymentProof(params)` — create a non-custodial payment proof
|
|
125
|
+
- `handleResponse(response)` — handle HTTP responses, returns AP2PaymentResponse for 402
|
|
126
|
+
|
|
127
|
+
### `validateMandate(mandate)`
|
|
128
|
+
|
|
129
|
+
Validates a mandate and throws `AP2ValidationError` on failure.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT © [AI Agent Economy](https://ai-agent-economy.com)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/handler.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const handler_1 = require("../handler");
|
|
4
|
+
const x402_1 = require("../x402");
|
|
5
|
+
const validation_1 = require("../validation");
|
|
6
|
+
const futureTime = Date.now() + 60000; // 1 minute from now
|
|
7
|
+
const pastTime = Date.now() - 60000; // 1 minute ago
|
|
8
|
+
function makeIntentMandate(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
type: 'intent',
|
|
11
|
+
agentId: 'agent-123',
|
|
12
|
+
merchantId: 'merchant-456',
|
|
13
|
+
maxAmount: 10.0,
|
|
14
|
+
currency: 'USDC',
|
|
15
|
+
ttl: futureTime,
|
|
16
|
+
isAgentInitiated: true,
|
|
17
|
+
isNonCustodial: true,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function makeCartMandate(overrides = {}) {
|
|
22
|
+
return {
|
|
23
|
+
type: 'cart',
|
|
24
|
+
agentId: 'agent-123',
|
|
25
|
+
lineItems: [
|
|
26
|
+
{ name: 'Widget', price: 5.0, qty: 2 },
|
|
27
|
+
],
|
|
28
|
+
total: 10.0,
|
|
29
|
+
currency: 'USDC',
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
describe('AP2PaymentHandler', () => {
|
|
34
|
+
let handler;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
handler = new handler_1.AP2PaymentHandler({ supportedMethods: ['usdc_base', 'x402'] });
|
|
37
|
+
});
|
|
38
|
+
// 1. IntentMandate creation with correct flags
|
|
39
|
+
test('createIntentMandate sets isAgentInitiated and isNonCustodial', () => {
|
|
40
|
+
const mandate = handler.createIntentMandate({
|
|
41
|
+
agentId: 'a1',
|
|
42
|
+
merchantId: 'm1',
|
|
43
|
+
maxAmount: 5,
|
|
44
|
+
currency: 'USDC',
|
|
45
|
+
ttl: futureTime,
|
|
46
|
+
});
|
|
47
|
+
expect(mandate.isAgentInitiated).toBe(true);
|
|
48
|
+
expect(mandate.isNonCustodial).toBe(true);
|
|
49
|
+
expect(mandate.type).toBe('intent');
|
|
50
|
+
});
|
|
51
|
+
// 2. CartMandate total validation (correct sum)
|
|
52
|
+
test('validateMandate accepts CartMandate with correct total', () => {
|
|
53
|
+
const cart = makeCartMandate();
|
|
54
|
+
expect(() => (0, validation_1.validateMandate)(cart)).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
// 3. CartMandate total validation (wrong sum — should throw)
|
|
57
|
+
test('validateMandate rejects CartMandate with wrong total', () => {
|
|
58
|
+
const cart = makeCartMandate({ total: 99.0 });
|
|
59
|
+
expect(() => (0, validation_1.validateMandate)(cart)).toThrow(validation_1.AP2ValidationError);
|
|
60
|
+
expect(() => (0, validation_1.validateMandate)(cart)).toThrow(/mismatch/);
|
|
61
|
+
});
|
|
62
|
+
// 4. IntentMandate TTL validation (future TTL — valid)
|
|
63
|
+
test('validateMandate accepts IntentMandate with future TTL', () => {
|
|
64
|
+
const intent = makeIntentMandate({ ttl: futureTime });
|
|
65
|
+
expect(() => (0, validation_1.validateMandate)(intent)).not.toThrow();
|
|
66
|
+
});
|
|
67
|
+
// 5. IntentMandate TTL validation (past TTL — invalid)
|
|
68
|
+
test('validateMandate rejects IntentMandate with expired TTL', () => {
|
|
69
|
+
const intent = makeIntentMandate({ ttl: pastTime });
|
|
70
|
+
expect(() => (0, validation_1.validateMandate)(intent)).toThrow(validation_1.AP2ValidationError);
|
|
71
|
+
expect(() => (0, validation_1.validateMandate)(intent)).toThrow(/expired/);
|
|
72
|
+
});
|
|
73
|
+
// 6. Handler with usdc_base method
|
|
74
|
+
test('handle succeeds with usdc_base method', async () => {
|
|
75
|
+
const response = await handler.handle({
|
|
76
|
+
mandate: makeIntentMandate(),
|
|
77
|
+
preferredMethod: 'usdc_base',
|
|
78
|
+
});
|
|
79
|
+
expect(response.success).toBe(true);
|
|
80
|
+
expect(response.paymentMandate?.method).toBe('usdc_base');
|
|
81
|
+
});
|
|
82
|
+
// 7. Handler with x402 method
|
|
83
|
+
test('handle succeeds with x402 method', async () => {
|
|
84
|
+
const response = await handler.handle({
|
|
85
|
+
mandate: makeIntentMandate(),
|
|
86
|
+
preferredMethod: 'x402',
|
|
87
|
+
});
|
|
88
|
+
expect(response.success).toBe(true);
|
|
89
|
+
expect(response.paymentMandate?.method).toBe('x402');
|
|
90
|
+
});
|
|
91
|
+
// 8. Handler with unsupported method falls back
|
|
92
|
+
test('handle falls back to first supported method when preferred is unsupported', async () => {
|
|
93
|
+
const response = await handler.handle({
|
|
94
|
+
mandate: makeIntentMandate(),
|
|
95
|
+
preferredMethod: 'usdc_arbitrum',
|
|
96
|
+
});
|
|
97
|
+
expect(response.success).toBe(true);
|
|
98
|
+
expect(response.paymentMandate?.method).toBe('usdc_base');
|
|
99
|
+
});
|
|
100
|
+
// 9. Handler returns success with transactionId
|
|
101
|
+
test('handle returns transactionId on success', async () => {
|
|
102
|
+
const response = await handler.handle({
|
|
103
|
+
mandate: makeIntentMandate(),
|
|
104
|
+
preferredMethod: 'usdc_base',
|
|
105
|
+
});
|
|
106
|
+
expect(response.success).toBe(true);
|
|
107
|
+
expect(response.transactionId).toBeDefined();
|
|
108
|
+
expect(typeof response.transactionId).toBe('string');
|
|
109
|
+
});
|
|
110
|
+
// 10. Handler audit trail includes entries
|
|
111
|
+
test('handle returns paymentMandate with non-empty auditTrail', async () => {
|
|
112
|
+
const response = await handler.handle({
|
|
113
|
+
mandate: makeIntentMandate(),
|
|
114
|
+
preferredMethod: 'usdc_base',
|
|
115
|
+
});
|
|
116
|
+
expect(response.paymentMandate?.auditTrail.length).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
// 11. PaymentMandate has isNonCustodial flag
|
|
119
|
+
test('paymentMandate has isNonCustodial set to true', async () => {
|
|
120
|
+
const response = await handler.handle({
|
|
121
|
+
mandate: makeIntentMandate(),
|
|
122
|
+
preferredMethod: 'usdc_base',
|
|
123
|
+
});
|
|
124
|
+
expect(response.paymentMandate?.isNonCustodial).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
// 16. Validation rejects missing agentId
|
|
127
|
+
test('validateMandate rejects missing agentId', () => {
|
|
128
|
+
const intent = makeIntentMandate({ agentId: '' });
|
|
129
|
+
expect(() => (0, validation_1.validateMandate)(intent)).toThrow(validation_1.AP2ValidationError);
|
|
130
|
+
expect(() => (0, validation_1.validateMandate)(intent)).toThrow(/agentId/);
|
|
131
|
+
});
|
|
132
|
+
// 17. Validation rejects negative amount
|
|
133
|
+
test('validateMandate rejects negative maxAmount on IntentMandate', () => {
|
|
134
|
+
const intent = makeIntentMandate({ maxAmount: -5 });
|
|
135
|
+
expect(() => (0, validation_1.validateMandate)(intent)).toThrow(validation_1.AP2ValidationError);
|
|
136
|
+
expect(() => (0, validation_1.validateMandate)(intent)).toThrow(/positive/);
|
|
137
|
+
});
|
|
138
|
+
// 18. createIntentMandate sets correct defaults
|
|
139
|
+
test('createIntentMandate preserves all provided params', () => {
|
|
140
|
+
const mandate = handler.createIntentMandate({
|
|
141
|
+
agentId: 'myAgent',
|
|
142
|
+
merchantId: 'myMerchant',
|
|
143
|
+
maxAmount: 25,
|
|
144
|
+
currency: 'USDC',
|
|
145
|
+
ttl: futureTime,
|
|
146
|
+
});
|
|
147
|
+
expect(mandate.agentId).toBe('myAgent');
|
|
148
|
+
expect(mandate.merchantId).toBe('myMerchant');
|
|
149
|
+
expect(mandate.maxAmount).toBe(25);
|
|
150
|
+
expect(mandate.currency).toBe('USDC');
|
|
151
|
+
});
|
|
152
|
+
// 19. createCartMandate with multiple items
|
|
153
|
+
test('createCartMandate with multiple line items', () => {
|
|
154
|
+
const mandate = handler.createCartMandate({
|
|
155
|
+
agentId: 'agent-1',
|
|
156
|
+
lineItems: [
|
|
157
|
+
{ name: 'Item A', price: 3, qty: 2 },
|
|
158
|
+
{ name: 'Item B', price: 4, qty: 1 },
|
|
159
|
+
],
|
|
160
|
+
total: 10,
|
|
161
|
+
currency: 'USDC',
|
|
162
|
+
});
|
|
163
|
+
expect(mandate.type).toBe('cart');
|
|
164
|
+
expect(mandate.lineItems.length).toBe(2);
|
|
165
|
+
expect(mandate.total).toBe(10);
|
|
166
|
+
});
|
|
167
|
+
// 20. getSupportedMethods returns config
|
|
168
|
+
test('getSupportedMethods returns configured methods', () => {
|
|
169
|
+
const methods = handler.getSupportedMethods();
|
|
170
|
+
expect(methods).toContain('usdc_base');
|
|
171
|
+
expect(methods).toContain('x402');
|
|
172
|
+
expect(methods.length).toBe(2);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('X402Bridge', () => {
|
|
176
|
+
let bridge;
|
|
177
|
+
beforeEach(() => {
|
|
178
|
+
bridge = new x402_1.X402Bridge();
|
|
179
|
+
});
|
|
180
|
+
// 12. X402Bridge parsePaymentRequired
|
|
181
|
+
test('parsePaymentRequired extracts payment details from headers', async () => {
|
|
182
|
+
const headers = {
|
|
183
|
+
'x-payment-amount': '5.00',
|
|
184
|
+
'x-payment-currency': 'USDC',
|
|
185
|
+
'x-payment-address': '0xabc123',
|
|
186
|
+
};
|
|
187
|
+
const result = await bridge.parsePaymentRequired(headers);
|
|
188
|
+
expect(result.amount).toBe('5.00');
|
|
189
|
+
expect(result.currency).toBe('USDC');
|
|
190
|
+
expect(result.address).toBe('0xabc123');
|
|
191
|
+
});
|
|
192
|
+
// 13. X402Bridge buildPaymentProof
|
|
193
|
+
test('buildPaymentProof returns a proof and timestamp', async () => {
|
|
194
|
+
const result = await bridge.buildPaymentProof({
|
|
195
|
+
amount: '5.00',
|
|
196
|
+
currency: 'USDC',
|
|
197
|
+
address: '0xabc123',
|
|
198
|
+
agentId: 'agent-1',
|
|
199
|
+
});
|
|
200
|
+
expect(result.proof).toBeTruthy();
|
|
201
|
+
expect(typeof result.proof).toBe('string');
|
|
202
|
+
expect(result.timestamp).toBeGreaterThan(0);
|
|
203
|
+
});
|
|
204
|
+
// 14. X402Bridge handleResponse for 402 status
|
|
205
|
+
test('handleResponse returns AP2PaymentResponse for 402 status', async () => {
|
|
206
|
+
const response = await bridge.handleResponse({
|
|
207
|
+
status: 402,
|
|
208
|
+
headers: {
|
|
209
|
+
'x-payment-amount': '10.00',
|
|
210
|
+
'x-payment-currency': 'USDC',
|
|
211
|
+
'x-payment-address': '0xdef456',
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
expect(response).not.toBeNull();
|
|
215
|
+
expect(response?.success).toBe(false);
|
|
216
|
+
expect(response?.error).toContain('Payment required');
|
|
217
|
+
});
|
|
218
|
+
// 15. X402Bridge handleResponse for 200 status (null)
|
|
219
|
+
test('handleResponse returns null for non-402 status', async () => {
|
|
220
|
+
const response = await bridge.handleResponse({
|
|
221
|
+
status: 200,
|
|
222
|
+
headers: {},
|
|
223
|
+
});
|
|
224
|
+
expect(response).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
// Additional: parsePaymentRequired with JSON header
|
|
227
|
+
test('parsePaymentRequired parses JSON x-payment-required header', async () => {
|
|
228
|
+
const headers = {
|
|
229
|
+
'x-payment-required': JSON.stringify({
|
|
230
|
+
amount: '7.50',
|
|
231
|
+
currency: 'USDC',
|
|
232
|
+
address: '0xfeed',
|
|
233
|
+
}),
|
|
234
|
+
};
|
|
235
|
+
const result = await bridge.parsePaymentRequired(headers);
|
|
236
|
+
expect(result.amount).toBe('7.50');
|
|
237
|
+
expect(result.address).toBe('0xfeed');
|
|
238
|
+
});
|
|
239
|
+
// Additional: buildPaymentProof encodes agentId
|
|
240
|
+
test('buildPaymentProof encodes agentId in proof', async () => {
|
|
241
|
+
const result = await bridge.buildPaymentProof({
|
|
242
|
+
amount: '1.00',
|
|
243
|
+
currency: 'USDC',
|
|
244
|
+
address: '0xabc',
|
|
245
|
+
agentId: 'agent-special',
|
|
246
|
+
});
|
|
247
|
+
const decoded = JSON.parse(Buffer.from(result.proof, 'base64').toString());
|
|
248
|
+
expect(decoded.agentId).toBe('agent-special');
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
//# sourceMappingURL=handler.test.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AP2PaymentMethod, AP2PaymentRequest, AP2PaymentResponse, IntentMandate, CartMandate } from './types';
|
|
2
|
+
export declare class AP2PaymentHandler {
|
|
3
|
+
private supportedMethods;
|
|
4
|
+
constructor(config: {
|
|
5
|
+
supportedMethods: AP2PaymentMethod[];
|
|
6
|
+
});
|
|
7
|
+
getSupportedMethods(): AP2PaymentMethod[];
|
|
8
|
+
createIntentMandate(params: Omit<IntentMandate, 'type' | 'isAgentInitiated' | 'isNonCustodial'>): IntentMandate;
|
|
9
|
+
createCartMandate(params: Omit<CartMandate, 'type'>): CartMandate;
|
|
10
|
+
private selectMethod;
|
|
11
|
+
private getAmount;
|
|
12
|
+
handle(request: AP2PaymentRequest): Promise<AP2PaymentResponse>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,WAAW,EAEZ,MAAM,SAAS,CAAC;AAOjB,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,gBAAgB,CAAqB;gBAEjC,MAAM,EAAE;QAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAA;KAAE;IAI5D,mBAAmB,IAAI,gBAAgB,EAAE;IAIzC,mBAAmB,CACjB,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,kBAAkB,GAAG,gBAAgB,CAAC,GAC1E,aAAa;IAShB,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG,WAAW;IAOjE,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,SAAS;IAOX,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAgDtE"}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AP2PaymentHandler = void 0;
|
|
4
|
+
const validation_1 = require("./validation");
|
|
5
|
+
function generateId() {
|
|
6
|
+
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
7
|
+
}
|
|
8
|
+
class AP2PaymentHandler {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.supportedMethods = config.supportedMethods;
|
|
11
|
+
}
|
|
12
|
+
getSupportedMethods() {
|
|
13
|
+
return [...this.supportedMethods];
|
|
14
|
+
}
|
|
15
|
+
createIntentMandate(params) {
|
|
16
|
+
return {
|
|
17
|
+
type: 'intent',
|
|
18
|
+
isAgentInitiated: true,
|
|
19
|
+
isNonCustodial: true,
|
|
20
|
+
...params,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
createCartMandate(params) {
|
|
24
|
+
return {
|
|
25
|
+
type: 'cart',
|
|
26
|
+
...params,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
selectMethod(preferred) {
|
|
30
|
+
if (this.supportedMethods.includes(preferred)) {
|
|
31
|
+
return preferred;
|
|
32
|
+
}
|
|
33
|
+
// Fallback to first supported method
|
|
34
|
+
return this.supportedMethods.length > 0 ? this.supportedMethods[0] : null;
|
|
35
|
+
}
|
|
36
|
+
getAmount(mandate) {
|
|
37
|
+
if (mandate.type === 'intent') {
|
|
38
|
+
return mandate.maxAmount;
|
|
39
|
+
}
|
|
40
|
+
return mandate.total;
|
|
41
|
+
}
|
|
42
|
+
async handle(request) {
|
|
43
|
+
try {
|
|
44
|
+
(0, validation_1.validateMandate)(request.mandate);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: err.message,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const method = this.selectMethod(request.preferredMethod);
|
|
53
|
+
if (!method) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: 'No supported payment methods available',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const mandateId = generateId();
|
|
60
|
+
const timestamp = Date.now();
|
|
61
|
+
const amount = this.getAmount(request.mandate);
|
|
62
|
+
const auditTrail = [
|
|
63
|
+
`[${new Date(timestamp).toISOString()}] Mandate received: type=${request.mandate.type}, agentId=${request.mandate.agentId}`,
|
|
64
|
+
`[${new Date(timestamp).toISOString()}] Payment method selected: ${method}`,
|
|
65
|
+
`[${new Date(timestamp).toISOString()}] Non-custodial payment initiated: amount=${amount} ${request.mandate.currency}`,
|
|
66
|
+
`[${new Date(timestamp).toISOString()}] MandateId assigned: ${mandateId}`,
|
|
67
|
+
];
|
|
68
|
+
const paymentMandate = {
|
|
69
|
+
type: 'payment',
|
|
70
|
+
mandateId,
|
|
71
|
+
method,
|
|
72
|
+
amount,
|
|
73
|
+
currency: request.mandate.currency,
|
|
74
|
+
auditTrail,
|
|
75
|
+
timestamp,
|
|
76
|
+
isNonCustodial: true,
|
|
77
|
+
};
|
|
78
|
+
const transactionId = `tx_${generateId()}`;
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
transactionId,
|
|
82
|
+
paymentMandate,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.AP2PaymentHandler = AP2PaymentHandler;
|
|
87
|
+
//# sourceMappingURL=handler.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./handler"), exports);
|
|
19
|
+
__exportStar(require("./x402"), exports);
|
|
20
|
+
__exportStar(require("./validation"), exports);
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type AP2PaymentMethod = 'usdc_base' | 'x402' | 'usdc_arbitrum' | 'usdc_optimism';
|
|
2
|
+
export type MandateType = 'intent' | 'cart' | 'payment';
|
|
3
|
+
export interface IntentMandate {
|
|
4
|
+
type: 'intent';
|
|
5
|
+
agentId: string;
|
|
6
|
+
merchantId: string;
|
|
7
|
+
maxAmount: number;
|
|
8
|
+
currency: string;
|
|
9
|
+
ttl: number;
|
|
10
|
+
isAgentInitiated: true;
|
|
11
|
+
isNonCustodial: true;
|
|
12
|
+
signature?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LineItem {
|
|
15
|
+
name: string;
|
|
16
|
+
price: number;
|
|
17
|
+
qty: number;
|
|
18
|
+
}
|
|
19
|
+
export interface CartMandate {
|
|
20
|
+
type: 'cart';
|
|
21
|
+
agentId: string;
|
|
22
|
+
lineItems: LineItem[];
|
|
23
|
+
total: number;
|
|
24
|
+
currency: string;
|
|
25
|
+
signature?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PaymentMandate {
|
|
28
|
+
type: 'payment';
|
|
29
|
+
mandateId: string;
|
|
30
|
+
method: AP2PaymentMethod;
|
|
31
|
+
amount: number;
|
|
32
|
+
currency: string;
|
|
33
|
+
auditTrail: string[];
|
|
34
|
+
timestamp: number;
|
|
35
|
+
isNonCustodial: true;
|
|
36
|
+
}
|
|
37
|
+
export interface AP2PaymentRequest {
|
|
38
|
+
mandate: IntentMandate | CartMandate;
|
|
39
|
+
preferredMethod: AP2PaymentMethod;
|
|
40
|
+
}
|
|
41
|
+
export interface AP2PaymentResponse {
|
|
42
|
+
success: boolean;
|
|
43
|
+
transactionId?: string;
|
|
44
|
+
error?: string;
|
|
45
|
+
paymentMandate?: PaymentMandate;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,MAAM,GAAG,eAAe,GAAG,eAAe,CAAC;AAExF,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,EAAE,IAAI,CAAC;IACvB,cAAc,EAAE,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,aAAa,GAAG,WAAW,CAAC;IACrC,eAAe,EAAE,gBAAgB,CAAC;CACnC;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { IntentMandate, CartMandate } from './types';
|
|
2
|
+
export declare class AP2ValidationError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function validateMandate(mandate: IntentMandate | CartMandate): void;
|
|
6
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAErD,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,WAAW,GAAG,IAAI,CAmD1E"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AP2ValidationError = void 0;
|
|
4
|
+
exports.validateMandate = validateMandate;
|
|
5
|
+
class AP2ValidationError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'AP2ValidationError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.AP2ValidationError = AP2ValidationError;
|
|
12
|
+
function validateMandate(mandate) {
|
|
13
|
+
if (!mandate.agentId) {
|
|
14
|
+
throw new AP2ValidationError('Missing required field: agentId');
|
|
15
|
+
}
|
|
16
|
+
if (mandate.type === 'intent') {
|
|
17
|
+
if (!mandate.merchantId) {
|
|
18
|
+
throw new AP2ValidationError('Missing required field: merchantId');
|
|
19
|
+
}
|
|
20
|
+
if (mandate.maxAmount === undefined || mandate.maxAmount === null) {
|
|
21
|
+
throw new AP2ValidationError('Missing required field: maxAmount');
|
|
22
|
+
}
|
|
23
|
+
if (typeof mandate.maxAmount !== 'number' || mandate.maxAmount <= 0) {
|
|
24
|
+
throw new AP2ValidationError('maxAmount must be a positive number');
|
|
25
|
+
}
|
|
26
|
+
if (!mandate.currency) {
|
|
27
|
+
throw new AP2ValidationError('Missing required field: currency');
|
|
28
|
+
}
|
|
29
|
+
if (!mandate.ttl) {
|
|
30
|
+
throw new AP2ValidationError('Missing required field: ttl');
|
|
31
|
+
}
|
|
32
|
+
if (mandate.ttl < Date.now()) {
|
|
33
|
+
throw new AP2ValidationError('IntentMandate TTL has expired');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (mandate.type === 'cart') {
|
|
37
|
+
if (!mandate.lineItems || mandate.lineItems.length === 0) {
|
|
38
|
+
throw new AP2ValidationError('CartMandate must have at least one line item');
|
|
39
|
+
}
|
|
40
|
+
if (!mandate.currency) {
|
|
41
|
+
throw new AP2ValidationError('Missing required field: currency');
|
|
42
|
+
}
|
|
43
|
+
if (mandate.total === undefined || mandate.total === null) {
|
|
44
|
+
throw new AP2ValidationError('Missing required field: total');
|
|
45
|
+
}
|
|
46
|
+
if (typeof mandate.total !== 'number' || mandate.total <= 0) {
|
|
47
|
+
throw new AP2ValidationError('total must be a positive number');
|
|
48
|
+
}
|
|
49
|
+
const computedTotal = mandate.lineItems.reduce((sum, item) => {
|
|
50
|
+
return sum + item.price * item.qty;
|
|
51
|
+
}, 0);
|
|
52
|
+
const diff = Math.abs(computedTotal - mandate.total);
|
|
53
|
+
if (diff > 0.001) {
|
|
54
|
+
throw new AP2ValidationError(`CartMandate total mismatch: expected ${computedTotal.toFixed(4)}, got ${mandate.total.toFixed(4)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
throw new AP2ValidationError('Unknown mandate type');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=validation.js.map
|