mppx 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +155 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/discovery/Discovery.d.ts +146 -0
- package/dist/discovery/Discovery.d.ts.map +1 -0
- package/dist/discovery/Discovery.js +60 -0
- package/dist/discovery/Discovery.js.map +1 -0
- package/dist/discovery/OpenApi.d.ts +61 -0
- package/dist/discovery/OpenApi.d.ts.map +1 -0
- package/dist/discovery/OpenApi.js +139 -0
- package/dist/discovery/OpenApi.js.map +1 -0
- package/dist/discovery/Validate.d.ts +10 -0
- package/dist/discovery/Validate.d.ts.map +1 -0
- package/dist/discovery/Validate.js +63 -0
- package/dist/discovery/Validate.js.map +1 -0
- package/dist/discovery/index.d.ts +4 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +4 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/middlewares/elysia.d.ts +52 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +17 -0
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts +13 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +18 -0
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts +19 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js +51 -0
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts +4 -2
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +10 -3
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/middlewares/nextjs.d.ts +11 -0
- package/dist/middlewares/nextjs.d.ts.map +1 -1
- package/dist/middlewares/nextjs.js +15 -0
- package/dist/middlewares/nextjs.js.map +1 -1
- package/dist/proxy/Proxy.d.ts +6 -0
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +56 -80
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.d.ts +16 -23
- package/dist/proxy/Service.d.ts.map +1 -1
- package/dist/proxy/Service.js +19 -83
- package/dist/proxy/Service.js.map +1 -1
- package/dist/proxy/internal/Route.js +1 -1
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/proxy/services/anthropic.d.ts.map +1 -1
- package/dist/proxy/services/anthropic.js +5 -0
- package/dist/proxy/services/anthropic.js.map +1 -1
- package/dist/proxy/services/openai.d.ts.map +1 -1
- package/dist/proxy/services/openai.js +6 -3
- package/dist/proxy/services/openai.js.map +1 -1
- package/dist/proxy/services/stripe.d.ts.map +1 -1
- package/dist/proxy/services/stripe.js +6 -3
- package/dist/proxy/services/stripe.js.map +1 -1
- package/dist/stripe/internal/types.d.ts +3 -0
- package/dist/stripe/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +9 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +25 -8
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +8 -0
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.js +1 -1
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +6 -1
- package/src/BodyDigest.test.ts +1 -1
- package/src/Challenge.fuzz.test.ts +121 -0
- package/src/Challenge.test-d.ts +1 -1
- package/src/Challenge.test.ts +1 -1
- package/src/Credential.fuzz.test.ts +62 -0
- package/src/Credential.test.ts +1 -1
- package/src/Errors.test.ts +1 -1
- package/src/Expires.test.ts +1 -1
- package/src/Method.test.ts +1 -1
- package/src/PaymentRequest.test.ts +1 -1
- package/src/Receipt.test.ts +1 -1
- package/src/Store.test-d.ts +1 -1
- package/src/Store.test.ts +1 -1
- package/src/cli/cli.test.ts +212 -1
- package/src/cli/cli.ts +162 -0
- package/src/client/Mppx.test-d.ts +1 -1
- package/src/client/Mppx.test.ts +1 -1
- package/src/client/Transport.test.ts +1 -1
- package/src/client/internal/Fetch.browser.test.ts +1 -1
- package/src/client/internal/Fetch.test-d.ts +1 -1
- package/src/client/internal/Fetch.test.ts +2 -1
- package/src/discovery/Discovery.test.ts +152 -0
- package/src/discovery/Discovery.ts +72 -0
- package/src/discovery/OpenApi.test.ts +425 -0
- package/src/discovery/OpenApi.ts +224 -0
- package/src/discovery/Validate.test.ts +188 -0
- package/src/discovery/Validate.ts +76 -0
- package/src/discovery/index.ts +3 -0
- package/src/internal/constantTimeEqual.test.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test.ts +1 -1
- package/src/mcp-sdk/server/Transport.test.ts +1 -1
- package/src/middlewares/elysia.test.ts +27 -2
- package/src/middlewares/elysia.ts +35 -1
- package/src/middlewares/express.test.ts +35 -7
- package/src/middlewares/express.ts +34 -0
- package/src/middlewares/hono.test.ts +28 -6
- package/src/middlewares/hono.ts +73 -1
- package/src/middlewares/internal/mppx.test.ts +1 -1
- package/src/middlewares/internal/mppx.ts +14 -6
- package/src/middlewares/nextjs.test.ts +31 -6
- package/src/middlewares/nextjs.ts +28 -0
- package/src/proxy/Proxy.test.ts +54 -270
- package/src/proxy/Proxy.ts +71 -93
- package/src/proxy/Service.test.ts +23 -1
- package/src/proxy/Service.ts +40 -86
- package/src/proxy/internal/Headers.test.ts +1 -1
- package/src/proxy/internal/Route.test.ts +9 -1
- package/src/proxy/internal/Route.ts +1 -1
- package/src/proxy/services/anthropic.test.ts +132 -0
- package/src/proxy/services/anthropic.ts +5 -0
- package/src/proxy/services/openai.test.ts +1 -1
- package/src/proxy/services/openai.ts +6 -4
- package/src/proxy/services/stripe.test.ts +132 -0
- package/src/proxy/services/stripe.ts +6 -4
- package/src/server/Mppx.test-d.ts +1 -1
- package/src/server/Mppx.test.ts +2 -1
- package/src/server/NodeListener.test.ts +1 -1
- package/src/server/Request.test.ts +1 -1
- package/src/server/Response.test.ts +1 -1
- package/src/server/Transport.test.ts +1 -1
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/Methods.test.ts +1 -1
- package/src/stripe/client/Charge.test.ts +1 -1
- package/src/stripe/internal/types.ts +5 -1
- package/src/stripe/server/Charge.test.ts +53 -2
- package/src/stripe/server/Charge.ts +12 -4
- package/src/tempo/Attribution.test.ts +1 -1
- package/src/tempo/Methods.test.ts +1 -1
- package/src/tempo/client/ChannelOps.test.ts +6 -3
- package/src/tempo/client/Session.test.ts +5 -2
- package/src/tempo/client/SessionManager.test.ts +1 -1
- package/src/tempo/internal/auto-swap.test.ts +1 -1
- package/src/tempo/internal/defaults.test.ts +1 -1
- package/src/tempo/internal/fee-payer.test.ts +1 -1
- package/src/tempo/server/Charge.test.ts +1 -1
- package/src/tempo/server/Session.test.ts +116 -37
- package/src/tempo/server/Session.ts +32 -11
- package/src/tempo/server/Sse.test.ts +1 -1
- package/src/tempo/server/internal/transport.test.ts +24 -1
- package/src/tempo/server/internal/transport.ts +11 -0
- package/src/tempo/session/Chain.test.ts +5 -2
- package/src/tempo/session/Chain.ts +1 -1
- package/src/tempo/session/Channel.test.ts +1 -1
- package/src/tempo/session/ChannelStore.test.ts +1 -1
- package/src/tempo/session/Receipt.test.ts +1 -1
- package/src/tempo/session/Sse.fuzz.test.ts +138 -0
- package/src/tempo/session/Sse.test.ts +1 -1
- package/src/tempo/session/Voucher.test.ts +1 -1
- package/src/viem/Account.test.ts +1 -1
- package/src/viem/Client.test.ts +1 -1
- package/src/zod.test.ts +147 -0
|
@@ -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 '
|
|
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,
|
|
@@ -165,7 +193,7 @@ describe('payment', () => {
|
|
|
165
193
|
tempo_server({
|
|
166
194
|
getClient: () => client,
|
|
167
195
|
currency: asset,
|
|
168
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
+
account: accounts[0],
|
|
120
142
|
currency: asset,
|
|
121
143
|
escrowContract,
|
|
122
|
-
feePayer:
|
|
144
|
+
feePayer: true,
|
|
123
145
|
}),
|
|
124
146
|
],
|
|
125
147
|
secretKey,
|
package/src/middlewares/hono.ts
CHANGED
|
@@ -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,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
|
-
|
|
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] =
|
|
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] =
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
account: accounts[0],
|
|
134
159
|
currency: asset,
|
|
135
160
|
escrowContract,
|
|
136
|
-
feePayer:
|
|
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
|
+
}
|