@4mica/x402 1.0.0 → 1.0.1
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 +0 -14
- package/demo/src/deposit.ts +0 -2
- package/dist/client/scheme.js +13 -1
- package/dist/server/express/index.js +22 -11
- package/dist/server/facilitator.d.ts +12 -1
- package/dist/server/facilitator.js +74 -0
- package/package.json +1 -1
- package/src/server/express/index.ts +41 -9
- package/tests/client-scheme.test.ts +2 -6
package/README.md
CHANGED
|
@@ -277,20 +277,6 @@ const client = new x402Client()
|
|
|
277
277
|
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
-
## V2 Validation Policy Notes
|
|
281
|
-
|
|
282
|
-
For x402 V2 requests, include validation policy metadata in `paymentRequirements.extra`:
|
|
283
|
-
|
|
284
|
-
- `validationRegistryAddress`
|
|
285
|
-
- `validatorAddress`
|
|
286
|
-
- `validatorAgentId`
|
|
287
|
-
- `minValidationScore`
|
|
288
|
-
- optional `requiredValidationTag`
|
|
289
|
-
|
|
290
|
-
`validationChainId` is optional. When omitted, the underlying 4mica SDK derives the
|
|
291
|
-
`validation_chain_id` from the CAIP-2 `network` value (for example, `eip155:1`).
|
|
292
|
-
If `validationChainId` is provided, it must match that network chain id.
|
|
293
|
-
|
|
294
280
|
## Complete Example
|
|
295
281
|
|
|
296
282
|
### Server
|
package/demo/src/deposit.ts
CHANGED
|
@@ -2,8 +2,6 @@ import 'dotenv/config'
|
|
|
2
2
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
3
|
import { Client, ConfigBuilder } from '@4mica/sdk'
|
|
4
4
|
|
|
5
|
-
const USDC_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'
|
|
6
|
-
|
|
7
5
|
async function main() {
|
|
8
6
|
const privateKey = process.env.PRIVATE_KEY
|
|
9
7
|
if (!privateKey || !privateKey.startsWith('0x')) {
|
package/dist/client/scheme.js
CHANGED
|
@@ -49,9 +49,21 @@ export class FourMicaEvmScheme {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
else if (x402Version === 2) {
|
|
52
|
+
const resourcePayload = paymentRequirements.extra &&
|
|
53
|
+
typeof paymentRequirements.extra === 'object' &&
|
|
54
|
+
'resource' in paymentRequirements.extra &&
|
|
55
|
+
typeof paymentRequirements.extra.resource === 'object' &&
|
|
56
|
+
paymentRequirements.extra.resource !== null
|
|
57
|
+
? paymentRequirements.extra.resource
|
|
58
|
+
: {};
|
|
59
|
+
const resource = {
|
|
60
|
+
url: String(resourcePayload.url ?? ''),
|
|
61
|
+
description: String(resourcePayload.description ?? ''),
|
|
62
|
+
mimeType: String(resourcePayload.mimeType ?? ''),
|
|
63
|
+
};
|
|
52
64
|
const paymentRequired = {
|
|
53
65
|
x402Version: 2,
|
|
54
|
-
resource
|
|
66
|
+
resource,
|
|
55
67
|
accepts: [paymentRequirements],
|
|
56
68
|
};
|
|
57
69
|
const signed = await x402Flow.signPaymentV2(paymentRequired, paymentRequirements, this.signer.address);
|
|
@@ -2,11 +2,14 @@ import { x402HTTPResourceServer, x402ResourceServer, } from '@x402/core/server';
|
|
|
2
2
|
import { ExpressAdapter } from './adapter.js';
|
|
3
3
|
import { FourMicaEvmScheme, SUPPORTED_NETWORKS } from '../scheme.js';
|
|
4
4
|
import { FourMicaFacilitatorClient } from '../facilitator.js';
|
|
5
|
+
function getHTTPServerInternals(httpServer) {
|
|
6
|
+
return httpServer;
|
|
7
|
+
}
|
|
5
8
|
function registerNetworkServers(httpServer, tabEndpoint) {
|
|
6
9
|
const schemeServer = new FourMicaEvmScheme(tabEndpoint);
|
|
10
|
+
const server = getHTTPServerInternals(httpServer);
|
|
7
11
|
SUPPORTED_NETWORKS.forEach((network) => {
|
|
8
|
-
;
|
|
9
|
-
httpServer.ResourceServer.register(network, schemeServer);
|
|
12
|
+
server.ResourceServer.register(network, schemeServer);
|
|
10
13
|
});
|
|
11
14
|
}
|
|
12
15
|
function checkIfBazaarNeeded(routes) {
|
|
@@ -17,6 +20,13 @@ function checkIfBazaarNeeded(routes) {
|
|
|
17
20
|
return !!(routeConfig.extensions && 'bazaar' in routeConfig.extensions);
|
|
18
21
|
});
|
|
19
22
|
}
|
|
23
|
+
function isOpenTabHttpError(error) {
|
|
24
|
+
if (typeof error !== 'object' || error === null) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const candidate = error;
|
|
28
|
+
return typeof candidate.status === 'number' && 'response' in candidate;
|
|
29
|
+
}
|
|
20
30
|
/**
|
|
21
31
|
* Express payment middleware for x402 protocol (direct HTTP server instance).
|
|
22
32
|
*
|
|
@@ -57,12 +67,12 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
|
|
|
57
67
|
// Dynamically register bazaar extension if routes declare it and not already registered
|
|
58
68
|
// Skip if pre-registered (e.g., in serverless environments where static imports are used)
|
|
59
69
|
let bazaarPromise = null;
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
const httpServerInternals = getHTTPServerInternals(httpServer);
|
|
71
|
+
if (checkIfBazaarNeeded(httpServerInternals.routesConfig) &&
|
|
72
|
+
!httpServerInternals.ResourceServer.hasExtension('bazaar')) {
|
|
62
73
|
bazaarPromise = import('@x402/extensions/bazaar')
|
|
63
74
|
.then(({ bazaarResourceServerExtension }) => {
|
|
64
|
-
;
|
|
65
|
-
httpServer.ResourceServer.registerExtension(bazaarResourceServerExtension);
|
|
75
|
+
httpServerInternals.ResourceServer.registerExtension(bazaarResourceServerExtension);
|
|
66
76
|
})
|
|
67
77
|
.catch((err) => {
|
|
68
78
|
console.error('Failed to load bazaar extension:', err);
|
|
@@ -82,9 +92,8 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
|
|
|
82
92
|
return res.json(openTabResponse);
|
|
83
93
|
}
|
|
84
94
|
catch (error) {
|
|
85
|
-
if (error
|
|
86
|
-
|
|
87
|
-
return res.status(openTabError.status).json(openTabError.response);
|
|
95
|
+
if (isOpenTabHttpError(error)) {
|
|
96
|
+
return res.status(error.status).json(error.response);
|
|
88
97
|
}
|
|
89
98
|
console.error('Failed to open tab:', error);
|
|
90
99
|
return res.status(500).json({
|
|
@@ -126,7 +135,7 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
|
|
|
126
135
|
case 'no-payment-required':
|
|
127
136
|
// No payment needed, proceed directly to the route handler
|
|
128
137
|
return next();
|
|
129
|
-
case 'payment-error':
|
|
138
|
+
case 'payment-error': {
|
|
130
139
|
// Payment required but not provided or invalid
|
|
131
140
|
const { response } = result;
|
|
132
141
|
res.status(response.status);
|
|
@@ -140,7 +149,8 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
|
|
|
140
149
|
res.json(response.body || {});
|
|
141
150
|
}
|
|
142
151
|
return;
|
|
143
|
-
|
|
152
|
+
}
|
|
153
|
+
case 'payment-verified': {
|
|
144
154
|
// Payment is valid, need to wrap response for settlement
|
|
145
155
|
const { paymentPayload, paymentRequirements } = result;
|
|
146
156
|
// Intercept and buffer all core methods that can commit response to client
|
|
@@ -256,6 +266,7 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
|
|
|
256
266
|
bufferedCalls = [];
|
|
257
267
|
}
|
|
258
268
|
return;
|
|
269
|
+
}
|
|
259
270
|
}
|
|
260
271
|
};
|
|
261
272
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FacilitatorConfig, HTTPFacilitatorClient } from '@x402/core/server';
|
|
2
|
-
import { Network, PaymentRequirements } from '@x402/core/types';
|
|
2
|
+
import { Network, PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/core/types';
|
|
3
3
|
export interface OpenTabRequest {
|
|
4
4
|
userAddress: string;
|
|
5
5
|
recipientAddress: string;
|
|
@@ -16,6 +16,16 @@ export interface OpenTabResponse {
|
|
|
16
16
|
ttlSeconds: number;
|
|
17
17
|
nextReqId: string;
|
|
18
18
|
}
|
|
19
|
+
export interface CertificateResponse {
|
|
20
|
+
claims: string;
|
|
21
|
+
signature: string;
|
|
22
|
+
}
|
|
23
|
+
export type FourMicaSettleResponse = SettleResponse & {
|
|
24
|
+
certificate?: CertificateResponse;
|
|
25
|
+
txHash?: string;
|
|
26
|
+
networkId?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
};
|
|
19
29
|
export declare class OpenTabError extends Error {
|
|
20
30
|
readonly status: number;
|
|
21
31
|
readonly response: OpenTabResponse;
|
|
@@ -24,6 +34,7 @@ export declare class OpenTabError extends Error {
|
|
|
24
34
|
export declare class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
|
|
25
35
|
constructor(config?: FacilitatorConfig);
|
|
26
36
|
openTab(userAddress: string, paymentRequirements: PaymentRequirements, ttlSeconds?: number): Promise<OpenTabResponse>;
|
|
37
|
+
settle(paymentPayload: PaymentPayload, paymentRequirements: PaymentRequirements): Promise<FourMicaSettleResponse>;
|
|
27
38
|
/**
|
|
28
39
|
* Helper to convert objects to JSON-safe format.
|
|
29
40
|
* Handles BigInt and other non-JSON types.
|
|
@@ -39,6 +39,28 @@ export class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
|
|
|
39
39
|
}
|
|
40
40
|
throw new Error(`Facilitator openTab failed (${response.status}): ${JSON.stringify(data)}`);
|
|
41
41
|
}
|
|
42
|
+
async settle(paymentPayload, paymentRequirements) {
|
|
43
|
+
let headers = {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
};
|
|
46
|
+
const authHeaders = await this.createAuthHeaders('settle');
|
|
47
|
+
headers = { ...headers, ...authHeaders.headers };
|
|
48
|
+
const response = await fetch(`${this.url}/settle`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers,
|
|
51
|
+
body: JSON.stringify(this.safeJson({
|
|
52
|
+
x402Version: paymentPayload.x402Version,
|
|
53
|
+
paymentPayload,
|
|
54
|
+
paymentRequirements,
|
|
55
|
+
})),
|
|
56
|
+
});
|
|
57
|
+
const data = (await response.json());
|
|
58
|
+
const normalized = normalizeSettleResponse(data, paymentRequirements);
|
|
59
|
+
if (!response.ok || !normalized.success) {
|
|
60
|
+
throw new Error(`Facilitator settle failed (${response.status}): ${normalized.errorReason ?? normalized.error ?? 'unknown error'}`);
|
|
61
|
+
}
|
|
62
|
+
return normalized;
|
|
63
|
+
}
|
|
42
64
|
/**
|
|
43
65
|
* Helper to convert objects to JSON-safe format.
|
|
44
66
|
* Handles BigInt and other non-JSON types.
|
|
@@ -50,3 +72,55 @@ export class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
|
|
|
50
72
|
return JSON.parse(JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? value.toString() : value)));
|
|
51
73
|
}
|
|
52
74
|
}
|
|
75
|
+
function normalizeSettleResponse(payload, requirements) {
|
|
76
|
+
const transaction = String(payload.transaction ??
|
|
77
|
+
payload.transactionHash ??
|
|
78
|
+
payload.txHash ??
|
|
79
|
+
payload.tx_hash ??
|
|
80
|
+
payload.hash ??
|
|
81
|
+
'');
|
|
82
|
+
const network = String(payload.network ?? payload.networkId ?? payload.network_id ?? requirements.network);
|
|
83
|
+
const errorReason = typeof payload.errorReason === 'string'
|
|
84
|
+
? payload.errorReason
|
|
85
|
+
: typeof payload.error_reason === 'string'
|
|
86
|
+
? payload.error_reason
|
|
87
|
+
: typeof payload.error === 'string'
|
|
88
|
+
? payload.error
|
|
89
|
+
: typeof payload.message === 'string'
|
|
90
|
+
? payload.message
|
|
91
|
+
: undefined;
|
|
92
|
+
const certificate = payload.certificate &&
|
|
93
|
+
typeof payload.certificate === 'object' &&
|
|
94
|
+
typeof payload.certificate.claims === 'string' &&
|
|
95
|
+
typeof payload.certificate.signature === 'string'
|
|
96
|
+
? {
|
|
97
|
+
claims: payload.certificate.claims,
|
|
98
|
+
signature: payload.certificate.signature,
|
|
99
|
+
}
|
|
100
|
+
: undefined;
|
|
101
|
+
return {
|
|
102
|
+
success: Boolean(payload.success ?? errorReason === undefined),
|
|
103
|
+
errorReason,
|
|
104
|
+
payer: typeof payload.payer === 'string'
|
|
105
|
+
? payload.payer
|
|
106
|
+
: typeof payload.userAddress === 'string'
|
|
107
|
+
? payload.userAddress
|
|
108
|
+
: typeof payload.user_address === 'string'
|
|
109
|
+
? payload.user_address
|
|
110
|
+
: undefined,
|
|
111
|
+
transaction,
|
|
112
|
+
network,
|
|
113
|
+
certificate,
|
|
114
|
+
txHash: typeof payload.txHash === 'string'
|
|
115
|
+
? payload.txHash
|
|
116
|
+
: typeof payload.tx_hash === 'string'
|
|
117
|
+
? payload.tx_hash
|
|
118
|
+
: transaction || undefined,
|
|
119
|
+
networkId: typeof payload.networkId === 'string'
|
|
120
|
+
? payload.networkId
|
|
121
|
+
: typeof payload.network_id === 'string'
|
|
122
|
+
? payload.network_id
|
|
123
|
+
: network,
|
|
124
|
+
error: typeof payload.error === 'string' ? payload.error : undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -36,10 +36,26 @@ interface TabConfig {
|
|
|
36
36
|
ttlSeconds?: number
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
interface ResourceServerInternals {
|
|
40
|
+
register: (network: Network, server: SchemeNetworkServer) => unknown
|
|
41
|
+
hasExtension: (extension: string) => boolean
|
|
42
|
+
registerExtension: (extension: unknown) => unknown
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface HTTPServerInternals {
|
|
46
|
+
ResourceServer: ResourceServerInternals
|
|
47
|
+
routesConfig: RoutesConfig
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getHTTPServerInternals(httpServer: x402HTTPResourceServer): HTTPServerInternals {
|
|
51
|
+
return httpServer as unknown as HTTPServerInternals
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
function registerNetworkServers(httpServer: x402HTTPResourceServer, tabEndpoint: string) {
|
|
40
55
|
const schemeServer = new FourMicaEvmScheme(tabEndpoint)
|
|
56
|
+
const server = getHTTPServerInternals(httpServer)
|
|
41
57
|
SUPPORTED_NETWORKS.forEach((network) => {
|
|
42
|
-
|
|
58
|
+
server.ResourceServer.register(network, schemeServer)
|
|
43
59
|
})
|
|
44
60
|
}
|
|
45
61
|
|
|
@@ -53,6 +69,20 @@ function checkIfBazaarNeeded(routes: RoutesConfig): boolean {
|
|
|
53
69
|
})
|
|
54
70
|
}
|
|
55
71
|
|
|
72
|
+
interface OpenTabHttpError {
|
|
73
|
+
status: number
|
|
74
|
+
response: unknown
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isOpenTabHttpError(error: unknown): error is OpenTabHttpError {
|
|
78
|
+
if (typeof error !== 'object' || error === null) {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const candidate = error as { status?: unknown; response?: unknown }
|
|
83
|
+
return typeof candidate.status === 'number' && 'response' in candidate
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
/**
|
|
57
87
|
* Configuration for registering a payment scheme with a specific network
|
|
58
88
|
*/
|
|
@@ -118,13 +148,14 @@ export function paymentMiddlewareFromHTTPServer(
|
|
|
118
148
|
// Dynamically register bazaar extension if routes declare it and not already registered
|
|
119
149
|
// Skip if pre-registered (e.g., in serverless environments where static imports are used)
|
|
120
150
|
let bazaarPromise: Promise<void> | null = null
|
|
151
|
+
const httpServerInternals = getHTTPServerInternals(httpServer)
|
|
121
152
|
if (
|
|
122
|
-
checkIfBazaarNeeded(
|
|
123
|
-
!
|
|
153
|
+
checkIfBazaarNeeded(httpServerInternals.routesConfig) &&
|
|
154
|
+
!httpServerInternals.ResourceServer.hasExtension('bazaar')
|
|
124
155
|
) {
|
|
125
156
|
bazaarPromise = import('@x402/extensions/bazaar')
|
|
126
157
|
.then(({ bazaarResourceServerExtension }) => {
|
|
127
|
-
|
|
158
|
+
httpServerInternals.ResourceServer.registerExtension(bazaarResourceServerExtension)
|
|
128
159
|
})
|
|
129
160
|
.catch((err) => {
|
|
130
161
|
console.error('Failed to load bazaar extension:', err)
|
|
@@ -150,9 +181,8 @@ export function paymentMiddlewareFromHTTPServer(
|
|
|
150
181
|
// Return the response
|
|
151
182
|
return res.json(openTabResponse)
|
|
152
183
|
} catch (error) {
|
|
153
|
-
if (error
|
|
154
|
-
|
|
155
|
-
return res.status(openTabError.status).json(openTabError.response)
|
|
184
|
+
if (isOpenTabHttpError(error)) {
|
|
185
|
+
return res.status(error.status).json(error.response)
|
|
156
186
|
}
|
|
157
187
|
console.error('Failed to open tab:', error)
|
|
158
188
|
return res.status(500).json({
|
|
@@ -200,7 +230,7 @@ export function paymentMiddlewareFromHTTPServer(
|
|
|
200
230
|
// No payment needed, proceed directly to the route handler
|
|
201
231
|
return next()
|
|
202
232
|
|
|
203
|
-
case 'payment-error':
|
|
233
|
+
case 'payment-error': {
|
|
204
234
|
// Payment required but not provided or invalid
|
|
205
235
|
const { response } = result
|
|
206
236
|
res.status(response.status)
|
|
@@ -213,8 +243,9 @@ export function paymentMiddlewareFromHTTPServer(
|
|
|
213
243
|
res.json(response.body || {})
|
|
214
244
|
}
|
|
215
245
|
return
|
|
246
|
+
}
|
|
216
247
|
|
|
217
|
-
case 'payment-verified':
|
|
248
|
+
case 'payment-verified': {
|
|
218
249
|
// Payment is valid, need to wrap response for settlement
|
|
219
250
|
const { paymentPayload, paymentRequirements } = result
|
|
220
251
|
|
|
@@ -346,6 +377,7 @@ export function paymentMiddlewareFromHTTPServer(
|
|
|
346
377
|
bufferedCalls = []
|
|
347
378
|
}
|
|
348
379
|
return
|
|
380
|
+
}
|
|
349
381
|
}
|
|
350
382
|
}
|
|
351
383
|
}
|
|
@@ -24,9 +24,7 @@ describe('FourMicaEvmScheme', () => {
|
|
|
24
24
|
signPaymentV2,
|
|
25
25
|
} as never)
|
|
26
26
|
|
|
27
|
-
const scheme = await FourMicaEvmScheme.create(
|
|
28
|
-
privateKeyToAccount(`0x${'11'.repeat(32)}`)
|
|
29
|
-
)
|
|
27
|
+
const scheme = await FourMicaEvmScheme.create(privateKeyToAccount(`0x${'11'.repeat(32)}`))
|
|
30
28
|
|
|
31
29
|
const requirements = {
|
|
32
30
|
scheme: '4mica-credit',
|
|
@@ -84,9 +82,7 @@ describe('FourMicaEvmScheme', () => {
|
|
|
84
82
|
signPaymentV2: vi.fn(),
|
|
85
83
|
} as never)
|
|
86
84
|
|
|
87
|
-
const scheme = await FourMicaEvmScheme.create(
|
|
88
|
-
privateKeyToAccount(`0x${'11'.repeat(32)}`)
|
|
89
|
-
)
|
|
85
|
+
const scheme = await FourMicaEvmScheme.create(privateKeyToAccount(`0x${'11'.repeat(32)}`))
|
|
90
86
|
|
|
91
87
|
await expect(
|
|
92
88
|
scheme.createPaymentPayload(3, {
|