mppx 0.8.2 → 0.8.4

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 (72) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +2 -0
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  6. package/dist/cli/plugins/stripe.js +32 -12
  7. package/dist/cli/plugins/stripe.js.map +1 -1
  8. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  9. package/dist/cli/plugins/tempo.js +1 -0
  10. package/dist/cli/plugins/tempo.js.map +1 -1
  11. package/dist/cli/utils.d.ts +2 -2
  12. package/dist/cli/utils.d.ts.map +1 -1
  13. package/dist/cli/utils.js +8 -2
  14. package/dist/cli/utils.js.map +1 -1
  15. package/dist/cli/validate/challenge.d.ts +14 -0
  16. package/dist/cli/validate/challenge.d.ts.map +1 -0
  17. package/dist/cli/validate/challenge.js +211 -0
  18. package/dist/cli/validate/challenge.js.map +1 -0
  19. package/dist/cli/validate/discovery.d.ts +10 -0
  20. package/dist/cli/validate/discovery.d.ts.map +1 -0
  21. package/dist/cli/validate/discovery.js +128 -0
  22. package/dist/cli/validate/discovery.js.map +1 -0
  23. package/dist/cli/validate/helpers.d.ts +33 -0
  24. package/dist/cli/validate/helpers.d.ts.map +1 -0
  25. package/dist/cli/validate/helpers.js +136 -0
  26. package/dist/cli/validate/helpers.js.map +1 -0
  27. package/dist/cli/validate/index.d.ts +17 -0
  28. package/dist/cli/validate/index.d.ts.map +1 -0
  29. package/dist/cli/validate/index.js +224 -0
  30. package/dist/cli/validate/index.js.map +1 -0
  31. package/dist/cli/validate/payment.d.ts +7 -0
  32. package/dist/cli/validate/payment.d.ts.map +1 -0
  33. package/dist/cli/validate/payment.js +381 -0
  34. package/dist/cli/validate/payment.js.map +1 -0
  35. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  36. package/dist/stripe/server/internal/html.gen.js +1 -1
  37. package/dist/tempo/client/Charge.d.ts.map +1 -1
  38. package/dist/tempo/client/Charge.js +7 -1
  39. package/dist/tempo/client/Charge.js.map +1 -1
  40. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  41. package/dist/tempo/internal/auto-swap.js +21 -6
  42. package/dist/tempo/internal/auto-swap.js.map +1 -1
  43. package/dist/tempo/internal/fee-token.js +1 -1
  44. package/dist/tempo/internal/fee-token.js.map +1 -1
  45. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  46. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  47. package/dist/tempo/server/internal/html.gen.js +1 -1
  48. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  49. package/package.json +2 -2
  50. package/src/cli/cli.test.ts +134 -29
  51. package/src/cli/cli.ts +2 -0
  52. package/src/cli/mcp.test.ts +1 -0
  53. package/src/cli/plugins/stripe.ts +31 -12
  54. package/src/cli/plugins/tempo.ts +1 -0
  55. package/src/cli/utils.test.ts +36 -2
  56. package/src/cli/utils.ts +16 -3
  57. package/src/cli/validate/challenge.ts +335 -0
  58. package/src/cli/validate/discovery.ts +120 -0
  59. package/src/cli/validate/helpers.ts +154 -0
  60. package/src/cli/validate/index.ts +348 -0
  61. package/src/cli/validate/payment.ts +488 -0
  62. package/src/cli/validate.test.ts +539 -0
  63. package/src/mcp/client/McpClient.integration.test.ts +12 -1
  64. package/src/stripe/server/internal/html.gen.ts +1 -1
  65. package/src/tempo/client/Charge.test.ts +56 -0
  66. package/src/tempo/client/Charge.ts +20 -2
  67. package/src/tempo/internal/auto-swap.test.ts +85 -1
  68. package/src/tempo/internal/auto-swap.ts +52 -6
  69. package/src/tempo/internal/fee-token.ts +1 -1
  70. package/src/tempo/server/Charge.test.ts +31 -16
  71. package/src/tempo/server/internal/html/package.json +1 -1
  72. package/src/tempo/server/internal/html.gen.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"html.gen.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/html.gen.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,MAAM,CAAC,MAAM,IAAI,GAAG,2j2oBAA2j2oB,CAAA"}
1
+ {"version":3,"file":"html.gen.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/html.gen.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,MAAM,CAAC,MAAM,IAAI,GAAG,swrpBAAswrpB,CAAA"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mppx",
3
3
  "type": "module",
4
- "version": "0.8.2",
4
+ "version": "0.8.4",
5
5
  "main": "./dist/index.js",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/wevm/mppx#readme",
@@ -140,7 +140,7 @@
140
140
  "elysia": ">=1",
141
141
  "express": ">=5",
142
142
  "hono": ">=4.12.25",
143
- "viem": ">=2.51.0"
143
+ "viem": ">=2.54.0"
144
144
  },
145
145
  "peerDependenciesMeta": {
146
146
  "@modelcontextprotocol/sdk": {
@@ -20,6 +20,7 @@ import * as Receipt from '../Receipt.js'
20
20
  import * as Mppx_server from '../server/Mppx.js'
21
21
  import { toNodeListener } from '../server/Mppx.js'
22
22
  import * as Store from '../Store.js'
23
+ import { stripePreviewVersion } from '../stripe/internal/constants.js'
23
24
  import { stripe as stripe_server } from '../stripe/server/Methods.js'
24
25
  import { tempo } from '../tempo/server/Methods.js'
25
26
  import { escrowAbi } from '../tempo/session/precompile/escrow.abi.js'
@@ -1403,40 +1404,144 @@ describe('stripe charge', () => {
1403
1404
  }
1404
1405
  })
1405
1406
 
1406
- test('error: production key rejected', { timeout: 60_000 }, async () => {
1407
- const server = Mppx_server.create({
1408
- methods: [
1409
- stripe_server.charge({
1410
- secretKey: 'sk_test_mock',
1411
- networkId: 'internal',
1412
- paymentMethodTypes: ['card'],
1413
- }),
1414
- ],
1415
- realm: 'cli-test-stripe-live',
1416
- secretKey: 'cli-test-secret-cli-test-secret-32',
1417
- })
1407
+ test(
1408
+ 'happy path: live key uses production SPT endpoint and seller business profile',
1409
+ { timeout: 60_000 },
1410
+ async () => {
1411
+ const mockStripeClient = {
1412
+ paymentIntents: {
1413
+ create: async () => ({ id: 'pi_mock_cli_live_123', status: 'succeeded' }),
1414
+ },
1415
+ }
1418
1416
 
1419
- const appServer = await Http.createServer(async (req, res) => {
1420
- const result = await Mppx_server.toNodeListener(
1421
- server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
1422
- )(req, res)
1423
- if (result.status === 402) return
1424
- res.end('paid')
1425
- })
1417
+ const server = Mppx_server.create({
1418
+ methods: [
1419
+ stripe_server.charge({
1420
+ client: mockStripeClient,
1421
+ networkId: 'profile_live_mock',
1422
+ paymentMethodTypes: ['card'],
1423
+ }),
1424
+ ],
1425
+ realm: 'cli-test-stripe-live',
1426
+ secretKey: 'cli-test-secret-cli-test-secret-32',
1427
+ })
1426
1428
 
1427
- try {
1428
- const { output, exitCode } = await serve(
1429
- [appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'],
1430
- { env: { MPPX_STRIPE_SECRET_KEY: 'sk_live_fake' } },
1429
+ const appServer = await Http.createServer(async (req, res) => {
1430
+ const result = await Mppx_server.toNodeListener(
1431
+ server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
1432
+ )(req, res)
1433
+ if (result.status === 402) return
1434
+ res.end('paid')
1435
+ })
1436
+
1437
+ const { restore, stripeRequests } = mockSptFetch(
1438
+ 'https://api.stripe.com/v1/shared_payment/issued_tokens',
1431
1439
  )
1432
- expect(exitCode).toBe(2)
1433
- expect(output).toContain('test mode')
1434
- } finally {
1435
- appServer.close()
1436
- }
1437
- })
1440
+
1441
+ try {
1442
+ const { output } = await serve([appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'], {
1443
+ env: { MPPX_STRIPE_SECRET_KEY: 'sk_live_fake', MPPX_STRIPE_SPT_URL: undefined },
1444
+ })
1445
+ expect(output).toContain('paid')
1446
+ const stripeRequest = stripeRequests[0]
1447
+ expect(stripeRequest?.url).toBe('https://api.stripe.com/v1/shared_payment/issued_tokens')
1448
+ expect(stripeRequest?.body.get('seller_details[network_business_profile]')).toBe(
1449
+ 'profile_live_mock',
1450
+ )
1451
+ expect(stripeRequest?.body.has('seller_details[network_id]')).toBe(false)
1452
+ expect(stripeRequest?.headers['Stripe-Version']).toBe(stripePreviewVersion)
1453
+ } finally {
1454
+ restore()
1455
+ appServer.close()
1456
+ }
1457
+ },
1458
+ )
1459
+
1460
+ test(
1461
+ 'happy path: test key uses test-helper SPT endpoint and network ID',
1462
+ { timeout: 60_000 },
1463
+ async () => {
1464
+ const mockStripeClient = {
1465
+ paymentIntents: {
1466
+ create: async () => ({ id: 'pi_mock_cli_test_123', status: 'succeeded' }),
1467
+ },
1468
+ }
1469
+
1470
+ const server = Mppx_server.create({
1471
+ methods: [
1472
+ stripe_server.charge({
1473
+ client: mockStripeClient,
1474
+ networkId: 'internal',
1475
+ paymentMethodTypes: ['card'],
1476
+ }),
1477
+ ],
1478
+ realm: 'cli-test-stripe-testmode',
1479
+ secretKey: 'cli-test-secret-cli-test-secret-32',
1480
+ })
1481
+
1482
+ const appServer = await Http.createServer(async (req, res) => {
1483
+ const result = await Mppx_server.toNodeListener(
1484
+ server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
1485
+ )(req, res)
1486
+ if (result.status === 402) return
1487
+ res.end('paid')
1488
+ })
1489
+
1490
+ const { restore, stripeRequests } = mockSptFetch(
1491
+ 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens',
1492
+ )
1493
+
1494
+ try {
1495
+ const { output } = await serve([appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'], {
1496
+ env: { MPPX_STRIPE_SECRET_KEY: 'sk_test_fake', MPPX_STRIPE_SPT_URL: undefined },
1497
+ })
1498
+ expect(output).toContain('paid')
1499
+ const stripeRequest = stripeRequests[0]
1500
+ expect(stripeRequest?.url).toBe(
1501
+ 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens',
1502
+ )
1503
+ expect(stripeRequest?.body.get('seller_details[network_id]')).toBe('internal')
1504
+ expect(stripeRequest?.body.has('seller_details[network_business_profile]')).toBe(false)
1505
+ expect(stripeRequest?.headers['Stripe-Version']).toBe(stripePreviewVersion)
1506
+ } finally {
1507
+ restore()
1508
+ appServer.close()
1509
+ }
1510
+ },
1511
+ )
1438
1512
  })
1439
1513
 
1514
+ /**
1515
+ * Intercepts `globalThis.fetch` calls to `sptUrl`, recording each request and
1516
+ * responding with a mock SPT. Returns the recorded requests and a `restore`
1517
+ * function that reinstates the original fetch.
1518
+ */
1519
+ function mockSptFetch(sptUrl: string) {
1520
+ const originalFetch = globalThis.fetch
1521
+ const stripeRequests: {
1522
+ body: URLSearchParams
1523
+ headers: Record<string, string>
1524
+ url: string
1525
+ }[] = []
1526
+ globalThis.fetch = (async (input, init) => {
1527
+ const url =
1528
+ typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
1529
+ if (url === sptUrl) {
1530
+ stripeRequests.push({
1531
+ body: new URLSearchParams(init?.body as string),
1532
+ headers: (init?.headers ?? {}) as Record<string, string>,
1533
+ url,
1534
+ })
1535
+ return new Response(JSON.stringify({ id: 'spt_mock_cli_test' }), {
1536
+ headers: { 'Content-Type': 'application/json' },
1537
+ status: 200,
1538
+ })
1539
+ }
1540
+ return originalFetch(input, init)
1541
+ }) as typeof fetch
1542
+ return { restore: () => (globalThis.fetch = originalFetch), stripeRequests }
1543
+ }
1544
+
1440
1545
  // ---------------------------------------------------------------------------
1441
1546
  // account [action]
1442
1547
  // Keychain tests are opt-in because macOS/Linux keychain commands can block on
package/src/cli/cli.ts CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  resolveFundingNetwork,
41
41
  resolveRpcUrl,
42
42
  } from './utils.js'
43
+ import validate from './validate/index.js'
43
44
 
44
45
  const packageJson = createRequire(import.meta.url)('../../package.json') as {
45
46
  name: string
@@ -1575,5 +1576,6 @@ cli.command(discover)
1575
1576
  cli.command(init)
1576
1577
  cli.command(services)
1577
1578
  cli.command(sign)
1579
+ cli.command(validate)
1578
1580
 
1579
1581
  export default cli
@@ -194,6 +194,7 @@ test('tools/list exposes mppx commands with input and output schemas', async ()
194
194
  'services_list',
195
195
  'services_show',
196
196
  'sign',
197
+ 'validate',
197
198
  ])
198
199
  expect(tools.find((tool: { name: string }) => tool.name === 'account_list').outputSchema).toEqual(
199
200
  expect.objectContaining({
@@ -1,6 +1,7 @@
1
1
  import { Errors, z } from 'incur'
2
2
 
3
3
  import { stripe as stripeMethods } from '../../stripe/client/index.js'
4
+ import { stripePreviewVersion } from '../../stripe/internal/constants.js'
4
5
  import { pc } from '../utils.js'
5
6
  import { createPlugin } from './plugin.js'
6
7
 
@@ -30,13 +31,6 @@ export function stripe() {
30
31
  message: 'MPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.',
31
32
  exitCode: 2,
32
33
  })
33
- if (!stripeSecretKey.startsWith('sk_test_'))
34
- throw new Errors.IncurError({
35
- code: 'UNSUPPORTED_MODE',
36
- message:
37
- 'Stripe CLI payments are currently only supported in test mode (sk_test_... keys).',
38
- exitCode: 2,
39
- })
40
34
 
41
35
  return {
42
36
  tokenSymbol: currency?.toUpperCase() ?? '',
@@ -58,19 +52,35 @@ export function stripe() {
58
52
  'usage_limits[max_amount]': amount,
59
53
  'usage_limits[expires_at]': expiresAt.toString(),
60
54
  })
61
- if (networkId) body.set('seller_details[network_id]', networkId)
55
+ const testMode = isTestKey(stripeSecretKey)
56
+ const sptUrl = (() => {
57
+ // Allow tests and custom deployments to target a specific SPT endpoint.
58
+ if (process.env.MPPX_STRIPE_SPT_URL) return process.env.MPPX_STRIPE_SPT_URL
59
+ // Preserve test-key behavior against Stripe's test-helper SPT endpoint.
60
+ if (testMode)
61
+ return 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens'
62
+ // Live keys mint Shared Payment Tokens through the agent-side issued tokens API.
63
+ return 'https://api.stripe.com/v1/shared_payment/issued_tokens'
64
+ })()
65
+ if (networkId) {
66
+ // Live SPT issuance grants the token to the seller's Stripe business
67
+ // profile (`profile_...`); the test helper uses a plain network ID.
68
+ const sellerDetailsField = testMode
69
+ ? 'seller_details[network_id]'
70
+ : 'seller_details[network_business_profile]'
71
+ body.set(sellerDetailsField, networkId)
72
+ }
62
73
  if (metadata) {
63
74
  for (const [key, value] of Object.entries(metadata)) {
64
75
  body.set(`metadata[${key}]`, value)
65
76
  }
66
77
  }
67
78
 
68
- const sptUrl =
69
- process.env.MPPX_STRIPE_SPT_URL ??
70
- 'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens'
71
79
  const sptHeaders = {
72
80
  Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
73
81
  'Content-Type': 'application/x-www-form-urlencoded',
82
+ // SPT endpoints are in preview and require an explicit preview API version.
83
+ 'Stripe-Version': stripePreviewVersion,
74
84
  }
75
85
 
76
86
  let response = await globalThis.fetch(sptUrl, {
@@ -80,7 +90,11 @@ export function stripe() {
80
90
  })
81
91
  if (!response.ok) {
82
92
  const errorBody = (await response.json()) as { error: { message: string } }
93
+ // Retry without optional parameters only in test mode: the live
94
+ // issued-tokens endpoint requires `seller_details`, so a retry
95
+ // without it can never succeed.
83
96
  if (
97
+ testMode &&
84
98
  (metadata || networkId) &&
85
99
  errorBody.error.message.includes('Received unknown parameter')
86
100
  ) {
@@ -122,7 +136,8 @@ export function stripe() {
122
136
 
123
137
  formatReceiptField(key, value) {
124
138
  if (key === 'reference' && typeof value === 'string' && value.startsWith('pi_')) {
125
- const isTest = process.env.MPPX_STRIPE_SECRET_KEY?.startsWith('sk_test_')
139
+ const secretKey = process.env.MPPX_STRIPE_SECRET_KEY
140
+ const isTest = secretKey !== undefined && isTestKey(secretKey)
126
141
  const url = `https://dashboard.stripe.com${isTest ? '/test' : ''}/payments/${value}`
127
142
  return pc.link(url, value, true)
128
143
  }
@@ -132,6 +147,10 @@ export function stripe() {
132
147
 
133
148
  // --- Helpers ---
134
149
 
150
+ function isTestKey(secretKey: string) {
151
+ return secretKey.startsWith('sk_test_') || secretKey.startsWith('rk_test_')
152
+ }
153
+
135
154
  function parseOptions<const schema extends z.ZodType>(
136
155
  schema: schema,
137
156
  rawOptions: unknown,
@@ -883,6 +883,7 @@ async function scoreTempoChargeChallenge(
883
883
  account: probe.account,
884
884
  token: currency as Address,
885
885
  })
886
+ .then((value) => value.amount)
886
887
  .catch(() => undefined)
887
888
  if (balance === undefined) return 'unknownBalance'
888
889
  if (balance >= amountRaw) return 'payable'
@@ -1,7 +1,13 @@
1
1
  import { tempo as tempoMainnet, tempoModerato } from 'viem/tempo/chains'
2
- import { afterEach, describe, expect, test } from 'vp/test'
2
+ import { afterEach, describe, expect, test, vi } from 'vp/test'
3
3
 
4
- import { networkRpcUrls, resolveChain, resolveFundingNetwork, resolveRpcUrl } from './utils.js'
4
+ import {
5
+ fetchTokenInfo,
6
+ networkRpcUrls,
7
+ resolveChain,
8
+ resolveFundingNetwork,
9
+ resolveRpcUrl,
10
+ } from './utils.js'
5
11
 
6
12
  describe('resolveRpcUrl', () => {
7
13
  afterEach(() => {
@@ -97,3 +103,31 @@ describe('resolveChain', () => {
97
103
  expect(chain.id).not.toBe(tempoModerato.id)
98
104
  })
99
105
  })
106
+
107
+ describe('fetchTokenInfo', () => {
108
+ afterEach(() => {
109
+ vi.doUnmock('viem/tempo')
110
+ })
111
+
112
+ test('uses 6 decimals when token metadata omits a numeric decimals value', async () => {
113
+ const token = '0x1111111111111111111111111111111111111111'
114
+ const account = '0x2222222222222222222222222222222222222222'
115
+ vi.doMock('viem/tempo', () => ({
116
+ Actions: {
117
+ token: {
118
+ getBalance: vi.fn(async () => ({ amount: 123n })),
119
+ getMetadata: vi.fn(async () => ({ decimals: undefined, symbol: 'TEST' })),
120
+ },
121
+ },
122
+ }))
123
+
124
+ const info = await fetchTokenInfo({} as never, token, account)
125
+
126
+ expect(info).toEqual({
127
+ balance: 123n,
128
+ decimals: 6,
129
+ symbol: 'TEST',
130
+ token,
131
+ })
132
+ })
133
+ })
package/src/cli/utils.ts CHANGED
@@ -287,6 +287,10 @@ export const testnetTokens = [
287
287
  '0x20c0000000000000000000000000000000000003',
288
288
  ] as const
289
289
 
290
+ function toBaseUnitAmount(value: bigint | { amount: bigint }) {
291
+ return typeof value === 'bigint' ? value : value.amount
292
+ }
293
+
290
294
  export function isTestnet(chain: Chain) {
291
295
  return chain.id !== tempoMainnet.id
292
296
  }
@@ -295,10 +299,18 @@ export async function fetchTokenInfo(
295
299
  client: ReturnType<typeof createClient>,
296
300
  token: Address,
297
301
  account: Address,
298
- ) {
302
+ ): Promise<{
303
+ balance: bigint
304
+ decimals: number
305
+ symbol: string
306
+ token: Address
307
+ }> {
299
308
  const { Actions } = await import('viem/tempo')
300
309
  const [balance, metadata] = await Promise.all([
301
- Actions.token.getBalance(client, { account, token }).catch(() => 0n),
310
+ Actions.token
311
+ .getBalance(client, { account, token })
312
+ .then(toBaseUnitAmount)
313
+ .catch(() => 0n),
302
314
  Actions.token.getMetadata(client, { token }).catch(() => ({ symbol: token as string })),
303
315
  ])
304
316
  const knownSymbols: Record<string, string> = {
@@ -306,7 +318,8 @@ export async function fetchTokenInfo(
306
318
  [usdc]: 'USDC',
307
319
  }
308
320
  const symbol = knownSymbols[token] ?? metadata.symbol
309
- const decimals = 'decimals' in metadata ? metadata.decimals : 6
321
+ const decimals =
322
+ 'decimals' in metadata && typeof metadata.decimals === 'number' ? metadata.decimals : 6
310
323
  return { balance, symbol, decimals, token }
311
324
  }
312
325