mppx 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +155 -0
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/discovery/Discovery.d.ts +146 -0
  6. package/dist/discovery/Discovery.d.ts.map +1 -0
  7. package/dist/discovery/Discovery.js +60 -0
  8. package/dist/discovery/Discovery.js.map +1 -0
  9. package/dist/discovery/OpenApi.d.ts +61 -0
  10. package/dist/discovery/OpenApi.d.ts.map +1 -0
  11. package/dist/discovery/OpenApi.js +139 -0
  12. package/dist/discovery/OpenApi.js.map +1 -0
  13. package/dist/discovery/Validate.d.ts +10 -0
  14. package/dist/discovery/Validate.d.ts.map +1 -0
  15. package/dist/discovery/Validate.js +63 -0
  16. package/dist/discovery/Validate.js.map +1 -0
  17. package/dist/discovery/index.d.ts +4 -0
  18. package/dist/discovery/index.d.ts.map +1 -0
  19. package/dist/discovery/index.js +4 -0
  20. package/dist/discovery/index.js.map +1 -0
  21. package/dist/middlewares/elysia.d.ts +52 -1
  22. package/dist/middlewares/elysia.d.ts.map +1 -1
  23. package/dist/middlewares/elysia.js +17 -0
  24. package/dist/middlewares/elysia.js.map +1 -1
  25. package/dist/middlewares/express.d.ts +13 -1
  26. package/dist/middlewares/express.d.ts.map +1 -1
  27. package/dist/middlewares/express.js +18 -0
  28. package/dist/middlewares/express.js.map +1 -1
  29. package/dist/middlewares/hono.d.ts +19 -1
  30. package/dist/middlewares/hono.d.ts.map +1 -1
  31. package/dist/middlewares/hono.js +51 -0
  32. package/dist/middlewares/hono.js.map +1 -1
  33. package/dist/middlewares/internal/mppx.d.ts +4 -2
  34. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  35. package/dist/middlewares/internal/mppx.js +10 -3
  36. package/dist/middlewares/internal/mppx.js.map +1 -1
  37. package/dist/middlewares/nextjs.d.ts +11 -0
  38. package/dist/middlewares/nextjs.d.ts.map +1 -1
  39. package/dist/middlewares/nextjs.js +15 -0
  40. package/dist/middlewares/nextjs.js.map +1 -1
  41. package/dist/proxy/Proxy.d.ts +6 -0
  42. package/dist/proxy/Proxy.d.ts.map +1 -1
  43. package/dist/proxy/Proxy.js +56 -80
  44. package/dist/proxy/Proxy.js.map +1 -1
  45. package/dist/proxy/Service.d.ts +16 -23
  46. package/dist/proxy/Service.d.ts.map +1 -1
  47. package/dist/proxy/Service.js +19 -83
  48. package/dist/proxy/Service.js.map +1 -1
  49. package/dist/proxy/internal/Route.js +1 -1
  50. package/dist/proxy/internal/Route.js.map +1 -1
  51. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  52. package/dist/proxy/services/anthropic.js +5 -0
  53. package/dist/proxy/services/anthropic.js.map +1 -1
  54. package/dist/proxy/services/openai.d.ts.map +1 -1
  55. package/dist/proxy/services/openai.js +6 -3
  56. package/dist/proxy/services/openai.js.map +1 -1
  57. package/dist/proxy/services/stripe.d.ts.map +1 -1
  58. package/dist/proxy/services/stripe.js +6 -3
  59. package/dist/proxy/services/stripe.js.map +1 -1
  60. package/dist/stripe/internal/types.d.ts +3 -0
  61. package/dist/stripe/internal/types.d.ts.map +1 -1
  62. package/dist/stripe/server/Charge.d.ts.map +1 -1
  63. package/dist/stripe/server/Charge.js +9 -2
  64. package/dist/stripe/server/Charge.js.map +1 -1
  65. package/dist/tempo/server/Session.d.ts.map +1 -1
  66. package/dist/tempo/server/Session.js +25 -8
  67. package/dist/tempo/server/Session.js.map +1 -1
  68. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  69. package/dist/tempo/server/internal/transport.js +8 -0
  70. package/dist/tempo/server/internal/transport.js.map +1 -1
  71. package/dist/tempo/session/Chain.js +1 -1
  72. package/dist/tempo/session/Chain.js.map +1 -1
  73. package/package.json +6 -1
  74. package/src/BodyDigest.test.ts +1 -1
  75. package/src/Challenge.fuzz.test.ts +121 -0
  76. package/src/Challenge.test-d.ts +1 -1
  77. package/src/Challenge.test.ts +1 -1
  78. package/src/Credential.fuzz.test.ts +62 -0
  79. package/src/Credential.test.ts +1 -1
  80. package/src/Errors.test.ts +1 -1
  81. package/src/Expires.test.ts +1 -1
  82. package/src/Method.test.ts +1 -1
  83. package/src/PaymentRequest.test.ts +1 -1
  84. package/src/Receipt.test.ts +1 -1
  85. package/src/Store.test-d.ts +1 -1
  86. package/src/Store.test.ts +1 -1
  87. package/src/cli/cli.test.ts +212 -1
  88. package/src/cli/cli.ts +162 -0
  89. package/src/client/Mppx.test-d.ts +1 -1
  90. package/src/client/Mppx.test.ts +1 -1
  91. package/src/client/Transport.test.ts +1 -1
  92. package/src/client/internal/Fetch.browser.test.ts +1 -1
  93. package/src/client/internal/Fetch.test-d.ts +1 -1
  94. package/src/client/internal/Fetch.test.ts +2 -1
  95. package/src/discovery/Discovery.test.ts +152 -0
  96. package/src/discovery/Discovery.ts +72 -0
  97. package/src/discovery/OpenApi.test.ts +425 -0
  98. package/src/discovery/OpenApi.ts +224 -0
  99. package/src/discovery/Validate.test.ts +188 -0
  100. package/src/discovery/Validate.ts +76 -0
  101. package/src/discovery/index.ts +3 -0
  102. package/src/internal/constantTimeEqual.test.ts +1 -1
  103. package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
  104. package/src/mcp-sdk/client/McpClient.test.ts +1 -1
  105. package/src/mcp-sdk/server/Transport.test.ts +1 -1
  106. package/src/middlewares/elysia.test.ts +27 -2
  107. package/src/middlewares/elysia.ts +35 -1
  108. package/src/middlewares/express.test.ts +35 -7
  109. package/src/middlewares/express.ts +34 -0
  110. package/src/middlewares/hono.test.ts +28 -6
  111. package/src/middlewares/hono.ts +73 -1
  112. package/src/middlewares/internal/mppx.test.ts +1 -1
  113. package/src/middlewares/internal/mppx.ts +14 -6
  114. package/src/middlewares/nextjs.test.ts +31 -6
  115. package/src/middlewares/nextjs.ts +28 -0
  116. package/src/proxy/Proxy.test.ts +54 -270
  117. package/src/proxy/Proxy.ts +71 -93
  118. package/src/proxy/Service.test.ts +23 -1
  119. package/src/proxy/Service.ts +40 -86
  120. package/src/proxy/internal/Headers.test.ts +1 -1
  121. package/src/proxy/internal/Route.test.ts +9 -1
  122. package/src/proxy/internal/Route.ts +1 -1
  123. package/src/proxy/services/anthropic.test.ts +132 -0
  124. package/src/proxy/services/anthropic.ts +5 -0
  125. package/src/proxy/services/openai.test.ts +1 -1
  126. package/src/proxy/services/openai.ts +6 -4
  127. package/src/proxy/services/stripe.test.ts +132 -0
  128. package/src/proxy/services/stripe.ts +6 -4
  129. package/src/server/Mppx.test-d.ts +1 -1
  130. package/src/server/Mppx.test.ts +2 -1
  131. package/src/server/NodeListener.test.ts +1 -1
  132. package/src/server/Request.test.ts +1 -1
  133. package/src/server/Response.test.ts +1 -1
  134. package/src/server/Transport.test.ts +1 -1
  135. package/src/stripe/Charge.integration.test.ts +1 -1
  136. package/src/stripe/Methods.test.ts +1 -1
  137. package/src/stripe/client/Charge.test.ts +1 -1
  138. package/src/stripe/internal/types.ts +5 -1
  139. package/src/stripe/server/Charge.test.ts +53 -2
  140. package/src/stripe/server/Charge.ts +12 -4
  141. package/src/tempo/Attribution.test.ts +1 -1
  142. package/src/tempo/Methods.test.ts +1 -1
  143. package/src/tempo/client/ChannelOps.test.ts +6 -3
  144. package/src/tempo/client/Session.test.ts +5 -2
  145. package/src/tempo/client/SessionManager.test.ts +1 -1
  146. package/src/tempo/internal/auto-swap.test.ts +1 -1
  147. package/src/tempo/internal/defaults.test.ts +1 -1
  148. package/src/tempo/internal/fee-payer.test.ts +1 -1
  149. package/src/tempo/server/Charge.test.ts +1 -1
  150. package/src/tempo/server/Session.test.ts +116 -37
  151. package/src/tempo/server/Session.ts +32 -11
  152. package/src/tempo/server/Sse.test.ts +1 -1
  153. package/src/tempo/server/internal/transport.test.ts +24 -1
  154. package/src/tempo/server/internal/transport.ts +11 -0
  155. package/src/tempo/session/Chain.test.ts +5 -2
  156. package/src/tempo/session/Chain.ts +1 -1
  157. package/src/tempo/session/Channel.test.ts +1 -1
  158. package/src/tempo/session/ChannelStore.test.ts +1 -1
  159. package/src/tempo/session/Receipt.test.ts +1 -1
  160. package/src/tempo/session/Sse.fuzz.test.ts +138 -0
  161. package/src/tempo/session/Sse.test.ts +1 -1
  162. package/src/tempo/session/Voucher.test.ts +1 -1
  163. package/src/viem/Account.test.ts +1 -1
  164. package/src/viem/Client.test.ts +1 -1
  165. package/src/zod.test.ts +147 -0
@@ -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, payment } from 'mppx/express'
4
+ import { Mppx, discovery, payment } from 'mppx/express'
5
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,
@@ -165,7 +193,7 @@ describe('payment', () => {
165
193
  tempo_server({
166
194
  getClient: () => client,
167
195
  currency: asset,
168
- recipient: accounts[0].address,
196
+ account: accounts[0],
169
197
  }),
170
198
  ],
171
199
  secretKey,
@@ -1,10 +1,12 @@
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'
7
8
 
9
+ import { generate, type GenerateConfig, type RouteConfig } from '../discovery/OpenApi.js'
8
10
  import * as Mppx_core from '../server/Mppx.js'
9
11
  import * as Mppx_internal from './internal/mppx.js'
10
12
 
@@ -84,3 +86,35 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
84
86
  next()
85
87
  }
86
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
+ }
@@ -2,11 +2,11 @@ import { serve } from '@hono/node-server'
2
2
  import { Hono } from 'hono'
3
3
  import { Receipt } from 'mppx'
4
4
  import { Mppx as Mppx_client, session as sessionIntent, tempo as tempo_client } from 'mppx/client'
5
- import { Mppx } from 'mppx/hono'
5
+ import { Mppx, discovery } from 'mppx/hono'
6
6
  import { tempo as tempo_server } from 'mppx/server'
7
7
  import type { Address } from 'viem'
8
8
  import { Addresses } from 'viem/tempo'
9
- import { beforeAll, describe, expect, test } from 'vitest'
9
+ import { beforeAll, describe, expect, test } from 'vp/test'
10
10
  import { deployEscrow } from '~test/tempo/session.js'
11
11
  import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
12
12
 
@@ -29,7 +29,7 @@ describe('charge', () => {
29
29
  tempo_server.charge({
30
30
  getClient: () => client,
31
31
  currency: asset,
32
- recipient: accounts[0].address,
32
+ account: accounts[0],
33
33
  }),
34
34
  ],
35
35
  secretKey,
@@ -74,6 +74,28 @@ describe('charge', () => {
74
74
 
75
75
  server.close()
76
76
  })
77
+
78
+ test('serves /openapi.json via auto discovery', async () => {
79
+ const app = new Hono()
80
+ app.get('/', mppx.charge({ amount: '1' }), (c) => c.json({ fortune: 'You will be rich' }))
81
+ discovery(app, mppx, { auto: true, info: { title: 'Auto API', version: '2.0.0' } })
82
+
83
+ const server = await createServer(app)
84
+ const response = await globalThis.fetch(`${server.url}/openapi.json`)
85
+ expect(response.status).toBe(200)
86
+ expect(response.headers.get('cache-control')).toBe('public, max-age=300')
87
+
88
+ const body = (await response.json()) as Record<string, any>
89
+ expect(body.info).toEqual({ title: 'Auto API', version: '2.0.0' })
90
+ expect(body.paths['/'].get['x-payment-info']).toMatchObject({
91
+ amount: '1000000',
92
+ currency: asset,
93
+ intent: 'charge',
94
+ method: 'tempo',
95
+ })
96
+
97
+ server.close()
98
+ })
77
99
  })
78
100
 
79
101
  describe('session', () => {
@@ -90,7 +112,7 @@ describe('session', () => {
90
112
  methods: [
91
113
  tempo_server.session({
92
114
  getClient: () => client,
93
- recipient: accounts[0].address,
115
+ account: accounts[0],
94
116
  currency: asset,
95
117
  escrowContract,
96
118
  }),
@@ -116,10 +138,10 @@ describe('session', () => {
116
138
  methods: [
117
139
  tempo_server.session({
118
140
  getClient: () => client,
119
- recipient: accounts[0].address,
141
+ account: accounts[0],
120
142
  currency: asset,
121
143
  escrowContract,
122
- feePayer: accounts[0],
144
+ feePayer: true,
123
145
  }),
124
146
  ],
125
147
  secretKey,
@@ -1,5 +1,6 @@
1
- import type { MiddlewareHandler } from 'hono'
1
+ import type { Hono, MiddlewareHandler } from 'hono'
2
2
 
3
+ import { generate, type GenerateConfig, type RouteConfig } from '../discovery/OpenApi.js'
3
4
  import * as Mppx_core from '../server/Mppx.js'
4
5
  import * as Mppx_internal from './internal/mppx.js'
5
6
 
@@ -61,3 +62,74 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
61
62
  c.res = result.withReceipt(c.res)
62
63
  }
63
64
  }
65
+
66
+ export type DiscoveryConfig = Omit<GenerateConfig, 'routes'> & {
67
+ auto?: boolean
68
+ path?: string
69
+ routes?: RouteConfig[]
70
+ }
71
+
72
+ const discoveryHeaders = { 'Cache-Control': 'public, max-age=300' }
73
+
74
+ /**
75
+ * Mounts a `GET /openapi.json` route that serves an OpenAPI discovery document.
76
+ *
77
+ * When `auto` is true, routes are introspected from Hono's internal `app.routes`
78
+ * array. This is a **best-effort / experimental** convenience — `app.routes` is
79
+ * not part of Hono's stable public API and may change across versions. Prefer
80
+ * passing explicit `routes` for production use.
81
+ */
82
+ export function discovery(
83
+ app: Hono<any>,
84
+ mppx: { methods: readonly Mppx_internal.AnyServer[]; realm: string },
85
+ config: DiscoveryConfig = {},
86
+ ): void {
87
+ const mountPath = config.path ?? '/openapi.json'
88
+
89
+ let cached: string | undefined
90
+
91
+ app.get(mountPath, (c) => {
92
+ if (!cached) {
93
+ const routes = config.routes ?? (config.auto ? introspectRoutes(app) : [])
94
+ const doc = generate(mppx, {
95
+ ...(config.info ? { info: config.info } : {}),
96
+ routes,
97
+ ...(config.serviceInfo ? { serviceInfo: config.serviceInfo } : {}),
98
+ })
99
+ cached = JSON.stringify(doc)
100
+ }
101
+
102
+ c.header('Cache-Control', discoveryHeaders['Cache-Control'])
103
+ c.header('Content-Type', 'application/json')
104
+ return c.body(cached)
105
+ })
106
+ }
107
+
108
+ function introspectRoutes(app: Hono<any>): RouteConfig[] {
109
+ const routes: RouteConfig[] = []
110
+ const appRoutes = (app as any).routes as
111
+ | { handler: any; method: string; path: string }[]
112
+ | undefined
113
+
114
+ if (!appRoutes) return routes
115
+
116
+ const seen = new Set<string>()
117
+
118
+ for (const route of appRoutes) {
119
+ const internal = (route.handler as { _internal?: Record<string, unknown> } | undefined)
120
+ ?._internal
121
+ if (!internal) continue
122
+
123
+ const key = `${route.method}:${route.path}:${internal.name}/${internal.intent}`
124
+ if (seen.has(key)) continue
125
+ seen.add(key)
126
+
127
+ routes.push({
128
+ handler: route.handler,
129
+ method: route.method,
130
+ path: route.path,
131
+ })
132
+ }
133
+
134
+ return routes
135
+ }
@@ -1,6 +1,6 @@
1
1
  import { Challenge, Credential, Method, z } from 'mppx'
2
2
  import { Mppx } from 'mppx/server'
3
- import { describe, expect, test } from 'vitest'
3
+ import { describe, expect, test } from 'vp/test'
4
4
 
5
5
  import { wrap } from './mppx.js'
6
6
 
@@ -1,13 +1,16 @@
1
+ import type { DiscoveryHandler } from '../../discovery/OpenApi.js'
1
2
  import type * as Method from '../../Method.js'
2
3
  import type * as Mppx from '../../server/Mppx.js'
3
4
 
4
5
  export type AnyMethodFn = Mppx.AnyMethodFn
5
6
  export type AnyServer = Method.AnyServer
6
7
 
8
+ type DiscoveryMeta = Pick<DiscoveryHandler, '_internal'>
9
+
7
10
  /** Recursively wraps nested handler objects one level deep. */
8
11
  type WrapNested<obj, handler> = {
9
12
  [key in keyof obj]: obj[key] extends (options: infer options) => any
10
- ? (o: options) => handler
13
+ ? (o: options) => handler & DiscoveryMeta
11
14
  : obj[key]
12
15
  }
13
16
 
@@ -21,7 +24,7 @@ export type Wrap<mppx, handler> = {
21
24
  | 'transport'
22
25
  ? mppx[key]
23
26
  : mppx[key] extends (options: infer options) => any
24
- ? (o: options) => handler
27
+ ? (o: options) => handler & DiscoveryMeta
25
28
  : mppx[key] extends Record<string, (options: any) => any>
26
29
  ? WrapNested<mppx[key], handler>
27
30
  : mppx[key]
@@ -43,14 +46,19 @@ export function wrap<mppx extends Mppx.Mppx<any, any>, handler>(
43
46
  for (const mi of mppx.methods as readonly Method.AnyServer[]) {
44
47
  const key = `${mi.name}/${mi.intent}`
45
48
  const methodFn = (mppx as any)[key]
46
- result[key] = (options: any) => wrapper(methodFn, options)
49
+ const wrapWithMeta = (options: any) => {
50
+ const configured = methodFn(options)
51
+ const handler = wrapper(methodFn, options) as any
52
+ if (configured._internal) handler._internal = configured._internal
53
+ return handler
54
+ }
55
+ result[key] = wrapWithMeta
47
56
  // Also set shorthand intent key if Mppx registered it (no collision)
48
- if ((mppx as any)[mi.intent]) result[mi.intent] = (options: any) => wrapper(methodFn, options)
57
+ if ((mppx as any)[mi.intent]) result[mi.intent] = wrapWithMeta
49
58
  // Build nested handlers: wrapped.tempo.charge(...)
50
59
  if (!result[mi.name] || typeof result[mi.name] !== 'object')
51
60
  result[mi.name] = {} as Record<string, unknown>
52
- ;(result[mi.name] as Record<string, unknown>)[mi.intent] = (options: any) =>
53
- wrapper(methodFn, options)
61
+ ;(result[mi.name] as Record<string, unknown>)[mi.intent] = wrapWithMeta
54
62
  }
55
63
  return result as never
56
64
  }
@@ -2,11 +2,11 @@ import * as http from 'node:http'
2
2
 
3
3
  import { Receipt } from 'mppx'
4
4
  import { Mppx as Mppx_client, session as sessionIntent, tempo as tempo_client } from 'mppx/client'
5
- import { Mppx } from 'mppx/nextjs'
5
+ import { Mppx, discovery } from 'mppx/nextjs'
6
6
  import { tempo as tempo_server } from 'mppx/server'
7
7
  import type { Address } from 'viem'
8
8
  import { Addresses } from 'viem/tempo'
9
- import { beforeAll, describe, expect, test } from 'vitest'
9
+ import { beforeAll, describe, expect, test } from 'vp/test'
10
10
  import { deployEscrow } from '~test/tempo/session.js'
11
11
  import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
12
12
 
@@ -42,7 +42,7 @@ describe('charge', () => {
42
42
  tempo_server.charge({
43
43
  getClient: () => client,
44
44
  currency: asset,
45
- recipient: accounts[0].address,
45
+ account: accounts[0],
46
46
  }),
47
47
  ],
48
48
  secretKey,
@@ -89,6 +89,31 @@ describe('charge', () => {
89
89
 
90
90
  server.close()
91
91
  })
92
+
93
+ test('serves /openapi.json from a handler-derived route config', async () => {
94
+ const pay = mppx.charge({ amount: '1' })
95
+ const server = await createServer(
96
+ discovery(mppx, {
97
+ info: { title: 'Next API', version: '3.0.0' },
98
+ routes: [{ handler: pay, method: 'get', path: '/' }],
99
+ }),
100
+ )
101
+
102
+ const response = await globalThis.fetch(server.url)
103
+ expect(response.status).toBe(200)
104
+ expect(response.headers.get('cache-control')).toBe('public, max-age=300')
105
+
106
+ const body = (await response.json()) as Record<string, any>
107
+ expect(body.info).toEqual({ title: 'Next API', version: '3.0.0' })
108
+ expect(body.paths['/'].get['x-payment-info']).toMatchObject({
109
+ amount: '1000000',
110
+ currency: asset,
111
+ intent: 'charge',
112
+ method: 'tempo',
113
+ })
114
+
115
+ server.close()
116
+ })
92
117
  })
93
118
 
94
119
  describe('session', () => {
@@ -105,7 +130,7 @@ describe('session', () => {
105
130
  methods: [
106
131
  tempo_server.session({
107
132
  getClient: () => client,
108
- recipient: accounts[0].address,
133
+ account: accounts[0],
109
134
  currency: asset,
110
135
  escrowContract,
111
136
  }),
@@ -130,10 +155,10 @@ describe('session', () => {
130
155
  methods: [
131
156
  tempo_server.session({
132
157
  getClient: () => client,
133
- recipient: accounts[0].address,
158
+ account: accounts[0],
134
159
  currency: asset,
135
160
  escrowContract,
136
- feePayer: accounts[0],
161
+ feePayer: true,
137
162
  }),
138
163
  ],
139
164
  secretKey,
@@ -1,3 +1,4 @@
1
+ import { generate, type GenerateConfig, type RouteConfig } from '../discovery/OpenApi.js'
1
2
  import * as Mppx_core from '../server/Mppx.js'
2
3
  import * as Mppx_internal from './internal/mppx.js'
3
4
 
@@ -64,3 +65,30 @@ export function payment<const intent extends Mppx_internal.AnyMethodFn>(
64
65
  return result.withReceipt(response)
65
66
  }
66
67
  }
68
+
69
+ export type DiscoveryConfig = Omit<GenerateConfig, 'routes'> & {
70
+ routes?: RouteConfig[]
71
+ }
72
+
73
+ const discoveryHeaders = { 'Cache-Control': 'public, max-age=300' }
74
+
75
+ /**
76
+ * Creates a route handler that serves an OpenAPI discovery document.
77
+ */
78
+ export function discovery(
79
+ mppx: { methods: readonly Mppx_internal.AnyServer[]; realm: string },
80
+ config: DiscoveryConfig = {},
81
+ ): RouteHandler {
82
+ const cached = JSON.stringify(
83
+ generate(mppx, {
84
+ ...(config.info ? { info: config.info } : {}),
85
+ routes: config.routes ?? [],
86
+ ...(config.serviceInfo ? { serviceInfo: config.serviceInfo } : {}),
87
+ }),
88
+ )
89
+
90
+ return () =>
91
+ new Response(cached, {
92
+ headers: { ...discoveryHeaders, 'Content-Type': 'application/json' },
93
+ })
94
+ }