mppx 0.3.13 → 0.3.15
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/dist/Challenge.d.ts +1 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +107 -15
- package/dist/Challenge.js.map +1 -1
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +26 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1478 -915
- package/dist/cli.js.map +1 -1
- package/dist/client/Mppx.d.ts +2 -0
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +2 -0
- package/dist/client/Mppx.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +2 -1
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/Methods.d.ts +0 -3
- package/dist/stripe/Methods.d.ts.map +1 -1
- package/dist/stripe/Methods.js +0 -2
- package/dist/stripe/Methods.js.map +1 -1
- package/dist/stripe/client/Charge.d.ts +0 -3
- package/dist/stripe/client/Charge.d.ts.map +1 -1
- package/dist/stripe/client/Charge.js +2 -2
- package/dist/stripe/client/Charge.js.map +1 -1
- package/dist/stripe/client/Methods.d.ts +0 -3
- package/dist/stripe/client/Methods.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts +0 -3
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +2 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/Methods.d.ts +0 -3
- package/dist/stripe/server/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.d.ts +0 -3
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +3 -3
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +13 -3
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +18 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +4 -3
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Charge.d.ts +0 -3
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +2 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +0 -3
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/Challenge.test.ts +94 -18
- package/src/Challenge.ts +118 -15
- package/src/PaymentRequest.test.ts +0 -5
- package/src/bin.ts +4 -0
- package/src/cli.test.ts +180 -252
- package/src/cli.ts +1085 -485
- package/src/client/Mppx.test-d.ts +9 -0
- package/src/client/Mppx.test.ts +83 -5
- package/src/client/Mppx.ts +5 -0
- package/src/client/Transport.test.ts +5 -8
- package/src/client/internal/Fetch.browser.test.ts +135 -0
- package/src/client/internal/Fetch.test.ts +0 -88
- package/src/mcp-sdk/client/McpClient.test.ts +1 -1
- package/src/server/Mppx.ts +3 -1
- package/src/server/Transport.test.ts +6 -9
- package/src/stripe/Methods.ts +0 -2
- package/src/stripe/client/Charge.ts +2 -2
- package/src/stripe/server/Charge.ts +2 -2
- package/src/tempo/Methods.test.ts +22 -0
- package/src/tempo/Methods.ts +3 -3
- package/src/tempo/client/Charge.ts +29 -1
- package/src/tempo/server/Charge.test.ts +34 -72
- package/src/tempo/server/Charge.ts +2 -1
|
@@ -28,6 +28,15 @@ describe('Mppx', () => {
|
|
|
28
28
|
expectTypeOf(mppx.createCredential).toBeFunction()
|
|
29
29
|
expectTypeOf(mppx.createCredential).returns.toMatchTypeOf<Promise<string>>()
|
|
30
30
|
})
|
|
31
|
+
|
|
32
|
+
test('has rawFetch with standard fetch signature', () => {
|
|
33
|
+
const method = charge({
|
|
34
|
+
account: {} as Account,
|
|
35
|
+
})
|
|
36
|
+
const mppx = Mppx.create({ methods: [method] })
|
|
37
|
+
|
|
38
|
+
expectTypeOf(mppx.rawFetch).toEqualTypeOf<typeof globalThis.fetch>()
|
|
39
|
+
})
|
|
31
40
|
})
|
|
32
41
|
|
|
33
42
|
describe('create.Config', () => {
|
package/src/client/Mppx.test.ts
CHANGED
|
@@ -28,6 +28,7 @@ describe('Mppx.create', () => {
|
|
|
28
28
|
expect(mppx.transport.name).toBe('http')
|
|
29
29
|
expect(typeof mppx.createCredential).toBe('function')
|
|
30
30
|
expect(typeof mppx.fetch).toBe('function')
|
|
31
|
+
expect(typeof mppx.rawFetch).toBe('function')
|
|
31
32
|
})
|
|
32
33
|
|
|
33
34
|
test('behavior: with mcp transport', () => {
|
|
@@ -82,12 +83,12 @@ describe('createCredential', () => {
|
|
|
82
83
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
83
84
|
realm,
|
|
84
85
|
secretKey,
|
|
86
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
85
87
|
request: {
|
|
86
88
|
amount: '1000',
|
|
87
89
|
currency: '0x1234567890123456789012345678901234567890',
|
|
88
90
|
decimals: 6,
|
|
89
91
|
recipient: '0x1234567890123456789012345678901234567890',
|
|
90
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
91
92
|
},
|
|
92
93
|
})
|
|
93
94
|
|
|
@@ -164,11 +165,11 @@ describe('createCredential', () => {
|
|
|
164
165
|
realm,
|
|
165
166
|
method: 'stripe',
|
|
166
167
|
intent: 'charge',
|
|
168
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
167
169
|
request: {
|
|
168
170
|
amount: '2000',
|
|
169
171
|
currency: '0xabcd',
|
|
170
172
|
recipient: '0xefgh',
|
|
171
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
172
173
|
},
|
|
173
174
|
})
|
|
174
175
|
|
|
@@ -195,12 +196,12 @@ describe('createCredential', () => {
|
|
|
195
196
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
196
197
|
realm,
|
|
197
198
|
secretKey,
|
|
199
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
198
200
|
request: {
|
|
199
201
|
amount: '1000',
|
|
200
202
|
currency: '0x1234567890123456789012345678901234567890',
|
|
201
203
|
decimals: 6,
|
|
202
204
|
recipient: '0x1234567890123456789012345678901234567890',
|
|
203
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
204
205
|
},
|
|
205
206
|
})
|
|
206
207
|
|
|
@@ -227,12 +228,12 @@ describe('createCredential', () => {
|
|
|
227
228
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
228
229
|
realm,
|
|
229
230
|
secretKey,
|
|
231
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
230
232
|
request: {
|
|
231
233
|
amount: '1000',
|
|
232
234
|
currency: '0x1234567890123456789012345678901234567890',
|
|
233
235
|
decimals: 6,
|
|
234
236
|
recipient: '0x1234567890123456789012345678901234567890',
|
|
235
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
236
237
|
},
|
|
237
238
|
})
|
|
238
239
|
|
|
@@ -258,12 +259,12 @@ describe('createCredential', () => {
|
|
|
258
259
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
259
260
|
realm,
|
|
260
261
|
secretKey,
|
|
262
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
261
263
|
request: {
|
|
262
264
|
amount: '1000',
|
|
263
265
|
currency: '0x1234567890123456789012345678901234567890',
|
|
264
266
|
decimals: 6,
|
|
265
267
|
recipient: '0x1234567890123456789012345678901234567890',
|
|
266
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
267
268
|
},
|
|
268
269
|
})
|
|
269
270
|
|
|
@@ -471,3 +472,80 @@ describe('restore', () => {
|
|
|
471
472
|
expect(globalThis.fetch).toBe(originalFetch)
|
|
472
473
|
})
|
|
473
474
|
})
|
|
475
|
+
|
|
476
|
+
describe('rawFetch', () => {
|
|
477
|
+
test('default: returns the original fetch when polyfill is enabled', () => {
|
|
478
|
+
const originalFetch = globalThis.fetch
|
|
479
|
+
|
|
480
|
+
const mppx = Mppx.create({
|
|
481
|
+
methods: [
|
|
482
|
+
tempo({
|
|
483
|
+
account: accounts[1],
|
|
484
|
+
getClient: () => client,
|
|
485
|
+
}),
|
|
486
|
+
],
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
expect(globalThis.fetch).not.toBe(originalFetch)
|
|
490
|
+
expect(mppx.rawFetch).toBe(originalFetch)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
test('behavior: returns the original fetch when polyfill is disabled', () => {
|
|
494
|
+
const originalFetch = globalThis.fetch
|
|
495
|
+
|
|
496
|
+
const mppx = Mppx.create({
|
|
497
|
+
polyfill: false,
|
|
498
|
+
methods: [
|
|
499
|
+
tempo({
|
|
500
|
+
account: accounts[1],
|
|
501
|
+
getClient: () => client,
|
|
502
|
+
}),
|
|
503
|
+
],
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
expect(mppx.rawFetch).toBe(originalFetch)
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
test('behavior: returns custom fetch when provided', () => {
|
|
510
|
+
const customFetch = async () => new Response('custom')
|
|
511
|
+
|
|
512
|
+
const mppx = Mppx.create({
|
|
513
|
+
polyfill: false,
|
|
514
|
+
fetch: customFetch as typeof globalThis.fetch,
|
|
515
|
+
methods: [
|
|
516
|
+
tempo({
|
|
517
|
+
account: accounts[1],
|
|
518
|
+
getClient: () => client,
|
|
519
|
+
}),
|
|
520
|
+
],
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
expect(mppx.rawFetch).toBe(customFetch)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
test('behavior: rawFetch does not intercept 402 responses', async () => {
|
|
527
|
+
const mppx = Mppx.create({
|
|
528
|
+
methods: [
|
|
529
|
+
tempo({
|
|
530
|
+
account: accounts[1],
|
|
531
|
+
getClient: () => client,
|
|
532
|
+
}),
|
|
533
|
+
],
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
537
|
+
const result = await Mppx_server.toNodeListener(
|
|
538
|
+
server.charge({
|
|
539
|
+
amount: '1',
|
|
540
|
+
}),
|
|
541
|
+
)(req, res)
|
|
542
|
+
if (result.status === 402) return
|
|
543
|
+
res.end('OK')
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
const response = await mppx.rawFetch(httpServer.url)
|
|
547
|
+
expect(response.status).toBe(402)
|
|
548
|
+
|
|
549
|
+
httpServer.close()
|
|
550
|
+
})
|
|
551
|
+
})
|
package/src/client/Mppx.ts
CHANGED
|
@@ -15,6 +15,8 @@ export type Mppx<
|
|
|
15
15
|
> = {
|
|
16
16
|
/** Payment-aware fetch function that automatically handles 402 responses. */
|
|
17
17
|
fetch: Fetch.from.Fetch<FlattenMethods<methods>>
|
|
18
|
+
/** The original, unwrapped fetch function (pre-polyfill). Useful when you need to make requests that should not be intercepted (e.g. 402 probes for websocket auth). */
|
|
19
|
+
rawFetch: typeof globalThis.fetch
|
|
18
20
|
/** Methods to configure. */
|
|
19
21
|
methods: FlattenMethods<methods>
|
|
20
22
|
/** The transport used. */
|
|
@@ -56,6 +58,8 @@ export function create<
|
|
|
56
58
|
>(config: create.Config<methods, transport>): Mppx<methods, transport> {
|
|
57
59
|
const { onChallenge, polyfill = true, transport = Transport.http() as transport } = config
|
|
58
60
|
|
|
61
|
+
const rawFetch = config.fetch ?? globalThis.fetch
|
|
62
|
+
|
|
59
63
|
const methods = config.methods.flat() as unknown as FlattenMethods<methods>
|
|
60
64
|
|
|
61
65
|
const resolvedOnChallenge = onChallenge as Fetch.from.Config<
|
|
@@ -71,6 +75,7 @@ export function create<
|
|
|
71
75
|
if (polyfill) Fetch.polyfill(config_fetch)
|
|
72
76
|
return {
|
|
73
77
|
fetch,
|
|
78
|
+
rawFetch,
|
|
74
79
|
methods,
|
|
75
80
|
transport,
|
|
76
81
|
async createCredential(response: Transport.ResponseOf<transport>, context?: unknown) {
|
|
@@ -9,12 +9,12 @@ const secretKey = 'test-secret-key'
|
|
|
9
9
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
10
10
|
realm,
|
|
11
11
|
secretKey,
|
|
12
|
+
expires: '2025-01-01T00:00:00.000Z',
|
|
12
13
|
request: {
|
|
13
14
|
amount: '0.001',
|
|
14
15
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
15
16
|
decimals: 6,
|
|
16
17
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
17
|
-
expires: '2025-01-01T00:00:00.000Z',
|
|
18
18
|
},
|
|
19
19
|
})
|
|
20
20
|
|
|
@@ -60,14 +60,13 @@ describe('http', () => {
|
|
|
60
60
|
expect(transport.getChallenge(response)).toMatchInlineSnapshot(`
|
|
61
61
|
{
|
|
62
62
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
63
|
-
"id": "
|
|
63
|
+
"id": "0hnrySRDqWfttlDIJpuxV4mJsRJIS7d7RjnufuonJOE",
|
|
64
64
|
"intent": "charge",
|
|
65
65
|
"method": "tempo",
|
|
66
66
|
"realm": "api.example.com",
|
|
67
67
|
"request": {
|
|
68
68
|
"amount": "1000",
|
|
69
69
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
70
|
-
"expires": "2025-01-01T00:00:00.000Z",
|
|
71
70
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
72
71
|
},
|
|
73
72
|
}
|
|
@@ -91,7 +90,7 @@ describe('http', () => {
|
|
|
91
90
|
const headers = result.headers as Headers
|
|
92
91
|
|
|
93
92
|
expect(headers.get('Authorization')).toMatchInlineSnapshot(
|
|
94
|
-
`"Payment
|
|
93
|
+
`"Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjUtMDEtMDFUMDA6MDA6MDAuMDAwWiIsImlkIjoiMGhucnlTUkRxV2Z0dGxESUpwdXhWNG1Kc1JKSVM3ZDdSam51ZnVvbkpPRSIsImludGVudCI6ImNoYXJnZSIsIm1ldGhvZCI6InRlbXBvIiwicmVhbG0iOiJhcGkuZXhhbXBsZS5jb20iLCJyZXF1ZXN0IjoiZXlKaGJXOTFiblFpT2lJeE1EQXdJaXdpWTNWeWNtVnVZM2tpT2lJd2VESXdZekF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREVpTENKeVpXTnBjR2xsYm5RaU9pSXdlRGMwTW1Rek5VTmpOall6TkVNd05UTXlPVEkxWVROaU9EUTBRbU01WlRjMU9UVm1PR1pGTURBaWZRIn0sInBheWxvYWQiOnsic2lnbmF0dXJlIjoiMHhhYmMxMjMiLCJ0eXBlIjoidHJhbnNhY3Rpb24ifX0"`,
|
|
95
94
|
)
|
|
96
95
|
})
|
|
97
96
|
|
|
@@ -182,14 +181,13 @@ describe('mcp', () => {
|
|
|
182
181
|
expect(transport.getChallenge(response)).toMatchInlineSnapshot(`
|
|
183
182
|
{
|
|
184
183
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
185
|
-
"id": "
|
|
184
|
+
"id": "0hnrySRDqWfttlDIJpuxV4mJsRJIS7d7RjnufuonJOE",
|
|
186
185
|
"intent": "charge",
|
|
187
186
|
"method": "tempo",
|
|
188
187
|
"realm": "api.example.com",
|
|
189
188
|
"request": {
|
|
190
189
|
"amount": "1000",
|
|
191
190
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
192
|
-
"expires": "2025-01-01T00:00:00.000Z",
|
|
193
191
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
194
192
|
},
|
|
195
193
|
}
|
|
@@ -239,14 +237,13 @@ describe('mcp', () => {
|
|
|
239
237
|
"org.paymentauth/credential": {
|
|
240
238
|
"challenge": {
|
|
241
239
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
242
|
-
"id": "
|
|
240
|
+
"id": "0hnrySRDqWfttlDIJpuxV4mJsRJIS7d7RjnufuonJOE",
|
|
243
241
|
"intent": "charge",
|
|
244
242
|
"method": "tempo",
|
|
245
243
|
"realm": "api.example.com",
|
|
246
244
|
"request": {
|
|
247
245
|
"amount": "1000",
|
|
248
246
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
249
|
-
"expires": "2025-01-01T00:00:00.000Z",
|
|
250
247
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
251
248
|
},
|
|
252
249
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import * as Fetch from './Fetch.js'
|
|
3
|
+
|
|
4
|
+
const noopMethod = {
|
|
5
|
+
name: 'test',
|
|
6
|
+
intent: 'test',
|
|
7
|
+
context: undefined,
|
|
8
|
+
createCredential: async () => 'credential',
|
|
9
|
+
} as any
|
|
10
|
+
|
|
11
|
+
function make402() {
|
|
12
|
+
const request = btoa(JSON.stringify({ amount: '1' }))
|
|
13
|
+
.replace(/\+/g, '-')
|
|
14
|
+
.replace(/\//g, '_')
|
|
15
|
+
.replace(/=+$/, '')
|
|
16
|
+
return new Response(null, {
|
|
17
|
+
status: 402,
|
|
18
|
+
headers: {
|
|
19
|
+
'WWW-Authenticate': `Payment id="abc", realm="test", method="test", intent="test", request="${request}"`,
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Returns a fetch wrapper and the init captured from the 402 retry call. */
|
|
25
|
+
function setup() {
|
|
26
|
+
const calls: (RequestInit | undefined)[] = []
|
|
27
|
+
let callCount = 0
|
|
28
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
29
|
+
calls.push(init)
|
|
30
|
+
callCount++
|
|
31
|
+
if (callCount === 1) return make402()
|
|
32
|
+
return new Response('OK', { status: 200 })
|
|
33
|
+
}
|
|
34
|
+
const fetch = Fetch.from({ fetch: mockFetch, methods: [noopMethod] })
|
|
35
|
+
return {
|
|
36
|
+
fetch,
|
|
37
|
+
/** Headers sent on the retry (second) request. */
|
|
38
|
+
retryHeaders: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
39
|
+
await fetch(input, init)
|
|
40
|
+
return (calls[1] as Record<string, unknown>)?.headers as Record<string, string>
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('Fetch.from: browser header normalization', () => {
|
|
46
|
+
test('preserves Headers instance', async () => {
|
|
47
|
+
const { retryHeaders } = setup()
|
|
48
|
+
const h = await retryHeaders('https://example.com', {
|
|
49
|
+
headers: new Headers({ 'X-Custom': 'value', 'Content-Type': 'application/json' }),
|
|
50
|
+
})
|
|
51
|
+
expect(h['x-custom']).toBe('value')
|
|
52
|
+
expect(h['content-type']).toBe('application/json')
|
|
53
|
+
expect(h.Authorization).toBe('credential')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('preserves header tuples', async () => {
|
|
57
|
+
const { retryHeaders } = setup()
|
|
58
|
+
const h = await retryHeaders('https://example.com', {
|
|
59
|
+
headers: [
|
|
60
|
+
['X-Custom', 'value'],
|
|
61
|
+
['Accept', 'application/json'],
|
|
62
|
+
],
|
|
63
|
+
})
|
|
64
|
+
expect(h['X-Custom']).toBe('value')
|
|
65
|
+
expect(h.Accept).toBe('application/json')
|
|
66
|
+
expect(h.Authorization).toBe('credential')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('replaces authorization case-insensitively', async () => {
|
|
70
|
+
const { retryHeaders } = setup()
|
|
71
|
+
const h = await retryHeaders('https://example.com', {
|
|
72
|
+
headers: { authorization: 'Bearer stale', 'X-Custom': 'value' },
|
|
73
|
+
})
|
|
74
|
+
expect(h.authorization).toBeUndefined()
|
|
75
|
+
expect(h.Authorization).toBe('credential')
|
|
76
|
+
expect(h['X-Custom']).toBe('value')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('preserves plain object headers', async () => {
|
|
80
|
+
const { retryHeaders } = setup()
|
|
81
|
+
const h = await retryHeaders('https://example.com', { headers: { 'X-Custom': 'val' } })
|
|
82
|
+
expect(h['X-Custom']).toBe('val')
|
|
83
|
+
expect(h.Authorization).toBe('credential')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('adds Authorization when no headers provided', async () => {
|
|
87
|
+
const { retryHeaders } = setup()
|
|
88
|
+
const h = await retryHeaders('https://example.com')
|
|
89
|
+
expect(h.Authorization).toBe('credential')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('Fetch.polyfill / restore: browser', () => {
|
|
94
|
+
test('restore is a no-op when polyfill was never called', () => {
|
|
95
|
+
const before = globalThis.fetch
|
|
96
|
+
Fetch.restore()
|
|
97
|
+
expect(globalThis.fetch).toBe(before)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('restore reverts to original fetch', () => {
|
|
101
|
+
const original = globalThis.fetch
|
|
102
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
103
|
+
expect(globalThis.fetch).not.toBe(original)
|
|
104
|
+
Fetch.restore()
|
|
105
|
+
expect(globalThis.fetch).toBe(original)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('stacked polyfill calls preserve the true original', () => {
|
|
109
|
+
const original = globalThis.fetch
|
|
110
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
111
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
112
|
+
Fetch.restore()
|
|
113
|
+
expect(globalThis.fetch).toBe(original)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('double restore does not clobber fetch', () => {
|
|
117
|
+
const original = globalThis.fetch
|
|
118
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
119
|
+
Fetch.restore()
|
|
120
|
+
Fetch.restore()
|
|
121
|
+
expect(globalThis.fetch).toBe(original)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('restore is a no-op when fetch was replaced externally', () => {
|
|
125
|
+
const original = globalThis.fetch
|
|
126
|
+
const external = vi.fn(
|
|
127
|
+
async () => new Response('external'),
|
|
128
|
+
) as unknown as typeof globalThis.fetch
|
|
129
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
130
|
+
globalThis.fetch = external
|
|
131
|
+
Fetch.restore()
|
|
132
|
+
expect(globalThis.fetch).toBe(external)
|
|
133
|
+
globalThis.fetch = original
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -552,94 +552,6 @@ describe('Fetch.from: 402 retry path', () => {
|
|
|
552
552
|
})
|
|
553
553
|
})
|
|
554
554
|
|
|
555
|
-
describe('Fetch.from: 402 retry headers normalization', () => {
|
|
556
|
-
test('preserves headers when passed as a Headers instance', async () => {
|
|
557
|
-
let callCount = 0
|
|
558
|
-
const calls: { init: RequestInit | undefined }[] = []
|
|
559
|
-
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
560
|
-
calls.push({ init })
|
|
561
|
-
callCount++
|
|
562
|
-
if (callCount === 1) return make402()
|
|
563
|
-
return new Response('OK', { status: 200 })
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const fetch = Fetch.from({
|
|
567
|
-
fetch: mockFetch,
|
|
568
|
-
methods: [noopMethod],
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
const headers = new Headers({ 'X-Custom': 'value', 'Content-Type': 'application/json' })
|
|
572
|
-
await fetch('https://example.com/api', { headers })
|
|
573
|
-
|
|
574
|
-
const retryHeaders = (calls[1]!.init as Record<string, unknown>).headers as Record<
|
|
575
|
-
string,
|
|
576
|
-
string
|
|
577
|
-
>
|
|
578
|
-
expect(retryHeaders['x-custom']).toBe('value')
|
|
579
|
-
expect(retryHeaders['content-type']).toBe('application/json')
|
|
580
|
-
expect(retryHeaders.Authorization).toBe('credential')
|
|
581
|
-
})
|
|
582
|
-
|
|
583
|
-
test('preserves headers when passed as array of tuples', async () => {
|
|
584
|
-
let callCount = 0
|
|
585
|
-
const calls: { init: RequestInit | undefined }[] = []
|
|
586
|
-
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
587
|
-
calls.push({ init })
|
|
588
|
-
callCount++
|
|
589
|
-
if (callCount === 1) return make402()
|
|
590
|
-
return new Response('OK', { status: 200 })
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const fetch = Fetch.from({
|
|
594
|
-
fetch: mockFetch,
|
|
595
|
-
methods: [noopMethod],
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
await fetch('https://example.com/api', {
|
|
599
|
-
headers: [
|
|
600
|
-
['X-Custom', 'value'],
|
|
601
|
-
['Accept', 'application/json'],
|
|
602
|
-
],
|
|
603
|
-
})
|
|
604
|
-
|
|
605
|
-
const retryHeaders = (calls[1]!.init as Record<string, unknown>).headers as Record<
|
|
606
|
-
string,
|
|
607
|
-
string
|
|
608
|
-
>
|
|
609
|
-
expect(retryHeaders['X-Custom']).toBe('value')
|
|
610
|
-
expect(retryHeaders.Accept).toBe('application/json')
|
|
611
|
-
expect(retryHeaders.Authorization).toBe('credential')
|
|
612
|
-
})
|
|
613
|
-
|
|
614
|
-
test('replaces existing authorization header case-insensitively', async () => {
|
|
615
|
-
let callCount = 0
|
|
616
|
-
const calls: { init: RequestInit | undefined }[] = []
|
|
617
|
-
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
618
|
-
calls.push({ init })
|
|
619
|
-
callCount++
|
|
620
|
-
if (callCount === 1) return make402()
|
|
621
|
-
return new Response('OK', { status: 200 })
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const fetch = Fetch.from({
|
|
625
|
-
fetch: mockFetch,
|
|
626
|
-
methods: [noopMethod],
|
|
627
|
-
})
|
|
628
|
-
|
|
629
|
-
await fetch('https://example.com/api', {
|
|
630
|
-
headers: { authorization: 'Bearer stale-token', 'X-Custom': 'value' },
|
|
631
|
-
})
|
|
632
|
-
|
|
633
|
-
const retryHeaders = (calls[1]!.init as Record<string, unknown>).headers as Record<
|
|
634
|
-
string,
|
|
635
|
-
string
|
|
636
|
-
>
|
|
637
|
-
expect(retryHeaders.authorization).toBeUndefined()
|
|
638
|
-
expect(retryHeaders.Authorization).toBe('credential')
|
|
639
|
-
expect(retryHeaders['X-Custom']).toBe('value')
|
|
640
|
-
})
|
|
641
|
-
})
|
|
642
|
-
|
|
643
555
|
describe('Fetch.from: input passthrough', () => {
|
|
644
556
|
test('passes URL input through on both initial and retry calls', async () => {
|
|
645
557
|
let callCount = 0
|
|
@@ -157,11 +157,11 @@ describe('McpClient.wrap', () => {
|
|
|
157
157
|
const challenge = Challenge.fromMethod(tempo_server.charge({ getClient: () => testClient }), {
|
|
158
158
|
realm,
|
|
159
159
|
secretKey,
|
|
160
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
160
161
|
request: {
|
|
161
162
|
amount: '1',
|
|
162
163
|
currency: asset,
|
|
163
164
|
decimals: 6,
|
|
164
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
165
165
|
recipient: accounts[0].address,
|
|
166
166
|
},
|
|
167
167
|
})
|
package/src/server/Mppx.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
|
2
2
|
import * as Challenge from '../Challenge.js'
|
|
3
3
|
import type * as Credential from '../Credential.js'
|
|
4
4
|
import * as Errors from '../Errors.js'
|
|
5
|
+
import * as Expires from '../Expires.js'
|
|
5
6
|
import * as Env from '../internal/env.js'
|
|
6
7
|
import type * as Method from '../Method.js'
|
|
7
8
|
import type * as Receipt from '../Receipt.js'
|
|
@@ -173,7 +174,8 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
173
174
|
return Object.assign(
|
|
174
175
|
async (input: Transport.InputOf): Promise<MethodFn.Response> => {
|
|
175
176
|
const { description, meta, ...rest } = options
|
|
176
|
-
const expires =
|
|
177
|
+
const expires =
|
|
178
|
+
'expires' in options ? (options.expires as string | undefined) : Expires.minutes(5)
|
|
177
179
|
|
|
178
180
|
// Merge defaults with per-request options
|
|
179
181
|
const merged = { ...defaults, ...rest }
|
|
@@ -10,12 +10,12 @@ const secretKey = 'test-secret-key'
|
|
|
10
10
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
11
11
|
realm,
|
|
12
12
|
secretKey,
|
|
13
|
+
expires: '2025-01-01T00:00:00.000Z',
|
|
13
14
|
request: {
|
|
14
15
|
amount: '1000',
|
|
15
16
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
16
17
|
decimals: 6,
|
|
17
18
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
18
|
-
expires: '2025-01-01T00:00:00.000Z',
|
|
19
19
|
},
|
|
20
20
|
})
|
|
21
21
|
|
|
@@ -43,14 +43,13 @@ describe('http', () => {
|
|
|
43
43
|
{
|
|
44
44
|
"challenge": {
|
|
45
45
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
46
|
-
"id": "
|
|
46
|
+
"id": "QNLtjAvrKKR0VlEGSIowhULqcGlCDU4fjrP-O7js8XE",
|
|
47
47
|
"intent": "charge",
|
|
48
48
|
"method": "tempo",
|
|
49
49
|
"realm": "api.example.com",
|
|
50
50
|
"request": {
|
|
51
51
|
"amount": "1000000000",
|
|
52
52
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
53
|
-
"expires": "2025-01-01T00:00:00.000Z",
|
|
54
53
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
55
54
|
},
|
|
56
55
|
},
|
|
@@ -93,7 +92,7 @@ describe('http', () => {
|
|
|
93
92
|
{
|
|
94
93
|
"headers": {
|
|
95
94
|
"cache-control": "no-store",
|
|
96
|
-
"www-authenticate": "Payment id="
|
|
95
|
+
"www-authenticate": "Payment id="QNLtjAvrKKR0VlEGSIowhULqcGlCDU4fjrP-O7js8XE", realm="api.example.com", method="tempo", intent="charge", request="eyJhbW91bnQiOiIxMDAwMDAwMDAwIiwiY3VycmVuY3kiOiIweDIwYzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEiLCJyZWNpcGllbnQiOiIweDc0MmQzNUNjNjYzNEMwNTMyOTI1YTNiODQ0QmM5ZTc1OTVmOGZFMDAifQ", expires="2025-01-01T00:00:00.000Z"",
|
|
97
96
|
},
|
|
98
97
|
"status": 402,
|
|
99
98
|
}
|
|
@@ -183,14 +182,13 @@ describe('mcp', () => {
|
|
|
183
182
|
{
|
|
184
183
|
"challenge": {
|
|
185
184
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
186
|
-
"id": "
|
|
185
|
+
"id": "QNLtjAvrKKR0VlEGSIowhULqcGlCDU4fjrP-O7js8XE",
|
|
187
186
|
"intent": "charge",
|
|
188
187
|
"method": "tempo",
|
|
189
188
|
"realm": "api.example.com",
|
|
190
189
|
"request": {
|
|
191
190
|
"amount": "1000000000",
|
|
192
191
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
193
|
-
"expires": "2025-01-01T00:00:00.000Z",
|
|
194
192
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
195
193
|
},
|
|
196
194
|
},
|
|
@@ -221,14 +219,13 @@ describe('mcp', () => {
|
|
|
221
219
|
"challenges": [
|
|
222
220
|
{
|
|
223
221
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
224
|
-
"id": "
|
|
222
|
+
"id": "QNLtjAvrKKR0VlEGSIowhULqcGlCDU4fjrP-O7js8XE",
|
|
225
223
|
"intent": "charge",
|
|
226
224
|
"method": "tempo",
|
|
227
225
|
"realm": "api.example.com",
|
|
228
226
|
"request": {
|
|
229
227
|
"amount": "1000000000",
|
|
230
228
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
231
|
-
"expires": "2025-01-01T00:00:00.000Z",
|
|
232
229
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
233
230
|
},
|
|
234
231
|
},
|
|
@@ -262,7 +259,7 @@ describe('mcp', () => {
|
|
|
262
259
|
"result": {
|
|
263
260
|
"_meta": {
|
|
264
261
|
"org.paymentauth/receipt": {
|
|
265
|
-
"challengeId": "
|
|
262
|
+
"challengeId": "QNLtjAvrKKR0VlEGSIowhULqcGlCDU4fjrP-O7js8XE",
|
|
266
263
|
"method": "tempo",
|
|
267
264
|
"reference": "0xtxhash",
|
|
268
265
|
"status": "success",
|
package/src/stripe/Methods.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { parseUnits } from 'viem'
|
|
2
|
-
import * as Expires from '../Expires.js'
|
|
3
2
|
import * as Method from '../Method.js'
|
|
4
3
|
import * as z from '../zod.js'
|
|
5
4
|
|
|
@@ -24,7 +23,6 @@ export const charge = Method.from({
|
|
|
24
23
|
currency: z.string(),
|
|
25
24
|
decimals: z.number(),
|
|
26
25
|
description: z.optional(z.string()),
|
|
27
|
-
expires: z._default(z.datetime(), () => Expires.minutes(5)),
|
|
28
26
|
externalId: z.optional(z.string()),
|
|
29
27
|
metadata: z.optional(z.record(z.string(), z.string())),
|
|
30
28
|
networkId: z.string(),
|
|
@@ -66,8 +66,8 @@ export function charge(parameters: charge.Parameters) {
|
|
|
66
66
|
)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const expiresAt = challenge.
|
|
70
|
-
? Math.floor(new Date(challenge.
|
|
69
|
+
const expiresAt = challenge.expires
|
|
70
|
+
? Math.floor(new Date(challenge.expires).getTime() / 1000)
|
|
71
71
|
: Math.floor(Date.now() / 1000) + 3600
|
|
72
72
|
|
|
73
73
|
const spt = await createToken({
|
|
@@ -66,8 +66,8 @@ export function charge<const parameters extends charge.Parameters>(parameters: p
|
|
|
66
66
|
const { challenge } = credential
|
|
67
67
|
const { request } = challenge
|
|
68
68
|
|
|
69
|
-
if (
|
|
70
|
-
throw new PaymentExpiredError({ expires:
|
|
69
|
+
if (challenge.expires && new Date(challenge.expires) < new Date())
|
|
70
|
+
throw new PaymentExpiredError({ expires: challenge.expires })
|
|
71
71
|
|
|
72
72
|
const parsed = Methods.charge.schema.credential.payload.safeParse(credential.payload)
|
|
73
73
|
if (!parsed.success) throw new Error('Invalid credential payload: missing or malformed spt')
|
|
@@ -82,3 +82,25 @@ describe('charge', () => {
|
|
|
82
82
|
expect(result.success).toBe(false)
|
|
83
83
|
})
|
|
84
84
|
})
|
|
85
|
+
|
|
86
|
+
describe('session', () => {
|
|
87
|
+
test('has correct name and intent', () => {
|
|
88
|
+
expect(Methods.session.intent).toBe('session')
|
|
89
|
+
expect(Methods.session.name).toBe('tempo')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('schema: encodes minVoucherDelta in base units', () => {
|
|
93
|
+
const request = Methods.session.schema.request.parse({
|
|
94
|
+
amount: '1',
|
|
95
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
96
|
+
decimals: 6,
|
|
97
|
+
escrowContract: '0x1234567890abcdef1234567890abcdef12345678',
|
|
98
|
+
minVoucherDelta: '0.1',
|
|
99
|
+
recipient: '0x1234567890abcdef1234567890abcdef12345678',
|
|
100
|
+
unitType: 'token',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(request.amount).toBe('1000000')
|
|
104
|
+
expect(request.methodDetails?.minVoucherDelta).toBe('100000')
|
|
105
|
+
})
|
|
106
|
+
})
|