mppx 0.3.4 → 0.3.6
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/README.md +0 -52
- package/dist/Challenge.d.ts +8 -0
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +20 -4
- package/dist/Challenge.js.map +1 -1
- package/dist/cli.js +193 -66
- package/dist/cli.js.map +1 -1
- package/dist/internal/types.d.ts +10 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.d.ts +2 -0
- package/dist/proxy/internal/Headers.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.js +2 -0
- package/dist/proxy/internal/Headers.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts +4 -0
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +4 -0
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts +2 -0
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +4 -3
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/NodeListener.d.ts +6 -0
- package/dist/server/NodeListener.d.ts.map +1 -1
- package/dist/server/NodeListener.js +6 -0
- package/dist/server/NodeListener.js.map +1 -1
- package/dist/server/Response.d.ts +17 -0
- package/dist/server/Response.d.ts.map +1 -1
- package/dist/server/Response.js +17 -0
- package/dist/server/Response.js.map +1 -1
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/internal/defaults.d.ts +34 -8
- package/dist/tempo/internal/defaults.d.ts.map +1 -1
- package/dist/tempo/internal/defaults.js +30 -8
- package/dist/tempo/internal/defaults.js.map +1 -1
- package/dist/tempo/server/Charge.js +2 -2
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +8 -3
- package/dist/tempo/server/Session.js.map +1 -1
- package/package.json +1 -1
- package/src/Challenge.test.ts +201 -11
- package/src/Challenge.ts +34 -4
- package/src/Store.test.ts +93 -0
- package/src/cli.test.ts +233 -37
- package/src/cli.ts +229 -79
- package/src/client/Transport.test.ts +4 -4
- package/src/internal/env.test.ts +42 -0
- package/src/internal/types.ts +11 -0
- package/src/proxy/internal/Headers.ts +2 -0
- package/src/proxy/internal/Route.ts +4 -0
- package/src/server/Mppx.test.ts +173 -0
- package/src/server/Mppx.ts +6 -3
- package/src/server/NodeListener.ts +6 -0
- package/src/server/Response.ts +17 -0
- package/src/server/Transport.test.ts +5 -5
- package/src/tempo/client/ChannelOps.ts +1 -1
- package/src/tempo/internal/defaults.test.ts +94 -0
- package/src/tempo/internal/defaults.ts +41 -8
- package/src/tempo/server/Charge.test.ts +150 -0
- package/src/tempo/server/Charge.ts +2 -2
- package/src/tempo/server/Session.test.ts +241 -1
- package/src/tempo/server/Session.ts +8 -3
- package/src/tempo/server/internal/transport.test.ts +285 -0
- package/src/tempo/session/Voucher.test.ts +46 -0
package/src/Challenge.ts
CHANGED
|
@@ -27,6 +27,8 @@ export const Schema = z.object({
|
|
|
27
27
|
intent: z.string(),
|
|
28
28
|
/** Payment method (e.g., "tempo", "stripe"). */
|
|
29
29
|
method: z.string(),
|
|
30
|
+
/** Optional server-defined correlation data. Flat string-to-string map; clients MUST NOT modify. */
|
|
31
|
+
opaque: z.optional(z.record(z.string(), z.string())),
|
|
30
32
|
/** Server realm (e.g., hostname). */
|
|
31
33
|
realm: z.string(),
|
|
32
34
|
/** Method-specific request data. */
|
|
@@ -111,11 +113,20 @@ export function from<
|
|
|
111
113
|
const methods extends readonly Method.Method[] | undefined = undefined,
|
|
112
114
|
>(parameters: parameters, options?: from.Options<methods>): from.ReturnType<parameters, methods> {
|
|
113
115
|
void options
|
|
114
|
-
const {
|
|
116
|
+
const {
|
|
117
|
+
description,
|
|
118
|
+
digest,
|
|
119
|
+
meta,
|
|
120
|
+
method: methodName,
|
|
121
|
+
intent,
|
|
122
|
+
realm,
|
|
123
|
+
request,
|
|
124
|
+
secretKey,
|
|
125
|
+
} = parameters
|
|
115
126
|
|
|
116
127
|
const expires = (parameters.expires ?? request.expires) as string
|
|
117
128
|
const id = secretKey
|
|
118
|
-
? computeId({ ...parameters, expires }, { secretKey })
|
|
129
|
+
? computeId({ ...parameters, expires, ...(meta && { opaque: meta }) }, { secretKey })
|
|
119
130
|
: (parameters as { id: string }).id
|
|
120
131
|
|
|
121
132
|
return Schema.parse({
|
|
@@ -127,6 +138,7 @@ export function from<
|
|
|
127
138
|
...(description && { description }),
|
|
128
139
|
...(digest && { digest }),
|
|
129
140
|
...(expires && { expires }),
|
|
141
|
+
...(meta && { opaque: meta }),
|
|
130
142
|
}) as from.ReturnType<parameters, methods>
|
|
131
143
|
}
|
|
132
144
|
|
|
@@ -153,6 +165,8 @@ export declare namespace from {
|
|
|
153
165
|
expires?: string | undefined
|
|
154
166
|
/** Intent type (e.g., "charge", "session"). */
|
|
155
167
|
intent: string
|
|
168
|
+
/** Optional server-defined correlation data (serialized as `opaque` on the challenge). Flat string-to-string map; clients MUST NOT modify. */
|
|
169
|
+
meta?: Record<string, string> | undefined
|
|
156
170
|
/** Payment method (e.g., "tempo", "stripe"). */
|
|
157
171
|
method: string
|
|
158
172
|
/** Server realm (e.g., hostname). */
|
|
@@ -206,7 +220,7 @@ export function fromMethod<const method extends Method.Method>(
|
|
|
206
220
|
parameters: fromMethod.Parameters<method>,
|
|
207
221
|
): fromMethod.ReturnType<method> {
|
|
208
222
|
const { name: methodName, intent } = method
|
|
209
|
-
const { description, digest, expires, id, realm, secretKey } = parameters
|
|
223
|
+
const { description, digest, expires, id, meta, realm, secretKey } = parameters
|
|
210
224
|
|
|
211
225
|
const request = PaymentRequest.fromMethod(method, parameters.request)
|
|
212
226
|
|
|
@@ -219,6 +233,7 @@ export function fromMethod<const method extends Method.Method>(
|
|
|
219
233
|
description,
|
|
220
234
|
digest,
|
|
221
235
|
expires,
|
|
236
|
+
meta,
|
|
222
237
|
} as from.Parameters) as fromMethod.ReturnType<method>
|
|
223
238
|
}
|
|
224
239
|
|
|
@@ -239,6 +254,8 @@ export declare namespace fromMethod {
|
|
|
239
254
|
digest?: string | undefined
|
|
240
255
|
/** Optional expiration timestamp (ISO 8601). */
|
|
241
256
|
expires?: string | undefined
|
|
257
|
+
/** Optional server-defined correlation data (serialized as `opaque` on the challenge). Flat string-to-string map; clients MUST NOT modify. */
|
|
258
|
+
meta?: Record<string, string> | undefined
|
|
242
259
|
/** Server realm (e.g., hostname). */
|
|
243
260
|
realm: string
|
|
244
261
|
/** Method-specific request data. */
|
|
@@ -274,6 +291,8 @@ export function serialize(challenge: Challenge): string {
|
|
|
274
291
|
if (challenge.description !== undefined) parts.push(`description="${challenge.description}"`)
|
|
275
292
|
if (challenge.digest !== undefined) parts.push(`digest="${challenge.digest}"`)
|
|
276
293
|
if (challenge.expires !== undefined) parts.push(`expires="${challenge.expires}"`)
|
|
294
|
+
if (challenge.opaque !== undefined)
|
|
295
|
+
parts.push(`opaque="${PaymentRequest.serialize(challenge.opaque)}"`)
|
|
277
296
|
|
|
278
297
|
return `Payment ${parts.join(', ')}`
|
|
279
298
|
}
|
|
@@ -314,13 +333,14 @@ export function deserialize<const methods extends readonly Method.Method[] | und
|
|
|
314
333
|
}
|
|
315
334
|
}
|
|
316
335
|
|
|
317
|
-
const { request, ...rest } = result
|
|
336
|
+
const { request, opaque, ...rest } = result
|
|
318
337
|
if (!request) throw new Error('Missing request parameter.')
|
|
319
338
|
|
|
320
339
|
return from(
|
|
321
340
|
{
|
|
322
341
|
...rest,
|
|
323
342
|
request: PaymentRequest.deserialize(request),
|
|
343
|
+
...(opaque && { meta: PaymentRequest.deserialize(opaque) as Record<string, string> }),
|
|
324
344
|
} as from.Parameters,
|
|
325
345
|
options,
|
|
326
346
|
)
|
|
@@ -404,8 +424,17 @@ export declare namespace verify {
|
|
|
404
424
|
}
|
|
405
425
|
}
|
|
406
426
|
|
|
427
|
+
/** Alias for `challenge.opaque`. Extracts server-defined correlation data from a challenge. */
|
|
428
|
+
export function meta(challenge: Challenge): Record<string, string> | undefined {
|
|
429
|
+
return challenge.opaque
|
|
430
|
+
}
|
|
431
|
+
|
|
407
432
|
/** @internal Computes HMAC-SHA256 challenge ID from parameters. */
|
|
408
433
|
function computeId(challenge: Omit<Challenge, 'id'>, options: { secretKey: string }): string {
|
|
434
|
+
// Each field occupies a fixed positional slot joined by '|'. Optional fields
|
|
435
|
+
// use an empty string when absent so the slot count is stable — this avoids
|
|
436
|
+
// ambiguity between e.g. (expires set, no digest) vs (no expires, digest set)
|
|
437
|
+
// and means adding a new optional field changes all HMACs exactly once.
|
|
409
438
|
const input = [
|
|
410
439
|
challenge.realm,
|
|
411
440
|
challenge.method,
|
|
@@ -413,6 +442,7 @@ function computeId(challenge: Omit<Challenge, 'id'>, options: { secretKey: strin
|
|
|
413
442
|
PaymentRequest.serialize(challenge.request),
|
|
414
443
|
challenge.expires ?? '',
|
|
415
444
|
challenge.digest ?? '',
|
|
445
|
+
challenge.opaque ? PaymentRequest.serialize(challenge.opaque) : '',
|
|
416
446
|
].join('|')
|
|
417
447
|
|
|
418
448
|
const key = Bytes.fromString(options.secretKey)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import * as Store from './Store.js'
|
|
3
|
+
|
|
4
|
+
const nested = {
|
|
5
|
+
name: 'alice',
|
|
6
|
+
scores: [1, 2, 3],
|
|
7
|
+
meta: { active: true, tags: ['a', 'b'] },
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function fakeKv(): Store.cloudflare.Parameters {
|
|
11
|
+
const map = new Map<string, string>()
|
|
12
|
+
return {
|
|
13
|
+
async get(key) {
|
|
14
|
+
return map.get(key) ?? null
|
|
15
|
+
},
|
|
16
|
+
async put(key, value) {
|
|
17
|
+
map.set(key, value)
|
|
18
|
+
},
|
|
19
|
+
async delete(key) {
|
|
20
|
+
map.delete(key)
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe.each([
|
|
26
|
+
{ label: 'memory', create: () => Store.memory() },
|
|
27
|
+
{ label: 'cloudflare', create: () => Store.cloudflare(fakeKv()) },
|
|
28
|
+
{
|
|
29
|
+
label: 'upstash',
|
|
30
|
+
create: () => {
|
|
31
|
+
const kv = fakeKv()
|
|
32
|
+
return Store.upstash({
|
|
33
|
+
get: kv.get,
|
|
34
|
+
set: (key, value) => kv.put(key, value as string),
|
|
35
|
+
del: (key) => kv.delete(key),
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
])('$label', ({ create }) => {
|
|
40
|
+
test('roundtrip', async () => {
|
|
41
|
+
const store = create()
|
|
42
|
+
await store.put('k', nested)
|
|
43
|
+
expect(await store.get('k')).toEqual(nested)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('get missing key returns null', async () => {
|
|
47
|
+
const store = create()
|
|
48
|
+
expect(await store.get('missing')).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('delete removes key', async () => {
|
|
52
|
+
const store = create()
|
|
53
|
+
await store.put('k', 'value')
|
|
54
|
+
await store.delete('k')
|
|
55
|
+
expect(await store.get('k')).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('put overwrites existing value', async () => {
|
|
59
|
+
const store = create()
|
|
60
|
+
await store.put('k', 'first')
|
|
61
|
+
await store.put('k', 'second')
|
|
62
|
+
expect(await store.get('k')).toBe('second')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('json roundtrip behavior', () => {
|
|
67
|
+
test('memory json-roundtrips nested objects', async () => {
|
|
68
|
+
const store = Store.memory()
|
|
69
|
+
const value = { a: [1, { b: 'c' }], d: null }
|
|
70
|
+
await store.put('k', value)
|
|
71
|
+
expect(await store.get('k')).toEqual(value)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('cloudflare json-roundtrips nested objects', async () => {
|
|
75
|
+
const store = Store.cloudflare(fakeKv())
|
|
76
|
+
const value = { a: [1, { b: 'c' }], d: null }
|
|
77
|
+
await store.put('k', value)
|
|
78
|
+
expect(await store.get('k')).toEqual(value)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('upstash passes values through without json serialization', async () => {
|
|
82
|
+
const kv = fakeKv()
|
|
83
|
+
const store = Store.upstash({
|
|
84
|
+
get: kv.get,
|
|
85
|
+
set: (key, value) => kv.put(key, value as string),
|
|
86
|
+
del: (key) => kv.delete(key),
|
|
87
|
+
})
|
|
88
|
+
const value = { a: 1 }
|
|
89
|
+
await store.put('k', value)
|
|
90
|
+
// upstash store does not JSON-serialize; the fake map holds the original reference
|
|
91
|
+
expect(await kv.get('k')).toBe(value)
|
|
92
|
+
})
|
|
93
|
+
})
|
package/src/cli.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { accounts, asset, client, fundAccount } from '~test/tempo/viem.js'
|
|
|
11
11
|
import * as Store from './Store.js'
|
|
12
12
|
import * as Mppx_server from './server/Mppx.js'
|
|
13
13
|
import { toNodeListener } from './server/Mppx.js'
|
|
14
|
+
import { stripe as stripe_server } from './stripe/server/Methods.js'
|
|
14
15
|
import { tempo } from './tempo/server/Methods.js'
|
|
15
16
|
|
|
16
17
|
const cliPath = path.resolve(import.meta.dirname, 'cli.ts')
|
|
@@ -44,12 +45,12 @@ function runRaw(
|
|
|
44
45
|
|
|
45
46
|
function runAsync(
|
|
46
47
|
args: string[],
|
|
47
|
-
options?: { input?: string },
|
|
48
|
+
options?: { input?: string; env?: NodeJS.ProcessEnv },
|
|
48
49
|
): Promise<{ stdout: string; stderr: string }> {
|
|
49
50
|
return new Promise((resolve, reject) => {
|
|
50
51
|
const child = spawn('node', ['--import', 'tsx', cliPath, ...args], {
|
|
51
52
|
cwd,
|
|
52
|
-
env,
|
|
53
|
+
env: options?.env ?? env,
|
|
53
54
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
55
|
})
|
|
55
56
|
|
|
@@ -126,19 +127,36 @@ describe('basic charge (examples/basic)', () => {
|
|
|
126
127
|
}
|
|
127
128
|
})
|
|
128
129
|
|
|
129
|
-
test('error: no account found', { timeout: 60_000 }, () => {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
test('error: no account found', { timeout: 60_000 }, async () => {
|
|
131
|
+
const server = Mppx_server.create({
|
|
132
|
+
methods: [tempo.charge({ getClient: () => client })],
|
|
133
|
+
realm: 'cli-test-no-account',
|
|
134
|
+
secretKey: 'cli-test-secret',
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
138
|
+
const result = await toNodeListener(
|
|
139
|
+
server.charge({
|
|
140
|
+
amount: '1',
|
|
141
|
+
currency: asset,
|
|
142
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
143
|
+
recipient: accounts[0].address,
|
|
144
|
+
}),
|
|
145
|
+
)(req, res)
|
|
146
|
+
if (result.status === 402) return
|
|
147
|
+
res.end('paid')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const result = await runAsync([httpServer.url, '--account', 'nonexistent-account'], {
|
|
152
|
+
input: '',
|
|
137
153
|
env: { ...process.env, NODE_NO_WARNINGS: '1' },
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
}).catch((err) => err as Error)
|
|
155
|
+
expect(result).toBeInstanceOf(Error)
|
|
156
|
+
expect((result as Error).message).toContain('Account "nonexistent-account" not found')
|
|
157
|
+
} finally {
|
|
158
|
+
httpServer.close()
|
|
159
|
+
}
|
|
142
160
|
})
|
|
143
161
|
})
|
|
144
162
|
|
|
@@ -179,7 +197,7 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
|
|
|
179
197
|
|
|
180
198
|
try {
|
|
181
199
|
const { stdout } = await runAsync(
|
|
182
|
-
[httpServer.url, '--rpc-url', rpcUrl, '-s', '
|
|
200
|
+
[httpServer.url, '--rpc-url', rpcUrl, '-s', '-M', 'deposit=10'],
|
|
183
201
|
{ input: '' },
|
|
184
202
|
)
|
|
185
203
|
expect(stdout).toContain('scraped-content')
|
|
@@ -228,7 +246,7 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
|
|
|
228
246
|
try {
|
|
229
247
|
// First request: open a channel, answer "y" to proceed, "n" to close channel
|
|
230
248
|
const first = await runAsync(
|
|
231
|
-
[httpServer.url, '--rpc-url', rpcUrl, '--confirm', '
|
|
249
|
+
[httpServer.url, '--rpc-url', rpcUrl, '--confirm', '-M', 'deposit=10'],
|
|
232
250
|
{ input: 'y\nn\n' },
|
|
233
251
|
)
|
|
234
252
|
expect(first.stdout).toContain('scraped-content')
|
|
@@ -238,9 +256,18 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
|
|
|
238
256
|
expect(match).toBeTruthy()
|
|
239
257
|
const channelId = match![1]!
|
|
240
258
|
|
|
241
|
-
// Second request: reuse the channel via
|
|
259
|
+
// Second request: reuse the channel via -M channel=<id>
|
|
242
260
|
const second = await runAsync(
|
|
243
|
-
[
|
|
261
|
+
[
|
|
262
|
+
httpServer.url,
|
|
263
|
+
'--rpc-url',
|
|
264
|
+
rpcUrl,
|
|
265
|
+
'-s',
|
|
266
|
+
'-M',
|
|
267
|
+
`channel=${channelId}`,
|
|
268
|
+
'-M',
|
|
269
|
+
'deposit=10',
|
|
270
|
+
],
|
|
244
271
|
{ input: '' },
|
|
245
272
|
)
|
|
246
273
|
expect(second.stdout).toContain('scraped-content')
|
|
@@ -266,6 +293,53 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
|
|
|
266
293
|
})
|
|
267
294
|
})
|
|
268
295
|
|
|
296
|
+
describe.skipIf(!process.env.VITE_STRIPE_SECRET_KEY)('stripe charge (integration)', () => {
|
|
297
|
+
test('happy path: makes Stripe payment via real API', { timeout: 120_000 }, async () => {
|
|
298
|
+
const stripeSecretKey = process.env.VITE_STRIPE_SECRET_KEY!
|
|
299
|
+
|
|
300
|
+
const server = Mppx_server.create({
|
|
301
|
+
methods: [
|
|
302
|
+
stripe_server.charge({
|
|
303
|
+
secretKey: stripeSecretKey,
|
|
304
|
+
networkId: 'internal',
|
|
305
|
+
paymentMethodTypes: ['card'],
|
|
306
|
+
}),
|
|
307
|
+
],
|
|
308
|
+
realm: 'cli-test-stripe',
|
|
309
|
+
secretKey: 'cli-test-secret',
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
313
|
+
const result = await toNodeListener(
|
|
314
|
+
server.charge({
|
|
315
|
+
amount: '1',
|
|
316
|
+
currency: 'usd',
|
|
317
|
+
decimals: 2,
|
|
318
|
+
}),
|
|
319
|
+
)(req, res)
|
|
320
|
+
if (result.status === 402) return
|
|
321
|
+
res.end('paid')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
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
|
+
},
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
expect(stdout).toContain('paid')
|
|
337
|
+
} finally {
|
|
338
|
+
httpServer.close()
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
269
343
|
describe('session sse (examples/session/sse)', () => {
|
|
270
344
|
test('streams SSE tokens to stdout', { timeout: 120_000 }, async () => {
|
|
271
345
|
await fundAccount({ address: testAccount.address, token: Addresses.pathUsd })
|
|
@@ -313,7 +387,7 @@ describe('session sse (examples/session/sse)', () => {
|
|
|
313
387
|
})
|
|
314
388
|
|
|
315
389
|
try {
|
|
316
|
-
const { stdout } = await runAsync([httpServer.url, '--rpc-url', rpcUrl, '
|
|
390
|
+
const { stdout } = await runAsync([httpServer.url, '--rpc-url', rpcUrl, '-M', 'deposit=10'], {
|
|
317
391
|
input: '',
|
|
318
392
|
})
|
|
319
393
|
expect(stdout.trim()).toBe('Hello world!')
|
|
@@ -337,6 +411,129 @@ describe('session sse (examples/session/sse)', () => {
|
|
|
337
411
|
})
|
|
338
412
|
})
|
|
339
413
|
|
|
414
|
+
describe('stripe charge', () => {
|
|
415
|
+
test('happy path: makes Stripe payment and receives response', { timeout: 60_000 }, async () => {
|
|
416
|
+
const mockStripeClient = {
|
|
417
|
+
paymentIntents: { create: async () => ({ id: 'pi_mock_cli_123', status: 'succeeded' }) },
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const server = Mppx_server.create({
|
|
421
|
+
methods: [
|
|
422
|
+
stripe_server.charge({
|
|
423
|
+
client: mockStripeClient,
|
|
424
|
+
networkId: 'internal',
|
|
425
|
+
paymentMethodTypes: ['card'],
|
|
426
|
+
}),
|
|
427
|
+
],
|
|
428
|
+
realm: 'cli-test-stripe',
|
|
429
|
+
secretKey: 'cli-test-secret',
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
const sptServer = await Http.createServer(async (_req, res) => {
|
|
433
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
434
|
+
res.end(JSON.stringify({ id: 'spt_mock_cli_test' }))
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
const appServer = await Http.createServer(async (req, res) => {
|
|
438
|
+
const result = await Mppx_server.toNodeListener(
|
|
439
|
+
server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
|
|
440
|
+
)(req, res)
|
|
441
|
+
if (result.status === 402) return
|
|
442
|
+
res.end('paid')
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const { stdout } = await runAsync([appServer.url, '-s', '-M', 'paymentMethod=pm_card_visa'], {
|
|
447
|
+
input: '',
|
|
448
|
+
env: {
|
|
449
|
+
...process.env,
|
|
450
|
+
NODE_NO_WARNINGS: '1',
|
|
451
|
+
MPPX_STRIPE_SECRET_KEY: 'sk_test_mock',
|
|
452
|
+
MPPX_STRIPE_SPT_URL: sptServer.url,
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
expect(stdout).toContain('paid')
|
|
456
|
+
} finally {
|
|
457
|
+
appServer.close()
|
|
458
|
+
sptServer.close()
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
test('error: missing MPPX_STRIPE_SECRET_KEY', { timeout: 60_000 }, async () => {
|
|
463
|
+
const server = Mppx_server.create({
|
|
464
|
+
methods: [
|
|
465
|
+
stripe_server.charge({
|
|
466
|
+
secretKey: 'sk_test_mock',
|
|
467
|
+
networkId: 'internal',
|
|
468
|
+
paymentMethodTypes: ['card'],
|
|
469
|
+
}),
|
|
470
|
+
],
|
|
471
|
+
realm: 'cli-test-stripe-nokey',
|
|
472
|
+
secretKey: 'cli-test-secret',
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
const appServer = await Http.createServer(async (req, res) => {
|
|
476
|
+
const result = await Mppx_server.toNodeListener(
|
|
477
|
+
server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
|
|
478
|
+
)(req, res)
|
|
479
|
+
if (result.status === 402) return
|
|
480
|
+
res.end('paid')
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
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')
|
|
494
|
+
} finally {
|
|
495
|
+
appServer.close()
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
test('error: production key rejected', { timeout: 60_000 }, async () => {
|
|
500
|
+
const server = Mppx_server.create({
|
|
501
|
+
methods: [
|
|
502
|
+
stripe_server.charge({
|
|
503
|
+
secretKey: 'sk_test_mock',
|
|
504
|
+
networkId: 'internal',
|
|
505
|
+
paymentMethodTypes: ['card'],
|
|
506
|
+
}),
|
|
507
|
+
],
|
|
508
|
+
realm: 'cli-test-stripe-live',
|
|
509
|
+
secretKey: 'cli-test-secret',
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
const appServer = await Http.createServer(async (req, res) => {
|
|
513
|
+
const result = await Mppx_server.toNodeListener(
|
|
514
|
+
server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
|
|
515
|
+
)(req, res)
|
|
516
|
+
if (result.status === 402) return
|
|
517
|
+
res.end('paid')
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
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')
|
|
531
|
+
} finally {
|
|
532
|
+
appServer.close()
|
|
533
|
+
}
|
|
534
|
+
})
|
|
535
|
+
})
|
|
536
|
+
|
|
340
537
|
// ---------------------------------------------------------------------------
|
|
341
538
|
// account [action]
|
|
342
539
|
// TODO: investigate account tests timing out in CI (secret-tool/gnome-keyring hangs)
|
|
@@ -505,24 +702,23 @@ test('mppx --help', () => {
|
|
|
505
702
|
view View account address
|
|
506
703
|
|
|
507
704
|
Options:
|
|
508
|
-
-a, --account <name>
|
|
509
|
-
-d, --data <data>
|
|
510
|
-
-f, --fail
|
|
511
|
-
-i, --include
|
|
512
|
-
-k, --insecure
|
|
513
|
-
-r, --rpc-url <url>
|
|
514
|
-
-s, --silent
|
|
515
|
-
-v, --verbose
|
|
516
|
-
-A, --user-agent <ua>
|
|
517
|
-
-H, --header <header>
|
|
518
|
-
-L, --location
|
|
519
|
-
-X, --method <method>
|
|
520
|
-
--
|
|
521
|
-
--confirm
|
|
522
|
-
--
|
|
523
|
-
|
|
524
|
-
-
|
|
525
|
-
-h, --help Display this message
|
|
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
|
|
526
722
|
|
|
527
723
|
Examples:
|
|
528
724
|
mppx example.com/content
|