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.
- package/CHANGELOG.md +25 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +155 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/discovery/Discovery.d.ts +146 -0
- package/dist/discovery/Discovery.d.ts.map +1 -0
- package/dist/discovery/Discovery.js +60 -0
- package/dist/discovery/Discovery.js.map +1 -0
- package/dist/discovery/OpenApi.d.ts +61 -0
- package/dist/discovery/OpenApi.d.ts.map +1 -0
- package/dist/discovery/OpenApi.js +139 -0
- package/dist/discovery/OpenApi.js.map +1 -0
- package/dist/discovery/Validate.d.ts +10 -0
- package/dist/discovery/Validate.d.ts.map +1 -0
- package/dist/discovery/Validate.js +63 -0
- package/dist/discovery/Validate.js.map +1 -0
- package/dist/discovery/index.d.ts +4 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +4 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/middlewares/elysia.d.ts +52 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +17 -0
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts +13 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +18 -0
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts +19 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js +51 -0
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts +4 -2
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +10 -3
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/middlewares/nextjs.d.ts +11 -0
- package/dist/middlewares/nextjs.d.ts.map +1 -1
- package/dist/middlewares/nextjs.js +15 -0
- package/dist/middlewares/nextjs.js.map +1 -1
- package/dist/proxy/Proxy.d.ts +6 -0
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +56 -80
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.d.ts +16 -23
- package/dist/proxy/Service.d.ts.map +1 -1
- package/dist/proxy/Service.js +19 -83
- package/dist/proxy/Service.js.map +1 -1
- package/dist/proxy/internal/Route.js +1 -1
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/proxy/services/anthropic.d.ts.map +1 -1
- package/dist/proxy/services/anthropic.js +5 -0
- package/dist/proxy/services/anthropic.js.map +1 -1
- package/dist/proxy/services/openai.d.ts.map +1 -1
- package/dist/proxy/services/openai.js +6 -3
- package/dist/proxy/services/openai.js.map +1 -1
- package/dist/proxy/services/stripe.d.ts.map +1 -1
- package/dist/proxy/services/stripe.js +6 -3
- package/dist/proxy/services/stripe.js.map +1 -1
- package/dist/stripe/internal/types.d.ts +3 -0
- package/dist/stripe/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +9 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +25 -8
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +8 -0
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.js +1 -1
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +6 -1
- package/src/BodyDigest.test.ts +1 -1
- package/src/Challenge.fuzz.test.ts +121 -0
- package/src/Challenge.test-d.ts +1 -1
- package/src/Challenge.test.ts +1 -1
- package/src/Credential.fuzz.test.ts +62 -0
- package/src/Credential.test.ts +1 -1
- package/src/Errors.test.ts +1 -1
- package/src/Expires.test.ts +1 -1
- package/src/Method.test.ts +1 -1
- package/src/PaymentRequest.test.ts +1 -1
- package/src/Receipt.test.ts +1 -1
- package/src/Store.test-d.ts +1 -1
- package/src/Store.test.ts +1 -1
- package/src/cli/cli.test.ts +212 -1
- package/src/cli/cli.ts +162 -0
- package/src/client/Mppx.test-d.ts +1 -1
- package/src/client/Mppx.test.ts +1 -1
- package/src/client/Transport.test.ts +1 -1
- package/src/client/internal/Fetch.browser.test.ts +1 -1
- package/src/client/internal/Fetch.test-d.ts +1 -1
- package/src/client/internal/Fetch.test.ts +2 -1
- package/src/discovery/Discovery.test.ts +152 -0
- package/src/discovery/Discovery.ts +72 -0
- package/src/discovery/OpenApi.test.ts +425 -0
- package/src/discovery/OpenApi.ts +224 -0
- package/src/discovery/Validate.test.ts +188 -0
- package/src/discovery/Validate.ts +76 -0
- package/src/discovery/index.ts +3 -0
- package/src/internal/constantTimeEqual.test.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test.ts +1 -1
- package/src/mcp-sdk/server/Transport.test.ts +1 -1
- package/src/middlewares/elysia.test.ts +27 -2
- package/src/middlewares/elysia.ts +35 -1
- package/src/middlewares/express.test.ts +35 -7
- package/src/middlewares/express.ts +34 -0
- package/src/middlewares/hono.test.ts +28 -6
- package/src/middlewares/hono.ts +73 -1
- package/src/middlewares/internal/mppx.test.ts +1 -1
- package/src/middlewares/internal/mppx.ts +14 -6
- package/src/middlewares/nextjs.test.ts +31 -6
- package/src/middlewares/nextjs.ts +28 -0
- package/src/proxy/Proxy.test.ts +54 -270
- package/src/proxy/Proxy.ts +71 -93
- package/src/proxy/Service.test.ts +23 -1
- package/src/proxy/Service.ts +40 -86
- package/src/proxy/internal/Headers.test.ts +1 -1
- package/src/proxy/internal/Route.test.ts +9 -1
- package/src/proxy/internal/Route.ts +1 -1
- package/src/proxy/services/anthropic.test.ts +132 -0
- package/src/proxy/services/anthropic.ts +5 -0
- package/src/proxy/services/openai.test.ts +1 -1
- package/src/proxy/services/openai.ts +6 -4
- package/src/proxy/services/stripe.test.ts +132 -0
- package/src/proxy/services/stripe.ts +6 -4
- package/src/server/Mppx.test-d.ts +1 -1
- package/src/server/Mppx.test.ts +2 -1
- package/src/server/NodeListener.test.ts +1 -1
- package/src/server/Request.test.ts +1 -1
- package/src/server/Response.test.ts +1 -1
- package/src/server/Transport.test.ts +1 -1
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/Methods.test.ts +1 -1
- package/src/stripe/client/Charge.test.ts +1 -1
- package/src/stripe/internal/types.ts +5 -1
- package/src/stripe/server/Charge.test.ts +53 -2
- package/src/stripe/server/Charge.ts +12 -4
- package/src/tempo/Attribution.test.ts +1 -1
- package/src/tempo/Methods.test.ts +1 -1
- package/src/tempo/client/ChannelOps.test.ts +6 -3
- package/src/tempo/client/Session.test.ts +5 -2
- package/src/tempo/client/SessionManager.test.ts +1 -1
- package/src/tempo/internal/auto-swap.test.ts +1 -1
- package/src/tempo/internal/defaults.test.ts +1 -1
- package/src/tempo/internal/fee-payer.test.ts +1 -1
- package/src/tempo/server/Charge.test.ts +1 -1
- package/src/tempo/server/Session.test.ts +116 -37
- package/src/tempo/server/Session.ts +32 -11
- package/src/tempo/server/Sse.test.ts +1 -1
- package/src/tempo/server/internal/transport.test.ts +24 -1
- package/src/tempo/server/internal/transport.ts +11 -0
- package/src/tempo/session/Chain.test.ts +5 -2
- package/src/tempo/session/Chain.ts +1 -1
- package/src/tempo/session/Channel.test.ts +1 -1
- package/src/tempo/session/ChannelStore.test.ts +1 -1
- package/src/tempo/session/Receipt.test.ts +1 -1
- package/src/tempo/session/Sse.fuzz.test.ts +138 -0
- package/src/tempo/session/Sse.test.ts +1 -1
- package/src/tempo/session/Voucher.test.ts +1 -1
- package/src/viem/Account.test.ts +1 -1
- package/src/viem/Client.test.ts +1 -1
- package/src/zod.test.ts +147 -0
package/src/proxy/Service.ts
CHANGED
|
@@ -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?: {
|
|
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(`-
|
|
195
|
+
lines.push(`- ${label}${desc}`)
|
|
201
196
|
}
|
|
202
|
-
lines.push('',
|
|
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
|
|
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
|
|
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
|
-
):
|
|
242
|
+
): string | undefined {
|
|
289
243
|
if (!input) return undefined
|
|
290
|
-
if (typeof input === '
|
|
291
|
-
return ({
|
|
244
|
+
if (typeof input === 'string') return input
|
|
245
|
+
return input({}) ?? undefined
|
|
292
246
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, test } from '
|
|
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 '
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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¤cy=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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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}:`)}`)
|
package/src/server/Mppx.test.ts
CHANGED
|
@@ -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 '
|
|
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 '
|
|
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 '
|
|
5
|
+
import { describe, expect, test } from 'vp/test'
|
|
6
6
|
|
|
7
7
|
function createMockRequest(options: {
|
|
8
8
|
method?: string
|
|
@@ -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 '
|
|
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 '
|
|
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,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 '
|
|
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<{
|
|
9
|
+
create(...args: any[]): Promise<{
|
|
10
|
+
id: string
|
|
11
|
+
status: string
|
|
12
|
+
lastResponse?: { headers?: Record<string, string> }
|
|
13
|
+
}>
|
|
10
14
|
}
|
|
11
15
|
}
|
|
12
16
|
|