@routedock/routedock 0.1.1 → 0.2.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 +281 -19
- package/dist/MppSessionHandler-B4TyD7Xg.d.cts +121 -0
- package/dist/MppSessionHandler-CLB5SzgN.d.ts +121 -0
- package/dist/ProviderRegistry-BJ9oJLVz.d.cts +142 -0
- package/dist/ProviderRegistry-C50nyhaF.d.ts +142 -0
- package/dist/RouteDockClient-B1roCa8h.d.cts +455 -0
- package/dist/RouteDockClient-BbkqjhMG.d.ts +455 -0
- package/dist/SessionReconciler-Dscw43F3.d.ts +125 -0
- package/dist/SessionReconciler-bi0z2qtN.d.cts +125 -0
- package/dist/SessionStore-BpK8vv0L.d.cts +111 -0
- package/dist/SessionStore-DvkZv8-E.d.ts +111 -0
- package/dist/client.cjs +2419 -202
- package/dist/client.d.cts +50 -81
- package/dist/client.d.ts +50 -81
- package/dist/client.js +2373 -203
- package/dist/index.cjs +3080 -298
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +3026 -298
- package/dist/provider/fastify.cjs +882 -0
- package/dist/provider/fastify.d.cts +35 -0
- package/dist/provider/fastify.d.ts +35 -0
- package/dist/provider/fastify.js +880 -0
- package/dist/provider/hono.cjs +948 -0
- package/dist/provider/hono.d.cts +99 -0
- package/dist/provider/hono.d.ts +99 -0
- package/dist/provider/hono.js +938 -0
- package/dist/provider.cjs +827 -97
- package/dist/provider.d.cts +32 -25
- package/dist/provider.d.ts +32 -25
- package/dist/provider.js +810 -99
- package/dist/react.cjs +2641 -0
- package/dist/react.d.cts +107 -0
- package/dist/react.d.ts +107 -0
- package/dist/react.js +2634 -0
- package/dist/routedockMiddleware-BlDsHuPl.d.ts +59 -0
- package/dist/routedockMiddleware-D97A5ptQ.d.cts +59 -0
- package/dist/schema.cjs +364 -0
- package/dist/schema.d.cts +3 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +359 -0
- package/dist/sign-CUf3rhTx.d.cts +53 -0
- package/dist/sign-Dpj2Mp7k.d.ts +53 -0
- package/dist/testing.cjs +57 -0
- package/dist/testing.d.cts +95 -0
- package/dist/testing.d.ts +95 -0
- package/dist/testing.js +54 -0
- package/dist/types-DmhmfLzP.d.cts +410 -0
- package/dist/types-DmhmfLzP.d.ts +410 -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
|
@@ -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
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://routedock.dev/schemas/routedock.schema.json",
|
|
4
|
-
"title": "RouteDock Manifest",
|
|
5
|
-
"description": "Discovery manifest served at /.well-known/routedock.json on every RouteDock provider endpoint",
|
|
6
|
-
"type": "object",
|
|
7
|
-
"required": [
|
|
8
|
-
"routedock",
|
|
9
|
-
"name",
|
|
10
|
-
"description",
|
|
11
|
-
"modes",
|
|
12
|
-
"network",
|
|
13
|
-
"asset",
|
|
14
|
-
"asset_contract",
|
|
15
|
-
"payee",
|
|
16
|
-
"pricing",
|
|
17
|
-
"endpoints",
|
|
18
|
-
"tags"
|
|
19
|
-
],
|
|
20
|
-
"additionalProperties": false,
|
|
21
|
-
"properties": {
|
|
22
|
-
"routedock": {
|
|
23
|
-
"type": "string",
|
|
24
|
-
"const": "1.0",
|
|
25
|
-
"description": "Manifest schema version. Always '1.0' for this version."
|
|
26
|
-
},
|
|
27
|
-
"name": {
|
|
28
|
-
"type": "string",
|
|
29
|
-
"minLength": 1,
|
|
30
|
-
"maxLength": 120,
|
|
31
|
-
"description": "Human-readable provider name"
|
|
32
|
-
},
|
|
33
|
-
"description": {
|
|
34
|
-
"type": "string",
|
|
35
|
-
"minLength": 1,
|
|
36
|
-
"maxLength": 500,
|
|
37
|
-
"description": "Human-readable description of the provider's data or service"
|
|
38
|
-
},
|
|
39
|
-
"modes": {
|
|
40
|
-
"type": "array",
|
|
41
|
-
"minItems": 1,
|
|
42
|
-
"uniqueItems": true,
|
|
43
|
-
"items": {
|
|
44
|
-
"$ref": "#/definitions/PaymentMode"
|
|
45
|
-
},
|
|
46
|
-
"description": "Payment modes supported by this provider"
|
|
47
|
-
},
|
|
48
|
-
"network": {
|
|
49
|
-
"type": "string",
|
|
50
|
-
"enum": ["testnet", "mainnet"],
|
|
51
|
-
"description": "Stellar network this provider operates on"
|
|
52
|
-
},
|
|
53
|
-
"asset": {
|
|
54
|
-
"type": "string",
|
|
55
|
-
"description": "Asset ticker symbol (e.g. 'USDC')"
|
|
56
|
-
},
|
|
57
|
-
"asset_contract": {
|
|
58
|
-
"type": "string",
|
|
59
|
-
"description": "Stellar Asset Contract (SAC) address for the payment asset"
|
|
60
|
-
},
|
|
61
|
-
"payee": {
|
|
62
|
-
"type": "string",
|
|
63
|
-
"description": "Stellar address (G...) that receives payments"
|
|
64
|
-
},
|
|
65
|
-
"pricing": {
|
|
66
|
-
"type": "object",
|
|
67
|
-
"description": "Pricing configuration per supported payment mode",
|
|
68
|
-
"additionalProperties": false,
|
|
69
|
-
"properties": {
|
|
70
|
-
"x402": {
|
|
71
|
-
"$ref": "#/definitions/PricingConfig"
|
|
72
|
-
},
|
|
73
|
-
"mpp-charge": {
|
|
74
|
-
"$ref": "#/definitions/PricingConfig"
|
|
75
|
-
},
|
|
76
|
-
"mpp-session": {
|
|
77
|
-
"$ref": "#/definitions/SessionPricingConfig"
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
"minProperties": 1
|
|
81
|
-
},
|
|
82
|
-
"endpoints": {
|
|
83
|
-
"type": "object",
|
|
84
|
-
"description": "Map of endpoint name to HTTP method + path (e.g. { 'price': 'GET /price' })",
|
|
85
|
-
"additionalProperties": {
|
|
86
|
-
"type": "string"
|
|
87
|
-
},
|
|
88
|
-
"minProperties": 1
|
|
89
|
-
},
|
|
90
|
-
"tags": {
|
|
91
|
-
"type": "array",
|
|
92
|
-
"items": {
|
|
93
|
-
"type": "string",
|
|
94
|
-
"minLength": 1,
|
|
95
|
-
"maxLength": 64
|
|
96
|
-
},
|
|
97
|
-
"uniqueItems": true,
|
|
98
|
-
"description": "Capability tags indexed with trigram search in the provider registry"
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
"definitions": {
|
|
102
|
-
"PaymentMode": {
|
|
103
|
-
"type": "string",
|
|
104
|
-
"enum": ["x402", "mpp-charge", "mpp-session"],
|
|
105
|
-
"description": "A supported RouteDock payment mode"
|
|
106
|
-
},
|
|
107
|
-
"PricingConfig": {
|
|
108
|
-
"type": "object",
|
|
109
|
-
"description": "Per-request pricing for x402 or mpp-charge modes",
|
|
110
|
-
"required": ["amount", "per"],
|
|
111
|
-
"additionalProperties": false,
|
|
112
|
-
"properties": {
|
|
113
|
-
"amount": {
|
|
114
|
-
"type": "string",
|
|
115
|
-
"description": "Cost per request in the payment asset (decimal string, e.g. '0.001')"
|
|
116
|
-
},
|
|
117
|
-
"per": {
|
|
118
|
-
"type": "string",
|
|
119
|
-
"const": "request",
|
|
120
|
-
"description": "Billing unit — always 'request' for non-session modes"
|
|
121
|
-
},
|
|
122
|
-
"facilitator": {
|
|
123
|
-
"type": "string",
|
|
124
|
-
"description": "x402 facilitator URL (required for x402 mode). OpenZeppelin Channels endpoint."
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
"SessionPricingConfig": {
|
|
129
|
-
"type": "object",
|
|
130
|
-
"description": "Per-voucher pricing for mpp-session (one-way-channel) mode",
|
|
131
|
-
"required": ["rate", "per", "channel_contract", "min_deposit", "refund_waiting_period_ledgers"],
|
|
132
|
-
"additionalProperties": false,
|
|
133
|
-
"properties": {
|
|
134
|
-
"rate": {
|
|
135
|
-
"type": "string",
|
|
136
|
-
"description": "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
|
|
137
|
-
},
|
|
138
|
-
"per": {
|
|
139
|
-
"type": "string",
|
|
140
|
-
"const": "voucher",
|
|
141
|
-
"description": "Billing unit — always 'voucher' for session mode"
|
|
142
|
-
},
|
|
143
|
-
"channel_contract": {
|
|
144
|
-
"type": "string",
|
|
145
|
-
"description": "Soroban contract address (C...) for the one-way-channel contract"
|
|
146
|
-
},
|
|
147
|
-
"min_deposit": {
|
|
148
|
-
"type": "string",
|
|
149
|
-
"description": "Minimum initial deposit to open a channel (decimal string)"
|
|
150
|
-
},
|
|
151
|
-
"refund_waiting_period_ledgers": {
|
|
152
|
-
"type": "integer",
|
|
153
|
-
"minimum": 1,
|
|
154
|
-
"description": "Ledgers the server has to settle before the funder can reclaim. Default: 17280 (~24h). Do not reduce in production."
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
2
|
-
import type { SessionState } from '../types.js'
|
|
3
|
-
import { RouteDockSessionError } from '../types.js'
|
|
4
|
-
|
|
5
|
-
// ── Interface ──────────────────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
export interface SessionStore {
|
|
8
|
-
get(channelId: string): Promise<SessionState | null>
|
|
9
|
-
/** Enforce monotonic invariant at application level before writing */
|
|
10
|
-
upsert(channelId: string, state: SessionState): Promise<void>
|
|
11
|
-
close(channelId: string): Promise<void>
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// ── Supabase implementation ────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
export class SupabaseSessionStore implements SessionStore {
|
|
17
|
-
constructor(private readonly supabase: SupabaseClient) {}
|
|
18
|
-
|
|
19
|
-
async get(channelId: string): Promise<SessionState | null> {
|
|
20
|
-
const { data, error } = await this.supabase
|
|
21
|
-
.from('sessions')
|
|
22
|
-
.select('*')
|
|
23
|
-
.eq('channel_id', channelId)
|
|
24
|
-
.maybeSingle()
|
|
25
|
-
|
|
26
|
-
if (error) throw new RouteDockSessionError(`SessionStore.get failed: ${error.message}`)
|
|
27
|
-
if (!data) return null
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
channel_id: data.channel_id as string,
|
|
31
|
-
payee: data.payee as string,
|
|
32
|
-
payer: data.payer as string,
|
|
33
|
-
cumulative_amount: String(data.cumulative_amount),
|
|
34
|
-
last_signature: data.last_signature as string,
|
|
35
|
-
status: data.status as SessionState['status'],
|
|
36
|
-
opened_at: data.opened_at as string,
|
|
37
|
-
updated_at: data.updated_at as string,
|
|
38
|
-
settlement_tx_hash: data.settlement_tx_hash as string | null,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async upsert(channelId: string, state: SessionState): Promise<void> {
|
|
43
|
-
// Application-level monotonic invariant check — catches non-increasing
|
|
44
|
-
// amounts before hitting the DB trigger, giving a typed error.
|
|
45
|
-
const existing = await this.get(channelId)
|
|
46
|
-
if (existing) {
|
|
47
|
-
const prev = parseFloat(existing.cumulative_amount)
|
|
48
|
-
const next = parseFloat(state.cumulative_amount)
|
|
49
|
-
if (next <= prev) {
|
|
50
|
-
throw new RouteDockSessionError(
|
|
51
|
-
`cumulative_amount must be strictly increasing: ${next} <= ${prev}`,
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const { error } = await this.supabase.from('sessions').upsert(
|
|
57
|
-
{
|
|
58
|
-
channel_id: state.channel_id,
|
|
59
|
-
payee: state.payee,
|
|
60
|
-
payer: state.payer,
|
|
61
|
-
cumulative_amount: state.cumulative_amount,
|
|
62
|
-
last_signature: state.last_signature,
|
|
63
|
-
status: state.status,
|
|
64
|
-
opened_at: state.opened_at,
|
|
65
|
-
updated_at: new Date().toISOString(),
|
|
66
|
-
settlement_tx_hash: state.settlement_tx_hash,
|
|
67
|
-
},
|
|
68
|
-
{ onConflict: 'channel_id' },
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
if (error) {
|
|
72
|
-
// DB trigger will raise 'cumulative_amount must be strictly increasing'
|
|
73
|
-
if (error.message.includes('strictly increasing')) {
|
|
74
|
-
throw new RouteDockSessionError(
|
|
75
|
-
`cumulative_amount must be strictly increasing (DB): ${error.message}`,
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
throw new RouteDockSessionError(`SessionStore.upsert failed: ${error.message}`)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async close(channelId: string): Promise<void> {
|
|
83
|
-
const { error } = await this.supabase
|
|
84
|
-
.from('sessions')
|
|
85
|
-
.update({ status: 'closed', updated_at: new Date().toISOString() })
|
|
86
|
-
.eq('channel_id', channelId)
|
|
87
|
-
|
|
88
|
-
if (error) throw new RouteDockSessionError(`SessionStore.close failed: ${error.message}`)
|
|
89
|
-
}
|
|
90
|
-
}
|