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,82 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as os from 'node:os'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import { defineConfig } from './config.js'
|
|
5
|
+
import { loadConfig } from './internal.js'
|
|
6
|
+
|
|
7
|
+
let tmpDir: string
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-config-test-'))
|
|
11
|
+
vi.stubEnv('MPPX_CONFIG', '')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.unstubAllEnvs()
|
|
16
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('defineConfig', () => {
|
|
20
|
+
test('returns the config object as-is', () => {
|
|
21
|
+
const config = defineConfig({ methods: [] })
|
|
22
|
+
expect(config).toEqual({ methods: [] })
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('loadConfig', () => {
|
|
27
|
+
test('returns undefined when no config file exists', async () => {
|
|
28
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tmpDir)
|
|
29
|
+
const config = await loadConfig()
|
|
30
|
+
expect(config).toBeUndefined()
|
|
31
|
+
vi.mocked(process.cwd).mockRestore()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('loads config from MPPX_CONFIG env var', async () => {
|
|
35
|
+
const configPath = path.join(tmpDir, 'custom.mjs')
|
|
36
|
+
fs.writeFileSync(configPath, 'export default { methods: [] }')
|
|
37
|
+
vi.stubEnv('MPPX_CONFIG', configPath)
|
|
38
|
+
const result = await loadConfig()
|
|
39
|
+
expect(result?.config).toEqual({ methods: [] })
|
|
40
|
+
expect(result?.path).toBe(configPath)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('returns undefined when MPPX_CONFIG points to nonexistent file', async () => {
|
|
44
|
+
vi.stubEnv('MPPX_CONFIG', path.join(tmpDir, 'nonexistent.ts'))
|
|
45
|
+
const config = await loadConfig()
|
|
46
|
+
expect(config).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('loads mppx.config.mjs from cwd', async () => {
|
|
50
|
+
const configPath = path.join(tmpDir, 'mppx.config.mjs')
|
|
51
|
+
fs.writeFileSync(configPath, 'export default { methods: [] }')
|
|
52
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tmpDir)
|
|
53
|
+
const result = await loadConfig()
|
|
54
|
+
expect(result?.config).toEqual({ methods: [] })
|
|
55
|
+
expect(result?.path).toBe(configPath)
|
|
56
|
+
vi.mocked(process.cwd).mockRestore()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('walks up from cwd to find config', async () => {
|
|
60
|
+
const nested = path.join(tmpDir, 'a', 'b', 'c')
|
|
61
|
+
fs.mkdirSync(nested, { recursive: true })
|
|
62
|
+
const configPath = path.join(tmpDir, 'mppx.config.mjs')
|
|
63
|
+
fs.writeFileSync(configPath, 'export default { methods: [] }')
|
|
64
|
+
vi.spyOn(process, 'cwd').mockReturnValue(nested)
|
|
65
|
+
const result = await loadConfig()
|
|
66
|
+
expect(result?.config).toEqual({ methods: [] })
|
|
67
|
+
expect(result?.path).toBe(configPath)
|
|
68
|
+
vi.mocked(process.cwd).mockRestore()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('MPPX_CONFIG takes priority over cwd config', async () => {
|
|
72
|
+
fs.writeFileSync(path.join(tmpDir, 'mppx.config.mjs'), 'export default { methods: ["cwd"] }')
|
|
73
|
+
const envConfig = path.join(tmpDir, 'env.mjs')
|
|
74
|
+
fs.writeFileSync(envConfig, 'export default { methods: ["env"] }')
|
|
75
|
+
vi.stubEnv('MPPX_CONFIG', envConfig)
|
|
76
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tmpDir)
|
|
77
|
+
const result = await loadConfig()
|
|
78
|
+
expect(result?.config).toEqual({ methods: ['env'] })
|
|
79
|
+
expect(result?.path).toBe(envConfig)
|
|
80
|
+
vi.mocked(process.cwd).mockRestore()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export { resolveAccount } from './account.js'
|
|
2
|
+
|
|
3
|
+
import type * as Mppx from '../client/Mppx.js'
|
|
4
|
+
import type { Plugin } from './plugins/plugin.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Define mppx configuration file
|
|
8
|
+
*
|
|
9
|
+
* @example Add plugins for more custom logging/handling (default: [stripe(), tempo()])
|
|
10
|
+
* ```ts
|
|
11
|
+
* // mppx.config.ts
|
|
12
|
+
* import { defineConfig } from 'mppx/cli'
|
|
13
|
+
* import { tempo } from 'mppx/cli/plugins'
|
|
14
|
+
*
|
|
15
|
+
* export default defineConfig({
|
|
16
|
+
* plugins: [tempo()],
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @example Add client methods to extend mppx support (e.g. third-party mppx packages)
|
|
21
|
+
* ```ts
|
|
22
|
+
* // mppx.config.ts
|
|
23
|
+
* import { defineConfig, resolveAccount } from 'mppx/cli'
|
|
24
|
+
* import { tempo } from 'mppx/client'
|
|
25
|
+
*
|
|
26
|
+
* export default defineConfig({
|
|
27
|
+
* methods: [tempo({ account: await resolveAccount() })],
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function defineConfig(config: defineConfig.Config): defineConfig.Config {
|
|
32
|
+
return config
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export declare namespace defineConfig {
|
|
36
|
+
type Config = {
|
|
37
|
+
/** Array of methods to use. */
|
|
38
|
+
methods?: Mppx.create.Config['methods'] | undefined
|
|
39
|
+
/** Array of plugins to use. */
|
|
40
|
+
plugins?: Plugin[] | undefined
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type Config = defineConfig.Config
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import type * as Challenge from '../Challenge.js'
|
|
4
|
+
import type * as Method from '../Method.js'
|
|
5
|
+
import type { Config } from './config.js'
|
|
6
|
+
import { stripe as stripePlugin, tempo as tempoPlugin } from './plugins/index.js'
|
|
7
|
+
import type { Plugin } from './plugins/plugin.js'
|
|
8
|
+
|
|
9
|
+
const builtinPlugins: Plugin[] = [tempoPlugin(), stripePlugin()]
|
|
10
|
+
|
|
11
|
+
export function resolvePlugin(
|
|
12
|
+
challenge: Challenge.Challenge,
|
|
13
|
+
config?: { plugins?: Plugin[] | undefined; methods?: any },
|
|
14
|
+
): { plugin?: Plugin | undefined; method?: Method.AnyClient | undefined } {
|
|
15
|
+
const configPlugin = config?.plugins?.find((p) => p.method === challenge.method)
|
|
16
|
+
if (configPlugin) return { plugin: configPlugin }
|
|
17
|
+
|
|
18
|
+
const builtin = builtinPlugins.find((p) => p.method === challenge.method)
|
|
19
|
+
if (builtin) return { plugin: builtin }
|
|
20
|
+
|
|
21
|
+
const configMethods = config?.methods?.flat() as Method.AnyClient[] | undefined
|
|
22
|
+
const matched = configMethods?.find(
|
|
23
|
+
(m) => m.name === challenge.method && m.intent === challenge.intent,
|
|
24
|
+
)
|
|
25
|
+
if (matched) return { method: matched }
|
|
26
|
+
|
|
27
|
+
return {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CONFIG_NAMES = ['mppx.config.ts', 'mppx.config.js', 'mppx.config.mjs'] as const
|
|
31
|
+
|
|
32
|
+
export async function loadConfig(
|
|
33
|
+
configFile?: string | undefined,
|
|
34
|
+
): Promise<{ config: Config; path: string } | undefined> {
|
|
35
|
+
const configPath = resolveConfigPath(configFile)
|
|
36
|
+
if (!configPath) return undefined
|
|
37
|
+
const mod = await import(configPath)
|
|
38
|
+
return { config: (mod.default ?? mod) as Config, path: configPath }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveConfigPath(configFile?: string | undefined): string | undefined {
|
|
42
|
+
// 0. Explicit --config flag
|
|
43
|
+
if (configFile) {
|
|
44
|
+
const resolved = path.resolve(configFile)
|
|
45
|
+
if (fs.existsSync(resolved)) return resolved
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 1. Explicit env var
|
|
50
|
+
const envPath = process.env.MPPX_CONFIG?.trim()
|
|
51
|
+
if (envPath) {
|
|
52
|
+
const resolved = path.resolve(envPath)
|
|
53
|
+
if (fs.existsSync(resolved)) return resolved
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Walk up from cwd, stopping at project root
|
|
58
|
+
let dir = process.cwd()
|
|
59
|
+
while (true) {
|
|
60
|
+
for (const name of CONFIG_NAMES) {
|
|
61
|
+
const candidate = path.join(dir, name)
|
|
62
|
+
if (fs.existsSync(candidate)) return candidate
|
|
63
|
+
}
|
|
64
|
+
const isProjectRoot =
|
|
65
|
+
fs.existsSync(path.join(dir, 'package.json')) || fs.existsSync(path.join(dir, '.git'))
|
|
66
|
+
const parent = path.dirname(dir)
|
|
67
|
+
if (isProjectRoot || parent === dir) break
|
|
68
|
+
dir = parent
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type * as Challenge from '../../Challenge.js'
|
|
2
|
+
import type * as Method from '../../Method.js'
|
|
3
|
+
|
|
4
|
+
export function createPlugin(plugin: Plugin): Plugin {
|
|
5
|
+
return plugin
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Plugin {
|
|
9
|
+
/** Payment method name (e.g., 'tempo', 'stripe') */
|
|
10
|
+
method: string
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve account, client, and display info for a challenge.
|
|
14
|
+
* Returns methods for credential creation.
|
|
15
|
+
*/
|
|
16
|
+
setup(ctx: {
|
|
17
|
+
challenge: Challenge.Challenge
|
|
18
|
+
options: { account?: string | undefined; rpcUrl?: string | undefined }
|
|
19
|
+
methodOpts: Record<string, string>
|
|
20
|
+
}): Promise<{
|
|
21
|
+
/** Token symbol for display (e.g., 'PathUSD', 'USD') */
|
|
22
|
+
tokenSymbol: string
|
|
23
|
+
/** Token decimals for display */
|
|
24
|
+
tokenDecimals: number
|
|
25
|
+
/** Block explorer URL for links */
|
|
26
|
+
explorerUrl?: string | undefined
|
|
27
|
+
/** Client methods for credential creation */
|
|
28
|
+
methods: Method.AnyClient[]
|
|
29
|
+
/** Optional context to pass to createCredential */
|
|
30
|
+
credentialContext?: unknown
|
|
31
|
+
/** Override credential creation entirely (e.g., delegating to an external CLI) */
|
|
32
|
+
createCredential?: ((response: Response) => Promise<string>) | undefined
|
|
33
|
+
}>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Modify the credential request before sending.
|
|
37
|
+
* Called after credential creation, before the fetch with Authorization header.
|
|
38
|
+
* Plugins can add headers (e.g., Accept: text/event-stream for sessions).
|
|
39
|
+
*/
|
|
40
|
+
prepareCredentialRequest?(ctx: {
|
|
41
|
+
challenge: Challenge.Challenge
|
|
42
|
+
credential: string
|
|
43
|
+
headers: Record<string, string>
|
|
44
|
+
}): void
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Handle the full post-credential response lifecycle.
|
|
48
|
+
* Return `true` if fully handled (caller skips default body printing).
|
|
49
|
+
* Return `false` or leave unimplemented for default behavior (print body).
|
|
50
|
+
*/
|
|
51
|
+
handleResponse?(ctx: ResponseContext): Promise<boolean>
|
|
52
|
+
|
|
53
|
+
/** Format a receipt field for display. Return undefined to use default formatting. */
|
|
54
|
+
formatReceiptField?(key: string, value: unknown): string | undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Context passed to handleResponse */
|
|
58
|
+
export interface ResponseContext {
|
|
59
|
+
challenge: Challenge.Challenge
|
|
60
|
+
credential: string
|
|
61
|
+
response: Response
|
|
62
|
+
fetchUrl: string
|
|
63
|
+
fetchInit: RequestInit
|
|
64
|
+
silent: boolean
|
|
65
|
+
verbose: number
|
|
66
|
+
confirmEnabled: boolean
|
|
67
|
+
confirm: (msg: string, defaultYes?: boolean) => Promise<boolean>
|
|
68
|
+
tokenSymbol: string
|
|
69
|
+
tokenDecimals: number
|
|
70
|
+
explorerUrl?: string | undefined
|
|
71
|
+
/** Keys already shown in the challenge display (to avoid duplicating in receipts) */
|
|
72
|
+
shownKeys: Set<string>
|
|
73
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Errors, z } from 'incur'
|
|
2
|
+
import { stripe as stripeMethods } from '../../stripe/client/index.js'
|
|
3
|
+
import { pc } from '../utils.js'
|
|
4
|
+
import { createPlugin } from './plugin.js'
|
|
5
|
+
|
|
6
|
+
export function stripe() {
|
|
7
|
+
return createPlugin({
|
|
8
|
+
method: 'stripe',
|
|
9
|
+
|
|
10
|
+
async setup({ challenge, methodOpts }) {
|
|
11
|
+
const challengeRequest = challenge.request as Record<string, unknown>
|
|
12
|
+
const currency = challengeRequest.currency as string | undefined
|
|
13
|
+
|
|
14
|
+
const stripeOpts = parseOptions(
|
|
15
|
+
z.object({
|
|
16
|
+
paymentMethod: z.string(),
|
|
17
|
+
}),
|
|
18
|
+
methodOpts,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY
|
|
22
|
+
if (!stripeSecretKey)
|
|
23
|
+
throw new Errors.IncurError({
|
|
24
|
+
code: 'MISSING_ENV',
|
|
25
|
+
message: 'MPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.',
|
|
26
|
+
exitCode: 2,
|
|
27
|
+
})
|
|
28
|
+
if (!stripeSecretKey.startsWith('sk_test_'))
|
|
29
|
+
throw new Errors.IncurError({
|
|
30
|
+
code: 'UNSUPPORTED_MODE',
|
|
31
|
+
message:
|
|
32
|
+
'Stripe CLI payments are currently only supported in test mode (sk_test_... keys).',
|
|
33
|
+
exitCode: 2,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
tokenSymbol: currency?.toUpperCase() ?? '',
|
|
38
|
+
tokenDecimals: (challengeRequest.decimals as number | undefined) ?? 2,
|
|
39
|
+
methods: [
|
|
40
|
+
stripeMethods.charge({
|
|
41
|
+
paymentMethod: stripeOpts.paymentMethod,
|
|
42
|
+
createToken: async ({
|
|
43
|
+
paymentMethod,
|
|
44
|
+
amount,
|
|
45
|
+
currency,
|
|
46
|
+
networkId,
|
|
47
|
+
expiresAt,
|
|
48
|
+
metadata,
|
|
49
|
+
}) => {
|
|
50
|
+
const body = new URLSearchParams({
|
|
51
|
+
payment_method: paymentMethod!,
|
|
52
|
+
'usage_limits[currency]': currency,
|
|
53
|
+
'usage_limits[max_amount]': amount,
|
|
54
|
+
'usage_limits[expires_at]': expiresAt.toString(),
|
|
55
|
+
})
|
|
56
|
+
if (networkId) body.set('seller_details[network_id]', networkId)
|
|
57
|
+
if (metadata) {
|
|
58
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
59
|
+
body.set(`metadata[${key}]`, value)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sptUrl =
|
|
64
|
+
process.env.MPPX_STRIPE_SPT_URL ??
|
|
65
|
+
'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens'
|
|
66
|
+
const sptHeaders = {
|
|
67
|
+
Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
|
|
68
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let response = await globalThis.fetch(sptUrl, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: sptHeaders,
|
|
74
|
+
body,
|
|
75
|
+
})
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const errorBody = (await response.json()) as { error: { message: string } }
|
|
78
|
+
if (
|
|
79
|
+
(metadata || networkId) &&
|
|
80
|
+
errorBody.error.message.includes('Received unknown parameter')
|
|
81
|
+
) {
|
|
82
|
+
const fallbackBody = new URLSearchParams({
|
|
83
|
+
payment_method: paymentMethod!,
|
|
84
|
+
'usage_limits[currency]': currency,
|
|
85
|
+
'usage_limits[max_amount]': amount,
|
|
86
|
+
'usage_limits[expires_at]': expiresAt.toString(),
|
|
87
|
+
})
|
|
88
|
+
response = await globalThis.fetch(sptUrl, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: sptHeaders,
|
|
91
|
+
body: fallbackBody,
|
|
92
|
+
})
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const fallbackError = (await response.json()) as {
|
|
95
|
+
error: { message: string }
|
|
96
|
+
}
|
|
97
|
+
throw new Errors.IncurError({
|
|
98
|
+
code: 'STRIPE_ERROR',
|
|
99
|
+
message: `Failed to create SPT: ${fallbackError.error.message}`,
|
|
100
|
+
exitCode: 77,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
} else
|
|
104
|
+
throw new Errors.IncurError({
|
|
105
|
+
code: 'STRIPE_ERROR',
|
|
106
|
+
message: `Failed to create SPT: ${errorBody.error.message}`,
|
|
107
|
+
exitCode: 77,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
const { id } = (await response.json()) as { id: string }
|
|
111
|
+
return id
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
formatReceiptField(key, value) {
|
|
119
|
+
if (key === 'reference' && typeof value === 'string' && value.startsWith('pi_')) {
|
|
120
|
+
const isTest = process.env.MPPX_STRIPE_SECRET_KEY?.startsWith('sk_test_')
|
|
121
|
+
const url = `https://dashboard.stripe.com${isTest ? '/test' : ''}/payments/${value}`
|
|
122
|
+
return pc.link(url, value, true)
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Helpers ---
|
|
129
|
+
|
|
130
|
+
function parseOptions<const schema extends z.ZodType>(
|
|
131
|
+
schema: schema,
|
|
132
|
+
rawOptions: unknown,
|
|
133
|
+
): z.output<schema> {
|
|
134
|
+
const result = schema.safeParse(rawOptions ?? {})
|
|
135
|
+
if (result.success) return result.data
|
|
136
|
+
const summary = result.error.issues
|
|
137
|
+
.map((issue) => {
|
|
138
|
+
const path = issue.path.length ? issue.path.join('.') : 'options'
|
|
139
|
+
return `${path}: ${issue.message}`
|
|
140
|
+
})
|
|
141
|
+
.join(', ')
|
|
142
|
+
throw new Error(`Invalid CLI options (${summary})`)
|
|
143
|
+
}
|