mppx 0.5.11 → 0.5.13
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 +16 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +41 -16
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/config.d.ts +6 -4
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/internal.d.ts +8 -0
- package/dist/cli/internal.d.ts.map +1 -1
- package/dist/cli/internal.js +33 -3
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/plugin.d.ts +2 -0
- package/dist/cli/plugins/plugin.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js +3 -0
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +3 -0
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/client/Mppx.d.ts +10 -1
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +17 -5
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/Transport.d.ts +2 -0
- package/dist/client/Transport.d.ts.map +1 -1
- package/dist/client/Transport.js +11 -0
- package/dist/client/Transport.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +3 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +65 -19
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts +72 -0
- package/dist/internal/AcceptPayment.d.ts.map +1 -0
- package/dist/internal/AcceptPayment.js +185 -0
- package/dist/internal/AcceptPayment.js.map +1 -0
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +8 -4
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +33 -24
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/internal/html/config.d.ts.map +1 -1
- package/dist/server/internal/html/config.js +8 -1
- package/dist/server/internal/html/config.js.map +1 -1
- package/dist/stripe/internal/constants.d.ts +8 -0
- package/dist/stripe/internal/constants.d.ts.map +1 -0
- package/dist/stripe/internal/constants.js +8 -0
- package/dist/stripe/internal/constants.js.map +1 -0
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +23 -5
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +6 -2
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/Proof.d.ts +12 -0
- package/dist/tempo/Proof.d.ts.map +1 -0
- package/dist/tempo/Proof.js +10 -0
- package/dist/tempo/Proof.js.map +1 -0
- package/dist/tempo/client/Charge.d.ts +11 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +14 -2
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +6 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/index.d.ts +1 -0
- package/dist/tempo/index.d.ts.map +1 -1
- package/dist/tempo/index.js +1 -0
- package/dist/tempo/index.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +8 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +29 -3
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +17 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +69 -4
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +6 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/cli.test.ts +278 -0
- package/src/cli/cli.ts +47 -16
- package/src/cli/config.ts +10 -4
- package/src/cli/internal.ts +59 -3
- package/src/cli/plugins/plugin.ts +3 -0
- package/src/cli/plugins/stripe.ts +3 -0
- package/src/cli/plugins/tempo.ts +3 -0
- package/src/client/Mppx.test-d.ts +33 -0
- package/src/client/Mppx.test.ts +130 -1
- package/src/client/Mppx.ts +35 -5
- package/src/client/Transport.test.ts +88 -55
- package/src/client/Transport.ts +13 -0
- package/src/client/internal/Fetch.browser.test.ts +16 -13
- package/src/client/internal/Fetch.test.ts +307 -10
- package/src/client/internal/Fetch.ts +85 -19
- package/src/internal/AcceptPayment.test.ts +211 -0
- package/src/internal/AcceptPayment.ts +304 -0
- package/src/mcp-sdk/client/McpClient.ts +11 -5
- package/src/server/Mppx.test.ts +141 -44
- package/src/server/Mppx.ts +43 -23
- package/src/server/Transport.test.ts +20 -0
- package/src/server/internal/html/config.ts +9 -1
- package/src/stripe/internal/constants.ts +7 -0
- package/src/stripe/server/Charge.ts +22 -4
- package/src/tempo/Methods.test.ts +25 -0
- package/src/tempo/Methods.ts +30 -22
- package/src/tempo/Proof.test-d.ts +13 -0
- package/src/tempo/Proof.test.ts +31 -0
- package/src/tempo/Proof.ts +13 -0
- package/src/tempo/client/Charge.ts +20 -6
- package/src/tempo/client/SessionManager.test.ts +4 -7
- package/src/tempo/index.ts +1 -0
- package/src/tempo/internal/fee-payer.test.ts +75 -1
- package/src/tempo/internal/fee-payer.ts +41 -3
- package/src/tempo/server/Charge.test.ts +309 -1
- package/src/tempo/server/Charge.ts +99 -1
- package/src/tempo/server/internal/html/main.ts +2 -2
- package/src/tempo/server/internal/html.gen.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.gen.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/html.gen.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,MAAM,CAAC,MAAM,IAAI,GAAG,
|
|
1
|
+
{"version":3,"file":"html.gen.js","sourceRoot":"","sources":["../../../../src/tempo/server/internal/html.gen.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,MAAM,CAAC,MAAM,IAAI,GAAG,w91bAAw91b,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mppx",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.13",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"files": [
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
},
|
|
125
125
|
"dependencies": {
|
|
126
126
|
"incur": "^0.3.23",
|
|
127
|
-
"ox": "0.14.
|
|
127
|
+
"ox": "0.14.10",
|
|
128
128
|
"zod": "^4.3.6"
|
|
129
129
|
},
|
|
130
130
|
"repository": {
|
package/src/cli/cli.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
2
2
|
import * as fs from 'node:fs'
|
|
3
3
|
import * as os from 'node:os'
|
|
4
4
|
import * as path from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
5
6
|
|
|
6
7
|
import { parseUnits } from 'viem'
|
|
7
8
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
@@ -14,6 +15,7 @@ import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js
|
|
|
14
15
|
|
|
15
16
|
import * as Challenge from '../Challenge.js'
|
|
16
17
|
import * as Credential from '../Credential.js'
|
|
18
|
+
import * as Method from '../Method.js'
|
|
17
19
|
import * as Receipt from '../Receipt.js'
|
|
18
20
|
import * as Mppx_server from '../server/Mppx.js'
|
|
19
21
|
import { toNodeListener } from '../server/Mppx.js'
|
|
@@ -21,6 +23,7 @@ import * as Store from '../Store.js'
|
|
|
21
23
|
import { stripe as stripe_server } from '../stripe/server/Methods.js'
|
|
22
24
|
import { tempo } from '../tempo/server/Methods.js'
|
|
23
25
|
import type { SessionCredentialPayload } from '../tempo/session/Types.js'
|
|
26
|
+
import * as z from '../zod.js'
|
|
24
27
|
import cli from './cli.js'
|
|
25
28
|
|
|
26
29
|
const testPrivateKey = generatePrivateKey()
|
|
@@ -78,6 +81,24 @@ async function serve(argv: string[], options?: { env?: Record<string, string | u
|
|
|
78
81
|
return { output, stderr, exitCode }
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
function createMockChargeMethod(name: string) {
|
|
85
|
+
return Method.from({
|
|
86
|
+
name,
|
|
87
|
+
intent: 'charge',
|
|
88
|
+
schema: {
|
|
89
|
+
credential: {
|
|
90
|
+
payload: z.object({ token: z.string() }),
|
|
91
|
+
},
|
|
92
|
+
request: z.object({
|
|
93
|
+
amount: z.string(),
|
|
94
|
+
currency: z.string(),
|
|
95
|
+
decimals: z.number(),
|
|
96
|
+
recipient: z.string(),
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
describe('discover validate', () => {
|
|
82
103
|
test('validates a local discovery document', async () => {
|
|
83
104
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-discovery-'))
|
|
@@ -329,6 +350,198 @@ describe('basic charge (examples/basic)', () => {
|
|
|
329
350
|
}
|
|
330
351
|
})
|
|
331
352
|
|
|
353
|
+
test('selects a later supported challenge when the first offer is unsupported', async () => {
|
|
354
|
+
const unsupportedMethod = Method.toServer(createMockChargeMethod('unknown'), {
|
|
355
|
+
async verify() {
|
|
356
|
+
return {
|
|
357
|
+
method: 'unknown',
|
|
358
|
+
reference: 'unknown-ref',
|
|
359
|
+
status: 'success' as const,
|
|
360
|
+
timestamp: new Date().toISOString(),
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
const tempoMethod = tempo.charge({ getClient: () => client })
|
|
365
|
+
|
|
366
|
+
const server = Mppx_server.create({
|
|
367
|
+
methods: [unsupportedMethod, tempoMethod],
|
|
368
|
+
realm: 'cli-test-multi-offer',
|
|
369
|
+
secretKey: 'cli-test-secret',
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
373
|
+
const result = await toNodeListener(
|
|
374
|
+
server.compose(
|
|
375
|
+
[
|
|
376
|
+
unsupportedMethod,
|
|
377
|
+
{
|
|
378
|
+
amount: '1',
|
|
379
|
+
currency: asset,
|
|
380
|
+
decimals: 6,
|
|
381
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
382
|
+
recipient: accounts[0].address,
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
[
|
|
386
|
+
tempoMethod,
|
|
387
|
+
{
|
|
388
|
+
amount: '1',
|
|
389
|
+
currency: asset,
|
|
390
|
+
decimals: 6,
|
|
391
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
392
|
+
recipient: accounts[0].address,
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
),
|
|
396
|
+
)(req, res)
|
|
397
|
+
if (result.status === 402) return
|
|
398
|
+
res.end('paid-from-second-offer')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const { output, exitCode } = await serve([httpServer.url, '--rpc-url', rpcUrl, '-s'], {
|
|
403
|
+
env: { MPPX_PRIVATE_KEY: testPrivateKey },
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
expect(exitCode).toBeUndefined()
|
|
407
|
+
expect(output).toContain('paid-from-second-offer')
|
|
408
|
+
} finally {
|
|
409
|
+
httpServer.close()
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('config methods emit Accept-Payment and select the preferred challenge', async () => {
|
|
414
|
+
const alphaMethod = Method.toServer(createMockChargeMethod('alpha'), {
|
|
415
|
+
async verify({ envelope }) {
|
|
416
|
+
if (!envelope) throw new Error('expected envelope')
|
|
417
|
+
if ((envelope.credential.payload as { token: string }).token !== 'alpha-token') {
|
|
418
|
+
throw new Error('expected alpha credential')
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
method: 'alpha',
|
|
423
|
+
reference: 'alpha-ref',
|
|
424
|
+
status: 'success' as const,
|
|
425
|
+
timestamp: new Date().toISOString(),
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
})
|
|
429
|
+
const betaMethod = Method.toServer(createMockChargeMethod('beta'), {
|
|
430
|
+
async verify({ envelope }) {
|
|
431
|
+
if (!envelope) throw new Error('expected envelope')
|
|
432
|
+
if ((envelope.credential.payload as { token: string }).token !== 'beta-token') {
|
|
433
|
+
throw new Error('expected beta credential')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
method: 'beta',
|
|
438
|
+
reference: 'beta-ref',
|
|
439
|
+
status: 'success' as const,
|
|
440
|
+
timestamp: new Date().toISOString(),
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
const server = Mppx_server.create({
|
|
446
|
+
methods: [betaMethod, alphaMethod],
|
|
447
|
+
realm: 'cli-test-config-offers',
|
|
448
|
+
secretKey: 'cli-test-secret',
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-cli-config-'))
|
|
452
|
+
const configPath = path.join(configDir, 'mppx.config.mjs')
|
|
453
|
+
const mppxModuleUrl = pathToFileURL(path.join(process.cwd(), 'src/index.ts')).href
|
|
454
|
+
const cliModuleUrl = pathToFileURL(path.join(process.cwd(), 'src/cli/config.ts')).href
|
|
455
|
+
fs.writeFileSync(
|
|
456
|
+
configPath,
|
|
457
|
+
`
|
|
458
|
+
import { Credential, Method, z } from '${mppxModuleUrl}'
|
|
459
|
+
import { defineConfig } from '${cliModuleUrl}'
|
|
460
|
+
|
|
461
|
+
const alpha = Method.toClient(Method.from({
|
|
462
|
+
name: 'alpha',
|
|
463
|
+
intent: 'charge',
|
|
464
|
+
schema: {
|
|
465
|
+
credential: { payload: z.object({ token: z.string() }) },
|
|
466
|
+
request: z.object({ amount: z.string(), currency: z.string(), decimals: z.number(), recipient: z.string() }),
|
|
467
|
+
},
|
|
468
|
+
}), {
|
|
469
|
+
async createCredential({ challenge }) {
|
|
470
|
+
return Credential.serialize({ challenge, payload: { token: 'alpha-token' } })
|
|
471
|
+
},
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
const beta = Method.toClient(Method.from({
|
|
475
|
+
name: 'beta',
|
|
476
|
+
intent: 'charge',
|
|
477
|
+
schema: {
|
|
478
|
+
credential: { payload: z.object({ token: z.string() }) },
|
|
479
|
+
request: z.object({ amount: z.string(), currency: z.string(), decimals: z.number(), recipient: z.string() }),
|
|
480
|
+
},
|
|
481
|
+
}), {
|
|
482
|
+
async createCredential({ challenge }) {
|
|
483
|
+
return Credential.serialize({ challenge, payload: { token: 'beta-token' } })
|
|
484
|
+
},
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
export default defineConfig({
|
|
488
|
+
methods: [beta, alpha],
|
|
489
|
+
paymentPreferences: ({ alpha, beta }) => ({
|
|
490
|
+
[alpha.charge]: 1,
|
|
491
|
+
[beta.charge]: 0.2,
|
|
492
|
+
}),
|
|
493
|
+
})
|
|
494
|
+
`.trim(),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
let acceptPaymentHeader: string | undefined
|
|
498
|
+
let authorization: string | undefined
|
|
499
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
500
|
+
if (!req.headers.authorization)
|
|
501
|
+
acceptPaymentHeader = req.headers['accept-payment'] as string | undefined
|
|
502
|
+
else authorization = req.headers.authorization
|
|
503
|
+
|
|
504
|
+
const result = await toNodeListener(
|
|
505
|
+
server.compose(
|
|
506
|
+
[
|
|
507
|
+
betaMethod,
|
|
508
|
+
{
|
|
509
|
+
amount: '1',
|
|
510
|
+
currency: asset,
|
|
511
|
+
decimals: 6,
|
|
512
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
513
|
+
recipient: accounts[0].address,
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
[
|
|
517
|
+
alphaMethod,
|
|
518
|
+
{
|
|
519
|
+
amount: '1',
|
|
520
|
+
currency: asset,
|
|
521
|
+
decimals: 6,
|
|
522
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
523
|
+
recipient: accounts[0].address,
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
),
|
|
527
|
+
)(req, res)
|
|
528
|
+
if (result.status === 402) return
|
|
529
|
+
res.end('paid-from-config-preference')
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const { output, exitCode } = await serve([httpServer.url, '--config', configPath, '-s'])
|
|
534
|
+
|
|
535
|
+
expect(exitCode).toBeUndefined()
|
|
536
|
+
expect(output).toContain('paid-from-config-preference')
|
|
537
|
+
expect(acceptPaymentHeader).toBe('beta/charge;q=0.2, alpha/charge')
|
|
538
|
+
expect(Credential.deserialize(authorization!).payload).toEqual({ token: 'alpha-token' })
|
|
539
|
+
} finally {
|
|
540
|
+
httpServer.close()
|
|
541
|
+
fs.rmSync(configDir, { recursive: true, force: true })
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
|
|
332
545
|
test(
|
|
333
546
|
'zero-amount charge uses a proof credential and receives response',
|
|
334
547
|
{ timeout: 120_000 },
|
|
@@ -1120,6 +1333,71 @@ describe('sign', () => {
|
|
|
1120
1333
|
expect(output).toContain('Unsupported payment method')
|
|
1121
1334
|
})
|
|
1122
1335
|
|
|
1336
|
+
test('paymentPreferences opt-out is not bypassed by CLI fallback selection', async () => {
|
|
1337
|
+
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-sign-config-'))
|
|
1338
|
+
const configPath = path.join(configDir, 'mppx.config.mjs')
|
|
1339
|
+
const mppxModuleUrl = pathToFileURL(path.join(process.cwd(), 'src/index.ts')).href
|
|
1340
|
+
const cliModuleUrl = pathToFileURL(path.join(process.cwd(), 'src/cli/config.ts')).href
|
|
1341
|
+
fs.writeFileSync(
|
|
1342
|
+
configPath,
|
|
1343
|
+
`
|
|
1344
|
+
import { Credential, Method, z } from '${mppxModuleUrl}'
|
|
1345
|
+
import { defineConfig } from '${cliModuleUrl}'
|
|
1346
|
+
|
|
1347
|
+
const alpha = Method.toClient(Method.from({
|
|
1348
|
+
name: 'alpha',
|
|
1349
|
+
intent: 'charge',
|
|
1350
|
+
schema: {
|
|
1351
|
+
credential: { payload: z.object({ token: z.string() }) },
|
|
1352
|
+
request: z.object({ amount: z.string() }),
|
|
1353
|
+
},
|
|
1354
|
+
}), {
|
|
1355
|
+
async createCredential({ challenge }) {
|
|
1356
|
+
return Credential.serialize({ challenge, payload: { token: 'alpha-token' } })
|
|
1357
|
+
},
|
|
1358
|
+
})
|
|
1359
|
+
|
|
1360
|
+
export default defineConfig({
|
|
1361
|
+
methods: [alpha],
|
|
1362
|
+
paymentPreferences: ({ alpha }) => ({
|
|
1363
|
+
[alpha.charge]: 0,
|
|
1364
|
+
}),
|
|
1365
|
+
})
|
|
1366
|
+
`.trim(),
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
const challenge =
|
|
1370
|
+
'Payment id="x", realm="x", method="alpha", intent="charge", request="eyJhbW91bnQiOiIxIn0"'
|
|
1371
|
+
|
|
1372
|
+
try {
|
|
1373
|
+
const { exitCode, output } = await serve([
|
|
1374
|
+
'sign',
|
|
1375
|
+
'--challenge',
|
|
1376
|
+
challenge,
|
|
1377
|
+
'--config',
|
|
1378
|
+
configPath,
|
|
1379
|
+
])
|
|
1380
|
+
expect(exitCode).toBe(2)
|
|
1381
|
+
expect(output).toContain('Unsupported payment method')
|
|
1382
|
+
} finally {
|
|
1383
|
+
fs.rmSync(configDir, { recursive: true, force: true })
|
|
1384
|
+
}
|
|
1385
|
+
})
|
|
1386
|
+
|
|
1387
|
+
test('selects a later supported challenge from a merged challenge value', async () => {
|
|
1388
|
+
const merged = [
|
|
1389
|
+
'Payment id="x", realm="x", method="unknown", intent="charge", request="e30"',
|
|
1390
|
+
validChallenge,
|
|
1391
|
+
].join(', ')
|
|
1392
|
+
|
|
1393
|
+
const { output, exitCode } = await serve(['sign', '--challenge', merged, '--rpc-url', rpcUrl], {
|
|
1394
|
+
env: { MPPX_PRIVATE_KEY: testPrivateKey },
|
|
1395
|
+
})
|
|
1396
|
+
|
|
1397
|
+
expect(exitCode).toBeUndefined()
|
|
1398
|
+
expect(output.trim()).toMatch(/^Payment\s+\S+/)
|
|
1399
|
+
})
|
|
1400
|
+
|
|
1123
1401
|
test('error: no account for tempo', async () => {
|
|
1124
1402
|
const { exitCode, output } = await serve(
|
|
1125
1403
|
['sign', '--challenge', validChallenge, '--account', 'nonexistent-sign-test'],
|
package/src/cli/cli.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { normalizeHeaders } from '../client/internal/Fetch.js'
|
|
|
13
13
|
import * as Mppx from '../client/Mppx.js'
|
|
14
14
|
import { validate as validateDiscovery } from '../discovery/Validate.js'
|
|
15
15
|
import { createDefaultStore, createKeychain, resolveAccountName } from './account.js'
|
|
16
|
-
import { loadConfig,
|
|
16
|
+
import { loadConfig, resolveAcceptPayment, selectChallenge } from './internal.js'
|
|
17
17
|
import type { Plugin } from './plugins/plugin.js'
|
|
18
18
|
import { readTempoKeystore, resolveTempoAccount } from './plugins/tempo.js'
|
|
19
19
|
import {
|
|
@@ -128,6 +128,13 @@ const cli = Cli.create('mppx', {
|
|
|
128
128
|
headers[header.slice(0, index).trim()] = header.slice(index + 1).trim()
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
const acceptPayment = resolveAcceptPayment(loaded?.config)
|
|
132
|
+
if (
|
|
133
|
+
acceptPayment &&
|
|
134
|
+
!Object.keys(headers).some((key) => key.toLowerCase() === 'accept-payment')
|
|
135
|
+
) {
|
|
136
|
+
headers['Accept-Payment'] = acceptPayment
|
|
137
|
+
}
|
|
131
138
|
|
|
132
139
|
const url = (() => {
|
|
133
140
|
const hasProtocol = /^https?:\/\//.test(c.args.url)
|
|
@@ -200,8 +207,26 @@ const cli = Cli.create('mppx', {
|
|
|
200
207
|
return
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
const
|
|
204
|
-
|
|
210
|
+
const selected = selectChallenge(
|
|
211
|
+
Challenge.fromResponseList(challengeResponse),
|
|
212
|
+
loaded?.config,
|
|
213
|
+
)
|
|
214
|
+
if (!selected) {
|
|
215
|
+
const offers = Challenge.fromResponseList(challengeResponse)
|
|
216
|
+
.map((challenge) => `${challenge.method}/${challenge.intent}`)
|
|
217
|
+
.join(', ')
|
|
218
|
+
return c.error({
|
|
219
|
+
code: 'UNSUPPORTED_METHOD',
|
|
220
|
+
message: `Unsupported payment method. Server offers: ${offers}. Add it to mppx.config.ts using defineConfig().`,
|
|
221
|
+
exitCode: 2,
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { challenge, plugin, method: configMethod } = selected
|
|
226
|
+
const selectedChallengeResponse = new Response(null, {
|
|
227
|
+
status: 402,
|
|
228
|
+
headers: { 'WWW-Authenticate': Challenge.serialize(challenge) },
|
|
229
|
+
})
|
|
205
230
|
|
|
206
231
|
let tokenSymbol = (challenge.request.currency as string | undefined) ?? ''
|
|
207
232
|
let tokenDecimals = (challenge.request.decimals as number | undefined) ?? 6
|
|
@@ -297,23 +322,17 @@ const cli = Cli.create('mppx', {
|
|
|
297
322
|
// Create credential
|
|
298
323
|
let credential: string
|
|
299
324
|
if (pluginResult?.createCredential)
|
|
300
|
-
credential = await pluginResult.createCredential(
|
|
325
|
+
credential = await pluginResult.createCredential(selectedChallengeResponse)
|
|
301
326
|
else if (pluginResult) {
|
|
302
327
|
const mppx = Mppx.create({ methods: pluginResult.methods, polyfill: false })
|
|
303
328
|
credential = await mppx.createCredential(
|
|
304
|
-
|
|
329
|
+
selectedChallengeResponse,
|
|
305
330
|
pluginResult.credentialContext as undefined,
|
|
306
331
|
)
|
|
307
332
|
} else if (configMethod) {
|
|
308
333
|
const mppx = Mppx.create({ methods: [configMethod], polyfill: false })
|
|
309
|
-
credential = await mppx.createCredential(
|
|
310
|
-
} else
|
|
311
|
-
return c.error({
|
|
312
|
-
code: 'UNSUPPORTED_METHOD',
|
|
313
|
-
message: `Unsupported payment method: ${challenge.method}/${challenge.intent}. Add it to mppx.config.ts using defineConfig().`,
|
|
314
|
-
exitCode: 2,
|
|
315
|
-
})
|
|
316
|
-
}
|
|
334
|
+
credential = await mppx.createCredential(selectedChallengeResponse)
|
|
335
|
+
} else throw new Error('unreachable')
|
|
317
336
|
|
|
318
337
|
// Send credential and get response
|
|
319
338
|
const credentialHeaders = {
|
|
@@ -815,9 +834,9 @@ const sign = Cli.create('sign', {
|
|
|
815
834
|
})
|
|
816
835
|
}
|
|
817
836
|
|
|
818
|
-
let
|
|
837
|
+
let challenges: Challenge.Challenge[]
|
|
819
838
|
try {
|
|
820
|
-
|
|
839
|
+
challenges = Challenge.deserializeList(raw)
|
|
821
840
|
} catch (err) {
|
|
822
841
|
return c.error({
|
|
823
842
|
code: 'INVALID_CHALLENGE',
|
|
@@ -832,7 +851,19 @@ const sign = Cli.create('sign', {
|
|
|
832
851
|
}
|
|
833
852
|
|
|
834
853
|
const loaded = await loadConfig(c.options.config)
|
|
835
|
-
const
|
|
854
|
+
const selected = selectChallenge(challenges, loaded?.config)
|
|
855
|
+
if (!selected) {
|
|
856
|
+
const offers = challenges
|
|
857
|
+
.map((challenge) => `${challenge.method}/${challenge.intent}`)
|
|
858
|
+
.join(', ')
|
|
859
|
+
return c.error({
|
|
860
|
+
code: 'UNSUPPORTED_METHOD',
|
|
861
|
+
message: `Unsupported payment method. Server offers: ${offers}. Add it to mppx.config.ts using defineConfig().`,
|
|
862
|
+
exitCode: 2,
|
|
863
|
+
})
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const { challenge, plugin, method: configMethod } = selected
|
|
836
867
|
const methodOpts = parseMethodOpts(c.options.methodOpt)
|
|
837
868
|
|
|
838
869
|
const wwwAuth = Challenge.serialize(challenge)
|
package/src/cli/config.ts
CHANGED
|
@@ -28,17 +28,23 @@ import type { Plugin } from './plugins/plugin.js'
|
|
|
28
28
|
* })
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
export function defineConfig
|
|
31
|
+
export function defineConfig<const methods extends Mppx.Methods | undefined = undefined>(
|
|
32
|
+
config: defineConfig.Config<methods>,
|
|
33
|
+
): defineConfig.Config<methods> {
|
|
32
34
|
return config
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export declare namespace defineConfig {
|
|
36
|
-
type Config = {
|
|
38
|
+
type Config<methods extends Mppx.Methods | undefined = undefined> = {
|
|
37
39
|
/** Array of methods to use. */
|
|
38
|
-
methods?:
|
|
40
|
+
methods?: methods
|
|
41
|
+
/** Optional payment preferences for configured client methods. */
|
|
42
|
+
paymentPreferences?: methods extends Mppx.Methods
|
|
43
|
+
? Mppx.create.Config<methods>['paymentPreferences']
|
|
44
|
+
: undefined
|
|
39
45
|
/** Array of plugins to use. */
|
|
40
46
|
plugins?: Plugin[] | undefined
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
export type Config = defineConfig.Config
|
|
50
|
+
export type Config = defineConfig.Config<Mppx.Methods>
|
package/src/cli/internal.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs'
|
|
|
2
2
|
import * as path from 'node:path'
|
|
3
3
|
|
|
4
4
|
import type * as Challenge from '../Challenge.js'
|
|
5
|
+
import * as AcceptPayment from '../internal/AcceptPayment.js'
|
|
5
6
|
import type * as Method from '../Method.js'
|
|
6
7
|
import type { Config } from './config.js'
|
|
7
8
|
import { stripe as stripePlugin, tempo as tempoPlugin } from './plugins/index.js'
|
|
@@ -13,13 +14,13 @@ export function resolvePlugin(
|
|
|
13
14
|
challenge: Challenge.Challenge,
|
|
14
15
|
config?: { plugins?: Plugin[] | undefined; methods?: any },
|
|
15
16
|
): { plugin?: Plugin | undefined; method?: Method.AnyClient | undefined } {
|
|
16
|
-
const configPlugin = config?.plugins?.find((p) => p
|
|
17
|
+
const configPlugin = config?.plugins?.find((p) => supportsPlugin(p, challenge))
|
|
17
18
|
if (configPlugin) return { plugin: configPlugin }
|
|
18
19
|
|
|
19
|
-
const builtin = builtinPlugins.find((p) => p
|
|
20
|
+
const builtin = builtinPlugins.find((p) => supportsPlugin(p, challenge))
|
|
20
21
|
if (builtin) return { plugin: builtin }
|
|
21
22
|
|
|
22
|
-
const configMethods = config
|
|
23
|
+
const configMethods = flattenConfigMethods(config)
|
|
23
24
|
const matched = configMethods?.find(
|
|
24
25
|
(m) => m.name === challenge.method && m.intent === challenge.intent,
|
|
25
26
|
)
|
|
@@ -28,6 +29,61 @@ export function resolvePlugin(
|
|
|
28
29
|
return {}
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export function selectChallenge(
|
|
33
|
+
challenges: readonly Challenge.Challenge[],
|
|
34
|
+
config?: Config | undefined,
|
|
35
|
+
):
|
|
36
|
+
| ({ challenge: Challenge.Challenge } & {
|
|
37
|
+
plugin?: Plugin | undefined
|
|
38
|
+
method?: Method.AnyClient | undefined
|
|
39
|
+
})
|
|
40
|
+
| undefined {
|
|
41
|
+
const configMethods = flattenConfigMethods(config)
|
|
42
|
+
if (configMethods?.length) {
|
|
43
|
+
const resolvedPreferences = AcceptPayment.resolve(
|
|
44
|
+
configMethods,
|
|
45
|
+
config?.paymentPreferences as AcceptPayment.Config<typeof configMethods> | undefined,
|
|
46
|
+
)
|
|
47
|
+
const selected = AcceptPayment.selectChallenge(
|
|
48
|
+
challenges,
|
|
49
|
+
configMethods,
|
|
50
|
+
resolvedPreferences.entries,
|
|
51
|
+
)
|
|
52
|
+
if (selected) {
|
|
53
|
+
return { challenge: selected.challenge, ...resolvePlugin(selected.challenge, config) }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const challenge of challenges) {
|
|
60
|
+
const resolved = resolvePlugin(challenge, config)
|
|
61
|
+
if (resolved.plugin || resolved.method) return { challenge, ...resolved }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveAcceptPayment(config?: Config | undefined): string | undefined {
|
|
68
|
+
const methods = flattenConfigMethods(config)
|
|
69
|
+
if (!methods?.length) return undefined
|
|
70
|
+
|
|
71
|
+
return AcceptPayment.resolve(
|
|
72
|
+
methods,
|
|
73
|
+
config?.paymentPreferences as AcceptPayment.Config<typeof methods> | undefined,
|
|
74
|
+
).header
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function flattenConfigMethods(
|
|
78
|
+
config?: Pick<Config, 'methods'> | undefined,
|
|
79
|
+
): Method.AnyClient[] | undefined {
|
|
80
|
+
return Array.isArray(config?.methods) ? (config.methods.flat() as Method.AnyClient[]) : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function supportsPlugin(plugin: Plugin, challenge: Challenge.Challenge): boolean {
|
|
84
|
+
return plugin.supports ? plugin.supports(challenge) : plugin.method === challenge.method
|
|
85
|
+
}
|
|
86
|
+
|
|
31
87
|
const CONFIG_NAMES = ['mppx.config.ts', 'mppx.config.js', 'mppx.config.mjs'] as const
|
|
32
88
|
|
|
33
89
|
export async function loadConfig(
|
|
@@ -9,6 +9,9 @@ export interface Plugin {
|
|
|
9
9
|
/** Payment method name (e.g., 'tempo', 'stripe') */
|
|
10
10
|
method: string
|
|
11
11
|
|
|
12
|
+
/** Optional predicate for challenge support when a plugin does not support every intent for its method. */
|
|
13
|
+
supports?(challenge: Challenge.Challenge): boolean
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Resolve account, client, and display info for a challenge.
|
|
14
17
|
* Returns methods for credential creation.
|
|
@@ -7,6 +7,9 @@ import { createPlugin } from './plugin.js'
|
|
|
7
7
|
export function stripe() {
|
|
8
8
|
return createPlugin({
|
|
9
9
|
method: 'stripe',
|
|
10
|
+
supports(challenge) {
|
|
11
|
+
return challenge.method === 'stripe' && challenge.intent === 'charge'
|
|
12
|
+
},
|
|
10
13
|
|
|
11
14
|
async setup({ challenge, methodOpts }) {
|
|
12
15
|
const challengeRequest = challenge.request as Record<string, unknown>
|
package/src/cli/plugins/tempo.ts
CHANGED
|
@@ -46,6 +46,9 @@ export function tempo() {
|
|
|
46
46
|
|
|
47
47
|
return createPlugin({
|
|
48
48
|
method: 'tempo',
|
|
49
|
+
supports(challenge) {
|
|
50
|
+
return challenge.method === 'tempo' && ['charge', 'session'].includes(challenge.intent)
|
|
51
|
+
},
|
|
49
52
|
|
|
50
53
|
async setup({ challenge, options, methodOpts }) {
|
|
51
54
|
const accountName = resolveAccountName(options.account)
|
|
@@ -46,6 +46,23 @@ describe('create.Config', () => {
|
|
|
46
46
|
|
|
47
47
|
expectTypeOf<Config>().toHaveProperty('methods')
|
|
48
48
|
})
|
|
49
|
+
|
|
50
|
+
test('paymentPreferences callback exposes typed method keys', () => {
|
|
51
|
+
const mppx = Mppx.create({
|
|
52
|
+
methods: [tempo({ account: {} as Account })],
|
|
53
|
+
paymentPreferences: ({ tempo }) => {
|
|
54
|
+
expectTypeOf(tempo.charge).toEqualTypeOf<'tempo/charge'>()
|
|
55
|
+
expectTypeOf(tempo.session).toEqualTypeOf<'tempo/session'>()
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
[tempo.charge]: 0.5,
|
|
59
|
+
[tempo.session]: 0,
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expectTypeOf(mppx.fetch).toBeFunction()
|
|
65
|
+
})
|
|
49
66
|
})
|
|
50
67
|
|
|
51
68
|
describe('Method.toClient', () => {
|
|
@@ -99,6 +116,22 @@ describe('Mppx with context', () => {
|
|
|
99
116
|
expectTypeOf(mppx.createCredential).toBeFunction()
|
|
100
117
|
expectTypeOf(mppx.createCredential).returns.toMatchTypeOf<Promise<string>>()
|
|
101
118
|
})
|
|
119
|
+
|
|
120
|
+
test('createCredential accepts an optional Accept-Payment override', () => {
|
|
121
|
+
const method = charge({
|
|
122
|
+
account: {} as Account,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const mppx = Mppx.create({ methods: [method] })
|
|
126
|
+
|
|
127
|
+
const createCredential: (
|
|
128
|
+
response: Response,
|
|
129
|
+
context?: Parameters<typeof mppx.createCredential>[1],
|
|
130
|
+
options?: Parameters<typeof mppx.createCredential>[2],
|
|
131
|
+
) => Promise<string> = mppx.createCredential
|
|
132
|
+
|
|
133
|
+
expectTypeOf(createCredential).toBeFunction()
|
|
134
|
+
})
|
|
102
135
|
})
|
|
103
136
|
|
|
104
137
|
describe('fetch context', () => {
|