backend-manager 5.0.110 → 5.0.112
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
CHANGED
|
@@ -14,6 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
# [5.0.111] - 2026-03-05
|
|
18
|
+
### Changed
|
|
19
|
+
- PayPal client ID is now read from `backend-manager-config.json` (`payment.processors.paypal.clientId`) instead of requiring a `PAYPAL_CLIENT_ID` environment variable.
|
|
20
|
+
- PayPal auth now auto-detects sandbox vs live environment by trying both endpoints in parallel on first auth, with live taking priority.
|
|
21
|
+
|
|
17
22
|
# [5.0.109] - 2026-03-04
|
|
18
23
|
### Added
|
|
19
24
|
- Immediate trial cancellation: cancelling during a free trial now terminates the subscription instantly instead of scheduling cancel at period end, preventing free premium access for the remainder of the trial.
|
package/package.json
CHANGED
package/src/manager/index.js
CHANGED
|
@@ -138,6 +138,9 @@ Manager.prototype.init = function (exporter, options) {
|
|
|
138
138
|
(_objValue, srcValue) => isArray(srcValue) ? srcValue : undefined,
|
|
139
139
|
);
|
|
140
140
|
|
|
141
|
+
// Set PAYPAL_CLIENT_ID from config (clientId is public, not a secret — lives in config, not .env)
|
|
142
|
+
process.env.PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || self.config?.payment?.processors?.paypal?.clientId || '';
|
|
143
|
+
|
|
141
144
|
// Resolve legacy paths
|
|
142
145
|
// TODO: Remove this in future versions (after we migrate to removing app.id from config)
|
|
143
146
|
self.config.app = self.config.app || {};
|
|
@@ -8,12 +8,41 @@ const EPOCH_ZERO_UNIX = powertools.timestamp(EPOCH_ZERO, { output: 'unix' });
|
|
|
8
8
|
const INTERVAL_TO_FREQUENCY = { YEAR: 'annually', MONTH: 'monthly', WEEK: 'weekly', DAY: 'daily' };
|
|
9
9
|
const FREQUENCY_TO_INTERVAL = { annually: 'YEAR', monthly: 'MONTH', weekly: 'WEEK', daily: 'DAY' };
|
|
10
10
|
|
|
11
|
-
// PayPal API base
|
|
12
|
-
const
|
|
11
|
+
// PayPal API base URLs
|
|
12
|
+
const LIVE_URL = 'https://api-m.paypal.com';
|
|
13
|
+
const SANDBOX_URL = 'https://api-m.sandbox.paypal.com';
|
|
13
14
|
|
|
14
|
-
// Cached access token
|
|
15
|
+
// Cached access token, expiry, and resolved base URL
|
|
15
16
|
let cachedToken = null;
|
|
16
17
|
let tokenExpiresAt = 0;
|
|
18
|
+
let resolvedBaseUrl = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Try to authenticate against a specific PayPal endpoint
|
|
22
|
+
* @param {string} auth - Base64-encoded client_id:secret
|
|
23
|
+
* @param {string} baseUrl - PayPal API base URL
|
|
24
|
+
* @returns {Promise<object|null>} Token data or null if auth failed
|
|
25
|
+
*/
|
|
26
|
+
async function tryAuth(auth, baseUrl) {
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(`${baseUrl}/v1/oauth2/token`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Authorization': `Basic ${auth}`,
|
|
32
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
33
|
+
},
|
|
34
|
+
body: 'grant_type=client_credentials',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await response.json();
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
17
46
|
|
|
18
47
|
/**
|
|
19
48
|
* PayPal shared library
|
|
@@ -22,7 +51,7 @@ let tokenExpiresAt = 0;
|
|
|
22
51
|
const PayPal = {
|
|
23
52
|
/**
|
|
24
53
|
* Initialize or return a PayPal access token
|
|
25
|
-
*
|
|
54
|
+
* Tries both live and sandbox endpoints in parallel on first auth
|
|
26
55
|
* @returns {Promise<string>} Access token
|
|
27
56
|
*/
|
|
28
57
|
async init() {
|
|
@@ -40,22 +69,39 @@ const PayPal = {
|
|
|
40
69
|
|
|
41
70
|
const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
42
71
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
// First auth — try both endpoints in parallel to detect environment
|
|
73
|
+
if (!resolvedBaseUrl) {
|
|
74
|
+
const [liveResult, sandboxResult] = await Promise.all([
|
|
75
|
+
tryAuth(auth, LIVE_URL),
|
|
76
|
+
tryAuth(auth, SANDBOX_URL),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
if (liveResult) {
|
|
80
|
+
resolvedBaseUrl = LIVE_URL;
|
|
81
|
+
cachedToken = liveResult.access_token;
|
|
82
|
+
tokenExpiresAt = Date.now() + (liveResult.expires_in * 1000);
|
|
83
|
+
return cachedToken;
|
|
84
|
+
}
|
|
51
85
|
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
if (sandboxResult) {
|
|
87
|
+
resolvedBaseUrl = SANDBOX_URL;
|
|
88
|
+
cachedToken = sandboxResult.access_token;
|
|
89
|
+
tokenExpiresAt = Date.now() + (sandboxResult.expires_in * 1000);
|
|
90
|
+
return cachedToken;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error('PayPal auth failed on both live and sandbox — check your client ID and secret');
|
|
54
94
|
}
|
|
55
95
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
96
|
+
// Subsequent auths — use the resolved endpoint
|
|
97
|
+
const result = await tryAuth(auth, resolvedBaseUrl);
|
|
98
|
+
|
|
99
|
+
if (!result) {
|
|
100
|
+
throw new Error(`PayPal auth failed (${resolvedBaseUrl})`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
cachedToken = result.access_token;
|
|
104
|
+
tokenExpiresAt = Date.now() + (result.expires_in * 1000);
|
|
59
105
|
|
|
60
106
|
return cachedToken;
|
|
61
107
|
},
|
|
@@ -69,7 +115,7 @@ const PayPal = {
|
|
|
69
115
|
async request(endpoint, options = {}) {
|
|
70
116
|
const token = await this.init();
|
|
71
117
|
|
|
72
|
-
const response = await fetch(`${
|
|
118
|
+
const response = await fetch(`${resolvedBaseUrl}${endpoint}`, {
|
|
73
119
|
...options,
|
|
74
120
|
headers: {
|
|
75
121
|
'Authorization': `Bearer ${token}`,
|
|
@@ -267,8 +313,10 @@ const PayPal = {
|
|
|
267
313
|
throw new Error(`No price configured for ${product.id}/${frequency}`);
|
|
268
314
|
}
|
|
269
315
|
|
|
270
|
-
// Fetch plans
|
|
271
|
-
const response = await this.request(`/v1/billing/plans?product_id=${paypalProductId}&page_size=20&total_required=true
|
|
316
|
+
// Fetch plans with full details (Prefer header includes billing_cycles in list response)
|
|
317
|
+
const response = await this.request(`/v1/billing/plans?product_id=${paypalProductId}&page_size=20&total_required=true`, {
|
|
318
|
+
headers: { 'Prefer': 'return=representation' },
|
|
319
|
+
});
|
|
272
320
|
const plans = response.plans || [];
|
|
273
321
|
|
|
274
322
|
// Map frequency to PayPal interval unit
|