mppx 0.4.7 → 0.4.9
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/CHANGELOG.md +15 -3
- package/README.md +13 -13
- package/dist/BodyDigest.d.ts.map +1 -1
- package/dist/BodyDigest.js.map +1 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js.map +1 -1
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js.map +1 -1
- package/dist/Errors.js +64 -67
- package/dist/Errors.js.map +1 -1
- package/dist/PaymentRequest.d.ts.map +1 -1
- package/dist/PaymentRequest.js.map +1 -1
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js.map +1 -1
- package/dist/Store.d.ts +14 -4
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +17 -0
- package/dist/Store.js.map +1 -1
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +40 -5
- package/dist/cli/account.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +24 -8
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/internal.d.ts.map +1 -1
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +11 -23
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +1 -1
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +1 -1
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +5 -1
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +5 -2
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +3 -1
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.js +1 -1
- package/dist/proxy/Service.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts +2 -2
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +4 -2
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +47 -11
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Request.d.ts.map +1 -1
- package/dist/server/Request.js.map +1 -1
- package/dist/stripe/Methods.d.ts.map +1 -1
- package/dist/stripe/Methods.js.map +1 -1
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +1 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/address.d.ts +3 -0
- package/dist/tempo/internal/address.d.ts.map +1 -0
- package/dist/tempo/internal/address.js +4 -0
- package/dist/tempo/internal/address.js.map +1 -0
- package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
- package/dist/tempo/internal/auto-swap.js +4 -4
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +4 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +12 -4
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +11 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +110 -51
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +31 -23
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +41 -1
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +51 -10
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +2 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +4 -2
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Receipt.d.ts.map +1 -1
- package/dist/tempo/session/Receipt.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js +3 -2
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +2 -2
- package/src/BodyDigest.ts +1 -0
- package/src/Challenge.test-d.ts +1 -0
- package/src/Challenge.ts +1 -0
- package/src/Credential.ts +1 -0
- package/src/Errors.test.ts +27 -39
- package/src/Expires.test.ts +1 -0
- package/src/PaymentRequest.ts +1 -0
- package/src/Receipt.ts +1 -0
- package/src/Store.test-d.ts +59 -0
- package/src/Store.test.ts +56 -6
- package/src/Store.ts +31 -4
- package/src/cli/account.ts +65 -30
- package/src/cli/cli.test.ts +127 -1
- package/src/cli/cli.ts +23 -8
- package/src/cli/config.test.ts +1 -0
- package/src/cli/internal.ts +1 -0
- package/src/cli/plugins/stripe.ts +1 -0
- package/src/cli/plugins/tempo.ts +21 -24
- package/src/cli/utils.ts +1 -0
- package/src/client/Mppx.test-d.ts +1 -0
- package/src/client/internal/Fetch.browser.test.ts +1 -0
- package/src/client/internal/Fetch.test-d.ts +1 -0
- package/src/client/internal/Fetch.test.ts +1 -0
- package/src/client/internal/Fetch.ts +1 -1
- package/src/internal/constantTimeEqual.test.ts +1 -0
- package/src/internal/types.ts +1 -3
- package/src/mcp-sdk/client/McpClient.test-d.ts +1 -0
- package/src/mcp-sdk/client/McpClient.test.ts +1 -0
- package/src/mcp-sdk/client/McpClient.ts +2 -0
- package/src/mcp-sdk/server/Transport.test.ts +1 -0
- package/src/mcp-sdk/server/Transport.ts +1 -0
- package/src/middlewares/elysia.test.ts +90 -0
- package/src/middlewares/elysia.ts +5 -1
- package/src/middlewares/express.test.ts +62 -2
- package/src/middlewares/express.ts +6 -2
- package/src/middlewares/hono.ts +1 -0
- package/src/middlewares/internal/mppx.test.ts +1 -0
- package/src/middlewares/nextjs.test.ts +1 -0
- package/src/proxy/Proxy.test.ts +57 -0
- package/src/proxy/Proxy.ts +8 -1
- package/src/proxy/Service.test.ts +1 -0
- package/src/proxy/Service.ts +8 -2
- package/src/proxy/internal/Headers.test.ts +1 -0
- package/src/proxy/internal/Route.test.ts +57 -0
- package/src/proxy/internal/Route.ts +3 -1
- package/src/proxy/services/openai.test.ts +1 -0
- package/src/server/Mppx.test.ts +438 -0
- package/src/server/Mppx.ts +51 -13
- package/src/server/Request.test.ts +1 -0
- package/src/server/Request.ts +1 -0
- package/src/server/Response.test.ts +1 -0
- package/src/server/Transport.test.ts +1 -0
- package/src/stripe/Methods.ts +1 -0
- package/src/stripe/client/Charge.test.ts +1 -0
- package/src/stripe/server/Charge.test.ts +1 -0
- package/src/tempo/Attribution.test.ts +1 -0
- package/src/tempo/Methods.ts +1 -0
- package/src/tempo/client/ChannelOps.test.ts +1 -0
- package/src/tempo/client/ChannelOps.ts +1 -0
- package/src/tempo/client/Charge.ts +1 -0
- package/src/tempo/client/Session.test.ts +1 -0
- package/src/tempo/client/Session.ts +1 -0
- package/src/tempo/client/SessionManager.test.ts +28 -0
- package/src/tempo/client/SessionManager.ts +2 -1
- package/src/tempo/internal/address.ts +6 -0
- package/src/tempo/internal/auto-swap.test.ts +1 -0
- package/src/tempo/internal/auto-swap.ts +4 -3
- package/src/tempo/internal/defaults.test.ts +1 -0
- package/src/tempo/internal/fee-payer.test.ts +1 -0
- package/src/tempo/internal/fee-payer.ts +19 -4
- package/src/tempo/server/Charge.test.ts +1081 -31
- package/src/tempo/server/Charge.ts +159 -63
- package/src/tempo/server/Session.test.ts +896 -107
- package/src/tempo/server/Session.ts +41 -23
- package/src/tempo/server/Sse.test.ts +2 -0
- package/src/tempo/server/internal/transport.test.ts +30 -0
- package/src/tempo/server/internal/transport.ts +41 -2
- package/src/tempo/session/Chain.test.ts +145 -0
- package/src/tempo/session/Chain.ts +59 -10
- package/src/tempo/session/Channel.test.ts +1 -0
- package/src/tempo/session/ChannelStore.test.ts +11 -0
- package/src/tempo/session/ChannelStore.ts +7 -3
- package/src/tempo/session/Receipt.test.ts +1 -0
- package/src/tempo/session/Receipt.ts +1 -0
- package/src/tempo/session/Sse.test.ts +2 -0
- package/src/tempo/session/Sse.ts +1 -0
- package/src/tempo/session/Voucher.test.ts +1 -0
- package/src/tempo/session/Voucher.ts +4 -2
- package/src/viem/Account.test.ts +1 -0
- package/src/viem/Client.test.ts +1 -0
- package/src/viem/Client.ts +1 -0
package/src/cli/cli.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import * as fs from 'node:fs'
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import * as path from 'node:path'
|
|
4
|
+
|
|
4
5
|
import { Cli, Errors, z } from 'incur'
|
|
5
6
|
import { Base64 } from 'ox'
|
|
6
7
|
import { type Address, createClient, http } from 'viem'
|
|
7
8
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
8
9
|
import { tempo as tempoMainnet } from 'viem/chains'
|
|
10
|
+
|
|
9
11
|
import * as Challenge from '../Challenge.js'
|
|
12
|
+
import { normalizeHeaders } from '../client/internal/Fetch.js'
|
|
10
13
|
import * as Mppx from '../client/Mppx.js'
|
|
11
14
|
import { createDefaultStore, createKeychain, resolveAccountName } from './account.js'
|
|
12
15
|
import { loadConfig, resolvePlugin } from './internal.js'
|
|
@@ -130,15 +133,27 @@ const cli = Cli.create('mppx', {
|
|
|
130
133
|
return hasProtocol ? c.args.url : `${isLocal ? 'http' : 'https'}://${c.args.url}`
|
|
131
134
|
})()
|
|
132
135
|
const { hostname } = new URL(url)
|
|
133
|
-
|
|
136
|
+
const insecure =
|
|
134
137
|
c.options.insecure ||
|
|
135
138
|
hostname === 'localhost' ||
|
|
136
139
|
hostname.endsWith('.localhost') ||
|
|
137
140
|
hostname.endsWith('.local')
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
|
|
142
|
+
// Scoped fetch that temporarily disables TLS verification only for
|
|
143
|
+
// the target connection when `insecure` is true, then restores
|
|
144
|
+
// the original value so other HTTPS connections are unaffected.
|
|
145
|
+
const targetFetch: typeof globalThis.fetch = insecure
|
|
146
|
+
? async (input, init) => {
|
|
147
|
+
const orig = process.env.NODE_TLS_REJECT_UNAUTHORIZED
|
|
148
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
149
|
+
try {
|
|
150
|
+
return await globalThis.fetch(input, init)
|
|
151
|
+
} finally {
|
|
152
|
+
if (orig === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
|
|
153
|
+
else process.env.NODE_TLS_REJECT_UNAUTHORIZED = orig
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
: globalThis.fetch
|
|
142
157
|
|
|
143
158
|
// Node.js doesn't resolve *.localhost subdomains to loopback (unlike
|
|
144
159
|
// browsers per RFC 6761). Rewrite the URL to 127.0.0.1 and set the
|
|
@@ -170,7 +185,7 @@ const cli = Cli.create('mppx', {
|
|
|
170
185
|
}
|
|
171
186
|
|
|
172
187
|
if (c.options.verbose >= 2) printRequestHeaders(url, init, info)
|
|
173
|
-
const challengeResponse = await
|
|
188
|
+
const challengeResponse = await targetFetch(fetchUrl, init)
|
|
174
189
|
if (challengeResponse.status !== 402) {
|
|
175
190
|
if (c.options.fail && challengeResponse.status >= 400)
|
|
176
191
|
return c.error({
|
|
@@ -300,14 +315,14 @@ const cli = Cli.create('mppx', {
|
|
|
300
315
|
|
|
301
316
|
// Send credential and get response
|
|
302
317
|
const credentialHeaders = {
|
|
303
|
-
...(init.headers
|
|
318
|
+
...normalizeHeaders(init.headers),
|
|
304
319
|
Authorization: credential,
|
|
305
320
|
}
|
|
306
321
|
plugin?.prepareCredentialRequest?.({ challenge, credential, headers: credentialHeaders })
|
|
307
322
|
|
|
308
323
|
const credentialFetchInit = { ...init, headers: credentialHeaders }
|
|
309
324
|
if (c.options.verbose >= 2) printRequestHeaders(url, credentialFetchInit, info)
|
|
310
|
-
const credentialResponse = await
|
|
325
|
+
const credentialResponse = await targetFetch(fetchUrl, credentialFetchInit)
|
|
311
326
|
|
|
312
327
|
if (c.options.fail && credentialResponse.status >= 400)
|
|
313
328
|
return c.error({
|
package/src/cli/config.test.ts
CHANGED
package/src/cli/internal.ts
CHANGED
package/src/cli/plugins/tempo.ts
CHANGED
|
@@ -3,11 +3,14 @@ import * as fs from 'node:fs'
|
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
4
|
import * as os from 'node:os'
|
|
5
5
|
import * as path from 'node:path'
|
|
6
|
+
|
|
6
7
|
import { Errors, z } from 'incur'
|
|
7
8
|
import { Base64 } from 'ox'
|
|
8
9
|
import type { Address } from 'viem'
|
|
9
10
|
import { createClient, http } from 'viem'
|
|
10
11
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
12
|
+
|
|
13
|
+
import { normalizeHeaders } from '../../client/internal/Fetch.js'
|
|
11
14
|
import * as Credential from '../../Credential.js'
|
|
12
15
|
import { tempo as tempoMethods } from '../../tempo/client/index.js'
|
|
13
16
|
import type { SessionCredentialPayload } from '../../tempo/session/Types.js'
|
|
@@ -34,6 +37,7 @@ export function tempo() {
|
|
|
34
37
|
cumulativeAmount: bigint
|
|
35
38
|
escrowContract: Address
|
|
36
39
|
chainId: number
|
|
40
|
+
action?: 'voucher' | 'close'
|
|
37
41
|
}): Promise<string>
|
|
38
42
|
source: string
|
|
39
43
|
}
|
|
@@ -183,11 +187,17 @@ export function tempo() {
|
|
|
183
187
|
|
|
184
188
|
// Store session support for use in lifecycle hooks
|
|
185
189
|
_session = {
|
|
186
|
-
async signVoucher({
|
|
190
|
+
async signVoucher({
|
|
191
|
+
channelId,
|
|
192
|
+
cumulativeAmount,
|
|
193
|
+
escrowContract,
|
|
194
|
+
chainId,
|
|
195
|
+
action = 'voucher',
|
|
196
|
+
}) {
|
|
187
197
|
return Credential.serialize({
|
|
188
198
|
challenge,
|
|
189
199
|
payload: {
|
|
190
|
-
action
|
|
200
|
+
action,
|
|
191
201
|
channelId,
|
|
192
202
|
cumulativeAmount: cumulativeAmount.toString(),
|
|
193
203
|
signature: await signVoucher(
|
|
@@ -254,32 +264,17 @@ export function tempo() {
|
|
|
254
264
|
}
|
|
255
265
|
}
|
|
256
266
|
|
|
257
|
-
// Handle non-SSE session response (server returned non-streaming)
|
|
258
|
-
|
|
267
|
+
// Handle non-SSE session response (server returned non-streaming).
|
|
268
|
+
// The open credential already paid for this unit — no follow-up
|
|
269
|
+
// voucher is needed. Just record the cumulativeAmount so the
|
|
270
|
+
// channel close uses the correct value.
|
|
271
|
+
const credentialResponse = response
|
|
259
272
|
if (
|
|
260
273
|
credentialResponse.ok &&
|
|
261
274
|
!credentialResponse.headers.get('Content-Type')?.includes('text/event-stream')
|
|
262
275
|
) {
|
|
263
276
|
if (parsed.payload.action === 'open' && 'cumulativeAmount' in parsed.payload) {
|
|
264
|
-
|
|
265
|
-
cumulativeAmount = BigInt(parsed.payload.cumulativeAmount) + tickAmount
|
|
266
|
-
|
|
267
|
-
if (escrowContract) {
|
|
268
|
-
const voucherCred = await _session.signVoucher({
|
|
269
|
-
channelId,
|
|
270
|
-
cumulativeAmount,
|
|
271
|
-
escrowContract,
|
|
272
|
-
chainId,
|
|
273
|
-
})
|
|
274
|
-
credentialResponse = await globalThis.fetch(fetchUrl, {
|
|
275
|
-
...fetchInit,
|
|
276
|
-
headers: {
|
|
277
|
-
...(fetchInit.headers as Record<string, string>),
|
|
278
|
-
Accept: 'text/event-stream',
|
|
279
|
-
Authorization: voucherCred,
|
|
280
|
-
},
|
|
281
|
-
})
|
|
282
|
-
}
|
|
277
|
+
cumulativeAmount = BigInt(parsed.payload.cumulativeAmount)
|
|
283
278
|
}
|
|
284
279
|
}
|
|
285
280
|
|
|
@@ -598,6 +593,7 @@ async function closeChannel(opts: {
|
|
|
598
593
|
cumulativeAmount: bigint
|
|
599
594
|
escrowContract: Address
|
|
600
595
|
chainId: number
|
|
596
|
+
action?: 'voucher' | 'close'
|
|
601
597
|
}): Promise<string>
|
|
602
598
|
}
|
|
603
599
|
info: (msg: string) => void
|
|
@@ -612,11 +608,12 @@ async function closeChannel(opts: {
|
|
|
612
608
|
cumulativeAmount: opts.cumulativeAmount,
|
|
613
609
|
escrowContract: opts.escrowContract,
|
|
614
610
|
chainId: opts.chainId,
|
|
611
|
+
action: 'close',
|
|
615
612
|
})
|
|
616
613
|
const closeRes = await globalThis.fetch(opts.fetchUrl, {
|
|
617
614
|
...opts.fetchInit,
|
|
618
615
|
headers: {
|
|
619
|
-
...(opts.fetchInit.headers
|
|
616
|
+
...normalizeHeaders(opts.fetchInit.headers),
|
|
620
617
|
Authorization: closeCred,
|
|
621
618
|
},
|
|
622
619
|
})
|
package/src/cli/utils.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { describe, expect, test, vi } from 'vitest'
|
|
|
6
6
|
import * as Http from '~test/Http.js'
|
|
7
7
|
import { rpcUrl } from '~test/tempo/prool.js'
|
|
8
8
|
import { accounts, asset, chain, client, http } from '~test/tempo/viem.js'
|
|
9
|
+
|
|
9
10
|
import * as Fetch from './Fetch.js'
|
|
10
11
|
|
|
11
12
|
const realm = 'api.example.com'
|
|
@@ -193,7 +193,7 @@ export function restore(): void {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/** @internal Normalizes headers to a plain object for spreading. */
|
|
196
|
-
function normalizeHeaders(headers: unknown): Record<string, string> {
|
|
196
|
+
export function normalizeHeaders(headers: unknown): Record<string, string> {
|
|
197
197
|
if (!headers) return {}
|
|
198
198
|
if (headers instanceof Headers) {
|
|
199
199
|
const result: Record<string, string> = {}
|
package/src/internal/types.ts
CHANGED
|
@@ -256,9 +256,7 @@ export type LastInUnion<U> =
|
|
|
256
256
|
|
|
257
257
|
/** @internal */
|
|
258
258
|
export type UnionToIntersection<union> = (
|
|
259
|
-
union extends unknown
|
|
260
|
-
? (arg: union) => 0
|
|
261
|
-
: never
|
|
259
|
+
union extends unknown ? (arg: union) => 0 : never
|
|
262
260
|
) extends (arg: infer i) => 0
|
|
263
261
|
? i
|
|
264
262
|
: never
|
|
@@ -2,6 +2,7 @@ import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
|
2
2
|
import { tempo } from 'mppx/client'
|
|
3
3
|
import type { Account } from 'viem'
|
|
4
4
|
import { describe, expectTypeOf, test } from 'vitest'
|
|
5
|
+
|
|
5
6
|
import * as McpClient from './McpClient.js'
|
|
6
7
|
|
|
7
8
|
describe('McpClient.wrap', () => {
|
|
@@ -8,6 +8,7 @@ import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
|
8
8
|
import { createClient } from 'viem'
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
10
10
|
import { accounts, asset, chain, http, client as testClient } from '~test/tempo/viem.js'
|
|
11
|
+
|
|
11
12
|
import * as McpServer_transport from '../server/Transport.js'
|
|
12
13
|
import * as McpClient from './McpClient.js'
|
|
13
14
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
2
2
|
import type { McpError } from '@modelcontextprotocol/sdk/types.js'
|
|
3
|
+
|
|
3
4
|
import type * as Challenge from '../../Challenge.js'
|
|
4
5
|
import * as Credential from '../../Credential.js'
|
|
5
6
|
import * as core_Mcp from '../../Mcp.js'
|
|
@@ -84,6 +85,7 @@ export function wrap<
|
|
|
84
85
|
const installed = methods.map((m) => `${m.name}.${m.intent}`).join(', ')
|
|
85
86
|
throw new Error(
|
|
86
87
|
`No compatible payment method. Server offers: ${available}. Client has: ${installed}`,
|
|
88
|
+
{ cause: error },
|
|
87
89
|
)
|
|
88
90
|
}
|
|
89
91
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as http from 'node:http'
|
|
2
|
+
|
|
3
|
+
import { Elysia } from 'elysia'
|
|
4
|
+
import { Receipt } from 'mppx'
|
|
5
|
+
import { Mppx as Mppx_client, tempo as tempo_client } from 'mppx/client'
|
|
6
|
+
import { Mppx } from 'mppx/elysia'
|
|
7
|
+
import { tempo as tempo_server } from 'mppx/server'
|
|
8
|
+
import { describe, expect, test } from 'vitest'
|
|
9
|
+
import { accounts, asset, client } from '~test/tempo/viem.js'
|
|
10
|
+
|
|
11
|
+
function createServer(app: Elysia<any, any, any, any, any, any, any>) {
|
|
12
|
+
return new Promise<{ url: string; close: () => void }>((resolve) => {
|
|
13
|
+
const server = http.createServer(async (req, res) => {
|
|
14
|
+
const url = `http://localhost${req.url}`
|
|
15
|
+
const headers = new Headers()
|
|
16
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2)
|
|
17
|
+
headers.append(req.rawHeaders[i]!, req.rawHeaders[i + 1]!)
|
|
18
|
+
const request = new Request(url, { method: req.method!, headers })
|
|
19
|
+
const response = await app.fetch(request)
|
|
20
|
+
res.writeHead(response.status, Object.fromEntries(response.headers))
|
|
21
|
+
const body = await response.text()
|
|
22
|
+
if (body) res.write(body)
|
|
23
|
+
res.end()
|
|
24
|
+
})
|
|
25
|
+
server.listen(0, () => {
|
|
26
|
+
const { port } = server.address() as { port: number }
|
|
27
|
+
resolve({
|
|
28
|
+
url: `http://localhost:${port}`,
|
|
29
|
+
close: () => server.close(),
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const secretKey = 'test-secret-key'
|
|
36
|
+
|
|
37
|
+
describe('charge', () => {
|
|
38
|
+
const mppx = Mppx.create({
|
|
39
|
+
methods: [
|
|
40
|
+
tempo_server.charge({
|
|
41
|
+
getClient: () => client,
|
|
42
|
+
currency: asset,
|
|
43
|
+
recipient: accounts[0].address,
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
secretKey,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const { fetch } = Mppx_client.create({
|
|
50
|
+
polyfill: false,
|
|
51
|
+
methods: [
|
|
52
|
+
tempo_client.charge({
|
|
53
|
+
account: accounts[1],
|
|
54
|
+
getClient: () => client,
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('returns 402 when no credential', async () => {
|
|
60
|
+
const app = new Elysia().guard({ beforeHandle: mppx.charge({ amount: '1' }) }, (app) =>
|
|
61
|
+
app.get('/', () => ({ fortune: 'You will be rich' })),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const server = await createServer(app)
|
|
65
|
+
const response = await globalThis.fetch(server.url)
|
|
66
|
+
expect(response.status).toBe(402)
|
|
67
|
+
expect(response.headers.get('WWW-Authenticate')).toContain('Payment')
|
|
68
|
+
|
|
69
|
+
server.close()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('returns 200 with receipt on valid payment', async () => {
|
|
73
|
+
const app = new Elysia().guard({ beforeHandle: mppx.charge({ amount: '1' }) }, (app) =>
|
|
74
|
+
app.get('/', () => ({ fortune: 'You will be rich' })),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const server = await createServer(app)
|
|
78
|
+
const response = await fetch(server.url)
|
|
79
|
+
expect(response.status).toBe(200)
|
|
80
|
+
|
|
81
|
+
const body = await response.json()
|
|
82
|
+
expect(body).toEqual({ fortune: 'You will be rich' })
|
|
83
|
+
|
|
84
|
+
const receipt = Receipt.fromResponse(response)
|
|
85
|
+
expect(receipt.status).toBe('success')
|
|
86
|
+
expect(receipt.method).toBe('tempo')
|
|
87
|
+
|
|
88
|
+
server.close()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Context } from 'elysia'
|
|
2
|
+
|
|
2
3
|
import * as Mppx_core from '../server/Mppx.js'
|
|
3
4
|
import * as Mppx_internal from './internal/mppx.js'
|
|
4
5
|
|
|
@@ -59,8 +60,11 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
|
|
|
59
60
|
intent: intent,
|
|
60
61
|
options: intent extends (options: infer options) => any ? options : never,
|
|
61
62
|
): ElysiaHook {
|
|
62
|
-
return async ({ request }) => {
|
|
63
|
+
return async ({ request, set }) => {
|
|
63
64
|
const result = await intent(options)(request)
|
|
64
65
|
if (result.status === 402) return result.challenge
|
|
66
|
+
const receipt = result.withReceipt(new Response())
|
|
67
|
+
const header = receipt.headers.get('Payment-Receipt')
|
|
68
|
+
if (header) set.headers['Payment-Receipt'] = header
|
|
65
69
|
}
|
|
66
70
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
2
|
import { Receipt } from 'mppx'
|
|
3
3
|
import { Mppx as Mppx_client, session as sessionIntent, tempo as tempo_client } from 'mppx/client'
|
|
4
|
-
import { Mppx } from 'mppx/express'
|
|
5
|
-
import { tempo as tempo_server } from 'mppx/server'
|
|
4
|
+
import { Mppx, payment } from 'mppx/express'
|
|
5
|
+
import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
6
6
|
import type { Address } from 'viem'
|
|
7
7
|
import { Addresses } from 'viem/tempo'
|
|
8
8
|
import { beforeAll, describe, expect, test } from 'vitest'
|
|
@@ -158,3 +158,63 @@ describe('session', () => {
|
|
|
158
158
|
server.close()
|
|
159
159
|
})
|
|
160
160
|
})
|
|
161
|
+
|
|
162
|
+
describe('payment', () => {
|
|
163
|
+
const mppx = Mppx_server.create({
|
|
164
|
+
methods: [
|
|
165
|
+
tempo_server({
|
|
166
|
+
getClient: () => client,
|
|
167
|
+
currency: asset,
|
|
168
|
+
recipient: accounts[0].address,
|
|
169
|
+
}),
|
|
170
|
+
],
|
|
171
|
+
secretKey,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const { fetch } = Mppx_client.create({
|
|
175
|
+
polyfill: false,
|
|
176
|
+
methods: [
|
|
177
|
+
tempo_client({
|
|
178
|
+
account: accounts[1],
|
|
179
|
+
getClient: () => client,
|
|
180
|
+
}),
|
|
181
|
+
],
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('returns 402 when no credential', async () => {
|
|
185
|
+
const app = express()
|
|
186
|
+
app.get('/', payment(mppx.charge, { amount: '1' }), (_req, res) => {
|
|
187
|
+
res.json({ fortune: 'You will be rich' })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const server = await createServer(app)
|
|
191
|
+
const response = await globalThis.fetch(server.url)
|
|
192
|
+
expect(response.status).toBe(402)
|
|
193
|
+
expect(response.headers.get('WWW-Authenticate')).toContain('Payment')
|
|
194
|
+
|
|
195
|
+
server.close()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('returns 200 with receipt on valid payment', async () => {
|
|
199
|
+
const app = express()
|
|
200
|
+
app.get('/', payment(mppx.charge, { amount: '1' }), (_req, res) => {
|
|
201
|
+
res.json({ fortune: 'You will be rich' })
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const server = await createServer(app)
|
|
205
|
+
const response = await fetch(server.url)
|
|
206
|
+
expect(response.status).toBe(200)
|
|
207
|
+
|
|
208
|
+
const body = await response.json()
|
|
209
|
+
expect(body).toEqual({ fortune: 'You will be rich' })
|
|
210
|
+
|
|
211
|
+
const receiptHeader = response.headers.get('Payment-Receipt')
|
|
212
|
+
expect(receiptHeader).toBeTruthy()
|
|
213
|
+
|
|
214
|
+
const receipt = Receipt.fromResponse(response)
|
|
215
|
+
expect(receipt.status).toBe('success')
|
|
216
|
+
expect(receipt.method).toBe('tempo')
|
|
217
|
+
|
|
218
|
+
server.close()
|
|
219
|
+
})
|
|
220
|
+
})
|
|
@@ -4,8 +4,8 @@ import type {
|
|
|
4
4
|
NextFunction,
|
|
5
5
|
RequestHandler,
|
|
6
6
|
} from 'express'
|
|
7
|
+
|
|
7
8
|
import * as Mppx_core from '../server/Mppx.js'
|
|
8
|
-
import * as Request from '../server/Request.js'
|
|
9
9
|
import * as Mppx_internal from './internal/mppx.js'
|
|
10
10
|
|
|
11
11
|
export * from '../server/Methods.js'
|
|
@@ -60,7 +60,11 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
|
|
|
60
60
|
options: intent extends (options: infer options) => any ? options : never,
|
|
61
61
|
): RequestHandler {
|
|
62
62
|
return async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
63
|
-
const
|
|
63
|
+
const request = new Request(`${req.protocol}://${req.hostname}${req.originalUrl}`, {
|
|
64
|
+
method: req.method,
|
|
65
|
+
headers: req.headers as Record<string, string>,
|
|
66
|
+
})
|
|
67
|
+
const result = await intent(options)(request)
|
|
64
68
|
|
|
65
69
|
if (result.status === 402) {
|
|
66
70
|
const challenge = result.challenge as Response
|
package/src/middlewares/hono.ts
CHANGED
package/src/proxy/Proxy.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
|
4
4
|
import { afterEach, describe, expect, test } from 'vitest'
|
|
5
5
|
import * as Http from '~test/Http.js'
|
|
6
6
|
import { accounts, asset, client } from '~test/tempo/viem.js'
|
|
7
|
+
|
|
7
8
|
import * as ApiProxy from './Proxy.js'
|
|
8
9
|
import * as Service from './Service.js'
|
|
9
10
|
import { anthropic } from './services/anthropic.js'
|
|
@@ -630,6 +631,62 @@ describe('create', () => {
|
|
|
630
631
|
})
|
|
631
632
|
})
|
|
632
633
|
|
|
634
|
+
test('behavior: management POST falls back to paid route with different method', async () => {
|
|
635
|
+
upstream = await createUpstream(() => Response.json({ ok: true }))
|
|
636
|
+
const proxy = ApiProxy.create({
|
|
637
|
+
services: [
|
|
638
|
+
Service.from('api', {
|
|
639
|
+
baseUrl: upstream.url,
|
|
640
|
+
routes: {
|
|
641
|
+
// Registered as GET but management POSTs (e.g. session close)
|
|
642
|
+
// should still reach this paid endpoint via fallback.
|
|
643
|
+
'GET /v1/stream': mppx_server.charge({ amount: '1', decimals: 6 }),
|
|
644
|
+
},
|
|
645
|
+
}),
|
|
646
|
+
],
|
|
647
|
+
})
|
|
648
|
+
proxyServer = await Http.createServer(proxy.listener)
|
|
649
|
+
|
|
650
|
+
const res = await fetch(`${proxyServer.url}/api/v1/stream`, {
|
|
651
|
+
method: 'POST',
|
|
652
|
+
headers: { Authorization: 'x' },
|
|
653
|
+
})
|
|
654
|
+
// Should hit the paid endpoint and get a 402 challenge, not 404
|
|
655
|
+
expect(res.status).toBe(402)
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
test('behavior: POST to unregistered method does not fall back to free GET route', async () => {
|
|
659
|
+
upstream = await createUpstream(() => Response.json({ ok: true }))
|
|
660
|
+
const proxy = ApiProxy.create({
|
|
661
|
+
services: [
|
|
662
|
+
Service.from('api', {
|
|
663
|
+
baseUrl: upstream.url,
|
|
664
|
+
routes: {
|
|
665
|
+
// GET is free, but there is no POST handler
|
|
666
|
+
'GET /v1beta/cachedContents': true,
|
|
667
|
+
'POST /v1beta/models/:model': mppx_server.charge({
|
|
668
|
+
amount: '1',
|
|
669
|
+
decimals: 6,
|
|
670
|
+
}),
|
|
671
|
+
},
|
|
672
|
+
}),
|
|
673
|
+
],
|
|
674
|
+
})
|
|
675
|
+
proxyServer = await Http.createServer(proxy.listener)
|
|
676
|
+
|
|
677
|
+
// A POST with a bogus authorization header should NOT fall back
|
|
678
|
+
// to the free GET route — it must return 404.
|
|
679
|
+
const res = await fetch(`${proxyServer.url}/api/v1beta/cachedContents`, {
|
|
680
|
+
method: 'POST',
|
|
681
|
+
headers: {
|
|
682
|
+
Authorization: 'x',
|
|
683
|
+
'Content-Type': 'application/json',
|
|
684
|
+
},
|
|
685
|
+
body: JSON.stringify({ model: 'models/gemini-2.0-flash-001', contents: [] }),
|
|
686
|
+
})
|
|
687
|
+
expect(res.status).toBe(404)
|
|
688
|
+
})
|
|
689
|
+
|
|
633
690
|
test('behavior: forwards query params to upstream', async () => {
|
|
634
691
|
upstream = await createUpstream((req) => Response.json({ search: new URL(req.url).search }))
|
|
635
692
|
const proxy = ApiProxy.create({
|
package/src/proxy/Proxy.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type * as http from 'node:http'
|
|
2
|
+
|
|
2
3
|
import { createFetchProxy } from '@remix-run/fetch-proxy'
|
|
4
|
+
|
|
3
5
|
import * as Request from '../server/Request.js'
|
|
4
6
|
import * as Headers from './internal/Headers.js'
|
|
5
7
|
import * as Route from './internal/Route.js'
|
|
@@ -137,7 +139,12 @@ export function create(config: create.Config): Proxy {
|
|
|
137
139
|
// is registered for a different HTTP method (e.g. GET). Fall back to
|
|
138
140
|
// path-only matching so the payment handler can process the action.
|
|
139
141
|
(request.method === 'POST' && request.headers.has('authorization')
|
|
140
|
-
? Route.matchPath(
|
|
142
|
+
? Route.matchPath(
|
|
143
|
+
service.routes,
|
|
144
|
+
upstreamPath,
|
|
145
|
+
// skip free routes (e.g. `'GET /foo/bar': true`)
|
|
146
|
+
(endpoint) => endpoint !== true,
|
|
147
|
+
)
|
|
141
148
|
: null)
|
|
142
149
|
if (!matched) return new Response('Not Found', { status: 404 })
|
|
143
150
|
|