mppx 0.6.3 → 0.6.6
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 +20 -0
- package/dist/Credential.d.ts +4 -0
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js +18 -2
- package/dist/Credential.js.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +166 -54
- package/dist/cli/cli.js.map +1 -1
- package/dist/discovery/Discovery.d.ts +234 -18
- package/dist/discovery/Discovery.d.ts.map +1 -1
- package/dist/discovery/Discovery.js +24 -2
- package/dist/discovery/Discovery.js.map +1 -1
- package/dist/discovery/OpenApi.d.ts +1 -1
- package/dist/discovery/OpenApi.d.ts.map +1 -1
- package/dist/discovery/OpenApi.js +2 -1
- package/dist/discovery/OpenApi.js.map +1 -1
- package/dist/discovery/Validate.d.ts.map +1 -1
- package/dist/discovery/Validate.js +2 -1
- package/dist/discovery/Validate.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/client/Charge.js +1 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/internal/proof.d.ts +6 -2
- package/dist/tempo/internal/proof.d.ts.map +1 -1
- package/dist/tempo/internal/proof.js +7 -4
- package/dist/tempo/internal/proof.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +4 -3
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/package.json +2 -2
- package/src/Challenge.test.ts +45 -0
- package/src/Credential.test.ts +66 -0
- package/src/Credential.ts +23 -3
- package/src/bin.ts +1 -1
- package/src/cli/cli.ts +194 -58
- package/src/cli/mcp.test.ts +233 -0
- package/src/discovery/Discovery.test.ts +66 -4
- package/src/discovery/Discovery.ts +40 -7
- package/src/discovery/OpenApi.test.ts +61 -33
- package/src/discovery/OpenApi.ts +2 -2
- package/src/discovery/Validate.test.ts +117 -0
- package/src/discovery/Validate.ts +2 -1
- package/src/middlewares/elysia.test.ts +1 -1
- package/src/middlewares/express.test.ts +1 -1
- package/src/middlewares/hono.test.ts +1 -1
- package/src/middlewares/nextjs.test.ts +1 -1
- package/src/proxy/Proxy.test.ts +3 -3
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/client/Charge.ts +1 -1
- package/src/tempo/internal/proof.test.ts +11 -5
- package/src/tempo/internal/proof.ts +7 -4
- package/src/tempo/server/Charge.test.ts +51 -15
- package/src/tempo/server/Charge.ts +5 -3
- package/src/tempo/server/internal/html.gen.ts +1 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
2
|
+
import * as fs from 'node:fs'
|
|
3
|
+
import * as os from 'node:os'
|
|
4
|
+
import * as path from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { afterEach, expect, test } from 'vp/test'
|
|
7
|
+
|
|
8
|
+
const cwd = path.resolve(import.meta.dirname, '../..')
|
|
9
|
+
const binPath = path.join(cwd, 'src/bin.ts')
|
|
10
|
+
const children = new Set<ChildProcessWithoutNullStreams>()
|
|
11
|
+
const homes = new Set<string>()
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
for (const child of children) {
|
|
15
|
+
if (!child.killed) child.kill('SIGTERM')
|
|
16
|
+
}
|
|
17
|
+
children.clear()
|
|
18
|
+
for (const home of homes) fs.rmSync(home, { force: true, recursive: true })
|
|
19
|
+
homes.clear()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function startMcpServer() {
|
|
23
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-mcp-home-'))
|
|
24
|
+
homes.add(home)
|
|
25
|
+
const child = spawn(process.execPath, ['--import', 'tsx', binPath, '--mcp'], {
|
|
26
|
+
cwd,
|
|
27
|
+
env: {
|
|
28
|
+
...process.env,
|
|
29
|
+
HOME: home,
|
|
30
|
+
NODE_NO_WARNINGS: '1',
|
|
31
|
+
XDG_DATA_HOME: path.join(home, '.local', 'share'),
|
|
32
|
+
},
|
|
33
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
34
|
+
})
|
|
35
|
+
children.add(child)
|
|
36
|
+
|
|
37
|
+
const client = createLineClient(child)
|
|
38
|
+
return { child, client, home }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createLineClient(child: ChildProcessWithoutNullStreams) {
|
|
42
|
+
let buffer = ''
|
|
43
|
+
const messages: any[] = []
|
|
44
|
+
const nonJsonLines: string[] = []
|
|
45
|
+
let stderr = ''
|
|
46
|
+
|
|
47
|
+
child.stdout.setEncoding('utf8')
|
|
48
|
+
child.stderr.setEncoding('utf8')
|
|
49
|
+
child.stdout.on('data', (chunk) => {
|
|
50
|
+
buffer += chunk
|
|
51
|
+
for (;;) {
|
|
52
|
+
const index = buffer.indexOf('\n')
|
|
53
|
+
if (index === -1) break
|
|
54
|
+
const line = buffer.slice(0, index)
|
|
55
|
+
buffer = buffer.slice(index + 1)
|
|
56
|
+
if (!line.trim()) continue
|
|
57
|
+
try {
|
|
58
|
+
messages.push(JSON.parse(line))
|
|
59
|
+
} catch {
|
|
60
|
+
nonJsonLines.push(line)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
child.stderr.on('data', (chunk) => {
|
|
65
|
+
stderr += chunk
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
get nonJsonLines() {
|
|
70
|
+
return nonJsonLines
|
|
71
|
+
},
|
|
72
|
+
get stderr() {
|
|
73
|
+
return stderr
|
|
74
|
+
},
|
|
75
|
+
notify(method: string, params: Record<string, unknown> = {}) {
|
|
76
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`)
|
|
77
|
+
},
|
|
78
|
+
request(id: number, method: string, params: Record<string, unknown>) {
|
|
79
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`)
|
|
80
|
+
},
|
|
81
|
+
async waitFor(id: number) {
|
|
82
|
+
const started = Date.now()
|
|
83
|
+
while (Date.now() - started < 5_000) {
|
|
84
|
+
const message = messages.find((candidate) => candidate.id === id)
|
|
85
|
+
if (message) return message
|
|
86
|
+
if (child.exitCode !== null)
|
|
87
|
+
throw new Error(`MCP server exited before response ${id}: ${child.exitCode}`)
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
89
|
+
}
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Timed out waiting for MCP response ${id}. stderr=${stderr} nonJson=${JSON.stringify(
|
|
92
|
+
nonJsonLines,
|
|
93
|
+
)}`,
|
|
94
|
+
)
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function initialize(client: ReturnType<typeof createLineClient>) {
|
|
100
|
+
client.request(1, 'initialize', {
|
|
101
|
+
capabilities: {},
|
|
102
|
+
clientInfo: { name: 'mppx-test', version: '0' },
|
|
103
|
+
protocolVersion: '2025-03-26',
|
|
104
|
+
})
|
|
105
|
+
const response = await client.waitFor(1)
|
|
106
|
+
client.notify('notifications/initialized')
|
|
107
|
+
return response
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeDiscoveryDocument() {
|
|
111
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-mcp-discovery-'))
|
|
112
|
+
homes.add(dir)
|
|
113
|
+
const file = path.join(dir, 'openapi.json')
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
file,
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
info: { title: 'MCP Test', version: '1.0.0' },
|
|
118
|
+
openapi: '3.1.0',
|
|
119
|
+
paths: {
|
|
120
|
+
'/search': {
|
|
121
|
+
post: {
|
|
122
|
+
'x-payment-info': { amount: '100', intent: 'charge', method: 'tempo' },
|
|
123
|
+
requestBody: {
|
|
124
|
+
content: { 'application/json': { schema: { type: 'object' } } },
|
|
125
|
+
},
|
|
126
|
+
responses: {
|
|
127
|
+
'200': { description: 'OK' },
|
|
128
|
+
'402': { description: 'Payment Required' },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
135
|
+
return file
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function callTool(
|
|
139
|
+
client: ReturnType<typeof createLineClient>,
|
|
140
|
+
id: number,
|
|
141
|
+
name: string,
|
|
142
|
+
args: Record<string, unknown> = {},
|
|
143
|
+
) {
|
|
144
|
+
client.request(id, 'tools/call', { arguments: args, name })
|
|
145
|
+
const response = await client.waitFor(id)
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
147
|
+
expect(client.nonJsonLines).toEqual([])
|
|
148
|
+
return response
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
test('mppx --mcp stays alive long enough to handle initialize', async () => {
|
|
152
|
+
const { client } = startMcpServer()
|
|
153
|
+
|
|
154
|
+
const response = await initialize(client)
|
|
155
|
+
|
|
156
|
+
expect(response.result.serverInfo.name).toBe('mppx')
|
|
157
|
+
expect(client.nonJsonLines).toEqual([])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('tools/list exposes mppx commands with input and output schemas', async () => {
|
|
161
|
+
const { client } = startMcpServer()
|
|
162
|
+
await initialize(client)
|
|
163
|
+
|
|
164
|
+
client.request(2, 'tools/list', {})
|
|
165
|
+
const response = await client.waitFor(2)
|
|
166
|
+
const tools = response.result.tools
|
|
167
|
+
|
|
168
|
+
expect(tools.map((tool: { name: string }) => tool.name)).toEqual([
|
|
169
|
+
'account_create',
|
|
170
|
+
'account_default',
|
|
171
|
+
'account_delete',
|
|
172
|
+
'account_export',
|
|
173
|
+
'account_fund',
|
|
174
|
+
'account_list',
|
|
175
|
+
'account_view',
|
|
176
|
+
'discover_generate',
|
|
177
|
+
'discover_validate',
|
|
178
|
+
'init',
|
|
179
|
+
'sign',
|
|
180
|
+
])
|
|
181
|
+
expect(tools.find((tool: { name: string }) => tool.name === 'account_list').outputSchema).toEqual(
|
|
182
|
+
expect.objectContaining({
|
|
183
|
+
properties: expect.objectContaining({ accounts: expect.any(Object) }),
|
|
184
|
+
type: 'object',
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
expect(tools.find((tool: { name: string }) => tool.name === 'sign').inputSchema).toEqual(
|
|
188
|
+
expect.objectContaining({
|
|
189
|
+
properties: expect.objectContaining({ challenge: expect.any(Object) }),
|
|
190
|
+
type: 'object',
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
expect(client.nonJsonLines).toEqual([])
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('MCP tool calls return structured data without raw stdout lines', async () => {
|
|
197
|
+
const { client } = startMcpServer()
|
|
198
|
+
await initialize(client)
|
|
199
|
+
|
|
200
|
+
const response = await callTool(client, 2, 'account_list')
|
|
201
|
+
|
|
202
|
+
expect(response.result.content[0].text).not.toBe('null')
|
|
203
|
+
expect(JSON.parse(response.result.content[0].text)).toEqual({ accounts: [] })
|
|
204
|
+
expect(response.result.structuredContent).toEqual({ accounts: [] })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('MCP session survives mixed success and error tool calls without stdout pollution', async () => {
|
|
208
|
+
const { client } = startMcpServer()
|
|
209
|
+
await initialize(client)
|
|
210
|
+
|
|
211
|
+
const discovery = await callTool(client, 2, 'discover_validate', {
|
|
212
|
+
input: writeDiscoveryDocument(),
|
|
213
|
+
})
|
|
214
|
+
expect(JSON.parse(discovery.result.content[0].text)).toEqual({
|
|
215
|
+
errorCount: 0,
|
|
216
|
+
issues: [],
|
|
217
|
+
valid: true,
|
|
218
|
+
warningCount: 0,
|
|
219
|
+
})
|
|
220
|
+
expect(discovery.result.structuredContent).toEqual({
|
|
221
|
+
errorCount: 0,
|
|
222
|
+
issues: [],
|
|
223
|
+
valid: true,
|
|
224
|
+
warningCount: 0,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const sign = await callTool(client, 3, 'sign')
|
|
228
|
+
expect(sign.result.isError).toBe(true)
|
|
229
|
+
expect(sign.result.content[0].text).toContain('No challenge provided')
|
|
230
|
+
|
|
231
|
+
const accounts = await callTool(client, 4, 'account_list')
|
|
232
|
+
expect(JSON.parse(accounts.result.content[0].text)).toEqual({ accounts: [] })
|
|
233
|
+
})
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { DiscoveryDocument, PaymentInfo, ServiceInfo } from './Discovery.js'
|
|
2
2
|
|
|
3
3
|
describe('PaymentInfo', () => {
|
|
4
|
-
test('
|
|
4
|
+
test('normalizes legacy shorthand to offers', () => {
|
|
5
5
|
const result = PaymentInfo.safeParse({
|
|
6
6
|
amount: '1000',
|
|
7
7
|
intent: 'charge',
|
|
8
8
|
method: 'tempo',
|
|
9
9
|
})
|
|
10
10
|
expect(result.success).toBe(true)
|
|
11
|
-
expect(result.data).toEqual({
|
|
11
|
+
expect(result.data).toEqual({
|
|
12
|
+
offers: [{ amount: '1000', intent: 'charge', method: 'tempo' }],
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('parses offers format without modification', () => {
|
|
17
|
+
const result = PaymentInfo.safeParse({
|
|
18
|
+
offers: [{ amount: '1000', intent: 'charge', method: 'tempo' }],
|
|
19
|
+
})
|
|
20
|
+
expect(result.success).toBe(true)
|
|
21
|
+
expect(result.data).toEqual({
|
|
22
|
+
offers: [{ amount: '1000', intent: 'charge', method: 'tempo' }],
|
|
23
|
+
})
|
|
12
24
|
})
|
|
13
25
|
|
|
14
26
|
test('parses a session with null amount', () => {
|
|
@@ -18,7 +30,7 @@ describe('PaymentInfo', () => {
|
|
|
18
30
|
method: 'tempo',
|
|
19
31
|
})
|
|
20
32
|
expect(result.success).toBe(true)
|
|
21
|
-
expect(result.data?.amount).toBeNull()
|
|
33
|
+
expect(result.data?.offers[0]?.amount).toBeNull()
|
|
22
34
|
})
|
|
23
35
|
|
|
24
36
|
test('accepts custom intents', () => {
|
|
@@ -28,7 +40,7 @@ describe('PaymentInfo', () => {
|
|
|
28
40
|
method: 'tempo',
|
|
29
41
|
})
|
|
30
42
|
expect(result.success).toBe(true)
|
|
31
|
-
expect(result.data?.intent).toBe('subscribe')
|
|
43
|
+
expect(result.data?.offers[0]?.intent).toBe('subscribe')
|
|
32
44
|
})
|
|
33
45
|
|
|
34
46
|
test('rejects invalid amount pattern', () => {
|
|
@@ -40,6 +52,26 @@ describe('PaymentInfo', () => {
|
|
|
40
52
|
expect(result.success).toBe(false)
|
|
41
53
|
})
|
|
42
54
|
|
|
55
|
+
test('rejects mixed shorthand and offers shapes', () => {
|
|
56
|
+
const result = PaymentInfo.safeParse({
|
|
57
|
+
amount: '100',
|
|
58
|
+
offers: [{ amount: '100', intent: 'charge', method: 'tempo' }],
|
|
59
|
+
})
|
|
60
|
+
expect(result.success).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('rejects empty offers arrays', () => {
|
|
64
|
+
const result = PaymentInfo.safeParse({ offers: [] })
|
|
65
|
+
expect(result.success).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('rejects malformed offers', () => {
|
|
69
|
+
const result = PaymentInfo.safeParse({
|
|
70
|
+
offers: [{ amount: '01', intent: 'charge', method: 'tempo' }],
|
|
71
|
+
})
|
|
72
|
+
expect(result.success).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
|
|
43
75
|
test('accepts x402 format with unknown fields', () => {
|
|
44
76
|
const result = PaymentInfo.safeParse({
|
|
45
77
|
price: '0.54',
|
|
@@ -47,6 +79,9 @@ describe('PaymentInfo', () => {
|
|
|
47
79
|
protocols: ['x402', 'mpp'],
|
|
48
80
|
})
|
|
49
81
|
expect(result.success).toBe(true)
|
|
82
|
+
expect(result.data).toEqual({
|
|
83
|
+
offers: [{ price: '0.54', pricingMode: 'fixed', protocols: ['x402', 'mpp'] }],
|
|
84
|
+
})
|
|
50
85
|
})
|
|
51
86
|
})
|
|
52
87
|
|
|
@@ -118,6 +153,33 @@ describe('DiscoveryDocument', () => {
|
|
|
118
153
|
},
|
|
119
154
|
})
|
|
120
155
|
expect(result.success).toBe(true)
|
|
156
|
+
expect(result.data?.paths?.['/search']?.post?.['x-payment-info']).toEqual({
|
|
157
|
+
offers: [{ amount: '100', intent: 'charge', method: 'tempo' }],
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('normalizes offers-based discovery documents', () => {
|
|
162
|
+
const result = DiscoveryDocument.safeParse({
|
|
163
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
164
|
+
openapi: '3.1.0',
|
|
165
|
+
paths: {
|
|
166
|
+
'/search': {
|
|
167
|
+
post: {
|
|
168
|
+
'x-payment-info': {
|
|
169
|
+
offers: [{ amount: '100', intent: 'charge', method: 'tempo' }],
|
|
170
|
+
},
|
|
171
|
+
responses: {
|
|
172
|
+
'200': { description: 'OK' },
|
|
173
|
+
'402': { description: 'Payment Required' },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
expect(result.success).toBe(true)
|
|
180
|
+
expect(result.data?.paths?.['/search']?.post?.['x-payment-info']).toEqual({
|
|
181
|
+
offers: [{ amount: '100', intent: 'charge', method: 'tempo' }],
|
|
182
|
+
})
|
|
121
183
|
})
|
|
122
184
|
|
|
123
185
|
test('accepts path items with summary, parameters, and extensions', () => {
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import * as z from '../zod.js'
|
|
2
2
|
|
|
3
3
|
const uriOrPathPattern = /^([a-zA-Z][a-zA-Z\d+.-]*:\/\/\S+|\/\S*)$/
|
|
4
|
+
const paymentInfoFieldNames = new Set(['amount', 'currency', 'description', 'intent', 'method'])
|
|
4
5
|
|
|
5
6
|
function uriOrPath() {
|
|
6
7
|
return z.string().check(z.regex(uriOrPathPattern, 'Invalid URI or path'))
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
* Schema for the `x-payment-info` OpenAPI extension on an operation.
|
|
11
|
-
*
|
|
12
|
-
* Only validates spec-defined fields when present; unknown fields are ignored.
|
|
13
|
-
* Discovery is advisory only. Runtime 402 challenges remain authoritative.
|
|
14
|
-
*/
|
|
15
|
-
export const PaymentInfo = z.looseObject({
|
|
10
|
+
const PaymentOffer = z.looseObject({
|
|
16
11
|
amount: z.optional(
|
|
17
12
|
z.union([z.null(), z.string().check(z.regex(/^(0|[1-9][0-9]*)$/, 'Invalid amount'))]),
|
|
18
13
|
),
|
|
@@ -21,6 +16,44 @@ export const PaymentInfo = z.looseObject({
|
|
|
21
16
|
intent: z.optional(z.string()),
|
|
22
17
|
method: z.optional(z.string()),
|
|
23
18
|
})
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Schema for the `x-payment-info` OpenAPI extension on an operation.
|
|
22
|
+
*
|
|
23
|
+
* Only validates spec-defined fields when present; unknown fields are ignored.
|
|
24
|
+
* Discovery is advisory only. Runtime 402 challenges remain authoritative.
|
|
25
|
+
*/
|
|
26
|
+
export const PaymentInfo = z.pipe(
|
|
27
|
+
z
|
|
28
|
+
.looseObject({
|
|
29
|
+
amount: z.optional(
|
|
30
|
+
z.union([z.null(), z.string().check(z.regex(/^(0|[1-9][0-9]*)$/, 'Invalid amount'))]),
|
|
31
|
+
),
|
|
32
|
+
currency: z.optional(z.string()),
|
|
33
|
+
description: z.optional(z.string()),
|
|
34
|
+
intent: z.optional(z.string()),
|
|
35
|
+
method: z.optional(z.string()),
|
|
36
|
+
offers: z.optional(z.array(PaymentOffer).check(z.minLength(1))),
|
|
37
|
+
})
|
|
38
|
+
.check(
|
|
39
|
+
z.refine(
|
|
40
|
+
(value) =>
|
|
41
|
+
value.offers === undefined || Object.keys(value).every((key) => key === 'offers'),
|
|
42
|
+
'Cannot mix offers with flat payment info fields',
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
z.transform((value) => {
|
|
46
|
+
if (value.offers) return { offers: value.offers }
|
|
47
|
+
|
|
48
|
+
const offer: Record<string, unknown> = {}
|
|
49
|
+
for (const [key, field] of Object.entries(value)) {
|
|
50
|
+
if (key === 'offers') continue
|
|
51
|
+
if (paymentInfoFieldNames.has(key) || field !== undefined) offer[key] = field
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { offers: [offer] }
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
24
57
|
export type PaymentInfo = z.infer<typeof PaymentInfo>
|
|
25
58
|
|
|
26
59
|
const ServiceDocs = z.looseObject({
|
|
@@ -110,11 +110,15 @@ describe('generate', () => {
|
|
|
110
110
|
},
|
|
111
111
|
},
|
|
112
112
|
"x-payment-info": {
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
"offers": [
|
|
114
|
+
{
|
|
115
|
+
"amount": "100",
|
|
116
|
+
"currency": "0xUSDC",
|
|
117
|
+
"intent": "charge",
|
|
118
|
+
"method": "tempo",
|
|
119
|
+
"recipient": "0x123",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
118
122
|
},
|
|
119
123
|
},
|
|
120
124
|
},
|
|
@@ -161,11 +165,15 @@ describe('generate', () => {
|
|
|
161
165
|
},
|
|
162
166
|
},
|
|
163
167
|
"x-payment-info": {
|
|
164
|
-
"
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
"offers": [
|
|
169
|
+
{
|
|
170
|
+
"amount": "50",
|
|
171
|
+
"currency": "usd",
|
|
172
|
+
"intent": "charge",
|
|
173
|
+
"method": "tempo",
|
|
174
|
+
"recipient": "0x1",
|
|
175
|
+
},
|
|
176
|
+
],
|
|
169
177
|
},
|
|
170
178
|
},
|
|
171
179
|
},
|
|
@@ -206,10 +214,14 @@ describe('generate', () => {
|
|
|
206
214
|
},
|
|
207
215
|
},
|
|
208
216
|
"x-payment-info": {
|
|
209
|
-
"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
"offers": [
|
|
218
|
+
{
|
|
219
|
+
"amount": null,
|
|
220
|
+
"intent": "session",
|
|
221
|
+
"method": "tempo",
|
|
222
|
+
"recipient": "0x123",
|
|
223
|
+
},
|
|
224
|
+
],
|
|
213
225
|
},
|
|
214
226
|
},
|
|
215
227
|
},
|
|
@@ -305,11 +317,15 @@ describe('generate', () => {
|
|
|
305
317
|
},
|
|
306
318
|
},
|
|
307
319
|
"x-payment-info": {
|
|
308
|
-
"
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
320
|
+
"offers": [
|
|
321
|
+
{
|
|
322
|
+
"amount": "100",
|
|
323
|
+
"currency": "0xUSDC",
|
|
324
|
+
"intent": "charge",
|
|
325
|
+
"method": "tempo",
|
|
326
|
+
"recipient": "0xABC",
|
|
327
|
+
},
|
|
328
|
+
],
|
|
313
329
|
},
|
|
314
330
|
},
|
|
315
331
|
},
|
|
@@ -334,11 +350,15 @@ describe('generate', () => {
|
|
|
334
350
|
},
|
|
335
351
|
"summary": "Search the index",
|
|
336
352
|
"x-payment-info": {
|
|
337
|
-
"
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
353
|
+
"offers": [
|
|
354
|
+
{
|
|
355
|
+
"amount": "500",
|
|
356
|
+
"currency": "0xUSDC",
|
|
357
|
+
"intent": "charge",
|
|
358
|
+
"method": "tempo",
|
|
359
|
+
"recipient": "0xABC",
|
|
360
|
+
},
|
|
361
|
+
],
|
|
342
362
|
},
|
|
343
363
|
},
|
|
344
364
|
},
|
|
@@ -353,10 +373,14 @@ describe('generate', () => {
|
|
|
353
373
|
},
|
|
354
374
|
},
|
|
355
375
|
"x-payment-info": {
|
|
356
|
-
"
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
376
|
+
"offers": [
|
|
377
|
+
{
|
|
378
|
+
"amount": null,
|
|
379
|
+
"intent": "session",
|
|
380
|
+
"method": "tempo",
|
|
381
|
+
"recipient": "0xABC",
|
|
382
|
+
},
|
|
383
|
+
],
|
|
360
384
|
},
|
|
361
385
|
},
|
|
362
386
|
},
|
|
@@ -410,11 +434,15 @@ describe('generate', () => {
|
|
|
410
434
|
},
|
|
411
435
|
"summary": "Monthly subscription",
|
|
412
436
|
"x-payment-info": {
|
|
413
|
-
"
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
437
|
+
"offers": [
|
|
438
|
+
{
|
|
439
|
+
"amount": "100",
|
|
440
|
+
"intent": "subscribe",
|
|
441
|
+
"interval": "monthly",
|
|
442
|
+
"method": "tempo",
|
|
443
|
+
"recipient": "0xABC",
|
|
444
|
+
},
|
|
445
|
+
],
|
|
418
446
|
},
|
|
419
447
|
},
|
|
420
448
|
},
|
package/src/discovery/OpenApi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as Method from '../Method.js'
|
|
2
|
-
import type
|
|
2
|
+
import { PaymentInfo, type ServiceInfo } from './Discovery.js'
|
|
3
3
|
|
|
4
4
|
export type DiscoveryHandler = ((...args: any[]) => unknown) & {
|
|
5
5
|
_internal?: {
|
|
@@ -121,7 +121,7 @@ function createDocument(config: {
|
|
|
121
121
|
},
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
if (route.payment) operation['x-payment-info'] = route.payment
|
|
124
|
+
if (route.payment) operation['x-payment-info'] = PaymentInfo.parse(route.payment)
|
|
125
125
|
if (route.summary) operation.summary = route.summary
|
|
126
126
|
if (route.requestBody) operation.requestBody = route.requestBody
|
|
127
127
|
|