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.
- package/CHANGELOG.md +14 -0
- package/dist/Challenge.d.ts +2 -2
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +1 -1
- package/dist/Challenge.js.map +1 -1
- package/dist/Method.d.ts +34 -0
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +3 -1
- package/dist/Method.js.map +1 -1
- package/dist/Receipt.d.ts +1 -0
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js +2 -0
- package/dist/Receipt.js.map +1 -1
- package/dist/client/Methods.d.ts +1 -0
- package/dist/client/Methods.d.ts.map +1 -1
- package/dist/client/Methods.js +1 -0
- package/dist/client/Methods.js.map +1 -1
- package/dist/client/Mppx.d.ts +12 -1
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +127 -10
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +69 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +250 -20
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +14 -0
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +1 -2
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js +14 -0
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts +1 -1
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +2 -1
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/middlewares/nextjs.d.ts.map +1 -1
- package/dist/middlewares/nextjs.js +14 -0
- package/dist/middlewares/nextjs.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +2 -2
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.d.ts.map +1 -1
- package/dist/proxy/Service.js +1 -1
- package/dist/proxy/Service.js.map +1 -1
- package/dist/server/Mppx.d.ts +96 -5
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +739 -115
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/Methods.d.ts +96 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +97 -0
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +3 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/client/Methods.js +3 -0
- package/dist/tempo/client/Methods.js.map +1 -1
- package/dist/tempo/client/Subscription.d.ts +114 -0
- package/dist/tempo/client/Subscription.d.ts.map +1 -0
- package/dist/tempo/client/Subscription.js +100 -0
- package/dist/tempo/client/Subscription.js.map +1 -0
- package/dist/tempo/client/index.d.ts +1 -0
- package/dist/tempo/client/index.d.ts.map +1 -1
- package/dist/tempo/client/index.js +1 -0
- package/dist/tempo/client/index.js.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/server/Methods.d.ts +5 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +5 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +221 -0
- package/dist/tempo/server/Subscription.d.ts.map +1 -0
- package/dist/tempo/server/Subscription.js +637 -0
- package/dist/tempo/server/Subscription.js.map +1 -0
- package/dist/tempo/server/index.d.ts +1 -0
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js +1 -0
- package/dist/tempo/server/index.js.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/dist/tempo/subscription/KeyAuthorization.d.ts +282 -0
- package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -0
- package/dist/tempo/subscription/KeyAuthorization.js +297 -0
- package/dist/tempo/subscription/KeyAuthorization.js.map +1 -0
- package/dist/tempo/subscription/Receipt.d.ts +10 -0
- package/dist/tempo/subscription/Receipt.d.ts.map +1 -0
- package/dist/tempo/subscription/Receipt.js +16 -0
- package/dist/tempo/subscription/Receipt.js.map +1 -0
- package/dist/tempo/subscription/Store.d.ts +99 -0
- package/dist/tempo/subscription/Store.d.ts.map +1 -0
- package/dist/tempo/subscription/Store.js +292 -0
- package/dist/tempo/subscription/Store.js.map +1 -0
- package/dist/tempo/subscription/Types.d.ts +65 -0
- package/dist/tempo/subscription/Types.d.ts.map +1 -0
- package/dist/tempo/subscription/Types.js +2 -0
- package/dist/tempo/subscription/Types.js.map +1 -0
- package/dist/tempo/subscription/index.d.ts +6 -0
- package/dist/tempo/subscription/index.d.ts.map +1 -0
- package/dist/tempo/subscription/index.js +4 -0
- package/dist/tempo/subscription/index.js.map +1 -0
- package/dist/zod.d.ts +7 -0
- package/dist/zod.d.ts.map +1 -1
- package/dist/zod.js +18 -0
- package/dist/zod.js.map +1 -1
- package/package.json +3 -3
- package/src/Challenge.test.ts +13 -0
- package/src/Challenge.ts +3 -3
- package/src/Method.ts +46 -1
- package/src/Receipt.ts +2 -0
- package/src/client/Methods.ts +1 -0
- package/src/client/Mppx.test-d.ts +55 -0
- package/src/client/Mppx.test.ts +181 -0
- package/src/client/Mppx.ts +248 -16
- package/src/client/internal/Fetch.test-d.ts +31 -0
- package/src/client/internal/Fetch.test.ts +261 -0
- package/src/client/internal/Fetch.ts +467 -24
- package/src/middlewares/elysia.test.ts +31 -1
- package/src/middlewares/elysia.ts +13 -0
- package/src/middlewares/express.ts +1 -5
- package/src/middlewares/hono.test.ts +30 -1
- package/src/middlewares/hono.ts +13 -0
- package/src/middlewares/internal/mppx.ts +5 -6
- package/src/middlewares/nextjs.test.ts +28 -1
- package/src/middlewares/nextjs.ts +13 -0
- package/src/proxy/Proxy.test.ts +69 -0
- package/src/proxy/Proxy.ts +2 -5
- package/src/proxy/Service.test.ts +34 -0
- package/src/proxy/Service.ts +7 -0
- package/src/server/Mppx.authorize.test.ts +210 -0
- package/src/server/Mppx.test-d.ts +73 -1
- package/src/server/Mppx.test.ts +965 -3
- package/src/server/Mppx.ts +1138 -140
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/Methods.test.ts +131 -0
- package/src/tempo/Methods.ts +136 -0
- package/src/tempo/Subscription.integration.test.ts +591 -0
- package/src/tempo/client/Methods.ts +3 -0
- package/src/tempo/client/Subscription.test.ts +131 -0
- package/src/tempo/client/Subscription.ts +155 -0
- package/src/tempo/client/index.ts +1 -0
- package/src/tempo/index.ts +1 -0
- package/src/tempo/server/Methods.ts +5 -0
- package/src/tempo/server/Subscription.test.ts +1410 -0
- package/src/tempo/server/Subscription.ts +1014 -0
- package/src/tempo/server/index.ts +1 -0
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/subscription/KeyAuthorization.test.ts +204 -0
- package/src/tempo/subscription/KeyAuthorization.ts +394 -0
- package/src/tempo/subscription/Receipt.ts +28 -0
- package/src/tempo/subscription/Store.test.ts +554 -0
- package/src/tempo/subscription/Store.ts +431 -0
- package/src/tempo/subscription/Types.ts +68 -0
- package/src/tempo/subscription/index.ts +23 -0
- package/src/zod.test.ts +23 -1
- package/src/zod.ts +24 -0
package/src/client/Mppx.ts
CHANGED
|
@@ -7,6 +7,9 @@ import * as Fetch from './internal/Fetch.js'
|
|
|
7
7
|
import * as Transport from './Transport.js'
|
|
8
8
|
|
|
9
9
|
export type Methods = readonly (Method.AnyClient | readonly Method.AnyClient[])[]
|
|
10
|
+
type EventResponseOf<transport extends Transport.Transport> =
|
|
11
|
+
| Response
|
|
12
|
+
| Transport.ResponseOf<transport>
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Client-side payment handler.
|
|
@@ -29,6 +32,43 @@ export type Mppx<
|
|
|
29
32
|
context?: AnyContextFor<FlattenMethods<methods>> | undefined,
|
|
30
33
|
options?: createCredential.Options | undefined,
|
|
31
34
|
) => Promise<string>
|
|
35
|
+
/** Register a client event handler by canonical event name. */
|
|
36
|
+
on<name extends Fetch.ClientEventName<FlattenMethods<methods>, EventResponseOf<transport>>>(
|
|
37
|
+
name: name,
|
|
38
|
+
handler: Fetch.ClientEventHandler<FlattenMethods<methods>, name, EventResponseOf<transport>>,
|
|
39
|
+
): Fetch.Unsubscribe
|
|
40
|
+
/** Register a handler for received payment challenges. */
|
|
41
|
+
onChallengeReceived(
|
|
42
|
+
handler: Fetch.ClientEventHandler<
|
|
43
|
+
FlattenMethods<methods>,
|
|
44
|
+
'challenge.received',
|
|
45
|
+
EventResponseOf<transport>
|
|
46
|
+
>,
|
|
47
|
+
): Fetch.Unsubscribe
|
|
48
|
+
/** Register a handler for created credentials. */
|
|
49
|
+
onCredentialCreated(
|
|
50
|
+
handler: Fetch.ClientEventHandler<
|
|
51
|
+
FlattenMethods<methods>,
|
|
52
|
+
'credential.created',
|
|
53
|
+
EventResponseOf<transport>
|
|
54
|
+
>,
|
|
55
|
+
): Fetch.Unsubscribe
|
|
56
|
+
/** Register a handler for failed automatic payment handling. */
|
|
57
|
+
onPaymentFailed(
|
|
58
|
+
handler: Fetch.ClientEventHandler<
|
|
59
|
+
FlattenMethods<methods>,
|
|
60
|
+
'payment.failed',
|
|
61
|
+
EventResponseOf<transport>
|
|
62
|
+
>,
|
|
63
|
+
): Fetch.Unsubscribe
|
|
64
|
+
/** Register a handler for payment retry responses. */
|
|
65
|
+
onPaymentResponse(
|
|
66
|
+
handler: Fetch.ClientEventHandler<
|
|
67
|
+
FlattenMethods<methods>,
|
|
68
|
+
'payment.response',
|
|
69
|
+
EventResponseOf<transport>
|
|
70
|
+
>,
|
|
71
|
+
): Fetch.Unsubscribe
|
|
32
72
|
}
|
|
33
73
|
|
|
34
74
|
/**
|
|
@@ -71,6 +111,7 @@ export function create<
|
|
|
71
111
|
const rawFetch = config.fetch ?? globalThis.fetch
|
|
72
112
|
const methods = config.methods.flat() as unknown as FlattenMethods<methods>
|
|
73
113
|
const acceptPayment = AcceptPayment.resolve(methods, config.paymentPreferences)
|
|
114
|
+
const events = Fetch.createEventDispatcher<FlattenMethods<methods>, EventResponseOf<transport>>()
|
|
74
115
|
|
|
75
116
|
const resolvedOnChallenge = onChallenge as Fetch.from.Config<
|
|
76
117
|
FlattenMethods<methods>
|
|
@@ -79,17 +120,64 @@ export function create<
|
|
|
79
120
|
acceptPayment,
|
|
80
121
|
acceptPaymentPolicy,
|
|
81
122
|
...(config.fetch && { fetch: config.fetch }),
|
|
123
|
+
eventDispatcher: events,
|
|
82
124
|
...(resolvedOnChallenge && { onChallenge: resolvedOnChallenge }),
|
|
83
125
|
methods,
|
|
84
126
|
} satisfies Fetch.from.Config<FlattenMethods<methods>>
|
|
85
127
|
const fetch = Fetch.from<FlattenMethods<methods>>(config_fetch)
|
|
86
128
|
|
|
87
129
|
if (polyfill) Fetch.polyfill(config_fetch)
|
|
130
|
+
|
|
131
|
+
function onChallengeReceived(
|
|
132
|
+
handler: Fetch.ClientEventHandler<
|
|
133
|
+
FlattenMethods<methods>,
|
|
134
|
+
'challenge.received',
|
|
135
|
+
EventResponseOf<transport>
|
|
136
|
+
>,
|
|
137
|
+
) {
|
|
138
|
+
return events.on('challenge.received', handler)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function onCredentialCreated(
|
|
142
|
+
handler: Fetch.ClientEventHandler<
|
|
143
|
+
FlattenMethods<methods>,
|
|
144
|
+
'credential.created',
|
|
145
|
+
EventResponseOf<transport>
|
|
146
|
+
>,
|
|
147
|
+
) {
|
|
148
|
+
return events.on('credential.created', handler)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function onPaymentFailed(
|
|
152
|
+
handler: Fetch.ClientEventHandler<
|
|
153
|
+
FlattenMethods<methods>,
|
|
154
|
+
'payment.failed',
|
|
155
|
+
EventResponseOf<transport>
|
|
156
|
+
>,
|
|
157
|
+
) {
|
|
158
|
+
return events.on('payment.failed', handler)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function onPaymentResponse(
|
|
162
|
+
handler: Fetch.ClientEventHandler<
|
|
163
|
+
FlattenMethods<methods>,
|
|
164
|
+
'payment.response',
|
|
165
|
+
EventResponseOf<transport>
|
|
166
|
+
>,
|
|
167
|
+
) {
|
|
168
|
+
return events.on('payment.response', handler)
|
|
169
|
+
}
|
|
170
|
+
|
|
88
171
|
return {
|
|
89
172
|
fetch,
|
|
90
173
|
rawFetch,
|
|
91
174
|
methods,
|
|
92
175
|
transport,
|
|
176
|
+
on: events.on,
|
|
177
|
+
onChallengeReceived,
|
|
178
|
+
onCredentialCreated,
|
|
179
|
+
onPaymentFailed,
|
|
180
|
+
onPaymentResponse,
|
|
93
181
|
async createCredential(
|
|
94
182
|
response: Transport.ResponseOf<transport>,
|
|
95
183
|
context?: unknown,
|
|
@@ -100,23 +188,59 @@ export function create<
|
|
|
100
188
|
: [transport.getChallenge(response as never)]
|
|
101
189
|
const preferences = resolveChallengePreferences(acceptPayment.entries, options?.acceptPayment)
|
|
102
190
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
191
|
+
let challenge: Challenge.Challenge | undefined
|
|
192
|
+
let mi: FlattenMethods<methods>[number] | undefined
|
|
193
|
+
try {
|
|
194
|
+
const selected = AcceptPayment.selectChallenge(challenges, methods, preferences)
|
|
195
|
+
if (!selected)
|
|
196
|
+
throw new Error(
|
|
197
|
+
`No method found for challenges: ${challenges.map((challenge) => `${challenge.method}.${challenge.intent}`).join(', ')}. Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`,
|
|
198
|
+
)
|
|
108
199
|
|
|
109
|
-
|
|
110
|
-
|
|
200
|
+
const selectedChallenge = selected.challenge
|
|
201
|
+
challenge = selectedChallenge
|
|
202
|
+
mi = selected.method as FlattenMethods<methods>[number]
|
|
203
|
+
if (challenge.expires) Expires.assert(challenge.expires, challenge.id)
|
|
111
204
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
205
|
+
const createCredential = memoizeCreateCredential(
|
|
206
|
+
(overrideContext?: AnyContextFor<FlattenMethods<methods>>) =>
|
|
207
|
+
createCredentialForMethod(selectedChallenge, mi!, overrideContext ?? context),
|
|
208
|
+
)
|
|
209
|
+
const eventCredential = await events.emit(
|
|
210
|
+
'challenge.received',
|
|
211
|
+
createChallengeReceivedPayload({
|
|
212
|
+
challenge: selectedChallenge,
|
|
213
|
+
challenges,
|
|
214
|
+
createCredential,
|
|
215
|
+
method: mi,
|
|
216
|
+
response,
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
const credential = eventCredential ?? (await createCredential())
|
|
220
|
+
Fetch.validateCredentialHeaderValue(credential)
|
|
221
|
+
await events.emit(
|
|
222
|
+
'credential.created',
|
|
223
|
+
createCredentialCreatedPayload({
|
|
224
|
+
challenge: selectedChallenge,
|
|
225
|
+
credential,
|
|
226
|
+
method: mi,
|
|
227
|
+
response,
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
return credential
|
|
231
|
+
} catch (error) {
|
|
232
|
+
await events.emit(
|
|
233
|
+
'payment.failed',
|
|
234
|
+
createPaymentFailedPayload({
|
|
235
|
+
challenge,
|
|
236
|
+
challenges,
|
|
237
|
+
error,
|
|
238
|
+
method: mi,
|
|
239
|
+
response,
|
|
240
|
+
}),
|
|
241
|
+
)
|
|
242
|
+
throw error
|
|
243
|
+
}
|
|
120
244
|
},
|
|
121
245
|
}
|
|
122
246
|
}
|
|
@@ -155,7 +279,7 @@ export declare namespace create {
|
|
|
155
279
|
acceptPaymentPolicy?: Fetch.from.Config['acceptPaymentPolicy'] | undefined
|
|
156
280
|
/** Custom fetch function to wrap. Defaults to `globalThis.fetch`. */
|
|
157
281
|
fetch?: typeof globalThis.fetch
|
|
158
|
-
/** Called when a 402 challenge is received
|
|
282
|
+
/** Called when a 402 challenge is received and no event handler supplies a credential. */
|
|
159
283
|
onChallenge?:
|
|
160
284
|
| ((
|
|
161
285
|
challenge: Challenge.Challenge,
|
|
@@ -187,6 +311,103 @@ type AnyContextFor<methods extends readonly Method.AnyClient[]> = {
|
|
|
187
311
|
: undefined
|
|
188
312
|
}[number]
|
|
189
313
|
|
|
314
|
+
function memoizeCreateCredential<methods extends readonly Method.AnyClient[]>(
|
|
315
|
+
createCredential: (context?: AnyContextFor<methods>) => Promise<string>,
|
|
316
|
+
) {
|
|
317
|
+
let promise: Promise<string> | undefined
|
|
318
|
+
return (context?: AnyContextFor<methods>) => {
|
|
319
|
+
promise ??= createCredential(context)
|
|
320
|
+
return promise
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function createChallengeReceivedPayload<
|
|
325
|
+
methods extends readonly Method.AnyClient[],
|
|
326
|
+
response,
|
|
327
|
+
>(parameters: {
|
|
328
|
+
challenge: Challenge.Challenge
|
|
329
|
+
challenges: readonly Challenge.Challenge[]
|
|
330
|
+
createCredential: (context?: AnyContextFor<methods>) => Promise<string>
|
|
331
|
+
method: methods[number]
|
|
332
|
+
response: response
|
|
333
|
+
}): Fetch.ChallengeReceivedPayload<methods, response> {
|
|
334
|
+
return Object.freeze({
|
|
335
|
+
challenge: snapshotValue(parameters.challenge),
|
|
336
|
+
challenges: parameters.challenges.map((challenge) => snapshotValue(challenge)),
|
|
337
|
+
createCredential: parameters.createCredential,
|
|
338
|
+
method: snapshotMethod(parameters.method),
|
|
339
|
+
response: snapshotResponse(parameters.response),
|
|
340
|
+
}) as never
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function createCredentialCreatedPayload<
|
|
344
|
+
methods extends readonly Method.AnyClient[],
|
|
345
|
+
response,
|
|
346
|
+
>(parameters: {
|
|
347
|
+
challenge: Challenge.Challenge
|
|
348
|
+
credential: string
|
|
349
|
+
method: methods[number]
|
|
350
|
+
response: response
|
|
351
|
+
}): Fetch.CredentialCreatedPayload<methods, response> {
|
|
352
|
+
return Object.freeze({
|
|
353
|
+
challenge: snapshotValue(parameters.challenge),
|
|
354
|
+
credential: parameters.credential,
|
|
355
|
+
method: snapshotMethod(parameters.method),
|
|
356
|
+
response: snapshotResponse(parameters.response),
|
|
357
|
+
}) as never
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function createPaymentFailedPayload<
|
|
361
|
+
methods extends readonly Method.AnyClient[],
|
|
362
|
+
response,
|
|
363
|
+
>(parameters: {
|
|
364
|
+
challenge?: Challenge.Challenge | undefined
|
|
365
|
+
challenges?: readonly Challenge.Challenge[] | undefined
|
|
366
|
+
error: unknown
|
|
367
|
+
method?: methods[number] | undefined
|
|
368
|
+
response: response
|
|
369
|
+
}): Fetch.PaymentFailedPayload<methods, response> {
|
|
370
|
+
return Object.freeze({
|
|
371
|
+
...(parameters.challenge ? { challenge: snapshotValue(parameters.challenge) } : {}),
|
|
372
|
+
...(parameters.challenges
|
|
373
|
+
? { challenges: parameters.challenges.map((challenge) => snapshotValue(challenge)) }
|
|
374
|
+
: {}),
|
|
375
|
+
error: parameters.error,
|
|
376
|
+
...(parameters.method ? { method: snapshotMethod(parameters.method) } : {}),
|
|
377
|
+
response: snapshotResponse(parameters.response),
|
|
378
|
+
}) as never
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function snapshotMethod<method extends Method.AnyClient>(method: method): method {
|
|
382
|
+
return freezeSnapshot(Object.assign({}, method)) as method
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function snapshotResponse<response>(response: response): response {
|
|
386
|
+
if (response instanceof Response) return response.clone() as response
|
|
387
|
+
return snapshotValue(response)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function snapshotValue<value>(value: value): value {
|
|
391
|
+
try {
|
|
392
|
+
return deepFreeze(structuredClone(value))
|
|
393
|
+
} catch {
|
|
394
|
+
return freezeSnapshot(value)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function deepFreeze<value>(value: value): value {
|
|
399
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value
|
|
400
|
+
Object.freeze(value)
|
|
401
|
+
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child)
|
|
402
|
+
return value
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function freezeSnapshot<value>(value: value): value {
|
|
406
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value
|
|
407
|
+
Object.freeze(value)
|
|
408
|
+
return value
|
|
409
|
+
}
|
|
410
|
+
|
|
190
411
|
/**
|
|
191
412
|
* Flattens a methods config tuple, preserving positional types.
|
|
192
413
|
* @internal
|
|
@@ -209,3 +430,14 @@ function resolveChallengePreferences(
|
|
|
209
430
|
if (!override) return fallback
|
|
210
431
|
return typeof override === 'string' ? AcceptPayment.parse(override) : override
|
|
211
432
|
}
|
|
433
|
+
|
|
434
|
+
async function createCredentialForMethod(
|
|
435
|
+
challenge: Challenge.Challenge,
|
|
436
|
+
mi: Method.AnyClient,
|
|
437
|
+
context: unknown,
|
|
438
|
+
): Promise<string> {
|
|
439
|
+
const parsedContext = mi.context && context !== undefined ? mi.context.parse(context) : undefined
|
|
440
|
+
return mi.createCredential(
|
|
441
|
+
parsedContext !== undefined ? { challenge, context: parsedContext } : ({ challenge } as never),
|
|
442
|
+
)
|
|
443
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
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'
|
|
4
5
|
import { charge } from '../../tempo/client/Charge.js'
|
|
5
6
|
import * as Fetch from './Fetch.js'
|
|
6
7
|
|
|
@@ -44,6 +45,36 @@ describe('Fetch.from', () => {
|
|
|
44
45
|
body: JSON.stringify({ foo: 'bar' }),
|
|
45
46
|
})
|
|
46
47
|
})
|
|
48
|
+
|
|
49
|
+
test('behavior: events infer payload types from methods', () => {
|
|
50
|
+
const method = charge()
|
|
51
|
+
const dispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
52
|
+
dispatcher.on('*', (event) => {
|
|
53
|
+
if (event.name === 'challenge.received')
|
|
54
|
+
expectTypeOf(event.payload.challenge).toEqualTypeOf<Challenge.Challenge>()
|
|
55
|
+
})
|
|
56
|
+
dispatcher.on('challenge.received', (payload) => {
|
|
57
|
+
expectTypeOf(payload.method.intent).toEqualTypeOf<'charge'>()
|
|
58
|
+
return payload.createCredential({ account: {} as Account })
|
|
59
|
+
})
|
|
60
|
+
dispatcher.on('credential.created', (payload) => {
|
|
61
|
+
expectTypeOf(payload.method.intent).toEqualTypeOf<'charge'>()
|
|
62
|
+
expectTypeOf(payload.credential).toEqualTypeOf<string>()
|
|
63
|
+
})
|
|
64
|
+
dispatcher.on('payment.failed', (payload) => {
|
|
65
|
+
expectTypeOf(payload.error).toEqualTypeOf<unknown>()
|
|
66
|
+
})
|
|
67
|
+
dispatcher.on('payment.response', (payload) => {
|
|
68
|
+
expectTypeOf(payload.response).toEqualTypeOf<Response>()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const fetch = Fetch.from({
|
|
72
|
+
eventDispatcher: dispatcher,
|
|
73
|
+
methods: [method],
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expectTypeOf(fetch).toBeFunction()
|
|
77
|
+
})
|
|
47
78
|
})
|
|
48
79
|
|
|
49
80
|
describe('Fetch.from.RequestInit', () => {
|
|
@@ -669,6 +669,267 @@ describe('Fetch.from: 402 retry path', () => {
|
|
|
669
669
|
expect(headers.Authorization).toBe('credential')
|
|
670
670
|
})
|
|
671
671
|
|
|
672
|
+
test('emits client events and allows challenge handler to provide credential', async () => {
|
|
673
|
+
const events: string[] = []
|
|
674
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
675
|
+
let callCount = 0
|
|
676
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
677
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
678
|
+
calls.push({ init })
|
|
679
|
+
callCount++
|
|
680
|
+
if (callCount === 1) return make402()
|
|
681
|
+
return new Response('OK', { status: 200 })
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const method = { ...noopMethod, createCredential }
|
|
685
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
686
|
+
eventDispatcher.on('*', (event) => {
|
|
687
|
+
events.push(`*:${event.name}`)
|
|
688
|
+
})
|
|
689
|
+
eventDispatcher.on('challenge.received', async (payload) => {
|
|
690
|
+
events.push(`challenge:${payload.challenge.id}`)
|
|
691
|
+
return 'event-credential'
|
|
692
|
+
})
|
|
693
|
+
eventDispatcher.on('credential.created', (payload) => {
|
|
694
|
+
events.push(`credential:${payload.credential}`)
|
|
695
|
+
throw new Error('observer failed')
|
|
696
|
+
})
|
|
697
|
+
eventDispatcher.on('payment.response', (payload) => {
|
|
698
|
+
events.push(`response:${payload.response.status}`)
|
|
699
|
+
throw new Error('observer failed')
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
const fetch = Fetch.from({
|
|
703
|
+
eventDispatcher,
|
|
704
|
+
fetch: mockFetch,
|
|
705
|
+
methods: [method],
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
const response = await fetch('https://example.com/api')
|
|
709
|
+
|
|
710
|
+
expect(response.status).toBe(200)
|
|
711
|
+
expect(createCredential).not.toHaveBeenCalled()
|
|
712
|
+
const retryHeaders = new Headers((calls[1]!.init as RequestInit).headers)
|
|
713
|
+
expect(retryHeaders.get('Authorization')).toBe('event-credential')
|
|
714
|
+
expect(events).toEqual([
|
|
715
|
+
'challenge:abc',
|
|
716
|
+
'*:challenge.received',
|
|
717
|
+
'credential:event-credential',
|
|
718
|
+
'*:credential.created',
|
|
719
|
+
'response:200',
|
|
720
|
+
'*:payment.response',
|
|
721
|
+
])
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
test('uses the first challenge event credential', async () => {
|
|
725
|
+
const events: string[] = []
|
|
726
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
727
|
+
const method = { ...noopMethod, createCredential }
|
|
728
|
+
let callCount = 0
|
|
729
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
730
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
731
|
+
calls.push({ init })
|
|
732
|
+
callCount++
|
|
733
|
+
if (callCount === 1) return make402()
|
|
734
|
+
return new Response('OK', { status: 200 })
|
|
735
|
+
}
|
|
736
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
737
|
+
eventDispatcher.on('challenge.received', () => {
|
|
738
|
+
events.push('first')
|
|
739
|
+
return 'first-credential'
|
|
740
|
+
})
|
|
741
|
+
eventDispatcher.on('challenge.received', () => {
|
|
742
|
+
events.push('second')
|
|
743
|
+
return 'second-credential'
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
const fetch = Fetch.from({
|
|
747
|
+
eventDispatcher,
|
|
748
|
+
fetch: mockFetch,
|
|
749
|
+
methods: [method],
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
await fetch('https://example.com/api')
|
|
753
|
+
|
|
754
|
+
expect(createCredential).not.toHaveBeenCalled()
|
|
755
|
+
const retryHeaders = new Headers((calls[1]!.init as RequestInit).headers)
|
|
756
|
+
expect(retryHeaders.get('Authorization')).toBe('first-credential')
|
|
757
|
+
expect(events).toEqual(['first'])
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
test('does not emit payment.response for non-ok retry responses', async () => {
|
|
761
|
+
const events: string[] = []
|
|
762
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
763
|
+
const method = { ...noopMethod, createCredential }
|
|
764
|
+
let callCount = 0
|
|
765
|
+
const mockFetch: typeof globalThis.fetch = async () => {
|
|
766
|
+
callCount++
|
|
767
|
+
if (callCount === 1) return make402()
|
|
768
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
769
|
+
}
|
|
770
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
771
|
+
eventDispatcher.on('payment.response', (payload) => {
|
|
772
|
+
events.push(`response:${payload.response.status}`)
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
const fetch = Fetch.from({
|
|
776
|
+
eventDispatcher,
|
|
777
|
+
fetch: mockFetch,
|
|
778
|
+
methods: [method],
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
const response = await fetch('https://example.com/api')
|
|
782
|
+
|
|
783
|
+
expect(response.status).toBe(500)
|
|
784
|
+
expect(events).toEqual([])
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
test('ignores empty challenge event credentials and continues handlers', async () => {
|
|
788
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
789
|
+
const method = { ...noopMethod, createCredential }
|
|
790
|
+
let callCount = 0
|
|
791
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
792
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
793
|
+
calls.push({ init })
|
|
794
|
+
callCount++
|
|
795
|
+
if (callCount === 1) return make402()
|
|
796
|
+
return new Response('OK', { status: 200 })
|
|
797
|
+
}
|
|
798
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
799
|
+
eventDispatcher.on('challenge.received', () => '')
|
|
800
|
+
eventDispatcher.on('challenge.received', () => 'second-credential')
|
|
801
|
+
|
|
802
|
+
const fetch = Fetch.from({
|
|
803
|
+
eventDispatcher,
|
|
804
|
+
fetch: mockFetch,
|
|
805
|
+
methods: [method],
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
await fetch('https://example.com/api')
|
|
809
|
+
|
|
810
|
+
expect(createCredential).not.toHaveBeenCalled()
|
|
811
|
+
const retryHeaders = new Headers((calls[1]!.init as RequestInit).headers)
|
|
812
|
+
expect(retryHeaders.get('Authorization')).toBe('second-credential')
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
test('memoizes createCredential across wildcard observers and fallback', async () => {
|
|
816
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
817
|
+
const method = { ...noopMethod, createCredential }
|
|
818
|
+
let callCount = 0
|
|
819
|
+
const mockFetch: typeof globalThis.fetch = async () => {
|
|
820
|
+
callCount++
|
|
821
|
+
if (callCount === 1) return make402()
|
|
822
|
+
return new Response('OK', { status: 200 })
|
|
823
|
+
}
|
|
824
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
825
|
+
eventDispatcher.on('*', async (event) => {
|
|
826
|
+
if (event.name === 'challenge.received') await event.payload.createCredential()
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
const fetch = Fetch.from({
|
|
830
|
+
eventDispatcher,
|
|
831
|
+
fetch: mockFetch,
|
|
832
|
+
methods: [method],
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
await fetch('https://example.com/api')
|
|
836
|
+
|
|
837
|
+
expect(createCredential).toHaveBeenCalledTimes(1)
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
test('does not expose live challenges or init headers to observers', async () => {
|
|
841
|
+
const createCredential = vi.fn(async ({ challenge }) => `amount:${challenge.request.amount}`)
|
|
842
|
+
const method = { ...noopMethod, createCredential }
|
|
843
|
+
let callCount = 0
|
|
844
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
845
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
846
|
+
calls.push({ init })
|
|
847
|
+
callCount++
|
|
848
|
+
if (callCount === 1) return make402()
|
|
849
|
+
return new Response('OK', { status: 200 })
|
|
850
|
+
}
|
|
851
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
852
|
+
eventDispatcher.on('*', (event) => {
|
|
853
|
+
if (event.name !== 'challenge.received') return
|
|
854
|
+
try {
|
|
855
|
+
;(event.payload.challenge.request as { amount: string }).amount = '999'
|
|
856
|
+
} catch {}
|
|
857
|
+
const headers = new Headers(event.payload.init?.headers)
|
|
858
|
+
headers.set('Authorization', 'attacker')
|
|
859
|
+
if (event.payload.init) event.payload.init.headers = headers
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
const fetch = Fetch.from({
|
|
863
|
+
eventDispatcher,
|
|
864
|
+
fetch: mockFetch,
|
|
865
|
+
methods: [method],
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
await fetch('https://example.com/api', {
|
|
869
|
+
headers: { 'X-Test': '1' },
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
const retryHeaders = new Headers((calls[1]!.init as RequestInit).headers)
|
|
873
|
+
expect(retryHeaders.get('Authorization')).toBe('amount:1')
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
test('continues dispatching observer listeners after one throws', async () => {
|
|
877
|
+
const events: string[] = []
|
|
878
|
+
const method = { ...noopMethod, createCredential: vi.fn(async () => 'credential') }
|
|
879
|
+
let callCount = 0
|
|
880
|
+
const mockFetch: typeof globalThis.fetch = async () => {
|
|
881
|
+
callCount++
|
|
882
|
+
if (callCount === 1) return make402()
|
|
883
|
+
return new Response('OK', { status: 200 })
|
|
884
|
+
}
|
|
885
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
886
|
+
eventDispatcher.on('credential.created', () => {
|
|
887
|
+
events.push('first')
|
|
888
|
+
throw new Error('observer failed')
|
|
889
|
+
})
|
|
890
|
+
eventDispatcher.on('credential.created', () => {
|
|
891
|
+
events.push('second')
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
const fetch = Fetch.from({
|
|
895
|
+
eventDispatcher,
|
|
896
|
+
fetch: mockFetch,
|
|
897
|
+
methods: [method],
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
await fetch('https://example.com/api')
|
|
901
|
+
|
|
902
|
+
expect(events).toEqual(['first', 'second'])
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
test('emits payment.failed when automatic payment handling rejects', async () => {
|
|
906
|
+
const events: string[] = []
|
|
907
|
+
const createCredential = vi.fn(async () => 'credential')
|
|
908
|
+
const mockFetch = vi.fn(async () =>
|
|
909
|
+
make402({ expires: new Date(Date.now() - 60_000).toISOString() }),
|
|
910
|
+
)
|
|
911
|
+
const method = { ...noopMethod, createCredential }
|
|
912
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
913
|
+
eventDispatcher.on('*', (event) => {
|
|
914
|
+
events.push(`*:${event.name}`)
|
|
915
|
+
})
|
|
916
|
+
eventDispatcher.on('payment.failed', (payload) => {
|
|
917
|
+
events.push(
|
|
918
|
+
`failed:${payload.error instanceof Errors.PaymentExpiredError}:${payload.challenge?.id}`,
|
|
919
|
+
)
|
|
920
|
+
throw new Error('observer failed')
|
|
921
|
+
})
|
|
922
|
+
const fetch = Fetch.from({
|
|
923
|
+
eventDispatcher,
|
|
924
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
925
|
+
methods: [method],
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
await expect(fetch('https://example.com/api')).rejects.toThrow(Errors.PaymentExpiredError)
|
|
929
|
+
expect(createCredential).not.toHaveBeenCalled()
|
|
930
|
+
expect(events).toEqual(['failed:true:abc', '*:payment.failed'])
|
|
931
|
+
})
|
|
932
|
+
|
|
672
933
|
test('preserves existing headers on retry', async () => {
|
|
673
934
|
let callCount = 0
|
|
674
935
|
const calls: { init: RequestInit | undefined }[] = []
|