mppx 0.6.19 → 0.6.21

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.
Files changed (168) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/Challenge.d.ts +2 -2
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +1 -1
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Method.d.ts +34 -0
  7. package/dist/Method.d.ts.map +1 -1
  8. package/dist/Method.js +3 -1
  9. package/dist/Method.js.map +1 -1
  10. package/dist/Receipt.d.ts +1 -0
  11. package/dist/Receipt.d.ts.map +1 -1
  12. package/dist/Receipt.js +2 -0
  13. package/dist/Receipt.js.map +1 -1
  14. package/dist/client/Methods.d.ts +1 -0
  15. package/dist/client/Methods.d.ts.map +1 -1
  16. package/dist/client/Methods.js +1 -0
  17. package/dist/client/Methods.js.map +1 -1
  18. package/dist/client/Mppx.d.ts +12 -1
  19. package/dist/client/Mppx.d.ts.map +1 -1
  20. package/dist/client/Mppx.js +127 -10
  21. package/dist/client/Mppx.js.map +1 -1
  22. package/dist/client/internal/Fetch.d.ts +69 -1
  23. package/dist/client/internal/Fetch.d.ts.map +1 -1
  24. package/dist/client/internal/Fetch.js +250 -20
  25. package/dist/client/internal/Fetch.js.map +1 -1
  26. package/dist/middlewares/elysia.d.ts.map +1 -1
  27. package/dist/middlewares/elysia.js +14 -0
  28. package/dist/middlewares/elysia.js.map +1 -1
  29. package/dist/middlewares/express.d.ts.map +1 -1
  30. package/dist/middlewares/express.js +1 -2
  31. package/dist/middlewares/express.js.map +1 -1
  32. package/dist/middlewares/hono.d.ts.map +1 -1
  33. package/dist/middlewares/hono.js +14 -0
  34. package/dist/middlewares/hono.js.map +1 -1
  35. package/dist/middlewares/internal/mppx.d.ts +1 -1
  36. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  37. package/dist/middlewares/internal/mppx.js +2 -1
  38. package/dist/middlewares/internal/mppx.js.map +1 -1
  39. package/dist/middlewares/nextjs.d.ts.map +1 -1
  40. package/dist/middlewares/nextjs.js +14 -0
  41. package/dist/middlewares/nextjs.js.map +1 -1
  42. package/dist/proxy/Proxy.d.ts.map +1 -1
  43. package/dist/proxy/Proxy.js +2 -2
  44. package/dist/proxy/Proxy.js.map +1 -1
  45. package/dist/proxy/Service.d.ts.map +1 -1
  46. package/dist/proxy/Service.js +1 -1
  47. package/dist/proxy/Service.js.map +1 -1
  48. package/dist/server/Mppx.d.ts +96 -5
  49. package/dist/server/Mppx.d.ts.map +1 -1
  50. package/dist/server/Mppx.js +739 -115
  51. package/dist/server/Mppx.js.map +1 -1
  52. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  53. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  54. package/dist/stripe/server/internal/html.gen.js +1 -1
  55. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  56. package/dist/tempo/Methods.d.ts +96 -0
  57. package/dist/tempo/Methods.d.ts.map +1 -1
  58. package/dist/tempo/Methods.js +97 -0
  59. package/dist/tempo/Methods.js.map +1 -1
  60. package/dist/tempo/client/Methods.d.ts +3 -0
  61. package/dist/tempo/client/Methods.d.ts.map +1 -1
  62. package/dist/tempo/client/Methods.js +3 -0
  63. package/dist/tempo/client/Methods.js.map +1 -1
  64. package/dist/tempo/client/Subscription.d.ts +114 -0
  65. package/dist/tempo/client/Subscription.d.ts.map +1 -0
  66. package/dist/tempo/client/Subscription.js +100 -0
  67. package/dist/tempo/client/Subscription.js.map +1 -0
  68. package/dist/tempo/client/index.d.ts +1 -0
  69. package/dist/tempo/client/index.d.ts.map +1 -1
  70. package/dist/tempo/client/index.js +1 -0
  71. package/dist/tempo/client/index.js.map +1 -1
  72. package/dist/tempo/index.d.ts +1 -0
  73. package/dist/tempo/index.d.ts.map +1 -1
  74. package/dist/tempo/index.js +1 -0
  75. package/dist/tempo/index.js.map +1 -1
  76. package/dist/tempo/server/Methods.d.ts +5 -0
  77. package/dist/tempo/server/Methods.d.ts.map +1 -1
  78. package/dist/tempo/server/Methods.js +5 -0
  79. package/dist/tempo/server/Methods.js.map +1 -1
  80. package/dist/tempo/server/Subscription.d.ts +221 -0
  81. package/dist/tempo/server/Subscription.d.ts.map +1 -0
  82. package/dist/tempo/server/Subscription.js +637 -0
  83. package/dist/tempo/server/Subscription.js.map +1 -0
  84. package/dist/tempo/server/index.d.ts +1 -0
  85. package/dist/tempo/server/index.d.ts.map +1 -1
  86. package/dist/tempo/server/index.js +1 -0
  87. package/dist/tempo/server/index.js.map +1 -1
  88. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  89. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  90. package/dist/tempo/server/internal/html.gen.js +1 -1
  91. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  92. package/dist/tempo/subscription/KeyAuthorization.d.ts +282 -0
  93. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -0
  94. package/dist/tempo/subscription/KeyAuthorization.js +297 -0
  95. package/dist/tempo/subscription/KeyAuthorization.js.map +1 -0
  96. package/dist/tempo/subscription/Receipt.d.ts +10 -0
  97. package/dist/tempo/subscription/Receipt.d.ts.map +1 -0
  98. package/dist/tempo/subscription/Receipt.js +16 -0
  99. package/dist/tempo/subscription/Receipt.js.map +1 -0
  100. package/dist/tempo/subscription/Store.d.ts +99 -0
  101. package/dist/tempo/subscription/Store.d.ts.map +1 -0
  102. package/dist/tempo/subscription/Store.js +292 -0
  103. package/dist/tempo/subscription/Store.js.map +1 -0
  104. package/dist/tempo/subscription/Types.d.ts +65 -0
  105. package/dist/tempo/subscription/Types.d.ts.map +1 -0
  106. package/dist/tempo/subscription/Types.js +2 -0
  107. package/dist/tempo/subscription/Types.js.map +1 -0
  108. package/dist/tempo/subscription/index.d.ts +6 -0
  109. package/dist/tempo/subscription/index.d.ts.map +1 -0
  110. package/dist/tempo/subscription/index.js +4 -0
  111. package/dist/tempo/subscription/index.js.map +1 -0
  112. package/dist/zod.d.ts +7 -0
  113. package/dist/zod.d.ts.map +1 -1
  114. package/dist/zod.js +18 -0
  115. package/dist/zod.js.map +1 -1
  116. package/package.json +3 -3
  117. package/src/Challenge.test.ts +13 -0
  118. package/src/Challenge.ts +3 -3
  119. package/src/Method.ts +46 -1
  120. package/src/Receipt.ts +2 -0
  121. package/src/client/Methods.ts +1 -0
  122. package/src/client/Mppx.test-d.ts +55 -0
  123. package/src/client/Mppx.test.ts +181 -0
  124. package/src/client/Mppx.ts +248 -16
  125. package/src/client/internal/Fetch.test-d.ts +31 -0
  126. package/src/client/internal/Fetch.test.ts +261 -0
  127. package/src/client/internal/Fetch.ts +467 -24
  128. package/src/middlewares/elysia.test.ts +31 -1
  129. package/src/middlewares/elysia.ts +13 -0
  130. package/src/middlewares/express.ts +1 -5
  131. package/src/middlewares/hono.test.ts +30 -1
  132. package/src/middlewares/hono.ts +13 -0
  133. package/src/middlewares/internal/mppx.ts +5 -6
  134. package/src/middlewares/nextjs.test.ts +28 -1
  135. package/src/middlewares/nextjs.ts +13 -0
  136. package/src/proxy/Proxy.test.ts +69 -0
  137. package/src/proxy/Proxy.ts +2 -5
  138. package/src/proxy/Service.test.ts +34 -0
  139. package/src/proxy/Service.ts +7 -0
  140. package/src/server/Mppx.authorize.test.ts +210 -0
  141. package/src/server/Mppx.test-d.ts +73 -1
  142. package/src/server/Mppx.test.ts +965 -3
  143. package/src/server/Mppx.ts +1138 -140
  144. package/src/stripe/server/internal/html/package.json +1 -1
  145. package/src/stripe/server/internal/html.gen.ts +1 -1
  146. package/src/tempo/Methods.test.ts +131 -0
  147. package/src/tempo/Methods.ts +136 -0
  148. package/src/tempo/Subscription.integration.test.ts +591 -0
  149. package/src/tempo/client/Methods.ts +3 -0
  150. package/src/tempo/client/Subscription.test.ts +131 -0
  151. package/src/tempo/client/Subscription.ts +155 -0
  152. package/src/tempo/client/index.ts +1 -0
  153. package/src/tempo/index.ts +1 -0
  154. package/src/tempo/server/Methods.ts +5 -0
  155. package/src/tempo/server/Subscription.test.ts +1410 -0
  156. package/src/tempo/server/Subscription.ts +1014 -0
  157. package/src/tempo/server/index.ts +1 -0
  158. package/src/tempo/server/internal/html/package.json +1 -1
  159. package/src/tempo/server/internal/html.gen.ts +1 -1
  160. package/src/tempo/subscription/KeyAuthorization.test.ts +204 -0
  161. package/src/tempo/subscription/KeyAuthorization.ts +394 -0
  162. package/src/tempo/subscription/Receipt.ts +28 -0
  163. package/src/tempo/subscription/Store.test.ts +554 -0
  164. package/src/tempo/subscription/Store.ts +431 -0
  165. package/src/tempo/subscription/Types.ts +68 -0
  166. package/src/tempo/subscription/index.ts +23 -0
  167. package/src/zod.test.ts +23 -1
  168. package/src/zod.ts +24 -0
package/src/Method.ts CHANGED
@@ -130,10 +130,12 @@ export type Server<
130
130
  defaults extends ExactPartial<z.input<method['schema']['request']>> = {},
131
131
  transportOverride = undefined,
132
132
  > = method & {
133
+ authorize?: AuthorizeFn<method> | undefined
133
134
  defaults?: defaults | undefined
134
135
  html?: Html.Options | undefined
135
136
  request?: RequestFn<method> | undefined
136
137
  respond?: RespondFn<method> | undefined
138
+ stableBinding?: StableBindingFn<method> | undefined
137
139
  transport?: transportOverride | undefined
138
140
  verify: VerifyFn<method>
139
141
  }
@@ -155,6 +157,45 @@ export type RequestFn<method extends Method> = (
155
157
  options: RequestContext<method>,
156
158
  ) => MaybePromise<z.input<method['schema']['request']>>
157
159
 
160
+ /**
161
+ * Optional authorization hook for a server-side method.
162
+ *
163
+ * Called after request normalization but before the 402 challenge path. This lets
164
+ * a server grant access based on existing application state (for example, an
165
+ * active subscription) without requiring a fresh `Payment` credential.
166
+ *
167
+ * **HTTP-only.** The `input` parameter is a Fetch `Request`; non-HTTP transports
168
+ * do not invoke this hook.
169
+ *
170
+ * Transports that require credential context for `withReceipt()` should return a
171
+ * `response` from this hook so adapters can short-circuit protected handlers.
172
+ */
173
+ export type AuthorizeFn<method extends Method> = (parameters: {
174
+ challenge: Challenge.Challenge<
175
+ z.output<method['schema']['request']>,
176
+ method['intent'],
177
+ method['name']
178
+ >
179
+ input: globalThis.Request
180
+ request: z.output<method['schema']['request']>
181
+ }) => MaybePromise<AuthorizeResult | undefined>
182
+
183
+ /** Successful result returned from an {@link AuthorizeFn}. */
184
+ export type AuthorizeResult = {
185
+ receipt: Receipt.Receipt
186
+ response?: globalThis.Response | undefined
187
+ }
188
+
189
+ /**
190
+ * Produces the stable request fields used to bind credentials to a route.
191
+ *
192
+ * Methods can override this to opt into additional request fields beyond the
193
+ * default amount/currency/recipient binding used by generic methods.
194
+ */
195
+ export type StableBindingFn<method extends Method> = (
196
+ request: z.output<method['schema']['request']>,
197
+ ) => Record<string, unknown>
198
+
158
199
  /** Verification function for a single method. */
159
200
  export type VerifyFn<method extends Method> = (
160
201
  parameters: VerifyContext<method>,
@@ -251,13 +292,15 @@ export function toServer<
251
292
  method: method,
252
293
  options: toServer.Options<method, defaults, transportOverride>,
253
294
  ): Server<method, defaults, transportOverride> {
254
- const { defaults, html, request, respond, transport, verify } = options
295
+ const { authorize, defaults, html, request, respond, stableBinding, transport, verify } = options
255
296
  return {
256
297
  ...method,
298
+ authorize,
257
299
  defaults,
258
300
  html,
259
301
  request,
260
302
  respond,
303
+ stableBinding,
261
304
  transport,
262
305
  verify,
263
306
  } as Server<method, defaults, transportOverride>
@@ -269,10 +312,12 @@ export declare namespace toServer {
269
312
  defaults extends RequestDefaults<method> = {},
270
313
  transportOverride extends Transport.AnyTransport | undefined = undefined,
271
314
  > = {
315
+ authorize?: AuthorizeFn<method> | undefined
272
316
  defaults?: defaults | undefined
273
317
  html?: Html.Options | undefined
274
318
  request?: RequestFn<method> | undefined
275
319
  respond?: RespondFn<method> | undefined
320
+ stableBinding?: StableBindingFn<method> | undefined
276
321
  transport?: transportOverride | undefined
277
322
  verify: VerifyFn<method>
278
323
  }
package/src/Receipt.ts CHANGED
@@ -19,6 +19,8 @@ export const Schema = z.object({
19
19
  reference: z.string(),
20
20
  /** Optional external reference ID echoed from the credential payload. */
21
21
  externalId: z.optional(z.string()),
22
+ /** Optional server-issued subscription identifier for recurring payments. */
23
+ subscriptionId: z.optional(z.string()),
22
24
  /** Payment status. Always "success" — failures use 402 + Problem Details. */
23
25
  status: z.literal('success'),
24
26
  /** RFC 3339 settlement timestamp. */
@@ -1,3 +1,4 @@
1
1
  export { stripe } from '../stripe/client/index.js'
2
+ export { subscription } from '../tempo/client/Subscription.js'
2
3
  export { tempo } from '../tempo/client/index.js'
3
4
  export { session } from '../tempo/client/Session.js'
@@ -1,13 +1,17 @@
1
1
  import type { Account } from 'viem'
2
2
  import { describe, expectTypeOf, test } from 'vp/test'
3
3
 
4
+ import * as Challenge from '../Challenge.js'
5
+ import type * as Mcp from '../Mcp.js'
4
6
  import * as Method from '../Method.js'
5
7
  import { charge } from '../tempo/client/Charge.js'
6
8
  import { tempo } from '../tempo/client/Methods.js'
7
9
  import type * as AutoSwap from '../tempo/internal/auto-swap.js'
8
10
  import * as Methods from '../tempo/Methods.js'
9
11
  import * as z from '../zod.js'
12
+ import * as Fetch from './internal/Fetch.js'
10
13
  import * as Mppx from './Mppx.js'
14
+ import * as Transport from './Transport.js'
11
15
 
12
16
  describe('Mppx', () => {
13
17
  test('has methods array', () => {
@@ -63,6 +67,57 @@ describe('create.Config', () => {
63
67
 
64
68
  expectTypeOf(mppx.fetch).toBeFunction()
65
69
  })
70
+
71
+ test('client events expose typed payloads', () => {
72
+ const method = charge()
73
+ const mppx = Mppx.create({
74
+ methods: [method],
75
+ })
76
+
77
+ const unsubscribe = mppx.on('payment.response', (payload) => {
78
+ expectTypeOf(payload.response).toEqualTypeOf<Response>()
79
+ })
80
+ expectTypeOf(unsubscribe).toEqualTypeOf<Fetch.Unsubscribe>()
81
+
82
+ mppx.on('*', (event) => {
83
+ if (event.name === 'credential.created')
84
+ expectTypeOf(event.payload.credential).toEqualTypeOf<string>()
85
+ if (event.name === 'payment.response')
86
+ expectTypeOf(event.payload.response).toEqualTypeOf<Response>()
87
+ })
88
+ mppx.onChallengeReceived((payload) => {
89
+ expectTypeOf(payload.challenge.id).toEqualTypeOf<string>()
90
+ expectTypeOf(payload.challenges).toEqualTypeOf<readonly Challenge.Challenge[]>()
91
+ expectTypeOf(payload.method.intent).toEqualTypeOf<'charge'>()
92
+ expectTypeOf(payload.createCredential({ account: {} as Account })).toEqualTypeOf<
93
+ Promise<string>
94
+ >()
95
+ return payload.createCredential({ account: {} as Account })
96
+ })
97
+ mppx.onCredentialCreated((payload) => {
98
+ expectTypeOf(payload.credential).toEqualTypeOf<string>()
99
+ expectTypeOf(payload.method.intent).toEqualTypeOf<'charge'>()
100
+ })
101
+ mppx.onPaymentFailed((payload) => {
102
+ expectTypeOf(payload.error).toEqualTypeOf<unknown>()
103
+ expectTypeOf(payload.challenge).toEqualTypeOf<Challenge.Challenge | undefined>()
104
+ })
105
+ mppx.onPaymentResponse((payload) => {
106
+ expectTypeOf(payload.response).toEqualTypeOf<Response>()
107
+ expectTypeOf(payload.credential).toEqualTypeOf<string>()
108
+ })
109
+ })
110
+
111
+ test('client events use transport response types', () => {
112
+ const mppx = Mppx.create({
113
+ methods: [tempo({ account: {} as Account })],
114
+ transport: Transport.mcp(),
115
+ })
116
+
117
+ mppx.onChallengeReceived((payload) => {
118
+ expectTypeOf(payload.response).toMatchTypeOf<Response | Mcp.Response>()
119
+ })
120
+ })
66
121
  })
67
122
 
68
123
  describe('Method.toClient', () => {
@@ -108,11 +108,143 @@ describe('createCredential', () => {
108
108
  expect(parsed.challenge.method).toBe('tempo')
109
109
  })
110
110
 
111
+ test('behavior: createCredential emits client events and supports runtime handlers', async () => {
112
+ const events: string[] = []
113
+ const createCredential = vi.fn(async ({ challenge }) =>
114
+ Credential.serialize({
115
+ challenge,
116
+ payload: { signature: '0xsignature', type: 'transaction' },
117
+ }),
118
+ )
119
+ const method = Method.toClient(Methods.charge, { createCredential })
120
+ const mppx = Mppx.create({
121
+ polyfill: false,
122
+ methods: [method],
123
+ })
124
+ mppx.onCredentialCreated((payload) => {
125
+ events.push(`credential:${payload.credential.startsWith('Payment ')}`)
126
+ })
127
+ const offCredential = mppx.onCredentialCreated(() => {
128
+ events.push('removed')
129
+ })
130
+ const offChallenge = mppx.onChallengeReceived((payload) => {
131
+ events.push(`runtime:${payload.method.intent}`)
132
+ return payload.createCredential()
133
+ })
134
+ mppx.on('*', (event) => {
135
+ events.push(`*:${event.name}`)
136
+ })
137
+ offCredential()
138
+
139
+ const challenge = Challenge.fromMethod(Methods.charge, {
140
+ realm,
141
+ secretKey,
142
+ expires: new Date(Date.now() + 60_000).toISOString(),
143
+ request: {
144
+ amount: '1000',
145
+ currency: '0x1234567890123456789012345678901234567890',
146
+ decimals: 6,
147
+ recipient: '0x1234567890123456789012345678901234567890',
148
+ },
149
+ })
150
+ const response = new Response(null, {
151
+ status: 402,
152
+ headers: {
153
+ 'WWW-Authenticate': Challenge.serialize(challenge),
154
+ },
155
+ })
156
+
157
+ const credential = await mppx.createCredential(response)
158
+ offChallenge()
159
+
160
+ expect(credential).toMatch(/^Payment /)
161
+ expect(createCredential).toHaveBeenCalledTimes(1)
162
+ expect(events).toEqual([
163
+ 'runtime:charge',
164
+ '*:challenge.received',
165
+ 'credential:true',
166
+ '*:credential.created',
167
+ ])
168
+ })
169
+
170
+ test('behavior: createCredential memoizes event helper calls', async () => {
171
+ const createCredential = vi.fn(async ({ challenge }) =>
172
+ Credential.serialize({
173
+ challenge,
174
+ payload: { signature: '0xsignature', type: 'transaction' },
175
+ }),
176
+ )
177
+ const method = Method.toClient(Methods.charge, { createCredential })
178
+ const mppx = Mppx.create({
179
+ polyfill: false,
180
+ methods: [method],
181
+ })
182
+ mppx.on('*', async (event) => {
183
+ if (event.name === 'challenge.received') await event.payload.createCredential()
184
+ })
185
+
186
+ const challenge = Challenge.fromMethod(Methods.charge, {
187
+ realm,
188
+ secretKey,
189
+ expires: new Date(Date.now() + 60_000).toISOString(),
190
+ request: {
191
+ amount: '1000',
192
+ currency: '0x1234567890123456789012345678901234567890',
193
+ decimals: 6,
194
+ recipient: '0x1234567890123456789012345678901234567890',
195
+ },
196
+ })
197
+ const response = new Response(null, {
198
+ status: 402,
199
+ headers: {
200
+ 'WWW-Authenticate': Challenge.serialize(challenge),
201
+ },
202
+ })
203
+
204
+ await mppx.createCredential(response)
205
+
206
+ expect(createCredential).toHaveBeenCalledTimes(1)
207
+ })
208
+
209
+ test('behavior: createCredential validates event credentials', async () => {
210
+ const mppx = Mppx.create({
211
+ polyfill: false,
212
+ methods: [tempo({ account: accounts[1], getClient: () => client })],
213
+ })
214
+ mppx.onChallengeReceived(() => 'Payment invalid\r\nX-Injected: true')
215
+
216
+ const challenge = Challenge.fromMethod(Methods.charge, {
217
+ realm,
218
+ secretKey,
219
+ expires: new Date(Date.now() + 60_000).toISOString(),
220
+ request: {
221
+ amount: '1000',
222
+ currency: '0x1234567890123456789012345678901234567890',
223
+ decimals: 6,
224
+ recipient: '0x1234567890123456789012345678901234567890',
225
+ },
226
+ })
227
+ const response = new Response(null, {
228
+ status: 402,
229
+ headers: {
230
+ 'WWW-Authenticate': Challenge.serialize(challenge),
231
+ },
232
+ })
233
+
234
+ await expect(mppx.createCredential(response)).rejects.toThrow('illegal newline')
235
+ })
236
+
111
237
  test('behavior: throws when method not found', async () => {
238
+ const events: string[] = []
112
239
  const mppx = Mppx.create({
113
240
  polyfill: false,
114
241
  methods: [tempo({ account: accounts[1], getClient: () => client })],
115
242
  })
243
+ mppx.onPaymentFailed((payload) => {
244
+ events.push(
245
+ `failed:${payload.challenge === undefined}:${payload.challenges?.length}:${payload.error instanceof Error}`,
246
+ )
247
+ })
116
248
 
117
249
  const challenge = Challenge.from({
118
250
  id: 'test-id',
@@ -132,6 +264,7 @@ describe('createCredential', () => {
132
264
  await expect(mppx.createCredential(response)).rejects.toThrow(
133
265
  'No method found for challenges: unknown.charge. Available: tempo.charge, tempo.session',
134
266
  )
267
+ expect(events).toEqual(['failed:true:1:true'])
135
268
  })
136
269
 
137
270
  test('behavior: rejects expired challenges before creating credential', async () => {
@@ -451,6 +584,54 @@ describe('createCredential', () => {
451
584
  expect((parsed.payload as { type: string }).type).toBe('transaction')
452
585
  expect(parsed.challenge.method).toBe('tempo')
453
586
  })
587
+
588
+ test('behavior: mcp transport event responses are not cast to DOM Response', async () => {
589
+ const method = Method.toClient(Methods.charge, {
590
+ async createCredential({ challenge }) {
591
+ return Credential.serialize({
592
+ challenge,
593
+ payload: { signature: '0xsignature', type: 'transaction' },
594
+ })
595
+ },
596
+ })
597
+ const mppx = Mppx.create({
598
+ polyfill: false,
599
+ methods: [method],
600
+ transport: Transport.mcp(),
601
+ })
602
+
603
+ const challenge = Challenge.fromMethod(Methods.charge, {
604
+ realm,
605
+ secretKey,
606
+ expires: new Date(Date.now() + 60_000).toISOString(),
607
+ request: {
608
+ amount: '1000',
609
+ currency: '0x1234567890123456789012345678901234567890',
610
+ decimals: 6,
611
+ recipient: '0x1234567890123456789012345678901234567890',
612
+ },
613
+ })
614
+ const mcpResponse: Mcp.Response = {
615
+ jsonrpc: '2.0',
616
+ id: 1,
617
+ error: {
618
+ code: Mcp.paymentRequiredCode,
619
+ message: 'Payment Required',
620
+ data: {
621
+ httpStatus: 402,
622
+ challenges: [challenge],
623
+ },
624
+ },
625
+ }
626
+ const seen: unknown[] = []
627
+ mppx.onChallengeReceived((event) => {
628
+ seen.push(event.response)
629
+ })
630
+
631
+ await mppx.createCredential(mcpResponse)
632
+
633
+ expect(seen).toEqual([mcpResponse])
634
+ })
454
635
  })
455
636
 
456
637
  const server = Mppx_server.create({