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/dist/server/Mppx.js
CHANGED
|
@@ -6,12 +6,27 @@ import * as Expires from '../Expires.js';
|
|
|
6
6
|
import * as AcceptPayment from '../internal/AcceptPayment.js';
|
|
7
7
|
import * as Env from '../internal/env.js';
|
|
8
8
|
import * as PaymentRequest from '../PaymentRequest.js';
|
|
9
|
+
import * as z from '../zod.js';
|
|
9
10
|
import * as Html from './internal/html/config.js';
|
|
10
11
|
import { serviceWorker } from './internal/html/serviceWorker.gen.js';
|
|
11
12
|
import * as Scope from './internal/scope.js';
|
|
12
13
|
import * as NodeListener from './NodeListener.js';
|
|
13
14
|
import * as Request from './Request.js';
|
|
14
15
|
import * as Transport from './Transport.js';
|
|
16
|
+
const reservedMppxKeyValues = [
|
|
17
|
+
'challenge',
|
|
18
|
+
'compose',
|
|
19
|
+
'methods',
|
|
20
|
+
'on',
|
|
21
|
+
'onChallengeCreated',
|
|
22
|
+
'onPaymentFailed',
|
|
23
|
+
'onPaymentSuccess',
|
|
24
|
+
'realm',
|
|
25
|
+
'transport',
|
|
26
|
+
'verifyCredential',
|
|
27
|
+
];
|
|
28
|
+
/** Public instance keys that payment method names and shorthand intents cannot shadow. */
|
|
29
|
+
export const reservedMppxKeys = new Set(reservedMppxKeyValues);
|
|
15
30
|
/**
|
|
16
31
|
* Creates a server-side payment handler from methods.
|
|
17
32
|
*
|
|
@@ -34,17 +49,24 @@ export function create(config) {
|
|
|
34
49
|
throw new Error('Missing secret key. Set the MPP_SECRET_KEY environment variable or pass `secretKey` to Mppx.create().');
|
|
35
50
|
}
|
|
36
51
|
const methods = config.methods.flat();
|
|
52
|
+
const serverEvents = createServerEventDispatcher();
|
|
37
53
|
const handlers = {};
|
|
38
54
|
const intentCount = {};
|
|
39
55
|
for (const mi of methods) {
|
|
40
56
|
intentCount[mi.intent] = (intentCount[mi.intent] ?? 0) + 1;
|
|
57
|
+
}
|
|
58
|
+
assertNoReservedMppxKeys(methods);
|
|
59
|
+
for (const mi of methods) {
|
|
41
60
|
handlers[`${mi.name}/${mi.intent}`] = createMethodFn({
|
|
61
|
+
authorize: mi.authorize,
|
|
42
62
|
defaults: mi.defaults,
|
|
43
63
|
method: mi,
|
|
44
64
|
realm,
|
|
65
|
+
events: serverEvents,
|
|
45
66
|
request: mi.request,
|
|
46
67
|
respond: mi.respond,
|
|
47
68
|
secretKey,
|
|
69
|
+
stableBinding: mi.stableBinding,
|
|
48
70
|
transport: (mi.transport ?? transport),
|
|
49
71
|
verify: mi.verify,
|
|
50
72
|
});
|
|
@@ -78,30 +100,94 @@ export function create(config) {
|
|
|
78
100
|
// verifyCredential: single-call end-to-end verification
|
|
79
101
|
async function verifyCredentialFn(input, options) {
|
|
80
102
|
const credential = hydrateCredentialMeta(typeof input === 'string' ? Credential.deserialize(input) : input);
|
|
81
|
-
// HMAC provenance check (secretKey is guaranteed non-null by the guard at the top of create())
|
|
82
|
-
if (!Challenge.verify(credential.challenge, { secretKey: secretKey }))
|
|
83
|
-
throw new Errors.InvalidChallengeError({
|
|
84
|
-
id: credential.challenge.id,
|
|
85
|
-
reason: 'challenge was not issued by this server',
|
|
86
|
-
});
|
|
87
|
-
// Expiry check
|
|
88
|
-
Expires.assert(credential.challenge.expires, credential.challenge.id);
|
|
89
103
|
// Find matching method by name + intent
|
|
90
104
|
const { method: credMethod, intent: credIntent } = credential.challenge;
|
|
91
105
|
const mi = methods.find((m) => m.name === credMethod && m.intent === credIntent);
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
const eventMethod = mi ?? { intent: credIntent, name: credMethod };
|
|
107
|
+
const emitStandalonePaymentFailed = async (parameters) => {
|
|
108
|
+
await serverEvents.emit('payment.failed', createPaymentFailedContext({
|
|
109
|
+
capturedRequest: options?.capturedRequest,
|
|
110
|
+
challenge: parameters.challenge,
|
|
111
|
+
credential: parameters.credential,
|
|
112
|
+
error: parameters.error,
|
|
113
|
+
method: eventMethod,
|
|
114
|
+
request: parameters.request,
|
|
115
|
+
submittedChallenge: parameters.submittedChallenge,
|
|
116
|
+
}));
|
|
117
|
+
};
|
|
118
|
+
if (!mi) {
|
|
119
|
+
const error = new Errors.InvalidChallengeError({
|
|
94
120
|
id: credential.challenge.id,
|
|
95
121
|
reason: `no registered method for ${credMethod}/${credIntent}`,
|
|
96
122
|
});
|
|
123
|
+
await emitStandalonePaymentFailed({
|
|
124
|
+
challenge: credential.challenge,
|
|
125
|
+
credential,
|
|
126
|
+
error,
|
|
127
|
+
request: credential.challenge.request,
|
|
128
|
+
submittedChallenge: credential.challenge,
|
|
129
|
+
});
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
// HMAC provenance check (secretKey is guaranteed non-null by the guard at the top of create())
|
|
133
|
+
if (!Challenge.verify(credential.challenge, { secretKey: secretKey })) {
|
|
134
|
+
const error = new Errors.InvalidChallengeError({
|
|
135
|
+
id: credential.challenge.id,
|
|
136
|
+
reason: 'challenge was not issued by this server',
|
|
137
|
+
});
|
|
138
|
+
await emitStandalonePaymentFailed({
|
|
139
|
+
challenge: credential.challenge,
|
|
140
|
+
credential,
|
|
141
|
+
error,
|
|
142
|
+
request: credential.challenge.request,
|
|
143
|
+
submittedChallenge: credential.challenge,
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
// Expiry check
|
|
148
|
+
try {
|
|
149
|
+
Expires.assert(credential.challenge.expires, credential.challenge.id);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
if (e instanceof Errors.PaymentError)
|
|
153
|
+
await emitStandalonePaymentFailed({
|
|
154
|
+
challenge: credential.challenge,
|
|
155
|
+
credential,
|
|
156
|
+
error: e,
|
|
157
|
+
request: credential.challenge.request,
|
|
158
|
+
submittedChallenge: credential.challenge,
|
|
159
|
+
});
|
|
160
|
+
throw e;
|
|
161
|
+
}
|
|
97
162
|
// Validate payload against method schema
|
|
98
|
-
|
|
163
|
+
let parsedCredential;
|
|
164
|
+
try {
|
|
165
|
+
parsedCredential = withParsedCredentialPayload(credential, mi.schema.credential.payload.parse(credential.payload));
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
await emitStandalonePaymentFailed({
|
|
169
|
+
challenge: credential.challenge,
|
|
170
|
+
credential,
|
|
171
|
+
error: new Errors.InvalidPayloadError(),
|
|
172
|
+
request: credential.challenge.request,
|
|
173
|
+
submittedChallenge: credential.challenge,
|
|
174
|
+
});
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
99
177
|
const expectedMeta = Scope.merge({ meta: options?.meta, scope: options?.scope });
|
|
100
178
|
if (options?.scope !== undefined && Scope.read(credential.challenge.meta) !== options.scope) {
|
|
101
|
-
|
|
179
|
+
const error = new Errors.InvalidChallengeError({
|
|
102
180
|
id: credential.challenge.id,
|
|
103
181
|
reason: "credential scope does not match this route's requirements",
|
|
104
182
|
});
|
|
183
|
+
await emitStandalonePaymentFailed({
|
|
184
|
+
challenge: credential.challenge,
|
|
185
|
+
credential: parsedCredential,
|
|
186
|
+
error,
|
|
187
|
+
request: credential.challenge.request,
|
|
188
|
+
submittedChallenge: credential.challenge,
|
|
189
|
+
});
|
|
190
|
+
throw error;
|
|
105
191
|
}
|
|
106
192
|
const shouldValidateRoute = options?.capturedRequest !== undefined ||
|
|
107
193
|
options?.meta !== undefined ||
|
|
@@ -110,37 +196,77 @@ export function create(config) {
|
|
|
110
196
|
const expectedRealm = options?.realm ??
|
|
111
197
|
realm ??
|
|
112
198
|
(options?.capturedRequest === undefined ? credential.challenge.realm : undefined);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
199
|
+
let parsedRequest = credential.challenge.request;
|
|
200
|
+
let request;
|
|
201
|
+
try {
|
|
202
|
+
request = shouldValidateRoute
|
|
203
|
+
? await resolveRouteChallenge({
|
|
204
|
+
capturedRequest: options?.capturedRequest,
|
|
205
|
+
credential: parsedCredential,
|
|
206
|
+
defaults: mi.defaults,
|
|
207
|
+
expires: credential.challenge.expires,
|
|
208
|
+
meta: expectedMeta,
|
|
209
|
+
method: mi,
|
|
210
|
+
realm: expectedRealm,
|
|
211
|
+
request: mi.request,
|
|
212
|
+
routeRequest: options?.request ?? {},
|
|
213
|
+
secretKey: secretKey,
|
|
214
|
+
}).then((resolved) => {
|
|
215
|
+
const mismatch = getChallengeBindingMismatch(resolved.challenge, credential.challenge, mi.stableBinding);
|
|
216
|
+
if (mismatch)
|
|
217
|
+
throw new Errors.InvalidChallengeError({
|
|
218
|
+
id: credential.challenge.id,
|
|
219
|
+
reason: `credential ${mismatch} does not match this route's requirements`,
|
|
220
|
+
});
|
|
221
|
+
parsedRequest = resolved.parsedRequest;
|
|
222
|
+
return resolved.request;
|
|
223
|
+
})
|
|
224
|
+
: credential.challenge.request;
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
if (e instanceof Errors.PaymentError)
|
|
228
|
+
await emitStandalonePaymentFailed({
|
|
229
|
+
challenge: credential.challenge,
|
|
230
|
+
credential: parsedCredential,
|
|
231
|
+
error: e,
|
|
232
|
+
request: credential.challenge.request,
|
|
233
|
+
submittedChallenge: credential.challenge,
|
|
234
|
+
});
|
|
235
|
+
throw e;
|
|
236
|
+
}
|
|
135
237
|
const envelope = options?.capturedRequest
|
|
136
238
|
? {
|
|
137
239
|
capturedRequest: options.capturedRequest,
|
|
138
240
|
challenge: credential.challenge,
|
|
139
|
-
credential,
|
|
140
|
-
request,
|
|
241
|
+
credential: parsedCredential,
|
|
242
|
+
request: parsedRequest,
|
|
141
243
|
}
|
|
142
244
|
: undefined;
|
|
143
|
-
|
|
245
|
+
let receipt;
|
|
246
|
+
try {
|
|
247
|
+
receipt = await mi.verify({ credential: parsedCredential, envelope, request });
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
const error = e instanceof Errors.PaymentError ? e : new Errors.VerificationFailedError();
|
|
251
|
+
await emitStandalonePaymentFailed({
|
|
252
|
+
challenge: credential.challenge,
|
|
253
|
+
credential: parsedCredential,
|
|
254
|
+
error,
|
|
255
|
+
request: parsedRequest,
|
|
256
|
+
submittedChallenge: credential.challenge,
|
|
257
|
+
});
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
260
|
+
await serverEvents.emit('payment.success', createPaymentSuccessContext({
|
|
261
|
+
capturedRequest: options?.capturedRequest,
|
|
262
|
+
challenge: credential.challenge,
|
|
263
|
+
credential: parsedCredential,
|
|
264
|
+
envelope,
|
|
265
|
+
method: mi,
|
|
266
|
+
receipt,
|
|
267
|
+
request: parsedRequest,
|
|
268
|
+
}));
|
|
269
|
+
return receipt;
|
|
144
270
|
}
|
|
145
271
|
function composeFn(...entries) {
|
|
146
272
|
if (transport.name !== 'http')
|
|
@@ -160,10 +286,23 @@ export function create(config) {
|
|
|
160
286
|
});
|
|
161
287
|
return compose(...configured);
|
|
162
288
|
}
|
|
289
|
+
function onChallengeCreated(handler) {
|
|
290
|
+
return serverEvents.on('challenge.created', handler);
|
|
291
|
+
}
|
|
292
|
+
function onPaymentFailed(handler) {
|
|
293
|
+
return serverEvents.on('payment.failed', handler);
|
|
294
|
+
}
|
|
295
|
+
function onPaymentSuccess(handler) {
|
|
296
|
+
return serverEvents.on('payment.success', handler);
|
|
297
|
+
}
|
|
163
298
|
return {
|
|
164
299
|
methods,
|
|
165
300
|
challenge: challengeHandlers,
|
|
166
301
|
compose: composeFn,
|
|
302
|
+
on: serverEvents.on,
|
|
303
|
+
onChallengeCreated,
|
|
304
|
+
onPaymentFailed,
|
|
305
|
+
onPaymentSuccess,
|
|
167
306
|
realm: realm,
|
|
168
307
|
transport,
|
|
169
308
|
verifyCredential: verifyCredentialFn,
|
|
@@ -172,12 +311,19 @@ export function create(config) {
|
|
|
172
311
|
}
|
|
173
312
|
// biome-ignore lint/correctness/noUnusedVariables: _
|
|
174
313
|
function createMethodFn(parameters) {
|
|
175
|
-
const { defaults, method, realm, respond, secretKey, transport, verify } = parameters;
|
|
314
|
+
const { authorize, defaults, events, method, realm, respond, secretKey, stableBinding, transport, verify, } = parameters;
|
|
176
315
|
return (options) => {
|
|
177
316
|
const { description, meta, scope, ...rest } = options;
|
|
178
317
|
const staticMeta = Scope.merge({ meta, scope });
|
|
179
318
|
return Object.assign(async (input) => {
|
|
180
|
-
|
|
319
|
+
if (method.html && isServiceWorkerRequest(input))
|
|
320
|
+
return {
|
|
321
|
+
status: 402,
|
|
322
|
+
challenge: createServiceWorkerResponse(),
|
|
323
|
+
};
|
|
324
|
+
const expires = 'expires' in options
|
|
325
|
+
? normalizeExpires(options.expires)
|
|
326
|
+
: Expires.minutes(5);
|
|
181
327
|
const capturedRequest = await captureRequest(transport, input);
|
|
182
328
|
const effectiveMeta = scope === undefined && input instanceof globalThis.Request
|
|
183
329
|
? Scope.merge({ meta: staticMeta, scope: Scope.get(input) })
|
|
@@ -192,7 +338,39 @@ function createMethodFn(parameters) {
|
|
|
192
338
|
return [null, e];
|
|
193
339
|
}
|
|
194
340
|
})();
|
|
195
|
-
const
|
|
341
|
+
const emitChallenge = async (parameters) => {
|
|
342
|
+
const response = await transport.respondChallenge({
|
|
343
|
+
challenge: parameters.challenge,
|
|
344
|
+
input,
|
|
345
|
+
...(parameters.error && { error: parameters.error }),
|
|
346
|
+
...(parameters.html && { html: parameters.html }),
|
|
347
|
+
});
|
|
348
|
+
if (isIssuedChallengeResponse(response))
|
|
349
|
+
await events.emit('challenge.created', createChallengeContext({
|
|
350
|
+
capturedRequest,
|
|
351
|
+
challenge: parameters.challenge,
|
|
352
|
+
credential: parameters.credential,
|
|
353
|
+
error: parameters.error,
|
|
354
|
+
input,
|
|
355
|
+
method,
|
|
356
|
+
request: parameters.request,
|
|
357
|
+
}));
|
|
358
|
+
return response;
|
|
359
|
+
};
|
|
360
|
+
const emitPaymentFailed = async (parameters) => {
|
|
361
|
+
await events.emit('payment.failed', createPaymentFailedContext({
|
|
362
|
+
capturedRequest,
|
|
363
|
+
challenge: parameters.challenge,
|
|
364
|
+
credential: parameters.credential,
|
|
365
|
+
error: parameters.error,
|
|
366
|
+
input,
|
|
367
|
+
method,
|
|
368
|
+
request: parameters.request,
|
|
369
|
+
retryChallenge: parameters.retryChallenge,
|
|
370
|
+
submittedChallenge: parameters.submittedChallenge,
|
|
371
|
+
}));
|
|
372
|
+
};
|
|
373
|
+
const routeChallenge = await resolveRouteChallenge({
|
|
196
374
|
capturedRequest,
|
|
197
375
|
credential,
|
|
198
376
|
defaults,
|
|
@@ -204,24 +382,138 @@ function createMethodFn(parameters) {
|
|
|
204
382
|
request: parameters.request,
|
|
205
383
|
routeRequest: rest,
|
|
206
384
|
secretKey,
|
|
385
|
+
}).catch(async (e) => {
|
|
386
|
+
if (!(e instanceof Errors.PaymentError))
|
|
387
|
+
throw e;
|
|
388
|
+
const challenge = createFallbackChallenge({
|
|
389
|
+
capturedRequest,
|
|
390
|
+
defaults: defaults ?? {},
|
|
391
|
+
description,
|
|
392
|
+
expires,
|
|
393
|
+
meta: effectiveMeta,
|
|
394
|
+
method,
|
|
395
|
+
realm,
|
|
396
|
+
routeRequest: rest,
|
|
397
|
+
secretKey,
|
|
398
|
+
});
|
|
399
|
+
if (credential)
|
|
400
|
+
await emitPaymentFailed({
|
|
401
|
+
challenge,
|
|
402
|
+
credential,
|
|
403
|
+
error: e,
|
|
404
|
+
request: challenge.request,
|
|
405
|
+
retryChallenge: challenge,
|
|
406
|
+
submittedChallenge: credential.challenge,
|
|
407
|
+
});
|
|
408
|
+
const response = await emitChallenge({
|
|
409
|
+
challenge,
|
|
410
|
+
credential,
|
|
411
|
+
request: challenge.request,
|
|
412
|
+
error: e,
|
|
413
|
+
html: method.html,
|
|
414
|
+
});
|
|
415
|
+
return { response };
|
|
207
416
|
});
|
|
417
|
+
if ('response' in routeChallenge)
|
|
418
|
+
return { challenge: routeChallenge.response, status: 402 };
|
|
419
|
+
const { challenge, parsedRequest, request } = routeChallenge;
|
|
208
420
|
// Credential was provided but malformed
|
|
209
421
|
if (credentialError) {
|
|
210
422
|
const reason = getSafeCredentialReason(credentialError);
|
|
211
|
-
const
|
|
423
|
+
const error = new Errors.MalformedCredentialError(reason ? { reason } : {});
|
|
424
|
+
await emitPaymentFailed({
|
|
212
425
|
challenge,
|
|
213
|
-
|
|
214
|
-
error
|
|
426
|
+
credential: null,
|
|
427
|
+
error,
|
|
428
|
+
request: parsedRequest,
|
|
429
|
+
retryChallenge: challenge,
|
|
430
|
+
});
|
|
431
|
+
const response = await emitChallenge({
|
|
432
|
+
challenge,
|
|
433
|
+
credential: null,
|
|
434
|
+
request: parsedRequest,
|
|
435
|
+
error,
|
|
215
436
|
html: method.html,
|
|
216
437
|
});
|
|
217
438
|
return { challenge: response, status: 402 };
|
|
218
439
|
}
|
|
440
|
+
const success = (receiptData, options = {}) => {
|
|
441
|
+
const { challengeId = challenge.id, credentialForReceipt = { challenge, payload: {} }, envelopeForReceipt, managementResponse, } = options;
|
|
442
|
+
return {
|
|
443
|
+
status: 200,
|
|
444
|
+
withReceipt(response) {
|
|
445
|
+
if (managementResponse) {
|
|
446
|
+
return transport.respondReceipt({
|
|
447
|
+
challengeId,
|
|
448
|
+
credential: credentialForReceipt,
|
|
449
|
+
...(envelopeForReceipt ? { envelope: envelopeForReceipt } : {}),
|
|
450
|
+
input,
|
|
451
|
+
receipt: receiptData,
|
|
452
|
+
response: managementResponse,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (!response)
|
|
456
|
+
throw new MissingReceiptResponseError();
|
|
457
|
+
return transport.respondReceipt({
|
|
458
|
+
challengeId,
|
|
459
|
+
credential: credentialForReceipt,
|
|
460
|
+
...(envelopeForReceipt ? { envelope: envelopeForReceipt } : {}),
|
|
461
|
+
input,
|
|
462
|
+
receipt: receiptData,
|
|
463
|
+
response: response,
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
};
|
|
219
468
|
// No credential provided—issue challenge
|
|
220
469
|
if (!credential) {
|
|
221
|
-
|
|
470
|
+
if (authorize && input instanceof globalThis.Request) {
|
|
471
|
+
try {
|
|
472
|
+
const authorized = await authorize({
|
|
473
|
+
challenge,
|
|
474
|
+
input,
|
|
475
|
+
request: challenge.request,
|
|
476
|
+
});
|
|
477
|
+
if (authorized) {
|
|
478
|
+
await events.emit('payment.success', createPaymentSuccessContext({
|
|
479
|
+
capturedRequest,
|
|
480
|
+
challenge,
|
|
481
|
+
input,
|
|
482
|
+
method,
|
|
483
|
+
receipt: authorized.receipt,
|
|
484
|
+
request: parsedRequest,
|
|
485
|
+
}));
|
|
486
|
+
return success(authorized.receipt, {
|
|
487
|
+
managementResponse: authorized.response,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (e) {
|
|
492
|
+
if (!(e instanceof Errors.PaymentError))
|
|
493
|
+
console.error('mppx: internal authorization error', e);
|
|
494
|
+
const error = e instanceof Errors.PaymentError ? e : new Errors.VerificationFailedError();
|
|
495
|
+
await emitPaymentFailed({
|
|
496
|
+
challenge,
|
|
497
|
+
credential: null,
|
|
498
|
+
error,
|
|
499
|
+
request: parsedRequest,
|
|
500
|
+
retryChallenge: challenge,
|
|
501
|
+
});
|
|
502
|
+
const response = await emitChallenge({
|
|
503
|
+
challenge,
|
|
504
|
+
request: parsedRequest,
|
|
505
|
+
error,
|
|
506
|
+
html: method.html,
|
|
507
|
+
});
|
|
508
|
+
return { challenge: response, status: 402 };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const error = new Errors.PaymentRequiredError({ description });
|
|
512
|
+
const response = await emitChallenge({
|
|
222
513
|
challenge,
|
|
223
|
-
|
|
224
|
-
|
|
514
|
+
credential: null,
|
|
515
|
+
request: parsedRequest,
|
|
516
|
+
error,
|
|
225
517
|
html: method.html,
|
|
226
518
|
});
|
|
227
519
|
return { challenge: response, status: 402 };
|
|
@@ -238,13 +530,23 @@ function createMethodFn(parameters) {
|
|
|
238
530
|
// (https://paymentauth.org/draft-httpauth-payment-00.html#section-5.1.2.1.1).
|
|
239
531
|
// No database lookup is needed; the HMAC is stateless verification.
|
|
240
532
|
if (!Challenge.verify(credential.challenge, { secretKey })) {
|
|
241
|
-
const
|
|
533
|
+
const error = new Errors.InvalidChallengeError({
|
|
534
|
+
id: credential.challenge.id,
|
|
535
|
+
reason: 'challenge was not issued by this server',
|
|
536
|
+
});
|
|
537
|
+
await emitPaymentFailed({
|
|
242
538
|
challenge,
|
|
243
|
-
|
|
244
|
-
error
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
539
|
+
credential,
|
|
540
|
+
error,
|
|
541
|
+
request: parsedRequest,
|
|
542
|
+
retryChallenge: challenge,
|
|
543
|
+
submittedChallenge: credential.challenge,
|
|
544
|
+
});
|
|
545
|
+
const response = await emitChallenge({
|
|
546
|
+
challenge,
|
|
547
|
+
credential,
|
|
548
|
+
request: parsedRequest,
|
|
549
|
+
error,
|
|
248
550
|
html: method.html,
|
|
249
551
|
});
|
|
250
552
|
return { challenge: response, status: 402 };
|
|
@@ -272,15 +574,25 @@ function createMethodFn(parameters) {
|
|
|
272
574
|
// `expires` still is not pinned here because its default is generated
|
|
273
575
|
// per invocation, and `digest` is already bound by the echoed HMAC.
|
|
274
576
|
{
|
|
275
|
-
const mismatch =
|
|
577
|
+
const mismatch = getChallengeBindingMismatch(challenge, credential.challenge, stableBinding);
|
|
276
578
|
if (mismatch) {
|
|
277
|
-
const
|
|
579
|
+
const error = new Errors.InvalidChallengeError({
|
|
580
|
+
id: credential.challenge.id,
|
|
581
|
+
reason: `credential ${mismatch} does not match this route's requirements`,
|
|
582
|
+
});
|
|
583
|
+
await emitPaymentFailed({
|
|
278
584
|
challenge,
|
|
279
|
-
|
|
280
|
-
error
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
585
|
+
credential,
|
|
586
|
+
error,
|
|
587
|
+
request: parsedRequest,
|
|
588
|
+
retryChallenge: challenge,
|
|
589
|
+
submittedChallenge: credential.challenge,
|
|
590
|
+
});
|
|
591
|
+
const response = await emitChallenge({
|
|
592
|
+
challenge,
|
|
593
|
+
credential,
|
|
594
|
+
request: parsedRequest,
|
|
595
|
+
error,
|
|
284
596
|
html: method.html,
|
|
285
597
|
});
|
|
286
598
|
return { challenge: response, status: 402 };
|
|
@@ -291,44 +603,73 @@ function createMethodFn(parameters) {
|
|
|
291
603
|
Expires.assert(credential.challenge.expires, credential.challenge.id);
|
|
292
604
|
}
|
|
293
605
|
catch (error) {
|
|
294
|
-
|
|
606
|
+
await emitPaymentFailed({
|
|
295
607
|
challenge,
|
|
296
|
-
|
|
608
|
+
credential,
|
|
609
|
+
error: error,
|
|
610
|
+
request: parsedRequest,
|
|
611
|
+
retryChallenge: challenge,
|
|
612
|
+
submittedChallenge: credential.challenge,
|
|
613
|
+
});
|
|
614
|
+
const response = await emitChallenge({
|
|
615
|
+
challenge,
|
|
616
|
+
credential,
|
|
617
|
+
request: parsedRequest,
|
|
297
618
|
error: error,
|
|
298
619
|
});
|
|
299
620
|
return { challenge: response, status: 402 };
|
|
300
621
|
}
|
|
301
622
|
// Validate payload structure against method schema
|
|
623
|
+
let parsedCredential;
|
|
302
624
|
try {
|
|
303
|
-
method.schema.credential.payload.parse(credential.payload);
|
|
625
|
+
parsedCredential = withParsedCredentialPayload(credential, method.schema.credential.payload.parse(credential.payload));
|
|
304
626
|
}
|
|
305
627
|
catch {
|
|
306
|
-
const
|
|
628
|
+
const error = new Errors.InvalidPayloadError();
|
|
629
|
+
await emitPaymentFailed({
|
|
307
630
|
challenge,
|
|
308
|
-
|
|
309
|
-
error
|
|
631
|
+
credential,
|
|
632
|
+
error,
|
|
633
|
+
request: parsedRequest,
|
|
634
|
+
retryChallenge: challenge,
|
|
635
|
+
submittedChallenge: credential.challenge,
|
|
636
|
+
});
|
|
637
|
+
const response = await emitChallenge({
|
|
638
|
+
challenge,
|
|
639
|
+
credential,
|
|
640
|
+
request: parsedRequest,
|
|
641
|
+
error,
|
|
310
642
|
});
|
|
311
643
|
return { challenge: response, status: 402 };
|
|
312
644
|
}
|
|
313
645
|
const envelope = Object.freeze({
|
|
314
646
|
capturedRequest,
|
|
315
647
|
challenge: credential.challenge,
|
|
316
|
-
credential,
|
|
317
|
-
request,
|
|
648
|
+
credential: parsedCredential,
|
|
649
|
+
request: parsedRequest,
|
|
318
650
|
});
|
|
319
651
|
// User-provided verification (e.g., check signature, submit tx, verify payment).
|
|
320
652
|
// If verification fails, re-issue the challenge so the client can retry.
|
|
321
653
|
let receiptData;
|
|
322
654
|
try {
|
|
323
|
-
receiptData = await verify({ credential, envelope, request });
|
|
655
|
+
receiptData = await verify({ credential: parsedCredential, envelope, request });
|
|
324
656
|
}
|
|
325
657
|
catch (e) {
|
|
326
658
|
if (!(e instanceof Errors.PaymentError))
|
|
327
659
|
console.error('mppx: internal verification error', e);
|
|
328
660
|
const error = e instanceof Errors.PaymentError ? e : new Errors.VerificationFailedError();
|
|
329
|
-
|
|
661
|
+
await emitPaymentFailed({
|
|
330
662
|
challenge,
|
|
331
|
-
|
|
663
|
+
credential: parsedCredential,
|
|
664
|
+
error,
|
|
665
|
+
request: parsedRequest,
|
|
666
|
+
retryChallenge: challenge,
|
|
667
|
+
submittedChallenge: credential.challenge,
|
|
668
|
+
});
|
|
669
|
+
const response = await emitChallenge({
|
|
670
|
+
challenge,
|
|
671
|
+
credential: parsedCredential,
|
|
672
|
+
request: parsedRequest,
|
|
332
673
|
error,
|
|
333
674
|
});
|
|
334
675
|
return { challenge: response, status: 402 };
|
|
@@ -339,33 +680,30 @@ function createMethodFn(parameters) {
|
|
|
339
680
|
// return the management response directly. If undefined, `withReceipt()`
|
|
340
681
|
// expects the caller to pass the user handler's response instead.
|
|
341
682
|
const managementResponse = respond
|
|
342
|
-
? await respond({
|
|
683
|
+
? await respond({
|
|
684
|
+
credential: parsedCredential,
|
|
685
|
+
envelope,
|
|
686
|
+
input,
|
|
687
|
+
receipt: receiptData,
|
|
688
|
+
request,
|
|
689
|
+
})
|
|
343
690
|
: undefined;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
challengeId: credential.challenge.id,
|
|
361
|
-
credential,
|
|
362
|
-
envelope,
|
|
363
|
-
input,
|
|
364
|
-
receipt: receiptData,
|
|
365
|
-
response: response,
|
|
366
|
-
});
|
|
367
|
-
},
|
|
368
|
-
};
|
|
691
|
+
await events.emit('payment.success', createPaymentSuccessContext({
|
|
692
|
+
capturedRequest,
|
|
693
|
+
challenge: credential.challenge,
|
|
694
|
+
credential: parsedCredential,
|
|
695
|
+
envelope,
|
|
696
|
+
input,
|
|
697
|
+
method,
|
|
698
|
+
receipt: receiptData,
|
|
699
|
+
request: parsedRequest,
|
|
700
|
+
}));
|
|
701
|
+
return success(receiptData, {
|
|
702
|
+
challengeId: credential.challenge.id,
|
|
703
|
+
credentialForReceipt: parsedCredential,
|
|
704
|
+
envelopeForReceipt: envelope,
|
|
705
|
+
managementResponse,
|
|
706
|
+
});
|
|
369
707
|
}, {
|
|
370
708
|
_internal: {
|
|
371
709
|
...method,
|
|
@@ -375,6 +713,7 @@ function createMethodFn(parameters) {
|
|
|
375
713
|
name: method.name,
|
|
376
714
|
intent: method.intent,
|
|
377
715
|
_canonicalRequest: PaymentRequest.fromMethod(method, { ...defaults, ...rest }),
|
|
716
|
+
_stableBinding: stableBinding,
|
|
378
717
|
},
|
|
379
718
|
});
|
|
380
719
|
};
|
|
@@ -389,7 +728,9 @@ function createChallengeFn(parameters) {
|
|
|
389
728
|
return async (options) => {
|
|
390
729
|
const { description, meta, scope, ...rest } = options;
|
|
391
730
|
const effectiveMeta = Scope.merge({ meta, scope });
|
|
392
|
-
const expires = 'expires' in options
|
|
731
|
+
const expires = 'expires' in options
|
|
732
|
+
? normalizeExpires(options.expires)
|
|
733
|
+
: Expires.minutes(5);
|
|
393
734
|
return resolveRouteChallenge({
|
|
394
735
|
defaults,
|
|
395
736
|
description,
|
|
@@ -403,6 +744,197 @@ function createChallengeFn(parameters) {
|
|
|
403
744
|
}).then((resolved) => resolved.challenge);
|
|
404
745
|
};
|
|
405
746
|
}
|
|
747
|
+
function createServerEventDispatcher() {
|
|
748
|
+
const handlers = {
|
|
749
|
+
'*': new Set(),
|
|
750
|
+
'challenge.created': new Set(),
|
|
751
|
+
'payment.failed': new Set(),
|
|
752
|
+
'payment.success': new Set(),
|
|
753
|
+
};
|
|
754
|
+
const on = (name, handler) => {
|
|
755
|
+
switch (name) {
|
|
756
|
+
case '*':
|
|
757
|
+
case 'challenge.created':
|
|
758
|
+
case 'payment.failed':
|
|
759
|
+
case 'payment.success':
|
|
760
|
+
handlers[name].add(handler);
|
|
761
|
+
return () => handlers[name].delete(handler);
|
|
762
|
+
default:
|
|
763
|
+
throw new Error(`Unknown server event "${String(name)}".`);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
return {
|
|
767
|
+
async emit(name, context) {
|
|
768
|
+
await emitServerEventHandlers(handlers[name], context);
|
|
769
|
+
await emitServerEventHandlers(handlers['*'], toServerEventEnvelope(name, context));
|
|
770
|
+
},
|
|
771
|
+
on,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function toServerEventEnvelope(name, payload) {
|
|
775
|
+
return Object.freeze({ name, payload });
|
|
776
|
+
}
|
|
777
|
+
async function emitServerEventHandlers(handlers, context) {
|
|
778
|
+
for (const handler of handlers) {
|
|
779
|
+
try {
|
|
780
|
+
await handler(context);
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
// Errors are isolated, but handlers are still awaited inline.
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function assertNoReservedMppxKeys(methods) {
|
|
788
|
+
for (const method of methods) {
|
|
789
|
+
if (reservedMppxKeys.has(method.name))
|
|
790
|
+
throw new Error(`Method name "${method.name}" conflicts with a reserved Mppx property.`);
|
|
791
|
+
if (reservedMppxKeys.has(method.intent))
|
|
792
|
+
throw new Error(`Method intent "${method.intent}" conflicts with a reserved Mppx property.`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function createChallengeContext(parameters) {
|
|
796
|
+
return Object.freeze({
|
|
797
|
+
...(parameters.capturedRequest
|
|
798
|
+
? { capturedRequest: snapshotCapturedRequest(parameters.capturedRequest) }
|
|
799
|
+
: {}),
|
|
800
|
+
challenge: snapshotValue(parameters.challenge),
|
|
801
|
+
credential: parameters.credential === undefined
|
|
802
|
+
? undefined
|
|
803
|
+
: snapshotNullableValue(parameters.credential),
|
|
804
|
+
error: snapshotError(parameters.error),
|
|
805
|
+
...snapshotInputProperty(parameters.input),
|
|
806
|
+
method: snapshotMethod(parameters.method),
|
|
807
|
+
request: snapshotValue(parameters.request),
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
function createPaymentFailedContext(parameters) {
|
|
811
|
+
return Object.freeze({
|
|
812
|
+
...(parameters.capturedRequest
|
|
813
|
+
? { capturedRequest: snapshotCapturedRequest(parameters.capturedRequest) }
|
|
814
|
+
: {}),
|
|
815
|
+
challenge: snapshotValue(parameters.challenge),
|
|
816
|
+
credential: snapshotNullableValue(parameters.credential),
|
|
817
|
+
error: snapshotError(parameters.error),
|
|
818
|
+
...snapshotInputProperty(parameters.input),
|
|
819
|
+
method: snapshotMethod(parameters.method),
|
|
820
|
+
request: snapshotValue(parameters.request),
|
|
821
|
+
...(parameters.retryChallenge
|
|
822
|
+
? { retryChallenge: snapshotValue(parameters.retryChallenge) }
|
|
823
|
+
: {}),
|
|
824
|
+
...(parameters.submittedChallenge
|
|
825
|
+
? { submittedChallenge: snapshotValue(parameters.submittedChallenge) }
|
|
826
|
+
: {}),
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
function createPaymentSuccessContext(parameters) {
|
|
830
|
+
return Object.freeze({
|
|
831
|
+
...(parameters.capturedRequest
|
|
832
|
+
? { capturedRequest: snapshotCapturedRequest(parameters.capturedRequest) }
|
|
833
|
+
: {}),
|
|
834
|
+
challenge: snapshotValue(parameters.challenge),
|
|
835
|
+
...(parameters.credential ? { credential: snapshotValue(parameters.credential) } : {}),
|
|
836
|
+
...(parameters.envelope ? { envelope: snapshotVerifiedEnvelope(parameters.envelope) } : {}),
|
|
837
|
+
...snapshotInputProperty(parameters.input),
|
|
838
|
+
method: snapshotMethod(parameters.method),
|
|
839
|
+
receipt: snapshotValue(parameters.receipt),
|
|
840
|
+
request: snapshotValue(parameters.request),
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
function snapshotMethod(method) {
|
|
844
|
+
return Object.freeze({
|
|
845
|
+
intent: method.intent,
|
|
846
|
+
name: method.name,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
function snapshotError(error) {
|
|
850
|
+
if (!error)
|
|
851
|
+
return error;
|
|
852
|
+
const snapshot = Object.assign(Object.create(Object.getPrototypeOf(error)), error);
|
|
853
|
+
Object.defineProperties(snapshot, {
|
|
854
|
+
message: { value: error.message, enumerable: false },
|
|
855
|
+
name: { value: error.name, enumerable: false },
|
|
856
|
+
});
|
|
857
|
+
return Object.freeze(snapshot);
|
|
858
|
+
}
|
|
859
|
+
function snapshotVerifiedEnvelope(envelope) {
|
|
860
|
+
return Object.freeze({
|
|
861
|
+
capturedRequest: snapshotCapturedRequest(envelope.capturedRequest),
|
|
862
|
+
challenge: snapshotValue(envelope.challenge),
|
|
863
|
+
credential: snapshotValue(envelope.credential),
|
|
864
|
+
request: snapshotValue(envelope.request),
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
function snapshotCapturedRequest(capturedRequest) {
|
|
868
|
+
return Object.freeze({
|
|
869
|
+
headers: new Headers(capturedRequest.headers),
|
|
870
|
+
hasBody: capturedRequest.hasBody,
|
|
871
|
+
method: capturedRequest.method,
|
|
872
|
+
url: new URL(capturedRequest.url),
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
function snapshotNullableValue(value) {
|
|
876
|
+
if (value === null)
|
|
877
|
+
return null;
|
|
878
|
+
return snapshotValue(value);
|
|
879
|
+
}
|
|
880
|
+
function snapshotValue(value) {
|
|
881
|
+
try {
|
|
882
|
+
return freezeSnapshot(structuredClone(value));
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
return freezeSnapshot(value);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function snapshotInputProperty(input) {
|
|
889
|
+
if (input === undefined)
|
|
890
|
+
return {};
|
|
891
|
+
const snapshot = snapshotTransportInput(input);
|
|
892
|
+
return snapshot === undefined ? {} : { input: snapshot };
|
|
893
|
+
}
|
|
894
|
+
function snapshotTransportInput(input) {
|
|
895
|
+
if (input instanceof globalThis.Request) {
|
|
896
|
+
try {
|
|
897
|
+
return new globalThis.Request(input.url, {
|
|
898
|
+
headers: new Headers(input.headers),
|
|
899
|
+
method: input.method,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
return freezeSnapshot(structuredClone(input));
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
warnOnce(Warnings.transportInputSnapshot, 'Could not clone server event input; omitting `context.input`. Use `capturedRequest` for request correlation.');
|
|
911
|
+
return undefined;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// Event payloads are cloned before listeners see them; shallow freezing keeps
|
|
915
|
+
// the guard simple while preventing top-level mutation of receipts/challenges.
|
|
916
|
+
function freezeSnapshot(value) {
|
|
917
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value))
|
|
918
|
+
return value;
|
|
919
|
+
Object.freeze(value);
|
|
920
|
+
return value;
|
|
921
|
+
}
|
|
922
|
+
function isServiceWorkerRequest(input) {
|
|
923
|
+
return (input instanceof globalThis.Request &&
|
|
924
|
+
new URL(input.url).searchParams.has(Html.params.serviceWorker));
|
|
925
|
+
}
|
|
926
|
+
function createServiceWorkerResponse() {
|
|
927
|
+
return new Response(serviceWorker, {
|
|
928
|
+
status: 200,
|
|
929
|
+
headers: {
|
|
930
|
+
'Content-Type': 'application/javascript',
|
|
931
|
+
'Cache-Control': 'no-store',
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function isIssuedChallengeResponse(response) {
|
|
936
|
+
return !(response instanceof globalThis.Response) || response.status === 402;
|
|
937
|
+
}
|
|
406
938
|
function getSafeCredentialReason(error) {
|
|
407
939
|
if (error instanceof Credential.InvalidCredentialEncodingError)
|
|
408
940
|
return error.message;
|
|
@@ -415,7 +947,30 @@ function getSafeCredentialReason(error) {
|
|
|
415
947
|
const defaultRealm = 'MPP Payment';
|
|
416
948
|
const Warnings = {
|
|
417
949
|
realmFallback: 'realm-fallback',
|
|
950
|
+
transportInputSnapshot: 'transport-input-snapshot',
|
|
418
951
|
};
|
|
952
|
+
const missingReceiptResponseErrorName = 'MissingReceiptResponseError';
|
|
953
|
+
const missingReceiptResponseErrorMessage = 'withReceipt() requires a response argument';
|
|
954
|
+
/** Error thrown when `withReceipt()` needs a response but none was provided. */
|
|
955
|
+
export class MissingReceiptResponseError extends Error {
|
|
956
|
+
name = missingReceiptResponseErrorName;
|
|
957
|
+
constructor() {
|
|
958
|
+
super(missingReceiptResponseErrorMessage);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/** Returns true when an error is the typed `withReceipt()` no-response sentinel. */
|
|
962
|
+
export function isMissingReceiptResponseError(error) {
|
|
963
|
+
if (error instanceof MissingReceiptResponseError)
|
|
964
|
+
return true;
|
|
965
|
+
if (!error || typeof error !== 'object')
|
|
966
|
+
return false;
|
|
967
|
+
const value = error;
|
|
968
|
+
return (value.name === missingReceiptResponseErrorName &&
|
|
969
|
+
value.message === missingReceiptResponseErrorMessage);
|
|
970
|
+
}
|
|
971
|
+
function normalizeExpires(expires) {
|
|
972
|
+
return expires === undefined ? undefined : z.toDatetimeString(expires);
|
|
973
|
+
}
|
|
419
974
|
const _warned = new Set();
|
|
420
975
|
function warnOnce(key, message) {
|
|
421
976
|
if (_warned.has(key))
|
|
@@ -452,18 +1007,33 @@ async function resolveRouteChallenge(parameters) {
|
|
|
452
1007
|
(parameters.capturedRequest
|
|
453
1008
|
? resolveRealmFromCapturedRequest(parameters.capturedRequest)
|
|
454
1009
|
: defaultRealm);
|
|
1010
|
+
const challenge = Challenge.fromMethod(parameters.method, {
|
|
1011
|
+
description: parameters.description,
|
|
1012
|
+
expires: parameters.expires,
|
|
1013
|
+
meta: parameters.meta,
|
|
1014
|
+
realm: effectiveRealm,
|
|
1015
|
+
request: request,
|
|
1016
|
+
secretKey: parameters.secretKey,
|
|
1017
|
+
});
|
|
455
1018
|
return {
|
|
456
|
-
challenge
|
|
457
|
-
|
|
458
|
-
expires: parameters.expires,
|
|
459
|
-
meta: parameters.meta,
|
|
460
|
-
realm: effectiveRealm,
|
|
461
|
-
request: request,
|
|
462
|
-
secretKey: parameters.secretKey,
|
|
463
|
-
}),
|
|
1019
|
+
challenge,
|
|
1020
|
+
parsedRequest: challenge.request,
|
|
464
1021
|
request,
|
|
465
1022
|
};
|
|
466
1023
|
}
|
|
1024
|
+
function createFallbackChallenge(parameters) {
|
|
1025
|
+
return Challenge.fromMethod(parameters.method, {
|
|
1026
|
+
description: parameters.description,
|
|
1027
|
+
expires: parameters.expires,
|
|
1028
|
+
meta: parameters.meta,
|
|
1029
|
+
realm: parameters.realm ??
|
|
1030
|
+
(parameters.capturedRequest
|
|
1031
|
+
? resolveRealmFromCapturedRequest(parameters.capturedRequest)
|
|
1032
|
+
: defaultRealm),
|
|
1033
|
+
request: { ...parameters.defaults, ...parameters.routeRequest },
|
|
1034
|
+
secretKey: parameters.secretKey,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
467
1037
|
/**
|
|
468
1038
|
* Captures the transport request into a frozen snapshot at the start of the
|
|
469
1039
|
* verification flow. This snapshot is threaded through request() → verify() →
|
|
@@ -473,7 +1043,7 @@ async function resolveRouteChallenge(parameters) {
|
|
|
473
1043
|
*
|
|
474
1044
|
* Note: Object.freeze is shallow — it prevents reassigning top-level properties
|
|
475
1045
|
* but does not deep-freeze mutable class instances like Headers or URL. This is
|
|
476
|
-
* an accidental-mutation guard for trusted server
|
|
1046
|
+
* an accidental-mutation guard for trusted server events, not a security boundary.
|
|
477
1047
|
*/
|
|
478
1048
|
async function captureRequest(transport, input) {
|
|
479
1049
|
const capturedRequest = transport.captureRequest
|
|
@@ -493,6 +1063,17 @@ function captureRequestFromInput(input) {
|
|
|
493
1063
|
const coreBindingFields = ['amount', 'currency', 'recipient'];
|
|
494
1064
|
const methodBindingFields = ['chainId', 'memo', 'splits', 'unitType'];
|
|
495
1065
|
const pinnedRequestBindingFields = [...coreBindingFields, ...methodBindingFields];
|
|
1066
|
+
function getChallengeBindingMismatch(expectedChallenge, actualChallenge, stableBinding) {
|
|
1067
|
+
if (!stableBinding)
|
|
1068
|
+
return getPinnedChallengeMismatch(expectedChallenge, actualChallenge);
|
|
1069
|
+
for (const field of ['method', 'intent', 'realm']) {
|
|
1070
|
+
if (actualChallenge[field] !== expectedChallenge[field])
|
|
1071
|
+
return field;
|
|
1072
|
+
}
|
|
1073
|
+
if (!opaqueValuesMatch(expectedChallenge.meta, actualChallenge.meta))
|
|
1074
|
+
return 'opaque';
|
|
1075
|
+
return getRequestBindingMismatch(getStableBinding(expectedChallenge.request, stableBinding), getStableBinding(actualChallenge.request, stableBinding));
|
|
1076
|
+
}
|
|
496
1077
|
/**
|
|
497
1078
|
* Compares only the fields that MUST be stable across request-hook transforms.
|
|
498
1079
|
*
|
|
@@ -548,6 +1129,16 @@ function getPinnedRequestBinding(request) {
|
|
|
548
1129
|
},
|
|
549
1130
|
};
|
|
550
1131
|
}
|
|
1132
|
+
function getRequestBindingMismatch(expected, actual) {
|
|
1133
|
+
const fields = [
|
|
1134
|
+
...Object.keys(expected),
|
|
1135
|
+
...Object.keys(actual).filter((key) => !(key in expected)),
|
|
1136
|
+
];
|
|
1137
|
+
return fields.find((field) => !isDeepStrictEqual(normalizeComparable(expected[field]), normalizeComparable(actual[field])));
|
|
1138
|
+
}
|
|
1139
|
+
function getStableBinding(request, stableBinding) {
|
|
1140
|
+
return stableBinding(request);
|
|
1141
|
+
}
|
|
551
1142
|
function normalizeScalar(value) {
|
|
552
1143
|
return value === undefined ? undefined : String(value);
|
|
553
1144
|
}
|
|
@@ -584,6 +1175,12 @@ function hydrateCredentialMeta(credential) {
|
|
|
584
1175
|
},
|
|
585
1176
|
};
|
|
586
1177
|
}
|
|
1178
|
+
function withParsedCredentialPayload(credential, payload) {
|
|
1179
|
+
return {
|
|
1180
|
+
...credential,
|
|
1181
|
+
payload,
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
587
1184
|
export function compose(...args) {
|
|
588
1185
|
// Extract optional html options from last argument
|
|
589
1186
|
const last = args[args.length - 1];
|
|
@@ -642,14 +1239,18 @@ export function compose(...args) {
|
|
|
642
1239
|
// transformed fields (e.g. amount with decimals) match correctly.
|
|
643
1240
|
// Also checks inside methodDetails for fields moved there by transforms.
|
|
644
1241
|
const candidates = handlers.filter((h) => {
|
|
645
|
-
|
|
646
|
-
|
|
1242
|
+
try {
|
|
1243
|
+
const internal = h._internal;
|
|
1244
|
+
if (!internal || internal.name !== credMethod || internal.intent !== credIntent)
|
|
1245
|
+
return false;
|
|
1246
|
+
const mismatch = internal._stableBinding
|
|
1247
|
+
? getRequestBindingMismatch(getStableBinding(internal._canonicalRequest, internal._stableBinding), getStableBinding(credReq, internal._stableBinding))
|
|
1248
|
+
: getPinnedRequestBindingMismatch(internal._canonicalRequest, credReq);
|
|
1249
|
+
return !mismatch && opaqueValuesMatch(internal.meta, credential.challenge.meta);
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
647
1252
|
return false;
|
|
648
|
-
|
|
649
|
-
if (!canonical)
|
|
650
|
-
return true;
|
|
651
|
-
return (!getPinnedRequestBindingMismatch(canonical, credReq) &&
|
|
652
|
-
opaqueValuesMatch(internal.meta, credential.challenge.meta));
|
|
1253
|
+
}
|
|
653
1254
|
});
|
|
654
1255
|
const match = candidates[0] ??
|
|
655
1256
|
handlers.find((h) => {
|
|
@@ -663,8 +1264,15 @@ export function compose(...args) {
|
|
|
663
1264
|
// handler which will reject with an appropriate error (invalid challenge, etc.).
|
|
664
1265
|
return handlers[0](input);
|
|
665
1266
|
}
|
|
666
|
-
// No credential —
|
|
667
|
-
|
|
1267
|
+
// No credential — evaluate handlers sequentially so authorize()/renewal hooks
|
|
1268
|
+
// can safely claim the request without racing each other.
|
|
1269
|
+
const results = [];
|
|
1270
|
+
for (const handler of handlers) {
|
|
1271
|
+
const result = await handler(input);
|
|
1272
|
+
if (result.status === 200)
|
|
1273
|
+
return result;
|
|
1274
|
+
results.push(result);
|
|
1275
|
+
}
|
|
668
1276
|
const challengeEntries = (() => {
|
|
669
1277
|
const entries = [];
|
|
670
1278
|
for (let i = 0; i < handlers.length; i++) {
|
|
@@ -792,10 +1400,26 @@ export function toNodeListener(handler) {
|
|
|
792
1400
|
await NodeListener.sendResponse(res, result.challenge);
|
|
793
1401
|
}
|
|
794
1402
|
else {
|
|
1403
|
+
const managementResponse = getManagementResponse(result);
|
|
1404
|
+
if (managementResponse) {
|
|
1405
|
+
await NodeListener.sendResponse(res, managementResponse);
|
|
1406
|
+
return { challenge: managementResponse, status: 402 };
|
|
1407
|
+
}
|
|
795
1408
|
const wrapped = result.withReceipt(new globalThis.Response());
|
|
796
1409
|
res.setHeader('Payment-Receipt', wrapped.headers.get('Payment-Receipt'));
|
|
797
1410
|
}
|
|
798
1411
|
return result;
|
|
799
1412
|
};
|
|
800
1413
|
}
|
|
1414
|
+
function getManagementResponse(result) {
|
|
1415
|
+
try {
|
|
1416
|
+
return result.withReceipt();
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
if (isMissingReceiptResponseError(error)) {
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
801
1425
|
//# sourceMappingURL=Mppx.js.map
|