mppx 0.3.13 → 0.3.15

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 (75) hide show
  1. package/dist/Challenge.d.ts +1 -1
  2. package/dist/Challenge.d.ts.map +1 -1
  3. package/dist/Challenge.js +107 -15
  4. package/dist/Challenge.js.map +1 -1
  5. package/dist/bin.d.ts +3 -0
  6. package/dist/bin.d.ts.map +1 -0
  7. package/dist/bin.js +4 -0
  8. package/dist/bin.js.map +1 -0
  9. package/dist/cli.d.ts +26 -2
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +1478 -915
  12. package/dist/cli.js.map +1 -1
  13. package/dist/client/Mppx.d.ts +2 -0
  14. package/dist/client/Mppx.d.ts.map +1 -1
  15. package/dist/client/Mppx.js +2 -0
  16. package/dist/client/Mppx.js.map +1 -1
  17. package/dist/server/Mppx.d.ts.map +1 -1
  18. package/dist/server/Mppx.js +2 -1
  19. package/dist/server/Mppx.js.map +1 -1
  20. package/dist/stripe/Methods.d.ts +0 -3
  21. package/dist/stripe/Methods.d.ts.map +1 -1
  22. package/dist/stripe/Methods.js +0 -2
  23. package/dist/stripe/Methods.js.map +1 -1
  24. package/dist/stripe/client/Charge.d.ts +0 -3
  25. package/dist/stripe/client/Charge.d.ts.map +1 -1
  26. package/dist/stripe/client/Charge.js +2 -2
  27. package/dist/stripe/client/Charge.js.map +1 -1
  28. package/dist/stripe/client/Methods.d.ts +0 -3
  29. package/dist/stripe/client/Methods.d.ts.map +1 -1
  30. package/dist/stripe/server/Charge.d.ts +0 -3
  31. package/dist/stripe/server/Charge.d.ts.map +1 -1
  32. package/dist/stripe/server/Charge.js +2 -2
  33. package/dist/stripe/server/Charge.js.map +1 -1
  34. package/dist/stripe/server/Methods.d.ts +0 -3
  35. package/dist/stripe/server/Methods.d.ts.map +1 -1
  36. package/dist/tempo/Methods.d.ts +0 -3
  37. package/dist/tempo/Methods.d.ts.map +1 -1
  38. package/dist/tempo/Methods.js +3 -3
  39. package/dist/tempo/Methods.js.map +1 -1
  40. package/dist/tempo/client/Charge.d.ts +13 -3
  41. package/dist/tempo/client/Charge.d.ts.map +1 -1
  42. package/dist/tempo/client/Charge.js +18 -1
  43. package/dist/tempo/client/Charge.js.map +1 -1
  44. package/dist/tempo/client/Methods.d.ts +4 -3
  45. package/dist/tempo/client/Methods.d.ts.map +1 -1
  46. package/dist/tempo/server/Charge.d.ts +0 -3
  47. package/dist/tempo/server/Charge.d.ts.map +1 -1
  48. package/dist/tempo/server/Charge.js +2 -1
  49. package/dist/tempo/server/Charge.js.map +1 -1
  50. package/dist/tempo/server/Methods.d.ts +0 -3
  51. package/dist/tempo/server/Methods.d.ts.map +1 -1
  52. package/package.json +4 -4
  53. package/src/Challenge.test.ts +94 -18
  54. package/src/Challenge.ts +118 -15
  55. package/src/PaymentRequest.test.ts +0 -5
  56. package/src/bin.ts +4 -0
  57. package/src/cli.test.ts +180 -252
  58. package/src/cli.ts +1085 -485
  59. package/src/client/Mppx.test-d.ts +9 -0
  60. package/src/client/Mppx.test.ts +83 -5
  61. package/src/client/Mppx.ts +5 -0
  62. package/src/client/Transport.test.ts +5 -8
  63. package/src/client/internal/Fetch.browser.test.ts +135 -0
  64. package/src/client/internal/Fetch.test.ts +0 -88
  65. package/src/mcp-sdk/client/McpClient.test.ts +1 -1
  66. package/src/server/Mppx.ts +3 -1
  67. package/src/server/Transport.test.ts +6 -9
  68. package/src/stripe/Methods.ts +0 -2
  69. package/src/stripe/client/Charge.ts +2 -2
  70. package/src/stripe/server/Charge.ts +2 -2
  71. package/src/tempo/Methods.test.ts +22 -0
  72. package/src/tempo/Methods.ts +3 -3
  73. package/src/tempo/client/Charge.ts +29 -1
  74. package/src/tempo/server/Charge.test.ts +34 -72
  75. package/src/tempo/server/Charge.ts +2 -1
package/src/cli.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync } from 'node:child_process'
1
+ import { spawnSync } from 'node:child_process'
2
2
  import * as path from 'node:path'
3
3
  import { parseUnits } from 'viem'
4
4
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
@@ -8,83 +8,66 @@ import * as Http from '~test/Http.js'
8
8
  import { rpcUrl } from '~test/tempo/prool.js'
9
9
  import { deployEscrow } from '~test/tempo/session.js'
10
10
  import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
11
+ import cli from './cli.js'
11
12
  import * as Store from './Store.js'
12
13
  import * as Mppx_server from './server/Mppx.js'
13
14
  import { toNodeListener } from './server/Mppx.js'
14
15
  import { stripe as stripe_server } from './stripe/server/Methods.js'
15
16
  import { tempo } from './tempo/server/Methods.js'
16
17
 
17
- const cliPath = path.resolve(import.meta.dirname, 'cli.ts')
18
- const cwd = path.resolve(import.meta.dirname, '..')
19
18
  const testPrivateKey = generatePrivateKey()
20
19
  const testAccount = privateKeyToAccount(testPrivateKey)
21
- const env = { ...process.env, NODE_NO_WARNINGS: '1', MPPX_PRIVATE_KEY: testPrivateKey }
22
20
 
23
- function run(args: string[], options?: { input?: string }): string {
24
- const result = runRaw(args, options)
25
- if (result.status !== 0) {
26
- const msg = result.stderr?.trim() || result.stdout?.trim() || `exit code ${result.status}`
27
- throw new Error(msg)
21
+ async function serve(argv: string[], options?: { env?: Record<string, string | undefined> }) {
22
+ let output = ''
23
+ let stderr = ''
24
+ let exitCode: number | undefined
25
+ const saved: Record<string, string | undefined> = {}
26
+ if (options?.env) {
27
+ for (const [key, value] of Object.entries(options.env)) {
28
+ saved[key] = process.env[key]
29
+ if (value === undefined) delete process.env[key]
30
+ else process.env[key] = value
31
+ }
28
32
  }
29
- return result.stdout
30
- }
31
-
32
- function runRaw(
33
- args: string[],
34
- options?: { input?: string; env?: NodeJS.ProcessEnv },
35
- ): { stdout: string; stderr: string; status: number | null } {
36
- const result = spawnSync('node', ['--import', 'tsx', cliPath, ...args], {
37
- encoding: 'utf8',
38
- cwd,
39
- timeout: 60_000,
40
- ...(options?.input !== undefined && { input: options.input }),
41
- env: options?.env ?? env,
42
- })
43
- return { stdout: result.stdout ?? '', stderr: result.stderr ?? '', status: result.status }
44
- }
45
-
46
- function runAsync(
47
- args: string[],
48
- options?: { input?: string; env?: NodeJS.ProcessEnv },
49
- ): Promise<{ stdout: string; stderr: string }> {
50
- return new Promise((resolve, reject) => {
51
- const child = spawn('node', ['--import', 'tsx', cliPath, ...args], {
52
- cwd,
53
- env: options?.env ?? env,
54
- stdio: ['pipe', 'pipe', 'pipe'],
55
- })
56
-
57
- let stdout = ''
58
- let stderr = ''
59
- child.stdout.on('data', (data: Buffer) => {
60
- stdout += data.toString()
61
- })
62
- child.stderr.on('data', (data: Buffer) => {
63
- stderr += data.toString()
33
+ const origStdoutWrite = process.stdout.write
34
+ const origStderrWrite = process.stderr.write
35
+ const origLog = console.log
36
+ const origError = console.error
37
+ process.stdout.write = ((chunk: unknown) => {
38
+ output += typeof chunk === 'string' ? chunk : String(chunk)
39
+ return true
40
+ }) as typeof process.stdout.write
41
+ process.stderr.write = ((chunk: unknown) => {
42
+ stderr += typeof chunk === 'string' ? chunk : String(chunk)
43
+ return true
44
+ }) as typeof process.stderr.write
45
+ console.log = (...args: unknown[]) => {
46
+ output += `${args.map(String).join(' ')}\n`
47
+ }
48
+ console.error = (...args: unknown[]) => {
49
+ stderr += `${args.map(String).join(' ')}\n`
50
+ }
51
+ try {
52
+ await cli.serve(argv, {
53
+ stdout(s: string) {
54
+ output += s
55
+ },
56
+ exit(code: number) {
57
+ exitCode = code
58
+ },
64
59
  })
65
-
66
- if (options?.input !== undefined) {
67
- child.stdin.write(options.input)
68
- child.stdin.end()
69
- } else {
70
- child.stdin.end()
60
+ } finally {
61
+ process.stdout.write = origStdoutWrite
62
+ process.stderr.write = origStderrWrite
63
+ console.log = origLog
64
+ console.error = origError
65
+ for (const [key, value] of Object.entries(saved)) {
66
+ if (value === undefined) delete process.env[key]
67
+ else process.env[key] = value
71
68
  }
72
-
73
- const timer = setTimeout(() => {
74
- child.kill()
75
- reject(new Error(`Timed out.\nstdout: ${stdout}\nstderr: ${stderr}`))
76
- }, 60_000)
77
-
78
- child.on('close', (code) => {
79
- clearTimeout(timer)
80
- if (code !== 0) reject(new Error(stderr.trim() || `exit code ${code}`))
81
- else resolve({ stdout, stderr })
82
- })
83
- child.on('error', (err) => {
84
- clearTimeout(timer)
85
- reject(err)
86
- })
87
- })
69
+ }
70
+ return { output, stderr, exitCode }
88
71
  }
89
72
 
90
73
  describe('basic charge (examples/basic)', () => {
@@ -118,10 +101,10 @@ describe('basic charge (examples/basic)', () => {
118
101
  })
119
102
 
120
103
  try {
121
- const { stdout } = await runAsync([httpServer.url, '--rpc-url', rpcUrl, '-s'], {
122
- input: '',
104
+ const { output } = await serve([httpServer.url, '--rpc-url', rpcUrl, '-s'], {
105
+ env: { MPPX_PRIVATE_KEY: testPrivateKey },
123
106
  })
124
- expect(stdout).toContain('paid')
107
+ expect(output).toContain('paid')
125
108
  } finally {
126
109
  httpServer.close()
127
110
  }
@@ -148,12 +131,13 @@ describe('basic charge (examples/basic)', () => {
148
131
  })
149
132
 
150
133
  try {
151
- const result = await runAsync([httpServer.url, '--account', 'nonexistent-account'], {
152
- input: '',
153
- env: { ...process.env, NODE_NO_WARNINGS: '1' },
154
- }).catch((err) => err as Error)
155
- expect(result).toBeInstanceOf(Error)
156
- expect((result as Error).message).toContain('Account "nonexistent-account" not found')
134
+ const { output, exitCode } = await serve(
135
+ [httpServer.url, '--account', 'nonexistent-account'],
136
+ { env: { MPPX_PRIVATE_KEY: undefined } },
137
+ )
138
+ expect(exitCode).toBe(69)
139
+ expect(output).toContain('nonexistent-account')
140
+ expect(output).toContain('not found')
157
141
  } finally {
158
142
  httpServer.close()
159
143
  }
@@ -196,87 +180,16 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
196
180
  })
197
181
 
198
182
  try {
199
- const { stdout } = await runAsync(
183
+ const { output } = await serve(
200
184
  [httpServer.url, '--rpc-url', rpcUrl, '-s', '-M', 'deposit=10'],
201
- { input: '' },
185
+ { env: { MPPX_PRIVATE_KEY: testPrivateKey } },
202
186
  )
203
- expect(stdout).toContain('scraped-content')
187
+ expect(output).toContain('scraped-content')
204
188
  } finally {
205
189
  httpServer.close()
206
190
  }
207
191
  })
208
192
 
209
- test(
210
- '--channel reuse: second request reuses existing channel',
211
- { timeout: 120_000 },
212
- async () => {
213
- await fundAccount({ address: testAccount.address, token: Addresses.pathUsd })
214
- await fundAccount({ address: testAccount.address, token: asset })
215
-
216
- const escrow = await deployEscrow()
217
- const store = Store.memory()
218
- const server = Mppx_server.create({
219
- methods: [
220
- tempo.session({
221
- account: accounts[0],
222
- store,
223
- getClient: () => client,
224
- currency: asset,
225
- escrowContract: escrow,
226
- chainId: client.chain.id,
227
- feePayer: true,
228
- }),
229
- ],
230
- realm: 'cli-test-channel-reuse',
231
- secretKey: 'cli-test-secret',
232
- })
233
-
234
- const httpServer = await Http.createServer(async (req, res) => {
235
- const result = await toNodeListener(
236
- server.session({
237
- amount: '0.001',
238
- recipient: accounts[0].address,
239
- unitType: 'page',
240
- }),
241
- )(req, res)
242
- if (result.status === 402) return
243
- res.end('scraped-content')
244
- })
245
-
246
- try {
247
- // First request: open a channel, answer "y" to proceed, "n" to close channel
248
- const first = await runAsync(
249
- [httpServer.url, '--rpc-url', rpcUrl, '--confirm', '-M', 'deposit=10'],
250
- { input: 'y\nn\n' },
251
- )
252
- expect(first.stdout).toContain('scraped-content')
253
-
254
- // Extract channel ID from stderr (logged as "Channel opened 0x...")
255
- const match = first.stderr.match(/Channel opened (0x[0-9a-fA-F]+)/)
256
- expect(match).toBeTruthy()
257
- const channelId = match![1]!
258
-
259
- // Second request: reuse the channel via -M channel=<id>
260
- const second = await runAsync(
261
- [
262
- httpServer.url,
263
- '--rpc-url',
264
- rpcUrl,
265
- '-s',
266
- '-M',
267
- `channel=${channelId}`,
268
- '-M',
269
- 'deposit=10',
270
- ],
271
- { input: '' },
272
- )
273
- expect(second.stdout).toContain('scraped-content')
274
- } finally {
275
- httpServer.close()
276
- }
277
- },
278
- )
279
-
280
193
  test('error: --fail exits on server error', { timeout: 60_000 }, async () => {
281
194
  const httpServer = await Http.createServer(async (_req, res) => {
282
195
  res.writeHead(500)
@@ -284,9 +197,10 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
284
197
  })
285
198
 
286
199
  try {
287
- await expect(
288
- runAsync([httpServer.url, '--rpc-url', rpcUrl, '--fail'], { input: '' }),
289
- ).rejects.toThrow()
200
+ const { exitCode } = await serve([httpServer.url, '--rpc-url', rpcUrl, '--fail'], {
201
+ env: { MPPX_PRIVATE_KEY: testPrivateKey },
202
+ })
203
+ expect(exitCode).toBe(22)
290
204
  } finally {
291
205
  httpServer.close()
292
206
  }
@@ -322,18 +236,13 @@ describe.skipIf(!process.env.VITE_STRIPE_SECRET_KEY)('stripe charge (integration
322
236
  })
323
237
 
324
238
  try {
325
- const { stdout } = await runAsync(
326
- [httpServer.url, '-M', 'paymentMethod=pm_card_visa', '-s'],
327
- {
328
- input: '',
329
- env: {
330
- ...env,
331
- MPPX_STRIPE_SECRET_KEY: stripeSecretKey,
332
- MPPX_PRIVATE_KEY: undefined as unknown as string,
333
- },
239
+ const { output } = await serve([httpServer.url, '-M', 'paymentMethod=pm_card_visa', '-s'], {
240
+ env: {
241
+ MPPX_STRIPE_SECRET_KEY: stripeSecretKey,
242
+ MPPX_PRIVATE_KEY: undefined,
334
243
  },
335
- )
336
- expect(stdout).toContain('paid')
244
+ })
245
+ expect(output).toContain('paid')
337
246
  } finally {
338
247
  httpServer.close()
339
248
  }
@@ -387,10 +296,10 @@ describe('session sse (examples/session/sse)', () => {
387
296
  })
388
297
 
389
298
  try {
390
- const { stdout } = await runAsync([httpServer.url, '--rpc-url', rpcUrl, '-M', 'deposit=10'], {
391
- input: '',
299
+ const { output } = await serve([httpServer.url, '--rpc-url', rpcUrl, '-M', 'deposit=10'], {
300
+ env: { MPPX_PRIVATE_KEY: testPrivateKey },
392
301
  })
393
- expect(stdout.trim()).toBe('Hello world!')
302
+ expect(output.trim()).toBe('Hello world!')
394
303
  } finally {
395
304
  httpServer.close()
396
305
  }
@@ -402,9 +311,10 @@ describe('session sse (examples/session/sse)', () => {
402
311
  res.end('Internal Server Error')
403
312
  })
404
313
  try {
405
- await expect(
406
- runAsync([httpServer.url, '--rpc-url', rpcUrl, '--fail'], { input: '' }),
407
- ).rejects.toThrow()
314
+ const { exitCode } = await serve([httpServer.url, '--rpc-url', rpcUrl, '--fail'], {
315
+ env: { MPPX_PRIVATE_KEY: testPrivateKey },
316
+ })
317
+ expect(exitCode).toBe(22)
408
318
  } finally {
409
319
  httpServer.close()
410
320
  }
@@ -443,16 +353,13 @@ describe('stripe charge', () => {
443
353
  })
444
354
 
445
355
  try {
446
- const { stdout } = await runAsync([appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'], {
447
- input: '',
356
+ const { output } = await serve([appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'], {
448
357
  env: {
449
- ...process.env,
450
- NODE_NO_WARNINGS: '1',
451
- MPPX_STRIPE_SECRET_KEY: 'sk_test_mock',
358
+ MPPX_STRIPE_SECRET_KEY: 'sk_test_mock_cli_value',
452
359
  MPPX_STRIPE_SPT_URL: sptServer.url,
453
360
  },
454
361
  })
455
- expect(stdout).toContain('paid')
362
+ expect(output).toContain('paid')
456
363
  } finally {
457
364
  appServer.close()
458
365
  sptServer.close()
@@ -481,16 +388,12 @@ describe('stripe charge', () => {
481
388
  })
482
389
 
483
390
  try {
484
- const result = await runAsync([appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'], {
485
- input: '',
486
- env: {
487
- ...process.env,
488
- NODE_NO_WARNINGS: '1',
489
- MPPX_STRIPE_SECRET_KEY: '',
490
- },
491
- }).catch((err) => err as Error)
492
- expect(result).toBeInstanceOf(Error)
493
- expect((result as Error).message).toContain('MPPX_STRIPE_SECRET_KEY')
391
+ const { output, exitCode } = await serve(
392
+ [appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'],
393
+ { env: { MPPX_STRIPE_SECRET_KEY: '' } },
394
+ )
395
+ expect(exitCode).toBe(2)
396
+ expect(output).toContain('MPPX_STRIPE_SECRET_KEY')
494
397
  } finally {
495
398
  appServer.close()
496
399
  }
@@ -518,16 +421,12 @@ describe('stripe charge', () => {
518
421
  })
519
422
 
520
423
  try {
521
- const result = await runAsync([appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'], {
522
- input: '',
523
- env: {
524
- ...process.env,
525
- NODE_NO_WARNINGS: '1',
526
- MPPX_STRIPE_SECRET_KEY: 'sk_live_fake',
527
- },
528
- }).catch((err) => err as Error)
529
- expect(result).toBeInstanceOf(Error)
530
- expect((result as Error).message).toContain('test mode')
424
+ const { output, exitCode } = await serve(
425
+ [appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'],
426
+ { env: { MPPX_STRIPE_SECRET_KEY: 'sk_live_fake' } },
427
+ )
428
+ expect(exitCode).toBe(2)
429
+ expect(output).toContain('test mode')
531
430
  } finally {
532
431
  appServer.close()
533
432
  }
@@ -539,13 +438,21 @@ describe('stripe charge', () => {
539
438
  // TODO: investigate account tests timing out in CI (secret-tool/gnome-keyring hangs)
540
439
  // ---------------------------------------------------------------------------
541
440
  describe.skipIf(!!process.env.CI)('account', () => {
542
- // Env without MPPX_PRIVATE_KEY so account commands use the keychain
441
+ const binPath = path.resolve(import.meta.dirname, 'bin.ts')
442
+ const cwd = path.resolve(import.meta.dirname, '..')
543
443
  const accountEnv = { ...process.env, NODE_NO_WARNINGS: '1' }
544
444
  const prefix = `__mppx_test_${Date.now()}`
545
445
  const createdAccounts: string[] = []
546
446
 
547
447
  function accountRun(args: string[], options?: { input?: string }) {
548
- return runRaw(args, { ...options, env: accountEnv })
448
+ const result = spawnSync('node', ['--import', 'tsx', binPath, ...args], {
449
+ encoding: 'utf8',
450
+ cwd,
451
+ timeout: 60_000,
452
+ ...(options?.input !== undefined && { input: options.input }),
453
+ env: accountEnv,
454
+ })
455
+ return { stdout: result.stdout ?? '', stderr: result.stderr ?? '', status: result.status }
549
456
  }
550
457
 
551
458
  function createAccount(name: string) {
@@ -577,9 +484,7 @@ describe.skipIf(!!process.env.CI)('account', () => {
577
484
  test('create: duplicate name exits with message', () => {
578
485
  const name = `${prefix}_dup`
579
486
  createAccount(name)
580
- // Second create with same name (non-interactive, stdin closed) should not succeed
581
487
  const result = accountRun(['account', 'create', '--account', name], { input: '' })
582
- // The CLI prompts for a different name; with empty stdin it exits
583
488
  expect(result.stdout).not.toContain('saved to keychain')
584
489
  })
585
490
 
@@ -639,11 +544,9 @@ describe.skipIf(!!process.env.CI)('account', () => {
639
544
  const result = deleteAccount(name)
640
545
  expect(result.status).toBe(0)
641
546
  expect(result.stdout).toContain(`Account "${name}" deleted`)
642
- // Remove from cleanup list since already deleted
643
547
  const idx = createdAccounts.indexOf(name)
644
548
  if (idx !== -1) createdAccounts.splice(idx, 1)
645
549
 
646
- // Verify it's gone
647
550
  const view = accountRun(['account', 'view', '--account', name])
648
551
  expect(view.status).not.toBe(0)
649
552
  })
@@ -664,7 +567,6 @@ describe.skipIf(!!process.env.CI)('account', () => {
664
567
  test('unknown action exits non-zero', () => {
665
568
  const result = accountRun(['account', 'bogus'])
666
569
  expect(result.status).not.toBe(0)
667
- expect(result.stderr).toContain('Unknown action: bogus')
668
570
  })
669
571
 
670
572
  // --- no action ---
@@ -672,57 +574,83 @@ describe.skipIf(!!process.env.CI)('account', () => {
672
574
  test('no action prints help', () => {
673
575
  const result = accountRun(['account'])
674
576
  expect(result.status).toBe(0)
675
- expect(result.stdout).toContain('account [action]')
577
+ expect(result.stdout).toContain('account')
676
578
  })
677
579
  })
678
580
 
679
- test('mppx --help', () => {
680
- const { version } = require('../package.json') as { version: string }
681
- const stdout = run(['--help']).replace(`mppx/${version}`, 'mppx/x.y.z')
682
- expect(stdout).toMatchInlineSnapshot(`
683
- "mppx/x.y.z
684
-
685
- Usage:
686
- $ mppx [url]
687
-
688
- Commands:
689
- [url] Make HTTP request with automatic payment
690
- account [action] Manage accounts (create, default, delete, fund, list, view)
691
-
692
- For more info, run any command with the \`--help\` flag:
693
- $ mppx --help
694
- $ mppx account --help
695
-
696
- Actions:
697
- create Create new account
698
- default Set default account
699
- delete Delete account
700
- fund Fund account with testnet tokens
701
- list List all accounts
702
- view View account address
703
-
704
- Options:
705
- -a, --account <name> Account name (env: MPPX_ACCOUNT)
706
- -d, --data <data> Send request body (implies POST unless -X is set)
707
- -f, --fail Fail silently on HTTP errors (exit 22)
708
- -i, --include Include response headers in output
709
- -k, --insecure Skip TLS certificate verification (true for localhost/.local)
710
- -r, --rpc-url <url> RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)
711
- -s, --silent Silent mode (suppress progress and info)
712
- -v, --verbose Show request/response headers
713
- -A, --user-agent <ua> Set User-Agent header
714
- -H, --header <header> Add header (repeatable)
715
- -L, --location Follow redirects
716
- -X, --method <method> HTTP method
717
- -M, --method-opt <opt> Method-specific option (key=value, repeatable)
718
- --confirm Show confirmation prompts
719
- --json <json> Send JSON body (sets Content-Type and Accept, implies POST)
720
- -V, --version Display version number
721
- -h, --help Display this message
722
-
723
- Examples:
724
- mppx example.com/content
725
- mppx example.com/api --json '{"key":"value"}'
726
- "
727
- `)
581
+ test('mppx --help', async () => {
582
+ const { output } = await serve(['--help'])
583
+ expect(output).toContain('mppx')
584
+ expect(output).toContain('<url>')
585
+ expect(output).toContain('account')
586
+ expect(output).toContain('sign')
587
+ })
588
+
589
+ // ---------------------------------------------------------------------------
590
+ // sign
591
+ // ---------------------------------------------------------------------------
592
+ describe('sign', () => {
593
+ const validChallenge =
594
+ 'Payment id="test", realm="test", method="tempo", intent="charge", request="eyJhbW91bnQiOiIxMDAwIiwiY3VycmVuY3kiOiIweDIwYzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEiLCJyZWNpcGllbnQiOiIweDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEiLCJtZXRob2REZXRhaWxzIjp7ImNoYWluSWQiOjEzMzd9fQ"'
595
+
596
+ test('--dry-run: validates a valid challenge', async () => {
597
+ const { exitCode, stderr } = await serve(['sign', '--dry-run', '--challenge', validChallenge])
598
+ expect(exitCode).toBeUndefined()
599
+ expect(stderr).toContain('Challenge is valid')
600
+ })
601
+
602
+ test('--dry-run: rejects an invalid challenge', async () => {
603
+ const { exitCode, output } = await serve([
604
+ 'sign',
605
+ '--dry-run',
606
+ '--challenge',
607
+ 'not a valid challenge',
608
+ ])
609
+ expect(exitCode).toBe(2)
610
+ expect(output).toContain('INVALID_CHALLENGE')
611
+ })
612
+
613
+ test('error: no challenge provided', async () => {
614
+ const { exitCode, output } = await serve(['sign'])
615
+ expect(exitCode).toBe(2)
616
+ expect(output).toContain('No challenge provided')
617
+ })
618
+
619
+ test('error: unsupported method', async () => {
620
+ const challenge = 'Payment id="x", realm="x", method="unknown", intent="charge", request="e30"'
621
+ const { exitCode, output } = await serve(['sign', '--challenge', challenge])
622
+ expect(exitCode).toBe(2)
623
+ expect(output).toContain('Unsupported payment method')
624
+ })
625
+
626
+ test('error: no account for tempo', async () => {
627
+ const { exitCode, output } = await serve(
628
+ ['sign', '--challenge', validChallenge, '--account', 'nonexistent-sign-test'],
629
+ { env: { MPPX_PRIVATE_KEY: undefined } },
630
+ )
631
+ expect(exitCode).toBe(69)
632
+ expect(output).toContain('not found')
633
+ })
634
+
635
+ test('happy path: signs a tempo charge challenge', { timeout: 120_000 }, async () => {
636
+ const { output, stderr, exitCode } = await serve(
637
+ ['sign', '--challenge', validChallenge, '--rpc-url', rpcUrl],
638
+ { env: { MPPX_PRIVATE_KEY: testPrivateKey } },
639
+ )
640
+ if (exitCode) console.info('SIGN DEBUG output:', output, 'stderr:', stderr)
641
+ expect(exitCode).toBeUndefined()
642
+ expect(output.trim()).toMatch(/^Payment\s+\S+/)
643
+ })
644
+
645
+ test('happy path: --json outputs authorization and from', { timeout: 120_000 }, async () => {
646
+ const { output, stderr, exitCode } = await serve(
647
+ ['sign', '--challenge', validChallenge, '--rpc-url', rpcUrl, '--json'],
648
+ { env: { MPPX_PRIVATE_KEY: testPrivateKey } },
649
+ )
650
+ if (exitCode) console.info('SIGN JSON DEBUG output:', output, 'stderr:', stderr)
651
+ expect(exitCode).toBeUndefined()
652
+ const parsed = JSON.parse(output.trim())
653
+ expect(parsed.authorization).toMatch(/^Payment\s+\S+/)
654
+ expect(parsed.from).toMatch(/^0x[0-9a-fA-F]{40}$/)
655
+ })
728
656
  })