@atxp/client 0.10.7 → 0.10.8
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/dist/_virtual/index10.js +2 -2
- package/dist/_virtual/index11.js +2 -2
- package/dist/_virtual/index17.js +2 -2
- package/dist/_virtual/index19.js +2 -2
- package/dist/_virtual/index20.js +2 -2
- package/dist/_virtual/index3.js +2 -2
- package/dist/_virtual/index4.js +2 -2
- package/dist/_virtual/index8.js +2 -2
- package/dist/_virtual/index9.js +2 -2
- package/dist/atxpClient.d.ts +1 -1
- package/dist/atxpClient.d.ts.map +1 -1
- package/dist/atxpFetcher.d.ts +18 -1
- package/dist/atxpFetcher.d.ts.map +1 -1
- package/dist/atxpFetcher.js +65 -2
- package/dist/atxpFetcher.js.map +1 -1
- package/dist/atxpProtocolHandler.d.ts +17 -0
- package/dist/atxpProtocolHandler.d.ts.map +1 -0
- package/dist/atxpProtocolHandler.js +77 -0
- package/dist/atxpProtocolHandler.js.map +1 -0
- package/dist/destinationMakers/index.d.ts.map +1 -1
- package/dist/destinationMakers/index.js +6 -0
- package/dist/destinationMakers/index.js.map +1 -1
- package/dist/index.cjs +558 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +183 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +555 -4
- package/dist/index.js.map +1 -1
- package/dist/mppProtocolHandler.d.ts +46 -0
- package/dist/mppProtocolHandler.d.ts.map +1 -0
- package/dist/mppProtocolHandler.js +182 -0
- package/dist/mppProtocolHandler.js.map +1 -0
- package/dist/node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/compile/validate/index.js +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/applicator/index.js +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/core/index.js +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/discriminator/index.js +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/format/index.js +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/node_modules/ajv/dist/vocabularies/validation/index.js +1 -1
- package/dist/node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/applicator/index.js +1 -1
- package/dist/node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/core/index.js +1 -1
- package/dist/node_modules/ajv-formats/node_modules/ajv/dist/vocabularies/format/index.js +1 -1
- package/dist/paymentClient.d.ts +55 -0
- package/dist/paymentClient.d.ts.map +1 -0
- package/dist/paymentClient.js +75 -0
- package/dist/paymentClient.js.map +1 -0
- package/dist/protocolHandler.d.ts +41 -0
- package/dist/protocolHandler.d.ts.map +1 -0
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/x402ProtocolHandler.d.ts +22 -0
- package/dist/x402ProtocolHandler.d.ts.map +1 -0
- package/dist/x402ProtocolHandler.js +166 -0
- package/dist/x402ProtocolHandler.js.map +1 -0
- package/package.json +5 -3
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
var common = require('@atxp/common');
|
|
4
4
|
var oauth = require('oauth4webapi');
|
|
5
5
|
var bignumber_js = require('bignumber.js');
|
|
6
|
+
var mpp = require('@atxp/mpp');
|
|
6
7
|
|
|
7
8
|
function _interopNamespaceDefault(e) {
|
|
8
9
|
var n = Object.create(null);
|
|
@@ -473,7 +474,9 @@ function atxpFetch(config) {
|
|
|
473
474
|
onAuthorizeFailure: config.onAuthorizeFailure,
|
|
474
475
|
onPayment: config.onPayment,
|
|
475
476
|
onPaymentFailure: config.onPaymentFailure,
|
|
476
|
-
onPaymentAttemptFailed: config.onPaymentAttemptFailed
|
|
477
|
+
onPaymentAttemptFailed: config.onPaymentAttemptFailed,
|
|
478
|
+
protocolHandlers: config.protocolHandlers,
|
|
479
|
+
protocolFlag: config.protocolFlag
|
|
477
480
|
});
|
|
478
481
|
return fetcher.fetch;
|
|
479
482
|
}
|
|
@@ -862,6 +865,11 @@ class ATXPFetcher {
|
|
|
862
865
|
try {
|
|
863
866
|
// Try to fetch the resource
|
|
864
867
|
response = await oauthClient.fetch(url, init);
|
|
868
|
+
// Check HTTP-level protocol handlers first (e.g., X402 402 responses)
|
|
869
|
+
const handlerResult = await this.tryProtocolHandlers(response, url, init);
|
|
870
|
+
if (handlerResult) {
|
|
871
|
+
return handlerResult;
|
|
872
|
+
}
|
|
865
873
|
await this.checkForATXPResponse(response);
|
|
866
874
|
return response;
|
|
867
875
|
}
|
|
@@ -874,6 +882,11 @@ class ATXPFetcher {
|
|
|
874
882
|
try {
|
|
875
883
|
// Retry the request once - we should be auth'd now
|
|
876
884
|
response = await oauthClient.fetch(url, init);
|
|
885
|
+
// Check protocol handlers again after auth
|
|
886
|
+
const handlerResult = await this.tryProtocolHandlers(response, url, init);
|
|
887
|
+
if (handlerResult) {
|
|
888
|
+
return handlerResult;
|
|
889
|
+
}
|
|
877
890
|
await this.checkForATXPResponse(response);
|
|
878
891
|
return response;
|
|
879
892
|
}
|
|
@@ -906,7 +919,7 @@ class ATXPFetcher {
|
|
|
906
919
|
throw error;
|
|
907
920
|
}
|
|
908
921
|
};
|
|
909
|
-
const { account, db, destinationMakers, fetchFn = fetch, sideChannelFetch = fetchFn, strict = true, allowInsecureRequests = process.env.NODE_ENV === 'development', allowedAuthorizationServers = [common.DEFAULT_AUTHORIZATION_SERVER], approvePayment = async () => true, logger = new common.ConsoleLogger(), onAuthorize = async () => { }, onAuthorizeFailure = async () => { }, onPayment = async () => { }, onPaymentFailure, onPaymentAttemptFailed } = config;
|
|
922
|
+
const { account, db, destinationMakers, fetchFn = fetch, sideChannelFetch = fetchFn, strict = true, allowInsecureRequests = process.env.NODE_ENV === 'development', allowedAuthorizationServers = [common.DEFAULT_AUTHORIZATION_SERVER], approvePayment = async () => true, logger = new common.ConsoleLogger(), onAuthorize = async () => { }, onAuthorizeFailure = async () => { }, onPayment = async () => { }, onPaymentFailure, onPaymentAttemptFailed, protocolHandlers = [], protocolFlag } = config;
|
|
910
923
|
// Use React Native safe fetch if in React Native environment
|
|
911
924
|
this.safeFetchFn = common.getIsReactNative() ? common.createReactNativeSafeFetch(fetchFn) : fetchFn;
|
|
912
925
|
const safeSideChannelFetch = common.getIsReactNative() ? common.createReactNativeSafeFetch(sideChannelFetch) : sideChannelFetch;
|
|
@@ -925,6 +938,8 @@ class ATXPFetcher {
|
|
|
925
938
|
this.onPayment = onPayment;
|
|
926
939
|
this.onPaymentFailure = onPaymentFailure || this.defaultPaymentFailureHandler;
|
|
927
940
|
this.onPaymentAttemptFailed = onPaymentAttemptFailed;
|
|
941
|
+
this.protocolHandlers = protocolHandlers;
|
|
942
|
+
this.protocolFlag = protocolFlag;
|
|
928
943
|
}
|
|
929
944
|
/**
|
|
930
945
|
* Get or create the OAuthClient (lazy initialization to support async accountId)
|
|
@@ -947,6 +962,55 @@ class ATXPFetcher {
|
|
|
947
962
|
});
|
|
948
963
|
return this.oauthClient;
|
|
949
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* Build protocol config for passing to protocol handlers.
|
|
967
|
+
*/
|
|
968
|
+
getProtocolConfig() {
|
|
969
|
+
return {
|
|
970
|
+
account: this.account,
|
|
971
|
+
logger: this.logger,
|
|
972
|
+
fetchFn: this.safeFetchFn,
|
|
973
|
+
approvePayment: this.approvePayment,
|
|
974
|
+
onPayment: this.onPayment,
|
|
975
|
+
onPaymentFailure: this.onPaymentFailure
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Try to handle an HTTP-level payment challenge using registered protocol handlers.
|
|
980
|
+
* For omni-challenges, uses protocolFlag to select the preferred handler.
|
|
981
|
+
* For single-protocol challenges, auto-detects from response format.
|
|
982
|
+
*
|
|
983
|
+
* @returns The retry response from the selected handler, or null if no handler matched
|
|
984
|
+
*/
|
|
985
|
+
async tryProtocolHandlers(response, url, init) {
|
|
986
|
+
if (this.protocolHandlers.length === 0)
|
|
987
|
+
return null;
|
|
988
|
+
// Find all handlers that can handle this response
|
|
989
|
+
const matchingHandlers = [];
|
|
990
|
+
for (const handler of this.protocolHandlers) {
|
|
991
|
+
if (await handler.canHandle(response.clone())) {
|
|
992
|
+
matchingHandlers.push(handler);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (matchingHandlers.length === 0)
|
|
996
|
+
return null;
|
|
997
|
+
// Select handler: use protocolFlag for omni-challenges, otherwise use first match
|
|
998
|
+
let selectedHandler;
|
|
999
|
+
if (matchingHandlers.length > 1 && this.protocolFlag) {
|
|
1000
|
+
const accountId = await this.account.getAccountId();
|
|
1001
|
+
const destination = typeof url === 'string' ? url : url.toString();
|
|
1002
|
+
const preferredProtocol = this.protocolFlag(accountId, destination);
|
|
1003
|
+
const preferred = matchingHandlers.find(h => h.protocol === preferredProtocol);
|
|
1004
|
+
selectedHandler = preferred ?? matchingHandlers[0];
|
|
1005
|
+
this.logger.info(`Protocol flag selected '${preferredProtocol}', using handler '${selectedHandler.protocol}'`);
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
selectedHandler = matchingHandlers[0];
|
|
1009
|
+
this.logger.debug(`Auto-detected protocol handler: ${selectedHandler.protocol}`);
|
|
1010
|
+
}
|
|
1011
|
+
const config = this.getProtocolConfig();
|
|
1012
|
+
return selectedHandler.handlePaymentChallenge(response, { url, init }, config);
|
|
1013
|
+
}
|
|
950
1014
|
}
|
|
951
1015
|
|
|
952
1016
|
/** A special constant with type `never` */
|
|
@@ -24542,6 +24606,12 @@ function createDestinationMakers(config) {
|
|
|
24542
24606
|
case common.NetworkEnum.PolygonAmoy:
|
|
24543
24607
|
makers.set(network, new PassthroughDestinationMaker(network));
|
|
24544
24608
|
break;
|
|
24609
|
+
case common.NetworkEnum.Tempo:
|
|
24610
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
24611
|
+
break;
|
|
24612
|
+
case common.NetworkEnum.TempoModerato:
|
|
24613
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
24614
|
+
break;
|
|
24545
24615
|
case common.NetworkEnum.ATXP:
|
|
24546
24616
|
makers.set(network, new ATXPDestinationMaker(atxpAccountsServer, fetchFn));
|
|
24547
24617
|
break;
|
|
@@ -24888,6 +24958,487 @@ class ATXPLocalAccount {
|
|
|
24888
24958
|
}
|
|
24889
24959
|
}
|
|
24890
24960
|
|
|
24961
|
+
/**
|
|
24962
|
+
* Build protocol-specific payment headers for retrying a request after authorization.
|
|
24963
|
+
*
|
|
24964
|
+
* @param result - The authorization result containing protocol and credential
|
|
24965
|
+
* @param originalHeaders - Optional original request headers to preserve
|
|
24966
|
+
* @returns New Headers object with protocol-specific payment headers added
|
|
24967
|
+
*/
|
|
24968
|
+
function buildPaymentHeaders(result, originalHeaders) {
|
|
24969
|
+
let headers;
|
|
24970
|
+
if (originalHeaders instanceof Headers) {
|
|
24971
|
+
headers = new Headers(originalHeaders);
|
|
24972
|
+
}
|
|
24973
|
+
else if (originalHeaders) {
|
|
24974
|
+
headers = new Headers(originalHeaders);
|
|
24975
|
+
}
|
|
24976
|
+
else {
|
|
24977
|
+
headers = new Headers();
|
|
24978
|
+
}
|
|
24979
|
+
switch (result.protocol) {
|
|
24980
|
+
case 'x402':
|
|
24981
|
+
headers.set('X-PAYMENT', result.credential);
|
|
24982
|
+
headers.set('Access-Control-Expose-Headers', 'X-PAYMENT-RESPONSE');
|
|
24983
|
+
break;
|
|
24984
|
+
case 'mpp':
|
|
24985
|
+
headers.set('Authorization', `Payment ${result.credential}`);
|
|
24986
|
+
break;
|
|
24987
|
+
}
|
|
24988
|
+
return headers;
|
|
24989
|
+
}
|
|
24990
|
+
/**
|
|
24991
|
+
* Client for authorizing payments.
|
|
24992
|
+
*
|
|
24993
|
+
* Resolves the payment protocol via protocolFlag, then delegates to
|
|
24994
|
+
* account.authorize() for the actual authorization logic.
|
|
24995
|
+
*/
|
|
24996
|
+
class PaymentClient {
|
|
24997
|
+
constructor(config) {
|
|
24998
|
+
this.protocolFlag = config.protocolFlag;
|
|
24999
|
+
this.logger = config.logger;
|
|
25000
|
+
}
|
|
25001
|
+
/**
|
|
25002
|
+
* Authorize a payment by delegating to the account's authorize method.
|
|
25003
|
+
*
|
|
25004
|
+
* PaymentClient resolves the protocol (via explicit param or protocolFlag),
|
|
25005
|
+
* then delegates all protocol-specific logic to account.authorize().
|
|
25006
|
+
*
|
|
25007
|
+
* @param params.account - The account to authorize the payment through
|
|
25008
|
+
* @param params.userId - Passed to protocolFlag for protocol selection
|
|
25009
|
+
* @param params.destination - Payment destination address
|
|
25010
|
+
* @param params.protocol - Explicit protocol override (skips protocolFlag)
|
|
25011
|
+
* @param params.amount - Payment amount
|
|
25012
|
+
* @param params.memo - Payment memo
|
|
25013
|
+
* @param params.paymentRequirements - X402 payment requirements
|
|
25014
|
+
* @param params.challenge - MPP challenge object
|
|
25015
|
+
* @returns AuthorizeResult with protocol and opaque credential
|
|
25016
|
+
*/
|
|
25017
|
+
async authorize(params) {
|
|
25018
|
+
const { account, userId, destination } = params;
|
|
25019
|
+
// Determine protocol
|
|
25020
|
+
const protocol = params.protocol
|
|
25021
|
+
?? (this.protocolFlag ? this.protocolFlag(userId, destination) : 'atxp');
|
|
25022
|
+
// Delegate to the account's authorize method
|
|
25023
|
+
return account.authorize({
|
|
25024
|
+
protocol,
|
|
25025
|
+
amount: params.amount,
|
|
25026
|
+
destination,
|
|
25027
|
+
memo: params.memo,
|
|
25028
|
+
paymentRequirements: params.paymentRequirements,
|
|
25029
|
+
challenge: params.challenge,
|
|
25030
|
+
});
|
|
25031
|
+
}
|
|
25032
|
+
}
|
|
25033
|
+
|
|
25034
|
+
function isX402Challenge(obj) {
|
|
25035
|
+
if (typeof obj !== 'object' || obj === null)
|
|
25036
|
+
return false;
|
|
25037
|
+
const candidate = obj;
|
|
25038
|
+
return (typeof candidate.x402Version !== 'undefined' &&
|
|
25039
|
+
Array.isArray(candidate.accepts));
|
|
25040
|
+
}
|
|
25041
|
+
/**
|
|
25042
|
+
* Protocol handler for X402 payment challenges.
|
|
25043
|
+
*
|
|
25044
|
+
* Detects HTTP 402 responses with x402Version in the JSON body.
|
|
25045
|
+
* Creates payment headers using the x402 library and retries the request.
|
|
25046
|
+
*/
|
|
25047
|
+
class X402ProtocolHandler {
|
|
25048
|
+
constructor(config) {
|
|
25049
|
+
this.protocol = 'x402';
|
|
25050
|
+
this.accountsServer = config?.accountsServer ?? 'https://accounts.atxp.ai';
|
|
25051
|
+
}
|
|
25052
|
+
async canHandle(response) {
|
|
25053
|
+
if (response.status !== 402)
|
|
25054
|
+
return false;
|
|
25055
|
+
try {
|
|
25056
|
+
const cloned = response.clone();
|
|
25057
|
+
const body = await cloned.text();
|
|
25058
|
+
const parsed = JSON.parse(body);
|
|
25059
|
+
return isX402Challenge(parsed);
|
|
25060
|
+
}
|
|
25061
|
+
catch {
|
|
25062
|
+
return false;
|
|
25063
|
+
}
|
|
25064
|
+
}
|
|
25065
|
+
async handlePaymentChallenge(response, originalRequest, config) {
|
|
25066
|
+
const { account, logger, fetchFn, approvePayment, onPayment, onPaymentFailure } = config;
|
|
25067
|
+
const responseBody = await response.text();
|
|
25068
|
+
let paymentChallenge;
|
|
25069
|
+
try {
|
|
25070
|
+
paymentChallenge = JSON.parse(responseBody);
|
|
25071
|
+
}
|
|
25072
|
+
catch {
|
|
25073
|
+
logger.error('X402: failed to parse challenge body');
|
|
25074
|
+
return null;
|
|
25075
|
+
}
|
|
25076
|
+
if (!isX402Challenge(paymentChallenge)) {
|
|
25077
|
+
return null;
|
|
25078
|
+
}
|
|
25079
|
+
try {
|
|
25080
|
+
const { selectPaymentRequirements } = await import('x402/client');
|
|
25081
|
+
const selectedPaymentRequirements = selectPaymentRequirements(paymentChallenge.accepts, undefined, 'exact');
|
|
25082
|
+
if (!selectedPaymentRequirements) {
|
|
25083
|
+
logger.info('X402: no suitable payment option found');
|
|
25084
|
+
return this.reconstructResponse(responseBody, response);
|
|
25085
|
+
}
|
|
25086
|
+
const amountInUsdc = Number(selectedPaymentRequirements.maxAmountRequired) / (10 ** 6);
|
|
25087
|
+
const network = selectedPaymentRequirements.network;
|
|
25088
|
+
logger.debug(`X402: payment required: ${amountInUsdc} USDC on ${network} to ${selectedPaymentRequirements.payTo}`);
|
|
25089
|
+
const url = typeof originalRequest.url === 'string' ? originalRequest.url : originalRequest.url.toString();
|
|
25090
|
+
const accountId = await account.getAccountId();
|
|
25091
|
+
const prospectivePayment = {
|
|
25092
|
+
accountId,
|
|
25093
|
+
resourceUrl: url,
|
|
25094
|
+
resourceName: selectedPaymentRequirements.description || url,
|
|
25095
|
+
currency: 'USDC',
|
|
25096
|
+
amount: new bignumber_js.BigNumber(amountInUsdc),
|
|
25097
|
+
iss: selectedPaymentRequirements.payTo
|
|
25098
|
+
};
|
|
25099
|
+
const approved = await approvePayment(prospectivePayment);
|
|
25100
|
+
if (!approved) {
|
|
25101
|
+
logger.info('X402: payment not approved');
|
|
25102
|
+
const error = new Error('Payment not approved');
|
|
25103
|
+
await onPaymentFailure({
|
|
25104
|
+
payment: prospectivePayment,
|
|
25105
|
+
error,
|
|
25106
|
+
attemptedNetworks: [network],
|
|
25107
|
+
failureReasons: new Map([[network, error]]),
|
|
25108
|
+
retryable: true,
|
|
25109
|
+
timestamp: new Date()
|
|
25110
|
+
});
|
|
25111
|
+
return this.reconstructResponse(responseBody, response);
|
|
25112
|
+
}
|
|
25113
|
+
// Authorize via account.authorize() — ATXPAccount calls the accounts
|
|
25114
|
+
// service, BaseAccount signs locally. No fallback — each account type
|
|
25115
|
+
// handles authorization according to its capabilities.
|
|
25116
|
+
const client = new PaymentClient({
|
|
25117
|
+
accountsServer: this.accountsServer,
|
|
25118
|
+
logger,
|
|
25119
|
+
fetchFn,
|
|
25120
|
+
});
|
|
25121
|
+
const authorizeResult = await client.authorize({
|
|
25122
|
+
account,
|
|
25123
|
+
userId: accountId,
|
|
25124
|
+
destination: url,
|
|
25125
|
+
protocol: 'x402',
|
|
25126
|
+
paymentRequirements: selectedPaymentRequirements,
|
|
25127
|
+
});
|
|
25128
|
+
const paymentHeader = authorizeResult.credential;
|
|
25129
|
+
// Retry with X-PAYMENT header
|
|
25130
|
+
const retryHeaders = buildPaymentHeaders({ protocol: 'x402', credential: paymentHeader }, originalRequest.init?.headers);
|
|
25131
|
+
const retryInit = { ...originalRequest.init, headers: retryHeaders };
|
|
25132
|
+
logger.info('X402: retrying request with X-PAYMENT header');
|
|
25133
|
+
const retryResponse = await fetchFn(originalRequest.url, retryInit);
|
|
25134
|
+
if (retryResponse.ok) {
|
|
25135
|
+
logger.info('X402: payment accepted');
|
|
25136
|
+
await onPayment({
|
|
25137
|
+
payment: prospectivePayment,
|
|
25138
|
+
transactionHash: paymentHeader.substring(0, 66),
|
|
25139
|
+
network
|
|
25140
|
+
});
|
|
25141
|
+
}
|
|
25142
|
+
else {
|
|
25143
|
+
logger.warn(`X402: request failed after payment with status ${retryResponse.status}`);
|
|
25144
|
+
const error = new Error(`Request failed with status ${retryResponse.status}`);
|
|
25145
|
+
await onPaymentFailure({
|
|
25146
|
+
payment: prospectivePayment,
|
|
25147
|
+
error,
|
|
25148
|
+
attemptedNetworks: [network],
|
|
25149
|
+
failureReasons: new Map([[network, error]]),
|
|
25150
|
+
retryable: false,
|
|
25151
|
+
timestamp: new Date()
|
|
25152
|
+
});
|
|
25153
|
+
}
|
|
25154
|
+
return retryResponse;
|
|
25155
|
+
}
|
|
25156
|
+
catch (error) {
|
|
25157
|
+
logger.error(`X402: failed to handle payment challenge: ${error}`);
|
|
25158
|
+
if (isX402Challenge(paymentChallenge) && paymentChallenge.accepts[0]) {
|
|
25159
|
+
const firstOption = paymentChallenge.accepts[0];
|
|
25160
|
+
const amount = firstOption.maxAmountRequired ? Number(firstOption.maxAmountRequired) / (10 ** 6) : 0;
|
|
25161
|
+
const url = typeof originalRequest.url === 'string' ? originalRequest.url : originalRequest.url.toString();
|
|
25162
|
+
const accountId = await account.getAccountId();
|
|
25163
|
+
const errorNetwork = firstOption.network || 'unknown';
|
|
25164
|
+
const typedError = error;
|
|
25165
|
+
const isRetryable = typedError instanceof ATXPPaymentError ? typedError.retryable : true;
|
|
25166
|
+
await onPaymentFailure({
|
|
25167
|
+
payment: {
|
|
25168
|
+
accountId,
|
|
25169
|
+
resourceUrl: url,
|
|
25170
|
+
resourceName: firstOption.description || url,
|
|
25171
|
+
currency: 'USDC',
|
|
25172
|
+
amount: new bignumber_js.BigNumber(amount),
|
|
25173
|
+
iss: firstOption.payTo || ''
|
|
25174
|
+
},
|
|
25175
|
+
error: typedError,
|
|
25176
|
+
attemptedNetworks: [errorNetwork],
|
|
25177
|
+
failureReasons: new Map([[errorNetwork, typedError]]),
|
|
25178
|
+
retryable: isRetryable,
|
|
25179
|
+
timestamp: new Date()
|
|
25180
|
+
});
|
|
25181
|
+
}
|
|
25182
|
+
return this.reconstructResponse(responseBody, response);
|
|
25183
|
+
}
|
|
25184
|
+
}
|
|
25185
|
+
reconstructResponse(body, original) {
|
|
25186
|
+
return new Response(body, {
|
|
25187
|
+
status: original.status,
|
|
25188
|
+
statusText: original.statusText,
|
|
25189
|
+
headers: original.headers
|
|
25190
|
+
});
|
|
25191
|
+
}
|
|
25192
|
+
}
|
|
25193
|
+
|
|
25194
|
+
/**
|
|
25195
|
+
* Protocol handler for ATXP-MCP payment challenges.
|
|
25196
|
+
*
|
|
25197
|
+
* Detects JSON-RPC errors with code -30402 (PAYMENT_REQUIRED_ERROR_CODE) in
|
|
25198
|
+
* MCP responses (both SSE and JSON formats).
|
|
25199
|
+
*/
|
|
25200
|
+
class ATXPProtocolHandler {
|
|
25201
|
+
constructor() {
|
|
25202
|
+
this.protocol = 'atxp';
|
|
25203
|
+
}
|
|
25204
|
+
async canHandle(response) {
|
|
25205
|
+
// ATXP-MCP challenges are embedded in MCP JSON-RPC responses,
|
|
25206
|
+
// not at the HTTP level. We detect them by looking for error code -30402.
|
|
25207
|
+
try {
|
|
25208
|
+
const cloned = response.clone();
|
|
25209
|
+
const body = await cloned.text();
|
|
25210
|
+
if (body.length === 0)
|
|
25211
|
+
return false;
|
|
25212
|
+
const paymentRequests = await this.extractPaymentRequests(body);
|
|
25213
|
+
return paymentRequests.length > 0;
|
|
25214
|
+
}
|
|
25215
|
+
catch {
|
|
25216
|
+
return false;
|
|
25217
|
+
}
|
|
25218
|
+
}
|
|
25219
|
+
async handlePaymentChallenge(response, _originalRequest, config) {
|
|
25220
|
+
const { logger } = config;
|
|
25221
|
+
try {
|
|
25222
|
+
const body = await response.clone().text();
|
|
25223
|
+
const paymentRequests = await this.extractPaymentRequests(body);
|
|
25224
|
+
if (paymentRequests.length === 0) {
|
|
25225
|
+
return null;
|
|
25226
|
+
}
|
|
25227
|
+
// ATXP-MCP payment challenges are handled through the fetcher's
|
|
25228
|
+
// checkForATXPResponse → handlePaymentRequestError flow, not at
|
|
25229
|
+
// the HTTP protocol handler level. Return null so the fetcher's
|
|
25230
|
+
// existing MCP payment flow picks it up.
|
|
25231
|
+
// This handler's role in the strategy pattern is detection via
|
|
25232
|
+
// canHandle() for protocol selection (e.g., protocolFlag), not
|
|
25233
|
+
// direct payment handling.
|
|
25234
|
+
if (paymentRequests.length > 1) {
|
|
25235
|
+
logger.warn(`ATXP: multiple payment requirements found in MCP response. ` +
|
|
25236
|
+
`The client does not support multiple payment requirements. ` +
|
|
25237
|
+
`${paymentRequests.map(pr => pr.url).join(', ')}`);
|
|
25238
|
+
}
|
|
25239
|
+
else {
|
|
25240
|
+
logger.info(`ATXP: payment requirement found in MCP response - ${paymentRequests[0].url}`);
|
|
25241
|
+
}
|
|
25242
|
+
return null;
|
|
25243
|
+
}
|
|
25244
|
+
catch (error) {
|
|
25245
|
+
logger.error(`ATXP: error checking for payment requirements: ${error}`);
|
|
25246
|
+
return null;
|
|
25247
|
+
}
|
|
25248
|
+
}
|
|
25249
|
+
async extractPaymentRequests(body) {
|
|
25250
|
+
try {
|
|
25251
|
+
if (common.isSSEResponse(body)) {
|
|
25252
|
+
const messages = await common.parseMcpMessages(body);
|
|
25253
|
+
return messages.flatMap(message => common.parsePaymentRequests(message)).filter((pr) => pr !== null);
|
|
25254
|
+
}
|
|
25255
|
+
else {
|
|
25256
|
+
const json = JSON.parse(body);
|
|
25257
|
+
const messages = await common.parseMcpMessages(json);
|
|
25258
|
+
return messages.flatMap(message => common.parsePaymentRequests(message)).filter((pr) => pr !== null);
|
|
25259
|
+
}
|
|
25260
|
+
}
|
|
25261
|
+
catch {
|
|
25262
|
+
return [];
|
|
25263
|
+
}
|
|
25264
|
+
}
|
|
25265
|
+
}
|
|
25266
|
+
|
|
25267
|
+
/**
|
|
25268
|
+
* Protocol handler for MPP (Machine Payments Protocol) payment challenges.
|
|
25269
|
+
*
|
|
25270
|
+
* Detects MPP challenges in two forms:
|
|
25271
|
+
* 1. HTTP level: HTTP 402 with WWW-Authenticate: Payment header
|
|
25272
|
+
* 2. MCP level: JSON-RPC error with code -32042 containing MPP data
|
|
25273
|
+
*
|
|
25274
|
+
* Handles the challenge by calling /authorize/mpp on the accounts service
|
|
25275
|
+
* and retrying with an Authorization: Payment header.
|
|
25276
|
+
*/
|
|
25277
|
+
class MPPProtocolHandler {
|
|
25278
|
+
constructor(config) {
|
|
25279
|
+
this.protocol = 'mpp';
|
|
25280
|
+
this.accountsServer = config?.accountsServer ?? 'https://accounts.atxp.ai';
|
|
25281
|
+
}
|
|
25282
|
+
async canHandle(response) {
|
|
25283
|
+
if (mpp.hasMPPChallenge(response))
|
|
25284
|
+
return true;
|
|
25285
|
+
return mpp.hasMPPMCPError(response);
|
|
25286
|
+
}
|
|
25287
|
+
async handlePaymentChallenge(response, originalRequest, config) {
|
|
25288
|
+
const { account, logger, approvePayment } = config;
|
|
25289
|
+
// Extract the challenge and body text from the response
|
|
25290
|
+
const extracted = await this.extractChallenge(response, logger);
|
|
25291
|
+
if (!extracted) {
|
|
25292
|
+
logger.error('MPP: failed to extract challenge from response');
|
|
25293
|
+
return null;
|
|
25294
|
+
}
|
|
25295
|
+
const { challenge, bodyText } = extracted;
|
|
25296
|
+
const url = typeof originalRequest.url === 'string'
|
|
25297
|
+
? originalRequest.url
|
|
25298
|
+
: originalRequest.url.toString();
|
|
25299
|
+
// Build prospective payment for approval
|
|
25300
|
+
const accountId = await account.getAccountId();
|
|
25301
|
+
const prospectivePayment = this.buildProspectivePayment(challenge, url, accountId);
|
|
25302
|
+
// Ask for approval
|
|
25303
|
+
const approved = await approvePayment(prospectivePayment);
|
|
25304
|
+
if (!approved) {
|
|
25305
|
+
logger.info('MPP: payment not approved');
|
|
25306
|
+
await this.reportFailure(config, prospectivePayment, new Error('Payment not approved'), challenge.network, true);
|
|
25307
|
+
return this.reconstructResponse(bodyText, response);
|
|
25308
|
+
}
|
|
25309
|
+
return this.authorizeAndRetry(challenge, prospectivePayment, originalRequest, config, bodyText, response);
|
|
25310
|
+
}
|
|
25311
|
+
/**
|
|
25312
|
+
* Extract MPP challenge from response - tries HTTP header first, then MCP error body.
|
|
25313
|
+
* Returns both the challenge and the body text to avoid double-consumption.
|
|
25314
|
+
*/
|
|
25315
|
+
async extractChallenge(response, logger) {
|
|
25316
|
+
// Read body once upfront to avoid double consumption (response.text() can only be called once)
|
|
25317
|
+
let bodyText = '';
|
|
25318
|
+
try {
|
|
25319
|
+
bodyText = await response.text();
|
|
25320
|
+
}
|
|
25321
|
+
catch {
|
|
25322
|
+
// Body may not be available
|
|
25323
|
+
}
|
|
25324
|
+
// Try HTTP header first
|
|
25325
|
+
const header = response.headers.get('WWW-Authenticate');
|
|
25326
|
+
if (header) {
|
|
25327
|
+
const challenge = mpp.parseMPPHeader(header);
|
|
25328
|
+
if (challenge) {
|
|
25329
|
+
logger.debug('MPP: parsed challenge from WWW-Authenticate header');
|
|
25330
|
+
return { challenge, bodyText };
|
|
25331
|
+
}
|
|
25332
|
+
}
|
|
25333
|
+
// Try MCP error body (use clone since response body may have been consumed above)
|
|
25334
|
+
try {
|
|
25335
|
+
const parsed = JSON.parse(bodyText);
|
|
25336
|
+
if (typeof parsed === 'object' &&
|
|
25337
|
+
parsed !== null &&
|
|
25338
|
+
typeof parsed.error === 'object' &&
|
|
25339
|
+
parsed.error !== null &&
|
|
25340
|
+
parsed.error.code === mpp.MPP_ERROR_CODE) {
|
|
25341
|
+
const challenge = mpp.parseMPPFromMCPError(parsed.error.data);
|
|
25342
|
+
if (challenge) {
|
|
25343
|
+
logger.debug('MPP: parsed challenge from MCP error body');
|
|
25344
|
+
return { challenge, bodyText };
|
|
25345
|
+
}
|
|
25346
|
+
}
|
|
25347
|
+
}
|
|
25348
|
+
catch {
|
|
25349
|
+
// Not JSON or malformed
|
|
25350
|
+
}
|
|
25351
|
+
return null;
|
|
25352
|
+
}
|
|
25353
|
+
/**
|
|
25354
|
+
* Build a ProspectivePayment from an MPP challenge.
|
|
25355
|
+
*/
|
|
25356
|
+
buildProspectivePayment(challenge, url, accountId) {
|
|
25357
|
+
const amountNum = Number(challenge.amount) / (10 ** 6);
|
|
25358
|
+
return {
|
|
25359
|
+
accountId,
|
|
25360
|
+
resourceUrl: url,
|
|
25361
|
+
resourceName: url,
|
|
25362
|
+
currency: challenge.currency,
|
|
25363
|
+
amount: new bignumber_js.BigNumber(amountNum),
|
|
25364
|
+
iss: challenge.recipient,
|
|
25365
|
+
};
|
|
25366
|
+
}
|
|
25367
|
+
/**
|
|
25368
|
+
* Report a payment failure via the onPaymentFailure callback.
|
|
25369
|
+
*/
|
|
25370
|
+
async reportFailure(config, payment, error, network, retryable) {
|
|
25371
|
+
await config.onPaymentFailure({
|
|
25372
|
+
payment,
|
|
25373
|
+
error,
|
|
25374
|
+
attemptedNetworks: [network],
|
|
25375
|
+
failureReasons: new Map([[network, error]]),
|
|
25376
|
+
retryable,
|
|
25377
|
+
timestamp: new Date(),
|
|
25378
|
+
});
|
|
25379
|
+
}
|
|
25380
|
+
/**
|
|
25381
|
+
* Call /authorize/mpp on accounts service and retry the original request with the credential.
|
|
25382
|
+
*/
|
|
25383
|
+
async authorizeAndRetry(challenge, prospectivePayment, originalRequest, config, bodyText, originalResponse) {
|
|
25384
|
+
const { account, logger, fetchFn, onPayment } = config;
|
|
25385
|
+
try {
|
|
25386
|
+
logger.debug('MPP: calling /authorize/mpp on accounts service');
|
|
25387
|
+
const client = new PaymentClient({
|
|
25388
|
+
accountsServer: this.accountsServer,
|
|
25389
|
+
logger,
|
|
25390
|
+
fetchFn,
|
|
25391
|
+
});
|
|
25392
|
+
const accountId = await account.getAccountId();
|
|
25393
|
+
let authorizeResult;
|
|
25394
|
+
try {
|
|
25395
|
+
authorizeResult = await client.authorize({
|
|
25396
|
+
account,
|
|
25397
|
+
userId: accountId,
|
|
25398
|
+
destination: typeof originalRequest.url === 'string' ? originalRequest.url : originalRequest.url.toString(),
|
|
25399
|
+
protocol: 'mpp',
|
|
25400
|
+
challenge,
|
|
25401
|
+
});
|
|
25402
|
+
}
|
|
25403
|
+
catch (authorizeError) {
|
|
25404
|
+
// AuthorizationError = server rejected the request (HTTP error from accounts)
|
|
25405
|
+
// Other errors = data validation or network failure
|
|
25406
|
+
if (authorizeError instanceof common.AuthorizationError) {
|
|
25407
|
+
logger.debug(`MPP: /authorize/mpp rejected (${authorizeError.statusCode}), returning original response`);
|
|
25408
|
+
return this.reconstructResponse(bodyText, originalResponse);
|
|
25409
|
+
}
|
|
25410
|
+
throw authorizeError;
|
|
25411
|
+
}
|
|
25412
|
+
const retryHeaders = buildPaymentHeaders(authorizeResult, originalRequest.init?.headers);
|
|
25413
|
+
const retryInit = { ...originalRequest.init, headers: retryHeaders };
|
|
25414
|
+
logger.info('MPP: retrying request with Authorization: Payment header');
|
|
25415
|
+
const retryResponse = await fetchFn(originalRequest.url, retryInit);
|
|
25416
|
+
if (retryResponse.ok) {
|
|
25417
|
+
logger.info('MPP: payment accepted');
|
|
25418
|
+
await onPayment({ payment: prospectivePayment, transactionHash: challenge.id, network: challenge.network });
|
|
25419
|
+
}
|
|
25420
|
+
else {
|
|
25421
|
+
logger.warn(`MPP: request failed after payment with status ${retryResponse.status}`);
|
|
25422
|
+
await this.reportFailure(config, prospectivePayment, new Error(`Request failed with status ${retryResponse.status}`), challenge.network, false);
|
|
25423
|
+
}
|
|
25424
|
+
return retryResponse;
|
|
25425
|
+
}
|
|
25426
|
+
catch (error) {
|
|
25427
|
+
logger.error(`MPP: failed to handle payment challenge: ${error}`);
|
|
25428
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
25429
|
+
await this.reportFailure(config, prospectivePayment, cause, challenge.network, true);
|
|
25430
|
+
return this.reconstructResponse(bodyText, originalResponse);
|
|
25431
|
+
}
|
|
25432
|
+
}
|
|
25433
|
+
reconstructResponse(body, original) {
|
|
25434
|
+
return new Response(body || null, {
|
|
25435
|
+
status: original.status,
|
|
25436
|
+
statusText: original.statusText,
|
|
25437
|
+
headers: original.headers,
|
|
25438
|
+
});
|
|
25439
|
+
}
|
|
25440
|
+
}
|
|
25441
|
+
|
|
24891
25442
|
Object.defineProperty(exports, "ATXPAccount", {
|
|
24892
25443
|
enumerable: true,
|
|
24893
25444
|
get: function () { return common.ATXPAccount; }
|
|
@@ -24895,14 +25446,17 @@ Object.defineProperty(exports, "ATXPAccount", {
|
|
|
24895
25446
|
exports.ATXPDestinationMaker = ATXPDestinationMaker;
|
|
24896
25447
|
exports.ATXPLocalAccount = ATXPLocalAccount;
|
|
24897
25448
|
exports.ATXPPaymentError = ATXPPaymentError;
|
|
25449
|
+
exports.ATXPProtocolHandler = ATXPProtocolHandler;
|
|
24898
25450
|
exports.DEFAULT_CLIENT_CONFIG = DEFAULT_CLIENT_CONFIG;
|
|
24899
25451
|
exports.GasEstimationError = GasEstimationError;
|
|
24900
25452
|
exports.InsufficientFundsError = InsufficientFundsError;
|
|
25453
|
+
exports.MPPProtocolHandler = MPPProtocolHandler;
|
|
24901
25454
|
exports.OAuthAuthenticationRequiredError = OAuthAuthenticationRequiredError;
|
|
24902
25455
|
exports.OAuthClient = OAuthClient;
|
|
24903
25456
|
exports.POLYGON_AMOY = POLYGON_AMOY;
|
|
24904
25457
|
exports.POLYGON_MAINNET = POLYGON_MAINNET;
|
|
24905
25458
|
exports.PassthroughDestinationMaker = PassthroughDestinationMaker;
|
|
25459
|
+
exports.PaymentClient = PaymentClient;
|
|
24906
25460
|
exports.PaymentExpiredError = PaymentExpiredError;
|
|
24907
25461
|
exports.PaymentNetworkError = PaymentNetworkError;
|
|
24908
25462
|
exports.PaymentServerError = PaymentServerError;
|
|
@@ -24916,9 +25470,11 @@ exports.UnsupportedCurrencyError = UnsupportedCurrencyError;
|
|
|
24916
25470
|
exports.UserRejectedError = UserRejectedError;
|
|
24917
25471
|
exports.WORLD_CHAIN_MAINNET = WORLD_CHAIN_MAINNET;
|
|
24918
25472
|
exports.WORLD_CHAIN_SEPOLIA = WORLD_CHAIN_SEPOLIA;
|
|
25473
|
+
exports.X402ProtocolHandler = X402ProtocolHandler;
|
|
24919
25474
|
exports.atxpClient = atxpClient;
|
|
24920
25475
|
exports.atxpFetch = atxpFetch;
|
|
24921
25476
|
exports.buildClientConfig = buildClientConfig;
|
|
25477
|
+
exports.buildPaymentHeaders = buildPaymentHeaders;
|
|
24922
25478
|
exports.buildStreamableTransport = buildStreamableTransport;
|
|
24923
25479
|
exports.getPolygonAmoyWithRPC = getPolygonAmoyWithRPC;
|
|
24924
25480
|
exports.getPolygonByChainId = getPolygonByChainId;
|