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,425 @@
1
+ import * as Method from '../Method.js'
2
+ import * as Mppx from '../server/Mppx.js'
3
+ import * as z from '../zod.js'
4
+ import { generate } from './OpenApi.js'
5
+
6
+ const charge = Method.toServer(
7
+ Method.from({
8
+ intent: 'charge',
9
+ name: 'tempo',
10
+ schema: {
11
+ credential: { payload: z.object({ signature: z.string() }) },
12
+ request: z.object({
13
+ amount: z.string(),
14
+ currency: z.string(),
15
+ recipient: z.string(),
16
+ }),
17
+ },
18
+ }),
19
+ {
20
+ verify: async () => ({
21
+ method: 'tempo',
22
+ reference: '',
23
+ status: 'success' as const,
24
+ timestamp: '',
25
+ }),
26
+ },
27
+ )
28
+
29
+ const session = Method.toServer(
30
+ Method.from({
31
+ intent: 'session',
32
+ name: 'tempo',
33
+ schema: {
34
+ credential: { payload: z.object({ signature: z.string() }) },
35
+ request: z.object({
36
+ amount: z.union([z.null(), z.string()]),
37
+ recipient: z.string(),
38
+ }),
39
+ },
40
+ }),
41
+ {
42
+ verify: async () => ({
43
+ method: 'tempo',
44
+ reference: '',
45
+ status: 'success' as const,
46
+ timestamp: '',
47
+ }),
48
+ },
49
+ )
50
+
51
+ const subscribe = Method.toServer(
52
+ Method.from({
53
+ intent: 'subscribe',
54
+ name: 'tempo',
55
+ schema: {
56
+ credential: { payload: z.object({ signature: z.string() }) },
57
+ request: z.object({
58
+ amount: z.string(),
59
+ }),
60
+ },
61
+ }),
62
+ {
63
+ verify: async () => ({
64
+ method: 'tempo',
65
+ reference: '',
66
+ status: 'success' as const,
67
+ timestamp: '',
68
+ }),
69
+ },
70
+ )
71
+
72
+ function createMppx<const methods extends Mppx.Methods>(methods: methods) {
73
+ return Mppx.create({
74
+ methods,
75
+ realm: 'test-realm',
76
+ secretKey: 'test-secret',
77
+ })
78
+ }
79
+
80
+ describe('generate', () => {
81
+ test('generates a valid OpenAPI 3.1.0 document for legacy route config', () => {
82
+ const mppx = createMppx([charge])
83
+ const doc = generate(mppx, {
84
+ routes: [
85
+ {
86
+ intent: 'charge',
87
+ method: 'get',
88
+ options: { amount: '100', currency: '0xUSDC', recipient: '0x123' },
89
+ path: '/api/resource',
90
+ },
91
+ ],
92
+ })
93
+
94
+ expect(doc).toMatchInlineSnapshot(`
95
+ {
96
+ "info": {
97
+ "title": "test-realm",
98
+ "version": "1.0.0",
99
+ },
100
+ "openapi": "3.1.0",
101
+ "paths": {
102
+ "/api/resource": {
103
+ "get": {
104
+ "responses": {
105
+ "200": {
106
+ "description": "Successful response",
107
+ },
108
+ "402": {
109
+ "description": "Payment Required",
110
+ },
111
+ },
112
+ "x-payment-info": {
113
+ "amount": "100",
114
+ "currency": "0xUSDC",
115
+ "intent": "charge",
116
+ "method": "tempo",
117
+ "recipient": "0x123",
118
+ },
119
+ },
120
+ },
121
+ },
122
+ }
123
+ `)
124
+ })
125
+
126
+ test('supports handler-derived route config', () => {
127
+ const mppx = createMppx([charge])
128
+ const handler = mppx.charge({
129
+ amount: '50',
130
+ currency: 'usd',
131
+ description: 'Search credits',
132
+ recipient: '0x1',
133
+ })
134
+
135
+ const doc = generate(mppx, {
136
+ routes: [
137
+ {
138
+ handler,
139
+ method: 'post',
140
+ path: '/api/search',
141
+ },
142
+ ],
143
+ })
144
+
145
+ expect(doc).toMatchInlineSnapshot(`
146
+ {
147
+ "info": {
148
+ "title": "test-realm",
149
+ "version": "1.0.0",
150
+ },
151
+ "openapi": "3.1.0",
152
+ "paths": {
153
+ "/api/search": {
154
+ "post": {
155
+ "responses": {
156
+ "200": {
157
+ "description": "Successful response",
158
+ },
159
+ "402": {
160
+ "description": "Payment Required",
161
+ },
162
+ },
163
+ "x-payment-info": {
164
+ "amount": "50",
165
+ "currency": "usd",
166
+ "intent": "charge",
167
+ "method": "tempo",
168
+ "recipient": "0x1",
169
+ },
170
+ },
171
+ },
172
+ },
173
+ }
174
+ `)
175
+ })
176
+
177
+ test('handles null amount for session intent', () => {
178
+ const mppx = createMppx([session])
179
+ const doc = generate(mppx, {
180
+ routes: [
181
+ {
182
+ intent: 'session',
183
+ method: 'post',
184
+ options: { amount: null, recipient: '0x123' },
185
+ path: '/api/stream',
186
+ },
187
+ ],
188
+ })
189
+
190
+ expect(doc).toMatchInlineSnapshot(`
191
+ {
192
+ "info": {
193
+ "title": "test-realm",
194
+ "version": "1.0.0",
195
+ },
196
+ "openapi": "3.1.0",
197
+ "paths": {
198
+ "/api/stream": {
199
+ "post": {
200
+ "responses": {
201
+ "200": {
202
+ "description": "Successful response",
203
+ },
204
+ "402": {
205
+ "description": "Payment Required",
206
+ },
207
+ },
208
+ "x-payment-info": {
209
+ "amount": null,
210
+ "intent": "session",
211
+ "method": "tempo",
212
+ "recipient": "0x123",
213
+ },
214
+ },
215
+ },
216
+ },
217
+ }
218
+ `)
219
+ })
220
+
221
+ test('includes x-service-info when provided', () => {
222
+ const mppx = createMppx([charge])
223
+ const doc = generate(mppx, {
224
+ routes: [],
225
+ serviceInfo: {
226
+ categories: ['ai'],
227
+ docs: { homepage: 'https://example.com' },
228
+ },
229
+ })
230
+
231
+ expect(doc).toMatchInlineSnapshot(`
232
+ {
233
+ "info": {
234
+ "title": "test-realm",
235
+ "version": "1.0.0",
236
+ },
237
+ "openapi": "3.1.0",
238
+ "paths": {},
239
+ "x-service-info": {
240
+ "categories": [
241
+ "ai",
242
+ ],
243
+ "docs": {
244
+ "homepage": "https://example.com",
245
+ },
246
+ },
247
+ }
248
+ `)
249
+ })
250
+
251
+ test('multi-route document with mixed intents', () => {
252
+ const mppx = createMppx([charge, session])
253
+ const doc = generate(mppx, {
254
+ info: { title: 'Multi-Route API', version: '2.0.0' },
255
+ routes: [
256
+ {
257
+ intent: 'charge',
258
+ method: 'post',
259
+ options: { amount: '500', currency: '0xUSDC', recipient: '0xABC' },
260
+ path: '/api/search',
261
+ summary: 'Search the index',
262
+ requestBody: {
263
+ content: { 'application/json': { schema: { type: 'object' } } },
264
+ },
265
+ },
266
+ {
267
+ intent: 'session',
268
+ method: 'post',
269
+ options: { amount: null, recipient: '0xABC' },
270
+ path: '/api/stream',
271
+ },
272
+ {
273
+ intent: 'charge',
274
+ method: 'get',
275
+ options: { amount: '100', currency: '0xUSDC', recipient: '0xABC' },
276
+ path: '/api/models',
277
+ },
278
+ ],
279
+ serviceInfo: {
280
+ categories: ['ai', 'search'],
281
+ docs: {
282
+ apiReference: 'https://example.com/api',
283
+ homepage: 'https://example.com',
284
+ llms: 'https://example.com/llms.txt',
285
+ },
286
+ },
287
+ })
288
+
289
+ expect(doc).toMatchInlineSnapshot(`
290
+ {
291
+ "info": {
292
+ "title": "Multi-Route API",
293
+ "version": "2.0.0",
294
+ },
295
+ "openapi": "3.1.0",
296
+ "paths": {
297
+ "/api/models": {
298
+ "get": {
299
+ "responses": {
300
+ "200": {
301
+ "description": "Successful response",
302
+ },
303
+ "402": {
304
+ "description": "Payment Required",
305
+ },
306
+ },
307
+ "x-payment-info": {
308
+ "amount": "100",
309
+ "currency": "0xUSDC",
310
+ "intent": "charge",
311
+ "method": "tempo",
312
+ "recipient": "0xABC",
313
+ },
314
+ },
315
+ },
316
+ "/api/search": {
317
+ "post": {
318
+ "requestBody": {
319
+ "content": {
320
+ "application/json": {
321
+ "schema": {
322
+ "type": "object",
323
+ },
324
+ },
325
+ },
326
+ },
327
+ "responses": {
328
+ "200": {
329
+ "description": "Successful response",
330
+ },
331
+ "402": {
332
+ "description": "Payment Required",
333
+ },
334
+ },
335
+ "summary": "Search the index",
336
+ "x-payment-info": {
337
+ "amount": "500",
338
+ "currency": "0xUSDC",
339
+ "intent": "charge",
340
+ "method": "tempo",
341
+ "recipient": "0xABC",
342
+ },
343
+ },
344
+ },
345
+ "/api/stream": {
346
+ "post": {
347
+ "responses": {
348
+ "200": {
349
+ "description": "Successful response",
350
+ },
351
+ "402": {
352
+ "description": "Payment Required",
353
+ },
354
+ },
355
+ "x-payment-info": {
356
+ "amount": null,
357
+ "intent": "session",
358
+ "method": "tempo",
359
+ "recipient": "0xABC",
360
+ },
361
+ },
362
+ },
363
+ },
364
+ "x-service-info": {
365
+ "categories": [
366
+ "ai",
367
+ "search",
368
+ ],
369
+ "docs": {
370
+ "apiReference": "https://example.com/api",
371
+ "homepage": "https://example.com",
372
+ "llms": "https://example.com/llms.txt",
373
+ },
374
+ },
375
+ }
376
+ `)
377
+ })
378
+
379
+ test('passes through custom intents and extra params', () => {
380
+ const mppx = createMppx([subscribe])
381
+ const doc = generate(mppx, {
382
+ routes: [
383
+ {
384
+ intent: 'subscribe',
385
+ method: 'post',
386
+ options: { amount: '100', interval: 'monthly', recipient: '0xABC' },
387
+ path: '/api/subscribe',
388
+ summary: 'Monthly subscription',
389
+ },
390
+ ],
391
+ })
392
+
393
+ expect(doc).toMatchInlineSnapshot(`
394
+ {
395
+ "info": {
396
+ "title": "test-realm",
397
+ "version": "1.0.0",
398
+ },
399
+ "openapi": "3.1.0",
400
+ "paths": {
401
+ "/api/subscribe": {
402
+ "post": {
403
+ "responses": {
404
+ "200": {
405
+ "description": "Successful response",
406
+ },
407
+ "402": {
408
+ "description": "Payment Required",
409
+ },
410
+ },
411
+ "summary": "Monthly subscription",
412
+ "x-payment-info": {
413
+ "amount": "100",
414
+ "intent": "subscribe",
415
+ "interval": "monthly",
416
+ "method": "tempo",
417
+ "recipient": "0xABC",
418
+ },
419
+ },
420
+ },
421
+ },
422
+ }
423
+ `)
424
+ })
425
+ })
@@ -0,0 +1,224 @@
1
+ import type * as Method from '../Method.js'
2
+ import type { ServiceInfo } from './Discovery.js'
3
+
4
+ export type DiscoveryHandler = ((...args: any[]) => unknown) & {
5
+ _internal?: {
6
+ _canonicalRequest: Record<string, unknown>
7
+ intent: string
8
+ name: string
9
+ }
10
+ }
11
+
12
+ export type LegacyRouteConfig = {
13
+ intent: string
14
+ method: string
15
+ options: Record<string, unknown>
16
+ path: string
17
+ requestBody?: Record<string, unknown>
18
+ summary?: string
19
+ }
20
+
21
+ export type HandlerRouteConfig = {
22
+ handler: DiscoveryHandler
23
+ method: string
24
+ path: string
25
+ requestBody?: Record<string, unknown>
26
+ summary?: string
27
+ }
28
+
29
+ export type RouteConfig = HandlerRouteConfig | LegacyRouteConfig
30
+
31
+ export type GenerateConfig = {
32
+ info?: { title?: string; version?: string } | undefined
33
+ routes: RouteConfig[]
34
+ serviceInfo?: ServiceInfo | undefined
35
+ }
36
+
37
+ export type GenerateProxyConfig = {
38
+ basePath?: string | undefined
39
+ info?: { title?: string; version?: string } | undefined
40
+ routes: Array<{
41
+ method: string
42
+ path: string
43
+ payment: Record<string, unknown> | null
44
+ requestBody?: Record<string, unknown>
45
+ summary?: string
46
+ }>
47
+ serviceInfo?: ServiceInfo | undefined
48
+ }
49
+
50
+ type ResolvedRoute = {
51
+ method: string
52
+ path: string
53
+ payment: Record<string, unknown> | null
54
+ requestBody?: Record<string, unknown>
55
+ summary?: string
56
+ }
57
+
58
+ /**
59
+ * Generates an OpenAPI 3.1.0 discovery document from an mppx instance
60
+ * and route configuration.
61
+ */
62
+ export function generate(
63
+ mppx: { methods: readonly Method.AnyServer[]; realm: string },
64
+ config: GenerateConfig,
65
+ ): Record<string, unknown> {
66
+ const methods = mppx.methods
67
+ const methodsByKey = new Map<string, Method.AnyServer>()
68
+ const intentCount: Record<string, number> = {}
69
+
70
+ for (const mi of methods) {
71
+ methodsByKey.set(`${mi.name}/${mi.intent}`, mi)
72
+ intentCount[mi.intent] = (intentCount[mi.intent] ?? 0) + 1
73
+ }
74
+ for (const mi of methods) {
75
+ if (intentCount[mi.intent] === 1) methodsByKey.set(mi.intent, mi)
76
+ }
77
+
78
+ const routes = config.routes.map((route) => resolveRoute(route, methodsByKey))
79
+ return createDocument({
80
+ info: {
81
+ title: config.info?.title ?? mppx.realm,
82
+ version: config.info?.version ?? '1.0.0',
83
+ },
84
+ routes,
85
+ serviceInfo: config.serviceInfo,
86
+ })
87
+ }
88
+
89
+ /**
90
+ * Generates an OpenAPI 3.1.0 discovery document for a proxy surface.
91
+ */
92
+ export function generateProxy(config: GenerateProxyConfig): Record<string, unknown> {
93
+ const routes = config.routes.map((route) => ({
94
+ ...route,
95
+ path: withBasePath(config.basePath, route.path),
96
+ }))
97
+
98
+ return createDocument({
99
+ info: {
100
+ title: config.info?.title ?? 'API Proxy',
101
+ version: config.info?.version ?? '1.0.0',
102
+ },
103
+ routes,
104
+ serviceInfo: config.serviceInfo,
105
+ })
106
+ }
107
+
108
+ function createDocument(config: {
109
+ info: { title: string; version: string }
110
+ routes: ResolvedRoute[]
111
+ serviceInfo?: ServiceInfo | undefined
112
+ }) {
113
+ const paths: Record<string, Record<string, unknown>> = {}
114
+
115
+ for (const route of config.routes) {
116
+ const method = route.method.toLowerCase()
117
+ const operation: Record<string, unknown> = {
118
+ responses: {
119
+ ...(route.payment ? { '402': { description: 'Payment Required' } } : {}),
120
+ '200': { description: 'Successful response' },
121
+ },
122
+ }
123
+
124
+ if (route.payment) operation['x-payment-info'] = route.payment
125
+ if (route.summary) operation.summary = route.summary
126
+ if (route.requestBody) operation.requestBody = route.requestBody
127
+
128
+ if (!paths[route.path]) paths[route.path] = {}
129
+ paths[route.path]![method] = operation
130
+ }
131
+
132
+ const doc: Record<string, unknown> = {
133
+ info: config.info,
134
+ openapi: '3.1.0',
135
+ paths,
136
+ }
137
+ if (config.serviceInfo) doc['x-service-info'] = config.serviceInfo
138
+ return doc
139
+ }
140
+
141
+ function resolveRoute(
142
+ route: RouteConfig,
143
+ methodsByKey: Map<string, Method.AnyServer>,
144
+ ): ResolvedRoute {
145
+ if ('handler' in route) {
146
+ const internal = route.handler._internal
147
+ if (!internal)
148
+ throw new Error(
149
+ `Route ${route.method.toUpperCase()} ${route.path} is missing discovery metadata`,
150
+ )
151
+ return {
152
+ method: route.method,
153
+ path: route.path,
154
+ payment: paymentInfoFromCanonical({
155
+ canonicalRequest: internal._canonicalRequest,
156
+ intent: internal.intent,
157
+ method: internal.name,
158
+ }),
159
+ ...(route.requestBody ? { requestBody: route.requestBody } : {}),
160
+ ...(route.summary ? { summary: route.summary } : {}),
161
+ }
162
+ }
163
+
164
+ const mi = methodsByKey.get(route.intent)
165
+ if (!mi) {
166
+ throw new Error(
167
+ `Unknown intent "${route.intent}" for route ${route.method.toUpperCase()} ${route.path}. Available: ${[...methodsByKey.keys()].join(', ')}`,
168
+ )
169
+ }
170
+
171
+ return {
172
+ method: route.method,
173
+ path: route.path,
174
+ payment: paymentInfoFromCanonical({
175
+ canonicalRequest: route.options,
176
+ intent: mi.intent,
177
+ method: mi.name,
178
+ }),
179
+ ...(route.requestBody ? { requestBody: route.requestBody } : {}),
180
+ ...(route.summary ? { summary: route.summary } : {}),
181
+ }
182
+ }
183
+
184
+ function paymentInfoFromCanonical(route: {
185
+ canonicalRequest: Record<string, unknown>
186
+ intent: string
187
+ method: string
188
+ }) {
189
+ const { canonicalRequest, intent, method } = route
190
+ const methodDetails = (canonicalRequest.methodDetails ?? {}) as Record<string, unknown>
191
+
192
+ const amount = pickString(canonicalRequest.amount) ?? pickString(methodDetails.amount) ?? null
193
+ const currency = pickString(canonicalRequest.currency) ?? pickString(methodDetails.currency)
194
+ const description = pickString(canonicalRequest.description)
195
+
196
+ const base: Record<string, unknown> = {
197
+ amount,
198
+ ...(currency ? { currency } : {}),
199
+ ...(description ? { description } : {}),
200
+ intent,
201
+ method,
202
+ }
203
+
204
+ // Forward any extra canonical params that aren't already covered.
205
+ const reserved = new Set(['amount', 'currency', 'description', 'methodDetails'])
206
+ for (const [key, value] of Object.entries(canonicalRequest)) {
207
+ if (!reserved.has(key) && value !== undefined) base[key] = value
208
+ }
209
+
210
+ return base
211
+ }
212
+
213
+ function pickString(value: unknown) {
214
+ return typeof value === 'string' ? value : undefined
215
+ }
216
+
217
+ function withBasePath(basePath: string | undefined, path: string) {
218
+ if (!basePath) return path
219
+ const normalizedBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`
220
+ const trimmedBasePath = normalizedBasePath.endsWith('/')
221
+ ? normalizedBasePath.slice(0, -1)
222
+ : normalizedBasePath
223
+ return `${trimmedBasePath}${path}`
224
+ }