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
package/src/client/Mppx.test.ts
CHANGED
|
@@ -130,7 +130,7 @@ describe('createCredential', () => {
|
|
|
130
130
|
})
|
|
131
131
|
|
|
132
132
|
await expect(mppx.createCredential(response)).rejects.toThrow(
|
|
133
|
-
'No method found for
|
|
133
|
+
'No method found for challenges: unknown.charge. Available: tempo.charge, tempo.session',
|
|
134
134
|
)
|
|
135
135
|
})
|
|
136
136
|
|
|
@@ -187,6 +187,135 @@ describe('createCredential', () => {
|
|
|
187
187
|
expect(parsed.challenge.method).toBe('stripe')
|
|
188
188
|
})
|
|
189
189
|
|
|
190
|
+
test('behavior: selects the preferred challenge from a multi-challenge response', async () => {
|
|
191
|
+
const stripeCharge = Method.from({
|
|
192
|
+
name: 'stripe',
|
|
193
|
+
intent: 'charge',
|
|
194
|
+
schema: {
|
|
195
|
+
credential: {
|
|
196
|
+
payload: Methods.charge.schema.credential.payload,
|
|
197
|
+
},
|
|
198
|
+
request: Methods.charge.schema.request,
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const stripe = Method.toClient(stripeCharge, {
|
|
203
|
+
async createCredential({ challenge }) {
|
|
204
|
+
return Credential.serialize({
|
|
205
|
+
challenge,
|
|
206
|
+
payload: { signature: '0xstripe', type: 'transaction' },
|
|
207
|
+
})
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const mppx = Mppx.create({
|
|
212
|
+
polyfill: false,
|
|
213
|
+
methods: [tempo({ account: accounts[1], getClient: () => client }), stripe],
|
|
214
|
+
paymentPreferences: ({ stripe }) => ({
|
|
215
|
+
[stripe.charge]: 0.5,
|
|
216
|
+
}),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const tempoChallenge = Challenge.fromMethod(Methods.charge, {
|
|
220
|
+
realm,
|
|
221
|
+
secretKey,
|
|
222
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
223
|
+
request: {
|
|
224
|
+
amount: '1000',
|
|
225
|
+
currency: '0x1234567890123456789012345678901234567890',
|
|
226
|
+
decimals: 6,
|
|
227
|
+
recipient: '0x1234567890123456789012345678901234567890',
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
const stripeChallenge = Challenge.from({
|
|
231
|
+
id: 'stripe-challenge-id',
|
|
232
|
+
realm,
|
|
233
|
+
method: 'stripe',
|
|
234
|
+
intent: 'charge',
|
|
235
|
+
request: {
|
|
236
|
+
amount: '2000',
|
|
237
|
+
currency: '0xabcd',
|
|
238
|
+
recipient: '0xefgh',
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const response = new Response(null, {
|
|
243
|
+
status: 402,
|
|
244
|
+
headers: {
|
|
245
|
+
'WWW-Authenticate': `${Challenge.serialize(stripeChallenge)}, ${Challenge.serialize(tempoChallenge)}`,
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const credential = await mppx.createCredential(response)
|
|
250
|
+
const parsed = Credential.deserialize(credential)
|
|
251
|
+
|
|
252
|
+
expect(parsed.challenge.method).toBe('tempo')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('behavior: createCredential accepts a request-local Accept-Payment override', async () => {
|
|
256
|
+
const stripeCharge = Method.from({
|
|
257
|
+
name: 'stripe',
|
|
258
|
+
intent: 'charge',
|
|
259
|
+
schema: {
|
|
260
|
+
credential: {
|
|
261
|
+
payload: Methods.charge.schema.credential.payload,
|
|
262
|
+
},
|
|
263
|
+
request: Methods.charge.schema.request,
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const stripe = Method.toClient(stripeCharge, {
|
|
268
|
+
async createCredential({ challenge }) {
|
|
269
|
+
return Credential.serialize({
|
|
270
|
+
challenge,
|
|
271
|
+
payload: { signature: '0xstripe', type: 'transaction' },
|
|
272
|
+
})
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const mppx = Mppx.create({
|
|
277
|
+
polyfill: false,
|
|
278
|
+
methods: [tempo({ account: accounts[1], getClient: () => client }), stripe],
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const tempoChallenge = Challenge.fromMethod(Methods.charge, {
|
|
282
|
+
realm,
|
|
283
|
+
secretKey,
|
|
284
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
285
|
+
request: {
|
|
286
|
+
amount: '1000',
|
|
287
|
+
currency: '0x1234567890123456789012345678901234567890',
|
|
288
|
+
decimals: 6,
|
|
289
|
+
recipient: '0x1234567890123456789012345678901234567890',
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
const stripeChallenge = Challenge.from({
|
|
293
|
+
id: 'stripe-challenge-id',
|
|
294
|
+
realm,
|
|
295
|
+
method: 'stripe',
|
|
296
|
+
intent: 'charge',
|
|
297
|
+
request: {
|
|
298
|
+
amount: '2000',
|
|
299
|
+
currency: '0xabcd',
|
|
300
|
+
recipient: '0xefgh',
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const response = new Response(null, {
|
|
305
|
+
status: 402,
|
|
306
|
+
headers: {
|
|
307
|
+
'WWW-Authenticate': `${Challenge.serialize(stripeChallenge)}, ${Challenge.serialize(tempoChallenge)}`,
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const credential = await mppx.createCredential(response, undefined, {
|
|
312
|
+
acceptPayment: 'stripe/charge, tempo/charge;q=0.1',
|
|
313
|
+
})
|
|
314
|
+
const parsed = Credential.deserialize(credential)
|
|
315
|
+
|
|
316
|
+
expect(parsed.challenge.method).toBe('stripe')
|
|
317
|
+
})
|
|
318
|
+
|
|
190
319
|
test('behavior: passes context to createCredential', async () => {
|
|
191
320
|
const mppx = Mppx.create({
|
|
192
321
|
polyfill: false,
|
package/src/client/Mppx.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type * as Challenge from '../Challenge.js'
|
|
2
|
+
import * as AcceptPayment from '../internal/AcceptPayment.js'
|
|
2
3
|
import type * as Method from '../Method.js'
|
|
3
4
|
import type * as z from '../zod.js'
|
|
4
5
|
import * as Fetch from './internal/Fetch.js'
|
|
@@ -25,6 +26,7 @@ export type Mppx<
|
|
|
25
26
|
createCredential: (
|
|
26
27
|
response: Transport.ResponseOf<transport>,
|
|
27
28
|
context?: AnyContextFor<FlattenMethods<methods>> | undefined,
|
|
29
|
+
options?: createCredential.Options | undefined,
|
|
28
30
|
) => Promise<string>
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -61,11 +63,13 @@ export function create<
|
|
|
61
63
|
const rawFetch = config.fetch ?? globalThis.fetch
|
|
62
64
|
|
|
63
65
|
const methods = config.methods.flat() as unknown as FlattenMethods<methods>
|
|
66
|
+
const acceptPayment = AcceptPayment.resolve(methods, config.paymentPreferences)
|
|
64
67
|
|
|
65
68
|
const resolvedOnChallenge = onChallenge as Fetch.from.Config<
|
|
66
69
|
FlattenMethods<methods>
|
|
67
70
|
>['onChallenge']
|
|
68
71
|
const config_fetch = {
|
|
72
|
+
acceptPayment,
|
|
69
73
|
...(config.fetch && { fetch: config.fetch }),
|
|
70
74
|
...(resolvedOnChallenge && { onChallenge: resolvedOnChallenge }),
|
|
71
75
|
methods,
|
|
@@ -78,15 +82,24 @@ export function create<
|
|
|
78
82
|
rawFetch,
|
|
79
83
|
methods,
|
|
80
84
|
transport,
|
|
81
|
-
async createCredential(
|
|
82
|
-
|
|
85
|
+
async createCredential(
|
|
86
|
+
response: Transport.ResponseOf<transport>,
|
|
87
|
+
context?: unknown,
|
|
88
|
+
options?: createCredential.Options,
|
|
89
|
+
) {
|
|
90
|
+
const challenges = transport.getChallenges
|
|
91
|
+
? transport.getChallenges(response as never)
|
|
92
|
+
: [transport.getChallenge(response as never)]
|
|
93
|
+
const preferences = resolveChallengePreferences(acceptPayment.entries, options?.acceptPayment)
|
|
83
94
|
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
95
|
+
const selected = AcceptPayment.selectChallenge(challenges, methods, preferences)
|
|
96
|
+
if (!selected)
|
|
86
97
|
throw new Error(
|
|
87
|
-
`No method found for
|
|
98
|
+
`No method found for challenges: ${challenges.map((challenge) => `${challenge.method}.${challenge.intent}`).join(', ')}. Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`,
|
|
88
99
|
)
|
|
89
100
|
|
|
101
|
+
const { challenge, method: mi } = selected
|
|
102
|
+
|
|
90
103
|
const parsedContext =
|
|
91
104
|
mi.context && context !== undefined ? mi.context.parse(context) : undefined
|
|
92
105
|
|
|
@@ -99,6 +112,13 @@ export function create<
|
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
export declare namespace createCredential {
|
|
116
|
+
type Options = {
|
|
117
|
+
/** Request-local Accept-Payment override for manual rawFetch + createCredential flows. */
|
|
118
|
+
acceptPayment?: string | readonly AcceptPayment.Entry[] | undefined
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
102
122
|
/**
|
|
103
123
|
* Restores the original `fetch` after `create()` polyfilled it.
|
|
104
124
|
*
|
|
@@ -133,6 +153,8 @@ export declare namespace create {
|
|
|
133
153
|
},
|
|
134
154
|
) => Promise<string | undefined>)
|
|
135
155
|
| undefined
|
|
156
|
+
/** Client-declared supported payment methods, keyed by typed `method/intent` strings. */
|
|
157
|
+
paymentPreferences?: AcceptPayment.Config<FlattenMethods<methods>> | undefined
|
|
136
158
|
/** Array of methods to use. Accepts individual clients or tuples (e.g. from `tempo()`). */
|
|
137
159
|
methods: methods
|
|
138
160
|
/** Whether to polyfill `globalThis.fetch` with the payment-aware wrapper. @default true */
|
|
@@ -168,3 +190,11 @@ type FlattenMethods<methods extends Methods> = methods extends readonly [
|
|
|
168
190
|
? readonly [head, ...FlattenMethods<tail>]
|
|
169
191
|
: never
|
|
170
192
|
: readonly []
|
|
193
|
+
|
|
194
|
+
function resolveChallengePreferences(
|
|
195
|
+
fallback: readonly AcceptPayment.Entry[],
|
|
196
|
+
override?: string | readonly AcceptPayment.Entry[] | undefined,
|
|
197
|
+
): readonly AcceptPayment.Entry[] {
|
|
198
|
+
if (!override) return fallback
|
|
199
|
+
return typeof override === 'string' ? AcceptPayment.parse(override) : override
|
|
200
|
+
}
|
|
@@ -57,20 +57,18 @@ describe('http', () => {
|
|
|
57
57
|
},
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
expect(transport.getChallenge(response)).
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
`)
|
|
60
|
+
expect(transport.getChallenge(response)).toMatchObject({
|
|
61
|
+
expires: '2025-01-01T00:00:00.000Z',
|
|
62
|
+
id: expect.any(String),
|
|
63
|
+
intent: 'charge',
|
|
64
|
+
method: 'tempo',
|
|
65
|
+
realm: 'api.example.com',
|
|
66
|
+
request: {
|
|
67
|
+
amount: '1000',
|
|
68
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
69
|
+
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
70
|
+
},
|
|
71
|
+
})
|
|
74
72
|
})
|
|
75
73
|
|
|
76
74
|
test('throws for non-402 response', () => {
|
|
@@ -81,6 +79,24 @@ describe('http', () => {
|
|
|
81
79
|
})
|
|
82
80
|
})
|
|
83
81
|
|
|
82
|
+
describe('getChallenges', () => {
|
|
83
|
+
test('returns all HTTP challenges', () => {
|
|
84
|
+
const transport = Transport.http()
|
|
85
|
+
const alternate = { ...challenge, id: 'alternate', method: 'stripe' as const }
|
|
86
|
+
const response = new Response(null, {
|
|
87
|
+
status: 402,
|
|
88
|
+
headers: {
|
|
89
|
+
'WWW-Authenticate': `${Challenge.serialize(challenge)}, ${Challenge.serialize(alternate)}`,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
expect(transport.getChallenges?.(response).map((entry) => entry.id)).toEqual([
|
|
94
|
+
challenge.id,
|
|
95
|
+
'alternate',
|
|
96
|
+
])
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
84
100
|
describe('setCredential', () => {
|
|
85
101
|
test('default', () => {
|
|
86
102
|
const transport = Transport.http()
|
|
@@ -89,9 +105,7 @@ describe('http', () => {
|
|
|
89
105
|
const result = transport.setCredential({}, serialized)
|
|
90
106
|
const headers = result.headers as Headers
|
|
91
107
|
|
|
92
|
-
expect(headers.get('Authorization')).
|
|
93
|
-
`"Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjUtMDEtMDFUMDA6MDA6MDAuMDAwWiIsImlkIjoiMGhucnlTUkRxV2Z0dGxESUpwdXhWNG1Kc1JKSVM3ZDdSam51ZnVvbkpPRSIsImludGVudCI6ImNoYXJnZSIsIm1ldGhvZCI6InRlbXBvIiwicmVhbG0iOiJhcGkuZXhhbXBsZS5jb20iLCJyZXF1ZXN0IjoiZXlKaGJXOTFiblFpT2lJeE1EQXdJaXdpWTNWeWNtVnVZM2tpT2lJd2VESXdZekF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREVpTENKeVpXTnBjR2xsYm5RaU9pSXdlRGMwTW1Rek5VTmpOall6TkVNd05UTXlPVEkxWVROaU9EUTBRbU01WlRjMU9UVm1PR1pGTURBaWZRIn0sInBheWxvYWQiOnsic2lnbmF0dXJlIjoiMHhhYmMxMjMiLCJ0eXBlIjoidHJhbnNhY3Rpb24ifX0"`,
|
|
94
|
-
)
|
|
108
|
+
expect(headers.get('Authorization')).toBe(serialized)
|
|
95
109
|
})
|
|
96
110
|
|
|
97
111
|
test('preserves existing headers', () => {
|
|
@@ -178,20 +192,18 @@ describe('mcp', () => {
|
|
|
178
192
|
},
|
|
179
193
|
}
|
|
180
194
|
|
|
181
|
-
expect(transport.getChallenge(response)).
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
`)
|
|
195
|
+
expect(transport.getChallenge(response)).toMatchObject({
|
|
196
|
+
expires: '2025-01-01T00:00:00.000Z',
|
|
197
|
+
id: expect.any(String),
|
|
198
|
+
intent: 'charge',
|
|
199
|
+
method: 'tempo',
|
|
200
|
+
realm: 'api.example.com',
|
|
201
|
+
request: {
|
|
202
|
+
amount: '1000',
|
|
203
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
204
|
+
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
205
|
+
},
|
|
206
|
+
})
|
|
195
207
|
})
|
|
196
208
|
|
|
197
209
|
test('throws for success response', () => {
|
|
@@ -224,39 +236,60 @@ describe('mcp', () => {
|
|
|
224
236
|
})
|
|
225
237
|
})
|
|
226
238
|
|
|
239
|
+
describe('getChallenges', () => {
|
|
240
|
+
test('returns all MCP challenges', () => {
|
|
241
|
+
const transport = Transport.mcp()
|
|
242
|
+
const response: Mcp.Response = {
|
|
243
|
+
jsonrpc: '2.0',
|
|
244
|
+
id: 1,
|
|
245
|
+
error: {
|
|
246
|
+
code: Mcp.paymentRequiredCode,
|
|
247
|
+
message: 'Payment Required',
|
|
248
|
+
data: {
|
|
249
|
+
httpStatus: 402,
|
|
250
|
+
challenges: [challenge, { ...challenge, id: 'alternate', method: 'stripe' }],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
expect(transport.getChallenges?.(response).map((entry) => entry.id)).toEqual([
|
|
256
|
+
challenge.id,
|
|
257
|
+
'alternate',
|
|
258
|
+
])
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
227
262
|
describe('setCredential', () => {
|
|
228
263
|
test('default', () => {
|
|
229
264
|
const transport = Transport.mcp()
|
|
230
265
|
const serialized = Credential.serialize(credential)
|
|
231
266
|
|
|
232
|
-
expect(transport.setCredential(mcpRequest, serialized)).
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
"payload": {
|
|
251
|
-
"signature": "0xabc123",
|
|
252
|
-
"type": "transaction",
|
|
267
|
+
expect(transport.setCredential(mcpRequest, serialized)).toMatchObject({
|
|
268
|
+
method: 'tools/call',
|
|
269
|
+
params: {
|
|
270
|
+
_meta: {
|
|
271
|
+
'org.paymentauth/credential': {
|
|
272
|
+
challenge: {
|
|
273
|
+
expires: '2025-01-01T00:00:00.000Z',
|
|
274
|
+
id: expect.any(String),
|
|
275
|
+
intent: 'charge',
|
|
276
|
+
method: 'tempo',
|
|
277
|
+
realm: 'api.example.com',
|
|
278
|
+
request: {
|
|
279
|
+
amount: '1000',
|
|
280
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
281
|
+
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
253
282
|
},
|
|
254
283
|
},
|
|
284
|
+
payload: {
|
|
285
|
+
signature: '0xabc123',
|
|
286
|
+
type: 'transaction',
|
|
287
|
+
},
|
|
255
288
|
},
|
|
256
|
-
"name": "test-tool",
|
|
257
289
|
},
|
|
258
|
-
|
|
259
|
-
|
|
290
|
+
name: 'test-tool',
|
|
291
|
+
},
|
|
292
|
+
})
|
|
260
293
|
})
|
|
261
294
|
|
|
262
295
|
test('preserves existing _meta', () => {
|
package/src/client/Transport.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type Transport<in out request = unknown, in out response = unknown> = {
|
|
|
13
13
|
name: string
|
|
14
14
|
/** Checks if a response indicates payment is required. */
|
|
15
15
|
isPaymentRequired: (response: response) => boolean
|
|
16
|
+
/** Extracts all challenges from a payment-required response, when the transport supports multiple offers. */
|
|
17
|
+
getChallenges?: (response: response) => Challenge.Challenge[]
|
|
16
18
|
/** Extracts the challenge from a payment-required response. */
|
|
17
19
|
getChallenge: (response: response) => Challenge.Challenge
|
|
18
20
|
/** Attaches a credential to a request. */
|
|
@@ -64,6 +66,10 @@ export function http() {
|
|
|
64
66
|
return response.status === 402
|
|
65
67
|
},
|
|
66
68
|
|
|
69
|
+
getChallenges(response) {
|
|
70
|
+
return Challenge.fromResponseList(response)
|
|
71
|
+
},
|
|
72
|
+
|
|
67
73
|
getChallenge(response) {
|
|
68
74
|
return Challenge.fromResponse(response)
|
|
69
75
|
},
|
|
@@ -91,6 +97,13 @@ export function mcp() {
|
|
|
91
97
|
return 'error' in response && response.error?.code === Mcp.paymentRequiredCode
|
|
92
98
|
},
|
|
93
99
|
|
|
100
|
+
getChallenges(response) {
|
|
101
|
+
if (!('error' in response) || !response.error) throw new Error('Response is not an error.')
|
|
102
|
+
const challenges = response.error.data?.challenges
|
|
103
|
+
if (!challenges?.length) throw new Error('No challenge in error response.')
|
|
104
|
+
return challenges
|
|
105
|
+
},
|
|
106
|
+
|
|
94
107
|
getChallenge(response) {
|
|
95
108
|
if (!('error' in response) || !response.error) throw new Error('Response is not an error.')
|
|
96
109
|
const challenge = response.error.data?.challenges[0]
|
|
@@ -22,6 +22,10 @@ function make402() {
|
|
|
22
22
|
})
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function toHeaders(headers: unknown): Headers {
|
|
26
|
+
return new Headers((headers ?? {}) as HeadersInit)
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
/** Returns a fetch wrapper and the init captured from the 402 retry call. */
|
|
26
30
|
function setup() {
|
|
27
31
|
const calls: (RequestInit | undefined)[] = []
|
|
@@ -38,7 +42,7 @@ function setup() {
|
|
|
38
42
|
/** Headers sent on the retry (second) request. */
|
|
39
43
|
retryHeaders: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
40
44
|
await fetch(input, init)
|
|
41
|
-
return (calls[1] as Record<string, unknown>)?.headers
|
|
45
|
+
return toHeaders((calls[1] as Record<string, unknown>)?.headers)
|
|
42
46
|
},
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -49,9 +53,9 @@ describe('Fetch.from: browser header normalization', () => {
|
|
|
49
53
|
const h = await retryHeaders('https://example.com', {
|
|
50
54
|
headers: new Headers({ 'X-Custom': 'value', 'Content-Type': 'application/json' }),
|
|
51
55
|
})
|
|
52
|
-
expect(h
|
|
53
|
-
expect(h
|
|
54
|
-
expect(h.
|
|
56
|
+
expect(h.get('x-custom')).toBe('value')
|
|
57
|
+
expect(h.get('content-type')).toBe('application/json')
|
|
58
|
+
expect(h.get('authorization')).toBe('credential')
|
|
55
59
|
})
|
|
56
60
|
|
|
57
61
|
test('preserves header tuples', async () => {
|
|
@@ -62,9 +66,9 @@ describe('Fetch.from: browser header normalization', () => {
|
|
|
62
66
|
['Accept', 'application/json'],
|
|
63
67
|
],
|
|
64
68
|
})
|
|
65
|
-
expect(h
|
|
66
|
-
expect(h.
|
|
67
|
-
expect(h.
|
|
69
|
+
expect(h.get('x-custom')).toBe('value')
|
|
70
|
+
expect(h.get('accept')).toBe('application/json')
|
|
71
|
+
expect(h.get('authorization')).toBe('credential')
|
|
68
72
|
})
|
|
69
73
|
|
|
70
74
|
test('replaces authorization case-insensitively', async () => {
|
|
@@ -72,22 +76,21 @@ describe('Fetch.from: browser header normalization', () => {
|
|
|
72
76
|
const h = await retryHeaders('https://example.com', {
|
|
73
77
|
headers: { authorization: 'Bearer stale', 'X-Custom': 'value' },
|
|
74
78
|
})
|
|
75
|
-
expect(h.authorization).
|
|
76
|
-
expect(h.
|
|
77
|
-
expect(h['X-Custom']).toBe('value')
|
|
79
|
+
expect(h.get('authorization')).toBe('credential')
|
|
80
|
+
expect(h.get('x-custom')).toBe('value')
|
|
78
81
|
})
|
|
79
82
|
|
|
80
83
|
test('preserves plain object headers', async () => {
|
|
81
84
|
const { retryHeaders } = setup()
|
|
82
85
|
const h = await retryHeaders('https://example.com', { headers: { 'X-Custom': 'val' } })
|
|
83
|
-
expect(h
|
|
84
|
-
expect(h.
|
|
86
|
+
expect(h.get('x-custom')).toBe('val')
|
|
87
|
+
expect(h.get('authorization')).toBe('credential')
|
|
85
88
|
})
|
|
86
89
|
|
|
87
90
|
test('adds Authorization when no headers provided', async () => {
|
|
88
91
|
const { retryHeaders } = setup()
|
|
89
92
|
const h = await retryHeaders('https://example.com')
|
|
90
|
-
expect(h.
|
|
93
|
+
expect(h.get('authorization')).toBe('credential')
|
|
91
94
|
})
|
|
92
95
|
})
|
|
93
96
|
|