From 6ee9177f734f5796168d461d74bf5ced08a47248 Mon Sep 17 00:00:00 2001 From: Mohsen Ghaemaghami Date: Mon, 2 Feb 2026 19:36:28 +0300 Subject: [PATCH 1/8] feat: add support for custom HTTP agents and proxy configuration in Braintree payment provider --- .../src/core/braintree-base.ts | 76 ++++++++++++++++--- .../payment-braintree/src/types/index.ts | 14 ++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index d20331b1..bdadae9c 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -45,9 +45,11 @@ import type { } from '@medusajs/types'; import type { Transaction, TransactionNotification, TransactionStatus } from 'braintree'; import Braintree from 'braintree'; +import http from 'http'; +import https from 'https'; import { z } from 'zod'; import { formatToTwoDecimalString } from '../../../../utils/format-amount'; -import type { BraintreeOptions, CustomFields } from '../types'; +import type { BraintreeOptions, CustomFields, HttpAgentConfig } from '../types'; export type BraintreeConstructorArgs = Record & { logger: Logger; @@ -181,6 +183,54 @@ class BraintreeBase extends AbstractPaymentProvider { return result.data as BraintreePaymentSessionData; } + private createHttpAgent(): http.Agent | https.Agent | undefined { + // Backward compatibility: if customHttpAgent is directly provided, use it + if (this.options_.customHttpAgent) { + return this.options_.customHttpAgent; + } + + // If proxy URL is provided, try to create a proxy agent + if (this.options_.proxyUrl) { + try { + // Try to use https-proxy-agent (most common for HTTPS proxies) + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { HttpsProxyAgent } = require('https-proxy-agent'); + + // Create proxy agent with URL string + // Proxy URL format: http://[username:password@]proxy.example.com:8080 + return new HttpsProxyAgent(this.options_.proxyUrl); + } catch (error) { + this.logger.warn( + 'https-proxy-agent package not found. Install it with: npm install https-proxy-agent. Falling back to regular agent.', + ); + // Fall through to create regular agent if proxy agent creation fails + } + } + + // If httpAgent config is provided, create a regular HTTPS agent + if (this.options_.httpAgent) { + const agentOptions: https.AgentOptions = { + keepAlive: this.options_.httpAgent.keepAlive ?? true, + keepAliveMsecs: this.options_.httpAgent.keepAliveMsecs ?? 1000, + maxSockets: this.options_.httpAgent.maxSockets, + maxFreeSockets: this.options_.httpAgent.maxFreeSockets, + timeout: this.options_.httpAgent.timeout, + rejectUnauthorized: this.options_.httpAgent.rejectUnauthorized ?? true, + }; + + // Remove undefined values + Object.keys(agentOptions).forEach((key) => { + if (agentOptions[key as keyof https.AgentOptions] === undefined) { + delete agentOptions[key as keyof https.AgentOptions]; + } + }); + + return new https.Agent(agentOptions); + } + + return undefined; + } + init(): void { const envKey = (this.options_.environment || 'sandbox').toLowerCase(); const envMap: Record = { @@ -191,14 +241,22 @@ class BraintreeBase extends AbstractPaymentProvider { }; const environment = envMap[envKey] ?? Braintree.Environment.Sandbox; - this.gateway = - this.gateway || - new Braintree.BraintreeGateway({ - environment, - merchantId: this.options_.merchantId!, - publicKey: this.options_.publicKey!, - privateKey: this.options_.privateKey!, - }); + const gatewayConfig: Braintree.GatewayConfig = { + environment, + merchantId: this.options_.merchantId!, + publicKey: this.options_.publicKey!, + privateKey: this.options_.privateKey!, + }; + + // Create and add HTTP agent if configured + // Using 'as any' because TypeScript definitions don't include customHttpAgent, + // but the runtime Braintree library supports it + const httpAgent = this.createHttpAgent(); + if (httpAgent) { + (gatewayConfig as any).customHttpAgent = httpAgent; + } + + this.gateway = this.gateway || new Braintree.BraintreeGateway(gatewayConfig); } static validateOptions(options: BraintreeOptions): void { diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts index ec2923dd..50cdc2fd 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts @@ -1,4 +1,15 @@ import type Braintree from 'braintree'; +import type { Agent } from 'http'; +import type { Agent as HttpsAgent } from 'https'; + +export interface HttpAgentConfig { + keepAlive?: boolean; + keepAliveMsecs?: number; + maxSockets?: number; + maxFreeSockets?: number; + timeout?: number; + rejectUnauthorized?: boolean; +} export interface BraintreeOptions extends Braintree.ClientGatewayConfig { defaultCurrencyCode?: string; @@ -11,6 +22,9 @@ export interface BraintreeOptions extends Braintree.ClientGatewayConfig { webhookSecret: string; autoCapture: boolean; allowRefundOnRefunded?: boolean; + httpAgent?: HttpAgentConfig; + proxyUrl?: string; + customHttpAgent?: Agent | HttpsAgent; } export const PaymentProviderKeys = { From 3ea8bf5a95ad1b3981eda127eeae98e48579d3aa Mon Sep 17 00:00:00 2001 From: Mohsen Ghaemaghami Date: Tue, 21 Apr 2026 18:01:46 +0300 Subject: [PATCH 2/8] docs: update README and types to include custom HTTP agent and proxy configuration options for Braintree payment provider --- plugins/braintree-payment/README.md | 5 +++++ .../src/providers/payment-braintree/src/types/index.ts | 3 +++ 2 files changed, 8 insertions(+) diff --git a/plugins/braintree-payment/README.md b/plugins/braintree-payment/README.md index 7d147143..c9df66e1 100644 --- a/plugins/braintree-payment/README.md +++ b/plugins/braintree-payment/README.md @@ -84,11 +84,16 @@ dependencies:[Modules.CACHE] - **savePaymentMethod**: Save payment methods for future use (default: `true`). - **autoCapture**: Automatically capture payments (default: `true`). - **allowRefundOnRefunded**: Allow refund attempts on already-refunded imported transactions (default: `false`). +- **customHttpAgent**: Optional pre-configured Node `http.Agent` or `https.Agent` used for all Braintree API requests. If set, this takes highest precedence. +- **proxyUrl**: Optional proxy URL (for example `http://user:pass@proxy.example.com:8080`). When provided, the provider will try to create an HTTPS proxy agent using `https-proxy-agent`. +- **httpAgent**: Optional HTTPS agent configuration object used to create a standard `https.Agent` when `customHttpAgent` and `proxyUrl` are not provided. > **Note:** > - `autoCapture`: If set to `true`, payments are captured automatically after authorization. > - `savePaymentMethod`: If set to `true`, customer payment methods are saved for future use. > - `allowRefundOnRefunded`: If set to `true`, the imported payment provider will gracefully handle refund attempts on transactions that have already been refunded in Braintree. Instead of throwing an error, it will log a warning and record the refund locally only. This is useful when orders are imported and later refunded directly in Braintree. +> - HTTP agent precedence is: `customHttpAgent` -> `proxyUrl` -> `httpAgent`. +> - If `proxyUrl` is set, install `https-proxy-agent` in your project so proxy agent creation succeeds. ### 3D Secure Setup diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts index 50cdc2fd..535b1880 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts @@ -22,8 +22,11 @@ export interface BraintreeOptions extends Braintree.ClientGatewayConfig { webhookSecret: string; autoCapture: boolean; allowRefundOnRefunded?: boolean; + /** Lowest precedence agent config, used to create a standard https.Agent. */ httpAgent?: HttpAgentConfig; + /** Optional proxy URL used to create an HTTPS proxy agent. */ proxyUrl?: string; + /** Highest precedence: pass a fully constructed Node HTTP(S) agent directly. */ customHttpAgent?: Agent | HttpsAgent; } From 9fcaa75f155f4605f8b85af3f7ec3827d030aecc Mon Sep 17 00:00:00 2001 From: Mohsen Ghaemaghami Date: Tue, 21 Apr 2026 18:38:14 +0300 Subject: [PATCH 3/8] chore: bump version to 0.1.1 for braintree payment plugin --- plugins/braintree-payment/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/braintree-payment/package.json b/plugins/braintree-payment/package.json index b3212a91..465b1962 100644 --- a/plugins/braintree-payment/package.json +++ b/plugins/braintree-payment/package.json @@ -1,6 +1,6 @@ { "name": "@lambdacurry/medusa-payment-braintree", - "version": "0.1.0", + "version": "0.1.1", "description": "Braintree plugin for Medusa", "author": "Lambda Curry (https://lambdacurry.dev)", "license": "MIT", From e2bcff7fcc3a2fa871123fdfe699ee3c556c8d9c Mon Sep 17 00:00:00 2001 From: Mohsen Ghaemaghami Date: Tue, 21 Apr 2026 18:43:53 +0300 Subject: [PATCH 4/8] feat: add validation for proxyUrl and httpAgent options in Braintree plugin --- .../src/core/braintree-base.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index 27d7fb45..ed132c66 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -384,6 +384,81 @@ class BraintreeBase extends AbstractPaymentProvider { ); } } + + if (isDefined(options.proxyUrl)) { + if (typeof options.proxyUrl !== 'string' || !options.proxyUrl.trim()) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + 'Option "proxyUrl" must be a non-empty string in Braintree plugin', + ); + } + + try { + new URL(options.proxyUrl); + } catch { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + `Option "proxyUrl" must be a valid URL in Braintree plugin: "${options.proxyUrl}"`, + ); + } + } + + if (isDefined(options.httpAgent)) { + if ( + typeof options.httpAgent !== 'object' || + options.httpAgent === null || + Array.isArray(options.httpAgent) + ) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + 'Option "httpAgent" must be an object in Braintree plugin', + ); + } + + const { keepAlive, keepAliveMsecs, maxSockets, maxFreeSockets, timeout, rejectUnauthorized } = options.httpAgent; + + if (isDefined(keepAlive) && typeof keepAlive !== 'boolean') { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + 'Option "httpAgent.keepAlive" must be a boolean in Braintree plugin', + ); + } + + if (isDefined(keepAliveMsecs) && (typeof keepAliveMsecs !== 'number' || keepAliveMsecs < 0)) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + 'Option "httpAgent.keepAliveMsecs" must be a number greater than or equal to 0 in Braintree plugin', + ); + } + + if (isDefined(maxSockets) && (typeof maxSockets !== 'number' || maxSockets < 0)) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + 'Option "httpAgent.maxSockets" must be a number greater than or equal to 0 in Braintree plugin', + ); + } + + if (isDefined(maxFreeSockets) && (typeof maxFreeSockets !== 'number' || maxFreeSockets < 0)) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + 'Option "httpAgent.maxFreeSockets" must be a number greater than or equal to 0 in Braintree plugin', + ); + } + + if (isDefined(timeout) && (typeof timeout !== 'number' || timeout < 0)) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + 'Option "httpAgent.timeout" must be a number greater than or equal to 0 in Braintree plugin', + ); + } + + if (isDefined(rejectUnauthorized) && typeof rejectUnauthorized !== 'boolean') { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + 'Option "httpAgent.rejectUnauthorized" must be a boolean in Braintree plugin', + ); + } + } } async capturePayment(input: CapturePaymentInput): Promise { From 05e03c98fa55f86f3e79137669f830b4c9bfa56f Mon Sep 17 00:00:00 2001 From: Mohsen Ghaemaghami Date: Tue, 21 Apr 2026 18:44:51 +0300 Subject: [PATCH 5/8] fix: improve error handling for missing https-proxy-agent module in Braintree plugin --- .../payment-braintree/src/core/braintree-base.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index ed132c66..e790b634 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -275,15 +275,17 @@ class BraintreeBase extends AbstractPaymentProvider { // Proxy URL format: http://[username:password@]proxy.example.com:8080 return new HttpsProxyAgent(this.options_.proxyUrl); } catch (error) { - const isModuleNotFound = - typeof error === 'object' && - error !== null && - ('code' in error ? (error as { code?: string }).code === 'MODULE_NOT_FOUND' : false); const isRequireMissingProxyAgent = error instanceof Error && /Cannot find module ['"]https-proxy-agent['"]/.test(error.message); + const isModuleNotFoundProxyAgent = + typeof error === 'object' && + error !== null && + ('code' in error ? (error as { code?: string }).code === 'MODULE_NOT_FOUND' : false) && + error instanceof Error && + error.message.includes('https-proxy-agent'); - if (isModuleNotFound || isRequireMissingProxyAgent) { + if (isRequireMissingProxyAgent || isModuleNotFoundProxyAgent) { this.logger.warn( 'https-proxy-agent package not found. Install it with: npm install https-proxy-agent. Falling back to regular agent.', ); From c23fe4c60ad1ff9ee076c1894a182d0b76d3a75e Mon Sep 17 00:00:00 2001 From: LC Mohsen Date: Tue, 21 Apr 2026 19:25:44 +0300 Subject: [PATCH 6/8] Update plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../src/providers/payment-braintree/src/core/braintree-base.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index e790b634..ab9c27b8 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -400,9 +400,10 @@ class BraintreeBase extends AbstractPaymentProvider { } catch { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, - `Option "proxyUrl" must be a valid URL in Braintree plugin: "${options.proxyUrl}"`, + 'Option "proxyUrl" must be a valid URL in Braintree plugin', ); } + } } if (isDefined(options.httpAgent)) { From baafd3f58b71eef78c99c8f66f3ae40e25c3327f Mon Sep 17 00:00:00 2001 From: Mohsen Ghaemaghami Date: Tue, 21 Apr 2026 19:40:56 +0300 Subject: [PATCH 7/8] fix: enhance validation for httpAgent options in Braintree plugin to ensure non-negative values --- .../payment-braintree/src/core/braintree-base.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index e790b634..fff60f94 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -419,6 +419,9 @@ class BraintreeBase extends AbstractPaymentProvider { const { keepAlive, keepAliveMsecs, maxSockets, maxFreeSockets, timeout, rejectUnauthorized } = options.httpAgent; + const isNonNegativeNumber = (value: unknown): value is number => + typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value) && value >= 0; + if (isDefined(keepAlive) && typeof keepAlive !== 'boolean') { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, @@ -426,28 +429,28 @@ class BraintreeBase extends AbstractPaymentProvider { ); } - if (isDefined(keepAliveMsecs) && (typeof keepAliveMsecs !== 'number' || keepAliveMsecs < 0)) { + if (isDefined(keepAliveMsecs) && !isNonNegativeNumber(keepAliveMsecs)) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, 'Option "httpAgent.keepAliveMsecs" must be a number greater than or equal to 0 in Braintree plugin', ); } - if (isDefined(maxSockets) && (typeof maxSockets !== 'number' || maxSockets < 0)) { + if (isDefined(maxSockets) && !isNonNegativeNumber(maxSockets)) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, 'Option "httpAgent.maxSockets" must be a number greater than or equal to 0 in Braintree plugin', ); } - if (isDefined(maxFreeSockets) && (typeof maxFreeSockets !== 'number' || maxFreeSockets < 0)) { + if (isDefined(maxFreeSockets) && !isNonNegativeNumber(maxFreeSockets)) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, 'Option "httpAgent.maxFreeSockets" must be a number greater than or equal to 0 in Braintree plugin', ); } - if (isDefined(timeout) && (typeof timeout !== 'number' || timeout < 0)) { + if (isDefined(timeout) && !isNonNegativeNumber(timeout)) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, 'Option "httpAgent.timeout" must be a number greater than or equal to 0 in Braintree plugin', From c4a49440e22074c253785955481a0e53f052020a Mon Sep 17 00:00:00 2001 From: Mohsen Ghaemaghami Date: Tue, 21 Apr 2026 19:41:30 +0300 Subject: [PATCH 8/8] fix: refine error messages for proxyUrl validation and streamline httpAgent checks in Braintree plugin --- .../payment-braintree/src/core/braintree-base.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index e87756cb..c7a41bc8 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -276,8 +276,7 @@ class BraintreeBase extends AbstractPaymentProvider { return new HttpsProxyAgent(this.options_.proxyUrl); } catch (error) { const isRequireMissingProxyAgent = - error instanceof Error && - /Cannot find module ['"]https-proxy-agent['"]/.test(error.message); + error instanceof Error && /Cannot find module ['"]https-proxy-agent['"]/.test(error.message); const isModuleNotFoundProxyAgent = typeof error === 'object' && error !== null && @@ -400,18 +399,13 @@ class BraintreeBase extends AbstractPaymentProvider { } catch { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, - 'Option "proxyUrl" must be a valid URL in Braintree plugin', + `Option "proxyUrl" must be a valid URL in Braintree plugin: "${options.proxyUrl}"`, ); } - } } if (isDefined(options.httpAgent)) { - if ( - typeof options.httpAgent !== 'object' || - options.httpAgent === null || - Array.isArray(options.httpAgent) - ) { + if (typeof options.httpAgent !== 'object' || options.httpAgent === null || Array.isArray(options.httpAgent)) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, 'Option "httpAgent" must be an object in Braintree plugin',