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.
Files changed (209) hide show
  1. package/CHANGELOG.md +15 -3
  2. package/README.md +13 -13
  3. package/dist/BodyDigest.d.ts.map +1 -1
  4. package/dist/BodyDigest.js.map +1 -1
  5. package/dist/Challenge.d.ts.map +1 -1
  6. package/dist/Challenge.js.map +1 -1
  7. package/dist/Credential.d.ts.map +1 -1
  8. package/dist/Credential.js.map +1 -1
  9. package/dist/Errors.js +64 -67
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/PaymentRequest.d.ts.map +1 -1
  12. package/dist/PaymentRequest.js.map +1 -1
  13. package/dist/Receipt.d.ts.map +1 -1
  14. package/dist/Receipt.js.map +1 -1
  15. package/dist/Store.d.ts +14 -4
  16. package/dist/Store.d.ts.map +1 -1
  17. package/dist/Store.js +17 -0
  18. package/dist/Store.js.map +1 -1
  19. package/dist/cli/account.d.ts.map +1 -1
  20. package/dist/cli/account.js +40 -5
  21. package/dist/cli/account.js.map +1 -1
  22. package/dist/cli/cli.d.ts.map +1 -1
  23. package/dist/cli/cli.js +24 -8
  24. package/dist/cli/cli.js.map +1 -1
  25. package/dist/cli/internal.d.ts.map +1 -1
  26. package/dist/cli/internal.js.map +1 -1
  27. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  28. package/dist/cli/plugins/stripe.js.map +1 -1
  29. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  30. package/dist/cli/plugins/tempo.js +11 -23
  31. package/dist/cli/plugins/tempo.js.map +1 -1
  32. package/dist/cli/utils.d.ts.map +1 -1
  33. package/dist/cli/utils.js.map +1 -1
  34. package/dist/client/internal/Fetch.d.ts +2 -0
  35. package/dist/client/internal/Fetch.d.ts.map +1 -1
  36. package/dist/client/internal/Fetch.js +1 -1
  37. package/dist/client/internal/Fetch.js.map +1 -1
  38. package/dist/internal/types.d.ts.map +1 -1
  39. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  40. package/dist/mcp-sdk/client/McpClient.js +1 -1
  41. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  42. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
  43. package/dist/mcp-sdk/server/Transport.js.map +1 -1
  44. package/dist/middlewares/elysia.d.ts.map +1 -1
  45. package/dist/middlewares/elysia.js +5 -1
  46. package/dist/middlewares/elysia.js.map +1 -1
  47. package/dist/middlewares/express.d.ts.map +1 -1
  48. package/dist/middlewares/express.js +5 -2
  49. package/dist/middlewares/express.js.map +1 -1
  50. package/dist/middlewares/hono.d.ts.map +1 -1
  51. package/dist/middlewares/hono.js.map +1 -1
  52. package/dist/proxy/Proxy.d.ts.map +1 -1
  53. package/dist/proxy/Proxy.js +3 -1
  54. package/dist/proxy/Proxy.js.map +1 -1
  55. package/dist/proxy/Service.js +1 -1
  56. package/dist/proxy/Service.js.map +1 -1
  57. package/dist/proxy/internal/Route.d.ts +2 -2
  58. package/dist/proxy/internal/Route.d.ts.map +1 -1
  59. package/dist/proxy/internal/Route.js +4 -2
  60. package/dist/proxy/internal/Route.js.map +1 -1
  61. package/dist/server/Mppx.d.ts.map +1 -1
  62. package/dist/server/Mppx.js +47 -11
  63. package/dist/server/Mppx.js.map +1 -1
  64. package/dist/server/Request.d.ts.map +1 -1
  65. package/dist/server/Request.js.map +1 -1
  66. package/dist/stripe/Methods.d.ts.map +1 -1
  67. package/dist/stripe/Methods.js.map +1 -1
  68. package/dist/tempo/Methods.d.ts.map +1 -1
  69. package/dist/tempo/Methods.js.map +1 -1
  70. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  71. package/dist/tempo/client/ChannelOps.js.map +1 -1
  72. package/dist/tempo/client/Charge.d.ts.map +1 -1
  73. package/dist/tempo/client/Charge.js.map +1 -1
  74. package/dist/tempo/client/Session.d.ts.map +1 -1
  75. package/dist/tempo/client/Session.js.map +1 -1
  76. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  77. package/dist/tempo/client/SessionManager.js +1 -1
  78. package/dist/tempo/client/SessionManager.js.map +1 -1
  79. package/dist/tempo/internal/address.d.ts +3 -0
  80. package/dist/tempo/internal/address.d.ts.map +1 -0
  81. package/dist/tempo/internal/address.js +4 -0
  82. package/dist/tempo/internal/address.js.map +1 -0
  83. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  84. package/dist/tempo/internal/auto-swap.js +4 -4
  85. package/dist/tempo/internal/auto-swap.js.map +1 -1
  86. package/dist/tempo/internal/fee-payer.d.ts +4 -1
  87. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  88. package/dist/tempo/internal/fee-payer.js +12 -4
  89. package/dist/tempo/internal/fee-payer.js.map +1 -1
  90. package/dist/tempo/server/Charge.d.ts +11 -0
  91. package/dist/tempo/server/Charge.d.ts.map +1 -1
  92. package/dist/tempo/server/Charge.js +110 -51
  93. package/dist/tempo/server/Charge.js.map +1 -1
  94. package/dist/tempo/server/Session.d.ts +1 -1
  95. package/dist/tempo/server/Session.d.ts.map +1 -1
  96. package/dist/tempo/server/Session.js +31 -23
  97. package/dist/tempo/server/Session.js.map +1 -1
  98. package/dist/tempo/server/internal/transport.d.ts +1 -1
  99. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  100. package/dist/tempo/server/internal/transport.js +41 -1
  101. package/dist/tempo/server/internal/transport.js.map +1 -1
  102. package/dist/tempo/session/Chain.d.ts.map +1 -1
  103. package/dist/tempo/session/Chain.js +51 -10
  104. package/dist/tempo/session/Chain.js.map +1 -1
  105. package/dist/tempo/session/ChannelStore.d.ts +2 -0
  106. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  107. package/dist/tempo/session/ChannelStore.js +4 -2
  108. package/dist/tempo/session/ChannelStore.js.map +1 -1
  109. package/dist/tempo/session/Receipt.d.ts.map +1 -1
  110. package/dist/tempo/session/Receipt.js.map +1 -1
  111. package/dist/tempo/session/Sse.d.ts.map +1 -1
  112. package/dist/tempo/session/Sse.js.map +1 -1
  113. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  114. package/dist/tempo/session/Voucher.js +3 -2
  115. package/dist/tempo/session/Voucher.js.map +1 -1
  116. package/dist/viem/Client.d.ts.map +1 -1
  117. package/dist/viem/Client.js.map +1 -1
  118. package/package.json +2 -2
  119. package/src/BodyDigest.ts +1 -0
  120. package/src/Challenge.test-d.ts +1 -0
  121. package/src/Challenge.ts +1 -0
  122. package/src/Credential.ts +1 -0
  123. package/src/Errors.test.ts +27 -39
  124. package/src/Expires.test.ts +1 -0
  125. package/src/PaymentRequest.ts +1 -0
  126. package/src/Receipt.ts +1 -0
  127. package/src/Store.test-d.ts +59 -0
  128. package/src/Store.test.ts +56 -6
  129. package/src/Store.ts +31 -4
  130. package/src/cli/account.ts +65 -30
  131. package/src/cli/cli.test.ts +127 -1
  132. package/src/cli/cli.ts +23 -8
  133. package/src/cli/config.test.ts +1 -0
  134. package/src/cli/internal.ts +1 -0
  135. package/src/cli/plugins/stripe.ts +1 -0
  136. package/src/cli/plugins/tempo.ts +21 -24
  137. package/src/cli/utils.ts +1 -0
  138. package/src/client/Mppx.test-d.ts +1 -0
  139. package/src/client/internal/Fetch.browser.test.ts +1 -0
  140. package/src/client/internal/Fetch.test-d.ts +1 -0
  141. package/src/client/internal/Fetch.test.ts +1 -0
  142. package/src/client/internal/Fetch.ts +1 -1
  143. package/src/internal/constantTimeEqual.test.ts +1 -0
  144. package/src/internal/types.ts +1 -3
  145. package/src/mcp-sdk/client/McpClient.test-d.ts +1 -0
  146. package/src/mcp-sdk/client/McpClient.test.ts +1 -0
  147. package/src/mcp-sdk/client/McpClient.ts +2 -0
  148. package/src/mcp-sdk/server/Transport.test.ts +1 -0
  149. package/src/mcp-sdk/server/Transport.ts +1 -0
  150. package/src/middlewares/elysia.test.ts +90 -0
  151. package/src/middlewares/elysia.ts +5 -1
  152. package/src/middlewares/express.test.ts +62 -2
  153. package/src/middlewares/express.ts +6 -2
  154. package/src/middlewares/hono.ts +1 -0
  155. package/src/middlewares/internal/mppx.test.ts +1 -0
  156. package/src/middlewares/nextjs.test.ts +1 -0
  157. package/src/proxy/Proxy.test.ts +57 -0
  158. package/src/proxy/Proxy.ts +8 -1
  159. package/src/proxy/Service.test.ts +1 -0
  160. package/src/proxy/Service.ts +8 -2
  161. package/src/proxy/internal/Headers.test.ts +1 -0
  162. package/src/proxy/internal/Route.test.ts +57 -0
  163. package/src/proxy/internal/Route.ts +3 -1
  164. package/src/proxy/services/openai.test.ts +1 -0
  165. package/src/server/Mppx.test.ts +438 -0
  166. package/src/server/Mppx.ts +51 -13
  167. package/src/server/Request.test.ts +1 -0
  168. package/src/server/Request.ts +1 -0
  169. package/src/server/Response.test.ts +1 -0
  170. package/src/server/Transport.test.ts +1 -0
  171. package/src/stripe/Methods.ts +1 -0
  172. package/src/stripe/client/Charge.test.ts +1 -0
  173. package/src/stripe/server/Charge.test.ts +1 -0
  174. package/src/tempo/Attribution.test.ts +1 -0
  175. package/src/tempo/Methods.ts +1 -0
  176. package/src/tempo/client/ChannelOps.test.ts +1 -0
  177. package/src/tempo/client/ChannelOps.ts +1 -0
  178. package/src/tempo/client/Charge.ts +1 -0
  179. package/src/tempo/client/Session.test.ts +1 -0
  180. package/src/tempo/client/Session.ts +1 -0
  181. package/src/tempo/client/SessionManager.test.ts +28 -0
  182. package/src/tempo/client/SessionManager.ts +2 -1
  183. package/src/tempo/internal/address.ts +6 -0
  184. package/src/tempo/internal/auto-swap.test.ts +1 -0
  185. package/src/tempo/internal/auto-swap.ts +4 -3
  186. package/src/tempo/internal/defaults.test.ts +1 -0
  187. package/src/tempo/internal/fee-payer.test.ts +1 -0
  188. package/src/tempo/internal/fee-payer.ts +19 -4
  189. package/src/tempo/server/Charge.test.ts +1081 -31
  190. package/src/tempo/server/Charge.ts +159 -63
  191. package/src/tempo/server/Session.test.ts +896 -107
  192. package/src/tempo/server/Session.ts +41 -23
  193. package/src/tempo/server/Sse.test.ts +2 -0
  194. package/src/tempo/server/internal/transport.test.ts +30 -0
  195. package/src/tempo/server/internal/transport.ts +41 -2
  196. package/src/tempo/session/Chain.test.ts +145 -0
  197. package/src/tempo/session/Chain.ts +59 -10
  198. package/src/tempo/session/Channel.test.ts +1 -0
  199. package/src/tempo/session/ChannelStore.test.ts +11 -0
  200. package/src/tempo/session/ChannelStore.ts +7 -3
  201. package/src/tempo/session/Receipt.test.ts +1 -0
  202. package/src/tempo/session/Receipt.ts +1 -0
  203. package/src/tempo/session/Sse.test.ts +2 -0
  204. package/src/tempo/session/Sse.ts +1 -0
  205. package/src/tempo/session/Voucher.test.ts +1 -0
  206. package/src/tempo/session/Voucher.ts +4 -2
  207. package/src/viem/Account.test.ts +1 -0
  208. package/src/viem/Client.test.ts +1 -0
  209. 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
- if (
136
+ const insecure =
134
137
  c.options.insecure ||
135
138
  hostname === 'localhost' ||
136
139
  hostname.endsWith('.localhost') ||
137
140
  hostname.endsWith('.local')
138
- ) {
139
- process.removeAllListeners('warning')
140
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
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 globalThis.fetch(fetchUrl, init)
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 as Record<string, string>),
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 globalThis.fetch(fetchUrl, credentialFetchInit)
325
+ const credentialResponse = await targetFetch(fetchUrl, credentialFetchInit)
311
326
 
312
327
  if (c.options.fail && credentialResponse.status >= 400)
313
328
  return c.error({
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as os from 'node:os'
3
3
  import * as path from 'node:path'
4
+
4
5
  import { defineConfig } from './config.js'
5
6
  import { loadConfig } from './internal.js'
6
7
 
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as path from 'node:path'
3
+
3
4
  import type * as Challenge from '../Challenge.js'
4
5
  import type * as Method from '../Method.js'
5
6
  import type { Config } from './config.js'
@@ -1,4 +1,5 @@
1
1
  import { Errors, z } from 'incur'
2
+
2
3
  import { stripe as stripeMethods } from '../../stripe/client/index.js'
3
4
  import { pc } from '../utils.js'
4
5
  import { createPlugin } from './plugin.js'
@@ -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({ channelId, cumulativeAmount, escrowContract, chainId }) {
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: 'voucher',
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
- let credentialResponse = response
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
- const tickAmount = BigInt(challengeRequest.amount as string)
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 as Record<string, string>),
616
+ ...normalizeHeaders(opts.fetchInit.headers),
620
617
  Authorization: closeCred,
621
618
  },
622
619
  })
package/src/cli/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as readline from 'node:readline'
2
+
2
3
  import type { Chain } from 'viem'
3
4
  import { type Address, createClient, http } from 'viem'
4
5
  import { tempo as tempoMainnet, tempoModerato } from 'viem/chains'
@@ -1,5 +1,6 @@
1
1
  import type { Account } from 'viem'
2
2
  import { describe, expectTypeOf, test } from 'vitest'
3
+
3
4
  import * as Method from '../Method.js'
4
5
  import { charge } from '../tempo/client/Charge.js'
5
6
  import { tempo } from '../tempo/client/Methods.js'
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test, vi } from 'vitest'
2
+
2
3
  import * as Fetch from './Fetch.js'
3
4
 
4
5
  const noopMethod = {
@@ -1,5 +1,6 @@
1
1
  import type { Account } from 'viem'
2
2
  import { describe, expectTypeOf, test } from 'vitest'
3
+
3
4
  import { charge } from '../../tempo/client/Charge.js'
4
5
  import * as Fetch from './Fetch.js'
5
6
 
@@ -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> = {}
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest'
2
+
2
3
  import { constantTimeEqual } from './constantTimeEqual.js'
3
4
 
4
5
  describe('constantTimeEqual', () => {
@@ -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
 
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest'
2
+
2
3
  import type { Challenge } from '../../Challenge.js'
3
4
  import type { Credential } from '../../Credential.js'
4
5
  import { VerificationFailedError } from '../../Errors.js'
@@ -1,4 +1,5 @@
1
1
  import type { CallToolResult, McpError } from '@modelcontextprotocol/sdk/types.js'
2
+
2
3
  import type * as Credential from '../../Credential.js'
3
4
  import * as core_Mcp from '../../Mcp.js'
4
5
  import * as Transport from '../../server/Transport.js'
@@ -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 result = await intent(options)(Request.fromNodeListener(req, res))
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
@@ -1,4 +1,5 @@
1
1
  import type { MiddlewareHandler } from 'hono'
2
+
2
3
  import * as Mppx_core from '../server/Mppx.js'
3
4
  import * as Mppx_internal from './internal/mppx.js'
4
5
 
@@ -1,6 +1,7 @@
1
1
  import { Challenge, Credential, Method, z } from 'mppx'
2
2
  import { Mppx } from 'mppx/server'
3
3
  import { describe, expect, test } from 'vitest'
4
+
4
5
  import { wrap } from './mppx.js'
5
6
 
6
7
  const realm = 'api.example.com'
@@ -1,4 +1,5 @@
1
1
  import * as http from 'node:http'
2
+
2
3
  import { Receipt } from 'mppx'
3
4
  import { Mppx as Mppx_client, session as sessionIntent, tempo as tempo_client } from 'mppx/client'
4
5
  import { Mppx } from 'mppx/nextjs'
@@ -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({
@@ -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(service.routes, upstreamPath)
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
 
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest'
2
+
2
3
  import * as Service from './Service.js'
3
4
 
4
5
  describe('from', () => {