mppx 0.12.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 +55 -0
- 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/cli.d.ts +8 -0
- 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 +34 -12
- 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/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 +13 -1
- package/dist/server/Mppx.js +85 -44
- 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/stripe/server/Methods.d.ts +10 -3
- package/dist/stripe/server/Methods.js +17 -2
- 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/fee-payer.d.ts +6 -3
- package/dist/tempo/internal/fee-payer.js +23 -6
- 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.js +75 -46
- package/dist/tempo/server/Methods.d.ts +55 -10
- package/dist/tempo/server/Subscription.d.ts +1 -1
- package/dist/tempo/server/Subscription.js +31 -4
- 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 +20 -2
- package/dist/tempo/session/server/ChannelStore.js +25 -4
- package/dist/tempo/session/server/CredentialVerification.d.ts +4 -1
- package/dist/tempo/session/server/CredentialVerification.js +34 -6
- package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
- package/dist/tempo/session/server/MeteredStream.js +85 -29
- package/dist/tempo/session/server/Session.d.ts +2 -1
- package/dist/tempo/session/server/Session.js +2 -1
- package/dist/tempo/session/server/Settlement.d.ts +21 -0
- package/dist/tempo/session/server/Settlement.js +92 -17
- 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/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 +6 -6
- 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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Elysia } from 'elysia';
|
|
2
|
+
import { mapResponse as mapElysiaResponse } from 'elysia/adapter/web-standard/handler';
|
|
2
3
|
import { generate } from '../discovery/OpenApi.js';
|
|
4
|
+
import * as Scope from '../server/internal/scope.js';
|
|
3
5
|
import * as Mppx_core from '../server/Mppx.js';
|
|
4
6
|
import * as Mppx_internal from './internal/mppx.js';
|
|
5
7
|
export * from '../server/Methods.js';
|
|
@@ -7,10 +9,10 @@ export var Mppx;
|
|
|
7
9
|
(function (Mppx) {
|
|
8
10
|
/**
|
|
9
11
|
* Creates an Elysia-aware payment handler where each intent
|
|
10
|
-
* returns
|
|
12
|
+
* returns Elysia lifecycle hooks.
|
|
11
13
|
*
|
|
12
|
-
* Use with `.guard()`
|
|
13
|
-
*
|
|
14
|
+
* Use with `.guard()` so both verification and response wrapping are scoped
|
|
15
|
+
* to the same routes.
|
|
14
16
|
*
|
|
15
17
|
* @example
|
|
16
18
|
* ```ts
|
|
@@ -21,7 +23,7 @@ export var Mppx;
|
|
|
21
23
|
*
|
|
22
24
|
* const app = new Elysia()
|
|
23
25
|
* .guard(
|
|
24
|
-
*
|
|
26
|
+
* mppx.charge({ amount: '1' }),
|
|
25
27
|
* (app) => app.get('/premium', () => ({ data: 'paid content' })),
|
|
26
28
|
* )
|
|
27
29
|
* ```
|
|
@@ -32,7 +34,7 @@ export var Mppx;
|
|
|
32
34
|
Mppx.create = create;
|
|
33
35
|
})(Mppx || (Mppx = {}));
|
|
34
36
|
/**
|
|
35
|
-
* Elysia
|
|
37
|
+
* Elysia lifecycle hooks that gate a route behind a payment intent.
|
|
36
38
|
*
|
|
37
39
|
* Returns a 402 challenge if no valid credential is provided.
|
|
38
40
|
*
|
|
@@ -46,27 +48,78 @@ export var Mppx;
|
|
|
46
48
|
*
|
|
47
49
|
* const app = new Elysia()
|
|
48
50
|
* .guard(
|
|
49
|
-
*
|
|
51
|
+
* payment(mppx.charge, { amount: '1' }),
|
|
50
52
|
* (app) => app.get('/premium', () => ({ data: 'paid content' })),
|
|
51
53
|
* )
|
|
52
54
|
* ```
|
|
53
55
|
*/
|
|
54
56
|
export function payment(intent, options) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const pending = new WeakMap();
|
|
58
|
+
const runBeforeHandle = async ({ request, route, set }, legacyRegistration) => {
|
|
59
|
+
const scopedRequest = options.scope === undefined && Scope.read(options.meta) === undefined
|
|
60
|
+
? Scope.attach(request, `${request.method.toUpperCase()} ${route || new URL(request.url).pathname}`)
|
|
61
|
+
: request;
|
|
62
|
+
const result = await intent(options)(scopedRequest);
|
|
57
63
|
if (result.status === 402)
|
|
58
64
|
return result.challenge;
|
|
59
|
-
const managementResponse = getManagementResponse(result);
|
|
65
|
+
const managementResponse = await getManagementResponse(result);
|
|
60
66
|
if (managementResponse)
|
|
61
67
|
return managementResponse;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
const supportsStreamingReceipts = Mppx_core.supportsStreamingReceipts(result);
|
|
69
|
+
if (legacyRegistration) {
|
|
70
|
+
if (supportsStreamingReceipts)
|
|
71
|
+
return new Response('Streaming payment hooks require paired beforeHandle and afterHandle registration.', { status: 500 });
|
|
72
|
+
const receiptResponse = await result.withReceipt(new Response(null, { status: 204 }));
|
|
73
|
+
if (!receiptResponse.ok)
|
|
74
|
+
return receiptResponse;
|
|
75
|
+
copyHeaders(receiptResponse, set.headers);
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
pending.set(request, { result, supportsStreamingReceipts });
|
|
79
|
+
return undefined;
|
|
80
|
+
};
|
|
81
|
+
const beforeHandle = (context) => runBeforeHandle(context, false);
|
|
82
|
+
const legacyBeforeHandle = (context) => runBeforeHandle(context, true);
|
|
83
|
+
const afterHandle = async ({ request, responseValue, set }) => {
|
|
84
|
+
const entry = pending.get(request);
|
|
85
|
+
if (!entry)
|
|
86
|
+
return;
|
|
87
|
+
const { result, supportsStreamingReceipts } = entry;
|
|
88
|
+
if (isAsyncIterableResponse(responseValue)) {
|
|
89
|
+
if (supportsStreamingReceipts) {
|
|
90
|
+
const response = await result.withReceipt(responseValue);
|
|
91
|
+
pending.delete(request);
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
const receiptResponse = await result.withReceipt(new Response(null, { status: 204 }));
|
|
95
|
+
pending.delete(request);
|
|
96
|
+
if (!receiptResponse.ok)
|
|
97
|
+
return receiptResponse;
|
|
98
|
+
copyHeaders(receiptResponse, set.headers);
|
|
99
|
+
return responseValue;
|
|
100
|
+
}
|
|
101
|
+
const response = await mapElysiaResponse(responseValue, set, request);
|
|
102
|
+
const wrapped = await result.withReceipt(response);
|
|
103
|
+
pending.delete(request);
|
|
104
|
+
return wrapped;
|
|
65
105
|
};
|
|
106
|
+
return Object.assign(legacyBeforeHandle, { afterHandle, beforeHandle });
|
|
107
|
+
}
|
|
108
|
+
function isAsyncIterableResponse(response) {
|
|
109
|
+
if (typeof response === 'function')
|
|
110
|
+
return true;
|
|
111
|
+
return (typeof response === 'object' &&
|
|
112
|
+
response !== null &&
|
|
113
|
+
Symbol.asyncIterator in response &&
|
|
114
|
+
typeof response[Symbol.asyncIterator] === 'function');
|
|
115
|
+
}
|
|
116
|
+
function copyHeaders(response, headers) {
|
|
117
|
+
for (const [key, value] of response.headers)
|
|
118
|
+
headers[key] = value;
|
|
66
119
|
}
|
|
67
|
-
function getManagementResponse(result) {
|
|
120
|
+
async function getManagementResponse(result) {
|
|
68
121
|
try {
|
|
69
|
-
return result.withReceipt();
|
|
122
|
+
return await result.withReceipt();
|
|
70
123
|
}
|
|
71
124
|
catch (error) {
|
|
72
125
|
if (Mppx_core.isMissingReceiptResponseError(error)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generate } from '../discovery/OpenApi.js';
|
|
2
|
+
import * as Scope from '../server/internal/scope.js';
|
|
2
3
|
import * as Mppx_core from '../server/Mppx.js';
|
|
3
4
|
import * as ExpressAdapter from './internal/express.js';
|
|
4
5
|
import * as Mppx_internal from './internal/mppx.js';
|
|
@@ -49,16 +50,20 @@ export var Mppx;
|
|
|
49
50
|
*/
|
|
50
51
|
export function payment(intent, options) {
|
|
51
52
|
return async (req, res, next) => {
|
|
52
|
-
const
|
|
53
|
+
const rawRequest = ExpressAdapter.toRequest(req);
|
|
54
|
+
const routePath = typeof req.route?.path === 'string' ? req.route.path : req.path;
|
|
55
|
+
const request = options.scope === undefined && Scope.read(options.meta) === undefined
|
|
56
|
+
? Scope.attach(rawRequest, `${req.method.toUpperCase()} ${req.baseUrl}${routePath}`)
|
|
57
|
+
: rawRequest;
|
|
53
58
|
const result = await intent(options)(request);
|
|
54
59
|
if (result.status === 402) {
|
|
55
60
|
const challenge = result.challenge;
|
|
56
61
|
await ExpressAdapter.sendResponse(res, challenge);
|
|
57
62
|
return;
|
|
58
63
|
}
|
|
59
|
-
const managementResponse = (() => {
|
|
64
|
+
const managementResponse = await (async () => {
|
|
60
65
|
try {
|
|
61
|
-
return result.withReceipt();
|
|
66
|
+
return await result.withReceipt();
|
|
62
67
|
}
|
|
63
68
|
catch (error) {
|
|
64
69
|
if (Mppx_core.isMissingReceiptResponseError(error))
|
|
@@ -78,9 +83,11 @@ export function payment(intent, options) {
|
|
|
78
83
|
}
|
|
79
84
|
const originalJson = res.json.bind(res);
|
|
80
85
|
res.json = (body) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
void Promise.resolve(result.withReceipt(Response.json(body))).then((wrapped) => {
|
|
87
|
+
ExpressAdapter.copyHeaders(res, wrapped.headers);
|
|
88
|
+
originalJson(body);
|
|
89
|
+
}, next);
|
|
90
|
+
return res;
|
|
84
91
|
};
|
|
85
92
|
next();
|
|
86
93
|
};
|
package/dist/middlewares/hono.js
CHANGED
|
@@ -55,16 +55,16 @@ export function payment(intent, options) {
|
|
|
55
55
|
const result = await intent(options)(request);
|
|
56
56
|
if (result.status === 402)
|
|
57
57
|
return result.challenge;
|
|
58
|
-
const managementResponse = getManagementResponse(result);
|
|
58
|
+
const managementResponse = await getManagementResponse(result);
|
|
59
59
|
if (managementResponse)
|
|
60
60
|
return managementResponse;
|
|
61
61
|
await next();
|
|
62
|
-
c.res = result.withReceipt(c.res);
|
|
62
|
+
c.res = await result.withReceipt(c.res);
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
function getManagementResponse(result) {
|
|
65
|
+
async function getManagementResponse(result) {
|
|
66
66
|
try {
|
|
67
|
-
return result.withReceipt();
|
|
67
|
+
return await result.withReceipt();
|
|
68
68
|
}
|
|
69
69
|
catch (error) {
|
|
70
70
|
if (Mppx_core.isMissingReceiptResponseError(error)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generate } from '../discovery/OpenApi.js';
|
|
2
|
+
import * as Scope from '../server/internal/scope.js';
|
|
2
3
|
import * as Mppx_core from '../server/Mppx.js';
|
|
3
4
|
import * as Mppx_internal from './internal/mppx.js';
|
|
4
5
|
export * from '../server/Methods.js';
|
|
@@ -48,19 +49,22 @@ export var Mppx;
|
|
|
48
49
|
*/
|
|
49
50
|
export function payment(intent, options, handler) {
|
|
50
51
|
return async (request) => {
|
|
51
|
-
const
|
|
52
|
+
const scopedRequest = options.scope === undefined && Scope.read(options.meta) === undefined
|
|
53
|
+
? Scope.attach(request, `${request.method.toUpperCase()} ${new URL(request.url).pathname}`)
|
|
54
|
+
: request;
|
|
55
|
+
const result = await intent(options)(scopedRequest);
|
|
52
56
|
if (result.status === 402)
|
|
53
57
|
return result.challenge;
|
|
54
|
-
const managementResponse = getManagementResponse(result);
|
|
58
|
+
const managementResponse = await getManagementResponse(result);
|
|
55
59
|
if (managementResponse)
|
|
56
60
|
return managementResponse;
|
|
57
61
|
const response = await handler(request);
|
|
58
|
-
return result.withReceipt(response);
|
|
62
|
+
return await result.withReceipt(response);
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
|
-
function getManagementResponse(result) {
|
|
65
|
+
async function getManagementResponse(result) {
|
|
62
66
|
try {
|
|
63
|
-
return result.withReceipt();
|
|
67
|
+
return await result.withReceipt();
|
|
64
68
|
}
|
|
65
69
|
catch (error) {
|
|
66
70
|
if (Mppx_core.isMissingReceiptResponseError(error)) {
|
|
@@ -12,9 +12,12 @@ const hopByHopHeaders = new Set([
|
|
|
12
12
|
const paymentHeaders = new Set([
|
|
13
13
|
'accept-payment',
|
|
14
14
|
'authorization',
|
|
15
|
+
'payment-authorization',
|
|
15
16
|
'payment-receipt',
|
|
16
17
|
'payment-required',
|
|
17
18
|
'payment-response',
|
|
19
|
+
'payment-session',
|
|
20
|
+
'payment-session-snapshot',
|
|
18
21
|
'payment-signature',
|
|
19
22
|
'www-authenticate',
|
|
20
23
|
]);
|
|
@@ -57,10 +60,72 @@ export function scrubResponse(response) {
|
|
|
57
60
|
headers.delete('content-encoding');
|
|
58
61
|
headers.delete('content-length');
|
|
59
62
|
headers.delete('set-cookie');
|
|
63
|
+
for (const name of paymentHeaders) {
|
|
64
|
+
if (name !== 'authorization' && name !== 'www-authenticate')
|
|
65
|
+
headers.delete(name);
|
|
66
|
+
}
|
|
67
|
+
headers.delete('payment-authorization');
|
|
68
|
+
const authorization = headers.get('authorization');
|
|
69
|
+
if (authorization && /^\s*Payment(?:\s|$)/i.test(authorization))
|
|
70
|
+
headers.delete('authorization');
|
|
71
|
+
const authenticate = headers.get('www-authenticate');
|
|
72
|
+
if (authenticate) {
|
|
73
|
+
const remaining = removePaymentChallenges(authenticate);
|
|
74
|
+
if (remaining)
|
|
75
|
+
headers.set('www-authenticate', remaining);
|
|
76
|
+
else
|
|
77
|
+
headers.delete('www-authenticate');
|
|
78
|
+
}
|
|
60
79
|
return new Response(response.body, {
|
|
61
80
|
status: response.status,
|
|
62
81
|
statusText: response.statusText,
|
|
63
82
|
headers,
|
|
64
83
|
});
|
|
65
84
|
}
|
|
85
|
+
/** Removes Payment authentication challenges while preserving other schemes. */
|
|
86
|
+
function removePaymentChallenges(value) {
|
|
87
|
+
const starts = [];
|
|
88
|
+
let inQuotes = false;
|
|
89
|
+
let escaped = false;
|
|
90
|
+
for (let index = 0; index < value.length; index++) {
|
|
91
|
+
const character = value[index];
|
|
92
|
+
if (inQuotes) {
|
|
93
|
+
if (escaped)
|
|
94
|
+
escaped = false;
|
|
95
|
+
else if (character === '\\')
|
|
96
|
+
escaped = true;
|
|
97
|
+
else if (character === '"')
|
|
98
|
+
inQuotes = false;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (character === '"') {
|
|
102
|
+
inQuotes = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const previous = value.slice(0, index).trimEnd().at(-1);
|
|
106
|
+
if (index !== 0 && previous !== ',')
|
|
107
|
+
continue;
|
|
108
|
+
const match = value.slice(index).match(/^\s*([!#$%&'*+.^_`|~0-9A-Za-z-]+)(?=\s|,|$)/);
|
|
109
|
+
if (!match)
|
|
110
|
+
continue;
|
|
111
|
+
const start = index + match[0].length - match[0].trimStart().length;
|
|
112
|
+
const afterScheme = start + match[1].length;
|
|
113
|
+
let next = afterScheme;
|
|
114
|
+
while (/\s/.test(value[next] ?? ''))
|
|
115
|
+
next++;
|
|
116
|
+
if (value[next] === '=')
|
|
117
|
+
continue;
|
|
118
|
+
starts.push({ index: start, scheme: match[1].toLowerCase() });
|
|
119
|
+
index = afterScheme - 1;
|
|
120
|
+
}
|
|
121
|
+
return starts
|
|
122
|
+
.filter(({ scheme }) => scheme !== 'payment')
|
|
123
|
+
.map(({ index }) => {
|
|
124
|
+
const originalPosition = starts.findIndex((start) => start.index === index);
|
|
125
|
+
const end = starts[originalPosition + 1]?.index ?? value.length;
|
|
126
|
+
return value.slice(index, end).replace(/,\s*$/, '').trim();
|
|
127
|
+
})
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
.join(', ');
|
|
130
|
+
}
|
|
66
131
|
//# sourceMappingURL=Headers.js.map
|
|
@@ -30,6 +30,7 @@ export function stripe(config) {
|
|
|
30
30
|
rewriteRequest(request, ctx) {
|
|
31
31
|
const apiKey = ctx.apiKey ?? config.apiKey;
|
|
32
32
|
request.headers.delete('Stripe-Account');
|
|
33
|
+
request.headers.delete('Stripe-Context');
|
|
33
34
|
request.headers.set('Authorization', `Basic ${btoa(`${apiKey}:`)}`);
|
|
34
35
|
return request;
|
|
35
36
|
},
|
package/dist/server/Mppx.d.ts
CHANGED
|
@@ -8,8 +8,14 @@ import * as Method from '../Method.js';
|
|
|
8
8
|
import type * as Receipt from '../Receipt.js';
|
|
9
9
|
import * as z from '../zod.js';
|
|
10
10
|
import * as Html from './internal/html/config.js';
|
|
11
|
+
import * as Request from './Request.js';
|
|
11
12
|
import * as Transport from './Transport.js';
|
|
12
13
|
export type Methods = readonly (Method.AnyServer | readonly Method.AnyServer[])[];
|
|
14
|
+
type ReceiptWrapper = (response?: unknown) => unknown;
|
|
15
|
+
/** Returns whether a successful payment result accepts streaming receipt responses. @internal */
|
|
16
|
+
export declare function supportsStreamingReceipts(result: {
|
|
17
|
+
withReceipt: ReceiptWrapper;
|
|
18
|
+
}): boolean;
|
|
13
19
|
export type ServerEventMap<methods extends readonly Method.Method[] = readonly Method.Method[], transport extends Transport.AnyTransport = Transport.AnyTransport> = {
|
|
14
20
|
'challenge.created': ChallengeContext<methods[number], transport>;
|
|
15
21
|
'payment.failed': PaymentFailedContext<methods[number], transport>;
|
|
@@ -374,10 +380,12 @@ type ConfiguredHandler<transport extends Transport.AnyTransport = Transport.Http
|
|
|
374
380
|
name: string;
|
|
375
381
|
intent: string;
|
|
376
382
|
html: Html.Options | undefined;
|
|
383
|
+
credentialHeaders?: readonly string[] | undefined;
|
|
377
384
|
meta?: Record<string, string> | undefined;
|
|
378
385
|
scope?: string | undefined;
|
|
379
386
|
_canonicalRequest: Record<string, unknown>;
|
|
380
387
|
_stableBinding?: Method.StableBindingFn<Method.Method> | undefined;
|
|
388
|
+
_transport: Transport.AnyTransport;
|
|
381
389
|
};
|
|
382
390
|
};
|
|
383
391
|
type ComposedHandler<transport extends Transport.AnyTransport = Transport.Http> = ((input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>) & {
|
|
@@ -413,6 +421,9 @@ export declare function compose(...args: readonly unknown[]): ComposedHandler;
|
|
|
413
421
|
* On 402: writes the challenge response and ends the connection.
|
|
414
422
|
* On 200: sets the Payment-Receipt header; caller should write response body.
|
|
415
423
|
*
|
|
424
|
+
* @param handler - Payment handler to adapt.
|
|
425
|
+
* @param options - Node request conversion and body-size options.
|
|
426
|
+
*
|
|
416
427
|
* @example
|
|
417
428
|
* ```ts
|
|
418
429
|
* import * as http from 'node:http'
|
|
@@ -431,7 +442,8 @@ export declare function compose(...args: readonly unknown[]): ComposedHandler;
|
|
|
431
442
|
* })
|
|
432
443
|
* ```
|
|
433
444
|
*/
|
|
434
|
-
export declare function toNodeListener(handler: (input: globalThis.Request) => Promise<MethodFn.Response<Transport.Http
|
|
445
|
+
export declare function toNodeListener(handler: (input: globalThis.Request) => Promise<MethodFn.Response<Transport.Http>>, options?: Request.NodeConversionOptions | undefined): (req: IncomingMessage, res: ServerResponse) => Promise<MethodFn.Response<Transport.Http>>;
|
|
446
|
+
export declare function toNodeListener(handler: (input: globalThis.Request) => Promise<MethodFn.Response<Transport.Sse>>, options?: Request.NodeConversionOptions | undefined): (req: IncomingMessage, res: ServerResponse) => Promise<MethodFn.Response<Transport.Sse>>;
|
|
435
447
|
/**
|
|
436
448
|
* Flattens a methods config tuple, preserving positional types.
|
|
437
449
|
* @internal
|
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',
|
|
@@ -306,6 +311,7 @@ export function create(config) {
|
|
|
306
311
|
return prepared.method.validate({
|
|
307
312
|
credential: prepared.parsedCredential,
|
|
308
313
|
envelope: prepared.envelope,
|
|
314
|
+
operation: 'validate',
|
|
309
315
|
request: prepared.request,
|
|
310
316
|
});
|
|
311
317
|
}
|
|
@@ -327,7 +333,12 @@ export function create(config) {
|
|
|
327
333
|
let receipt;
|
|
328
334
|
try {
|
|
329
335
|
if (mi.broadcast && mi.validate)
|
|
330
|
-
await mi.validate({
|
|
336
|
+
await mi.validate({
|
|
337
|
+
credential: parsedCredential,
|
|
338
|
+
envelope,
|
|
339
|
+
operation: 'broadcast',
|
|
340
|
+
request,
|
|
341
|
+
});
|
|
331
342
|
const broadcast = mi.broadcast ?? mi.verify;
|
|
332
343
|
receipt = await broadcast({ credential: parsedCredential, envelope, request });
|
|
333
344
|
}
|
|
@@ -429,8 +440,15 @@ function createMethodFn(parameters) {
|
|
|
429
440
|
name: method.name,
|
|
430
441
|
intent: method.intent,
|
|
431
442
|
html: method.html,
|
|
443
|
+
credentialHeaders: [
|
|
444
|
+
...new Set([
|
|
445
|
+
credentialHeader ?? Constants.Headers.authorization,
|
|
446
|
+
...(transport.credentialHeaders ?? []),
|
|
447
|
+
]),
|
|
448
|
+
],
|
|
432
449
|
_canonicalRequest: PaymentRequest.fromMethod(method, { ...defaults, ...rest }),
|
|
433
450
|
_stableBinding: stableBinding,
|
|
451
|
+
_transport: transport,
|
|
434
452
|
};
|
|
435
453
|
Object.defineProperty(internal, '_method', { value: method });
|
|
436
454
|
const handler = async (input) => {
|
|
@@ -608,7 +626,7 @@ function createMethodFn(parameters) {
|
|
|
608
626
|
}
|
|
609
627
|
const success = (receiptData, options = {}) => {
|
|
610
628
|
const { challengeId = challenge.id, credentialForReceipt = { challenge, payload: {} }, envelopeForReceipt, managementResponse, } = options;
|
|
611
|
-
|
|
629
|
+
const result = {
|
|
612
630
|
status: 200,
|
|
613
631
|
withReceipt(response) {
|
|
614
632
|
if (managementResponse) {
|
|
@@ -633,6 +651,9 @@ function createMethodFn(parameters) {
|
|
|
633
651
|
});
|
|
634
652
|
},
|
|
635
653
|
};
|
|
654
|
+
if (transport.supportsStreamingReceipts)
|
|
655
|
+
streamingReceiptWrappers.add(result.withReceipt);
|
|
656
|
+
return result;
|
|
636
657
|
};
|
|
637
658
|
// No credential provided—issue challenge
|
|
638
659
|
if (!credential) {
|
|
@@ -644,15 +665,6 @@ function createMethodFn(parameters) {
|
|
|
644
665
|
request: challenge.request,
|
|
645
666
|
});
|
|
646
667
|
if (authorized) {
|
|
647
|
-
await events.emit('payment.success', createPaymentSuccessContext({
|
|
648
|
-
capturedRequest,
|
|
649
|
-
challenge,
|
|
650
|
-
input,
|
|
651
|
-
method,
|
|
652
|
-
receipt: authorized.receipt,
|
|
653
|
-
request: parsedRequest,
|
|
654
|
-
requestInput: request,
|
|
655
|
-
}), method);
|
|
656
668
|
return success(authorized.receipt, {
|
|
657
669
|
managementResponse: authorized.response,
|
|
658
670
|
});
|
|
@@ -823,7 +835,12 @@ function createMethodFn(parameters) {
|
|
|
823
835
|
let receiptData;
|
|
824
836
|
try {
|
|
825
837
|
if (broadcast && validate)
|
|
826
|
-
await validate({
|
|
838
|
+
await validate({
|
|
839
|
+
credential: parsedCredential,
|
|
840
|
+
envelope,
|
|
841
|
+
operation: 'broadcast',
|
|
842
|
+
request,
|
|
843
|
+
});
|
|
827
844
|
const broadcastCredential = broadcast ?? verify;
|
|
828
845
|
receiptData = await broadcastCredential({
|
|
829
846
|
credential: parsedCredential,
|
|
@@ -1555,7 +1572,10 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1555
1572
|
// Try to extract a Payment credential to decide whether to dispatch or challenge.
|
|
1556
1573
|
// Only gate on the Payment scheme — other auth schemes (Bearer, Basic, etc.)
|
|
1557
1574
|
// should fall through to the merged-402 path so all offers are presented.
|
|
1558
|
-
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)
|
|
1559
1579
|
.map((value) => Credential.extractPaymentScheme(value))
|
|
1560
1580
|
.find((value) => value !== null);
|
|
1561
1581
|
if (paymentHeader) {
|
|
@@ -1594,6 +1614,24 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1594
1614
|
// Unrecognized or malformed credentials still use the currently eligible
|
|
1595
1615
|
// offer set before the first handler issues its rejection challenge.
|
|
1596
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
|
+
}
|
|
1597
1635
|
const selectedHandlers = hasOfferPolicy
|
|
1598
1636
|
? await selectOfferHandlers({
|
|
1599
1637
|
entries: offerEntries,
|
|
@@ -1601,11 +1639,19 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1601
1639
|
selectOffers: offerSelector,
|
|
1602
1640
|
})
|
|
1603
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);
|
|
1604
1650
|
// No credential — evaluate handlers sequentially so authorize()/renewal hooks
|
|
1605
1651
|
// can safely claim the request without racing each other.
|
|
1606
1652
|
const results = [];
|
|
1607
1653
|
for (const handler of selectedHandlers) {
|
|
1608
|
-
const result = await handler(input);
|
|
1654
|
+
const result = transportResults.get(handler) ?? (await handler(input));
|
|
1609
1655
|
if (result.status === 200)
|
|
1610
1656
|
return result;
|
|
1611
1657
|
if (result.challenge.status !== 402)
|
|
@@ -1920,52 +1966,47 @@ function mergeX402PaymentRequiredHeaders(values) {
|
|
|
1920
1966
|
}),
|
|
1921
1967
|
];
|
|
1922
1968
|
}
|
|
1923
|
-
|
|
1924
|
-
* Wraps a payment handler to create a Node.js HTTP listener.
|
|
1925
|
-
*
|
|
1926
|
-
* On 402: writes the challenge response and ends the connection.
|
|
1927
|
-
* On 200: sets the Payment-Receipt header; caller should write response body.
|
|
1928
|
-
*
|
|
1929
|
-
* @example
|
|
1930
|
-
* ```ts
|
|
1931
|
-
* import * as http from 'node:http'
|
|
1932
|
-
* import { Mppx } from 'mppx/server'
|
|
1933
|
-
*
|
|
1934
|
-
* const payment = Mppx.create({ ... })
|
|
1935
|
-
*
|
|
1936
|
-
* http.createServer(async (req, res) => {
|
|
1937
|
-
* const result = await Mppx.toNodeListener(
|
|
1938
|
-
* payment.charge({
|
|
1939
|
-
* amount: '1', currency: '...', recipient: '0x...',
|
|
1940
|
-
* }),
|
|
1941
|
-
* )(req, res)
|
|
1942
|
-
* if (result.status === 402) return
|
|
1943
|
-
* res.end('OK')
|
|
1944
|
-
* })
|
|
1945
|
-
* ```
|
|
1946
|
-
*/
|
|
1947
|
-
export function toNodeListener(handler) {
|
|
1969
|
+
export function toNodeListener(handler, options) {
|
|
1948
1970
|
return async (req, res) => {
|
|
1949
|
-
|
|
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
|
+
}
|
|
1950
1987
|
if (result.status === 402) {
|
|
1951
1988
|
await NodeListener.sendResponse(res, result.challenge);
|
|
1952
1989
|
}
|
|
1953
1990
|
else {
|
|
1954
|
-
const managementResponse = getManagementResponse(result);
|
|
1991
|
+
const managementResponse = await getManagementResponse(result);
|
|
1955
1992
|
if (managementResponse) {
|
|
1956
1993
|
await NodeListener.sendResponse(res, managementResponse);
|
|
1957
1994
|
return { challenge: managementResponse, status: 402 };
|
|
1958
1995
|
}
|
|
1959
|
-
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
|
+
}
|
|
1960
2001
|
for (const [name, value] of wrapped.headers)
|
|
1961
2002
|
res.setHeader(name, value);
|
|
1962
2003
|
}
|
|
1963
2004
|
return result;
|
|
1964
2005
|
};
|
|
1965
2006
|
}
|
|
1966
|
-
function getManagementResponse(result) {
|
|
2007
|
+
async function getManagementResponse(result) {
|
|
1967
2008
|
try {
|
|
1968
|
-
return result.withReceipt();
|
|
2009
|
+
return await result.withReceipt();
|
|
1969
2010
|
}
|
|
1970
2011
|
catch (error) {
|
|
1971
2012
|
if (isMissingReceiptResponseError(error)) {
|