@atxp/client 0.10.6 → 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/index18.js +2 -2
- package/dist/_virtual/index19.js +2 -2
- package/dist/_virtual/index3.js +2 -2
- package/dist/_virtual/index4.js +2 -2
- package/dist/_virtual/index5.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/codegen/index.js +1 -1
- 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/validation/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.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { crypto as crypto$1, OAuthResourceClient, ConsoleLogger, getErrorRecoveryHint, PAYMENT_REQUIRED_ERROR_CODE, isSSEResponse, parseMcpMessages, parsePaymentRequests, paymentRequiredError, DEFAULT_AUTHORIZATION_SERVER, createReactNativeSafeFetch, getIsReactNative, isEnumValue, ChainEnum, CurrencyEnum, NetworkEnum, assertNever, DEFAULT_ATXP_ACCOUNTS_SERVER, MemoryOAuthDb, ATXPAccount } from '@atxp/common';
|
|
1
|
+
import { crypto as crypto$1, OAuthResourceClient, ConsoleLogger, getErrorRecoveryHint, PAYMENT_REQUIRED_ERROR_CODE, isSSEResponse, parseMcpMessages, parsePaymentRequests, paymentRequiredError, DEFAULT_AUTHORIZATION_SERVER, createReactNativeSafeFetch, getIsReactNative, isEnumValue, ChainEnum, CurrencyEnum, NetworkEnum, assertNever, DEFAULT_ATXP_ACCOUNTS_SERVER, MemoryOAuthDb, ATXPAccount, AuthorizationError } from '@atxp/common';
|
|
2
2
|
export { ATXPAccount } from '@atxp/common';
|
|
3
3
|
import * as oauth from 'oauth4webapi';
|
|
4
4
|
import { BigNumber } from 'bignumber.js';
|
|
5
|
+
import { hasMPPChallenge, hasMPPMCPError, parseMPPHeader, MPP_ERROR_CODE, parseMPPFromMCPError } from '@atxp/mpp';
|
|
5
6
|
|
|
6
7
|
class OAuthAuthenticationRequiredError extends Error {
|
|
7
8
|
constructor(url, resourceServerUrl,
|
|
@@ -453,7 +454,9 @@ function atxpFetch(config) {
|
|
|
453
454
|
onAuthorizeFailure: config.onAuthorizeFailure,
|
|
454
455
|
onPayment: config.onPayment,
|
|
455
456
|
onPaymentFailure: config.onPaymentFailure,
|
|
456
|
-
onPaymentAttemptFailed: config.onPaymentAttemptFailed
|
|
457
|
+
onPaymentAttemptFailed: config.onPaymentAttemptFailed,
|
|
458
|
+
protocolHandlers: config.protocolHandlers,
|
|
459
|
+
protocolFlag: config.protocolFlag
|
|
457
460
|
});
|
|
458
461
|
return fetcher.fetch;
|
|
459
462
|
}
|
|
@@ -842,6 +845,11 @@ class ATXPFetcher {
|
|
|
842
845
|
try {
|
|
843
846
|
// Try to fetch the resource
|
|
844
847
|
response = await oauthClient.fetch(url, init);
|
|
848
|
+
// Check HTTP-level protocol handlers first (e.g., X402 402 responses)
|
|
849
|
+
const handlerResult = await this.tryProtocolHandlers(response, url, init);
|
|
850
|
+
if (handlerResult) {
|
|
851
|
+
return handlerResult;
|
|
852
|
+
}
|
|
845
853
|
await this.checkForATXPResponse(response);
|
|
846
854
|
return response;
|
|
847
855
|
}
|
|
@@ -854,6 +862,11 @@ class ATXPFetcher {
|
|
|
854
862
|
try {
|
|
855
863
|
// Retry the request once - we should be auth'd now
|
|
856
864
|
response = await oauthClient.fetch(url, init);
|
|
865
|
+
// Check protocol handlers again after auth
|
|
866
|
+
const handlerResult = await this.tryProtocolHandlers(response, url, init);
|
|
867
|
+
if (handlerResult) {
|
|
868
|
+
return handlerResult;
|
|
869
|
+
}
|
|
857
870
|
await this.checkForATXPResponse(response);
|
|
858
871
|
return response;
|
|
859
872
|
}
|
|
@@ -886,7 +899,7 @@ class ATXPFetcher {
|
|
|
886
899
|
throw error;
|
|
887
900
|
}
|
|
888
901
|
};
|
|
889
|
-
const { account, db, destinationMakers, fetchFn = fetch, sideChannelFetch = fetchFn, strict = true, allowInsecureRequests = process.env.NODE_ENV === 'development', allowedAuthorizationServers = [DEFAULT_AUTHORIZATION_SERVER], approvePayment = async () => true, logger = new ConsoleLogger(), onAuthorize = async () => { }, onAuthorizeFailure = async () => { }, onPayment = async () => { }, onPaymentFailure, onPaymentAttemptFailed } = config;
|
|
902
|
+
const { account, db, destinationMakers, fetchFn = fetch, sideChannelFetch = fetchFn, strict = true, allowInsecureRequests = process.env.NODE_ENV === 'development', allowedAuthorizationServers = [DEFAULT_AUTHORIZATION_SERVER], approvePayment = async () => true, logger = new ConsoleLogger(), onAuthorize = async () => { }, onAuthorizeFailure = async () => { }, onPayment = async () => { }, onPaymentFailure, onPaymentAttemptFailed, protocolHandlers = [], protocolFlag } = config;
|
|
890
903
|
// Use React Native safe fetch if in React Native environment
|
|
891
904
|
this.safeFetchFn = getIsReactNative() ? createReactNativeSafeFetch(fetchFn) : fetchFn;
|
|
892
905
|
const safeSideChannelFetch = getIsReactNative() ? createReactNativeSafeFetch(sideChannelFetch) : sideChannelFetch;
|
|
@@ -905,6 +918,8 @@ class ATXPFetcher {
|
|
|
905
918
|
this.onPayment = onPayment;
|
|
906
919
|
this.onPaymentFailure = onPaymentFailure || this.defaultPaymentFailureHandler;
|
|
907
920
|
this.onPaymentAttemptFailed = onPaymentAttemptFailed;
|
|
921
|
+
this.protocolHandlers = protocolHandlers;
|
|
922
|
+
this.protocolFlag = protocolFlag;
|
|
908
923
|
}
|
|
909
924
|
/**
|
|
910
925
|
* Get or create the OAuthClient (lazy initialization to support async accountId)
|
|
@@ -927,6 +942,55 @@ class ATXPFetcher {
|
|
|
927
942
|
});
|
|
928
943
|
return this.oauthClient;
|
|
929
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* Build protocol config for passing to protocol handlers.
|
|
947
|
+
*/
|
|
948
|
+
getProtocolConfig() {
|
|
949
|
+
return {
|
|
950
|
+
account: this.account,
|
|
951
|
+
logger: this.logger,
|
|
952
|
+
fetchFn: this.safeFetchFn,
|
|
953
|
+
approvePayment: this.approvePayment,
|
|
954
|
+
onPayment: this.onPayment,
|
|
955
|
+
onPaymentFailure: this.onPaymentFailure
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Try to handle an HTTP-level payment challenge using registered protocol handlers.
|
|
960
|
+
* For omni-challenges, uses protocolFlag to select the preferred handler.
|
|
961
|
+
* For single-protocol challenges, auto-detects from response format.
|
|
962
|
+
*
|
|
963
|
+
* @returns The retry response from the selected handler, or null if no handler matched
|
|
964
|
+
*/
|
|
965
|
+
async tryProtocolHandlers(response, url, init) {
|
|
966
|
+
if (this.protocolHandlers.length === 0)
|
|
967
|
+
return null;
|
|
968
|
+
// Find all handlers that can handle this response
|
|
969
|
+
const matchingHandlers = [];
|
|
970
|
+
for (const handler of this.protocolHandlers) {
|
|
971
|
+
if (await handler.canHandle(response.clone())) {
|
|
972
|
+
matchingHandlers.push(handler);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (matchingHandlers.length === 0)
|
|
976
|
+
return null;
|
|
977
|
+
// Select handler: use protocolFlag for omni-challenges, otherwise use first match
|
|
978
|
+
let selectedHandler;
|
|
979
|
+
if (matchingHandlers.length > 1 && this.protocolFlag) {
|
|
980
|
+
const accountId = await this.account.getAccountId();
|
|
981
|
+
const destination = typeof url === 'string' ? url : url.toString();
|
|
982
|
+
const preferredProtocol = this.protocolFlag(accountId, destination);
|
|
983
|
+
const preferred = matchingHandlers.find(h => h.protocol === preferredProtocol);
|
|
984
|
+
selectedHandler = preferred ?? matchingHandlers[0];
|
|
985
|
+
this.logger.info(`Protocol flag selected '${preferredProtocol}', using handler '${selectedHandler.protocol}'`);
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
selectedHandler = matchingHandlers[0];
|
|
989
|
+
this.logger.debug(`Auto-detected protocol handler: ${selectedHandler.protocol}`);
|
|
990
|
+
}
|
|
991
|
+
const config = this.getProtocolConfig();
|
|
992
|
+
return selectedHandler.handlePaymentChallenge(response, { url, init }, config);
|
|
993
|
+
}
|
|
930
994
|
}
|
|
931
995
|
|
|
932
996
|
/** A special constant with type `never` */
|
|
@@ -24522,6 +24586,12 @@ function createDestinationMakers(config) {
|
|
|
24522
24586
|
case NetworkEnum.PolygonAmoy:
|
|
24523
24587
|
makers.set(network, new PassthroughDestinationMaker(network));
|
|
24524
24588
|
break;
|
|
24589
|
+
case NetworkEnum.Tempo:
|
|
24590
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
24591
|
+
break;
|
|
24592
|
+
case NetworkEnum.TempoModerato:
|
|
24593
|
+
makers.set(network, new PassthroughDestinationMaker(network));
|
|
24594
|
+
break;
|
|
24525
24595
|
case NetworkEnum.ATXP:
|
|
24526
24596
|
makers.set(network, new ATXPDestinationMaker(atxpAccountsServer, fetchFn));
|
|
24527
24597
|
break;
|
|
@@ -24868,5 +24938,486 @@ class ATXPLocalAccount {
|
|
|
24868
24938
|
}
|
|
24869
24939
|
}
|
|
24870
24940
|
|
|
24871
|
-
|
|
24941
|
+
/**
|
|
24942
|
+
* Build protocol-specific payment headers for retrying a request after authorization.
|
|
24943
|
+
*
|
|
24944
|
+
* @param result - The authorization result containing protocol and credential
|
|
24945
|
+
* @param originalHeaders - Optional original request headers to preserve
|
|
24946
|
+
* @returns New Headers object with protocol-specific payment headers added
|
|
24947
|
+
*/
|
|
24948
|
+
function buildPaymentHeaders(result, originalHeaders) {
|
|
24949
|
+
let headers;
|
|
24950
|
+
if (originalHeaders instanceof Headers) {
|
|
24951
|
+
headers = new Headers(originalHeaders);
|
|
24952
|
+
}
|
|
24953
|
+
else if (originalHeaders) {
|
|
24954
|
+
headers = new Headers(originalHeaders);
|
|
24955
|
+
}
|
|
24956
|
+
else {
|
|
24957
|
+
headers = new Headers();
|
|
24958
|
+
}
|
|
24959
|
+
switch (result.protocol) {
|
|
24960
|
+
case 'x402':
|
|
24961
|
+
headers.set('X-PAYMENT', result.credential);
|
|
24962
|
+
headers.set('Access-Control-Expose-Headers', 'X-PAYMENT-RESPONSE');
|
|
24963
|
+
break;
|
|
24964
|
+
case 'mpp':
|
|
24965
|
+
headers.set('Authorization', `Payment ${result.credential}`);
|
|
24966
|
+
break;
|
|
24967
|
+
}
|
|
24968
|
+
return headers;
|
|
24969
|
+
}
|
|
24970
|
+
/**
|
|
24971
|
+
* Client for authorizing payments.
|
|
24972
|
+
*
|
|
24973
|
+
* Resolves the payment protocol via protocolFlag, then delegates to
|
|
24974
|
+
* account.authorize() for the actual authorization logic.
|
|
24975
|
+
*/
|
|
24976
|
+
class PaymentClient {
|
|
24977
|
+
constructor(config) {
|
|
24978
|
+
this.protocolFlag = config.protocolFlag;
|
|
24979
|
+
this.logger = config.logger;
|
|
24980
|
+
}
|
|
24981
|
+
/**
|
|
24982
|
+
* Authorize a payment by delegating to the account's authorize method.
|
|
24983
|
+
*
|
|
24984
|
+
* PaymentClient resolves the protocol (via explicit param or protocolFlag),
|
|
24985
|
+
* then delegates all protocol-specific logic to account.authorize().
|
|
24986
|
+
*
|
|
24987
|
+
* @param params.account - The account to authorize the payment through
|
|
24988
|
+
* @param params.userId - Passed to protocolFlag for protocol selection
|
|
24989
|
+
* @param params.destination - Payment destination address
|
|
24990
|
+
* @param params.protocol - Explicit protocol override (skips protocolFlag)
|
|
24991
|
+
* @param params.amount - Payment amount
|
|
24992
|
+
* @param params.memo - Payment memo
|
|
24993
|
+
* @param params.paymentRequirements - X402 payment requirements
|
|
24994
|
+
* @param params.challenge - MPP challenge object
|
|
24995
|
+
* @returns AuthorizeResult with protocol and opaque credential
|
|
24996
|
+
*/
|
|
24997
|
+
async authorize(params) {
|
|
24998
|
+
const { account, userId, destination } = params;
|
|
24999
|
+
// Determine protocol
|
|
25000
|
+
const protocol = params.protocol
|
|
25001
|
+
?? (this.protocolFlag ? this.protocolFlag(userId, destination) : 'atxp');
|
|
25002
|
+
// Delegate to the account's authorize method
|
|
25003
|
+
return account.authorize({
|
|
25004
|
+
protocol,
|
|
25005
|
+
amount: params.amount,
|
|
25006
|
+
destination,
|
|
25007
|
+
memo: params.memo,
|
|
25008
|
+
paymentRequirements: params.paymentRequirements,
|
|
25009
|
+
challenge: params.challenge,
|
|
25010
|
+
});
|
|
25011
|
+
}
|
|
25012
|
+
}
|
|
25013
|
+
|
|
25014
|
+
function isX402Challenge(obj) {
|
|
25015
|
+
if (typeof obj !== 'object' || obj === null)
|
|
25016
|
+
return false;
|
|
25017
|
+
const candidate = obj;
|
|
25018
|
+
return (typeof candidate.x402Version !== 'undefined' &&
|
|
25019
|
+
Array.isArray(candidate.accepts));
|
|
25020
|
+
}
|
|
25021
|
+
/**
|
|
25022
|
+
* Protocol handler for X402 payment challenges.
|
|
25023
|
+
*
|
|
25024
|
+
* Detects HTTP 402 responses with x402Version in the JSON body.
|
|
25025
|
+
* Creates payment headers using the x402 library and retries the request.
|
|
25026
|
+
*/
|
|
25027
|
+
class X402ProtocolHandler {
|
|
25028
|
+
constructor(config) {
|
|
25029
|
+
this.protocol = 'x402';
|
|
25030
|
+
this.accountsServer = config?.accountsServer ?? 'https://accounts.atxp.ai';
|
|
25031
|
+
}
|
|
25032
|
+
async canHandle(response) {
|
|
25033
|
+
if (response.status !== 402)
|
|
25034
|
+
return false;
|
|
25035
|
+
try {
|
|
25036
|
+
const cloned = response.clone();
|
|
25037
|
+
const body = await cloned.text();
|
|
25038
|
+
const parsed = JSON.parse(body);
|
|
25039
|
+
return isX402Challenge(parsed);
|
|
25040
|
+
}
|
|
25041
|
+
catch {
|
|
25042
|
+
return false;
|
|
25043
|
+
}
|
|
25044
|
+
}
|
|
25045
|
+
async handlePaymentChallenge(response, originalRequest, config) {
|
|
25046
|
+
const { account, logger, fetchFn, approvePayment, onPayment, onPaymentFailure } = config;
|
|
25047
|
+
const responseBody = await response.text();
|
|
25048
|
+
let paymentChallenge;
|
|
25049
|
+
try {
|
|
25050
|
+
paymentChallenge = JSON.parse(responseBody);
|
|
25051
|
+
}
|
|
25052
|
+
catch {
|
|
25053
|
+
logger.error('X402: failed to parse challenge body');
|
|
25054
|
+
return null;
|
|
25055
|
+
}
|
|
25056
|
+
if (!isX402Challenge(paymentChallenge)) {
|
|
25057
|
+
return null;
|
|
25058
|
+
}
|
|
25059
|
+
try {
|
|
25060
|
+
const { selectPaymentRequirements } = await import('x402/client');
|
|
25061
|
+
const selectedPaymentRequirements = selectPaymentRequirements(paymentChallenge.accepts, undefined, 'exact');
|
|
25062
|
+
if (!selectedPaymentRequirements) {
|
|
25063
|
+
logger.info('X402: no suitable payment option found');
|
|
25064
|
+
return this.reconstructResponse(responseBody, response);
|
|
25065
|
+
}
|
|
25066
|
+
const amountInUsdc = Number(selectedPaymentRequirements.maxAmountRequired) / (10 ** 6);
|
|
25067
|
+
const network = selectedPaymentRequirements.network;
|
|
25068
|
+
logger.debug(`X402: payment required: ${amountInUsdc} USDC on ${network} to ${selectedPaymentRequirements.payTo}`);
|
|
25069
|
+
const url = typeof originalRequest.url === 'string' ? originalRequest.url : originalRequest.url.toString();
|
|
25070
|
+
const accountId = await account.getAccountId();
|
|
25071
|
+
const prospectivePayment = {
|
|
25072
|
+
accountId,
|
|
25073
|
+
resourceUrl: url,
|
|
25074
|
+
resourceName: selectedPaymentRequirements.description || url,
|
|
25075
|
+
currency: 'USDC',
|
|
25076
|
+
amount: new BigNumber(amountInUsdc),
|
|
25077
|
+
iss: selectedPaymentRequirements.payTo
|
|
25078
|
+
};
|
|
25079
|
+
const approved = await approvePayment(prospectivePayment);
|
|
25080
|
+
if (!approved) {
|
|
25081
|
+
logger.info('X402: payment not approved');
|
|
25082
|
+
const error = new Error('Payment not approved');
|
|
25083
|
+
await onPaymentFailure({
|
|
25084
|
+
payment: prospectivePayment,
|
|
25085
|
+
error,
|
|
25086
|
+
attemptedNetworks: [network],
|
|
25087
|
+
failureReasons: new Map([[network, error]]),
|
|
25088
|
+
retryable: true,
|
|
25089
|
+
timestamp: new Date()
|
|
25090
|
+
});
|
|
25091
|
+
return this.reconstructResponse(responseBody, response);
|
|
25092
|
+
}
|
|
25093
|
+
// Authorize via account.authorize() — ATXPAccount calls the accounts
|
|
25094
|
+
// service, BaseAccount signs locally. No fallback — each account type
|
|
25095
|
+
// handles authorization according to its capabilities.
|
|
25096
|
+
const client = new PaymentClient({
|
|
25097
|
+
accountsServer: this.accountsServer,
|
|
25098
|
+
logger,
|
|
25099
|
+
fetchFn,
|
|
25100
|
+
});
|
|
25101
|
+
const authorizeResult = await client.authorize({
|
|
25102
|
+
account,
|
|
25103
|
+
userId: accountId,
|
|
25104
|
+
destination: url,
|
|
25105
|
+
protocol: 'x402',
|
|
25106
|
+
paymentRequirements: selectedPaymentRequirements,
|
|
25107
|
+
});
|
|
25108
|
+
const paymentHeader = authorizeResult.credential;
|
|
25109
|
+
// Retry with X-PAYMENT header
|
|
25110
|
+
const retryHeaders = buildPaymentHeaders({ protocol: 'x402', credential: paymentHeader }, originalRequest.init?.headers);
|
|
25111
|
+
const retryInit = { ...originalRequest.init, headers: retryHeaders };
|
|
25112
|
+
logger.info('X402: retrying request with X-PAYMENT header');
|
|
25113
|
+
const retryResponse = await fetchFn(originalRequest.url, retryInit);
|
|
25114
|
+
if (retryResponse.ok) {
|
|
25115
|
+
logger.info('X402: payment accepted');
|
|
25116
|
+
await onPayment({
|
|
25117
|
+
payment: prospectivePayment,
|
|
25118
|
+
transactionHash: paymentHeader.substring(0, 66),
|
|
25119
|
+
network
|
|
25120
|
+
});
|
|
25121
|
+
}
|
|
25122
|
+
else {
|
|
25123
|
+
logger.warn(`X402: request failed after payment with status ${retryResponse.status}`);
|
|
25124
|
+
const error = new Error(`Request failed with status ${retryResponse.status}`);
|
|
25125
|
+
await onPaymentFailure({
|
|
25126
|
+
payment: prospectivePayment,
|
|
25127
|
+
error,
|
|
25128
|
+
attemptedNetworks: [network],
|
|
25129
|
+
failureReasons: new Map([[network, error]]),
|
|
25130
|
+
retryable: false,
|
|
25131
|
+
timestamp: new Date()
|
|
25132
|
+
});
|
|
25133
|
+
}
|
|
25134
|
+
return retryResponse;
|
|
25135
|
+
}
|
|
25136
|
+
catch (error) {
|
|
25137
|
+
logger.error(`X402: failed to handle payment challenge: ${error}`);
|
|
25138
|
+
if (isX402Challenge(paymentChallenge) && paymentChallenge.accepts[0]) {
|
|
25139
|
+
const firstOption = paymentChallenge.accepts[0];
|
|
25140
|
+
const amount = firstOption.maxAmountRequired ? Number(firstOption.maxAmountRequired) / (10 ** 6) : 0;
|
|
25141
|
+
const url = typeof originalRequest.url === 'string' ? originalRequest.url : originalRequest.url.toString();
|
|
25142
|
+
const accountId = await account.getAccountId();
|
|
25143
|
+
const errorNetwork = firstOption.network || 'unknown';
|
|
25144
|
+
const typedError = error;
|
|
25145
|
+
const isRetryable = typedError instanceof ATXPPaymentError ? typedError.retryable : true;
|
|
25146
|
+
await onPaymentFailure({
|
|
25147
|
+
payment: {
|
|
25148
|
+
accountId,
|
|
25149
|
+
resourceUrl: url,
|
|
25150
|
+
resourceName: firstOption.description || url,
|
|
25151
|
+
currency: 'USDC',
|
|
25152
|
+
amount: new BigNumber(amount),
|
|
25153
|
+
iss: firstOption.payTo || ''
|
|
25154
|
+
},
|
|
25155
|
+
error: typedError,
|
|
25156
|
+
attemptedNetworks: [errorNetwork],
|
|
25157
|
+
failureReasons: new Map([[errorNetwork, typedError]]),
|
|
25158
|
+
retryable: isRetryable,
|
|
25159
|
+
timestamp: new Date()
|
|
25160
|
+
});
|
|
25161
|
+
}
|
|
25162
|
+
return this.reconstructResponse(responseBody, response);
|
|
25163
|
+
}
|
|
25164
|
+
}
|
|
25165
|
+
reconstructResponse(body, original) {
|
|
25166
|
+
return new Response(body, {
|
|
25167
|
+
status: original.status,
|
|
25168
|
+
statusText: original.statusText,
|
|
25169
|
+
headers: original.headers
|
|
25170
|
+
});
|
|
25171
|
+
}
|
|
25172
|
+
}
|
|
25173
|
+
|
|
25174
|
+
/**
|
|
25175
|
+
* Protocol handler for ATXP-MCP payment challenges.
|
|
25176
|
+
*
|
|
25177
|
+
* Detects JSON-RPC errors with code -30402 (PAYMENT_REQUIRED_ERROR_CODE) in
|
|
25178
|
+
* MCP responses (both SSE and JSON formats).
|
|
25179
|
+
*/
|
|
25180
|
+
class ATXPProtocolHandler {
|
|
25181
|
+
constructor() {
|
|
25182
|
+
this.protocol = 'atxp';
|
|
25183
|
+
}
|
|
25184
|
+
async canHandle(response) {
|
|
25185
|
+
// ATXP-MCP challenges are embedded in MCP JSON-RPC responses,
|
|
25186
|
+
// not at the HTTP level. We detect them by looking for error code -30402.
|
|
25187
|
+
try {
|
|
25188
|
+
const cloned = response.clone();
|
|
25189
|
+
const body = await cloned.text();
|
|
25190
|
+
if (body.length === 0)
|
|
25191
|
+
return false;
|
|
25192
|
+
const paymentRequests = await this.extractPaymentRequests(body);
|
|
25193
|
+
return paymentRequests.length > 0;
|
|
25194
|
+
}
|
|
25195
|
+
catch {
|
|
25196
|
+
return false;
|
|
25197
|
+
}
|
|
25198
|
+
}
|
|
25199
|
+
async handlePaymentChallenge(response, _originalRequest, config) {
|
|
25200
|
+
const { logger } = config;
|
|
25201
|
+
try {
|
|
25202
|
+
const body = await response.clone().text();
|
|
25203
|
+
const paymentRequests = await this.extractPaymentRequests(body);
|
|
25204
|
+
if (paymentRequests.length === 0) {
|
|
25205
|
+
return null;
|
|
25206
|
+
}
|
|
25207
|
+
// ATXP-MCP payment challenges are handled through the fetcher's
|
|
25208
|
+
// checkForATXPResponse → handlePaymentRequestError flow, not at
|
|
25209
|
+
// the HTTP protocol handler level. Return null so the fetcher's
|
|
25210
|
+
// existing MCP payment flow picks it up.
|
|
25211
|
+
// This handler's role in the strategy pattern is detection via
|
|
25212
|
+
// canHandle() for protocol selection (e.g., protocolFlag), not
|
|
25213
|
+
// direct payment handling.
|
|
25214
|
+
if (paymentRequests.length > 1) {
|
|
25215
|
+
logger.warn(`ATXP: multiple payment requirements found in MCP response. ` +
|
|
25216
|
+
`The client does not support multiple payment requirements. ` +
|
|
25217
|
+
`${paymentRequests.map(pr => pr.url).join(', ')}`);
|
|
25218
|
+
}
|
|
25219
|
+
else {
|
|
25220
|
+
logger.info(`ATXP: payment requirement found in MCP response - ${paymentRequests[0].url}`);
|
|
25221
|
+
}
|
|
25222
|
+
return null;
|
|
25223
|
+
}
|
|
25224
|
+
catch (error) {
|
|
25225
|
+
logger.error(`ATXP: error checking for payment requirements: ${error}`);
|
|
25226
|
+
return null;
|
|
25227
|
+
}
|
|
25228
|
+
}
|
|
25229
|
+
async extractPaymentRequests(body) {
|
|
25230
|
+
try {
|
|
25231
|
+
if (isSSEResponse(body)) {
|
|
25232
|
+
const messages = await parseMcpMessages(body);
|
|
25233
|
+
return messages.flatMap(message => parsePaymentRequests(message)).filter((pr) => pr !== null);
|
|
25234
|
+
}
|
|
25235
|
+
else {
|
|
25236
|
+
const json = JSON.parse(body);
|
|
25237
|
+
const messages = await parseMcpMessages(json);
|
|
25238
|
+
return messages.flatMap(message => parsePaymentRequests(message)).filter((pr) => pr !== null);
|
|
25239
|
+
}
|
|
25240
|
+
}
|
|
25241
|
+
catch {
|
|
25242
|
+
return [];
|
|
25243
|
+
}
|
|
25244
|
+
}
|
|
25245
|
+
}
|
|
25246
|
+
|
|
25247
|
+
/**
|
|
25248
|
+
* Protocol handler for MPP (Machine Payments Protocol) payment challenges.
|
|
25249
|
+
*
|
|
25250
|
+
* Detects MPP challenges in two forms:
|
|
25251
|
+
* 1. HTTP level: HTTP 402 with WWW-Authenticate: Payment header
|
|
25252
|
+
* 2. MCP level: JSON-RPC error with code -32042 containing MPP data
|
|
25253
|
+
*
|
|
25254
|
+
* Handles the challenge by calling /authorize/mpp on the accounts service
|
|
25255
|
+
* and retrying with an Authorization: Payment header.
|
|
25256
|
+
*/
|
|
25257
|
+
class MPPProtocolHandler {
|
|
25258
|
+
constructor(config) {
|
|
25259
|
+
this.protocol = 'mpp';
|
|
25260
|
+
this.accountsServer = config?.accountsServer ?? 'https://accounts.atxp.ai';
|
|
25261
|
+
}
|
|
25262
|
+
async canHandle(response) {
|
|
25263
|
+
if (hasMPPChallenge(response))
|
|
25264
|
+
return true;
|
|
25265
|
+
return hasMPPMCPError(response);
|
|
25266
|
+
}
|
|
25267
|
+
async handlePaymentChallenge(response, originalRequest, config) {
|
|
25268
|
+
const { account, logger, approvePayment } = config;
|
|
25269
|
+
// Extract the challenge and body text from the response
|
|
25270
|
+
const extracted = await this.extractChallenge(response, logger);
|
|
25271
|
+
if (!extracted) {
|
|
25272
|
+
logger.error('MPP: failed to extract challenge from response');
|
|
25273
|
+
return null;
|
|
25274
|
+
}
|
|
25275
|
+
const { challenge, bodyText } = extracted;
|
|
25276
|
+
const url = typeof originalRequest.url === 'string'
|
|
25277
|
+
? originalRequest.url
|
|
25278
|
+
: originalRequest.url.toString();
|
|
25279
|
+
// Build prospective payment for approval
|
|
25280
|
+
const accountId = await account.getAccountId();
|
|
25281
|
+
const prospectivePayment = this.buildProspectivePayment(challenge, url, accountId);
|
|
25282
|
+
// Ask for approval
|
|
25283
|
+
const approved = await approvePayment(prospectivePayment);
|
|
25284
|
+
if (!approved) {
|
|
25285
|
+
logger.info('MPP: payment not approved');
|
|
25286
|
+
await this.reportFailure(config, prospectivePayment, new Error('Payment not approved'), challenge.network, true);
|
|
25287
|
+
return this.reconstructResponse(bodyText, response);
|
|
25288
|
+
}
|
|
25289
|
+
return this.authorizeAndRetry(challenge, prospectivePayment, originalRequest, config, bodyText, response);
|
|
25290
|
+
}
|
|
25291
|
+
/**
|
|
25292
|
+
* Extract MPP challenge from response - tries HTTP header first, then MCP error body.
|
|
25293
|
+
* Returns both the challenge and the body text to avoid double-consumption.
|
|
25294
|
+
*/
|
|
25295
|
+
async extractChallenge(response, logger) {
|
|
25296
|
+
// Read body once upfront to avoid double consumption (response.text() can only be called once)
|
|
25297
|
+
let bodyText = '';
|
|
25298
|
+
try {
|
|
25299
|
+
bodyText = await response.text();
|
|
25300
|
+
}
|
|
25301
|
+
catch {
|
|
25302
|
+
// Body may not be available
|
|
25303
|
+
}
|
|
25304
|
+
// Try HTTP header first
|
|
25305
|
+
const header = response.headers.get('WWW-Authenticate');
|
|
25306
|
+
if (header) {
|
|
25307
|
+
const challenge = parseMPPHeader(header);
|
|
25308
|
+
if (challenge) {
|
|
25309
|
+
logger.debug('MPP: parsed challenge from WWW-Authenticate header');
|
|
25310
|
+
return { challenge, bodyText };
|
|
25311
|
+
}
|
|
25312
|
+
}
|
|
25313
|
+
// Try MCP error body (use clone since response body may have been consumed above)
|
|
25314
|
+
try {
|
|
25315
|
+
const parsed = JSON.parse(bodyText);
|
|
25316
|
+
if (typeof parsed === 'object' &&
|
|
25317
|
+
parsed !== null &&
|
|
25318
|
+
typeof parsed.error === 'object' &&
|
|
25319
|
+
parsed.error !== null &&
|
|
25320
|
+
parsed.error.code === MPP_ERROR_CODE) {
|
|
25321
|
+
const challenge = parseMPPFromMCPError(parsed.error.data);
|
|
25322
|
+
if (challenge) {
|
|
25323
|
+
logger.debug('MPP: parsed challenge from MCP error body');
|
|
25324
|
+
return { challenge, bodyText };
|
|
25325
|
+
}
|
|
25326
|
+
}
|
|
25327
|
+
}
|
|
25328
|
+
catch {
|
|
25329
|
+
// Not JSON or malformed
|
|
25330
|
+
}
|
|
25331
|
+
return null;
|
|
25332
|
+
}
|
|
25333
|
+
/**
|
|
25334
|
+
* Build a ProspectivePayment from an MPP challenge.
|
|
25335
|
+
*/
|
|
25336
|
+
buildProspectivePayment(challenge, url, accountId) {
|
|
25337
|
+
const amountNum = Number(challenge.amount) / (10 ** 6);
|
|
25338
|
+
return {
|
|
25339
|
+
accountId,
|
|
25340
|
+
resourceUrl: url,
|
|
25341
|
+
resourceName: url,
|
|
25342
|
+
currency: challenge.currency,
|
|
25343
|
+
amount: new BigNumber(amountNum),
|
|
25344
|
+
iss: challenge.recipient,
|
|
25345
|
+
};
|
|
25346
|
+
}
|
|
25347
|
+
/**
|
|
25348
|
+
* Report a payment failure via the onPaymentFailure callback.
|
|
25349
|
+
*/
|
|
25350
|
+
async reportFailure(config, payment, error, network, retryable) {
|
|
25351
|
+
await config.onPaymentFailure({
|
|
25352
|
+
payment,
|
|
25353
|
+
error,
|
|
25354
|
+
attemptedNetworks: [network],
|
|
25355
|
+
failureReasons: new Map([[network, error]]),
|
|
25356
|
+
retryable,
|
|
25357
|
+
timestamp: new Date(),
|
|
25358
|
+
});
|
|
25359
|
+
}
|
|
25360
|
+
/**
|
|
25361
|
+
* Call /authorize/mpp on accounts service and retry the original request with the credential.
|
|
25362
|
+
*/
|
|
25363
|
+
async authorizeAndRetry(challenge, prospectivePayment, originalRequest, config, bodyText, originalResponse) {
|
|
25364
|
+
const { account, logger, fetchFn, onPayment } = config;
|
|
25365
|
+
try {
|
|
25366
|
+
logger.debug('MPP: calling /authorize/mpp on accounts service');
|
|
25367
|
+
const client = new PaymentClient({
|
|
25368
|
+
accountsServer: this.accountsServer,
|
|
25369
|
+
logger,
|
|
25370
|
+
fetchFn,
|
|
25371
|
+
});
|
|
25372
|
+
const accountId = await account.getAccountId();
|
|
25373
|
+
let authorizeResult;
|
|
25374
|
+
try {
|
|
25375
|
+
authorizeResult = await client.authorize({
|
|
25376
|
+
account,
|
|
25377
|
+
userId: accountId,
|
|
25378
|
+
destination: typeof originalRequest.url === 'string' ? originalRequest.url : originalRequest.url.toString(),
|
|
25379
|
+
protocol: 'mpp',
|
|
25380
|
+
challenge,
|
|
25381
|
+
});
|
|
25382
|
+
}
|
|
25383
|
+
catch (authorizeError) {
|
|
25384
|
+
// AuthorizationError = server rejected the request (HTTP error from accounts)
|
|
25385
|
+
// Other errors = data validation or network failure
|
|
25386
|
+
if (authorizeError instanceof AuthorizationError) {
|
|
25387
|
+
logger.debug(`MPP: /authorize/mpp rejected (${authorizeError.statusCode}), returning original response`);
|
|
25388
|
+
return this.reconstructResponse(bodyText, originalResponse);
|
|
25389
|
+
}
|
|
25390
|
+
throw authorizeError;
|
|
25391
|
+
}
|
|
25392
|
+
const retryHeaders = buildPaymentHeaders(authorizeResult, originalRequest.init?.headers);
|
|
25393
|
+
const retryInit = { ...originalRequest.init, headers: retryHeaders };
|
|
25394
|
+
logger.info('MPP: retrying request with Authorization: Payment header');
|
|
25395
|
+
const retryResponse = await fetchFn(originalRequest.url, retryInit);
|
|
25396
|
+
if (retryResponse.ok) {
|
|
25397
|
+
logger.info('MPP: payment accepted');
|
|
25398
|
+
await onPayment({ payment: prospectivePayment, transactionHash: challenge.id, network: challenge.network });
|
|
25399
|
+
}
|
|
25400
|
+
else {
|
|
25401
|
+
logger.warn(`MPP: request failed after payment with status ${retryResponse.status}`);
|
|
25402
|
+
await this.reportFailure(config, prospectivePayment, new Error(`Request failed with status ${retryResponse.status}`), challenge.network, false);
|
|
25403
|
+
}
|
|
25404
|
+
return retryResponse;
|
|
25405
|
+
}
|
|
25406
|
+
catch (error) {
|
|
25407
|
+
logger.error(`MPP: failed to handle payment challenge: ${error}`);
|
|
25408
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
25409
|
+
await this.reportFailure(config, prospectivePayment, cause, challenge.network, true);
|
|
25410
|
+
return this.reconstructResponse(bodyText, originalResponse);
|
|
25411
|
+
}
|
|
25412
|
+
}
|
|
25413
|
+
reconstructResponse(body, original) {
|
|
25414
|
+
return new Response(body || null, {
|
|
25415
|
+
status: original.status,
|
|
25416
|
+
statusText: original.statusText,
|
|
25417
|
+
headers: original.headers,
|
|
25418
|
+
});
|
|
25419
|
+
}
|
|
25420
|
+
}
|
|
25421
|
+
|
|
25422
|
+
export { ATXPDestinationMaker, ATXPLocalAccount, ATXPPaymentError, ATXPProtocolHandler, DEFAULT_CLIENT_CONFIG, GasEstimationError, InsufficientFundsError, MPPProtocolHandler, OAuthAuthenticationRequiredError, OAuthClient, POLYGON_AMOY, POLYGON_MAINNET, PassthroughDestinationMaker, PaymentClient, PaymentExpiredError, PaymentNetworkError, PaymentServerError, RpcError, TransactionRevertedError, USDC_CONTRACT_ADDRESS_POLYGON_AMOY, USDC_CONTRACT_ADDRESS_POLYGON_MAINNET, USDC_CONTRACT_ADDRESS_WORLD_MAINNET, USDC_CONTRACT_ADDRESS_WORLD_SEPOLIA, UnsupportedCurrencyError, UserRejectedError, WORLD_CHAIN_MAINNET, WORLD_CHAIN_SEPOLIA, X402ProtocolHandler, atxpClient, atxpFetch, buildClientConfig, buildPaymentHeaders, buildStreamableTransport, getPolygonAmoyWithRPC, getPolygonByChainId, getPolygonMainnetWithRPC, getPolygonUSDCAddress, getWorldChainByChainId, getWorldChainMainnetWithRPC, getWorldChainSepoliaWithRPC, getWorldChainUSDCAddress };
|
|
24872
25423
|
//# sourceMappingURL=index.js.map
|