@routedock/routedock 0.1.2 → 0.3.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +532 -4
  3. package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
  4. package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
  5. package/dist/ProviderRegistry-CA_TdREG.d.ts +142 -0
  6. package/dist/ProviderRegistry-D-0rqK0N.d.cts +142 -0
  7. package/dist/RouteDockClient-Bh86bVly.d.ts +469 -0
  8. package/dist/RouteDockClient-go9aopfF.d.cts +469 -0
  9. package/dist/SessionReconciler-Bw-PoBVl.d.ts +134 -0
  10. package/dist/SessionReconciler-Cz2nuOlJ.d.cts +134 -0
  11. package/dist/SessionStore-B4-Hi4H8.d.cts +143 -0
  12. package/dist/SessionStore-BaYiegP0.d.ts +143 -0
  13. package/dist/client.cjs +2746 -217
  14. package/dist/client.d.cts +51 -81
  15. package/dist/client.d.ts +51 -81
  16. package/dist/client.js +2690 -218
  17. package/dist/index.cjs +3634 -420
  18. package/dist/index.d.cts +12 -3
  19. package/dist/index.d.ts +12 -3
  20. package/dist/index.js +3569 -419
  21. package/dist/logger-D8_Q2ytp.d.cts +467 -0
  22. package/dist/logger-D8_Q2ytp.d.ts +467 -0
  23. package/dist/provider/fastify.cjs +1099 -0
  24. package/dist/provider/fastify.d.cts +40 -0
  25. package/dist/provider/fastify.d.ts +40 -0
  26. package/dist/provider/fastify.js +1097 -0
  27. package/dist/provider/hono.cjs +1152 -0
  28. package/dist/provider/hono.d.cts +101 -0
  29. package/dist/provider/hono.d.ts +101 -0
  30. package/dist/provider/hono.js +1142 -0
  31. package/dist/provider.cjs +1105 -147
  32. package/dist/provider.d.cts +38 -27
  33. package/dist/provider.d.ts +38 -27
  34. package/dist/provider.js +1079 -148
  35. package/dist/react.cjs +2931 -0
  36. package/dist/react.d.cts +107 -0
  37. package/dist/react.d.ts +107 -0
  38. package/dist/react.js +2924 -0
  39. package/dist/routedockMiddleware-57gUaEMf.d.ts +64 -0
  40. package/dist/routedockMiddleware-DlyLrxp7.d.cts +64 -0
  41. package/dist/schema.cjs +404 -0
  42. package/dist/schema.d.cts +3 -0
  43. package/dist/schema.d.ts +3 -0
  44. package/dist/schema.js +399 -0
  45. package/dist/sign-AKH67sft.d.cts +53 -0
  46. package/dist/sign-BEq-OsSq.d.ts +53 -0
  47. package/dist/testing.cjs +59 -0
  48. package/dist/testing.d.cts +114 -0
  49. package/dist/testing.d.ts +114 -0
  50. package/dist/testing.js +56 -0
  51. package/package.json +143 -23
  52. package/PUBLISH_CHECKLIST.md +0 -70
  53. package/dist/SessionStore-CCVO0PHM.d.cts +0 -152
  54. package/dist/SessionStore-CCVO0PHM.d.ts +0 -152
  55. package/dist/routedockMiddleware-B7WTB3LT.d.cts +0 -53
  56. package/dist/routedockMiddleware-DRgdrycG.d.ts +0 -53
  57. package/src/client/ModeRouter.ts +0 -104
  58. package/src/client/MppChargeClient.ts +0 -44
  59. package/src/client/MppSessionClient.ts +0 -144
  60. package/src/client/RouteDockClient.ts +0 -124
  61. package/src/client/index.ts +0 -8
  62. package/src/client/x402Client.ts +0 -84
  63. package/src/index.ts +0 -9
  64. package/src/provider/MppChargeHandler.ts +0 -90
  65. package/src/provider/MppSessionHandler.ts +0 -157
  66. package/src/provider/index.ts +0 -7
  67. package/src/provider/routedockMiddleware.ts +0 -154
  68. package/src/provider/x402Handler.ts +0 -164
  69. package/src/schemas/routedock.schema.json +0 -159
  70. package/src/store/SessionStore.ts +0 -90
  71. package/src/tests/smoke.test.ts +0 -228
  72. package/src/types.ts +0 -165
  73. package/tsconfig.json +0 -14
  74. package/tsup.config.ts +0 -15
@@ -1,84 +0,0 @@
1
- import { createEd25519Signer } from '@x402/stellar'
2
- import { ExactStellarScheme } from '@x402/stellar/exact/client'
3
- import { x402Client, x402HTTPClient } from '@x402/core/client'
4
- import {
5
- decodePaymentRequiredHeader,
6
- decodePaymentResponseHeader,
7
- encodePaymentSignatureHeader,
8
- } from '@x402/core/http'
9
- import type { Network as X402Network } from '@x402/core/types'
10
- import type { RouteDockManifest, PaymentResult } from '../types.js'
11
- import { RouteDockManifestError } from '../types.js'
12
-
13
- type Network = 'testnet' | 'mainnet'
14
-
15
- const CAIP2: Record<Network, X402Network> = {
16
- testnet: 'stellar:testnet',
17
- mainnet: 'stellar:pubnet',
18
- }
19
-
20
- export class X402Client {
21
- private readonly httpClient: x402HTTPClient
22
-
23
- constructor(
24
- private readonly secretKey: string,
25
- private readonly network: Network,
26
- ) {
27
- const caip2 = CAIP2[network]
28
- const signer = createEd25519Signer(secretKey, caip2)
29
- const scheme = new ExactStellarScheme(signer)
30
- const core = new x402Client()
31
- core.register(caip2, scheme)
32
- this.httpClient = new x402HTTPClient(core)
33
- }
34
-
35
- async pay(url: string, manifest: RouteDockManifest): Promise<PaymentResult> {
36
- const pricing = manifest.pricing.x402
37
- if (!pricing) {
38
- throw new RouteDockManifestError('manifest.pricing.x402 missing')
39
- }
40
- if (!pricing.facilitator) {
41
- throw new RouteDockManifestError('manifest.pricing.x402.facilitator missing')
42
- }
43
-
44
- // Initial request — expect 402. Include mode hint so middleware routes correctly.
45
- const init = await fetch(url, { headers: { 'X-Preferred-Mode': 'x402' } })
46
- if (init.status !== 402) {
47
- const data = await init.json()
48
- return { data, txHash: null, mode: 'x402', amount: pricing.amount, timestamp: Date.now() }
49
- }
50
-
51
- // Parse payment requirements from header
52
- const reqHeader = init.headers.get('X-Payment-Requirements')
53
- if (!reqHeader) {
54
- throw new RouteDockManifestError('402 response missing X-Payment-Requirements header')
55
- }
56
-
57
- const paymentRequired = decodePaymentRequiredHeader(reqHeader)
58
-
59
- // Build signed payment payload
60
- const paymentPayload = await this.httpClient.createPaymentPayload(paymentRequired)
61
- const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload)
62
-
63
- // Re-request with payment
64
- const settled = await fetch(url, { headers: paymentHeaders })
65
- if (!settled.ok) {
66
- throw new RouteDockManifestError(`x402 payment failed: HTTP ${settled.status}`)
67
- }
68
-
69
- // Extract settlement tx hash from response header
70
- let txHash: string | null = null
71
- const responseHeader = settled.headers.get('X-Payment-Response')
72
- if (responseHeader) {
73
- try {
74
- const settleResponse = decodePaymentResponseHeader(responseHeader)
75
- txHash = (settleResponse as { transaction?: string }).transaction ?? null
76
- } catch {
77
- // non-fatal — some facilitators may omit the response header
78
- }
79
- }
80
-
81
- const data = await settled.json()
82
- return { data, txHash, mode: 'x402', amount: pricing.amount, timestamp: Date.now() }
83
- }
84
- }
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- // Full SDK — types, client, provider, store
2
- export * from './types.js'
3
- export * from './client/RouteDockClient.js'
4
- export * from './client/ModeRouter.js'
5
- export * from './client/x402Client.js'
6
- export * from './client/MppChargeClient.js'
7
- export * from './client/MppSessionClient.js'
8
- export * from './store/SessionStore.js'
9
- export * from './provider/routedockMiddleware.js'
@@ -1,90 +0,0 @@
1
- import type { Request, Response, NextFunction, RequestHandler } from 'express'
2
- import { stellar } from '@stellar/mpp/charge/server'
3
- import { Mppx, Request as MppxRequest } from 'mppx/server'
4
- import type { RouteDockManifest } from '../types.js'
5
- import type { SessionStore } from '../store/SessionStore.js'
6
-
7
- type Network = 'testnet' | 'mainnet'
8
-
9
- const MPP_NETWORK: Record<Network, 'stellar:testnet' | 'stellar:pubnet'> = {
10
- testnet: 'stellar:testnet',
11
- mainnet: 'stellar:pubnet',
12
- }
13
-
14
- export interface MppChargeHandlerOptions {
15
- payeeSecretKey: string
16
- network: Network
17
- amount: string
18
- assetContract: string
19
- manifest: RouteDockManifest
20
- store?: SessionStore
21
- onSettled?: (txHash: string, amount: string, mode: string) => Promise<void>
22
- }
23
-
24
- export function createMppChargeHandler(opts: MppChargeHandlerOptions): RequestHandler {
25
- const networkId = MPP_NETWORK[opts.network]
26
- const amountHumanReadable = opts.amount
27
-
28
- const mppx = Mppx.create({
29
- secretKey: opts.payeeSecretKey,
30
- methods: [
31
- stellar.charge({
32
- recipient: opts.manifest.payee,
33
- currency: opts.assetContract,
34
- network: networkId,
35
- feePayer: { envelopeSigner: opts.payeeSecretKey },
36
- }),
37
- ],
38
- })
39
-
40
- return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
41
- try {
42
- const fetchReq = MppxRequest.fromNodeListener(req, res)
43
- // `mppx['stellar/charge']` — method key is `name/intent`
44
- const handler = (
45
- mppx as unknown as {
46
- 'stellar/charge': (
47
- opts: { amount: string; currency: string; recipient: string; description?: string },
48
- ) => (input: globalThis.Request) => Promise<{ status: 402 | 200; challenge?: globalThis.Response; withReceipt?: (r: globalThis.Response) => globalThis.Response }>
49
- }
50
- )['stellar/charge']
51
- const result = await handler({
52
- amount: amountHumanReadable,
53
- currency: opts.assetContract,
54
- recipient: opts.manifest.payee,
55
- description: opts.manifest.name,
56
- })(fetchReq)
57
-
58
- if (result.status === 402) {
59
- const challenge = result.challenge!
60
- res.status(402)
61
- challenge.headers.forEach((v: string, k: string) => res.setHeader(k, v))
62
- res.send(await challenge.text())
63
- return
64
- }
65
-
66
- // status 200 — payment verified
67
- const receipt = result.withReceipt!(new Response(''))
68
- receipt.headers.forEach((v: string, k: string) => res.setHeader(k, v))
69
-
70
- // Extract tx hash from receipt headers if available
71
- const receiptHeader = receipt.headers.get('payment-receipt')
72
- if (receiptHeader && opts.onSettled) {
73
- try {
74
- const parsed = JSON.parse(
75
- Buffer.from(receiptHeader, 'base64').toString('utf8'),
76
- ) as { reference?: string }
77
- if (parsed.reference) {
78
- await opts.onSettled(parsed.reference, opts.amount, 'mpp-charge')
79
- }
80
- } catch {
81
- // non-fatal
82
- }
83
- }
84
-
85
- next()
86
- } catch (err) {
87
- next(err)
88
- }
89
- }
90
- }
@@ -1,157 +0,0 @@
1
- import type { Request, Response, NextFunction, RequestHandler } from 'express'
2
- import { Keypair } from '@stellar/stellar-sdk'
3
- import { stellar, close as channelClose, Store } from '@stellar/mpp/channel/server'
4
- import { Mppx, Request as MppxRequest } from 'mppx/server'
5
- import type { RouteDockManifest } from '../types.js'
6
-
7
- type Network = 'testnet' | 'mainnet'
8
-
9
- const MPP_NETWORK: Record<Network, 'stellar:testnet' | 'stellar:pubnet'> = {
10
- testnet: 'stellar:testnet',
11
- mainnet: 'stellar:pubnet',
12
- }
13
-
14
- export interface MppSessionHandlerOptions {
15
- payeeSecretKey: string
16
- network: Network
17
- channelContract: string
18
- rate: string
19
- assetContract: string
20
- manifest: RouteDockManifest
21
- commitmentPublicKey: string
22
- onSettled?: (txHash: string, totalPaid: string, mode: string) => Promise<void>
23
- onSessionOpen?: (channelId: string) => Promise<void>
24
- onVoucher?: (voucherIndex: number, cumulativeAmount: string) => Promise<void>
25
- }
26
-
27
- export function createMppSessionHandler(opts: MppSessionHandlerOptions): RequestHandler {
28
- const networkId = MPP_NETWORK[opts.network]
29
- const rateHuman = opts.rate
30
- const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey)
31
- const cumulativeKey = `stellar:channel:cumulative:${opts.channelContract}`
32
-
33
- const innerStore = Store.memory()
34
-
35
- let lastCumulativeAmount = 0n
36
- let voucherCount = 0
37
- let sessionOpened = false
38
-
39
- const wrappedStore: ReturnType<typeof Store.memory> = {
40
- async get(key: string) { return innerStore.get(key) },
41
- async put(key: string, value: unknown) {
42
- await innerStore.put(key, value)
43
- if (key === cumulativeKey && value && typeof value === 'object' && 'amount' in (value as Record<string, unknown>)) {
44
- lastCumulativeAmount = BigInt((value as { amount: string }).amount)
45
- voucherCount++
46
-
47
- if (!sessionOpened) {
48
- sessionOpened = true
49
- if (opts.onSessionOpen) {
50
- await opts.onSessionOpen(opts.channelContract)
51
- }
52
- }
53
-
54
- if (opts.onVoucher) {
55
- const humanAmount = (Number(lastCumulativeAmount) / 1e7).toFixed(7)
56
- await opts.onVoucher(voucherCount, humanAmount)
57
- }
58
- }
59
- },
60
- async delete(key: string) { return innerStore.delete(key) },
61
- }
62
-
63
- const mppx = Mppx.create({
64
- secretKey: opts.payeeSecretKey,
65
- methods: [
66
- stellar.channel({
67
- channel: opts.channelContract,
68
- commitmentKey: opts.commitmentPublicKey,
69
- network: networkId,
70
- store: wrappedStore,
71
- sourceAccount: payeeKeypair.publicKey(),
72
- feePayer: { envelopeSigner: payeeKeypair },
73
- }),
74
- ],
75
- })
76
-
77
- // Track the last signature from the authorization header
78
- let lastSignatureHex = ''
79
-
80
- return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
81
- try {
82
- if (req.method === 'DELETE') {
83
- const body = req.body as { amount?: string; signature?: string } | undefined
84
- const closeAmount = body?.amount ? BigInt(body.amount) : lastCumulativeAmount
85
- const closeSig = body?.signature ?? lastSignatureHex
86
-
87
- if (closeAmount > 0n && closeSig) {
88
- const closeTxHash = await channelClose({
89
- channel: opts.channelContract,
90
- amount: closeAmount,
91
- signature: Buffer.from(closeSig, 'hex'),
92
- feePayer: { envelopeSigner: payeeKeypair },
93
- network: networkId,
94
- })
95
-
96
- if (opts.onSettled) {
97
- const totalPaid = (Number(lastCumulativeAmount) / 1e7).toFixed(7)
98
- await opts.onSettled(closeTxHash, totalPaid, 'mpp-session')
99
- }
100
-
101
- res.json({ closeTxHash })
102
- } else {
103
- res.json({ closeTxHash: null, message: 'no vouchers received' })
104
- }
105
-
106
- sessionOpened = false
107
- voucherCount = 0
108
- lastCumulativeAmount = 0n
109
- return
110
- }
111
-
112
- // Extract the signature from the authorization header before passing to mppx
113
- const authHeader = req.headers['authorization']
114
- if (typeof authHeader === 'string' && authHeader.startsWith('Payment ')) {
115
- try {
116
- const credB64 = authHeader.replace(/^Payment\s+/, '').split(',').find(p => p.trim().startsWith('credential='))
117
- if (credB64) {
118
- const credJson = Buffer.from(credB64.split('=').slice(1).join('=').replace(/^"|"$/g, ''), 'base64').toString('utf8')
119
- const cred = JSON.parse(credJson) as { payload?: { signature?: string } }
120
- if (cred.payload?.signature) {
121
- lastSignatureHex = cred.payload.signature
122
- }
123
- }
124
- } catch {
125
- // non-fatal — signature extraction is best-effort
126
- }
127
- }
128
-
129
- const fetchReq = MppxRequest.fromNodeListener(req, res)
130
-
131
- const result = await (mppx as unknown as {
132
- channel: (o: { amount: string; description?: string }) =>
133
- (r: globalThis.Request) => Promise<{
134
- status: number
135
- challenge?: globalThis.Response
136
- withReceipt?: (r: globalThis.Response) => globalThis.Response
137
- }>
138
- }).channel({
139
- amount: rateHuman,
140
- description: opts.manifest.name,
141
- })(fetchReq)
142
-
143
- if (result.status === 402) {
144
- const challenge = result.challenge!
145
- res.status(402)
146
- challenge.headers.forEach((v: string, k: string) => res.setHeader(k, v))
147
- const body = await challenge.text()
148
- res.send(body)
149
- return
150
- }
151
-
152
- next()
153
- } catch (err) {
154
- next(err)
155
- }
156
- }
157
- }
@@ -1,7 +0,0 @@
1
- // Provider-only exports (Express middleware + handlers)
2
- export * from '../types.js'
3
- export * from './routedockMiddleware.js'
4
- export * from './x402Handler.js'
5
- export * from './MppChargeHandler.js'
6
- export * from './MppSessionHandler.js'
7
- export * from '../store/SessionStore.js'
@@ -1,154 +0,0 @@
1
- import type { Request, Response, NextFunction, RequestHandler } from 'express'
2
- import { createX402Handler } from './x402Handler.js'
3
- import { createMppChargeHandler } from './MppChargeHandler.js'
4
- import { createMppSessionHandler } from './MppSessionHandler.js'
5
- import type { RouteDockManifest, PaymentMode } from '../types.js'
6
-
7
- export interface RouteDockMiddlewareOptions {
8
- modes: PaymentMode[]
9
- pricing: {
10
- x402?: string
11
- 'mpp-charge'?: string
12
- 'mpp-session'?: { rate: string; channelContract: string }
13
- }
14
- asset: string
15
- /** Stellar Asset Contract address for the payment asset */
16
- assetContract: string
17
- payee: string
18
- network: 'testnet' | 'mainnet'
19
- /** Private key (S...) of the server/payee account */
20
- payeeSecretKey: string
21
- /** Required for x402 mode */
22
- facilitatorApiKey?: string
23
- /** Required for mpp-session mode — ed25519 public key (G...) for verifying commitments */
24
- commitmentPublicKey?: string
25
- /** Full RouteDock manifest — served at /.well-known/routedock.json */
26
- manifest: RouteDockManifest
27
- /** Called after each successful on-chain settlement */
28
- onSettled?: (txHash: string, amount: string, mode: string) => Promise<void>
29
- /** Called when a new mpp-session is opened (first voucher received) */
30
- onSessionOpen?: (channelId: string) => Promise<void>
31
- /** Called after each verified voucher in an mpp-session */
32
- onVoucher?: (voucherIndex: number, cumulativeAmount: string) => Promise<void>
33
- }
34
-
35
- /**
36
- * Express middleware factory for RouteDock provider endpoints.
37
- *
38
- * Automatically serves /.well-known/routedock.json and enforces payment
39
- * verification for all three modes (x402, mpp-charge, mpp-session).
40
- *
41
- * @example
42
- * ```ts
43
- * import { routedock } from '@routedock/sdk/provider'
44
- *
45
- * app.use('/price', routedock({
46
- * modes: ['x402', 'mpp-charge'],
47
- * pricing: { x402: '0.001', 'mpp-charge': '0.0008' },
48
- * ...
49
- * }))
50
- * ```
51
- */
52
- export function routedock(opts: RouteDockMiddlewareOptions): RequestHandler {
53
- const handlers: RequestHandler[] = []
54
-
55
- if (opts.modes.includes('x402')) {
56
- const x402Price = opts.pricing.x402
57
- if (x402Price) {
58
- handlers.push(
59
- createX402Handler({
60
- payeeSecretKey: opts.payeeSecretKey,
61
- network: opts.network,
62
- amount: x402Price,
63
- assetContract: opts.assetContract,
64
- ...(opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {}),
65
- manifest: opts.manifest,
66
- ...(opts.onSettled ? { onSettled: opts.onSettled } : {}),
67
- }),
68
- )
69
- }
70
- }
71
-
72
- if (opts.modes.includes('mpp-charge')) {
73
- const chargePrice = opts.pricing['mpp-charge']
74
- if (chargePrice) {
75
- handlers.push(
76
- createMppChargeHandler({
77
- payeeSecretKey: opts.payeeSecretKey,
78
- network: opts.network,
79
- amount: chargePrice,
80
- assetContract: opts.assetContract,
81
- manifest: opts.manifest,
82
- ...(opts.onSettled ? { onSettled: opts.onSettled } : {}),
83
- }),
84
- )
85
- }
86
- }
87
-
88
- if (opts.modes.includes('mpp-session')) {
89
- const sessionPricing = opts.pricing['mpp-session']
90
- if (sessionPricing) {
91
- if (!opts.commitmentPublicKey) {
92
- throw new Error('routedock: mpp-session mode requires commitmentPublicKey')
93
- }
94
- handlers.push(
95
- createMppSessionHandler({
96
- payeeSecretKey: opts.payeeSecretKey,
97
- network: opts.network,
98
- channelContract: sessionPricing.channelContract,
99
- rate: sessionPricing.rate,
100
- assetContract: opts.assetContract,
101
- manifest: opts.manifest,
102
- commitmentPublicKey: opts.commitmentPublicKey,
103
- ...(opts.onSettled ? { onSettled: opts.onSettled } : {}),
104
- ...(opts.onSessionOpen ? { onSessionOpen: opts.onSessionOpen } : {}),
105
- ...(opts.onVoucher ? { onVoucher: opts.onVoucher } : {}),
106
- }),
107
- )
108
- }
109
- }
110
-
111
- return (req: Request, res: Response, next: NextFunction): void => {
112
- // Serve manifest at /.well-known/routedock.json
113
- if (req.path === '/.well-known/routedock.json') {
114
- res.json(opts.manifest)
115
- return
116
- }
117
-
118
- // No handlers — pass through
119
- if (handlers.length === 0) {
120
- next()
121
- return
122
- }
123
-
124
- // Route to the correct handler based on request headers.
125
- // x402 clients send `payment-signature` or `x-payment` headers.
126
- // mppx clients send `authorization` with the mppx bearer scheme.
127
- // On initial request (no payment), use the last handler (mppx returns
128
- // WWW-Authenticate which x402 clients ignore; x402 only runs when its
129
- // header is explicitly present).
130
- const hasX402Header = !!(req.headers['payment-signature'] || req.headers['x-payment'])
131
- const prefersX402 = req.headers['x-preferred-mode'] === 'x402'
132
-
133
- let handler: RequestHandler | undefined
134
- if (hasX402Header || prefersX402) {
135
- handler = handlers[0]
136
- } else {
137
- handler = handlers[handlers.length - 1]
138
- }
139
-
140
- if (!handler) {
141
- next()
142
- return
143
- }
144
- void Promise.resolve()
145
- .then(() => new Promise<void>((resolve, reject) => {
146
- handler!(req, res, (handlerErr?: unknown) => {
147
- if (handlerErr != null) reject(handlerErr)
148
- else resolve()
149
- })
150
- }))
151
- .then(() => next())
152
- .catch((err) => next(err))
153
- }
154
- }
@@ -1,164 +0,0 @@
1
- import type { Request, Response, NextFunction, RequestHandler } from 'express'
2
- import { Keypair } from '@stellar/stellar-sdk'
3
- import { ExactStellarScheme as ExactStellarFacilitatorScheme } from '@x402/stellar/exact/facilitator'
4
- import { ExactStellarScheme as ExactStellarServerScheme } from '@x402/stellar/exact/server'
5
- import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server'
6
- import { createEd25519Signer } from '@x402/stellar'
7
- import {
8
- decodePaymentSignatureHeader,
9
- encodePaymentRequiredHeader,
10
- encodePaymentResponseHeader,
11
- } from '@x402/core/http'
12
- import type { Network as X402Network } from '@x402/core/types'
13
- import type { RouteDockManifest } from '../types.js'
14
-
15
- type Network = 'testnet' | 'mainnet'
16
-
17
- const CAIP2: Record<Network, X402Network> = {
18
- testnet: 'stellar:testnet',
19
- mainnet: 'stellar:pubnet',
20
- }
21
-
22
- const OZ_FACILITATOR_URL = 'https://channels.openzeppelin.com/x402'
23
-
24
- export interface X402HandlerOptions {
25
- payeeSecretKey: string
26
- network: Network
27
- amount: string
28
- assetContract: string
29
- facilitatorApiKey?: string
30
- manifest: RouteDockManifest
31
- onSettled?: (txHash: string, amount: string, mode: string) => Promise<void>
32
- }
33
-
34
- export function createX402Handler(opts: X402HandlerOptions): RequestHandler {
35
- const caip2 = CAIP2[opts.network]
36
- const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey)
37
- const signer = createEd25519Signer(opts.payeeSecretKey, caip2)
38
-
39
- const useOzFacilitator = opts.network === 'mainnet' && opts.facilitatorApiKey
40
-
41
- // Mainnet: OZ hosted facilitator via x402ResourceServer
42
- // Testnet: local ExactStellarFacilitatorScheme (OZ facilitator does not serve testnet)
43
- const localFacilitator = new ExactStellarFacilitatorScheme([signer], {
44
- areFeesSponsored: true,
45
- })
46
-
47
- let ozServer: x402ResourceServer | null = null
48
- if (useOzFacilitator) {
49
- const apiKey = opts.facilitatorApiKey!
50
- const facilitator = new HTTPFacilitatorClient({
51
- url: OZ_FACILITATOR_URL,
52
- createAuthHeaders: async () => ({
53
- verify: { Authorization: `Bearer ${apiKey}` },
54
- settle: { Authorization: `Bearer ${apiKey}` },
55
- supported: { Authorization: `Bearer ${apiKey}` },
56
- }),
57
- })
58
- ozServer = new x402ResourceServer(facilitator)
59
- ozServer.register(caip2, new ExactStellarServerScheme())
60
- }
61
-
62
- const amountInBaseUnits = String(Math.round(parseFloat(opts.amount) * 1e7))
63
- const requirements = {
64
- scheme: 'exact' as const,
65
- network: caip2,
66
- asset: opts.assetContract,
67
- amount: amountInBaseUnits,
68
- payTo: opts.manifest.payee,
69
- maxTimeoutSeconds: 60,
70
- extra: {
71
- areFeesSponsored: true,
72
- ...(useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }),
73
- },
74
- }
75
-
76
- return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
77
- try {
78
- const paymentHeader = (req.headers['payment-signature'] ?? req.headers['x-payment']) as string | undefined
79
-
80
- if (!paymentHeader) {
81
- if (ozServer) {
82
- const resourceInfo = {
83
- url: `${req.protocol}://${req.get('host') ?? ''}${req.originalUrl}`,
84
- description: opts.manifest.name,
85
- }
86
- const paymentRequired = await ozServer.createPaymentRequiredResponse(
87
- [requirements],
88
- resourceInfo,
89
- )
90
- res
91
- .status(402)
92
- .setHeader('Content-Type', 'application/json')
93
- .setHeader('X-Payment-Requirements', encodePaymentRequiredHeader(paymentRequired))
94
- .json({ error: 'Payment Required' })
95
- } else {
96
- const x402Response = {
97
- x402Version: 2,
98
- resource: {
99
- url: `${req.protocol}://${req.get('host') ?? ''}${req.originalUrl}`,
100
- description: opts.manifest.name,
101
- },
102
- accepts: [requirements],
103
- }
104
- res
105
- .status(402)
106
- .setHeader('Content-Type', 'application/json')
107
- .setHeader('X-Payment-Requirements', encodePaymentRequiredHeader(x402Response))
108
- .json({ error: 'Payment Required' })
109
- }
110
- return
111
- }
112
-
113
- const payload = decodePaymentSignatureHeader(paymentHeader)
114
- let txHash: string | null = null
115
-
116
- if (ozServer) {
117
- const settleResult = await ozServer.settlePayment(payload, requirements)
118
- txHash = (settleResult as { transaction?: string }).transaction ?? null
119
- if (settleResult) {
120
- res.setHeader(
121
- 'X-Payment-Response',
122
- encodePaymentResponseHeader(
123
- settleResult as Parameters<typeof encodePaymentResponseHeader>[0],
124
- ),
125
- )
126
- }
127
- } else {
128
- const verifyResult = await localFacilitator.verify(
129
- payload as Parameters<typeof localFacilitator.verify>[0],
130
- requirements,
131
- )
132
- if (!verifyResult.isValid) {
133
- res.status(401).json({
134
- error: 'Payment verification failed',
135
- reason: (verifyResult as { invalidReason?: string }).invalidReason,
136
- })
137
- return
138
- }
139
- const settleResult = await localFacilitator.settle(
140
- payload as Parameters<typeof localFacilitator.settle>[0],
141
- requirements,
142
- )
143
- txHash = (settleResult as { transaction?: string }).transaction ?? null
144
- if (settleResult) {
145
- res.setHeader(
146
- 'X-Payment-Response',
147
- encodePaymentResponseHeader(
148
- settleResult as Parameters<typeof encodePaymentResponseHeader>[0],
149
- ),
150
- )
151
- }
152
- }
153
-
154
- if (txHash && opts.onSettled) {
155
- await opts.onSettled(txHash, opts.amount, 'x402')
156
- }
157
-
158
- next()
159
- } catch (err) {
160
- console.error('[x402] Settlement error:', err)
161
- res.status(500).json({ error: 'Payment settlement failed' })
162
- }
163
- }
164
- }