mppx 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +26 -3
  2. package/README.md +13 -13
  3. package/dist/BodyDigest.d.ts.map +1 -1
  4. package/dist/BodyDigest.js.map +1 -1
  5. package/dist/Challenge.d.ts.map +1 -1
  6. package/dist/Challenge.js.map +1 -1
  7. package/dist/Credential.d.ts.map +1 -1
  8. package/dist/Credential.js.map +1 -1
  9. package/dist/Errors.js +64 -67
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/PaymentRequest.d.ts.map +1 -1
  12. package/dist/PaymentRequest.js.map +1 -1
  13. package/dist/Receipt.d.ts.map +1 -1
  14. package/dist/Receipt.js.map +1 -1
  15. package/dist/Store.d.ts +9 -0
  16. package/dist/Store.d.ts.map +1 -1
  17. package/dist/Store.js +17 -0
  18. package/dist/Store.js.map +1 -1
  19. package/dist/cli/account.d.ts.map +1 -1
  20. package/dist/cli/account.js +40 -5
  21. package/dist/cli/account.js.map +1 -1
  22. package/dist/cli/cli.d.ts.map +1 -1
  23. package/dist/cli/cli.js +157 -1
  24. package/dist/cli/cli.js.map +1 -1
  25. package/dist/cli/internal.d.ts.map +1 -1
  26. package/dist/cli/internal.js.map +1 -1
  27. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  28. package/dist/cli/plugins/stripe.js.map +1 -1
  29. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  30. package/dist/cli/plugins/tempo.js +2 -1
  31. package/dist/cli/plugins/tempo.js.map +1 -1
  32. package/dist/cli/utils.d.ts.map +1 -1
  33. package/dist/cli/utils.js.map +1 -1
  34. package/dist/client/internal/Fetch.d.ts +2 -0
  35. package/dist/client/internal/Fetch.d.ts.map +1 -1
  36. package/dist/client/internal/Fetch.js +1 -1
  37. package/dist/client/internal/Fetch.js.map +1 -1
  38. package/dist/discovery/Discovery.d.ts +146 -0
  39. package/dist/discovery/Discovery.d.ts.map +1 -0
  40. package/dist/discovery/Discovery.js +60 -0
  41. package/dist/discovery/Discovery.js.map +1 -0
  42. package/dist/discovery/OpenApi.d.ts +61 -0
  43. package/dist/discovery/OpenApi.d.ts.map +1 -0
  44. package/dist/discovery/OpenApi.js +139 -0
  45. package/dist/discovery/OpenApi.js.map +1 -0
  46. package/dist/discovery/Validate.d.ts +10 -0
  47. package/dist/discovery/Validate.d.ts.map +1 -0
  48. package/dist/discovery/Validate.js +63 -0
  49. package/dist/discovery/Validate.js.map +1 -0
  50. package/dist/discovery/index.d.ts +4 -0
  51. package/dist/discovery/index.d.ts.map +1 -0
  52. package/dist/discovery/index.js +4 -0
  53. package/dist/discovery/index.js.map +1 -0
  54. package/dist/internal/types.d.ts.map +1 -1
  55. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  56. package/dist/mcp-sdk/client/McpClient.js +1 -1
  57. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  58. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
  59. package/dist/mcp-sdk/server/Transport.js.map +1 -1
  60. package/dist/middlewares/elysia.d.ts +52 -1
  61. package/dist/middlewares/elysia.d.ts.map +1 -1
  62. package/dist/middlewares/elysia.js +17 -0
  63. package/dist/middlewares/elysia.js.map +1 -1
  64. package/dist/middlewares/express.d.ts +13 -1
  65. package/dist/middlewares/express.d.ts.map +1 -1
  66. package/dist/middlewares/express.js +23 -2
  67. package/dist/middlewares/express.js.map +1 -1
  68. package/dist/middlewares/hono.d.ts +19 -1
  69. package/dist/middlewares/hono.d.ts.map +1 -1
  70. package/dist/middlewares/hono.js +51 -0
  71. package/dist/middlewares/hono.js.map +1 -1
  72. package/dist/middlewares/internal/mppx.d.ts +4 -2
  73. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  74. package/dist/middlewares/internal/mppx.js +10 -3
  75. package/dist/middlewares/internal/mppx.js.map +1 -1
  76. package/dist/middlewares/nextjs.d.ts +11 -0
  77. package/dist/middlewares/nextjs.d.ts.map +1 -1
  78. package/dist/middlewares/nextjs.js +15 -0
  79. package/dist/middlewares/nextjs.js.map +1 -1
  80. package/dist/proxy/Proxy.d.ts +6 -0
  81. package/dist/proxy/Proxy.d.ts.map +1 -1
  82. package/dist/proxy/Proxy.js +56 -80
  83. package/dist/proxy/Proxy.js.map +1 -1
  84. package/dist/proxy/Service.d.ts +16 -23
  85. package/dist/proxy/Service.d.ts.map +1 -1
  86. package/dist/proxy/Service.js +20 -84
  87. package/dist/proxy/Service.js.map +1 -1
  88. package/dist/proxy/internal/Route.js +1 -1
  89. package/dist/proxy/internal/Route.js.map +1 -1
  90. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  91. package/dist/proxy/services/anthropic.js +5 -0
  92. package/dist/proxy/services/anthropic.js.map +1 -1
  93. package/dist/proxy/services/openai.d.ts.map +1 -1
  94. package/dist/proxy/services/openai.js +6 -3
  95. package/dist/proxy/services/openai.js.map +1 -1
  96. package/dist/proxy/services/stripe.d.ts.map +1 -1
  97. package/dist/proxy/services/stripe.js +6 -3
  98. package/dist/proxy/services/stripe.js.map +1 -1
  99. package/dist/server/Mppx.d.ts.map +1 -1
  100. package/dist/server/Mppx.js +35 -17
  101. package/dist/server/Mppx.js.map +1 -1
  102. package/dist/server/Request.d.ts.map +1 -1
  103. package/dist/server/Request.js.map +1 -1
  104. package/dist/stripe/Methods.d.ts.map +1 -1
  105. package/dist/stripe/Methods.js.map +1 -1
  106. package/dist/tempo/Methods.d.ts.map +1 -1
  107. package/dist/tempo/Methods.js.map +1 -1
  108. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  109. package/dist/tempo/client/ChannelOps.js.map +1 -1
  110. package/dist/tempo/client/Charge.d.ts.map +1 -1
  111. package/dist/tempo/client/Charge.js.map +1 -1
  112. package/dist/tempo/client/Session.d.ts.map +1 -1
  113. package/dist/tempo/client/Session.js.map +1 -1
  114. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  115. package/dist/tempo/client/SessionManager.js +1 -1
  116. package/dist/tempo/client/SessionManager.js.map +1 -1
  117. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  118. package/dist/tempo/internal/auto-swap.js +1 -1
  119. package/dist/tempo/internal/auto-swap.js.map +1 -1
  120. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  121. package/dist/tempo/internal/fee-payer.js +1 -1
  122. package/dist/tempo/internal/fee-payer.js.map +1 -1
  123. package/dist/tempo/server/Charge.d.ts.map +1 -1
  124. package/dist/tempo/server/Charge.js +1 -1
  125. package/dist/tempo/server/Charge.js.map +1 -1
  126. package/dist/tempo/server/Session.d.ts.map +1 -1
  127. package/dist/tempo/server/Session.js +18 -5
  128. package/dist/tempo/server/Session.js.map +1 -1
  129. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  130. package/dist/tempo/server/internal/transport.js +8 -0
  131. package/dist/tempo/server/internal/transport.js.map +1 -1
  132. package/dist/tempo/session/Chain.d.ts.map +1 -1
  133. package/dist/tempo/session/Chain.js +1 -1
  134. package/dist/tempo/session/Chain.js.map +1 -1
  135. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  136. package/dist/tempo/session/ChannelStore.js.map +1 -1
  137. package/dist/tempo/session/Receipt.d.ts.map +1 -1
  138. package/dist/tempo/session/Receipt.js.map +1 -1
  139. package/dist/tempo/session/Sse.d.ts.map +1 -1
  140. package/dist/tempo/session/Sse.js.map +1 -1
  141. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  142. package/dist/tempo/session/Voucher.js.map +1 -1
  143. package/dist/viem/Client.d.ts.map +1 -1
  144. package/dist/viem/Client.js.map +1 -1
  145. package/package.json +6 -1
  146. package/src/BodyDigest.test.ts +1 -1
  147. package/src/BodyDigest.ts +1 -0
  148. package/src/Challenge.fuzz.test.ts +121 -0
  149. package/src/Challenge.test-d.ts +2 -1
  150. package/src/Challenge.test.ts +1 -1
  151. package/src/Challenge.ts +1 -0
  152. package/src/Credential.fuzz.test.ts +62 -0
  153. package/src/Credential.test.ts +1 -1
  154. package/src/Credential.ts +1 -0
  155. package/src/Errors.test.ts +28 -40
  156. package/src/Expires.test.ts +2 -1
  157. package/src/Method.test.ts +1 -1
  158. package/src/PaymentRequest.test.ts +1 -1
  159. package/src/PaymentRequest.ts +1 -0
  160. package/src/Receipt.test.ts +1 -1
  161. package/src/Receipt.ts +1 -0
  162. package/src/Store.test-d.ts +2 -1
  163. package/src/Store.test.ts +57 -7
  164. package/src/Store.ts +25 -0
  165. package/src/cli/account.ts +65 -30
  166. package/src/cli/cli.test.ts +215 -2
  167. package/src/cli/cli.ts +166 -1
  168. package/src/cli/config.test.ts +1 -0
  169. package/src/cli/internal.ts +1 -0
  170. package/src/cli/plugins/stripe.ts +1 -0
  171. package/src/cli/plugins/tempo.ts +4 -1
  172. package/src/cli/utils.ts +1 -0
  173. package/src/client/Mppx.test-d.ts +2 -1
  174. package/src/client/Mppx.test.ts +1 -1
  175. package/src/client/Transport.test.ts +1 -1
  176. package/src/client/internal/Fetch.browser.test.ts +2 -1
  177. package/src/client/internal/Fetch.test-d.ts +2 -1
  178. package/src/client/internal/Fetch.test.ts +3 -1
  179. package/src/client/internal/Fetch.ts +1 -1
  180. package/src/discovery/Discovery.test.ts +152 -0
  181. package/src/discovery/Discovery.ts +72 -0
  182. package/src/discovery/OpenApi.test.ts +425 -0
  183. package/src/discovery/OpenApi.ts +224 -0
  184. package/src/discovery/Validate.test.ts +188 -0
  185. package/src/discovery/Validate.ts +76 -0
  186. package/src/discovery/index.ts +3 -0
  187. package/src/internal/constantTimeEqual.test.ts +2 -1
  188. package/src/internal/types.ts +1 -3
  189. package/src/mcp-sdk/client/McpClient.test-d.ts +2 -1
  190. package/src/mcp-sdk/client/McpClient.test.ts +2 -1
  191. package/src/mcp-sdk/client/McpClient.ts +2 -0
  192. package/src/mcp-sdk/server/Transport.test.ts +2 -1
  193. package/src/mcp-sdk/server/Transport.ts +1 -0
  194. package/src/middlewares/elysia.test.ts +28 -2
  195. package/src/middlewares/elysia.ts +36 -1
  196. package/src/middlewares/express.test.ts +95 -7
  197. package/src/middlewares/express.ts +40 -2
  198. package/src/middlewares/hono.test.ts +28 -6
  199. package/src/middlewares/hono.ts +74 -1
  200. package/src/middlewares/internal/mppx.test.ts +2 -1
  201. package/src/middlewares/internal/mppx.ts +14 -6
  202. package/src/middlewares/nextjs.test.ts +32 -6
  203. package/src/middlewares/nextjs.ts +28 -0
  204. package/src/proxy/Proxy.test.ts +55 -270
  205. package/src/proxy/Proxy.ts +73 -93
  206. package/src/proxy/Service.test.ts +24 -1
  207. package/src/proxy/Service.ts +48 -88
  208. package/src/proxy/internal/Headers.test.ts +2 -1
  209. package/src/proxy/internal/Route.test.ts +9 -1
  210. package/src/proxy/internal/Route.ts +1 -1
  211. package/src/proxy/services/anthropic.test.ts +132 -0
  212. package/src/proxy/services/anthropic.ts +5 -0
  213. package/src/proxy/services/openai.test.ts +2 -1
  214. package/src/proxy/services/openai.ts +6 -4
  215. package/src/proxy/services/stripe.test.ts +132 -0
  216. package/src/proxy/services/stripe.ts +6 -4
  217. package/src/server/Mppx.test-d.ts +1 -1
  218. package/src/server/Mppx.test.ts +194 -1
  219. package/src/server/Mppx.ts +38 -19
  220. package/src/server/NodeListener.test.ts +1 -1
  221. package/src/server/Request.test.ts +2 -1
  222. package/src/server/Request.ts +1 -0
  223. package/src/server/Response.test.ts +2 -1
  224. package/src/server/Transport.test.ts +2 -1
  225. package/src/stripe/Charge.integration.test.ts +1 -1
  226. package/src/stripe/Methods.test.ts +1 -1
  227. package/src/stripe/Methods.ts +1 -0
  228. package/src/stripe/client/Charge.test.ts +2 -1
  229. package/src/stripe/server/Charge.test.ts +2 -1
  230. package/src/tempo/Attribution.test.ts +2 -1
  231. package/src/tempo/Methods.test.ts +1 -1
  232. package/src/tempo/Methods.ts +1 -0
  233. package/src/tempo/client/ChannelOps.test.ts +7 -3
  234. package/src/tempo/client/ChannelOps.ts +1 -0
  235. package/src/tempo/client/Charge.ts +1 -0
  236. package/src/tempo/client/Session.test.ts +6 -2
  237. package/src/tempo/client/Session.ts +1 -0
  238. package/src/tempo/client/SessionManager.test.ts +29 -1
  239. package/src/tempo/client/SessionManager.ts +2 -1
  240. package/src/tempo/internal/auto-swap.test.ts +2 -1
  241. package/src/tempo/internal/auto-swap.ts +1 -0
  242. package/src/tempo/internal/defaults.test.ts +2 -1
  243. package/src/tempo/internal/fee-payer.test.ts +2 -1
  244. package/src/tempo/internal/fee-payer.ts +1 -0
  245. package/src/tempo/server/Charge.test.ts +2 -1
  246. package/src/tempo/server/Charge.ts +1 -0
  247. package/src/tempo/server/Session.test.ts +88 -37
  248. package/src/tempo/server/Session.ts +26 -8
  249. package/src/tempo/server/Sse.test.ts +2 -1
  250. package/src/tempo/server/internal/transport.test.ts +25 -1
  251. package/src/tempo/server/internal/transport.ts +11 -0
  252. package/src/tempo/session/Chain.test.ts +6 -2
  253. package/src/tempo/session/Chain.ts +2 -1
  254. package/src/tempo/session/Channel.test.ts +2 -1
  255. package/src/tempo/session/ChannelStore.test.ts +2 -1
  256. package/src/tempo/session/ChannelStore.ts +1 -0
  257. package/src/tempo/session/Receipt.test.ts +2 -1
  258. package/src/tempo/session/Receipt.ts +1 -0
  259. package/src/tempo/session/Sse.fuzz.test.ts +138 -0
  260. package/src/tempo/session/Sse.test.ts +2 -1
  261. package/src/tempo/session/Sse.ts +1 -0
  262. package/src/tempo/session/Voucher.test.ts +2 -1
  263. package/src/tempo/session/Voucher.ts +1 -0
  264. package/src/viem/Account.test.ts +2 -1
  265. package/src/viem/Client.test.ts +2 -1
  266. package/src/viem/Client.ts +1 -0
  267. package/src/zod.test.ts +147 -0
package/src/cli/cli.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import * as fs from 'node:fs'
2
2
  import { createRequire } from 'node:module'
3
3
  import * as path from 'node:path'
4
+
4
5
  import { Cli, Errors, z } from 'incur'
5
6
  import { Base64 } from 'ox'
6
7
  import { type Address, createClient, http } from 'viem'
7
8
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
8
9
  import { tempo as tempoMainnet } from 'viem/chains'
10
+
9
11
  import * as Challenge from '../Challenge.js'
12
+ import { normalizeHeaders } from '../client/internal/Fetch.js'
10
13
  import * as Mppx from '../client/Mppx.js'
14
+ import { validate as validateDiscovery } from '../discovery/Validate.js'
11
15
  import { createDefaultStore, createKeychain, resolveAccountName } from './account.js'
12
16
  import { loadConfig, resolvePlugin } from './internal.js'
13
17
  import type { Plugin } from './plugins/plugin.js'
@@ -312,7 +316,7 @@ const cli = Cli.create('mppx', {
312
316
 
313
317
  // Send credential and get response
314
318
  const credentialHeaders = {
315
- ...(init.headers as Record<string, string>),
319
+ ...normalizeHeaders(init.headers),
316
320
  Authorization: credential,
317
321
  }
318
322
  plugin?.prepareCredentialRequest?.({ challenge, credential, headers: credentialHeaders })
@@ -912,7 +916,168 @@ export default defineConfig({
912
916
  },
913
917
  })
914
918
 
919
+ const discover = Cli.create('discover', {
920
+ description: 'Discovery tooling',
921
+ })
922
+ .command('generate', {
923
+ description: 'Generate a static OpenAPI discovery document from a module',
924
+ args: z.object({
925
+ module: z.string().describe('Path to a module that default-exports a discovery config'),
926
+ }),
927
+ options: z.object({
928
+ output: z.string().optional().describe('Write output to a file instead of stdout'),
929
+ }),
930
+ alias: { output: 'o' },
931
+ async run(c) {
932
+ const modulePath = path.resolve(c.args.module)
933
+ if (!fs.existsSync(modulePath)) {
934
+ return c.error({
935
+ code: 'MODULE_NOT_FOUND',
936
+ message: `Module not found: ${modulePath}`,
937
+ exitCode: 1,
938
+ })
939
+ }
940
+
941
+ let mod: Record<string, unknown>
942
+ try {
943
+ mod = await import(modulePath)
944
+ } catch (error) {
945
+ return c.error({
946
+ code: 'MODULE_IMPORT_FAILED',
947
+ message: `Failed to import module: ${(error as Error).message}`,
948
+ exitCode: 1,
949
+ })
950
+ }
951
+
952
+ const exported = (mod.default ?? mod) as Record<string, unknown>
953
+
954
+ // If the export is already a plain OpenAPI doc (has `openapi` key), use it directly.
955
+ // Otherwise, expect { mppx, ...GenerateConfig } and call generate().
956
+ let doc: Record<string, unknown>
957
+ if (typeof exported.openapi === 'string') {
958
+ doc = exported
959
+ } else {
960
+ const { generate } = await import('../discovery/OpenApi.js')
961
+ const mppx = exported.mppx as { methods: readonly any[]; realm: string }
962
+ if (!mppx) {
963
+ return c.error({
964
+ code: 'INVALID_MODULE',
965
+ message:
966
+ 'Module must default-export an OpenAPI document (with `openapi` key) or an object with `mppx` (server instance) and `routes`.',
967
+ exitCode: 1,
968
+ })
969
+ }
970
+ doc = generate(mppx, exported as any)
971
+ }
972
+
973
+ const json = JSON.stringify(doc, null, 2)
974
+ if (c.options.output) {
975
+ const outPath = path.resolve(c.options.output)
976
+ fs.writeFileSync(outPath, `${json}\n`)
977
+ process.stderr.write(`Wrote ${outPath}\n`)
978
+ } else {
979
+ console.log(json)
980
+ }
981
+ },
982
+ })
983
+ .command('validate', {
984
+ description: 'Validate an OpenAPI discovery document from a file or URL',
985
+ args: z.object({
986
+ input: z.string().describe('Path or URL to a discovery document'),
987
+ }),
988
+ async run(c) {
989
+ const input = c.args.input
990
+ let raw: string
991
+ if (/^https?:\/\//.test(input)) {
992
+ const controller = new AbortController()
993
+ const timeout = setTimeout(() => controller.abort(), 30_000)
994
+ let response: Response
995
+ try {
996
+ response = await globalThis.fetch(input, { signal: controller.signal })
997
+ } catch (error) {
998
+ clearTimeout(timeout)
999
+ const msg =
1000
+ error instanceof DOMException && error.name === 'AbortError'
1001
+ ? 'Request timed out after 30s'
1002
+ : (error as Error).message
1003
+ return c.error({
1004
+ code: 'DISCOVERY_FETCH_FAILED',
1005
+ message: `Failed to fetch discovery document: ${msg}`,
1006
+ exitCode: 1,
1007
+ })
1008
+ }
1009
+ clearTimeout(timeout)
1010
+ if (!response.ok) {
1011
+ return c.error({
1012
+ code: 'DISCOVERY_FETCH_FAILED',
1013
+ message: `Failed to fetch discovery document: HTTP ${response.status}`,
1014
+ exitCode: 1,
1015
+ })
1016
+ }
1017
+ const maxSize = 10 * 1024 * 1024 // 10 MB
1018
+ const contentLength = response.headers.get('content-length')
1019
+ if (contentLength && Number(contentLength) > maxSize) {
1020
+ return c.error({
1021
+ code: 'DISCOVERY_TOO_LARGE',
1022
+ message: `Discovery document exceeds 10 MB limit`,
1023
+ exitCode: 1,
1024
+ })
1025
+ }
1026
+ raw = await response.text()
1027
+ if (raw.length > maxSize) {
1028
+ return c.error({
1029
+ code: 'DISCOVERY_TOO_LARGE',
1030
+ message: `Discovery document exceeds 10 MB limit`,
1031
+ exitCode: 1,
1032
+ })
1033
+ }
1034
+ } else {
1035
+ const resolved = path.resolve(input)
1036
+ if (!fs.existsSync(resolved)) {
1037
+ return c.error({
1038
+ code: 'DISCOVERY_NOT_FOUND',
1039
+ message: `Discovery document not found: ${resolved}`,
1040
+ exitCode: 1,
1041
+ })
1042
+ }
1043
+ raw = fs.readFileSync(resolved, 'utf-8')
1044
+ }
1045
+
1046
+ let doc: unknown
1047
+ try {
1048
+ doc = JSON.parse(raw)
1049
+ } catch (error) {
1050
+ return c.error({
1051
+ code: 'DISCOVERY_INVALID_JSON',
1052
+ message: `Invalid discovery JSON: ${(error as Error).message}`,
1053
+ exitCode: 1,
1054
+ })
1055
+ }
1056
+
1057
+ const issues = validateDiscovery(doc)
1058
+ for (const issue of issues) console.log(`[${issue.severity}] ${issue.path}: ${issue.message}`)
1059
+
1060
+ const errorCount = issues.filter((issue) => issue.severity === 'error').length
1061
+ const warningCount = issues.filter((issue) => issue.severity === 'warning').length
1062
+
1063
+ if (errorCount > 0) {
1064
+ return c.error({
1065
+ code: 'DISCOVERY_INVALID',
1066
+ message: `Discovery document has ${errorCount} error(s) and ${warningCount} warning(s).`,
1067
+ exitCode: 1,
1068
+ })
1069
+ }
1070
+
1071
+ console.log(
1072
+ warningCount > 0
1073
+ ? `Discovery document is valid with ${warningCount} warning(s).`
1074
+ : 'Discovery document is valid.',
1075
+ )
1076
+ },
1077
+ })
1078
+
915
1079
  cli.command(account)
1080
+ cli.command(discover)
916
1081
  cli.command(init)
917
1082
  cli.command(sign)
918
1083
 
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as os from 'node:os'
3
3
  import * as path from 'node:path'
4
+
4
5
  import { defineConfig } from './config.js'
5
6
  import { loadConfig } from './internal.js'
6
7
 
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as path from 'node:path'
3
+
3
4
  import type * as Challenge from '../Challenge.js'
4
5
  import type * as Method from '../Method.js'
5
6
  import type { Config } from './config.js'
@@ -1,4 +1,5 @@
1
1
  import { Errors, z } from 'incur'
2
+
2
3
  import { stripe as stripeMethods } from '../../stripe/client/index.js'
3
4
  import { pc } from '../utils.js'
4
5
  import { createPlugin } from './plugin.js'
@@ -3,11 +3,14 @@ import * as fs from 'node:fs'
3
3
  import { createRequire } from 'node:module'
4
4
  import * as os from 'node:os'
5
5
  import * as path from 'node:path'
6
+
6
7
  import { Errors, z } from 'incur'
7
8
  import { Base64 } from 'ox'
8
9
  import type { Address } from 'viem'
9
10
  import { createClient, http } from 'viem'
10
11
  import { privateKeyToAccount } from 'viem/accounts'
12
+
13
+ import { normalizeHeaders } from '../../client/internal/Fetch.js'
11
14
  import * as Credential from '../../Credential.js'
12
15
  import { tempo as tempoMethods } from '../../tempo/client/index.js'
13
16
  import type { SessionCredentialPayload } from '../../tempo/session/Types.js'
@@ -610,7 +613,7 @@ async function closeChannel(opts: {
610
613
  const closeRes = await globalThis.fetch(opts.fetchUrl, {
611
614
  ...opts.fetchInit,
612
615
  headers: {
613
- ...(opts.fetchInit.headers as Record<string, string>),
616
+ ...normalizeHeaders(opts.fetchInit.headers),
614
617
  Authorization: closeCred,
615
618
  },
616
619
  })
package/src/cli/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as readline from 'node:readline'
2
+
2
3
  import type { Chain } from 'viem'
3
4
  import { type Address, createClient, http } from 'viem'
4
5
  import { tempo as tempoMainnet, tempoModerato } from 'viem/chains'
@@ -1,5 +1,6 @@
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
  import * as Method from '../Method.js'
4
5
  import { charge } from '../tempo/client/Charge.js'
5
6
  import { tempo } from '../tempo/client/Methods.js'
@@ -2,7 +2,7 @@ import { Challenge, Credential, Mcp, Method, Receipt } from 'mppx'
2
2
  import { Mppx, Transport, tempo } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
4
  import { Methods } from 'mppx/tempo'
5
- import { afterEach, describe, expect, test } from 'vitest'
5
+ import { afterEach, describe, expect, test } from 'vp/test'
6
6
  import * as Http from '~test/Http.js'
7
7
  import { accounts, asset, client } from '~test/tempo/viem.js'
8
8
 
@@ -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,5 @@
1
- import { describe, expect, test, vi } from 'vitest'
1
+ import { describe, expect, test, vi } from 'vp/test'
2
+
2
3
  import * as Fetch from './Fetch.js'
3
4
 
4
5
  const noopMethod = {
@@ -1,5 +1,6 @@
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
  import { charge } from '../../tempo/client/Charge.js'
4
5
  import * as Fetch from './Fetch.js'
5
6
 
@@ -2,10 +2,11 @@ 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'
9
+
9
10
  import * as Fetch from './Fetch.js'
10
11
 
11
12
  const realm = 'api.example.com'
@@ -15,6 +16,7 @@ const server = Mppx_server.create({
15
16
  methods: [
16
17
  tempo_server({
17
18
  getClient: () => client,
19
+ account: accounts[0],
18
20
  }),
19
21
  ],
20
22
  realm,
@@ -193,7 +193,7 @@ export function restore(): void {
193
193
  }
194
194
 
195
195
  /** @internal Normalizes headers to a plain object for spreading. */
196
- function normalizeHeaders(headers: unknown): Record<string, string> {
196
+ export function normalizeHeaders(headers: unknown): Record<string, string> {
197
197
  if (!headers) return {}
198
198
  if (headers instanceof Headers) {
199
199
  const result: Record<string, string> = {}
@@ -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>