mppx 0.4.8 → 0.4.10
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 +26 -3
- package/README.md +13 -13
- package/dist/BodyDigest.d.ts.map +1 -1
- package/dist/BodyDigest.js.map +1 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js.map +1 -1
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js.map +1 -1
- package/dist/Errors.js +64 -67
- package/dist/Errors.js.map +1 -1
- package/dist/PaymentRequest.d.ts.map +1 -1
- package/dist/PaymentRequest.js.map +1 -1
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js.map +1 -1
- package/dist/Store.d.ts +9 -0
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +17 -0
- package/dist/Store.js.map +1 -1
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +40 -5
- package/dist/cli/account.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +157 -1
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/internal.d.ts.map +1 -1
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +2 -1
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +1 -1
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/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/internal/types.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +1 -1
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- package/dist/middlewares/elysia.d.ts +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 +23 -2
- 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 +20 -84
- 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/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +35 -17
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Request.d.ts.map +1 -1
- package/dist/server/Request.js.map +1 -1
- package/dist/stripe/Methods.d.ts.map +1 -1
- package/dist/stripe/Methods.js.map +1 -1
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +1 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
- package/dist/tempo/internal/auto-swap.js +1 -1
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +1 -1
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +18 -5
- 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.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +1 -1
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Receipt.d.ts.map +1 -1
- package/dist/tempo/session/Receipt.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +6 -1
- package/src/BodyDigest.test.ts +1 -1
- package/src/BodyDigest.ts +1 -0
- package/src/Challenge.fuzz.test.ts +121 -0
- package/src/Challenge.test-d.ts +2 -1
- package/src/Challenge.test.ts +1 -1
- package/src/Challenge.ts +1 -0
- package/src/Credential.fuzz.test.ts +62 -0
- package/src/Credential.test.ts +1 -1
- package/src/Credential.ts +1 -0
- package/src/Errors.test.ts +28 -40
- package/src/Expires.test.ts +2 -1
- package/src/Method.test.ts +1 -1
- package/src/PaymentRequest.test.ts +1 -1
- package/src/PaymentRequest.ts +1 -0
- package/src/Receipt.test.ts +1 -1
- package/src/Receipt.ts +1 -0
- package/src/Store.test-d.ts +2 -1
- package/src/Store.test.ts +57 -7
- package/src/Store.ts +25 -0
- package/src/cli/account.ts +65 -30
- package/src/cli/cli.test.ts +215 -2
- package/src/cli/cli.ts +166 -1
- package/src/cli/config.test.ts +1 -0
- package/src/cli/internal.ts +1 -0
- package/src/cli/plugins/stripe.ts +1 -0
- package/src/cli/plugins/tempo.ts +4 -1
- package/src/cli/utils.ts +1 -0
- package/src/client/Mppx.test-d.ts +2 -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 +2 -1
- package/src/client/internal/Fetch.test-d.ts +2 -1
- package/src/client/internal/Fetch.test.ts +3 -1
- package/src/client/internal/Fetch.ts +1 -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 +2 -1
- package/src/internal/types.ts +1 -3
- package/src/mcp-sdk/client/McpClient.test-d.ts +2 -1
- package/src/mcp-sdk/client/McpClient.test.ts +2 -1
- package/src/mcp-sdk/client/McpClient.ts +2 -0
- package/src/mcp-sdk/server/Transport.test.ts +2 -1
- package/src/mcp-sdk/server/Transport.ts +1 -0
- package/src/middlewares/elysia.test.ts +28 -2
- package/src/middlewares/elysia.ts +36 -1
- package/src/middlewares/express.test.ts +95 -7
- package/src/middlewares/express.ts +40 -2
- package/src/middlewares/hono.test.ts +28 -6
- package/src/middlewares/hono.ts +74 -1
- package/src/middlewares/internal/mppx.test.ts +2 -1
- package/src/middlewares/internal/mppx.ts +14 -6
- package/src/middlewares/nextjs.test.ts +32 -6
- package/src/middlewares/nextjs.ts +28 -0
- package/src/proxy/Proxy.test.ts +55 -270
- package/src/proxy/Proxy.ts +73 -93
- package/src/proxy/Service.test.ts +24 -1
- package/src/proxy/Service.ts +48 -88
- package/src/proxy/internal/Headers.test.ts +2 -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 +2 -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 +194 -1
- package/src/server/Mppx.ts +38 -19
- package/src/server/NodeListener.test.ts +1 -1
- package/src/server/Request.test.ts +2 -1
- package/src/server/Request.ts +1 -0
- package/src/server/Response.test.ts +2 -1
- package/src/server/Transport.test.ts +2 -1
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/Methods.test.ts +1 -1
- package/src/stripe/Methods.ts +1 -0
- package/src/stripe/client/Charge.test.ts +2 -1
- package/src/stripe/server/Charge.test.ts +2 -1
- package/src/tempo/Attribution.test.ts +2 -1
- package/src/tempo/Methods.test.ts +1 -1
- package/src/tempo/Methods.ts +1 -0
- package/src/tempo/client/ChannelOps.test.ts +7 -3
- package/src/tempo/client/ChannelOps.ts +1 -0
- package/src/tempo/client/Charge.ts +1 -0
- package/src/tempo/client/Session.test.ts +6 -2
- package/src/tempo/client/Session.ts +1 -0
- package/src/tempo/client/SessionManager.test.ts +29 -1
- package/src/tempo/client/SessionManager.ts +2 -1
- package/src/tempo/internal/auto-swap.test.ts +2 -1
- package/src/tempo/internal/auto-swap.ts +1 -0
- package/src/tempo/internal/defaults.test.ts +2 -1
- package/src/tempo/internal/fee-payer.test.ts +2 -1
- package/src/tempo/internal/fee-payer.ts +1 -0
- package/src/tempo/server/Charge.test.ts +2 -1
- package/src/tempo/server/Charge.ts +1 -0
- package/src/tempo/server/Session.test.ts +88 -37
- package/src/tempo/server/Session.ts +26 -8
- package/src/tempo/server/Sse.test.ts +2 -1
- package/src/tempo/server/internal/transport.test.ts +25 -1
- package/src/tempo/server/internal/transport.ts +11 -0
- package/src/tempo/session/Chain.test.ts +6 -2
- package/src/tempo/session/Chain.ts +2 -1
- package/src/tempo/session/Channel.test.ts +2 -1
- package/src/tempo/session/ChannelStore.test.ts +2 -1
- package/src/tempo/session/ChannelStore.ts +1 -0
- package/src/tempo/session/Receipt.test.ts +2 -1
- package/src/tempo/session/Receipt.ts +1 -0
- package/src/tempo/session/Sse.fuzz.test.ts +138 -0
- package/src/tempo/session/Sse.test.ts +2 -1
- package/src/tempo/session/Sse.ts +1 -0
- package/src/tempo/session/Voucher.test.ts +2 -1
- package/src/tempo/session/Voucher.ts +1 -0
- package/src/viem/Account.test.ts +2 -1
- package/src/viem/Client.test.ts +2 -1
- package/src/viem/Client.ts +1 -0
- package/src/zod.test.ts +147 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { validate } from './Validate.js'
|
|
2
|
+
|
|
3
|
+
function makeDoc(overrides: Record<string, unknown> = {}) {
|
|
4
|
+
return {
|
|
5
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
6
|
+
openapi: '3.1.0',
|
|
7
|
+
paths: {
|
|
8
|
+
'/search': {
|
|
9
|
+
post: {
|
|
10
|
+
'x-payment-info': {
|
|
11
|
+
amount: '100',
|
|
12
|
+
intent: 'charge',
|
|
13
|
+
method: 'tempo',
|
|
14
|
+
},
|
|
15
|
+
requestBody: {
|
|
16
|
+
content: { 'application/json': { schema: { type: 'object' } } },
|
|
17
|
+
},
|
|
18
|
+
responses: {
|
|
19
|
+
'200': { description: 'OK' },
|
|
20
|
+
'402': { description: 'Payment Required' },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
...overrides,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('validate', () => {
|
|
30
|
+
test('returns no errors for a valid document', () => {
|
|
31
|
+
const errors = validate(makeDoc())
|
|
32
|
+
expect(errors.filter((error) => error.severity === 'error')).toHaveLength(0)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('returns error for missing 402 response', () => {
|
|
36
|
+
const errors = validate(
|
|
37
|
+
makeDoc({
|
|
38
|
+
paths: {
|
|
39
|
+
'/search': {
|
|
40
|
+
post: {
|
|
41
|
+
'x-payment-info': {
|
|
42
|
+
amount: '100',
|
|
43
|
+
intent: 'charge',
|
|
44
|
+
method: 'tempo',
|
|
45
|
+
},
|
|
46
|
+
requestBody: {},
|
|
47
|
+
responses: {
|
|
48
|
+
'200': { description: 'OK' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
expect(errors.find((error) => error.severity === 'error')?.message).toContain('402')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('returns warning for missing requestBody', () => {
|
|
60
|
+
const errors = validate(
|
|
61
|
+
makeDoc({
|
|
62
|
+
paths: {
|
|
63
|
+
'/search': {
|
|
64
|
+
post: {
|
|
65
|
+
'x-payment-info': {
|
|
66
|
+
amount: '100',
|
|
67
|
+
intent: 'charge',
|
|
68
|
+
method: 'tempo',
|
|
69
|
+
},
|
|
70
|
+
responses: {
|
|
71
|
+
'200': { description: 'OK' },
|
|
72
|
+
'402': { description: 'Payment Required' },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
expect(errors.find((error) => error.severity === 'warning')?.message).toContain('requestBody')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('returns structural errors for invalid top-level document', () => {
|
|
84
|
+
const errors = validate({ openapi: '3.1.0' })
|
|
85
|
+
expect(errors.length).toBeGreaterThan(0)
|
|
86
|
+
expect(errors[0]!.severity).toBe('error')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('returns errors for invalid extension values', () => {
|
|
90
|
+
const errors = validate(
|
|
91
|
+
makeDoc({
|
|
92
|
+
'x-service-info': {
|
|
93
|
+
docs: { homepage: 'not-a-uri' },
|
|
94
|
+
},
|
|
95
|
+
paths: {
|
|
96
|
+
'/search': {
|
|
97
|
+
post: {
|
|
98
|
+
'x-payment-info': {
|
|
99
|
+
amount: '01',
|
|
100
|
+
intent: 'subscribe',
|
|
101
|
+
method: 'tempo',
|
|
102
|
+
},
|
|
103
|
+
responses: {
|
|
104
|
+
'402': { description: 'Payment Required' },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
expect(errors.some((error) => error.severity === 'error')).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('ignores path-item-level fields like summary and parameters', () => {
|
|
116
|
+
const errors = validate(
|
|
117
|
+
makeDoc({
|
|
118
|
+
paths: {
|
|
119
|
+
'/search': {
|
|
120
|
+
summary: 'Search endpoints',
|
|
121
|
+
parameters: [{ name: 'q', in: 'query' }],
|
|
122
|
+
'x-custom': 'value',
|
|
123
|
+
post: {
|
|
124
|
+
'x-payment-info': {
|
|
125
|
+
amount: '100',
|
|
126
|
+
intent: 'charge',
|
|
127
|
+
method: 'tempo',
|
|
128
|
+
},
|
|
129
|
+
requestBody: {
|
|
130
|
+
content: { 'application/json': { schema: { type: 'object' } } },
|
|
131
|
+
},
|
|
132
|
+
responses: {
|
|
133
|
+
'200': { description: 'OK' },
|
|
134
|
+
'402': { description: 'Payment Required' },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
expect(errors.filter((e) => e.severity === 'error')).toHaveLength(0)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('validates proxy-generated docs with relative llms path', () => {
|
|
146
|
+
const errors = validate({
|
|
147
|
+
info: { title: 'API Proxy', version: '1.0.0' },
|
|
148
|
+
openapi: '3.1.0',
|
|
149
|
+
paths: {},
|
|
150
|
+
'x-service-info': {
|
|
151
|
+
categories: ['gateway'],
|
|
152
|
+
docs: {
|
|
153
|
+
apiReference: 'https://example.com/api',
|
|
154
|
+
homepage: 'https://example.com',
|
|
155
|
+
llms: '/llms.txt',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
expect(errors.filter((e) => e.severity === 'error')).toHaveLength(0)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('accepts x-payment-info with unknown fields', () => {
|
|
164
|
+
const errors = validate({
|
|
165
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
166
|
+
openapi: '3.1.0',
|
|
167
|
+
paths: {
|
|
168
|
+
'/api/call': {
|
|
169
|
+
post: {
|
|
170
|
+
'x-payment-info': {
|
|
171
|
+
price: '0.54',
|
|
172
|
+
pricingMode: 'fixed',
|
|
173
|
+
protocols: ['x402', 'mpp'],
|
|
174
|
+
},
|
|
175
|
+
requestBody: {
|
|
176
|
+
content: { 'application/json': { schema: { type: 'object' } } },
|
|
177
|
+
},
|
|
178
|
+
responses: {
|
|
179
|
+
'200': { description: 'OK' },
|
|
180
|
+
'402': { description: 'Payment Required' },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
expect(errors.filter((e) => e.severity === 'error')).toHaveLength(0)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DiscoveryDocument, PaymentInfo } from './Discovery.js'
|
|
2
|
+
|
|
3
|
+
export type ValidationError = {
|
|
4
|
+
message: string
|
|
5
|
+
path: string
|
|
6
|
+
severity: 'error' | 'warning'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates a discovery document structurally and semantically.
|
|
11
|
+
*/
|
|
12
|
+
export function validate(doc: unknown): ValidationError[] {
|
|
13
|
+
const errors: ValidationError[] = []
|
|
14
|
+
|
|
15
|
+
const result = DiscoveryDocument.safeParse(doc)
|
|
16
|
+
if (!result.success) {
|
|
17
|
+
for (const issue of result.error.issues) {
|
|
18
|
+
errors.push({
|
|
19
|
+
message: issue.message,
|
|
20
|
+
path: issue.path.map(String).join('.') || '(root)',
|
|
21
|
+
severity: 'error',
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
return errors
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const parsed = result.data
|
|
28
|
+
const paths = parsed.paths
|
|
29
|
+
if (!paths) return errors
|
|
30
|
+
|
|
31
|
+
for (const [pathKey, pathItem] of Object.entries(paths)) {
|
|
32
|
+
for (const [method, operation] of Object.entries(pathItem as Record<string, unknown>)) {
|
|
33
|
+
if (!operation || typeof operation !== 'object' || Array.isArray(operation)) continue
|
|
34
|
+
const op = operation as Record<string, unknown>
|
|
35
|
+
|
|
36
|
+
const opPath = `paths.${pathKey}.${method}`
|
|
37
|
+
const rawPaymentInfo = op['x-payment-info']
|
|
38
|
+
if (!rawPaymentInfo) continue
|
|
39
|
+
|
|
40
|
+
const paymentResult = PaymentInfo.safeParse(rawPaymentInfo)
|
|
41
|
+
if (!paymentResult.success) {
|
|
42
|
+
for (const issue of paymentResult.error.issues) {
|
|
43
|
+
errors.push({
|
|
44
|
+
message: issue.message,
|
|
45
|
+
path: `${opPath}.x-payment-info.${issue.path.map(String).join('.')}`,
|
|
46
|
+
severity: 'error',
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const responses = op.responses as Record<string, unknown> | undefined
|
|
53
|
+
if (!responses || !('402' in responses)) {
|
|
54
|
+
errors.push({
|
|
55
|
+
message: 'Operation with x-payment-info MUST have a 402 response',
|
|
56
|
+
path: `${opPath}.responses`,
|
|
57
|
+
severity: 'error',
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const methodUpper = method.toUpperCase()
|
|
62
|
+
if (
|
|
63
|
+
!op.requestBody &&
|
|
64
|
+
(methodUpper === 'POST' || methodUpper === 'PUT' || methodUpper === 'PATCH')
|
|
65
|
+
) {
|
|
66
|
+
errors.push({
|
|
67
|
+
message: 'Operation with x-payment-info has no requestBody',
|
|
68
|
+
path: opPath,
|
|
69
|
+
severity: 'warning',
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return errors
|
|
76
|
+
}
|
package/src/internal/types.ts
CHANGED
|
@@ -256,9 +256,7 @@ export type LastInUnion<U> =
|
|
|
256
256
|
|
|
257
257
|
/** @internal */
|
|
258
258
|
export type UnionToIntersection<union> = (
|
|
259
|
-
union extends unknown
|
|
260
|
-
? (arg: union) => 0
|
|
261
|
-
: never
|
|
259
|
+
union extends unknown ? (arg: union) => 0 : never
|
|
262
260
|
) extends (arg: infer i) => 0
|
|
263
261
|
? i
|
|
264
262
|
: never
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
2
2
|
import { tempo } from 'mppx/client'
|
|
3
3
|
import type { Account } from 'viem'
|
|
4
|
-
import { describe, expectTypeOf, test } from '
|
|
4
|
+
import { describe, expectTypeOf, test } from 'vp/test'
|
|
5
|
+
|
|
5
6
|
import * as McpClient from './McpClient.js'
|
|
6
7
|
|
|
7
8
|
describe('McpClient.wrap', () => {
|
|
@@ -6,8 +6,9 @@ import { Challenge, Mcp as core_Mcp } from 'mppx'
|
|
|
6
6
|
import { tempo as tempo_client } from 'mppx/client'
|
|
7
7
|
import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
8
8
|
import { createClient } from 'viem'
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, test } from '
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vp/test'
|
|
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
|
-
import { describe, expect, test } from '
|
|
1
|
+
import { describe, expect, test } from 'vp/test'
|
|
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,10 +1,11 @@
|
|
|
1
1
|
import * as http from 'node:http'
|
|
2
|
+
|
|
2
3
|
import { Elysia } from 'elysia'
|
|
3
4
|
import { Receipt } from 'mppx'
|
|
4
5
|
import { Mppx as Mppx_client, tempo as tempo_client } from 'mppx/client'
|
|
5
|
-
import { Mppx } from 'mppx/elysia'
|
|
6
|
+
import { Mppx, discovery } from 'mppx/elysia'
|
|
6
7
|
import { tempo as tempo_server } from 'mppx/server'
|
|
7
|
-
import { describe, expect, test } from '
|
|
8
|
+
import { describe, expect, test } from 'vp/test'
|
|
8
9
|
import { accounts, asset, client } from '~test/tempo/viem.js'
|
|
9
10
|
|
|
10
11
|
function createServer(app: Elysia<any, any, any, any, any, any, any>) {
|
|
@@ -86,4 +87,29 @@ describe('charge', () => {
|
|
|
86
87
|
|
|
87
88
|
server.close()
|
|
88
89
|
})
|
|
90
|
+
|
|
91
|
+
test('serves /openapi.json from discovery plugin', async () => {
|
|
92
|
+
const app = new Elysia().use(
|
|
93
|
+
discovery(mppx, {
|
|
94
|
+
info: { title: 'Elysia API', version: '1.0.0' },
|
|
95
|
+
routes: [{ handler: mppx.charge({ amount: '1' }), method: 'get', path: '/' }],
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const server = await createServer(app)
|
|
100
|
+
const response = await globalThis.fetch(`${server.url}/openapi.json`)
|
|
101
|
+
expect(response.status).toBe(200)
|
|
102
|
+
expect(response.headers.get('cache-control')).toBe('public, max-age=300')
|
|
103
|
+
|
|
104
|
+
const body = (await response.json()) as Record<string, any>
|
|
105
|
+
expect(body.info).toEqual({ title: 'Elysia API', version: '1.0.0' })
|
|
106
|
+
expect(body.paths['/'].get['x-payment-info']).toMatchObject({
|
|
107
|
+
amount: '1000000',
|
|
108
|
+
currency: asset,
|
|
109
|
+
intent: 'charge',
|
|
110
|
+
method: 'tempo',
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
server.close()
|
|
114
|
+
})
|
|
89
115
|
})
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Elysia, type Context } from 'elysia'
|
|
2
|
+
|
|
3
|
+
import { generate, type GenerateConfig, type RouteConfig } from '../discovery/OpenApi.js'
|
|
2
4
|
import * as Mppx_core from '../server/Mppx.js'
|
|
3
5
|
import * as Mppx_internal from './internal/mppx.js'
|
|
4
6
|
|
|
@@ -67,3 +69,36 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
|
|
|
67
69
|
if (header) set.headers['Payment-Receipt'] = header
|
|
68
70
|
}
|
|
69
71
|
}
|
|
72
|
+
|
|
73
|
+
export type DiscoveryConfig = Omit<GenerateConfig, 'routes'> & {
|
|
74
|
+
path?: string
|
|
75
|
+
routes?: RouteConfig[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const discoveryHeaders = { 'Cache-Control': 'public, max-age=300' }
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns an Elysia plugin that serves an OpenAPI discovery document.
|
|
82
|
+
*/
|
|
83
|
+
export function discovery(
|
|
84
|
+
mppx: { methods: readonly Mppx_internal.AnyServer[]; realm: string },
|
|
85
|
+
config: DiscoveryConfig = {},
|
|
86
|
+
) {
|
|
87
|
+
const mountPath = config.path ?? '/openapi.json'
|
|
88
|
+
|
|
89
|
+
const cached = JSON.stringify(
|
|
90
|
+
generate(mppx, {
|
|
91
|
+
...(config.info ? { info: config.info } : {}),
|
|
92
|
+
routes: config.routes ?? [],
|
|
93
|
+
...(config.serviceInfo ? { serviceInfo: config.serviceInfo } : {}),
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return new Elysia().get(
|
|
98
|
+
mountPath,
|
|
99
|
+
() =>
|
|
100
|
+
new Response(cached, {
|
|
101
|
+
headers: { ...discoveryHeaders, 'Content-Type': 'application/json' },
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
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, discovery, 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
|
-
import { beforeAll, describe, expect, test } from '
|
|
8
|
+
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
9
9
|
import { deployEscrow } from '~test/tempo/session.js'
|
|
10
10
|
import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
|
|
11
11
|
|
|
@@ -29,7 +29,7 @@ describe('charge', () => {
|
|
|
29
29
|
tempo_server({
|
|
30
30
|
getClient: () => client,
|
|
31
31
|
currency: asset,
|
|
32
|
-
|
|
32
|
+
account: accounts[0],
|
|
33
33
|
}),
|
|
34
34
|
],
|
|
35
35
|
secretKey,
|
|
@@ -81,6 +81,34 @@ describe('charge', () => {
|
|
|
81
81
|
|
|
82
82
|
server.close()
|
|
83
83
|
})
|
|
84
|
+
|
|
85
|
+
test('serves /openapi.json from a handler-derived route config', async () => {
|
|
86
|
+
const app = express()
|
|
87
|
+
const pay = mppx.charge({ amount: '1' })
|
|
88
|
+
app.get('/', pay, (_req, res) => {
|
|
89
|
+
res.json({ fortune: 'You will be rich' })
|
|
90
|
+
})
|
|
91
|
+
discovery(app, mppx, {
|
|
92
|
+
info: { title: 'Express API', version: '1.2.3' },
|
|
93
|
+
routes: [{ handler: pay, method: 'get', path: '/' }],
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const server = await createServer(app)
|
|
97
|
+
const response = await globalThis.fetch(`${server.url}/openapi.json`)
|
|
98
|
+
expect(response.status).toBe(200)
|
|
99
|
+
expect(response.headers.get('cache-control')).toBe('public, max-age=300')
|
|
100
|
+
|
|
101
|
+
const body = (await response.json()) as Record<string, any>
|
|
102
|
+
expect(body.info).toEqual({ title: 'Express API', version: '1.2.3' })
|
|
103
|
+
expect(body.paths['/'].get['x-payment-info']).toMatchObject({
|
|
104
|
+
amount: '1000000',
|
|
105
|
+
currency: asset,
|
|
106
|
+
intent: 'charge',
|
|
107
|
+
method: 'tempo',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
server.close()
|
|
111
|
+
})
|
|
84
112
|
})
|
|
85
113
|
|
|
86
114
|
describe('session', () => {
|
|
@@ -97,7 +125,7 @@ describe('session', () => {
|
|
|
97
125
|
methods: [
|
|
98
126
|
tempo_server.session({
|
|
99
127
|
getClient: () => client,
|
|
100
|
-
|
|
128
|
+
account: accounts[0],
|
|
101
129
|
currency: asset,
|
|
102
130
|
escrowContract,
|
|
103
131
|
}),
|
|
@@ -123,10 +151,10 @@ describe('session', () => {
|
|
|
123
151
|
methods: [
|
|
124
152
|
tempo_server.session({
|
|
125
153
|
getClient: () => client,
|
|
126
|
-
|
|
154
|
+
account: accounts[0],
|
|
127
155
|
currency: asset,
|
|
128
156
|
escrowContract,
|
|
129
|
-
feePayer:
|
|
157
|
+
feePayer: true,
|
|
130
158
|
}),
|
|
131
159
|
],
|
|
132
160
|
secretKey,
|
|
@@ -158,3 +186,63 @@ describe('session', () => {
|
|
|
158
186
|
server.close()
|
|
159
187
|
})
|
|
160
188
|
})
|
|
189
|
+
|
|
190
|
+
describe('payment', () => {
|
|
191
|
+
const mppx = Mppx_server.create({
|
|
192
|
+
methods: [
|
|
193
|
+
tempo_server({
|
|
194
|
+
getClient: () => client,
|
|
195
|
+
currency: asset,
|
|
196
|
+
account: accounts[0],
|
|
197
|
+
}),
|
|
198
|
+
],
|
|
199
|
+
secretKey,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const { fetch } = Mppx_client.create({
|
|
203
|
+
polyfill: false,
|
|
204
|
+
methods: [
|
|
205
|
+
tempo_client({
|
|
206
|
+
account: accounts[1],
|
|
207
|
+
getClient: () => client,
|
|
208
|
+
}),
|
|
209
|
+
],
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('returns 402 when no credential', async () => {
|
|
213
|
+
const app = express()
|
|
214
|
+
app.get('/', payment(mppx.charge, { amount: '1' }), (_req, res) => {
|
|
215
|
+
res.json({ fortune: 'You will be rich' })
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const server = await createServer(app)
|
|
219
|
+
const response = await globalThis.fetch(server.url)
|
|
220
|
+
expect(response.status).toBe(402)
|
|
221
|
+
expect(response.headers.get('WWW-Authenticate')).toContain('Payment')
|
|
222
|
+
|
|
223
|
+
server.close()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('returns 200 with receipt on valid payment', async () => {
|
|
227
|
+
const app = express()
|
|
228
|
+
app.get('/', payment(mppx.charge, { amount: '1' }), (_req, res) => {
|
|
229
|
+
res.json({ fortune: 'You will be rich' })
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const server = await createServer(app)
|
|
233
|
+
const response = await fetch(server.url)
|
|
234
|
+
expect(response.status).toBe(200)
|
|
235
|
+
|
|
236
|
+
const body = await response.json()
|
|
237
|
+
expect(body).toEqual({ fortune: 'You will be rich' })
|
|
238
|
+
|
|
239
|
+
const receiptHeader = response.headers.get('Payment-Receipt')
|
|
240
|
+
expect(receiptHeader).toBeTruthy()
|
|
241
|
+
|
|
242
|
+
const receipt = Receipt.fromResponse(response)
|
|
243
|
+
expect(receipt.status).toBe('success')
|
|
244
|
+
expect(receipt.method).toBe('tempo')
|
|
245
|
+
|
|
246
|
+
server.close()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
Express,
|
|
2
3
|
Request as ExpressRequest,
|
|
3
4
|
Response as ExpressResponse,
|
|
4
5
|
NextFunction,
|
|
5
6
|
RequestHandler,
|
|
6
7
|
} from 'express'
|
|
8
|
+
|
|
9
|
+
import { generate, type GenerateConfig, type RouteConfig } from '../discovery/OpenApi.js'
|
|
7
10
|
import * as Mppx_core from '../server/Mppx.js'
|
|
8
|
-
import * as Request from '../server/Request.js'
|
|
9
11
|
import * as Mppx_internal from './internal/mppx.js'
|
|
10
12
|
|
|
11
13
|
export * from '../server/Methods.js'
|
|
@@ -60,7 +62,11 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
|
|
|
60
62
|
options: intent extends (options: infer options) => any ? options : never,
|
|
61
63
|
): RequestHandler {
|
|
62
64
|
return async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
63
|
-
const
|
|
65
|
+
const request = new Request(`${req.protocol}://${req.hostname}${req.originalUrl}`, {
|
|
66
|
+
method: req.method,
|
|
67
|
+
headers: req.headers as Record<string, string>,
|
|
68
|
+
})
|
|
69
|
+
const result = await intent(options)(request)
|
|
64
70
|
|
|
65
71
|
if (result.status === 402) {
|
|
66
72
|
const challenge = result.challenge as Response
|
|
@@ -80,3 +86,35 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
|
|
|
80
86
|
next()
|
|
81
87
|
}
|
|
82
88
|
}
|
|
89
|
+
|
|
90
|
+
export type DiscoveryConfig = Omit<GenerateConfig, 'routes'> & {
|
|
91
|
+
path?: string
|
|
92
|
+
routes?: RouteConfig[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const discoveryHeaders = { 'Cache-Control': 'public, max-age=300' }
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Mounts a `GET /openapi.json` route that serves an OpenAPI discovery document.
|
|
99
|
+
*/
|
|
100
|
+
export function discovery(
|
|
101
|
+
app: Express,
|
|
102
|
+
mppx: { methods: readonly Mppx_internal.AnyServer[]; realm: string },
|
|
103
|
+
config: DiscoveryConfig = {},
|
|
104
|
+
): void {
|
|
105
|
+
const mountPath = config.path ?? '/openapi.json'
|
|
106
|
+
|
|
107
|
+
const cached = JSON.stringify(
|
|
108
|
+
generate(mppx, {
|
|
109
|
+
...(config.info ? { info: config.info } : {}),
|
|
110
|
+
routes: config.routes ?? [],
|
|
111
|
+
...(config.serviceInfo ? { serviceInfo: config.serviceInfo } : {}),
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
app.get(mountPath, (_req: ExpressRequest, res: ExpressResponse) => {
|
|
116
|
+
res.setHeader('Cache-Control', discoveryHeaders['Cache-Control'])
|
|
117
|
+
res.setHeader('Content-Type', 'application/json')
|
|
118
|
+
res.end(cached)
|
|
119
|
+
})
|
|
120
|
+
}
|