@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 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
@@ -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')) {
@@ -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: { url: '', description: '', mimeType: '' },
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
- if (checkIfBazaarNeeded(httpServer.routesConfig) &&
61
- !httpServer.ResourceServer.hasExtension('bazaar')) {
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 instanceof Error && 'status' in error) {
86
- const openTabError = error;
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
- case 'payment-verified':
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4mica/x402",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "TypeScript x402 utilities for interacting with the 4Mica payment network",
6
6
  "license": "MIT",
@@ -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
- ;(httpServer as any).ResourceServer.register(network, schemeServer)
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((httpServer as any).routesConfig) &&
123
- !(httpServer as any).ResourceServer.hasExtension('bazaar')
153
+ checkIfBazaarNeeded(httpServerInternals.routesConfig) &&
154
+ !httpServerInternals.ResourceServer.hasExtension('bazaar')
124
155
  ) {
125
156
  bazaarPromise = import('@x402/extensions/bazaar')
126
157
  .then(({ bazaarResourceServerExtension }) => {
127
- ;(httpServer as any).ResourceServer.registerExtension(bazaarResourceServerExtension)
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 instanceof Error && 'status' in error) {
154
- const openTabError = error as any
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, {