@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.
- package/LICENSE +21 -0
- package/README.md +532 -4
- package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
- package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
- package/dist/ProviderRegistry-CA_TdREG.d.ts +142 -0
- package/dist/ProviderRegistry-D-0rqK0N.d.cts +142 -0
- package/dist/RouteDockClient-Bh86bVly.d.ts +469 -0
- package/dist/RouteDockClient-go9aopfF.d.cts +469 -0
- package/dist/SessionReconciler-Bw-PoBVl.d.ts +134 -0
- package/dist/SessionReconciler-Cz2nuOlJ.d.cts +134 -0
- package/dist/SessionStore-B4-Hi4H8.d.cts +143 -0
- package/dist/SessionStore-BaYiegP0.d.ts +143 -0
- package/dist/client.cjs +2746 -217
- package/dist/client.d.cts +51 -81
- package/dist/client.d.ts +51 -81
- package/dist/client.js +2690 -218
- package/dist/index.cjs +3634 -420
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +3569 -419
- package/dist/logger-D8_Q2ytp.d.cts +467 -0
- package/dist/logger-D8_Q2ytp.d.ts +467 -0
- package/dist/provider/fastify.cjs +1099 -0
- package/dist/provider/fastify.d.cts +40 -0
- package/dist/provider/fastify.d.ts +40 -0
- package/dist/provider/fastify.js +1097 -0
- package/dist/provider/hono.cjs +1152 -0
- package/dist/provider/hono.d.cts +101 -0
- package/dist/provider/hono.d.ts +101 -0
- package/dist/provider/hono.js +1142 -0
- package/dist/provider.cjs +1105 -147
- package/dist/provider.d.cts +38 -27
- package/dist/provider.d.ts +38 -27
- package/dist/provider.js +1079 -148
- package/dist/react.cjs +2931 -0
- package/dist/react.d.cts +107 -0
- package/dist/react.d.ts +107 -0
- package/dist/react.js +2924 -0
- package/dist/routedockMiddleware-57gUaEMf.d.ts +64 -0
- package/dist/routedockMiddleware-DlyLrxp7.d.cts +64 -0
- package/dist/schema.cjs +404 -0
- package/dist/schema.d.cts +3 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +399 -0
- package/dist/sign-AKH67sft.d.cts +53 -0
- package/dist/sign-BEq-OsSq.d.ts +53 -0
- package/dist/testing.cjs +59 -0
- package/dist/testing.d.cts +114 -0
- package/dist/testing.d.ts +114 -0
- package/dist/testing.js +56 -0
- package/package.json +143 -23
- package/PUBLISH_CHECKLIST.md +0 -70
- package/dist/SessionStore-CCVO0PHM.d.cts +0 -152
- package/dist/SessionStore-CCVO0PHM.d.ts +0 -152
- package/dist/routedockMiddleware-B7WTB3LT.d.cts +0 -53
- package/dist/routedockMiddleware-DRgdrycG.d.ts +0 -53
- package/src/client/ModeRouter.ts +0 -104
- package/src/client/MppChargeClient.ts +0 -44
- package/src/client/MppSessionClient.ts +0 -144
- package/src/client/RouteDockClient.ts +0 -124
- package/src/client/index.ts +0 -8
- package/src/client/x402Client.ts +0 -84
- package/src/index.ts +0 -9
- package/src/provider/MppChargeHandler.ts +0 -90
- package/src/provider/MppSessionHandler.ts +0 -157
- package/src/provider/index.ts +0 -7
- package/src/provider/routedockMiddleware.ts +0 -154
- package/src/provider/x402Handler.ts +0 -164
- package/src/schemas/routedock.schema.json +0 -159
- package/src/store/SessionStore.ts +0 -90
- package/src/tests/smoke.test.ts +0 -228
- package/src/types.ts +0 -165
- package/tsconfig.json +0 -14
- package/tsup.config.ts +0 -15
package/src/client/x402Client.ts
DELETED
|
@@ -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
|
-
}
|
package/src/provider/index.ts
DELETED
|
@@ -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
|
-
}
|