mppx 0.4.9 → 0.4.11

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 (165) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +155 -0
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/discovery/Discovery.d.ts +146 -0
  6. package/dist/discovery/Discovery.d.ts.map +1 -0
  7. package/dist/discovery/Discovery.js +60 -0
  8. package/dist/discovery/Discovery.js.map +1 -0
  9. package/dist/discovery/OpenApi.d.ts +61 -0
  10. package/dist/discovery/OpenApi.d.ts.map +1 -0
  11. package/dist/discovery/OpenApi.js +139 -0
  12. package/dist/discovery/OpenApi.js.map +1 -0
  13. package/dist/discovery/Validate.d.ts +10 -0
  14. package/dist/discovery/Validate.d.ts.map +1 -0
  15. package/dist/discovery/Validate.js +63 -0
  16. package/dist/discovery/Validate.js.map +1 -0
  17. package/dist/discovery/index.d.ts +4 -0
  18. package/dist/discovery/index.d.ts.map +1 -0
  19. package/dist/discovery/index.js +4 -0
  20. package/dist/discovery/index.js.map +1 -0
  21. package/dist/middlewares/elysia.d.ts +52 -1
  22. package/dist/middlewares/elysia.d.ts.map +1 -1
  23. package/dist/middlewares/elysia.js +17 -0
  24. package/dist/middlewares/elysia.js.map +1 -1
  25. package/dist/middlewares/express.d.ts +13 -1
  26. package/dist/middlewares/express.d.ts.map +1 -1
  27. package/dist/middlewares/express.js +18 -0
  28. package/dist/middlewares/express.js.map +1 -1
  29. package/dist/middlewares/hono.d.ts +19 -1
  30. package/dist/middlewares/hono.d.ts.map +1 -1
  31. package/dist/middlewares/hono.js +51 -0
  32. package/dist/middlewares/hono.js.map +1 -1
  33. package/dist/middlewares/internal/mppx.d.ts +4 -2
  34. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  35. package/dist/middlewares/internal/mppx.js +10 -3
  36. package/dist/middlewares/internal/mppx.js.map +1 -1
  37. package/dist/middlewares/nextjs.d.ts +11 -0
  38. package/dist/middlewares/nextjs.d.ts.map +1 -1
  39. package/dist/middlewares/nextjs.js +15 -0
  40. package/dist/middlewares/nextjs.js.map +1 -1
  41. package/dist/proxy/Proxy.d.ts +6 -0
  42. package/dist/proxy/Proxy.d.ts.map +1 -1
  43. package/dist/proxy/Proxy.js +56 -80
  44. package/dist/proxy/Proxy.js.map +1 -1
  45. package/dist/proxy/Service.d.ts +16 -23
  46. package/dist/proxy/Service.d.ts.map +1 -1
  47. package/dist/proxy/Service.js +19 -83
  48. package/dist/proxy/Service.js.map +1 -1
  49. package/dist/proxy/internal/Route.js +1 -1
  50. package/dist/proxy/internal/Route.js.map +1 -1
  51. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  52. package/dist/proxy/services/anthropic.js +5 -0
  53. package/dist/proxy/services/anthropic.js.map +1 -1
  54. package/dist/proxy/services/openai.d.ts.map +1 -1
  55. package/dist/proxy/services/openai.js +6 -3
  56. package/dist/proxy/services/openai.js.map +1 -1
  57. package/dist/proxy/services/stripe.d.ts.map +1 -1
  58. package/dist/proxy/services/stripe.js +6 -3
  59. package/dist/proxy/services/stripe.js.map +1 -1
  60. package/dist/stripe/internal/types.d.ts +3 -0
  61. package/dist/stripe/internal/types.d.ts.map +1 -1
  62. package/dist/stripe/server/Charge.d.ts.map +1 -1
  63. package/dist/stripe/server/Charge.js +9 -2
  64. package/dist/stripe/server/Charge.js.map +1 -1
  65. package/dist/tempo/server/Session.d.ts.map +1 -1
  66. package/dist/tempo/server/Session.js +25 -8
  67. package/dist/tempo/server/Session.js.map +1 -1
  68. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  69. package/dist/tempo/server/internal/transport.js +8 -0
  70. package/dist/tempo/server/internal/transport.js.map +1 -1
  71. package/dist/tempo/session/Chain.js +1 -1
  72. package/dist/tempo/session/Chain.js.map +1 -1
  73. package/package.json +6 -1
  74. package/src/BodyDigest.test.ts +1 -1
  75. package/src/Challenge.fuzz.test.ts +121 -0
  76. package/src/Challenge.test-d.ts +1 -1
  77. package/src/Challenge.test.ts +1 -1
  78. package/src/Credential.fuzz.test.ts +62 -0
  79. package/src/Credential.test.ts +1 -1
  80. package/src/Errors.test.ts +1 -1
  81. package/src/Expires.test.ts +1 -1
  82. package/src/Method.test.ts +1 -1
  83. package/src/PaymentRequest.test.ts +1 -1
  84. package/src/Receipt.test.ts +1 -1
  85. package/src/Store.test-d.ts +1 -1
  86. package/src/Store.test.ts +1 -1
  87. package/src/cli/cli.test.ts +212 -1
  88. package/src/cli/cli.ts +162 -0
  89. package/src/client/Mppx.test-d.ts +1 -1
  90. package/src/client/Mppx.test.ts +1 -1
  91. package/src/client/Transport.test.ts +1 -1
  92. package/src/client/internal/Fetch.browser.test.ts +1 -1
  93. package/src/client/internal/Fetch.test-d.ts +1 -1
  94. package/src/client/internal/Fetch.test.ts +2 -1
  95. package/src/discovery/Discovery.test.ts +152 -0
  96. package/src/discovery/Discovery.ts +72 -0
  97. package/src/discovery/OpenApi.test.ts +425 -0
  98. package/src/discovery/OpenApi.ts +224 -0
  99. package/src/discovery/Validate.test.ts +188 -0
  100. package/src/discovery/Validate.ts +76 -0
  101. package/src/discovery/index.ts +3 -0
  102. package/src/internal/constantTimeEqual.test.ts +1 -1
  103. package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
  104. package/src/mcp-sdk/client/McpClient.test.ts +1 -1
  105. package/src/mcp-sdk/server/Transport.test.ts +1 -1
  106. package/src/middlewares/elysia.test.ts +27 -2
  107. package/src/middlewares/elysia.ts +35 -1
  108. package/src/middlewares/express.test.ts +35 -7
  109. package/src/middlewares/express.ts +34 -0
  110. package/src/middlewares/hono.test.ts +28 -6
  111. package/src/middlewares/hono.ts +73 -1
  112. package/src/middlewares/internal/mppx.test.ts +1 -1
  113. package/src/middlewares/internal/mppx.ts +14 -6
  114. package/src/middlewares/nextjs.test.ts +31 -6
  115. package/src/middlewares/nextjs.ts +28 -0
  116. package/src/proxy/Proxy.test.ts +54 -270
  117. package/src/proxy/Proxy.ts +71 -93
  118. package/src/proxy/Service.test.ts +23 -1
  119. package/src/proxy/Service.ts +40 -86
  120. package/src/proxy/internal/Headers.test.ts +1 -1
  121. package/src/proxy/internal/Route.test.ts +9 -1
  122. package/src/proxy/internal/Route.ts +1 -1
  123. package/src/proxy/services/anthropic.test.ts +132 -0
  124. package/src/proxy/services/anthropic.ts +5 -0
  125. package/src/proxy/services/openai.test.ts +1 -1
  126. package/src/proxy/services/openai.ts +6 -4
  127. package/src/proxy/services/stripe.test.ts +132 -0
  128. package/src/proxy/services/stripe.ts +6 -4
  129. package/src/server/Mppx.test-d.ts +1 -1
  130. package/src/server/Mppx.test.ts +2 -1
  131. package/src/server/NodeListener.test.ts +1 -1
  132. package/src/server/Request.test.ts +1 -1
  133. package/src/server/Response.test.ts +1 -1
  134. package/src/server/Transport.test.ts +1 -1
  135. package/src/stripe/Charge.integration.test.ts +1 -1
  136. package/src/stripe/Methods.test.ts +1 -1
  137. package/src/stripe/client/Charge.test.ts +1 -1
  138. package/src/stripe/internal/types.ts +5 -1
  139. package/src/stripe/server/Charge.test.ts +53 -2
  140. package/src/stripe/server/Charge.ts +12 -4
  141. package/src/tempo/Attribution.test.ts +1 -1
  142. package/src/tempo/Methods.test.ts +1 -1
  143. package/src/tempo/client/ChannelOps.test.ts +6 -3
  144. package/src/tempo/client/Session.test.ts +5 -2
  145. package/src/tempo/client/SessionManager.test.ts +1 -1
  146. package/src/tempo/internal/auto-swap.test.ts +1 -1
  147. package/src/tempo/internal/defaults.test.ts +1 -1
  148. package/src/tempo/internal/fee-payer.test.ts +1 -1
  149. package/src/tempo/server/Charge.test.ts +1 -1
  150. package/src/tempo/server/Session.test.ts +116 -37
  151. package/src/tempo/server/Session.ts +32 -11
  152. package/src/tempo/server/Sse.test.ts +1 -1
  153. package/src/tempo/server/internal/transport.test.ts +24 -1
  154. package/src/tempo/server/internal/transport.ts +11 -0
  155. package/src/tempo/session/Chain.test.ts +5 -2
  156. package/src/tempo/session/Chain.ts +1 -1
  157. package/src/tempo/session/Channel.test.ts +1 -1
  158. package/src/tempo/session/ChannelStore.test.ts +1 -1
  159. package/src/tempo/session/Receipt.test.ts +1 -1
  160. package/src/tempo/session/Sse.fuzz.test.ts +138 -0
  161. package/src/tempo/session/Sse.test.ts +1 -1
  162. package/src/tempo/session/Voucher.test.ts +1 -1
  163. package/src/viem/Account.test.ts +1 -1
  164. package/src/viem/Client.test.ts +1 -1
  165. package/src/zod.test.ts +147 -0
@@ -4,12 +4,14 @@ import { Value } from 'ox'
4
4
  export type Service = {
5
5
  /** Base URL of the upstream service (e.g. `'https://api.openai.com'`). */
6
6
  baseUrl: string
7
+ /** Free-form service categories for discovery metadata. */
8
+ categories?: string[] | undefined
7
9
  /** Short description of the service. */
8
10
  description?: string | undefined
11
+ /** Structured service documentation links for discovery metadata. */
12
+ docs?: Docs | undefined
9
13
  /** Unique identifier used as the URL prefix (e.g. `'openai'` → `/{id}/...`). */
10
14
  id: string
11
- /** Returns a documentation URL. Called with no argument for the service root, or with a route pattern for per-endpoint docs. */
12
- docsLlmsUrl?: ((options: { route?: string | undefined }) => string | undefined) | undefined
13
15
  /** Hook to modify the upstream request before sending (e.g. inject auth headers). */
14
16
  rewriteRequest?: ((req: Request, ctx: Context) => Request | Promise<Request>) | undefined
15
17
  /** Hook to modify the upstream response before returning to the client. */
@@ -20,6 +22,12 @@ export type Service = {
20
22
  title?: string | undefined
21
23
  }
22
24
 
25
+ export type Docs = {
26
+ apiReference?: string | undefined
27
+ homepage?: string | undefined
28
+ llms?: string | undefined
29
+ }
30
+
23
31
  /**
24
32
  * An endpoint definition.
25
33
  *
@@ -80,9 +88,10 @@ export function from<options = unknown>(id: string, config: from.Config<options>
80
88
  const rewriteFromConfig = resolveRewriteRequest(config)
81
89
  return {
82
90
  baseUrl: config.baseUrl,
91
+ categories: config.categories,
83
92
  description: config.description,
93
+ docs: resolveDocs(config),
84
94
  id,
85
- docsLlmsUrl: resolveLlmsUrl(config.docsLlmsUrl),
86
95
  routes: config.routes,
87
96
  title: config.title,
88
97
  rewriteRequest: config.rewriteRequest
@@ -102,8 +111,12 @@ export declare namespace from {
102
111
  baseUrl: string
103
112
  /** Shorthand: inject `Authorization: Bearer {token}` header. */
104
113
  bearer?: string | undefined
114
+ /** Free-form service categories for discovery metadata. */
115
+ categories?: string[] | undefined
105
116
  /** Short description of the service. */
106
117
  description?: string | undefined
118
+ /** Structured service documentation links for discovery metadata. */
119
+ docs?: Docs | undefined
107
120
  /** Shorthand: inject custom headers. */
108
121
  headers?: Record<string, string> | undefined
109
122
  /** Documentation URL for the service. String for a static base URL, or a function receiving an optional endpoint pattern. */
@@ -157,32 +170,14 @@ function resolveRewriteRequest(
157
170
  return undefined
158
171
  }
159
172
 
160
- /** Serializes a service for discovery responses. */
161
- export function serialize(s: Service) {
162
- return {
163
- description: s.description,
164
- id: s.id,
165
- docsLlmsUrl: s.docsLlmsUrl?.({}),
166
- routes: Object.entries(s.routes).map(([pattern, endpoint]) => {
167
- const tokens = pattern.trim().split(/\s+/)
168
- const hasMethod = tokens.length >= 2
169
- const path = hasMethod ? tokens.slice(1).join(' ') : tokens[0]
170
- return {
171
- docsLlmsUrl: s.docsLlmsUrl?.({ route: pattern }),
172
- method: hasMethod ? tokens[0] : undefined,
173
- path: `/${s.id}${path}`,
174
- pattern: hasMethod ? `${tokens[0]} /${s.id}${path}` : `/${s.id}${path}`,
175
- payment: endpoint ? resolvePayment(endpoint) : null,
176
- }
177
- }),
178
- title: s.title,
179
- }
180
- }
181
-
182
173
  /** Renders an llms.txt markdown string for a list of services. */
183
174
  export function toLlmsTxt(
184
175
  services: Service[],
185
- options?: { title?: string | undefined; description?: string | undefined },
176
+ options?: {
177
+ description?: string | undefined
178
+ openApiPath?: string | undefined
179
+ title?: string | undefined
180
+ },
186
181
  ): string {
187
182
  const lines: string[] = [
188
183
  `# ${options?.title ?? 'API Proxy'}`,
@@ -197,65 +192,13 @@ export function toLlmsTxt(
197
192
  for (const s of services) {
198
193
  const label = s.title ?? s.id
199
194
  const desc = s.description ? `: ${s.description}` : ''
200
- lines.push(`- [${label}](/discover/${s.id}.md)${desc}`)
195
+ lines.push(`- ${label}${desc}`)
201
196
  }
202
- lines.push('', '[See all service definitions](/discover/all.md)')
197
+ lines.push('', `[OpenAPI discovery](${options?.openApiPath ?? '/openapi.json'})`)
203
198
 
204
199
  return lines.join('\n')
205
200
  }
206
201
 
207
- /** Renders a full markdown listing of all services with their routes. */
208
- export function toServicesMarkdown(services: Service[]): string {
209
- const lines: string[] = ['# Services', '']
210
-
211
- if (services.length === 0) return lines.join('\n')
212
-
213
- for (const s of services) {
214
- lines.push(`## [${s.title ?? s.id}](/discover/${s.id}.md)`, '')
215
- if (s.description) lines.push(s.description, '')
216
- pushRoutes(lines, s)
217
- }
218
-
219
- return lines.join('\n')
220
- }
221
-
222
- /** Renders a markdown string for a single service. */
223
- export function toMarkdown(s: Service): string {
224
- const docsLlmsUrl = s.docsLlmsUrl?.({})
225
- const lines: string[] = [`# ${s.title ?? s.id}`, '']
226
- if (docsLlmsUrl) lines.push(`> Documentation: ${docsLlmsUrl}`, '')
227
- if (s.description) lines.push(s.description, '')
228
- pushRoutes(lines, s, '##')
229
- return lines.join('\n')
230
- }
231
-
232
- function pushRoutes(lines: string[], s: Service, heading: '##' | '###' = '###') {
233
- lines.push(`${heading} Routes`, '')
234
- const serialized = serialize(s)
235
- for (const route of serialized.routes) {
236
- const p = route.payment as Record<string, unknown> | null
237
- const desc = p?.description ? `: ${p.description}` : ''
238
- lines.push(`- \`${route.pattern}\`${desc}`)
239
- if (!p) {
240
- lines.push(' - Type: free')
241
- } else {
242
- lines.push(` - Type: ${p.intent}`)
243
- if (p.amount) {
244
- const perUnit = p.unitType ? `/${p.unitType}` : ''
245
- if (p.decimals !== undefined) {
246
- const price = Number(p.amount) / 10 ** Number(p.decimals)
247
- lines.push(` - Price: ${price}${perUnit} (${p.amount} units, ${p.decimals} decimals)`)
248
- } else {
249
- lines.push(` - Units: ${p.amount}${perUnit}`)
250
- }
251
- }
252
- if (p.currency) lines.push(` - Currency: ${p.currency}`)
253
- }
254
- if (route.docsLlmsUrl) lines.push(` - Docs: ${route.docsLlmsUrl}`)
255
- lines.push('')
256
- }
257
- }
258
-
259
202
  /** Extracts per-endpoint options from an endpoint definition. */
260
203
  export function getOptions(endpoint: Endpoint): EndpointOptions | undefined {
261
204
  if (typeof endpoint === 'object' && endpoint !== null && 'options' in endpoint)
@@ -263,10 +206,10 @@ export function getOptions(endpoint: Endpoint): EndpointOptions | undefined {
263
206
  return undefined
264
207
  }
265
208
 
266
- function resolvePayment(endpoint: Endpoint): Record<string, unknown> | null {
209
+ export function paymentOf(endpoint: Endpoint): Record<string, unknown> | null {
267
210
  if (endpoint === true) return null
268
211
  const handler = typeof endpoint === 'function' ? endpoint : endpoint.pay
269
- if (!('_internal' in handler)) return {}
212
+ if (!('_internal' in handler)) return null
270
213
  const {
271
214
  name,
272
215
  intent,
@@ -283,10 +226,21 @@ function resolvePayment(endpoint: Endpoint): Record<string, unknown> | null {
283
226
  return { intent, method: name, ...rest, ...(amount !== undefined && { amount }) }
284
227
  }
285
228
 
286
- function resolveLlmsUrl(
229
+ function resolveDocs(config: from.Config): Docs | undefined {
230
+ if (config.docs) {
231
+ return {
232
+ ...config.docs,
233
+ ...(config.docs.llms ? {} : { llms: resolveLlmsFromLegacy(config.docsLlmsUrl) }),
234
+ }
235
+ }
236
+ const llms = resolveLlmsFromLegacy(config.docsLlmsUrl)
237
+ return llms ? { llms } : undefined
238
+ }
239
+
240
+ function resolveLlmsFromLegacy(
287
241
  input: string | ((options: { route?: string | undefined }) => string | undefined) | undefined,
288
- ): Service['docsLlmsUrl'] {
242
+ ): string | undefined {
289
243
  if (!input) return undefined
290
- if (typeof input === 'function') return input
291
- return ({ route }) => (route ? undefined : input)
244
+ if (typeof input === 'string') return input
245
+ return input({}) ?? undefined
292
246
  }
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from 'vitest'
1
+ import { describe, expect, test } from 'vp/test'
2
2
 
3
3
  import * as Headers from './Headers.js'
4
4
 
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from 'vitest'
1
+ import { describe, expect, test } from 'vp/test'
2
2
 
3
3
  import * as Route from './Route.js'
4
4
 
@@ -24,6 +24,14 @@ describe('pathname', () => {
24
24
  Route.pathname(new URL('http://localhost/other/openai/v1/models'), '/api/proxy'),
25
25
  ).toBeNull()
26
26
  })
27
+
28
+ test('error: returns null for basePath prefix collision', () => {
29
+ expect(Route.pathname(new URL('http://localhost/proxy2/openai/v1/models'), '/proxy')).toBeNull()
30
+ })
31
+
32
+ test('behavior: returns empty string when pathname equals basePath', () => {
33
+ expect(Route.pathname(new URL('http://localhost/proxy'), '/proxy')).toBe('')
34
+ })
27
35
  })
28
36
 
29
37
  describe('parse', () => {
@@ -5,7 +5,7 @@ export function pathname(url: URL, basePath?: string): string | null {
5
5
  let pathname = url.pathname
6
6
  if (basePath) {
7
7
  const base = basePath.replace(/\/+$/, '')
8
- if (!pathname.startsWith(base)) return null
8
+ if (!(pathname === base || pathname.startsWith(`${base}/`))) return null
9
9
  pathname = pathname.slice(base.length)
10
10
  }
11
11
  return pathname
@@ -0,0 +1,132 @@
1
+ import { Receipt } from 'mppx'
2
+ import { Mppx as Mppx_client, tempo as tempo_client } from 'mppx/client'
3
+ import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
+ import { afterEach, describe, expect, test } from 'vp/test'
5
+ import * as Http from '~test/Http.js'
6
+ import { accounts, asset, client } from '~test/tempo/viem.js'
7
+
8
+ import * as ApiProxy from '../Proxy.js'
9
+ import { anthropic } from './anthropic.js'
10
+
11
+ const apiKey = 'sk-ant-test-fake-anthropic-key'
12
+ const secretKey = 'test-secret-key'
13
+
14
+ const mppx_server = Mppx_server.create({
15
+ methods: [
16
+ tempo_server({
17
+ account: accounts[0],
18
+ currency: asset,
19
+ getClient: () => client,
20
+ }),
21
+ ],
22
+ secretKey,
23
+ })
24
+
25
+ const mppx_client = Mppx_client.create({
26
+ polyfill: false,
27
+ methods: [
28
+ tempo_client({
29
+ account: accounts[1],
30
+ getClient: () => client,
31
+ }),
32
+ ],
33
+ })
34
+
35
+ let proxyServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
36
+ let upstreamServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
37
+
38
+ afterEach(() => {
39
+ proxyServer?.close()
40
+ upstreamServer?.close()
41
+ })
42
+
43
+ describe('anthropic', () => {
44
+ test('behavior: proxies POST /v1/messages with charge and injects x-api-key', async () => {
45
+ upstreamServer = await Http.createServer((req, res) => {
46
+ res.writeHead(200, { 'Content-Type': 'application/json' })
47
+ res.end(
48
+ JSON.stringify({
49
+ headers: {
50
+ 'x-api-key': req.headers['x-api-key'],
51
+ },
52
+ }),
53
+ )
54
+ })
55
+
56
+ const proxy = ApiProxy.create({
57
+ services: [
58
+ anthropic({
59
+ apiKey,
60
+ baseUrl: upstreamServer.url,
61
+ routes: {
62
+ 'POST /v1/messages': mppx_server.charge({ amount: '1', decimals: 6 }),
63
+ },
64
+ }),
65
+ ],
66
+ })
67
+ proxyServer = await Http.createServer(proxy.listener)
68
+
69
+ const res = await mppx_client.fetch(`${proxyServer.url}/anthropic/v1/messages`, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({ model: 'claude-3-opus-20240229', max_tokens: 1, messages: [] }),
73
+ })
74
+ expect(res.status).toBe(200)
75
+
76
+ const body = (await res.json()) as { headers: { 'x-api-key': string } }
77
+ expect(body.headers['x-api-key']).toBe(apiKey)
78
+
79
+ const receipt = Receipt.fromResponse(res)
80
+ expect(receipt.status).toBe('success')
81
+ expect(receipt.method).toBe('tempo')
82
+ })
83
+
84
+ test('behavior: returns 402 without credential', async () => {
85
+ upstreamServer = await Http.createServer((_req, res) => {
86
+ res.writeHead(200, { 'Content-Type': 'application/json' })
87
+ res.end('{}')
88
+ })
89
+
90
+ const proxy = ApiProxy.create({
91
+ services: [
92
+ anthropic({
93
+ apiKey,
94
+ baseUrl: upstreamServer.url,
95
+ routes: {
96
+ 'POST /v1/messages': mppx_server.charge({ amount: '1', decimals: 6 }),
97
+ },
98
+ }),
99
+ ],
100
+ })
101
+ proxyServer = await Http.createServer(proxy.listener)
102
+
103
+ const res = await fetch(`${proxyServer.url}/anthropic/v1/messages`, {
104
+ method: 'POST',
105
+ })
106
+ expect(res.status).toBe(402)
107
+ expect(res.headers.get('WWW-Authenticate')).toContain('Payment')
108
+ })
109
+
110
+ test('behavior: returns 404 for unmatched route', async () => {
111
+ upstreamServer = await Http.createServer((_req, res) => {
112
+ res.writeHead(200, { 'Content-Type': 'application/json' })
113
+ res.end('{}')
114
+ })
115
+
116
+ const proxy = ApiProxy.create({
117
+ services: [
118
+ anthropic({
119
+ apiKey,
120
+ baseUrl: upstreamServer.url,
121
+ routes: {
122
+ 'POST /v1/messages': mppx_server.charge({ amount: '1', decimals: 6 }),
123
+ },
124
+ }),
125
+ ],
126
+ })
127
+ proxyServer = await Http.createServer(proxy.listener)
128
+
129
+ const res = await fetch(`${proxyServer.url}/anthropic/v1/unknown`)
130
+ expect(res.status).toBe(404)
131
+ })
132
+ })
@@ -20,7 +20,12 @@ import * as Service from '../Service.js'
20
20
  export function anthropic(config: anthropic.Config) {
21
21
  return Service.from<anthropic.Config>('anthropic', {
22
22
  baseUrl: config.baseUrl ?? 'https://api.anthropic.com',
23
+ categories: ['ai'],
23
24
  description: 'Claude language models for messages and completions.',
25
+ docs: {
26
+ apiReference: 'https://docs.anthropic.com/en/api/getting-started',
27
+ homepage: 'https://docs.anthropic.com/en/docs/intro-to-claude',
28
+ },
24
29
  rewriteRequest(request, ctx) {
25
30
  const apiKey = ctx.apiKey ?? config.apiKey
26
31
  request.headers.set('x-api-key', apiKey)
@@ -1,7 +1,7 @@
1
1
  import { Receipt } from 'mppx'
2
2
  import { Mppx as Mppx_client, tempo as tempo_client } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
- import { afterEach, describe, expect, test } from 'vitest'
4
+ import { afterEach, describe, expect, test } from 'vp/test'
5
5
  import * as Http from '~test/Http.js'
6
6
  import { accounts, asset, client } from '~test/tempo/viem.js'
7
7
 
@@ -20,11 +20,13 @@ import * as Service from '../Service.js'
20
20
  export function openai(config: openai.Config) {
21
21
  return Service.from<openai.Config>('openai', {
22
22
  baseUrl: config.baseUrl ?? 'https://api.openai.com',
23
+ categories: ['ai'],
23
24
  description: 'Chat completions, embeddings, image generation, and audio transcription.',
24
- docsLlmsUrl: ({ route }) =>
25
- route
26
- ? `https://context7.com/websites/platform_openai/llms.txt?topic=${encodeURIComponent(route)}`
27
- : 'https://context7.com/websites/platform_openai/llms.txt',
25
+ docs: {
26
+ apiReference: 'https://platform.openai.com/docs/api-reference',
27
+ homepage: 'https://platform.openai.com/docs',
28
+ llms: 'https://context7.com/websites/platform_openai/llms.txt',
29
+ },
28
30
  rewriteRequest(request, ctx) {
29
31
  const apiKey = ctx.apiKey ?? config.apiKey
30
32
  request.headers.set('Authorization', `Bearer ${apiKey}`)
@@ -0,0 +1,132 @@
1
+ import { Receipt } from 'mppx'
2
+ import { Mppx as Mppx_client, tempo as tempo_client } from 'mppx/client'
3
+ import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
+ import { afterEach, describe, expect, test } from 'vp/test'
5
+ import * as Http from '~test/Http.js'
6
+ import { accounts, asset, client } from '~test/tempo/viem.js'
7
+
8
+ import * as ApiProxy from '../Proxy.js'
9
+ import { stripe } from './stripe.js'
10
+
11
+ const apiKey = 'sk_test_fake_stripe_key'
12
+ const secretKey = 'test-secret-key'
13
+
14
+ const mppx_server = Mppx_server.create({
15
+ methods: [
16
+ tempo_server({
17
+ account: accounts[0],
18
+ currency: asset,
19
+ getClient: () => client,
20
+ }),
21
+ ],
22
+ secretKey,
23
+ })
24
+
25
+ const mppx_client = Mppx_client.create({
26
+ polyfill: false,
27
+ methods: [
28
+ tempo_client({
29
+ account: accounts[1],
30
+ getClient: () => client,
31
+ }),
32
+ ],
33
+ })
34
+
35
+ let proxyServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
36
+ let upstreamServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
37
+
38
+ afterEach(() => {
39
+ proxyServer?.close()
40
+ upstreamServer?.close()
41
+ })
42
+
43
+ describe('stripe', () => {
44
+ test('behavior: proxies POST /v1/charges with charge and injects Basic auth', async () => {
45
+ upstreamServer = await Http.createServer((req, res) => {
46
+ res.writeHead(200, { 'Content-Type': 'application/json' })
47
+ res.end(
48
+ JSON.stringify({
49
+ headers: {
50
+ authorization: req.headers.authorization,
51
+ },
52
+ }),
53
+ )
54
+ })
55
+
56
+ const proxy = ApiProxy.create({
57
+ services: [
58
+ stripe({
59
+ apiKey,
60
+ baseUrl: upstreamServer.url,
61
+ routes: {
62
+ 'POST /v1/charges': mppx_server.charge({ amount: '1', decimals: 6 }),
63
+ },
64
+ }),
65
+ ],
66
+ })
67
+ proxyServer = await Http.createServer(proxy.listener)
68
+
69
+ const res = await mppx_client.fetch(`${proxyServer.url}/stripe/v1/charges`, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
72
+ body: 'amount=100&currency=usd',
73
+ })
74
+ expect(res.status).toBe(200)
75
+
76
+ const body = (await res.json()) as { headers: { authorization: string } }
77
+ expect(body.headers.authorization).toBe(`Basic ${btoa(`${apiKey}:`)}`)
78
+
79
+ const receipt = Receipt.fromResponse(res)
80
+ expect(receipt.status).toBe('success')
81
+ expect(receipt.method).toBe('tempo')
82
+ })
83
+
84
+ test('behavior: returns 402 without credential', async () => {
85
+ upstreamServer = await Http.createServer((_req, res) => {
86
+ res.writeHead(200, { 'Content-Type': 'application/json' })
87
+ res.end('{}')
88
+ })
89
+
90
+ const proxy = ApiProxy.create({
91
+ services: [
92
+ stripe({
93
+ apiKey,
94
+ baseUrl: upstreamServer.url,
95
+ routes: {
96
+ 'POST /v1/charges': mppx_server.charge({ amount: '1', decimals: 6 }),
97
+ },
98
+ }),
99
+ ],
100
+ })
101
+ proxyServer = await Http.createServer(proxy.listener)
102
+
103
+ const res = await fetch(`${proxyServer.url}/stripe/v1/charges`, {
104
+ method: 'POST',
105
+ })
106
+ expect(res.status).toBe(402)
107
+ expect(res.headers.get('WWW-Authenticate')).toContain('Payment')
108
+ })
109
+
110
+ test('behavior: returns 404 for unmatched route', async () => {
111
+ upstreamServer = await Http.createServer((_req, res) => {
112
+ res.writeHead(200, { 'Content-Type': 'application/json' })
113
+ res.end('{}')
114
+ })
115
+
116
+ const proxy = ApiProxy.create({
117
+ services: [
118
+ stripe({
119
+ apiKey,
120
+ baseUrl: upstreamServer.url,
121
+ routes: {
122
+ 'POST /v1/charges': mppx_server.charge({ amount: '1', decimals: 6 }),
123
+ },
124
+ }),
125
+ ],
126
+ })
127
+ proxyServer = await Http.createServer(proxy.listener)
128
+
129
+ const res = await fetch(`${proxyServer.url}/stripe/v1/unknown`)
130
+ expect(res.status).toBe(404)
131
+ })
132
+ })
@@ -20,11 +20,13 @@ import * as Service from '../Service.js'
20
20
  export function stripe(config: stripe.Config) {
21
21
  return Service.from<stripe.Config>('stripe', {
22
22
  baseUrl: config.baseUrl ?? 'https://api.stripe.com',
23
+ categories: ['payments'],
23
24
  description: 'Payment processing, customers, subscriptions, and invoices.',
24
- docsLlmsUrl: ({ route }) =>
25
- route
26
- ? `https://context7.com/websites/stripe/llms.txt?topic=${encodeURIComponent(route)}`
27
- : 'https://docs.stripe.com/llms.txt',
25
+ docs: {
26
+ apiReference: 'https://docs.stripe.com/api',
27
+ homepage: 'https://docs.stripe.com',
28
+ llms: 'https://docs.stripe.com/llms.txt',
29
+ },
28
30
  rewriteRequest(request, ctx) {
29
31
  const apiKey = ctx.apiKey ?? config.apiKey
30
32
  request.headers.set('Authorization', `Basic ${btoa(`${apiKey}:`)}`)
@@ -1,6 +1,6 @@
1
1
  import { Method, z } from 'mppx'
2
2
  import { Mppx } from 'mppx/server'
3
- import { assertType, describe, expectTypeOf, test } from 'vitest'
3
+ import { assertType, describe, expectTypeOf, test } from 'vp/test'
4
4
 
5
5
  const mockChargeA = Method.from({
6
6
  name: 'alpha',
@@ -1,6 +1,6 @@
1
1
  import { Challenge, Credential, Method, z } from 'mppx'
2
2
  import { Mppx, Transport, tempo } from 'mppx/server'
3
- import { describe, expect, test } from 'vitest'
3
+ import { describe, expect, test } from 'vp/test'
4
4
  import * as Http from '~test/Http.js'
5
5
  import { accounts, asset, client } from '~test/tempo/viem.js'
6
6
 
@@ -9,6 +9,7 @@ const secretKey = 'test-secret-key'
9
9
 
10
10
  const method = tempo({
11
11
  getClient: () => client,
12
+ account: accounts[0],
12
13
  })
13
14
 
14
15
  describe('create', () => {
@@ -1,5 +1,5 @@
1
1
  import { NodeListener, Request } from 'mppx/server'
2
- import { afterEach, describe, expect, test } from 'vitest'
2
+ import { afterEach, describe, expect, test } from 'vp/test'
3
3
  import * as Http from '~test/Http.js'
4
4
 
5
5
  let server: Awaited<ReturnType<typeof Http.createServer>> | undefined
@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events'
2
2
  import type { IncomingMessage, ServerResponse } from 'node:http'
3
3
 
4
4
  import { Request } from 'mppx/server'
5
- import { describe, expect, test } from 'vitest'
5
+ import { describe, expect, test } from 'vp/test'
6
6
 
7
7
  function createMockRequest(options: {
8
8
  method?: string
@@ -1,6 +1,6 @@
1
1
  import { Challenge } from 'mppx'
2
2
  import { Response } from 'mppx/server'
3
- import { describe, expect, test } from 'vitest'
3
+ import { describe, expect, test } from 'vp/test'
4
4
 
5
5
  import * as Errors from '../Errors.js'
6
6
 
@@ -1,7 +1,7 @@
1
1
  import { Challenge, Credential, Mcp, Receipt } from 'mppx'
2
2
  import { Transport } from 'mppx/server'
3
3
  import { Methods } from 'mppx/tempo'
4
- import { describe, expect, test } from 'vitest'
4
+ import { describe, expect, test } from 'vp/test'
5
5
 
6
6
  import { BadRequestError, ChannelClosedError } from '../Errors.js'
7
7
 
@@ -1,7 +1,7 @@
1
1
  import { Challenge, Credential, Receipt } from 'mppx'
2
2
  import { Mppx as Mppx_client, stripe as stripe_client } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, stripe as stripe_server } from 'mppx/server'
4
- import { afterEach, describe, expect, test } from 'vitest'
4
+ import { afterEach, describe, expect, test } from 'vp/test'
5
5
  import * as Http from '~test/Http.js'
6
6
 
7
7
  const stripeSecretKey = process.env.VITE_STRIPE_SECRET_KEY
@@ -1,5 +1,5 @@
1
1
  import { Methods } from 'mppx/stripe'
2
- import { describe, expect, expectTypeOf, test } from 'vitest'
2
+ import { describe, expect, expectTypeOf, test } from 'vp/test'
3
3
 
4
4
  describe('charge', () => {
5
5
  test('has correct name and intent', () => {
@@ -1,7 +1,7 @@
1
1
  import { Challenge, Credential } from 'mppx'
2
2
  import { Mppx, stripe } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, stripe as stripe_server } from 'mppx/server'
4
- import { describe, expect, test, vi } from 'vitest'
4
+ import { describe, expect, test, vi } from 'vp/test'
5
5
 
6
6
  import type { StripeJs } from '../internal/types.js'
7
7
  import { charge as clientCharge_ } from './Charge.js'
@@ -6,7 +6,11 @@
6
6
  */
7
7
  export type StripeClient = {
8
8
  paymentIntents: {
9
- create(...args: any[]): Promise<{ id: string; status: string }>
9
+ create(...args: any[]): Promise<{
10
+ id: string
11
+ status: string
12
+ lastResponse?: { headers?: Record<string, string> }
13
+ }>
10
14
  }
11
15
  }
12
16