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,7 +1,7 @@
1
1
  import { Challenge, Credential, Mcp } from 'mppx'
2
2
  import { Transport } from 'mppx/client'
3
3
  import { Methods } from 'mppx/tempo'
4
- import { describe, expect, test } from 'vitest'
4
+ import { describe, expect, test } from 'vp/test'
5
5
 
6
6
  const realm = 'api.example.com'
7
7
  const secretKey = 'test-secret-key'
@@ -1,4 +1,4 @@
1
- import { describe, expect, test, vi } from 'vitest'
1
+ import { describe, expect, test, vi } from 'vp/test'
2
2
 
3
3
  import * as Fetch from './Fetch.js'
4
4
 
@@ -1,5 +1,5 @@
1
1
  import type { Account } from 'viem'
2
- import { describe, expectTypeOf, test } from 'vitest'
2
+ import { describe, expectTypeOf, test } from 'vp/test'
3
3
 
4
4
  import { charge } from '../../tempo/client/Charge.js'
5
5
  import * as Fetch from './Fetch.js'
@@ -2,7 +2,7 @@ import { Receipt } from 'mppx'
2
2
  import { tempo } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
4
  import { createClient, defineChain } from 'viem'
5
- import { describe, expect, test, vi } from 'vitest'
5
+ import { describe, expect, test, vi } from 'vp/test'
6
6
  import * as Http from '~test/Http.js'
7
7
  import { rpcUrl } from '~test/tempo/prool.js'
8
8
  import { accounts, asset, chain, client, http } from '~test/tempo/viem.js'
@@ -16,6 +16,7 @@ const server = Mppx_server.create({
16
16
  methods: [
17
17
  tempo_server({
18
18
  getClient: () => client,
19
+ account: accounts[0],
19
20
  }),
20
21
  ],
21
22
  realm,
@@ -0,0 +1,152 @@
1
+ import { DiscoveryDocument, PaymentInfo, ServiceInfo } from './Discovery.js'
2
+
3
+ describe('PaymentInfo', () => {
4
+ test('parses a valid charge payment info', () => {
5
+ const result = PaymentInfo.safeParse({
6
+ amount: '1000',
7
+ intent: 'charge',
8
+ method: 'tempo',
9
+ })
10
+ expect(result.success).toBe(true)
11
+ expect(result.data).toEqual({ amount: '1000', intent: 'charge', method: 'tempo' })
12
+ })
13
+
14
+ test('parses a session with null amount', () => {
15
+ const result = PaymentInfo.safeParse({
16
+ amount: null,
17
+ intent: 'session',
18
+ method: 'tempo',
19
+ })
20
+ expect(result.success).toBe(true)
21
+ expect(result.data?.amount).toBeNull()
22
+ })
23
+
24
+ test('accepts custom intents', () => {
25
+ const result = PaymentInfo.safeParse({
26
+ amount: '100',
27
+ intent: 'subscribe',
28
+ method: 'tempo',
29
+ })
30
+ expect(result.success).toBe(true)
31
+ expect(result.data?.intent).toBe('subscribe')
32
+ })
33
+
34
+ test('rejects invalid amount pattern', () => {
35
+ const result = PaymentInfo.safeParse({
36
+ amount: '01',
37
+ intent: 'charge',
38
+ method: 'tempo',
39
+ })
40
+ expect(result.success).toBe(false)
41
+ })
42
+
43
+ test('accepts x402 format with unknown fields', () => {
44
+ const result = PaymentInfo.safeParse({
45
+ price: '0.54',
46
+ pricingMode: 'fixed',
47
+ protocols: ['x402', 'mpp'],
48
+ })
49
+ expect(result.success).toBe(true)
50
+ })
51
+ })
52
+
53
+ describe('ServiceInfo', () => {
54
+ test('parses a full service info', () => {
55
+ const result = ServiceInfo.safeParse({
56
+ categories: ['ai', 'search'],
57
+ docs: {
58
+ apiReference: 'https://example.com/api',
59
+ homepage: 'https://example.com',
60
+ llms: 'https://example.com/llms.txt',
61
+ },
62
+ })
63
+ expect(result.success).toBe(true)
64
+ expect(result.data?.categories).toEqual(['ai', 'search'])
65
+ })
66
+
67
+ test('accepts relative paths for doc links', () => {
68
+ const result = ServiceInfo.safeParse({
69
+ docs: {
70
+ llms: '/llms.txt',
71
+ apiReference: '/docs/api',
72
+ },
73
+ })
74
+ expect(result.success).toBe(true)
75
+ expect(result.data?.docs?.llms).toBe('/llms.txt')
76
+ })
77
+
78
+ test('rejects invalid doc URIs', () => {
79
+ const result = ServiceInfo.safeParse({
80
+ docs: {
81
+ homepage: 'not-a-uri',
82
+ },
83
+ })
84
+ expect(result.success).toBe(false)
85
+ })
86
+ })
87
+
88
+ describe('DiscoveryDocument', () => {
89
+ test('parses a minimal document', () => {
90
+ const result = DiscoveryDocument.safeParse({
91
+ info: { title: 'Test', version: '1.0.0' },
92
+ openapi: '3.1.0',
93
+ })
94
+ expect(result.success).toBe(true)
95
+ })
96
+
97
+ test('parses a document with discovery extensions', () => {
98
+ const result = DiscoveryDocument.safeParse({
99
+ info: { title: 'Test', version: '1.0.0' },
100
+ openapi: '3.1.0',
101
+ paths: {
102
+ '/search': {
103
+ post: {
104
+ 'x-payment-info': {
105
+ amount: '100',
106
+ intent: 'charge',
107
+ method: 'tempo',
108
+ },
109
+ responses: {
110
+ '200': { description: 'OK' },
111
+ '402': { description: 'Payment Required' },
112
+ },
113
+ },
114
+ },
115
+ },
116
+ 'x-service-info': {
117
+ categories: ['search'],
118
+ },
119
+ })
120
+ expect(result.success).toBe(true)
121
+ })
122
+
123
+ test('accepts path items with summary, parameters, and extensions', () => {
124
+ const result = DiscoveryDocument.safeParse({
125
+ info: { title: 'Test', version: '1.0.0' },
126
+ openapi: '3.1.0',
127
+ paths: {
128
+ '/search': {
129
+ summary: 'Search endpoints',
130
+ parameters: [{ name: 'q', in: 'query' }],
131
+ 'x-custom': 'hello',
132
+ post: {
133
+ 'x-payment-info': {
134
+ amount: '100',
135
+ intent: 'charge',
136
+ method: 'tempo',
137
+ },
138
+ responses: { '402': { description: 'Payment Required' } },
139
+ },
140
+ },
141
+ },
142
+ })
143
+ expect(result.success).toBe(true)
144
+ })
145
+
146
+ test('rejects missing info', () => {
147
+ const result = DiscoveryDocument.safeParse({
148
+ openapi: '3.1.0',
149
+ })
150
+ expect(result.success).toBe(false)
151
+ })
152
+ })
@@ -0,0 +1,72 @@
1
+ import * as z from '../zod.js'
2
+
3
+ const uriOrPathPattern = /^([a-zA-Z][a-zA-Z\d+.-]*:\/\/\S+|\/\S*)$/
4
+
5
+ function uriOrPath() {
6
+ return z.string().check(z.regex(uriOrPathPattern, 'Invalid URI or path'))
7
+ }
8
+
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({
16
+ amount: z.optional(
17
+ z.union([z.null(), z.string().check(z.regex(/^(0|[1-9][0-9]*)$/, 'Invalid amount'))]),
18
+ ),
19
+ currency: z.optional(z.string()),
20
+ description: z.optional(z.string()),
21
+ intent: z.optional(z.string()),
22
+ method: z.optional(z.string()),
23
+ })
24
+ export type PaymentInfo = z.infer<typeof PaymentInfo>
25
+
26
+ const ServiceDocs = z.looseObject({
27
+ apiReference: z.optional(uriOrPath()),
28
+ homepage: z.optional(uriOrPath()),
29
+ llms: z.optional(uriOrPath()),
30
+ })
31
+
32
+ /**
33
+ * Schema for the `x-service-info` OpenAPI extension at the document root.
34
+ */
35
+ export const ServiceInfo = z.looseObject({
36
+ categories: z.optional(z.array(z.string())),
37
+ docs: z.optional(ServiceDocs),
38
+ })
39
+ export type ServiceInfo = z.infer<typeof ServiceInfo>
40
+
41
+ const OperationObject = z.looseObject({
42
+ 'x-payment-info': z.optional(PaymentInfo),
43
+ requestBody: z.optional(z.unknown()),
44
+ responses: z.optional(z.record(z.string(), z.unknown())),
45
+ summary: z.optional(z.string()),
46
+ })
47
+
48
+ const PathItem = z.looseObject({
49
+ delete: z.optional(OperationObject),
50
+ get: z.optional(OperationObject),
51
+ head: z.optional(OperationObject),
52
+ options: z.optional(OperationObject),
53
+ patch: z.optional(OperationObject),
54
+ post: z.optional(OperationObject),
55
+ put: z.optional(OperationObject),
56
+ trace: z.optional(OperationObject),
57
+ })
58
+
59
+ /**
60
+ * Minimal schema for an OpenAPI discovery document annotated with
61
+ * `x-service-info` and per-operation `x-payment-info`.
62
+ */
63
+ export const DiscoveryDocument = z.looseObject({
64
+ openapi: z.string(),
65
+ info: z.looseObject({
66
+ title: z.string(),
67
+ version: z.string(),
68
+ }),
69
+ 'x-service-info': z.optional(ServiceInfo),
70
+ paths: z.optional(z.record(z.string(), PathItem)),
71
+ })
72
+ export type DiscoveryDocument = z.infer<typeof DiscoveryDocument>
@@ -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
+ })