mppx 0.4.1 → 0.4.3

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 (63) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/README.md +30 -6
  3. package/dist/bin.js +2 -2
  4. package/dist/bin.js.map +1 -1
  5. package/dist/cli/account.d.ts +53 -0
  6. package/dist/cli/account.d.ts.map +1 -0
  7. package/dist/cli/account.js +156 -0
  8. package/dist/cli/account.js.map +1 -0
  9. package/dist/{cli.d.ts → cli/cli.d.ts} +4 -3
  10. package/dist/cli/cli.d.ts.map +1 -0
  11. package/dist/cli/cli.js +852 -0
  12. package/dist/cli/cli.js.map +1 -0
  13. package/dist/cli/config.d.ts +39 -0
  14. package/dist/cli/config.d.ts.map +1 -0
  15. package/dist/cli/config.js +30 -0
  16. package/dist/cli/config.js.map +1 -0
  17. package/dist/cli/internal.d.ts +16 -0
  18. package/dist/cli/internal.d.ts.map +1 -0
  19. package/dist/cli/internal.js +58 -0
  20. package/dist/cli/internal.js.map +1 -0
  21. package/dist/cli/plugins/index.d.ts +4 -0
  22. package/dist/cli/plugins/index.d.ts.map +1 -0
  23. package/dist/cli/plugins/index.js +4 -0
  24. package/dist/cli/plugins/index.js.map +1 -0
  25. package/dist/cli/plugins/plugin.d.ts +68 -0
  26. package/dist/cli/plugins/plugin.d.ts.map +1 -0
  27. package/dist/cli/plugins/plugin.js +4 -0
  28. package/dist/cli/plugins/plugin.js.map +1 -0
  29. package/dist/cli/plugins/stripe.d.ts +2 -0
  30. package/dist/cli/plugins/stripe.d.ts.map +1 -0
  31. package/dist/cli/plugins/stripe.js +118 -0
  32. package/dist/cli/plugins/stripe.js.map +1 -0
  33. package/dist/cli/plugins/tempo.d.ts +11 -0
  34. package/dist/cli/plugins/tempo.d.ts.map +1 -0
  35. package/dist/cli/plugins/tempo.js +706 -0
  36. package/dist/cli/plugins/tempo.js.map +1 -0
  37. package/dist/cli/utils.d.ts +93 -0
  38. package/dist/cli/utils.d.ts.map +1 -0
  39. package/dist/cli/utils.js +274 -0
  40. package/dist/cli/utils.js.map +1 -0
  41. package/dist/tempo/client/Methods.d.ts +1 -1
  42. package/dist/tempo/client/Session.d.ts +2 -2
  43. package/dist/tempo/internal/defaults.d.ts +1 -1
  44. package/dist/tempo/internal/defaults.js +1 -1
  45. package/package.json +12 -1
  46. package/src/bin.ts +2 -2
  47. package/src/cli/account.ts +157 -0
  48. package/src/{cli.test.ts → cli/cli.test.ts} +107 -51
  49. package/src/cli/cli.ts +907 -0
  50. package/src/cli/config.test.ts +82 -0
  51. package/src/cli/config.ts +44 -0
  52. package/src/cli/internal.ts +72 -0
  53. package/src/cli/plugins/index.ts +3 -0
  54. package/src/cli/plugins/plugin.ts +73 -0
  55. package/src/cli/plugins/stripe.ts +143 -0
  56. package/src/cli/plugins/tempo.ts +842 -0
  57. package/src/cli/utils.ts +336 -0
  58. package/src/tempo/internal/defaults.test.ts +1 -1
  59. package/src/tempo/internal/defaults.ts +1 -1
  60. package/dist/cli.d.ts.map +0 -1
  61. package/dist/cli.js +0 -1992
  62. package/dist/cli.js.map +0 -1
  63. package/src/cli.ts +0 -2178
@@ -0,0 +1,157 @@
1
+ import * as child from 'node:child_process'
2
+ import * as fs from 'node:fs'
3
+ import * as os from 'node:os'
4
+ import * as path from 'node:path'
5
+
6
+ const SERVICE_NAME = 'mppx'
7
+
8
+ export function execCommand(
9
+ command: string,
10
+ args: string[],
11
+ ): Promise<{ stdout: string; stderr: string; error: Error | null }> {
12
+ return new Promise((resolve) => {
13
+ child.execFile(command, args, (error, stdout, stderr) => {
14
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim(), error })
15
+ })
16
+ })
17
+ }
18
+
19
+ export function createDefaultStore() {
20
+ const configPath = path.join(
21
+ process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
22
+ 'mppx',
23
+ 'default',
24
+ )
25
+ return {
26
+ get(): string {
27
+ try {
28
+ return fs.readFileSync(configPath, 'utf-8').trim() || 'main'
29
+ } catch {
30
+ return 'main'
31
+ }
32
+ },
33
+ set(value: string): void {
34
+ fs.mkdirSync(path.dirname(configPath), { recursive: true })
35
+ fs.writeFileSync(configPath, value, 'utf-8')
36
+ },
37
+ clear(): void {
38
+ try {
39
+ fs.unlinkSync(configPath)
40
+ } catch {}
41
+ },
42
+ }
43
+ }
44
+
45
+ export function resolveAccountName(explicit?: string): string {
46
+ if (explicit) return explicit
47
+ if (process.env.MPPX_ACCOUNT?.trim()) return process.env.MPPX_ACCOUNT
48
+ return createDefaultStore().get()
49
+ }
50
+
51
+ // biome-ignore format: compact shell commands
52
+ export function createKeychain(account = 'main') {
53
+ const service = SERVICE_NAME
54
+ return {
55
+ async list(): Promise<string[]> {
56
+ const platform = os.platform()
57
+ if (platform === 'darwin') {
58
+ const { stdout, error } = await execCommand('security', ['dump-keychain'])
59
+ if (error) return []
60
+ const accounts: string[] = []
61
+ const blocks = stdout.split('keychain:')
62
+ for (const block of blocks) {
63
+ const serviceMatch = block.match(/"svce"<blob>="([^"]*)"/)
64
+ const accountMatch = block.match(/"acct"<blob>="([^"]*)"/)
65
+ if (serviceMatch?.[1] === service && accountMatch?.[1]) accounts.push(accountMatch[1])
66
+ }
67
+ return accounts
68
+ }
69
+ if (platform === 'linux') {
70
+ const { stdout, stderr, error } = await execCommand('secret-tool', ['search', '--all', '--unlock', 'service', service])
71
+ if (error) return []
72
+ const combined = `${stdout}\n${stderr}`
73
+ const accounts: string[] = []
74
+ const matches = combined.matchAll(/\baccount = (.+)/g)
75
+ for (const match of matches) if (match[1]) accounts.push(match[1])
76
+ return accounts
77
+ }
78
+ throw new Error(`Unsupported platform: ${platform}`)
79
+ },
80
+ async get(): Promise<string | undefined> {
81
+ const platform = os.platform()
82
+ if (platform === 'darwin') {
83
+ const { stdout, error } = await execCommand('security', ['find-generic-password', '-s', service, '-a', account, '-w'])
84
+ return error ? undefined : stdout
85
+ }
86
+ if (platform === 'linux') {
87
+ const { stdout, error } = await execCommand('secret-tool', ['lookup', 'service', service, 'account', account])
88
+ return error ? undefined : stdout || undefined
89
+ }
90
+ throw new Error(`Unsupported platform: ${platform}`)
91
+ },
92
+ async set(value: string): Promise<void> {
93
+ const platform = os.platform()
94
+ if (platform === 'darwin') {
95
+ await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
96
+ const { error } = await execCommand('security', ['add-generic-password', '-s', service, '-a', account, '-w', value])
97
+ if (error) throw error
98
+ return
99
+ }
100
+ if (platform === 'linux') {
101
+ const proc = child.execFile('secret-tool', ['store', '--label', `${service} ${account}`, 'service', service, 'account', account])
102
+ proc.stdin?.write(value)
103
+ proc.stdin?.end()
104
+ return new Promise((resolve, reject) => {
105
+ proc.on('close', (code) => {
106
+ if (code === 0) resolve()
107
+ else reject(new Error(`secret-tool exited with code ${code}`))
108
+ })
109
+ proc.on('error', reject)
110
+ })
111
+ }
112
+ throw new Error(`Unsupported platform: ${platform}`)
113
+ },
114
+ async delete(): Promise<void> {
115
+ const platform = os.platform()
116
+ if (platform === 'darwin') {
117
+ await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
118
+ return
119
+ }
120
+ if (platform === 'linux') {
121
+ await execCommand('secret-tool', ['clear', 'service', service, 'account', account])
122
+ return
123
+ }
124
+ throw new Error(`Unsupported platform: ${platform}`)
125
+ },
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Resolve a CLI account to a viem `LocalAccount`.
131
+ *
132
+ * Resolution order:
133
+ * 1. `MPPX_PRIVATE_KEY` environment variable
134
+ * 2. OS keychain lookup for the named account
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * import { resolveAccount } from 'mppx/cli'
139
+ * import { tempo } from 'mppx/client'
140
+ *
141
+ * export default defineConfig({
142
+ * methods: [tempo({ account: await resolveAccount() })],
143
+ * })
144
+ * ```
145
+ */
146
+ export async function resolveAccount(name?: string) {
147
+ const { privateKeyToAccount } = await import('viem/accounts')
148
+
149
+ const envKey = process.env.MPPX_PRIVATE_KEY?.trim()
150
+ if (envKey) return privateKeyToAccount(envKey as `0x${string}`)
151
+
152
+ const accountName = resolveAccountName(name)
153
+ const key = await createKeychain(accountName).get()
154
+ if (key) return privateKeyToAccount(key as `0x${string}`)
155
+
156
+ throw new Error(`Account "${accountName}" not found.`)
157
+ }
@@ -1,4 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process'
2
+ import * as fs from 'node:fs'
3
+ import * as os from 'node:os'
2
4
  import * as path from 'node:path'
3
5
  import { parseUnits } from 'viem'
4
6
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
@@ -8,12 +10,12 @@ import * as Http from '~test/Http.js'
8
10
  import { rpcUrl } from '~test/tempo/prool.js'
9
11
  import { deployEscrow } from '~test/tempo/session.js'
10
12
  import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
13
+ import * as Store from '../Store.js'
14
+ import * as Mppx_server from '../server/Mppx.js'
15
+ import { toNodeListener } from '../server/Mppx.js'
16
+ import { stripe as stripe_server } from '../stripe/server/Methods.js'
17
+ import { tempo } from '../tempo/server/Methods.js'
11
18
  import cli from './cli.js'
12
- import * as Store from './Store.js'
13
- import * as Mppx_server from './server/Mppx.js'
14
- import { toNodeListener } from './server/Mppx.js'
15
- import { stripe as stripe_server } from './stripe/server/Methods.js'
16
- import { tempo } from './tempo/server/Methods.js'
17
19
 
18
20
  const testPrivateKey = generatePrivateKey()
19
21
  const testAccount = privateKeyToAccount(testPrivateKey)
@@ -207,48 +209,6 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
207
209
  })
208
210
  })
209
211
 
210
- describe.skipIf(!process.env.VITE_STRIPE_SECRET_KEY)('stripe charge (integration)', () => {
211
- test('happy path: makes Stripe payment via real API', { timeout: 120_000 }, async () => {
212
- const stripeSecretKey = process.env.VITE_STRIPE_SECRET_KEY!
213
-
214
- const server = Mppx_server.create({
215
- methods: [
216
- stripe_server.charge({
217
- secretKey: stripeSecretKey,
218
- networkId: 'internal',
219
- paymentMethodTypes: ['card'],
220
- }),
221
- ],
222
- realm: 'cli-test-stripe',
223
- secretKey: 'cli-test-secret',
224
- })
225
-
226
- const httpServer = await Http.createServer(async (req, res) => {
227
- const result = await toNodeListener(
228
- server.charge({
229
- amount: '1',
230
- currency: 'usd',
231
- decimals: 2,
232
- }),
233
- )(req, res)
234
- if (result.status === 402) return
235
- res.end('paid')
236
- })
237
-
238
- try {
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,
243
- },
244
- })
245
- expect(output).toContain('paid')
246
- } finally {
247
- httpServer.close()
248
- }
249
- })
250
- })
251
-
252
212
  describe('session sse (examples/session/sse)', () => {
253
213
  test('streams SSE tokens to stdout', { timeout: 120_000 }, async () => {
254
214
  await fundAccount({ address: testAccount.address, token: Addresses.pathUsd })
@@ -438,8 +398,8 @@ describe('stripe charge', () => {
438
398
  // TODO: investigate account tests timing out in CI (secret-tool/gnome-keyring hangs)
439
399
  // ---------------------------------------------------------------------------
440
400
  describe.skipIf(!!process.env.CI)('account', () => {
441
- const binPath = path.resolve(import.meta.dirname, 'bin.ts')
442
- const cwd = path.resolve(import.meta.dirname, '..')
401
+ const binPath = path.resolve(import.meta.dirname, '../bin.ts')
402
+ const cwd = path.resolve(import.meta.dirname, '../..')
443
403
  const accountEnv = { ...process.env, NODE_NO_WARNINGS: '1' }
444
404
  const prefix = `__mppx_test_${Date.now()}`
445
405
  const createdAccounts: string[] = []
@@ -578,6 +538,103 @@ describe.skipIf(!!process.env.CI)('account', () => {
578
538
  })
579
539
  })
580
540
 
541
+ // ---------------------------------------------------------------------------
542
+ // init
543
+ // ---------------------------------------------------------------------------
544
+ describe('init', () => {
545
+ let tmpDir: string
546
+
547
+ function setup(files?: Record<string, string>) {
548
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-init-'))
549
+ if (files) {
550
+ for (const [name, content] of Object.entries(files))
551
+ fs.writeFileSync(path.join(tmpDir, name), content)
552
+ }
553
+ }
554
+
555
+ function teardown() {
556
+ fs.rmSync(tmpDir, { recursive: true, force: true })
557
+ }
558
+
559
+ test('creates mppx.config.ts when tsconfig.json exists', async () => {
560
+ setup({ 'tsconfig.json': '{}' })
561
+ const origCwd = process.cwd()
562
+ process.chdir(tmpDir)
563
+ try {
564
+ const { output, exitCode } = await serve(['init'])
565
+ expect(exitCode).toBeUndefined()
566
+ expect(output).toContain('Created mppx.config.ts')
567
+ const content = fs.readFileSync(path.join(tmpDir, 'mppx.config.ts'), 'utf-8')
568
+ expect(content).toContain("import { defineConfig } from 'mppx/cli'")
569
+ expect(content).toContain('methods:')
570
+ } finally {
571
+ process.chdir(origCwd)
572
+ teardown()
573
+ }
574
+ })
575
+
576
+ test('creates mppx.config.mjs when package.json has type:module', async () => {
577
+ setup({ 'package.json': '{"type":"module"}' })
578
+ const origCwd = process.cwd()
579
+ process.chdir(tmpDir)
580
+ try {
581
+ const { output, exitCode } = await serve(['init'])
582
+ expect(exitCode).toBeUndefined()
583
+ expect(output).toContain('Created mppx.config.mjs')
584
+ expect(fs.existsSync(path.join(tmpDir, 'mppx.config.mjs'))).toBe(true)
585
+ } finally {
586
+ process.chdir(origCwd)
587
+ teardown()
588
+ }
589
+ })
590
+
591
+ test('creates mppx.config.js as fallback', async () => {
592
+ setup()
593
+ const origCwd = process.cwd()
594
+ process.chdir(tmpDir)
595
+ try {
596
+ const { output, exitCode } = await serve(['init'])
597
+ expect(exitCode).toBeUndefined()
598
+ expect(output).toContain('Created mppx.config.js')
599
+ expect(fs.existsSync(path.join(tmpDir, 'mppx.config.js'))).toBe(true)
600
+ } finally {
601
+ process.chdir(origCwd)
602
+ teardown()
603
+ }
604
+ })
605
+
606
+ test('errors when config already exists', async () => {
607
+ setup({ 'tsconfig.json': '{}', 'mppx.config.ts': 'existing' })
608
+ const origCwd = process.cwd()
609
+ process.chdir(tmpDir)
610
+ try {
611
+ const { output, exitCode } = await serve(['init'])
612
+ expect(exitCode).toBe(1)
613
+ expect(output).toContain('already exists')
614
+ expect(fs.readFileSync(path.join(tmpDir, 'mppx.config.ts'), 'utf-8')).toBe('existing')
615
+ } finally {
616
+ process.chdir(origCwd)
617
+ teardown()
618
+ }
619
+ })
620
+
621
+ test('--force overwrites existing config', async () => {
622
+ setup({ 'tsconfig.json': '{}', 'mppx.config.ts': 'existing' })
623
+ const origCwd = process.cwd()
624
+ process.chdir(tmpDir)
625
+ try {
626
+ const { output, exitCode } = await serve(['init', '--force'])
627
+ expect(exitCode).toBeUndefined()
628
+ expect(output).toContain('Created mppx.config.ts')
629
+ const content = fs.readFileSync(path.join(tmpDir, 'mppx.config.ts'), 'utf-8')
630
+ expect(content).toContain('defineConfig')
631
+ } finally {
632
+ process.chdir(origCwd)
633
+ teardown()
634
+ }
635
+ })
636
+ })
637
+
581
638
  test('mppx --help', async () => {
582
639
  const { output } = await serve(['--help'])
583
640
  expect(output).toContain('mppx')
@@ -642,7 +699,7 @@ describe('sign', () => {
642
699
  expect(output.trim()).toMatch(/^Payment\s+\S+/)
643
700
  })
644
701
 
645
- test('happy path: --json outputs authorization and from', { timeout: 120_000 }, async () => {
702
+ test('happy path: --json outputs authorization', { timeout: 120_000 }, async () => {
646
703
  const { output, stderr, exitCode } = await serve(
647
704
  ['sign', '--challenge', validChallenge, '--rpc-url', rpcUrl, '--json'],
648
705
  { env: { MPPX_PRIVATE_KEY: testPrivateKey } },
@@ -651,6 +708,5 @@ describe('sign', () => {
651
708
  expect(exitCode).toBeUndefined()
652
709
  const parsed = JSON.parse(output.trim())
653
710
  expect(parsed.authorization).toMatch(/^Payment\s+\S+/)
654
- expect(parsed.from).toMatch(/^0x[0-9a-fA-F]{40}$/)
655
711
  })
656
712
  })