@foxtware/mineral 0.1.29 → 0.1.31

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.
Files changed (35) hide show
  1. package/.creds.yml.sample +21 -0
  2. package/api/dpd/docs.md +6 -0
  3. package/api/dpd/dpd.constants.js +9 -0
  4. package/api/dpd/dpd.utils.js +145 -0
  5. package/api/dpd/dpdTrackingGet.js +64 -0
  6. package/api/fedex/docs.md +6 -0
  7. package/api/fedex/fedex.constants.js +11 -0
  8. package/api/fedex/fedex.utils.js +151 -0
  9. package/api/fedex/fedexTrackingGet.js +76 -0
  10. package/api/paypal/docs.md +14 -0
  11. package/api/paypal/paypal.constants.js +13 -0
  12. package/api/paypal/paypal.utils.js +166 -0
  13. package/api/paypal/paypalAccessTokenGet.js +81 -0
  14. package/api/paypal/paypalAuthorizationCapture.js +75 -0
  15. package/api/paypal/paypalAuthorizationGet.js +57 -0
  16. package/api/paypal/paypalBalancesGet.js +65 -0
  17. package/api/paypal/paypalCaptureGet.js +57 -0
  18. package/api/paypal/paypalCaptureRefund.js +78 -0
  19. package/api/paypal/paypalOrderAuthorize.js +69 -0
  20. package/api/paypal/paypalOrderCapture.js +69 -0
  21. package/api/paypal/paypalOrderCreate.js +81 -0
  22. package/api/paypal/paypalOrderGet.js +57 -0
  23. package/api/paypal/paypalRefundGet.js +57 -0
  24. package/api/paypal/paypalTransactionsGet.js +101 -0
  25. package/api/paypal/paypalUserInfoGet.js +62 -0
  26. package/api/royalmail/docs.md +6 -0
  27. package/api/royalmail/royalmail.constants.js +7 -0
  28. package/api/royalmail/royalmail.utils.js +55 -0
  29. package/api/royalmail/royalmailTrackingGet.js +70 -0
  30. package/api/shopify/shopify.utils.js +12 -0
  31. package/api/starshipit/starshipitProductUpdate.js +102 -0
  32. package/api/starshipit/starshipitProductsGet.js +2 -2
  33. package/api/stylearcade/stylearcadeGet.js +5 -2
  34. package/api/utils.js +44 -0
  35. package/package.json +1 -1
package/.creds.yml.sample CHANGED
@@ -155,3 +155,24 @@ spotify:
155
155
  # Option B — app client credentials (catalog-only; auto-minted token)
156
156
  CLIENT_ID: _____________________________
157
157
  CLIENT_SECRET: _________________________
158
+
159
+ paypal:
160
+ CLIENT_ID: _____________________________
161
+ CLIENT_SECRET: _________________________
162
+ # SANDBOX: true # only if using sandbox credentials
163
+
164
+ royalmail:
165
+ CLIENT_ID: _____________________________
166
+ CLIENT_SECRET: _________________________
167
+
168
+ dpd:
169
+ ACCOUNT_NUMBER: _________________________
170
+ USERNAME: ______________________________
171
+ PASSWORD: ______________________________
172
+ # BASE_URL: https://api.dpdlocal.co.uk # only for DPD Local accounts
173
+
174
+ fedex:
175
+ CLIENT_ID: _____________________________
176
+ CLIENT_SECRET: _________________________
177
+ # SANDBOX: true # only if using sandbox credentials
178
+
@@ -0,0 +1,6 @@
1
+ # DPD
2
+
3
+ - [DPD UK technology and API information](https://dpd.co.uk/content/about_dpd/technology.jsp)
4
+ - [DPD API documentation](https://www.dpd.com/wp-content/uploads/sites/235/2023/04/DPD-API-documentation-v1-2-1.pdf)
5
+
6
+ DPD API access is account-managed. DPD UK credentials authenticate against `https://api.dpd.co.uk/user/?action=login`, returning a `geoSession` token. The tracking client uses that session with the DPD network tracking endpoint. DPD Local accounts can use the same client with their account-specific `BASE_URL`.
@@ -0,0 +1,9 @@
1
+ const DPD_API_BASE_URL = 'https://api.dpd.co.uk';
2
+ const DPD_AUTH_PATH = '/user/?action=login';
3
+ const DPD_TRACKING_PATH = '/shipping/network';
4
+
5
+ module.exports = {
6
+ DPD_API_BASE_URL,
7
+ DPD_AUTH_PATH,
8
+ DPD_TRACKING_PATH,
9
+ };
@@ -0,0 +1,145 @@
1
+ const { resolveCreds } = require('../pipelineSteps');
2
+ const {
3
+ FetchClient,
4
+ appendUrlToBase,
5
+ customFetch,
6
+ fetchClientCommonSteps,
7
+ } = require('../utils');
8
+ const {
9
+ DPD_API_BASE_URL,
10
+ DPD_AUTH_PATH,
11
+ } = require('./dpd.constants');
12
+
13
+ const geoSessionCache = new Map();
14
+
15
+ const baseUrlForCreds = (creds) => {
16
+ return (creds?.BASE_URL || DPD_API_BASE_URL).replace(/\/$/, '');
17
+ };
18
+
19
+ const geoClientForCreds = (creds) => {
20
+ return creds?.GEO_CLIENT || `account/${ creds?.ACCOUNT_NUMBER || '' }`;
21
+ };
22
+
23
+ const cacheKeyForCreds = (creds) => {
24
+ return [
25
+ baseUrlForCreds(creds),
26
+ creds?.USERNAME || '',
27
+ creds?.ACCOUNT_NUMBER || '',
28
+ ].join(':');
29
+ };
30
+
31
+ const getGeoSession = async (creds) => {
32
+ const {
33
+ USERNAME,
34
+ PASSWORD,
35
+ } = creds || {};
36
+
37
+ if (!USERNAME || !PASSWORD || !geoClientForCreds(creds).split('/').pop()) {
38
+ throw new Error(
39
+ 'DPD creds require USERNAME, PASSWORD, and ACCOUNT_NUMBER or GEO_CLIENT',
40
+ );
41
+ }
42
+
43
+ const cacheKey = cacheKeyForCreds(creds);
44
+ const cachedSession = geoSessionCache.get(cacheKey);
45
+ if (cachedSession && cachedSession.expiresAt > Date.now()) {
46
+ return cachedSession.geoSession;
47
+ }
48
+
49
+ const basicAuth = Buffer
50
+ .from(`${ USERNAME }:${ PASSWORD }`)
51
+ .toString('base64');
52
+ const response = await customFetch(
53
+ appendUrlToBase(baseUrlForCreds(creds), DPD_AUTH_PATH),
54
+ {
55
+ method: 'post',
56
+ headers: {
57
+ Accept: 'application/json',
58
+ Authorization: `Basic ${ basicAuth }`,
59
+ GEOClient: geoClientForCreds(creds),
60
+ },
61
+ },
62
+ );
63
+
64
+ if (!response.ok) {
65
+ throw new Error(
66
+ `DPD authentication failed: ${ JSON.stringify(response.error || response.data) }`,
67
+ );
68
+ }
69
+
70
+ const geoSession = response.data?.geoSession || response.data?.data?.geoSession;
71
+ if (!geoSession) {
72
+ throw new Error('DPD authentication response missing geoSession');
73
+ }
74
+
75
+ geoSessionCache.set(cacheKey, {
76
+ geoSession,
77
+ expiresAt: Date.now() + (60 * 60 * 1000),
78
+ });
79
+
80
+ return geoSession;
81
+ };
82
+
83
+ const useBaseUrlFromCreds = async (state) => {
84
+ const { requestPayload, context } = state;
85
+ const { creds } = context;
86
+
87
+ return {
88
+ requestPayload: {
89
+ ...requestPayload,
90
+ url: appendUrlToBase(
91
+ baseUrlForCreds(creds),
92
+ requestPayload.url || '',
93
+ ),
94
+ },
95
+ };
96
+ };
97
+
98
+ const useAuthHeaders = async (state) => {
99
+ const { requestPayload, context } = state;
100
+ const { creds } = context;
101
+
102
+ let geoSession;
103
+ try {
104
+ geoSession = await getGeoSession(creds);
105
+ } catch (error) {
106
+ return {
107
+ breakChain: true,
108
+ response: {
109
+ ok: false,
110
+ error: {
111
+ code: 'DPD_AUTH_ERROR',
112
+ message: error.message,
113
+ },
114
+ },
115
+ };
116
+ }
117
+
118
+ return {
119
+ requestPayload: {
120
+ ...requestPayload,
121
+ headers: {
122
+ Accept: 'application/json',
123
+ GEOClient: geoClientForCreds(creds),
124
+ GEOSession: geoSession,
125
+ ...requestPayload.headers,
126
+ },
127
+ },
128
+ };
129
+ };
130
+
131
+ const dpdClient = new FetchClient({
132
+ pipeline: [
133
+ resolveCreds,
134
+ useBaseUrlFromCreds,
135
+ useAuthHeaders,
136
+ 'fetch',
137
+ fetchClientCommonSteps.exitEarlyOnNotOk,
138
+ ],
139
+ });
140
+
141
+ module.exports = {
142
+ dpdClient,
143
+ geoSessionCache,
144
+ getGeoSession,
145
+ };
@@ -0,0 +1,64 @@
1
+ // https://www.dpd.com/wp-content/uploads/sites/235/2023/04/DPD-API-documentation-v1-2-1.pdf
2
+
3
+ const { ArgsWarden } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const { DPD_TRACKING_PATH } = require('./dpd.constants');
6
+ const { dpdClient } = require('./dpd.utils');
7
+
8
+ const trackingIdentifierValidator = (trackingIdentifier) => {
9
+ return Boolean(trackingIdentifier?.trackingNumber);
10
+ };
11
+
12
+ const argsWarden = new ArgsWarden([
13
+ ['credsPayload', credsValidator],
14
+ ['trackingIdentifier', trackingIdentifierValidator],
15
+ ]);
16
+
17
+ const dpdTrackingGet = async (
18
+ credsPayload,
19
+ trackingIdentifier,
20
+ {
21
+ fetchClient = dpdClient,
22
+ } = {},
23
+ ) => {
24
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
25
+ credsPayload,
26
+ trackingIdentifier,
27
+ });
28
+ if (rejectResponse) {
29
+ return rejectResponse;
30
+ }
31
+
32
+ const {
33
+ trackingNumber,
34
+ } = trackingIdentifier;
35
+
36
+ return fetchClient.fetch({
37
+ requestPayload: {
38
+ url: `${ DPD_TRACKING_PATH }/${ encodeURIComponent(trackingNumber) }`,
39
+ },
40
+ context: {
41
+ credsPayload,
42
+ },
43
+ });
44
+ };
45
+
46
+ const funcApiConfig = {
47
+ argsWarden,
48
+ };
49
+
50
+ module.exports = {
51
+ dpdTrackingGet,
52
+ funcApiConfig,
53
+ };
54
+
55
+ /*
56
+ curl -X POST "http://localhost:8000/dpdTrackingGet" \
57
+ -H "Content-Type: application/json" \
58
+ -d '{
59
+ "credsPayload": { "credsPath": "dpd" },
60
+ "trackingIdentifier": {
61
+ "trackingNumber": "15509742315259"
62
+ }
63
+ }'
64
+ */
@@ -0,0 +1,6 @@
1
+ # FedEx
2
+
3
+ - [FedEx Developer Portal](https://developer.fedex.com/)
4
+ - [FedEx Track API](https://developer.fedex.com/api/en-us/catalog/track.html)
5
+
6
+ FedEx tracking uses OAuth 2.0 client credentials. Create a project with the Track API enabled, obtain a bearer token from `https://apis.fedex.com/oauth/token`, and submit tracking requests to `https://apis.fedex.com/track/v1/trackingnumbers`. Sandbox requests use `https://apis-sandbox.fedex.com`.
@@ -0,0 +1,11 @@
1
+ const FEDEX_LIVE_BASE_URL = 'https://apis.fedex.com';
2
+ const FEDEX_SANDBOX_BASE_URL = 'https://apis-sandbox.fedex.com';
3
+ const FEDEX_OAUTH_PATH = '/oauth/token';
4
+ const FEDEX_TRACKING_PATH = '/track/v1/trackingnumbers';
5
+
6
+ module.exports = {
7
+ FEDEX_LIVE_BASE_URL,
8
+ FEDEX_SANDBOX_BASE_URL,
9
+ FEDEX_OAUTH_PATH,
10
+ FEDEX_TRACKING_PATH,
11
+ };
@@ -0,0 +1,151 @@
1
+ const { resolveCreds } = require('../pipelineSteps');
2
+ const {
3
+ FetchClient,
4
+ appendUrlToBase,
5
+ customFetch,
6
+ fetchClientCommonSteps,
7
+ } = require('../utils');
8
+ const {
9
+ FEDEX_LIVE_BASE_URL,
10
+ FEDEX_OAUTH_PATH,
11
+ FEDEX_SANDBOX_BASE_URL,
12
+ } = require('./fedex.constants');
13
+
14
+ const accessTokenCache = new Map();
15
+
16
+ const isSandboxCreds = (creds) => {
17
+ return creds?.SANDBOX === true
18
+ || creds?.SANDBOX === 'true'
19
+ || creds?.ENVIRONMENT === 'sandbox';
20
+ };
21
+
22
+ const baseUrlForCreds = (creds) => {
23
+ return (creds?.BASE_URL || (
24
+ isSandboxCreds(creds)
25
+ ? FEDEX_SANDBOX_BASE_URL
26
+ : FEDEX_LIVE_BASE_URL
27
+ )).replace(/\/$/, '');
28
+ };
29
+
30
+ const getAccessToken = async (creds) => {
31
+ if (creds?.ACCESS_TOKEN) {
32
+ return creds.ACCESS_TOKEN;
33
+ }
34
+
35
+ const {
36
+ CLIENT_ID,
37
+ CLIENT_SECRET,
38
+ } = creds || {};
39
+ if (!CLIENT_ID || !CLIENT_SECRET) {
40
+ throw new Error(
41
+ 'FedEx creds require ACCESS_TOKEN or CLIENT_ID and CLIENT_SECRET',
42
+ );
43
+ }
44
+
45
+ const cacheKey = `${ baseUrlForCreds(creds) }:${ CLIENT_ID }`;
46
+ const cachedToken = accessTokenCache.get(cacheKey);
47
+ if (cachedToken && cachedToken.expiresAt > Date.now()) {
48
+ return cachedToken.accessToken;
49
+ }
50
+
51
+ const basicAuth = Buffer
52
+ .from(`${ CLIENT_ID }:${ CLIENT_SECRET }`)
53
+ .toString('base64');
54
+ const response = await customFetch(
55
+ appendUrlToBase(baseUrlForCreds(creds), FEDEX_OAUTH_PATH),
56
+ {
57
+ method: 'post',
58
+ headers: {
59
+ Accept: 'application/json',
60
+ Authorization: `Basic ${ basicAuth }`,
61
+ 'Content-Type': 'application/x-www-form-urlencoded',
62
+ },
63
+ body: 'grant_type=client_credentials',
64
+ },
65
+ );
66
+
67
+ if (!response.ok) {
68
+ throw new Error(
69
+ `FedEx authentication failed: ${ JSON.stringify(response.error || response.data) }`,
70
+ );
71
+ }
72
+
73
+ const {
74
+ access_token: accessToken,
75
+ expires_in: expiresIn = 3600,
76
+ } = response.data || {};
77
+ if (!accessToken) {
78
+ throw new Error('FedEx authentication response missing access_token');
79
+ }
80
+
81
+ accessTokenCache.set(cacheKey, {
82
+ accessToken,
83
+ expiresAt: Date.now() + (Math.max(expiresIn - 60, 60) * 1000),
84
+ });
85
+
86
+ return accessToken;
87
+ };
88
+
89
+ const useBaseUrlFromCreds = async (state) => {
90
+ const { requestPayload, context } = state;
91
+ const { creds } = context;
92
+
93
+ return {
94
+ requestPayload: {
95
+ ...requestPayload,
96
+ url: appendUrlToBase(
97
+ baseUrlForCreds(creds),
98
+ requestPayload.url || '',
99
+ ),
100
+ },
101
+ };
102
+ };
103
+
104
+ const useAuthHeaders = async (state) => {
105
+ const { requestPayload, context } = state;
106
+ const { creds } = context;
107
+
108
+ let accessToken;
109
+ try {
110
+ accessToken = await getAccessToken(creds);
111
+ } catch (error) {
112
+ return {
113
+ breakChain: true,
114
+ response: {
115
+ ok: false,
116
+ error: {
117
+ code: 'FEDEX_AUTH_ERROR',
118
+ message: error.message,
119
+ },
120
+ },
121
+ };
122
+ }
123
+
124
+ return {
125
+ requestPayload: {
126
+ ...requestPayload,
127
+ headers: {
128
+ Accept: 'application/json',
129
+ Authorization: `Bearer ${ accessToken }`,
130
+ ...requestPayload.headers,
131
+ },
132
+ },
133
+ };
134
+ };
135
+
136
+ const fedexClient = new FetchClient({
137
+ pipeline: [
138
+ resolveCreds,
139
+ useBaseUrlFromCreds,
140
+ useAuthHeaders,
141
+ 'fetch',
142
+ fetchClientCommonSteps.exitEarlyOnNotOk,
143
+ ],
144
+ });
145
+
146
+ module.exports = {
147
+ accessTokenCache,
148
+ baseUrlForCreds,
149
+ fedexClient,
150
+ getAccessToken,
151
+ };
@@ -0,0 +1,76 @@
1
+ // https://developer.fedex.com/api/en-us/catalog/track.html
2
+
3
+ const { ArgsWarden } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const { FEDEX_TRACKING_PATH } = require('./fedex.constants');
6
+ const { fedexClient } = require('./fedex.utils');
7
+
8
+ const trackingIdentifierValidator = (trackingIdentifier) => {
9
+ return Boolean(trackingIdentifier?.trackingNumber);
10
+ };
11
+
12
+ const argsWarden = new ArgsWarden([
13
+ ['credsPayload', credsValidator],
14
+ ['trackingIdentifier', trackingIdentifierValidator],
15
+ ]);
16
+
17
+ const fedexTrackingGet = async (
18
+ credsPayload,
19
+ trackingIdentifier,
20
+ {
21
+ includeDetailedScans = true,
22
+ fetchClient = fedexClient,
23
+ } = {},
24
+ ) => {
25
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
26
+ credsPayload,
27
+ trackingIdentifier,
28
+ });
29
+ if (rejectResponse) {
30
+ return rejectResponse;
31
+ }
32
+
33
+ const {
34
+ trackingNumber,
35
+ } = trackingIdentifier;
36
+
37
+ return fetchClient.fetch({
38
+ requestPayload: {
39
+ method: 'post',
40
+ url: FEDEX_TRACKING_PATH,
41
+ body: {
42
+ includeDetailedScans,
43
+ trackingInfo: [
44
+ {
45
+ trackingNumberInfo: {
46
+ trackingNumber,
47
+ },
48
+ },
49
+ ],
50
+ },
51
+ },
52
+ context: {
53
+ credsPayload,
54
+ },
55
+ });
56
+ };
57
+
58
+ const funcApiConfig = {
59
+ argsWarden,
60
+ };
61
+
62
+ module.exports = {
63
+ fedexTrackingGet,
64
+ funcApiConfig,
65
+ };
66
+
67
+ /*
68
+ curl -X POST "http://localhost:8000/fedexTrackingGet" \
69
+ -H "Content-Type: application/json" \
70
+ -d '{
71
+ "credsPayload": { "credsPath": "fedex" },
72
+ "trackingIdentifier": {
73
+ "trackingNumber": "872442694112"
74
+ }
75
+ }'
76
+ */
@@ -0,0 +1,14 @@
1
+ # PayPal
2
+
3
+ - [REST API overview](https://developer.paypal.com/api/rest/)
4
+ - [Authentication](https://developer.paypal.com/api/rest/authentication/)
5
+ - [Orders v2](https://developer.paypal.com/docs/api/orders/v2/)
6
+ - [Payments v2](https://developer.paypal.com/docs/api/payments/v2/)
7
+ - [Transaction Search](https://developer.paypal.com/docs/api/transaction-search/v1/)
8
+ - [Identity / userinfo](https://developer.paypal.com/docs/api/identity/v1/)
9
+
10
+ REST JSON under `https://api-m.paypal.com` (live) or `https://api-m.sandbox.paypal.com` (sandbox). OAuth 2.0 client-credentials: exchange `CLIENT_ID` + `CLIENT_SECRET` for a short-lived bearer token via `POST /v1/oauth2/token`, then send `Authorization: Bearer <token>` on every call.
11
+
12
+ Create a REST app under [Developer Dashboard → Apps & Credentials](https://developer.paypal.com/dashboard/applications). Toggle Sandbox vs Live for the matching client id/secret. Personal (first-party) apps can call Orders, Payments, Transaction Search, and Balances for the account that owns the app; some identity scopes need additional app configuration.
13
+
14
+ Transaction Search and Balances typically need the reporting scopes enabled on the app. Tokens expire (~hours); this client caches and refreshes them automatically.
@@ -0,0 +1,13 @@
1
+ const LIVE_BASE_URL = 'https://api-m.paypal.com';
2
+ const SANDBOX_BASE_URL = 'https://api-m.sandbox.paypal.com';
3
+
4
+ // Default page size for list-style reporting endpoints
5
+ const DEFAULT_PAGE_SIZE = 100;
6
+ const MAX_PAGE_SIZE = 500;
7
+
8
+ module.exports = {
9
+ LIVE_BASE_URL,
10
+ SANDBOX_BASE_URL,
11
+ DEFAULT_PAGE_SIZE,
12
+ MAX_PAGE_SIZE,
13
+ };
@@ -0,0 +1,166 @@
1
+ // https://developer.paypal.com/api/rest/authentication/
2
+
3
+ const {
4
+ LIVE_BASE_URL,
5
+ SANDBOX_BASE_URL,
6
+ } = require('../paypal/paypal.constants');
7
+ const { resolveCreds } = require('../pipelineSteps');
8
+ const {
9
+ FetchClient,
10
+ fetchClientCommonSteps,
11
+ customFetch,
12
+ appendUrlToBase,
13
+ } = require('../utils');
14
+
15
+ // Cache client-credentials tokens per CLIENT_ID + environment.
16
+ const clientCredentialsTokenCache = new Map();
17
+
18
+ const isSandboxCreds = (creds) => {
19
+ if (creds?.SANDBOX === true || creds?.SANDBOX === 'true') {
20
+ return true;
21
+ }
22
+ if (creds?.ENVIRONMENT === 'sandbox' || creds?.MODE === 'sandbox') {
23
+ return true;
24
+ }
25
+ return false;
26
+ };
27
+
28
+ const baseUrlForCreds = (creds) => {
29
+ if (creds?.BASE_URL) {
30
+ return creds.BASE_URL.replace(/\/$/, '');
31
+ }
32
+ return isSandboxCreds(creds) ? SANDBOX_BASE_URL : LIVE_BASE_URL;
33
+ };
34
+
35
+ const tokenCacheKey = (creds) => {
36
+ const env = isSandboxCreds(creds) ? 'sandbox' : 'live';
37
+ return `${ env }:${ creds?.CLIENT_ID || '' }`;
38
+ };
39
+
40
+ const getClientCredentialsToken = async (creds) => {
41
+ const { CLIENT_ID, CLIENT_SECRET } = creds ?? {};
42
+ if (!CLIENT_ID || !CLIENT_SECRET) {
43
+ throw new Error('paypal client_credentials require CLIENT_ID and CLIENT_SECRET');
44
+ }
45
+
46
+ const cacheKey = tokenCacheKey(creds);
47
+ const cached = clientCredentialsTokenCache.get(cacheKey);
48
+ if (cached && cached.expiresAt > Date.now()) {
49
+ return cached.accessToken;
50
+ }
51
+
52
+ const baseUrl = baseUrlForCreds(creds);
53
+ const basic = Buffer
54
+ .from(`${ CLIENT_ID }:${ CLIENT_SECRET }`)
55
+ .toString('base64');
56
+
57
+ const response = await customFetch(
58
+ `${ baseUrl }/v1/oauth2/token`,
59
+ {
60
+ method: 'post',
61
+ headers: {
62
+ Authorization: `Basic ${ basic }`,
63
+ 'Content-Type': 'application/x-www-form-urlencoded',
64
+ Accept: 'application/json',
65
+ },
66
+ body: 'grant_type=client_credentials',
67
+ },
68
+ );
69
+
70
+ if (!response.ok) {
71
+ throw new Error(
72
+ `PayPal client_credentials failed: ${ JSON.stringify(response.error ?? response.data) }`,
73
+ );
74
+ }
75
+
76
+ const { access_token: accessToken, expires_in: expiresIn = 32400 } = response.data ?? {};
77
+ if (!accessToken) {
78
+ throw new Error('PayPal client_credentials response missing access_token');
79
+ }
80
+
81
+ clientCredentialsTokenCache.set(cacheKey, {
82
+ accessToken,
83
+ // Refresh a minute early.
84
+ expiresAt: Date.now() + (Math.max(expiresIn - 60, 60) * 1000),
85
+ });
86
+
87
+ return accessToken;
88
+ };
89
+
90
+ const resolveAccessToken = async (creds) => {
91
+ if (creds?.ACCESS_TOKEN) {
92
+ return creds.ACCESS_TOKEN;
93
+ }
94
+
95
+ const { CLIENT_ID, CLIENT_SECRET } = creds ?? {};
96
+ if (CLIENT_ID && CLIENT_SECRET) {
97
+ return getClientCredentialsToken(creds);
98
+ }
99
+
100
+ throw new Error(
101
+ 'paypal creds require ACCESS_TOKEN or CLIENT_ID + CLIENT_SECRET',
102
+ );
103
+ };
104
+
105
+ const useBaseUrlFromCreds = async (state) => {
106
+ const { requestPayload, context } = state;
107
+ const { creds } = context;
108
+ const baseUrl = baseUrlForCreds(creds);
109
+
110
+ return {
111
+ requestPayload: {
112
+ ...requestPayload,
113
+ url: appendUrlToBase(baseUrl, requestPayload.url || ''),
114
+ },
115
+ };
116
+ };
117
+
118
+ const useAuthHeaders = async (state) => {
119
+ const { requestPayload, context } = state;
120
+ const { creds } = context;
121
+
122
+ let accessToken;
123
+ try {
124
+ accessToken = await resolveAccessToken(creds);
125
+ } catch (err) {
126
+ return {
127
+ response: {
128
+ ok: false,
129
+ error: {
130
+ code: 'PAYPAL_AUTH_FAILED',
131
+ message: err.message || String(err),
132
+ },
133
+ },
134
+ breakChain: true,
135
+ };
136
+ }
137
+
138
+ return {
139
+ requestPayload: {
140
+ ...requestPayload,
141
+ headers: {
142
+ Accept: 'application/json',
143
+ Authorization: `Bearer ${ accessToken }`,
144
+ ...requestPayload.headers,
145
+ },
146
+ },
147
+ };
148
+ };
149
+
150
+ const paypalClient = new FetchClient({
151
+ pipeline: [
152
+ resolveCreds,
153
+ useBaseUrlFromCreds,
154
+ useAuthHeaders,
155
+ 'fetch',
156
+ fetchClientCommonSteps.exitEarlyOnNotOk,
157
+ ],
158
+ });
159
+
160
+ module.exports = {
161
+ paypalClient,
162
+ resolveAccessToken,
163
+ getClientCredentialsToken,
164
+ baseUrlForCreds,
165
+ clientCredentialsTokenCache,
166
+ };