@4mica/x402 0.1.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.
Files changed (43) hide show
  1. package/.eslintrc.cjs +29 -0
  2. package/.prettierignore +3 -0
  3. package/.prettierrc +6 -0
  4. package/CHANGELOG.md +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +389 -0
  7. package/demo/.env.example +8 -0
  8. package/demo/README.md +125 -0
  9. package/demo/package.json +26 -0
  10. package/demo/src/client.ts +54 -0
  11. package/demo/src/deposit.ts +39 -0
  12. package/demo/src/server.ts +74 -0
  13. package/demo/tsconfig.json +8 -0
  14. package/demo/yarn.lock +925 -0
  15. package/dist/client/index.d.ts +1 -0
  16. package/dist/client/index.js +1 -0
  17. package/dist/client/scheme.d.ts +11 -0
  18. package/dist/client/scheme.js +65 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/server/express/adapter.d.ts +71 -0
  22. package/dist/server/express/adapter.js +90 -0
  23. package/dist/server/express/index.d.ts +122 -0
  24. package/dist/server/express/index.js +340 -0
  25. package/dist/server/facilitator.d.ts +35 -0
  26. package/dist/server/facilitator.js +52 -0
  27. package/dist/server/index.d.ts +6 -0
  28. package/dist/server/index.js +4 -0
  29. package/dist/server/scheme.d.ts +93 -0
  30. package/dist/server/scheme.js +179 -0
  31. package/eslint.config.mjs +22 -0
  32. package/package.json +79 -0
  33. package/src/client/index.ts +1 -0
  34. package/src/client/scheme.ts +95 -0
  35. package/src/index.ts +7 -0
  36. package/src/server/express/adapter.ts +100 -0
  37. package/src/server/express/index.ts +466 -0
  38. package/src/server/facilitator.ts +90 -0
  39. package/src/server/index.ts +10 -0
  40. package/src/server/scheme.ts +223 -0
  41. package/tsconfig.build.json +5 -0
  42. package/tsconfig.json +17 -0
  43. package/vitest.config.ts +12 -0
@@ -0,0 +1,466 @@
1
+ import {
2
+ HTTPRequestContext,
3
+ PaywallConfig,
4
+ PaywallProvider,
5
+ x402HTTPResourceServer,
6
+ x402ResourceServer,
7
+ RoutesConfig,
8
+ FacilitatorClient,
9
+ } from '@x402/core/server'
10
+ import { SchemeNetworkServer, Network } from '@x402/core/types'
11
+ import { NextFunction, Request, Response } from 'express'
12
+ import { ExpressAdapter } from './adapter.js'
13
+ import { FourMicaEvmScheme, SUPPORTED_NETWORKS } from '../scheme.js'
14
+ import { FourMicaFacilitatorClient } from '../facilitator.js'
15
+
16
+ /**
17
+ * Configuration for payment tab handling
18
+ */
19
+ interface TabConfig {
20
+ /**
21
+ * The full URL endpoint for opening payment tabs. This URL is injected into
22
+ * paymentRequirements.extra and clients use it to open a payment tab.
23
+ * When a request matches this endpoint's path, the middleware will parse
24
+ * the request body and call the 4mica facilitator to open a tab.
25
+ *
26
+ * @example "https://api.example.com/x402/tab"
27
+ */
28
+ advertisedEndpoint: string
29
+
30
+ /**
31
+ * The lifetime of the payment tab in seconds. Defines how long the tab
32
+ * remains valid before expiring.
33
+ *
34
+ * @example 3600 // 1 hour
35
+ */
36
+ ttlSeconds?: number
37
+ }
38
+
39
+ function registerNetworkServers(httpServer: x402HTTPResourceServer, tabEndpoint: string) {
40
+ const schemeServer = new FourMicaEvmScheme(tabEndpoint)
41
+ SUPPORTED_NETWORKS.forEach((network) => {
42
+ ;(httpServer as any).ResourceServer.register(network, schemeServer)
43
+ })
44
+ }
45
+
46
+ function checkIfBazaarNeeded(routes: RoutesConfig): boolean {
47
+ if ('accepts' in routes) {
48
+ return !!(routes.extensions && 'bazaar' in routes.extensions)
49
+ }
50
+
51
+ return Object.values(routes).some((routeConfig) => {
52
+ return !!(routeConfig.extensions && 'bazaar' in routeConfig.extensions)
53
+ })
54
+ }
55
+
56
+ /**
57
+ * Configuration for registering a payment scheme with a specific network
58
+ */
59
+ export interface SchemeRegistration {
60
+ /**
61
+ * The network identifier (e.g., 'eip155:84532', 'solana:mainnet')
62
+ */
63
+ network: Network
64
+
65
+ /**
66
+ * The scheme server implementation for this network
67
+ */
68
+ server: SchemeNetworkServer
69
+ }
70
+
71
+ /**
72
+ * Express payment middleware for x402 protocol (direct HTTP server instance).
73
+ *
74
+ * Use this when you need to configure HTTP-level hooks.
75
+ *
76
+ * @param httpServer - Pre-configured x402HTTPResourceServer instance
77
+ * @param tabConfig - Configuration for payment tab handling (endpoint URL and TTL)
78
+ * @param paywallConfig - Optional configuration for the built-in paywall UI
79
+ * @param paywall - Optional custom paywall provider (overrides default)
80
+ * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true)
81
+ * @returns Express middleware handler
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * import { paymentMiddlewareFromHTTPServer, x402ResourceServer, x402HTTPResourceServer } from "@x402/express";
86
+ *
87
+ * const resourceServer = new x402ResourceServer(facilitatorClient)
88
+ * .register(NETWORK, new ExactEvmScheme())
89
+ *
90
+ * const httpServer = new x402HTTPResourceServer(resourceServer, routes)
91
+ * .onProtectedRequest(requestHook);
92
+ *
93
+ * app.use(paymentMiddlewareFromHTTPServer(
94
+ * httpServer,
95
+ * { advertisedEndpoint: "https://api.example.com/x402/tab" },
96
+ * )); * ```
97
+ */
98
+ export function paymentMiddlewareFromHTTPServer(
99
+ httpServer: x402HTTPResourceServer,
100
+ tabConfig: TabConfig,
101
+ paywallConfig?: PaywallConfig,
102
+ paywall?: PaywallProvider,
103
+ syncFacilitatorOnStart: boolean = true
104
+ ) {
105
+ const facilitatorClient = new FourMicaFacilitatorClient()
106
+
107
+ registerNetworkServers(httpServer, tabConfig.advertisedEndpoint)
108
+
109
+ // Register custom paywall provider if provided
110
+ if (paywall) {
111
+ httpServer.registerPaywallProvider(paywall)
112
+ }
113
+
114
+ // Store initialization promise (not the result)
115
+ // httpServer.initialize() fetches facilitator support and validates routes
116
+ let initPromise: Promise<void> | null = syncFacilitatorOnStart ? httpServer.initialize() : null
117
+
118
+ // Dynamically register bazaar extension if routes declare it and not already registered
119
+ // Skip if pre-registered (e.g., in serverless environments where static imports are used)
120
+ let bazaarPromise: Promise<void> | null = null
121
+ if (
122
+ checkIfBazaarNeeded((httpServer as any).routesConfig) &&
123
+ !(httpServer as any).ResourceServer.hasExtension('bazaar')
124
+ ) {
125
+ bazaarPromise = import('@x402/extensions/bazaar')
126
+ .then(({ bazaarResourceServerExtension }) => {
127
+ ;(httpServer as any).ResourceServer.registerExtension(bazaarResourceServerExtension)
128
+ })
129
+ .catch((err) => {
130
+ console.error('Failed to load bazaar extension:', err)
131
+ })
132
+ }
133
+
134
+ return async (req: Request, res: Response, next: NextFunction) => {
135
+ // Check if this request is for the tab opening endpoint
136
+ try {
137
+ const advertisedUrl = new URL(tabConfig.advertisedEndpoint)
138
+ if (req.path === advertisedUrl.pathname) {
139
+ // Parse the request body
140
+ const { userAddress, paymentRequirements } = req.body
141
+
142
+ try {
143
+ // Call the facilitator to open the tab
144
+ const openTabResponse = await facilitatorClient.openTab(
145
+ userAddress,
146
+ paymentRequirements,
147
+ tabConfig.ttlSeconds
148
+ )
149
+
150
+ // Return the response
151
+ return res.json(openTabResponse)
152
+ } 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)
156
+ }
157
+ console.error('Failed to open tab:', error)
158
+ return res.status(500).json({
159
+ error: 'Failed to open tab',
160
+ details: error instanceof Error ? error.message : 'Unknown error',
161
+ })
162
+ }
163
+ }
164
+ } catch (urlError) {
165
+ console.error('Invalid advertisedEndpoint URL:', urlError)
166
+ }
167
+
168
+ // Create adapter and context
169
+ const adapter = new ExpressAdapter(req)
170
+ const context: HTTPRequestContext = {
171
+ adapter,
172
+ path: req.path,
173
+ method: req.method,
174
+ paymentHeader: adapter.getHeader('payment-signature') || adapter.getHeader('x-payment'),
175
+ }
176
+
177
+ // Check if route requires payment before initializing facilitator
178
+ if (!httpServer.requiresPayment(context)) {
179
+ return next()
180
+ }
181
+
182
+ // Only initialize when processing a protected route
183
+ if (initPromise) {
184
+ await initPromise
185
+ initPromise = null // Clear after first await
186
+ }
187
+
188
+ // Await bazaar extension loading if needed
189
+ if (bazaarPromise) {
190
+ await bazaarPromise
191
+ bazaarPromise = null
192
+ }
193
+
194
+ // Process payment requirement check
195
+ const result = await httpServer.processHTTPRequest(context, paywallConfig)
196
+
197
+ // Handle the different result types
198
+ switch (result.type) {
199
+ case 'no-payment-required':
200
+ // No payment needed, proceed directly to the route handler
201
+ return next()
202
+
203
+ case 'payment-error':
204
+ // Payment required but not provided or invalid
205
+ const { response } = result
206
+ res.status(response.status)
207
+ Object.entries(response.headers).forEach(([key, value]) => {
208
+ res.setHeader(key, value)
209
+ })
210
+ if (response.isHtml) {
211
+ res.send(response.body)
212
+ } else {
213
+ res.json(response.body || {})
214
+ }
215
+ return
216
+
217
+ case 'payment-verified':
218
+ // Payment is valid, need to wrap response for settlement
219
+ const { paymentPayload, paymentRequirements } = result
220
+
221
+ // Intercept and buffer all core methods that can commit response to client
222
+ const originalWriteHead = res.writeHead.bind(res)
223
+ const originalWrite = res.write.bind(res)
224
+ const originalEnd = res.end.bind(res)
225
+ const originalFlushHeaders = res.flushHeaders.bind(res)
226
+
227
+ type BufferedCall =
228
+ | ['writeHead', Parameters<typeof originalWriteHead>]
229
+ | ['write', Parameters<typeof originalWrite>]
230
+ | ['end', Parameters<typeof originalEnd>]
231
+ | ['flushHeaders', []]
232
+ let bufferedCalls: BufferedCall[] = []
233
+ let settled = false
234
+
235
+ // Create a promise that resolves when the handler finishes and calls res.end()
236
+ let endCalled: () => void
237
+ const endPromise = new Promise<void>((resolve) => {
238
+ endCalled = resolve
239
+ })
240
+
241
+ res.writeHead = function (...args: Parameters<typeof originalWriteHead>) {
242
+ if (!settled) {
243
+ bufferedCalls.push(['writeHead', args])
244
+ return res
245
+ }
246
+ return originalWriteHead(...args)
247
+ } as typeof originalWriteHead
248
+
249
+ res.write = function (...args: Parameters<typeof originalWrite>) {
250
+ if (!settled) {
251
+ bufferedCalls.push(['write', args])
252
+ return true
253
+ }
254
+ return originalWrite(...args)
255
+ } as typeof originalWrite
256
+
257
+ res.end = function (...args: Parameters<typeof originalEnd>) {
258
+ if (!settled) {
259
+ bufferedCalls.push(['end', args])
260
+ // Signal that the handler has finished
261
+ endCalled()
262
+ return res
263
+ }
264
+ return originalEnd(...args)
265
+ } as typeof originalEnd
266
+
267
+ res.flushHeaders = function () {
268
+ if (!settled) {
269
+ bufferedCalls.push(['flushHeaders', []])
270
+ return
271
+ }
272
+ return originalFlushHeaders()
273
+ }
274
+
275
+ // Proceed to the next middleware or route handler
276
+ next()
277
+
278
+ // Wait for the handler to actually call res.end() before checking status
279
+ await endPromise
280
+
281
+ // If the response from the protected route is >= 400, do not settle payment
282
+ if (res.statusCode >= 400) {
283
+ settled = true
284
+ res.writeHead = originalWriteHead
285
+ res.write = originalWrite
286
+ res.end = originalEnd
287
+ res.flushHeaders = originalFlushHeaders
288
+ // Replay all buffered calls in order
289
+ for (const [method, args] of bufferedCalls) {
290
+ if (method === 'writeHead')
291
+ originalWriteHead(...(args as Parameters<typeof originalWriteHead>))
292
+ else if (method === 'write')
293
+ originalWrite(...(args as Parameters<typeof originalWrite>))
294
+ else if (method === 'end') originalEnd(...(args as Parameters<typeof originalEnd>))
295
+ else if (method === 'flushHeaders') originalFlushHeaders()
296
+ }
297
+ bufferedCalls = []
298
+ return
299
+ }
300
+
301
+ try {
302
+ const settleResult = await httpServer.processSettlement(
303
+ paymentPayload,
304
+ paymentRequirements
305
+ )
306
+
307
+ // If settlement fails, return an error and do not send the buffered response
308
+ if (!settleResult.success) {
309
+ bufferedCalls = []
310
+ res.status(402).json({
311
+ error: 'Settlement failed',
312
+ details: settleResult.errorReason,
313
+ })
314
+ return
315
+ }
316
+
317
+ // Settlement succeeded - add headers to response
318
+ Object.entries(settleResult.headers).forEach(([key, value]) => {
319
+ res.setHeader(key, value)
320
+ })
321
+ } catch (error) {
322
+ console.error(error)
323
+ // If settlement fails, don't send the buffered response
324
+ bufferedCalls = []
325
+ res.status(402).json({
326
+ error: 'Settlement failed',
327
+ details: error instanceof Error ? error.message : 'Unknown error',
328
+ })
329
+ return
330
+ } finally {
331
+ settled = true
332
+ res.writeHead = originalWriteHead
333
+ res.write = originalWrite
334
+ res.end = originalEnd
335
+ res.flushHeaders = originalFlushHeaders
336
+
337
+ // Replay all buffered calls in order
338
+ for (const [method, args] of bufferedCalls) {
339
+ if (method === 'writeHead')
340
+ originalWriteHead(...(args as Parameters<typeof originalWriteHead>))
341
+ else if (method === 'write')
342
+ originalWrite(...(args as Parameters<typeof originalWrite>))
343
+ else if (method === 'end') originalEnd(...(args as Parameters<typeof originalEnd>))
344
+ else if (method === 'flushHeaders') originalFlushHeaders()
345
+ }
346
+ bufferedCalls = []
347
+ }
348
+ return
349
+ }
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Express payment middleware for x402 protocol (direct server instance).
355
+ *
356
+ * Use this when you want to pass a pre-configured x402ResourceServer instance.
357
+ * This provides more flexibility for testing, custom configuration, and reusing
358
+ * server instances across multiple middlewares.
359
+ *
360
+ * @param routes - Route configurations for protected endpoints
361
+ * @param server - Pre-configured x402ResourceServer instance
362
+ * @param tabConfig - Configuration for payment tab handling (endpoint URL and TTL)
363
+ * @param paywallConfig - Optional configuration for the built-in paywall UI
364
+ * @param paywall - Optional custom paywall provider (overrides default)
365
+ * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true)
366
+ * @returns Express middleware handler
367
+ *
368
+ * @example
369
+ * ```typescript
370
+ * import { paymentMiddleware } from "@x402/express";
371
+ *
372
+ * const server = new x402ResourceServer(myFacilitatorClient)
373
+ * .register(NETWORK, new ExactEvmScheme());
374
+ *
375
+ * app.use(paymentMiddleware(
376
+ * routes,
377
+ * server,
378
+ * { advertisedEndpoint: "https://api.example.com/x402/tab" },
379
+ * ));
380
+ * ```
381
+ */
382
+ export function paymentMiddleware(
383
+ routes: RoutesConfig,
384
+ server: x402ResourceServer,
385
+ tabConfig: TabConfig,
386
+ paywallConfig?: PaywallConfig,
387
+ paywall?: PaywallProvider,
388
+ syncFacilitatorOnStart: boolean = true
389
+ ) {
390
+ // Create the x402 HTTP server instance with the resource server
391
+ const httpServer = new x402HTTPResourceServer(server, routes)
392
+
393
+ return paymentMiddlewareFromHTTPServer(
394
+ httpServer,
395
+ tabConfig,
396
+ paywallConfig,
397
+ paywall,
398
+ syncFacilitatorOnStart
399
+ )
400
+ }
401
+
402
+ /**
403
+ * Express payment middleware for x402 protocol (configuration-based).
404
+ *
405
+ * Use this when you want to quickly set up middleware with simple configuration.
406
+ * This function creates and configures the x402ResourceServer internally.
407
+ *
408
+ * @param routes - Route configurations for protected endpoints
409
+ * @param tabConfig - Configuration for payment tab handling
410
+ * @param facilitatorClients - Optional facilitator client(s) for payment processing
411
+ * @param schemes - Optional array of scheme registrations for server-side payment processing
412
+ * @param paywallConfig - Optional configuration for the built-in paywall UI
413
+ * @param paywall - Optional custom paywall provider (overrides default)
414
+ * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true)
415
+ * @returns Express middleware handler
416
+ *
417
+ * @example
418
+ * ```typescript
419
+ * import { paymentMiddlewareFromConfig } from "@x402/express";
420
+ *
421
+ * app.use(paymentMiddlewareFromConfig(
422
+ * routes,
423
+ * { advertisedEndpoint: "https://api.example.com/x402/tab" },
424
+ * ));
425
+ * ```
426
+ */
427
+ export function paymentMiddlewareFromConfig(
428
+ routes: RoutesConfig,
429
+ tabConfig: TabConfig,
430
+ facilitatorClients?: FacilitatorClient | FacilitatorClient[],
431
+ schemes?: SchemeRegistration[],
432
+ paywallConfig?: PaywallConfig,
433
+ paywall?: PaywallProvider,
434
+ syncFacilitatorOnStart: boolean = true
435
+ ) {
436
+ const facilitators = facilitatorClients
437
+ ? Array.isArray(facilitatorClients)
438
+ ? facilitatorClients
439
+ : [facilitatorClients]
440
+ : []
441
+
442
+ if (!facilitators.some((c) => c instanceof FourMicaFacilitatorClient)) {
443
+ facilitators.push(new FourMicaFacilitatorClient())
444
+ }
445
+
446
+ const ResourceServer = new x402ResourceServer(facilitators)
447
+
448
+ if (schemes) {
449
+ schemes.forEach(({ network, server: schemeServer }) => {
450
+ ResourceServer.register(network, schemeServer)
451
+ })
452
+ }
453
+
454
+ // Use the direct paymentMiddleware with the configured server
455
+ // Note: paymentMiddleware handles dynamic bazaar registration
456
+ return paymentMiddleware(
457
+ routes,
458
+ ResourceServer,
459
+ tabConfig,
460
+ paywallConfig,
461
+ paywall,
462
+ syncFacilitatorOnStart
463
+ )
464
+ }
465
+
466
+ export { ExpressAdapter } from './adapter.js'
@@ -0,0 +1,90 @@
1
+ import { FacilitatorConfig, HTTPFacilitatorClient } from '@x402/core/server'
2
+ import { Network, PaymentRequirements } from '@x402/core/types'
3
+
4
+ const DEFAULT_FACILITATOR_URL = 'https://x402.4mica.xyz'
5
+
6
+ export interface OpenTabRequest {
7
+ userAddress: string
8
+ recipientAddress: string
9
+ network?: Network
10
+ erc20Token?: string
11
+ ttlSeconds?: number
12
+ }
13
+
14
+ export interface OpenTabResponse {
15
+ tabId: string
16
+ userAddress: string
17
+ recipientAddress: string
18
+ assetAddress: string
19
+ startTimestamp: number
20
+ ttlSeconds: number
21
+ nextReqId: string
22
+ }
23
+
24
+ export class OpenTabError extends Error {
25
+ constructor(
26
+ public readonly status: number,
27
+ public readonly response: OpenTabResponse
28
+ ) {
29
+ super(`OpenTab failed with status ${status}`)
30
+ this.name = 'OpenTabError'
31
+ }
32
+ }
33
+
34
+ export class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
35
+ constructor(config?: FacilitatorConfig) {
36
+ super({ ...config, url: config?.url ?? DEFAULT_FACILITATOR_URL })
37
+ }
38
+
39
+ async openTab(
40
+ userAddress: string,
41
+ paymentRequirements: PaymentRequirements,
42
+ ttlSeconds?: number
43
+ ): Promise<OpenTabResponse> {
44
+ let headers: Record<string, string> = {
45
+ 'Content-Type': 'application/json',
46
+ }
47
+
48
+ const authHeaders = await this.createAuthHeaders('tabs')
49
+ headers = { ...headers, ...authHeaders.headers }
50
+
51
+ const response = await fetch(`${this.url}/tabs`, {
52
+ method: 'POST',
53
+ headers,
54
+ body: JSON.stringify(
55
+ this.safeJson({
56
+ userAddress,
57
+ recipientAddress: paymentRequirements.payTo,
58
+ network: paymentRequirements.network,
59
+ erc20Token: paymentRequirements.asset,
60
+ ttlSeconds,
61
+ })
62
+ ),
63
+ })
64
+
65
+ const data = await response.json()
66
+
67
+ if (typeof data === 'object' && data !== null && 'tabId' in data) {
68
+ const openTabResponse = data as OpenTabResponse
69
+ if (!response.ok) {
70
+ throw new OpenTabError(response.status, openTabResponse)
71
+ }
72
+ return openTabResponse
73
+ }
74
+
75
+ throw new Error(`Facilitator openTab failed (${response.status}): ${JSON.stringify(data)}`)
76
+ }
77
+
78
+ /**
79
+ * Helper to convert objects to JSON-safe format.
80
+ * Handles BigInt and other non-JSON types.
81
+ *
82
+ * @param obj - The object to convert
83
+ * @returns The JSON-safe representation of the object
84
+ */
85
+ private safeJson<T>(obj: T): T {
86
+ return JSON.parse(
87
+ JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? value.toString() : value))
88
+ )
89
+ }
90
+ }
@@ -0,0 +1,10 @@
1
+ export * from './facilitator.js'
2
+ export * from './scheme.js'
3
+
4
+ export { x402ResourceServer, x402HTTPResourceServer } from '@x402/core/server'
5
+
6
+ export type { PaywallProvider, PaywallConfig } from '@x402/core/server'
7
+
8
+ export { RouteConfigurationError } from '@x402/core/server'
9
+
10
+ export type { RouteValidationError } from '@x402/core/server'