@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,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MppSessionClient — uses @stellar/mpp channel/client for off-chain
|
|
3
|
-
* cumulative commitments against a pre-deployed one-way-channel contract.
|
|
4
|
-
*
|
|
5
|
-
* The channel is deployed and funded before the agent runs. The client
|
|
6
|
-
* signs ed25519 commitments per the one-way-channel protocol — the
|
|
7
|
-
* @stellar/mpp library handles the 402 challenge-response cycle.
|
|
8
|
-
*/
|
|
9
|
-
import { Keypair } from '@stellar/stellar-sdk'
|
|
10
|
-
import { stellar } from '@stellar/mpp/channel/client'
|
|
11
|
-
import { Mppx } from 'mppx/client'
|
|
12
|
-
import type {
|
|
13
|
-
RouteDockManifest,
|
|
14
|
-
SessionHandle,
|
|
15
|
-
SessionCloseResult,
|
|
16
|
-
} from '../types.js'
|
|
17
|
-
import {
|
|
18
|
-
RouteDockManifestError,
|
|
19
|
-
RouteDockSessionError,
|
|
20
|
-
} from '../types.js'
|
|
21
|
-
|
|
22
|
-
const MIN_REFUND_WAITING_PERIOD = 17_280
|
|
23
|
-
|
|
24
|
-
export class MppSessionClient {
|
|
25
|
-
constructor(
|
|
26
|
-
private readonly keypair: Keypair,
|
|
27
|
-
private readonly network: 'testnet' | 'mainnet',
|
|
28
|
-
) {}
|
|
29
|
-
|
|
30
|
-
async openSession(
|
|
31
|
-
url: string,
|
|
32
|
-
manifest: RouteDockManifest,
|
|
33
|
-
commitmentSecret: string,
|
|
34
|
-
): Promise<SessionHandle> {
|
|
35
|
-
const pricing = manifest.pricing['mpp-session']
|
|
36
|
-
if (!pricing) {
|
|
37
|
-
throw new RouteDockManifestError('manifest.pricing.mpp-session missing')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const refundPeriod = pricing.refund_waiting_period_ledgers
|
|
41
|
-
if (refundPeriod < MIN_REFUND_WAITING_PERIOD) {
|
|
42
|
-
throw new RouteDockManifestError(
|
|
43
|
-
`refund_waiting_period_ledgers ${refundPeriod} < minimum ${MIN_REFUND_WAITING_PERIOD}`,
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const commitmentKey = Keypair.fromSecret(commitmentSecret)
|
|
48
|
-
const channelContract = pricing.channel_contract
|
|
49
|
-
const agentPublicKey = this.keypair.publicKey()
|
|
50
|
-
|
|
51
|
-
let currentCumulative = 0n
|
|
52
|
-
let vouchersIssued = 0
|
|
53
|
-
|
|
54
|
-
const mppx = Mppx.create({
|
|
55
|
-
polyfill: false,
|
|
56
|
-
methods: [
|
|
57
|
-
stellar.channel({
|
|
58
|
-
commitmentKey,
|
|
59
|
-
sourceAccount: this.keypair.publicKey(),
|
|
60
|
-
onProgress(event) {
|
|
61
|
-
if (event.type === 'signed') {
|
|
62
|
-
currentCumulative = BigInt(event.cumulativeAmount)
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
}),
|
|
66
|
-
],
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
const openTxHash = channelContract
|
|
70
|
-
|
|
71
|
-
const handle: SessionHandle = {
|
|
72
|
-
channelId: channelContract,
|
|
73
|
-
openTxHash,
|
|
74
|
-
|
|
75
|
-
async *stream(): AsyncIterable<unknown> {
|
|
76
|
-
while (true) {
|
|
77
|
-
let resp: globalThis.Response | null = null
|
|
78
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
79
|
-
try {
|
|
80
|
-
resp = await mppx.fetch(url)
|
|
81
|
-
if (resp.ok) break
|
|
82
|
-
resp = null
|
|
83
|
-
} catch {
|
|
84
|
-
if (attempt === 2) throw new RouteDockSessionError('Voucher request failed after 3 retries')
|
|
85
|
-
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)))
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (!resp || !resp.ok) {
|
|
89
|
-
throw new RouteDockSessionError(`Voucher request failed: HTTP ${resp?.status ?? 'unknown'}`)
|
|
90
|
-
}
|
|
91
|
-
vouchersIssued++
|
|
92
|
-
yield await resp.json()
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
async close(): Promise<SessionCloseResult> {
|
|
97
|
-
const { rpc: rpcMod, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk')
|
|
98
|
-
const rpcUrl = 'https://soroban-testnet.stellar.org'
|
|
99
|
-
const server = new rpcMod.Server(rpcUrl)
|
|
100
|
-
const contract = new Contract(channelContract)
|
|
101
|
-
const passphrase = 'Test SDF Network ; September 2015'
|
|
102
|
-
|
|
103
|
-
const account = await server.getAccount(agentPublicKey)
|
|
104
|
-
const simTx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase })
|
|
105
|
-
.addOperation(contract.call('prepare_commitment', nativeToScVal(currentCumulative, { type: 'i128' })))
|
|
106
|
-
.setTimeout(30)
|
|
107
|
-
.build()
|
|
108
|
-
const simResult = await server.simulateTransaction(simTx)
|
|
109
|
-
if (rpcMod.Api.isSimulationError(simResult)) {
|
|
110
|
-
throw new RouteDockSessionError(`prepare_commitment simulation failed: ${simResult.error}`)
|
|
111
|
-
}
|
|
112
|
-
const commitmentBytes = (simResult as { result?: { retval?: { bytes: () => Buffer } } }).result?.retval?.bytes()
|
|
113
|
-
if (!commitmentBytes) throw new RouteDockSessionError('prepare_commitment returned no bytes')
|
|
114
|
-
|
|
115
|
-
const signature = commitmentKey.sign(Buffer.from(commitmentBytes))
|
|
116
|
-
|
|
117
|
-
const closeResp = await fetch(url, {
|
|
118
|
-
method: 'DELETE',
|
|
119
|
-
headers: { 'Content-Type': 'application/json' },
|
|
120
|
-
body: JSON.stringify({
|
|
121
|
-
amount: currentCumulative.toString(),
|
|
122
|
-
signature: Buffer.from(signature).toString('hex'),
|
|
123
|
-
}),
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
if (!closeResp.ok) {
|
|
127
|
-
throw new RouteDockSessionError(`Channel close failed: HTTP ${closeResp.status}`)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const closeData = (await closeResp.json()) as { closeTxHash?: string }
|
|
131
|
-
const closeTxHash = closeData.closeTxHash ?? null
|
|
132
|
-
|
|
133
|
-
if (!closeTxHash) {
|
|
134
|
-
throw new RouteDockSessionError('Channel close response missing closeTxHash')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const totalPaid = (Number(currentCumulative) / 1e7).toFixed(7)
|
|
138
|
-
return { closeTxHash, totalPaid, vouchersIssued }
|
|
139
|
-
},
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return handle
|
|
143
|
-
}
|
|
144
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { Keypair } from '@stellar/stellar-sdk'
|
|
2
|
-
import { fetchManifest, selectMode, type ModeSelectOptions } from './ModeRouter.js'
|
|
3
|
-
import { X402Client } from './x402Client.js'
|
|
4
|
-
import { MppChargeClient } from './MppChargeClient.js'
|
|
5
|
-
import { MppSessionClient } from './MppSessionClient.js'
|
|
6
|
-
import type { PaymentResult, SessionHandle } from '../types.js'
|
|
7
|
-
import {
|
|
8
|
-
RouteDockManifestError,
|
|
9
|
-
RouteDockPolicyRejectedError,
|
|
10
|
-
} from '../types.js'
|
|
11
|
-
|
|
12
|
-
export interface SpendCap {
|
|
13
|
-
/** Maximum total USDC spend per day (decimal string, e.g. "1.00") */
|
|
14
|
-
daily: string
|
|
15
|
-
asset: 'USDC'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface RouteDockClientConfig {
|
|
19
|
-
/** Stellar keypair or raw secret key (S...) */
|
|
20
|
-
wallet: Keypair | string
|
|
21
|
-
network: 'testnet' | 'mainnet'
|
|
22
|
-
/** Optional local daily spend cap — checked before every payment */
|
|
23
|
-
spendCap?: SpendCap
|
|
24
|
-
/** Ed25519 secret key (S...) for signing channel commitments. Required for mpp-session. */
|
|
25
|
-
commitmentSecret?: string | undefined
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class RouteDockClient {
|
|
29
|
-
private readonly keypair: Keypair
|
|
30
|
-
private readonly network: 'testnet' | 'mainnet'
|
|
31
|
-
private readonly spendCap: SpendCap | undefined
|
|
32
|
-
private readonly commitmentSecret: string | undefined
|
|
33
|
-
|
|
34
|
-
/** Local daily accumulator keyed by YYYY-MM-DD */
|
|
35
|
-
private dailySpend: { date: string; total: number } = { date: '', total: 0 }
|
|
36
|
-
|
|
37
|
-
private readonly x402: X402Client
|
|
38
|
-
private readonly charge: MppChargeClient
|
|
39
|
-
private readonly session: MppSessionClient
|
|
40
|
-
|
|
41
|
-
constructor(config: RouteDockClientConfig) {
|
|
42
|
-
this.keypair =
|
|
43
|
-
typeof config.wallet === 'string' ? Keypair.fromSecret(config.wallet) : config.wallet
|
|
44
|
-
this.network = config.network
|
|
45
|
-
this.spendCap = config.spendCap
|
|
46
|
-
this.commitmentSecret = config.commitmentSecret
|
|
47
|
-
|
|
48
|
-
const secretKey = this.keypair.secret()
|
|
49
|
-
this.x402 = new X402Client(secretKey, this.network)
|
|
50
|
-
this.charge = new MppChargeClient(this.keypair, this.network)
|
|
51
|
-
this.session = new MppSessionClient(this.keypair, this.network)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Pay for one request at `url`. Fetches manifest, selects payment mode,
|
|
56
|
-
* checks local spend cap, executes payment, returns result.
|
|
57
|
-
*/
|
|
58
|
-
async pay(url: string, options?: ModeSelectOptions): Promise<PaymentResult> {
|
|
59
|
-
const baseUrl = new URL(url).origin
|
|
60
|
-
const manifest = await fetchManifest(baseUrl)
|
|
61
|
-
const mode = selectMode(manifest, options)
|
|
62
|
-
|
|
63
|
-
let result: PaymentResult
|
|
64
|
-
|
|
65
|
-
switch (mode) {
|
|
66
|
-
case 'x402':
|
|
67
|
-
result = await this.x402.pay(url, manifest)
|
|
68
|
-
break
|
|
69
|
-
case 'mpp-charge':
|
|
70
|
-
result = await this.charge.pay(url, manifest)
|
|
71
|
-
break
|
|
72
|
-
case 'mpp-session':
|
|
73
|
-
throw new RouteDockManifestError(
|
|
74
|
-
'Use client.openSession() for mpp-session mode — client.pay() only handles discrete payments',
|
|
75
|
-
)
|
|
76
|
-
default:
|
|
77
|
-
throw new RouteDockManifestError(`Unknown payment mode: ${mode as string}`)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
this._checkAndRecordSpend(result.amount)
|
|
81
|
-
return result
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Open a sustained MPP session at `url`. Verifies mpp-session is supported
|
|
86
|
-
* before opening a channel. Returns a SessionHandle for streaming + closing.
|
|
87
|
-
*/
|
|
88
|
-
async openSession(url: string): Promise<SessionHandle> {
|
|
89
|
-
const baseUrl = new URL(url).origin
|
|
90
|
-
const manifest = await fetchManifest(baseUrl)
|
|
91
|
-
|
|
92
|
-
if (!manifest.modes.includes('mpp-session')) {
|
|
93
|
-
throw new RouteDockManifestError(
|
|
94
|
-
`Provider at ${baseUrl} does not support mpp-session mode`,
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!this.commitmentSecret) {
|
|
99
|
-
throw new RouteDockManifestError(
|
|
100
|
-
'commitmentSecret is required in RouteDockClientConfig for mpp-session mode',
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return this.session.openSession(url, manifest, this.commitmentSecret)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Check local daily cap and record the spend. Throws if cap exceeded. */
|
|
108
|
-
private _checkAndRecordSpend(amount: string): void {
|
|
109
|
-
if (!this.spendCap) return
|
|
110
|
-
|
|
111
|
-
const today = new Date().toISOString().slice(0, 10)
|
|
112
|
-
if (this.dailySpend.date !== today) {
|
|
113
|
-
this.dailySpend = { date: today, total: 0 }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const amountNum = parseFloat(amount)
|
|
117
|
-
const capNum = parseFloat(this.spendCap.daily)
|
|
118
|
-
if (this.dailySpend.total + amountNum > capNum) {
|
|
119
|
-
throw new RouteDockPolicyRejectedError('local_daily_cap_exceeded')
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
this.dailySpend.total += amountNum
|
|
123
|
-
}
|
|
124
|
-
}
|
package/src/client/index.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Client-only exports (no provider/Express)
|
|
2
|
-
export * from '../types.js'
|
|
3
|
-
export * from './RouteDockClient.js'
|
|
4
|
-
export * from './ModeRouter.js'
|
|
5
|
-
export * from './x402Client.js'
|
|
6
|
-
export * from './MppChargeClient.js'
|
|
7
|
-
export * from './MppSessionClient.js'
|
|
8
|
-
export * from '../store/SessionStore.js'
|
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'
|