mppx 0.11.0 → 0.13.1
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 +83 -0
- package/README.md +22 -1
- package/dist/Method.d.ts +7 -1
- package/dist/Method.js +7 -2
- package/dist/Receipt.d.ts +2 -0
- package/dist/Receipt.js +2 -0
- package/dist/cli/agent.d.ts +3 -0
- package/dist/cli/agent.js +20 -0
- package/dist/cli/cli.d.ts +9 -1
- package/dist/cli/cli.js +134 -136
- package/dist/cli/plugins/index.js +12 -14
- package/dist/cli/plugins/tempo.js +9 -2
- package/dist/cli/utils.d.ts +11 -1
- package/dist/cli/utils.js +15 -2
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.js +19 -4
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.js +10 -1
- package/dist/cli/validate/payment.js +63 -24
- package/dist/client/Methods.d.ts +0 -2
- package/dist/client/Methods.js +0 -2
- package/dist/client/Mppx.js +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.js +8 -2
- package/dist/client/internal/MethodChallenge.d.ts +1 -0
- package/dist/evm/Types.js +1 -1
- package/dist/evm/server/Charge.js +5 -0
- package/dist/evm/server/Methods.d.ts +164 -3
- package/dist/evm/server/Methods.js +50 -2
- package/dist/integrations/x402/Evm.js +4 -3
- package/dist/internal/HeaderCodec.d.ts +2 -2
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.js +4 -7
- package/dist/middlewares/elysia.d.ts +11 -8
- package/dist/middlewares/elysia.js +67 -14
- package/dist/middlewares/express.js +13 -6
- package/dist/middlewares/hono.js +4 -4
- package/dist/middlewares/nextjs.js +9 -5
- package/dist/proxy/internal/Headers.js +65 -0
- package/dist/proxy/services/stripe.js +1 -0
- package/dist/server/Mppx.d.ts +18 -2
- package/dist/server/Mppx.js +204 -92
- package/dist/server/NodeListener.js +71 -6
- package/dist/server/Request.d.ts +11 -1
- package/dist/server/Request.js +132 -13
- package/dist/server/Transport.d.ts +14 -1
- package/dist/server/Transport.js +1 -0
- package/dist/server/internal/html/compose.main.gen.d.ts +1 -1
- package/dist/server/internal/html/serviceWorker.gen.d.ts +1 -1
- package/dist/stripe/server/Methods.d.ts +10 -3
- package/dist/stripe/server/Methods.js +19 -6
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/Methods.d.ts +1 -0
- package/dist/tempo/Methods.js +1 -0
- package/dist/tempo/client/Charge.js +17 -13
- package/dist/tempo/client/Methods.d.ts +1 -5
- package/dist/tempo/client/Methods.js +10 -5
- package/dist/tempo/client/ResolveAccount.d.ts +3 -0
- package/dist/tempo/client/index.d.ts +0 -3
- package/dist/tempo/client/index.js +0 -2
- package/dist/tempo/internal/currencies.d.ts +19 -0
- package/dist/tempo/internal/currencies.js +41 -0
- package/dist/tempo/internal/defaults.d.ts +5 -3
- package/dist/tempo/internal/defaults.js +6 -2
- package/dist/tempo/internal/fee-payer.d.ts +11 -7
- package/dist/tempo/internal/fee-payer.js +30 -11
- package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
- package/dist/tempo/internal/machine-token-charge.js +2 -1
- package/dist/tempo/legacy/index.d.ts +1 -3
- package/dist/tempo/legacy/index.js +1 -3
- package/dist/tempo/server/Charge.d.ts +12 -1
- package/dist/tempo/server/Charge.js +109 -78
- package/dist/tempo/server/Methods.d.ts +5048 -40
- package/dist/tempo/server/Methods.js +104 -13
- package/dist/tempo/server/Subscription.d.ts +1 -1
- package/dist/tempo/server/Subscription.js +31 -4
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.js +28 -11
- package/dist/tempo/session/client/ChannelOps.js +5 -0
- package/dist/tempo/session/client/Session.d.ts +1 -0
- package/dist/tempo/session/client/Session.js +7 -4
- package/dist/tempo/session/client/SessionManager.js +3 -0
- package/dist/tempo/session/client/Transports.d.ts +8 -1
- package/dist/tempo/session/client/Transports.js +12 -3
- package/dist/tempo/session/precompile/Chain.js +16 -1
- package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
- package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
- package/dist/tempo/session/precompile/Voucher.js +48 -0
- package/dist/tempo/session/server/ChannelStore.d.ts +22 -2
- package/dist/tempo/session/server/ChannelStore.js +37 -7
- package/dist/tempo/session/server/CredentialVerification.d.ts +6 -1
- package/dist/tempo/session/server/CredentialVerification.js +56 -19
- package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
- package/dist/tempo/session/server/MeteredStream.js +85 -29
- package/dist/tempo/session/server/RequestState.d.ts +6 -0
- package/dist/tempo/session/server/RequestState.js +11 -4
- package/dist/tempo/session/server/Session.d.ts +2 -1
- package/dist/tempo/session/server/Session.js +10 -2
- package/dist/tempo/session/server/Settlement.d.ts +23 -0
- package/dist/tempo/session/server/Settlement.js +105 -25
- package/dist/tempo/session/server/Sse.d.ts +3 -1
- package/dist/tempo/session/server/Sse.js +105 -32
- package/dist/tempo/session/server/Transports.d.ts +24 -19
- package/dist/tempo/session/server/Transports.js +169 -58
- package/dist/tempo/session/server/Ws.d.ts +5 -1
- package/dist/tempo/session/server/Ws.js +75 -20
- package/dist/tempo/subscription/KeyAuthorization.d.ts +70 -8
- package/dist/tempo/subscription/Store.d.ts +2 -0
- package/dist/tempo/subscription/Store.js +3 -0
- package/dist/validation/core.js +2 -1
- package/dist/x402/server/EvmCharge.d.ts +3 -1
- package/dist/x402/server/EvmCharge.js +14 -1
- package/package.json +8 -8
- package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
- package/dist/tempo/legacy/client/ChannelOps.js +0 -168
- package/dist/tempo/legacy/client/Session.d.ts +0 -185
- package/dist/tempo/legacy/client/Session.js +0 -279
- package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
- package/dist/tempo/legacy/client/SessionManager.js +0 -706
- package/dist/tempo/legacy/client/index.d.ts +0 -7
- package/dist/tempo/legacy/client/index.js +0 -5
package/dist/server/Mppx.js
CHANGED
|
@@ -19,6 +19,11 @@ import * as SecretKey from './internal/SecretKey.js';
|
|
|
19
19
|
import * as NodeListener from './NodeListener.js';
|
|
20
20
|
import * as Request from './Request.js';
|
|
21
21
|
import * as Transport from './Transport.js';
|
|
22
|
+
const streamingReceiptWrappers = new WeakSet();
|
|
23
|
+
/** Returns whether a successful payment result accepts streaming receipt responses. @internal */
|
|
24
|
+
export function supportsStreamingReceipts(result) {
|
|
25
|
+
return streamingReceiptWrappers.has(result.withReceipt);
|
|
26
|
+
}
|
|
22
27
|
const reservedMppxKeyValues = [
|
|
23
28
|
'challenge',
|
|
24
29
|
'compose',
|
|
@@ -64,22 +69,18 @@ export function create(config) {
|
|
|
64
69
|
? createAttestationGuard(attestation)
|
|
65
70
|
: undefined;
|
|
66
71
|
const handlers = {};
|
|
72
|
+
const methodHandlers = new Map();
|
|
67
73
|
const intentCount = {};
|
|
74
|
+
function composeConfigured(configured) {
|
|
75
|
+
if (transport.name === 'mcp' || transport.name === 'mcp-sdk')
|
|
76
|
+
return composeMcpHandlers(configured, transport);
|
|
77
|
+
return composeHandlers(configured, undefined, selectOffers);
|
|
78
|
+
}
|
|
79
|
+
function createGroupedHandler(group) {
|
|
80
|
+
return (options) => composeConfigured(group.map((method) => methodHandlers.get(method)(options)));
|
|
81
|
+
}
|
|
68
82
|
for (const mi of methods) {
|
|
69
83
|
intentCount[mi.intent] = (intentCount[mi.intent] ?? 0) + 1;
|
|
70
|
-
if (mi.onPaymentSuccess) {
|
|
71
|
-
serverEvents.on('payment.success', (async (ctx) => {
|
|
72
|
-
if (ctx.method.name === mi.name && ctx.method.intent === mi.intent) {
|
|
73
|
-
await mi.onPaymentSuccess({
|
|
74
|
-
challenge: ctx.challenge,
|
|
75
|
-
input: ctx.input,
|
|
76
|
-
receipt: ctx.receipt,
|
|
77
|
-
request: ctx.request,
|
|
78
|
-
...(ctx.requestInput !== undefined && { requestInput: ctx.requestInput }),
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
84
|
}
|
|
84
85
|
assertNoReservedMppxKeys(methods);
|
|
85
86
|
for (const mi of methods) {
|
|
@@ -105,6 +106,8 @@ export function create(config) {
|
|
|
105
106
|
const aliasKey = mi.alias ? `${mi.name}/${mi.alias}` : undefined;
|
|
106
107
|
if (mi.extensions)
|
|
107
108
|
Object.assign(fn, mi.extensions);
|
|
109
|
+
Object.assign(fn, { _method: mi });
|
|
110
|
+
methodHandlers.set(mi, fn);
|
|
108
111
|
if (!aliasKey || !handlers[wireKey])
|
|
109
112
|
handlers[wireKey] = fn;
|
|
110
113
|
if (aliasKey)
|
|
@@ -122,23 +125,25 @@ export function create(config) {
|
|
|
122
125
|
}
|
|
123
126
|
else {
|
|
124
127
|
const intentMethods = methods.filter((m) => m.intent === mi.intent);
|
|
125
|
-
handlers[mi.intent] = (
|
|
126
|
-
const configured = intentMethods.map((m) => {
|
|
127
|
-
const key = `${m.name}/${m.intent}`;
|
|
128
|
-
const handlerFn = handlers[key];
|
|
129
|
-
return handlerFn(options);
|
|
130
|
-
});
|
|
131
|
-
return composeHandlers(configured, undefined, selectOffers);
|
|
132
|
-
};
|
|
128
|
+
handlers[mi.intent] = createGroupedHandler(intentMethods);
|
|
133
129
|
}
|
|
134
130
|
}
|
|
131
|
+
// Preserve distinct configured offers sharing a wire method, including nested handlers.
|
|
132
|
+
for (const key of new Set(methods.filter((m) => !m.alias).map((m) => `${m.name}/${m.intent}`))) {
|
|
133
|
+
const matching = methods.filter((m) => !m.alias && `${m.name}/${m.intent}` === key);
|
|
134
|
+
if (matching.length < 2)
|
|
135
|
+
continue;
|
|
136
|
+
const composed = createGroupedHandler(matching);
|
|
137
|
+
Object.assign(composed, matching[0].extensions);
|
|
138
|
+
handlers[key] = composed;
|
|
139
|
+
}
|
|
135
140
|
// Build nested handlers: mppx.tempo.charge(...)
|
|
136
141
|
for (const mi of methods) {
|
|
137
142
|
if (!handlers[mi.name])
|
|
138
143
|
handlers[mi.name] = {};
|
|
139
144
|
const key = mi.alias ? `${mi.name}/${mi.alias}` : `${mi.name}/${mi.intent}`;
|
|
140
145
|
const fn = handlers[key];
|
|
141
|
-
fn._method
|
|
146
|
+
fn._method ??= mi;
|
|
142
147
|
handlers[mi.name][mi.alias ?? mi.intent] = fn;
|
|
143
148
|
}
|
|
144
149
|
// Build challenge generators: mppx.challenge.tempo.charge(...)
|
|
@@ -146,6 +151,8 @@ export function create(config) {
|
|
|
146
151
|
for (const mi of methods) {
|
|
147
152
|
if (!challengeHandlers[mi.name])
|
|
148
153
|
challengeHandlers[mi.name] = {};
|
|
154
|
+
if (challengeHandlers[mi.name][mi.alias ?? mi.intent])
|
|
155
|
+
continue;
|
|
149
156
|
challengeHandlers[mi.name][mi.alias ?? mi.intent] = createChallengeFn({
|
|
150
157
|
credentialHeader,
|
|
151
158
|
defaults: mi.defaults,
|
|
@@ -160,7 +167,7 @@ export function create(config) {
|
|
|
160
167
|
const emitFailures = parameters.emitFailures === true;
|
|
161
168
|
const { method: credMethod, intent: credIntent } = credential.challenge;
|
|
162
169
|
const methodCandidates = methods.filter((m) => m.name === credMethod && m.intent === credIntent);
|
|
163
|
-
|
|
170
|
+
let mi = methodCandidates[0];
|
|
164
171
|
const eventMethod = mi ?? { intent: credIntent, name: credMethod };
|
|
165
172
|
const emitStandalonePaymentFailed = async (failure) => {
|
|
166
173
|
if (!emitFailures)
|
|
@@ -191,11 +198,6 @@ export function create(config) {
|
|
|
191
198
|
reason: `no registered method for ${credMethod}/${credIntent}`,
|
|
192
199
|
}));
|
|
193
200
|
}
|
|
194
|
-
if (parameters.requireValidate && !mi.validate)
|
|
195
|
-
await fail(new Errors.VerificationFailedError({
|
|
196
|
-
details: { intent: credIntent, method: credMethod },
|
|
197
|
-
reason: `${credMethod}/${credIntent} does not support non-mutating credential validation`,
|
|
198
|
-
}));
|
|
199
201
|
if (!Challenge.verify(credential.challenge, { secretKey: secretKey })) {
|
|
200
202
|
await fail(new Errors.InvalidChallengeError({
|
|
201
203
|
id: credential.challenge.id,
|
|
@@ -210,6 +212,24 @@ export function create(config) {
|
|
|
210
212
|
await fail(e);
|
|
211
213
|
throw e;
|
|
212
214
|
}
|
|
215
|
+
if (methodCandidates.length > 1) {
|
|
216
|
+
const matches = [
|
|
217
|
+
...new Set(methodCandidates.filter((candidate) => matchesStandaloneMethod(candidate, credential.challenge.request, options?.request))),
|
|
218
|
+
];
|
|
219
|
+
if (matches.length !== 1)
|
|
220
|
+
await fail(new Errors.InvalidChallengeError({
|
|
221
|
+
id: credential.challenge.id,
|
|
222
|
+
reason: matches.length === 0
|
|
223
|
+
? 'no configured method matches the challenge request'
|
|
224
|
+
: 'multiple configured methods match the challenge request; use a configured route handler',
|
|
225
|
+
}));
|
|
226
|
+
mi = matches[0];
|
|
227
|
+
}
|
|
228
|
+
if (parameters.requireValidate && !mi.validate)
|
|
229
|
+
await fail(new Errors.VerificationFailedError({
|
|
230
|
+
details: { intent: credIntent, method: credMethod },
|
|
231
|
+
reason: `${credMethod}/${credIntent} does not support non-mutating credential validation`,
|
|
232
|
+
}));
|
|
213
233
|
let parsedCredential;
|
|
214
234
|
try {
|
|
215
235
|
parsedCredential = withParsedCredentialPayload(credential, mi.schema.credential.payload.parse(credential.payload));
|
|
@@ -291,6 +311,7 @@ export function create(config) {
|
|
|
291
311
|
return prepared.method.validate({
|
|
292
312
|
credential: prepared.parsedCredential,
|
|
293
313
|
envelope: prepared.envelope,
|
|
314
|
+
operation: 'validate',
|
|
294
315
|
request: prepared.request,
|
|
295
316
|
});
|
|
296
317
|
}
|
|
@@ -312,7 +333,12 @@ export function create(config) {
|
|
|
312
333
|
let receipt;
|
|
313
334
|
try {
|
|
314
335
|
if (mi.broadcast && mi.validate)
|
|
315
|
-
await mi.validate({
|
|
336
|
+
await mi.validate({
|
|
337
|
+
credential: parsedCredential,
|
|
338
|
+
envelope,
|
|
339
|
+
operation: 'broadcast',
|
|
340
|
+
request,
|
|
341
|
+
});
|
|
316
342
|
const broadcast = mi.broadcast ?? mi.verify;
|
|
317
343
|
receipt = await broadcast({ credential: parsedCredential, envelope, request });
|
|
318
344
|
}
|
|
@@ -336,7 +362,7 @@ export function create(config) {
|
|
|
336
362
|
receipt,
|
|
337
363
|
request: parsedRequest,
|
|
338
364
|
...(requestInput !== undefined && { requestInput }),
|
|
339
|
-
}));
|
|
365
|
+
}), mi);
|
|
340
366
|
return receipt;
|
|
341
367
|
}
|
|
342
368
|
const verifyCredentialFn = broadcastCredentialFn;
|
|
@@ -347,18 +373,27 @@ export function create(config) {
|
|
|
347
373
|
if (entries.length === 0)
|
|
348
374
|
throw new Error('compose() requires at least one entry');
|
|
349
375
|
const configured = entries.map(([methodOrKey, options]) => {
|
|
376
|
+
const method = typeof methodOrKey === 'string'
|
|
377
|
+
? undefined
|
|
378
|
+
: typeof methodOrKey === 'function'
|
|
379
|
+
? methodOrKey._method
|
|
380
|
+
: methodOrKey;
|
|
350
381
|
const key = typeof methodOrKey === 'string'
|
|
351
382
|
? methodOrKey
|
|
352
|
-
:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
383
|
+
: `${method.name}/${method.alias ?? method.intent}`;
|
|
384
|
+
const handlerFn = typeof methodOrKey === 'function' && handlers[key] === methodOrKey
|
|
385
|
+
? methodOrKey
|
|
386
|
+
: method
|
|
387
|
+
? methodHandlers.get(method)
|
|
388
|
+
: handlers[key];
|
|
356
389
|
if (!handlerFn)
|
|
357
390
|
throw new Error(`No handler for "${key}". Is this method in your methods array?`);
|
|
358
|
-
const
|
|
359
|
-
if (isMcp &&
|
|
391
|
+
const configuredMethod = handlerFn._method;
|
|
392
|
+
if (isMcp &&
|
|
393
|
+
configuredMethod?.transport &&
|
|
394
|
+
configuredMethod.transport.name !== transport.name)
|
|
360
395
|
throw new Error('MCP compose() requires methods using the configured MCP transport');
|
|
361
|
-
if (isMcp &&
|
|
396
|
+
if (isMcp && configuredMethod?.canOffer)
|
|
362
397
|
throw new Error('MCP compose() does not support HTTP canOffer hooks');
|
|
363
398
|
return handlerFn(options);
|
|
364
399
|
});
|
|
@@ -405,8 +440,15 @@ function createMethodFn(parameters) {
|
|
|
405
440
|
name: method.name,
|
|
406
441
|
intent: method.intent,
|
|
407
442
|
html: method.html,
|
|
443
|
+
credentialHeaders: [
|
|
444
|
+
...new Set([
|
|
445
|
+
credentialHeader ?? Constants.Headers.authorization,
|
|
446
|
+
...(transport.credentialHeaders ?? []),
|
|
447
|
+
]),
|
|
448
|
+
],
|
|
408
449
|
_canonicalRequest: PaymentRequest.fromMethod(method, { ...defaults, ...rest }),
|
|
409
450
|
_stableBinding: stableBinding,
|
|
451
|
+
_transport: transport,
|
|
410
452
|
};
|
|
411
453
|
Object.defineProperty(internal, '_method', { value: method });
|
|
412
454
|
const handler = async (input) => {
|
|
@@ -584,7 +626,7 @@ function createMethodFn(parameters) {
|
|
|
584
626
|
}
|
|
585
627
|
const success = (receiptData, options = {}) => {
|
|
586
628
|
const { challengeId = challenge.id, credentialForReceipt = { challenge, payload: {} }, envelopeForReceipt, managementResponse, } = options;
|
|
587
|
-
|
|
629
|
+
const result = {
|
|
588
630
|
status: 200,
|
|
589
631
|
withReceipt(response) {
|
|
590
632
|
if (managementResponse) {
|
|
@@ -609,6 +651,9 @@ function createMethodFn(parameters) {
|
|
|
609
651
|
});
|
|
610
652
|
},
|
|
611
653
|
};
|
|
654
|
+
if (transport.supportsStreamingReceipts)
|
|
655
|
+
streamingReceiptWrappers.add(result.withReceipt);
|
|
656
|
+
return result;
|
|
612
657
|
};
|
|
613
658
|
// No credential provided—issue challenge
|
|
614
659
|
if (!credential) {
|
|
@@ -620,15 +665,6 @@ function createMethodFn(parameters) {
|
|
|
620
665
|
request: challenge.request,
|
|
621
666
|
});
|
|
622
667
|
if (authorized) {
|
|
623
|
-
await events.emit('payment.success', createPaymentSuccessContext({
|
|
624
|
-
capturedRequest,
|
|
625
|
-
challenge,
|
|
626
|
-
input,
|
|
627
|
-
method,
|
|
628
|
-
receipt: authorized.receipt,
|
|
629
|
-
request: parsedRequest,
|
|
630
|
-
requestInput: request,
|
|
631
|
-
}));
|
|
632
668
|
return success(authorized.receipt, {
|
|
633
669
|
managementResponse: authorized.response,
|
|
634
670
|
});
|
|
@@ -799,7 +835,12 @@ function createMethodFn(parameters) {
|
|
|
799
835
|
let receiptData;
|
|
800
836
|
try {
|
|
801
837
|
if (broadcast && validate)
|
|
802
|
-
await validate({
|
|
838
|
+
await validate({
|
|
839
|
+
credential: parsedCredential,
|
|
840
|
+
envelope,
|
|
841
|
+
operation: 'broadcast',
|
|
842
|
+
request,
|
|
843
|
+
});
|
|
803
844
|
const broadcastCredential = broadcast ?? verify;
|
|
804
845
|
receiptData = await broadcastCredential({
|
|
805
846
|
credential: parsedCredential,
|
|
@@ -851,7 +892,7 @@ function createMethodFn(parameters) {
|
|
|
851
892
|
receipt: receiptData,
|
|
852
893
|
request: parsedRequest,
|
|
853
894
|
requestInput: request,
|
|
854
|
-
}));
|
|
895
|
+
}), method);
|
|
855
896
|
return success(receiptData, {
|
|
856
897
|
challengeId: credential.challenge.id,
|
|
857
898
|
credentialForReceipt: parsedCredential,
|
|
@@ -916,6 +957,7 @@ function createAttestationGuard(verifiers) {
|
|
|
916
957
|
return verification;
|
|
917
958
|
};
|
|
918
959
|
}
|
|
960
|
+
/** Dispatches instance events and scopes success hooks to the supplied configured method. */
|
|
919
961
|
function createServerEventDispatcher() {
|
|
920
962
|
const handlers = {
|
|
921
963
|
'*': new Set(),
|
|
@@ -936,7 +978,22 @@ function createServerEventDispatcher() {
|
|
|
936
978
|
}
|
|
937
979
|
};
|
|
938
980
|
return {
|
|
939
|
-
async emit(name, context) {
|
|
981
|
+
async emit(name, context, method) {
|
|
982
|
+
if (name === 'payment.success' && method?.onPaymentSuccess) {
|
|
983
|
+
const success = context;
|
|
984
|
+
try {
|
|
985
|
+
await method.onPaymentSuccess({
|
|
986
|
+
challenge: success.challenge,
|
|
987
|
+
input: success.input,
|
|
988
|
+
receipt: success.receipt,
|
|
989
|
+
request: success.request,
|
|
990
|
+
...(success.requestInput !== undefined && { requestInput: success.requestInput }),
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
catch {
|
|
994
|
+
// Per-method hooks are isolated just like instance event handlers.
|
|
995
|
+
}
|
|
996
|
+
}
|
|
940
997
|
await emitServerEventHandlers(handlers[name], context);
|
|
941
998
|
await emitServerEventHandlers(handlers['*'], toServerEventEnvelope(name, context));
|
|
942
999
|
},
|
|
@@ -1328,6 +1385,30 @@ function getPinnedChallengeMismatch(expectedChallenge, actualChallenge) {
|
|
|
1328
1385
|
return 'opaque';
|
|
1329
1386
|
return getPinnedRequestBindingMismatch(expectedChallenge.request, actualChallenge.request);
|
|
1330
1387
|
}
|
|
1388
|
+
/** Matches standalone credentials against the configured portion of a method's stable binding. */
|
|
1389
|
+
function matchesStandaloneMethod(method, request, routeRequest) {
|
|
1390
|
+
try {
|
|
1391
|
+
const configured = { ...method.defaults, ...routeRequest };
|
|
1392
|
+
const parsed = method.schema.request.safeParse(configured);
|
|
1393
|
+
// Standalone callers may omit route-only inputs such as amount. In that
|
|
1394
|
+
// case, constrain selection by the defaults we have rather than parsing
|
|
1395
|
+
// the echoed wire request as input and applying its transforms twice.
|
|
1396
|
+
const expectedRequest = parsed.success ? parsed.data : configured;
|
|
1397
|
+
const binding = (value) => {
|
|
1398
|
+
if (method.stableBinding)
|
|
1399
|
+
return getStableBinding(value, method.stableBinding);
|
|
1400
|
+
const { coreBinding, methodBinding } = getPinnedRequestBinding(value);
|
|
1401
|
+
return { ...coreBinding, ...methodBinding };
|
|
1402
|
+
};
|
|
1403
|
+
const expected = binding(expectedRequest);
|
|
1404
|
+
const actual = binding(request);
|
|
1405
|
+
return Object.entries(expected).every(([key, value]) => value === undefined ||
|
|
1406
|
+
isDeepStrictEqual(normalizeComparable(value), normalizeComparable(actual[key])));
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1331
1412
|
function getPinnedRequestBindingMismatch(expectedRequest, actualRequest) {
|
|
1332
1413
|
const expected = getPinnedRequestBinding(expectedRequest);
|
|
1333
1414
|
const actual = getPinnedRequestBinding(actualRequest);
|
|
@@ -1457,6 +1538,11 @@ export function compose(...args) {
|
|
|
1457
1538
|
return composeHandlers(handlers, composeOptions);
|
|
1458
1539
|
}
|
|
1459
1540
|
function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
1541
|
+
// Flatten only shared policies; a static wrapper must retain a nested server's selector.
|
|
1542
|
+
handlers = handlers.flatMap((handler) => {
|
|
1543
|
+
const internal = handler._internal;
|
|
1544
|
+
return internal?.selectOffers === selectOffers ? (internal?.handlers ?? [handler]) : [handler];
|
|
1545
|
+
});
|
|
1460
1546
|
if (handlers.length === 0)
|
|
1461
1547
|
throw new Error('compose() requires at least one handler');
|
|
1462
1548
|
const offerSelector = selectOffers;
|
|
@@ -1486,7 +1572,10 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1486
1572
|
// Try to extract a Payment credential to decide whether to dispatch or challenge.
|
|
1487
1573
|
// Only gate on the Payment scheme — other auth schemes (Bearer, Basic, etc.)
|
|
1488
1574
|
// should fall through to the merged-402 path so all offers are presented.
|
|
1489
|
-
const
|
|
1575
|
+
const credentialHeaders = new Set(handlers.flatMap((handler) => getConfiguredOffers(handler).flatMap((internal) => internal.credentialHeaders ?? [])));
|
|
1576
|
+
const paymentHeader = Array.from(credentialHeaders)
|
|
1577
|
+
.map((header) => input.headers.get(header))
|
|
1578
|
+
.filter((value) => value !== null)
|
|
1490
1579
|
.map((value) => Credential.extractPaymentScheme(value))
|
|
1491
1580
|
.find((value) => value !== null);
|
|
1492
1581
|
if (paymentHeader) {
|
|
@@ -1525,6 +1614,24 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1525
1614
|
// Unrecognized or malformed credentials still use the currently eligible
|
|
1526
1615
|
// offer set before the first handler issues its rejection challenge.
|
|
1527
1616
|
}
|
|
1617
|
+
const transportResults = new Map();
|
|
1618
|
+
for (const handler of handlers) {
|
|
1619
|
+
for (const internal of getConfiguredOffers(handler)) {
|
|
1620
|
+
const matchCredential = internal._transport.matchCredential;
|
|
1621
|
+
if (!matchCredential)
|
|
1622
|
+
continue;
|
|
1623
|
+
try {
|
|
1624
|
+
if (await matchCredential({ input, request: internal._canonicalRequest })) {
|
|
1625
|
+
const result = await handler(input);
|
|
1626
|
+
transportResults.set(handler, result);
|
|
1627
|
+
if (result.status === 200)
|
|
1628
|
+
return result;
|
|
1629
|
+
break;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
catch { }
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1528
1635
|
const selectedHandlers = hasOfferPolicy
|
|
1529
1636
|
? await selectOfferHandlers({
|
|
1530
1637
|
entries: offerEntries,
|
|
@@ -1532,13 +1639,23 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1532
1639
|
selectOffers: offerSelector,
|
|
1533
1640
|
})
|
|
1534
1641
|
: handlers;
|
|
1642
|
+
const transportCredentialHandler = selectedHandlers.find((handler) => getConfiguredOffers(handler).some((internal) => internal.credentialHeaders?.some((header) => {
|
|
1643
|
+
const value = input.headers.get(header);
|
|
1644
|
+
return (header.toLowerCase() !== Constants.Headers.authorization.toLowerCase() &&
|
|
1645
|
+
value !== null &&
|
|
1646
|
+
Credential.extractPaymentScheme(value) === null);
|
|
1647
|
+
})));
|
|
1648
|
+
if (transportCredentialHandler)
|
|
1649
|
+
return transportCredentialHandler(input);
|
|
1535
1650
|
// No credential — evaluate handlers sequentially so authorize()/renewal hooks
|
|
1536
1651
|
// can safely claim the request without racing each other.
|
|
1537
1652
|
const results = [];
|
|
1538
1653
|
for (const handler of selectedHandlers) {
|
|
1539
|
-
const result = await handler(input);
|
|
1654
|
+
const result = transportResults.get(handler) ?? (await handler(input));
|
|
1540
1655
|
if (result.status === 200)
|
|
1541
1656
|
return result;
|
|
1657
|
+
if (result.challenge.status !== 402)
|
|
1658
|
+
return result;
|
|
1542
1659
|
results.push(result);
|
|
1543
1660
|
}
|
|
1544
1661
|
const challengeEntries = (() => {
|
|
@@ -1655,7 +1772,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1655
1772
|
};
|
|
1656
1773
|
const offers = handlers.flatMap(getConfiguredOffers);
|
|
1657
1774
|
if (offers.length > 0)
|
|
1658
|
-
composed._internal = { offers };
|
|
1775
|
+
composed._internal = { offers, handlers: handlers, selectOffers };
|
|
1659
1776
|
return composed;
|
|
1660
1777
|
}
|
|
1661
1778
|
/** Returns the flattened configured offers accepted by a handler. */
|
|
@@ -1669,7 +1786,7 @@ function getConfiguredOffers(handler) {
|
|
|
1669
1786
|
}
|
|
1670
1787
|
/** Dispatches one MCP credential or gathers unsigned offers in their configured order. */
|
|
1671
1788
|
function composeMcpHandlers(handlers, transport) {
|
|
1672
|
-
|
|
1789
|
+
const composed = async (input) => {
|
|
1673
1790
|
let credential;
|
|
1674
1791
|
try {
|
|
1675
1792
|
const parsed = transport.getCredential(input);
|
|
@@ -1680,13 +1797,9 @@ function composeMcpHandlers(handlers, transport) {
|
|
|
1680
1797
|
return handlers[0](input);
|
|
1681
1798
|
}
|
|
1682
1799
|
if (credential) {
|
|
1683
|
-
const candidates = handlers.filter((handler) =>
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
internal.intent === credential.challenge.intent);
|
|
1687
|
-
});
|
|
1688
|
-
const match = candidates.find((handler) => {
|
|
1689
|
-
const internal = handler._internal;
|
|
1800
|
+
const candidates = handlers.filter((handler) => getConfiguredOffers(handler).some((internal) => internal.name === credential.challenge.method &&
|
|
1801
|
+
internal.intent === credential.challenge.intent));
|
|
1802
|
+
const match = candidates.find((handler) => getConfiguredOffers(handler).some((internal) => {
|
|
1690
1803
|
try {
|
|
1691
1804
|
const mismatch = internal._stableBinding
|
|
1692
1805
|
? getRequestBindingMismatch(getStableBinding(internal._canonicalRequest, internal._stableBinding), getStableBinding(credential.challenge.request, internal._stableBinding))
|
|
@@ -1696,7 +1809,7 @@ function composeMcpHandlers(handlers, transport) {
|
|
|
1696
1809
|
catch {
|
|
1697
1810
|
return false;
|
|
1698
1811
|
}
|
|
1699
|
-
});
|
|
1812
|
+
}));
|
|
1700
1813
|
// Verification and settlement belong to exactly one handler, including failures.
|
|
1701
1814
|
return (match ?? candidates[0] ?? handlers[0])(input);
|
|
1702
1815
|
}
|
|
@@ -1738,6 +1851,10 @@ function composeMcpHandlers(handlers, transport) {
|
|
|
1738
1851
|
},
|
|
1739
1852
|
};
|
|
1740
1853
|
};
|
|
1854
|
+
composed._internal = {
|
|
1855
|
+
offers: handlers.flatMap((handler) => getConfiguredOffers(handler)),
|
|
1856
|
+
};
|
|
1857
|
+
return composed;
|
|
1741
1858
|
}
|
|
1742
1859
|
function isComposedHandlerMetadata(internal) {
|
|
1743
1860
|
return 'offers' in internal && !('_canonicalRequest' in internal);
|
|
@@ -1849,52 +1966,47 @@ function mergeX402PaymentRequiredHeaders(values) {
|
|
|
1849
1966
|
}),
|
|
1850
1967
|
];
|
|
1851
1968
|
}
|
|
1852
|
-
|
|
1853
|
-
* Wraps a payment handler to create a Node.js HTTP listener.
|
|
1854
|
-
*
|
|
1855
|
-
* On 402: writes the challenge response and ends the connection.
|
|
1856
|
-
* On 200: sets the Payment-Receipt header; caller should write response body.
|
|
1857
|
-
*
|
|
1858
|
-
* @example
|
|
1859
|
-
* ```ts
|
|
1860
|
-
* import * as http from 'node:http'
|
|
1861
|
-
* import { Mppx } from 'mppx/server'
|
|
1862
|
-
*
|
|
1863
|
-
* const payment = Mppx.create({ ... })
|
|
1864
|
-
*
|
|
1865
|
-
* http.createServer(async (req, res) => {
|
|
1866
|
-
* const result = await Mppx.toNodeListener(
|
|
1867
|
-
* payment.charge({
|
|
1868
|
-
* amount: '1', currency: '...', recipient: '0x...',
|
|
1869
|
-
* }),
|
|
1870
|
-
* )(req, res)
|
|
1871
|
-
* if (result.status === 402) return
|
|
1872
|
-
* res.end('OK')
|
|
1873
|
-
* })
|
|
1874
|
-
* ```
|
|
1875
|
-
*/
|
|
1876
|
-
export function toNodeListener(handler) {
|
|
1969
|
+
export function toNodeListener(handler, options) {
|
|
1877
1970
|
return async (req, res) => {
|
|
1878
|
-
|
|
1971
|
+
let result;
|
|
1972
|
+
try {
|
|
1973
|
+
const request = Request.fromNodeListener(req, res, options);
|
|
1974
|
+
await Request.waitForBody(request);
|
|
1975
|
+
result = await handler(request);
|
|
1976
|
+
}
|
|
1977
|
+
catch (error) {
|
|
1978
|
+
if (!(error instanceof Request.RequestBodyTooLargeError))
|
|
1979
|
+
throw error;
|
|
1980
|
+
const response = new globalThis.Response('Payload Too Large', {
|
|
1981
|
+
status: 413,
|
|
1982
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
1983
|
+
});
|
|
1984
|
+
await NodeListener.sendResponse(res, response);
|
|
1985
|
+
return { challenge: response, status: 402 };
|
|
1986
|
+
}
|
|
1879
1987
|
if (result.status === 402) {
|
|
1880
1988
|
await NodeListener.sendResponse(res, result.challenge);
|
|
1881
1989
|
}
|
|
1882
1990
|
else {
|
|
1883
|
-
const managementResponse = getManagementResponse(result);
|
|
1991
|
+
const managementResponse = await getManagementResponse(result);
|
|
1884
1992
|
if (managementResponse) {
|
|
1885
1993
|
await NodeListener.sendResponse(res, managementResponse);
|
|
1886
1994
|
return { challenge: managementResponse, status: 402 };
|
|
1887
1995
|
}
|
|
1888
|
-
const wrapped = result.withReceipt(new globalThis.Response());
|
|
1996
|
+
const wrapped = await result.withReceipt(new globalThis.Response(null, { status: 204 }));
|
|
1997
|
+
if (wrapped.status === 402) {
|
|
1998
|
+
await NodeListener.sendResponse(res, wrapped);
|
|
1999
|
+
return { challenge: wrapped, status: 402 };
|
|
2000
|
+
}
|
|
1889
2001
|
for (const [name, value] of wrapped.headers)
|
|
1890
2002
|
res.setHeader(name, value);
|
|
1891
2003
|
}
|
|
1892
2004
|
return result;
|
|
1893
2005
|
};
|
|
1894
2006
|
}
|
|
1895
|
-
function getManagementResponse(result) {
|
|
2007
|
+
async function getManagementResponse(result) {
|
|
1896
2008
|
try {
|
|
1897
|
-
return result.withReceipt();
|
|
2009
|
+
return await result.withReceipt();
|
|
1898
2010
|
}
|
|
1899
2011
|
catch (error) {
|
|
1900
2012
|
if (isMissingReceiptResponseError(error)) {
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Useful when bridging Fetch API handlers with Node.js HTTP servers.
|
|
5
5
|
*/
|
|
6
6
|
export async function sendResponse(res, response) {
|
|
7
|
+
if (isResponseClosed(res)) {
|
|
8
|
+
await response.body?.cancel('response connection closed').catch(() => { });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
7
11
|
const headers = {};
|
|
8
12
|
for (const [key, value] of response.headers) {
|
|
9
13
|
if (key in headers) {
|
|
@@ -23,21 +27,82 @@ export async function sendResponse(res, response) {
|
|
|
23
27
|
res.writeHead(response.status, headers);
|
|
24
28
|
if (response.body != null && res.req?.method !== 'HEAD') {
|
|
25
29
|
const reader = response.body.getReader();
|
|
30
|
+
let connectionClosed = false;
|
|
31
|
+
const onClose = () => {
|
|
32
|
+
connectionClosed = true;
|
|
33
|
+
void reader.cancel('response connection closed').catch(() => { });
|
|
34
|
+
};
|
|
35
|
+
res.once('close', onClose);
|
|
36
|
+
if (isResponseClosed(res))
|
|
37
|
+
onClose();
|
|
26
38
|
try {
|
|
39
|
+
if (res.destroyed || res.writableEnded) {
|
|
40
|
+
await reader.cancel('response connection closed');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
27
43
|
while (true) {
|
|
28
44
|
const { done, value } = await reader.read();
|
|
29
|
-
if (done)
|
|
45
|
+
if (done || connectionClosed)
|
|
30
46
|
break;
|
|
31
|
-
if (res.write(value) === false)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
if (res.write(value) === false) {
|
|
48
|
+
try {
|
|
49
|
+
if (!(await waitForDrain(res))) {
|
|
50
|
+
await reader.cancel('response connection closed');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
await reader.cancel(error).catch(() => { });
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
35
59
|
}
|
|
36
60
|
}
|
|
37
61
|
finally {
|
|
62
|
+
res.removeListener('close', onClose);
|
|
38
63
|
reader.releaseLock();
|
|
39
64
|
}
|
|
40
65
|
}
|
|
41
|
-
res
|
|
66
|
+
if (!isResponseClosed(res))
|
|
67
|
+
res.end();
|
|
68
|
+
}
|
|
69
|
+
/** Returns whether the HTTP/1 response or underlying HTTP/2 stream has closed. */
|
|
70
|
+
function isResponseClosed(res) {
|
|
71
|
+
const stream = 'stream' in res ? res.stream : undefined;
|
|
72
|
+
return Boolean(res.destroyed || res.writableEnded || stream?.destroyed || stream?.closed);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Waits for write backpressure to clear.
|
|
76
|
+
*
|
|
77
|
+
* Returns `true` on drain, `false` when the response closes, and rejects on a
|
|
78
|
+
* write-side error. Event listeners are always removed before settlement.
|
|
79
|
+
*/
|
|
80
|
+
function waitForDrain(res) {
|
|
81
|
+
if (isResponseClosed(res))
|
|
82
|
+
return Promise.resolve(false);
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const cleanup = () => {
|
|
85
|
+
res.removeListener('drain', onDrain);
|
|
86
|
+
res.removeListener('close', onClose);
|
|
87
|
+
res.removeListener('error', onError);
|
|
88
|
+
};
|
|
89
|
+
const onDrain = () => {
|
|
90
|
+
cleanup();
|
|
91
|
+
resolve(true);
|
|
92
|
+
};
|
|
93
|
+
const onClose = () => {
|
|
94
|
+
cleanup();
|
|
95
|
+
resolve(false);
|
|
96
|
+
};
|
|
97
|
+
const onError = (error) => {
|
|
98
|
+
cleanup();
|
|
99
|
+
reject(error);
|
|
100
|
+
};
|
|
101
|
+
res.once('drain', onDrain);
|
|
102
|
+
res.once('close', onClose);
|
|
103
|
+
res.once('error', onError);
|
|
104
|
+
if (isResponseClosed(res))
|
|
105
|
+
onClose();
|
|
106
|
+
});
|
|
42
107
|
}
|
|
43
108
|
//# sourceMappingURL=NodeListener.js.map
|
package/dist/server/Request.d.ts
CHANGED
|
@@ -2,9 +2,17 @@ import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http
|
|
|
2
2
|
export type FetchHandler = (request: Request) => Promise<Response> | Response;
|
|
3
3
|
export type RequestListenerOptions = {
|
|
4
4
|
host?: string | undefined;
|
|
5
|
+
/** Maximum accepted request body size in bytes. @default 10485760 (10 MiB) */
|
|
6
|
+
maxBodySize?: number | undefined;
|
|
5
7
|
onError?: ((error: unknown) => void | Response | Promise<void | Response>) | undefined;
|
|
6
8
|
protocol?: string | undefined;
|
|
7
9
|
};
|
|
10
|
+
/** Options used only while converting a Node request to a Fetch request. */
|
|
11
|
+
export type NodeConversionOptions = Omit<RequestListenerOptions, 'onError'>;
|
|
12
|
+
/** Raised when a Node request body exceeds the configured byte limit. */
|
|
13
|
+
export declare class RequestBodyTooLargeError extends Error {
|
|
14
|
+
readonly name = "RequestBodyTooLargeError";
|
|
15
|
+
}
|
|
8
16
|
/**
|
|
9
17
|
* Converts a Fetch API handler into a Node.js HTTP request listener.
|
|
10
18
|
*
|
|
@@ -20,5 +28,7 @@ export declare function toNodeListener(handler: FetchHandler, options?: RequestL
|
|
|
20
28
|
* @param res - The Node.js ServerResponse (used for abort signal lifecycle).
|
|
21
29
|
* @returns A Fetch API Request.
|
|
22
30
|
*/
|
|
23
|
-
export declare function fromNodeListener(req: IncomingMessage, res: ServerResponse, options?:
|
|
31
|
+
export declare function fromNodeListener(req: IncomingMessage, res: ServerResponse, options?: NodeConversionOptions | undefined): Request;
|
|
32
|
+
/** Waits for a Node-backed request body to finish and enforces its size limit. */
|
|
33
|
+
export declare function waitForBody(request: Request): Promise<void>;
|
|
24
34
|
//# sourceMappingURL=Request.d.ts.map
|