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.
- package/CHANGELOG.md +266 -0
- package/README.md +30 -6
- package/dist/bin.js +2 -2
- package/dist/bin.js.map +1 -1
- package/dist/cli/account.d.ts +53 -0
- package/dist/cli/account.d.ts.map +1 -0
- package/dist/cli/account.js +156 -0
- package/dist/cli/account.js.map +1 -0
- package/dist/{cli.d.ts → cli/cli.d.ts} +4 -3
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +852 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/config.d.ts +39 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +30 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/internal.d.ts +16 -0
- package/dist/cli/internal.d.ts.map +1 -0
- package/dist/cli/internal.js +58 -0
- package/dist/cli/internal.js.map +1 -0
- package/dist/cli/plugins/index.d.ts +4 -0
- package/dist/cli/plugins/index.d.ts.map +1 -0
- package/dist/cli/plugins/index.js +4 -0
- package/dist/cli/plugins/index.js.map +1 -0
- package/dist/cli/plugins/plugin.d.ts +68 -0
- package/dist/cli/plugins/plugin.d.ts.map +1 -0
- package/dist/cli/plugins/plugin.js +4 -0
- package/dist/cli/plugins/plugin.js.map +1 -0
- package/dist/cli/plugins/stripe.d.ts +2 -0
- package/dist/cli/plugins/stripe.d.ts.map +1 -0
- package/dist/cli/plugins/stripe.js +118 -0
- package/dist/cli/plugins/stripe.js.map +1 -0
- package/dist/cli/plugins/tempo.d.ts +11 -0
- package/dist/cli/plugins/tempo.d.ts.map +1 -0
- package/dist/cli/plugins/tempo.js +706 -0
- package/dist/cli/plugins/tempo.js.map +1 -0
- package/dist/cli/utils.d.ts +93 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +274 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/tempo/client/Methods.d.ts +1 -1
- package/dist/tempo/client/Session.d.ts +2 -2
- package/dist/tempo/internal/defaults.d.ts +1 -1
- package/dist/tempo/internal/defaults.js +1 -1
- package/package.json +12 -1
- package/src/bin.ts +2 -2
- package/src/cli/account.ts +157 -0
- package/src/{cli.test.ts → cli/cli.test.ts} +107 -51
- package/src/cli/cli.ts +907 -0
- package/src/cli/config.test.ts +82 -0
- package/src/cli/config.ts +44 -0
- package/src/cli/internal.ts +72 -0
- package/src/cli/plugins/index.ts +3 -0
- package/src/cli/plugins/plugin.ts +73 -0
- package/src/cli/plugins/stripe.ts +143 -0
- package/src/cli/plugins/tempo.ts +842 -0
- package/src/cli/utils.ts +336 -0
- package/src/tempo/internal/defaults.test.ts +1 -1
- package/src/tempo/internal/defaults.ts +1 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -1992
- package/dist/cli.js.map +0 -1
- 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
|
|
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
|
})
|