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.
Files changed (267) hide show
  1. package/CHANGELOG.md +26 -3
  2. package/README.md +13 -13
  3. package/dist/BodyDigest.d.ts.map +1 -1
  4. package/dist/BodyDigest.js.map +1 -1
  5. package/dist/Challenge.d.ts.map +1 -1
  6. package/dist/Challenge.js.map +1 -1
  7. package/dist/Credential.d.ts.map +1 -1
  8. package/dist/Credential.js.map +1 -1
  9. package/dist/Errors.js +64 -67
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/PaymentRequest.d.ts.map +1 -1
  12. package/dist/PaymentRequest.js.map +1 -1
  13. package/dist/Receipt.d.ts.map +1 -1
  14. package/dist/Receipt.js.map +1 -1
  15. package/dist/Store.d.ts +9 -0
  16. package/dist/Store.d.ts.map +1 -1
  17. package/dist/Store.js +17 -0
  18. package/dist/Store.js.map +1 -1
  19. package/dist/cli/account.d.ts.map +1 -1
  20. package/dist/cli/account.js +40 -5
  21. package/dist/cli/account.js.map +1 -1
  22. package/dist/cli/cli.d.ts.map +1 -1
  23. package/dist/cli/cli.js +157 -1
  24. package/dist/cli/cli.js.map +1 -1
  25. package/dist/cli/internal.d.ts.map +1 -1
  26. package/dist/cli/internal.js.map +1 -1
  27. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  28. package/dist/cli/plugins/stripe.js.map +1 -1
  29. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  30. package/dist/cli/plugins/tempo.js +2 -1
  31. package/dist/cli/plugins/tempo.js.map +1 -1
  32. package/dist/cli/utils.d.ts.map +1 -1
  33. package/dist/cli/utils.js.map +1 -1
  34. package/dist/client/internal/Fetch.d.ts +2 -0
  35. package/dist/client/internal/Fetch.d.ts.map +1 -1
  36. package/dist/client/internal/Fetch.js +1 -1
  37. package/dist/client/internal/Fetch.js.map +1 -1
  38. package/dist/discovery/Discovery.d.ts +146 -0
  39. package/dist/discovery/Discovery.d.ts.map +1 -0
  40. package/dist/discovery/Discovery.js +60 -0
  41. package/dist/discovery/Discovery.js.map +1 -0
  42. package/dist/discovery/OpenApi.d.ts +61 -0
  43. package/dist/discovery/OpenApi.d.ts.map +1 -0
  44. package/dist/discovery/OpenApi.js +139 -0
  45. package/dist/discovery/OpenApi.js.map +1 -0
  46. package/dist/discovery/Validate.d.ts +10 -0
  47. package/dist/discovery/Validate.d.ts.map +1 -0
  48. package/dist/discovery/Validate.js +63 -0
  49. package/dist/discovery/Validate.js.map +1 -0
  50. package/dist/discovery/index.d.ts +4 -0
  51. package/dist/discovery/index.d.ts.map +1 -0
  52. package/dist/discovery/index.js +4 -0
  53. package/dist/discovery/index.js.map +1 -0
  54. package/dist/internal/types.d.ts.map +1 -1
  55. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  56. package/dist/mcp-sdk/client/McpClient.js +1 -1
  57. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  58. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
  59. package/dist/mcp-sdk/server/Transport.js.map +1 -1
  60. package/dist/middlewares/elysia.d.ts +52 -1
  61. package/dist/middlewares/elysia.d.ts.map +1 -1
  62. package/dist/middlewares/elysia.js +17 -0
  63. package/dist/middlewares/elysia.js.map +1 -1
  64. package/dist/middlewares/express.d.ts +13 -1
  65. package/dist/middlewares/express.d.ts.map +1 -1
  66. package/dist/middlewares/express.js +23 -2
  67. package/dist/middlewares/express.js.map +1 -1
  68. package/dist/middlewares/hono.d.ts +19 -1
  69. package/dist/middlewares/hono.d.ts.map +1 -1
  70. package/dist/middlewares/hono.js +51 -0
  71. package/dist/middlewares/hono.js.map +1 -1
  72. package/dist/middlewares/internal/mppx.d.ts +4 -2
  73. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  74. package/dist/middlewares/internal/mppx.js +10 -3
  75. package/dist/middlewares/internal/mppx.js.map +1 -1
  76. package/dist/middlewares/nextjs.d.ts +11 -0
  77. package/dist/middlewares/nextjs.d.ts.map +1 -1
  78. package/dist/middlewares/nextjs.js +15 -0
  79. package/dist/middlewares/nextjs.js.map +1 -1
  80. package/dist/proxy/Proxy.d.ts +6 -0
  81. package/dist/proxy/Proxy.d.ts.map +1 -1
  82. package/dist/proxy/Proxy.js +56 -80
  83. package/dist/proxy/Proxy.js.map +1 -1
  84. package/dist/proxy/Service.d.ts +16 -23
  85. package/dist/proxy/Service.d.ts.map +1 -1
  86. package/dist/proxy/Service.js +20 -84
  87. package/dist/proxy/Service.js.map +1 -1
  88. package/dist/proxy/internal/Route.js +1 -1
  89. package/dist/proxy/internal/Route.js.map +1 -1
  90. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  91. package/dist/proxy/services/anthropic.js +5 -0
  92. package/dist/proxy/services/anthropic.js.map +1 -1
  93. package/dist/proxy/services/openai.d.ts.map +1 -1
  94. package/dist/proxy/services/openai.js +6 -3
  95. package/dist/proxy/services/openai.js.map +1 -1
  96. package/dist/proxy/services/stripe.d.ts.map +1 -1
  97. package/dist/proxy/services/stripe.js +6 -3
  98. package/dist/proxy/services/stripe.js.map +1 -1
  99. package/dist/server/Mppx.d.ts.map +1 -1
  100. package/dist/server/Mppx.js +35 -17
  101. package/dist/server/Mppx.js.map +1 -1
  102. package/dist/server/Request.d.ts.map +1 -1
  103. package/dist/server/Request.js.map +1 -1
  104. package/dist/stripe/Methods.d.ts.map +1 -1
  105. package/dist/stripe/Methods.js.map +1 -1
  106. package/dist/tempo/Methods.d.ts.map +1 -1
  107. package/dist/tempo/Methods.js.map +1 -1
  108. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  109. package/dist/tempo/client/ChannelOps.js.map +1 -1
  110. package/dist/tempo/client/Charge.d.ts.map +1 -1
  111. package/dist/tempo/client/Charge.js.map +1 -1
  112. package/dist/tempo/client/Session.d.ts.map +1 -1
  113. package/dist/tempo/client/Session.js.map +1 -1
  114. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  115. package/dist/tempo/client/SessionManager.js +1 -1
  116. package/dist/tempo/client/SessionManager.js.map +1 -1
  117. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  118. package/dist/tempo/internal/auto-swap.js +1 -1
  119. package/dist/tempo/internal/auto-swap.js.map +1 -1
  120. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  121. package/dist/tempo/internal/fee-payer.js +1 -1
  122. package/dist/tempo/internal/fee-payer.js.map +1 -1
  123. package/dist/tempo/server/Charge.d.ts.map +1 -1
  124. package/dist/tempo/server/Charge.js +1 -1
  125. package/dist/tempo/server/Charge.js.map +1 -1
  126. package/dist/tempo/server/Session.d.ts.map +1 -1
  127. package/dist/tempo/server/Session.js +18 -5
  128. package/dist/tempo/server/Session.js.map +1 -1
  129. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  130. package/dist/tempo/server/internal/transport.js +8 -0
  131. package/dist/tempo/server/internal/transport.js.map +1 -1
  132. package/dist/tempo/session/Chain.d.ts.map +1 -1
  133. package/dist/tempo/session/Chain.js +1 -1
  134. package/dist/tempo/session/Chain.js.map +1 -1
  135. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  136. package/dist/tempo/session/ChannelStore.js.map +1 -1
  137. package/dist/tempo/session/Receipt.d.ts.map +1 -1
  138. package/dist/tempo/session/Receipt.js.map +1 -1
  139. package/dist/tempo/session/Sse.d.ts.map +1 -1
  140. package/dist/tempo/session/Sse.js.map +1 -1
  141. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  142. package/dist/tempo/session/Voucher.js.map +1 -1
  143. package/dist/viem/Client.d.ts.map +1 -1
  144. package/dist/viem/Client.js.map +1 -1
  145. package/package.json +6 -1
  146. package/src/BodyDigest.test.ts +1 -1
  147. package/src/BodyDigest.ts +1 -0
  148. package/src/Challenge.fuzz.test.ts +121 -0
  149. package/src/Challenge.test-d.ts +2 -1
  150. package/src/Challenge.test.ts +1 -1
  151. package/src/Challenge.ts +1 -0
  152. package/src/Credential.fuzz.test.ts +62 -0
  153. package/src/Credential.test.ts +1 -1
  154. package/src/Credential.ts +1 -0
  155. package/src/Errors.test.ts +28 -40
  156. package/src/Expires.test.ts +2 -1
  157. package/src/Method.test.ts +1 -1
  158. package/src/PaymentRequest.test.ts +1 -1
  159. package/src/PaymentRequest.ts +1 -0
  160. package/src/Receipt.test.ts +1 -1
  161. package/src/Receipt.ts +1 -0
  162. package/src/Store.test-d.ts +2 -1
  163. package/src/Store.test.ts +57 -7
  164. package/src/Store.ts +25 -0
  165. package/src/cli/account.ts +65 -30
  166. package/src/cli/cli.test.ts +215 -2
  167. package/src/cli/cli.ts +166 -1
  168. package/src/cli/config.test.ts +1 -0
  169. package/src/cli/internal.ts +1 -0
  170. package/src/cli/plugins/stripe.ts +1 -0
  171. package/src/cli/plugins/tempo.ts +4 -1
  172. package/src/cli/utils.ts +1 -0
  173. package/src/client/Mppx.test-d.ts +2 -1
  174. package/src/client/Mppx.test.ts +1 -1
  175. package/src/client/Transport.test.ts +1 -1
  176. package/src/client/internal/Fetch.browser.test.ts +2 -1
  177. package/src/client/internal/Fetch.test-d.ts +2 -1
  178. package/src/client/internal/Fetch.test.ts +3 -1
  179. package/src/client/internal/Fetch.ts +1 -1
  180. package/src/discovery/Discovery.test.ts +152 -0
  181. package/src/discovery/Discovery.ts +72 -0
  182. package/src/discovery/OpenApi.test.ts +425 -0
  183. package/src/discovery/OpenApi.ts +224 -0
  184. package/src/discovery/Validate.test.ts +188 -0
  185. package/src/discovery/Validate.ts +76 -0
  186. package/src/discovery/index.ts +3 -0
  187. package/src/internal/constantTimeEqual.test.ts +2 -1
  188. package/src/internal/types.ts +1 -3
  189. package/src/mcp-sdk/client/McpClient.test-d.ts +2 -1
  190. package/src/mcp-sdk/client/McpClient.test.ts +2 -1
  191. package/src/mcp-sdk/client/McpClient.ts +2 -0
  192. package/src/mcp-sdk/server/Transport.test.ts +2 -1
  193. package/src/mcp-sdk/server/Transport.ts +1 -0
  194. package/src/middlewares/elysia.test.ts +28 -2
  195. package/src/middlewares/elysia.ts +36 -1
  196. package/src/middlewares/express.test.ts +95 -7
  197. package/src/middlewares/express.ts +40 -2
  198. package/src/middlewares/hono.test.ts +28 -6
  199. package/src/middlewares/hono.ts +74 -1
  200. package/src/middlewares/internal/mppx.test.ts +2 -1
  201. package/src/middlewares/internal/mppx.ts +14 -6
  202. package/src/middlewares/nextjs.test.ts +32 -6
  203. package/src/middlewares/nextjs.ts +28 -0
  204. package/src/proxy/Proxy.test.ts +55 -270
  205. package/src/proxy/Proxy.ts +73 -93
  206. package/src/proxy/Service.test.ts +24 -1
  207. package/src/proxy/Service.ts +48 -88
  208. package/src/proxy/internal/Headers.test.ts +2 -1
  209. package/src/proxy/internal/Route.test.ts +9 -1
  210. package/src/proxy/internal/Route.ts +1 -1
  211. package/src/proxy/services/anthropic.test.ts +132 -0
  212. package/src/proxy/services/anthropic.ts +5 -0
  213. package/src/proxy/services/openai.test.ts +2 -1
  214. package/src/proxy/services/openai.ts +6 -4
  215. package/src/proxy/services/stripe.test.ts +132 -0
  216. package/src/proxy/services/stripe.ts +6 -4
  217. package/src/server/Mppx.test-d.ts +1 -1
  218. package/src/server/Mppx.test.ts +194 -1
  219. package/src/server/Mppx.ts +38 -19
  220. package/src/server/NodeListener.test.ts +1 -1
  221. package/src/server/Request.test.ts +2 -1
  222. package/src/server/Request.ts +1 -0
  223. package/src/server/Response.test.ts +2 -1
  224. package/src/server/Transport.test.ts +2 -1
  225. package/src/stripe/Charge.integration.test.ts +1 -1
  226. package/src/stripe/Methods.test.ts +1 -1
  227. package/src/stripe/Methods.ts +1 -0
  228. package/src/stripe/client/Charge.test.ts +2 -1
  229. package/src/stripe/server/Charge.test.ts +2 -1
  230. package/src/tempo/Attribution.test.ts +2 -1
  231. package/src/tempo/Methods.test.ts +1 -1
  232. package/src/tempo/Methods.ts +1 -0
  233. package/src/tempo/client/ChannelOps.test.ts +7 -3
  234. package/src/tempo/client/ChannelOps.ts +1 -0
  235. package/src/tempo/client/Charge.ts +1 -0
  236. package/src/tempo/client/Session.test.ts +6 -2
  237. package/src/tempo/client/Session.ts +1 -0
  238. package/src/tempo/client/SessionManager.test.ts +29 -1
  239. package/src/tempo/client/SessionManager.ts +2 -1
  240. package/src/tempo/internal/auto-swap.test.ts +2 -1
  241. package/src/tempo/internal/auto-swap.ts +1 -0
  242. package/src/tempo/internal/defaults.test.ts +2 -1
  243. package/src/tempo/internal/fee-payer.test.ts +2 -1
  244. package/src/tempo/internal/fee-payer.ts +1 -0
  245. package/src/tempo/server/Charge.test.ts +2 -1
  246. package/src/tempo/server/Charge.ts +1 -0
  247. package/src/tempo/server/Session.test.ts +88 -37
  248. package/src/tempo/server/Session.ts +26 -8
  249. package/src/tempo/server/Sse.test.ts +2 -1
  250. package/src/tempo/server/internal/transport.test.ts +25 -1
  251. package/src/tempo/server/internal/transport.ts +11 -0
  252. package/src/tempo/session/Chain.test.ts +6 -2
  253. package/src/tempo/session/Chain.ts +2 -1
  254. package/src/tempo/session/Channel.test.ts +2 -1
  255. package/src/tempo/session/ChannelStore.test.ts +2 -1
  256. package/src/tempo/session/ChannelStore.ts +1 -0
  257. package/src/tempo/session/Receipt.test.ts +2 -1
  258. package/src/tempo/session/Receipt.ts +1 -0
  259. package/src/tempo/session/Sse.fuzz.test.ts +138 -0
  260. package/src/tempo/session/Sse.test.ts +2 -1
  261. package/src/tempo/session/Sse.ts +1 -0
  262. package/src/tempo/session/Voucher.test.ts +2 -1
  263. package/src/tempo/session/Voucher.ts +1 -0
  264. package/src/viem/Account.test.ts +2 -1
  265. package/src/viem/Client.test.ts +2 -1
  266. package/src/viem/Client.ts +1 -0
  267. 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
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Discovery.js'
2
+ export * from './OpenApi.js'
3
+ export * from './Validate.js'
@@ -1,4 +1,5 @@
1
- import { describe, expect, test } from 'vitest'
1
+ import { describe, expect, test } from 'vp/test'
2
+
2
3
  import { constantTimeEqual } from './constantTimeEqual.js'
3
4
 
4
5
  describe('constantTimeEqual', () => {
@@ -256,9 +256,7 @@ export type LastInUnion<U> =
256
256
 
257
257
  /** @internal */
258
258
  export type UnionToIntersection<union> = (
259
- union extends unknown
260
- ? (arg: union) => 0
261
- : never
259
+ union extends unknown ? (arg: union) => 0 : never
262
260
  ) extends (arg: infer i) => 0
263
261
  ? i
264
262
  : never
@@ -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 'vitest'
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 'vitest'
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 'vitest'
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,4 +1,5 @@
1
1
  import type { CallToolResult, McpError } from '@modelcontextprotocol/sdk/types.js'
2
+
2
3
  import type * as Credential from '../../Credential.js'
3
4
  import * as core_Mcp from '../../Mcp.js'
4
5
  import * as Transport from '../../server/Transport.js'
@@ -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 'vitest'
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 { Context } from 'elysia'
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 'vitest'
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
- recipient: accounts[0].address,
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
- recipient: accounts[0].address,
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
- recipient: accounts[0].address,
154
+ account: accounts[0],
127
155
  currency: asset,
128
156
  escrowContract,
129
- feePayer: accounts[0],
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 result = await intent(options)(Request.fromNodeListener(req, res))
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
+ }