@foxtware/mineral 0.1.28 → 0.1.30

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 (95) hide show
  1. package/.creds.yml.sample +35 -2
  2. package/AGENTS.md +7 -0
  3. package/_build_scripts/createNewFunction.js +26 -3
  4. package/api/dpd/docs.md +6 -0
  5. package/api/dpd/dpd.constants.js +9 -0
  6. package/api/dpd/dpd.utils.js +145 -0
  7. package/api/dpd/dpdTrackingGet.js +64 -0
  8. package/api/dropbox/docs.md +7 -0
  9. package/api/dropbox/dropbox.constants.js +10 -0
  10. package/api/dropbox/dropbox.utils.js +67 -0
  11. package/api/dropbox/dropboxAccountGet.js +64 -0
  12. package/api/dropbox/dropboxFileCopy.js +67 -0
  13. package/api/dropbox/dropboxFileDelete.js +89 -0
  14. package/api/dropbox/dropboxFileDownload.js +139 -0
  15. package/api/dropbox/dropboxFileMetadataGet.js +100 -0
  16. package/api/dropbox/dropboxFileMove.js +67 -0
  17. package/api/dropbox/dropboxFileUpload.js +129 -0
  18. package/api/dropbox/dropboxFolderCreate.js +68 -0
  19. package/api/dropbox/dropboxFolderList.js +66 -0
  20. package/api/dropbox/dropboxGet.js +158 -0
  21. package/api/dropbox/dropboxSearch.js +161 -0
  22. package/api/dropbox/dropboxSharedLinkCreate.js +68 -0
  23. package/api/dropbox/dropboxSpaceUsageGet.js +55 -0
  24. package/api/dropbox/dropboxTemporaryLinkGet.js +75 -0
  25. package/api/fedex/docs.md +6 -0
  26. package/api/fedex/fedex.constants.js +11 -0
  27. package/api/fedex/fedex.utils.js +151 -0
  28. package/api/fedex/fedexTrackingGet.js +76 -0
  29. package/api/google/docs.md +1 -0
  30. package/api/google/google.constants.js +1 -0
  31. package/api/google/google.utils.js +21 -0
  32. package/api/google/googleanalyticsMetadataGet.js +62 -0
  33. package/api/google/googleanalyticsRealtimeReportRun.js +103 -0
  34. package/api/google/googleanalyticsReportRun.js +114 -0
  35. package/api/paypal/docs.md +14 -0
  36. package/api/paypal/paypal.constants.js +13 -0
  37. package/api/paypal/paypal.utils.js +166 -0
  38. package/api/paypal/paypalAccessTokenGet.js +81 -0
  39. package/api/paypal/paypalAuthorizationCapture.js +75 -0
  40. package/api/paypal/paypalAuthorizationGet.js +57 -0
  41. package/api/paypal/paypalBalancesGet.js +65 -0
  42. package/api/paypal/paypalCaptureGet.js +57 -0
  43. package/api/paypal/paypalCaptureRefund.js +78 -0
  44. package/api/paypal/paypalOrderAuthorize.js +69 -0
  45. package/api/paypal/paypalOrderCapture.js +69 -0
  46. package/api/paypal/paypalOrderCreate.js +81 -0
  47. package/api/paypal/paypalOrderGet.js +57 -0
  48. package/api/paypal/paypalRefundGet.js +57 -0
  49. package/api/paypal/paypalTransactionsGet.js +101 -0
  50. package/api/paypal/paypalUserInfoGet.js +62 -0
  51. package/api/royalmail/docs.md +6 -0
  52. package/api/royalmail/royalmail.constants.js +7 -0
  53. package/api/royalmail/royalmail.utils.js +55 -0
  54. package/api/royalmail/royalmailTrackingGet.js +70 -0
  55. package/api/shopify/shopify.utils.js +65 -4
  56. package/api/shopify/shopifyMetaobjectCreate.js +13 -6
  57. package/api/shopify/shopifyMetaobjectDelete.js +90 -0
  58. package/api/shopify/shopifyMetaobjectGet.js +80 -0
  59. package/api/shopify/shopifyMetaobjectUpdate.js +108 -0
  60. package/api/shopify/shopifyStorefrontSearch.js +158 -0
  61. package/api/spotify/docs.md +8 -0
  62. package/api/spotify/spotify.constants.js +12 -0
  63. package/api/spotify/spotify.utils.js +106 -0
  64. package/api/spotify/spotifyAlbumGet.js +76 -0
  65. package/api/spotify/spotifyAlbumTracksGet.js +49 -0
  66. package/api/spotify/spotifyArtistAlbumsGet.js +55 -0
  67. package/api/spotify/spotifyArtistGet.js +71 -0
  68. package/api/spotify/spotifyArtistTopTracksGet.js +56 -0
  69. package/api/spotify/spotifyGet.js +171 -0
  70. package/api/spotify/spotifyMeGet.js +48 -0
  71. package/api/spotify/spotifyPlayerCurrentlyPlayingGet.js +62 -0
  72. package/api/spotify/spotifyPlayerDevicesGet.js +51 -0
  73. package/api/spotify/spotifyPlayerGet.js +63 -0
  74. package/api/spotify/spotifyPlayerNext.js +55 -0
  75. package/api/spotify/spotifyPlayerPause.js +55 -0
  76. package/api/spotify/spotifyPlayerPlay.js +70 -0
  77. package/api/spotify/spotifyPlayerPrevious.js +55 -0
  78. package/api/spotify/spotifyPlaylistCreate.js +63 -0
  79. package/api/spotify/spotifyPlaylistGet.js +91 -0
  80. package/api/spotify/spotifyPlaylistItemsAdd.js +61 -0
  81. package/api/spotify/spotifyPlaylistItemsGet.js +58 -0
  82. package/api/spotify/spotifyPlaylistItemsRemove.js +67 -0
  83. package/api/spotify/spotifyPlaylistUpdate.js +64 -0
  84. package/api/spotify/spotifyPlaylistsGet.js +47 -0
  85. package/api/spotify/spotifySavedTracksGet.js +46 -0
  86. package/api/spotify/spotifySavedTracksRemove.js +59 -0
  87. package/api/spotify/spotifySavedTracksSave.js +59 -0
  88. package/api/spotify/spotifySearch.js +174 -0
  89. package/api/spotify/spotifyTrackGet.js +76 -0
  90. package/api/starshipit/starshipitProductUpdate.js +102 -0
  91. package/api/starshipit/starshipitProductsGet.js +2 -2
  92. package/api/stylearcade/stylearcadeGet.js +5 -2
  93. package/api/utils.js +39 -0
  94. package/package.json +1 -1
  95. package/server.js +3 -1
@@ -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
+ */
@@ -5,3 +5,4 @@ Service account auth via `SERVICE_ACCOUNT_JSON` on the `google` creds path. Uses
5
5
  - [Sheets API](https://developers.google.com/workspace/sheets/api/reference/rest)
6
6
  - [Drive API](https://developers.google.com/workspace/drive/api/reference/rest/v3)
7
7
  - [Calendar API](https://developers.google.com/workspace/calendar/api/v3/reference)
8
+ - [Analytics Data API](https://developers.google.com/analytics/devguides/reporting/data/v1/rest)
@@ -2,6 +2,7 @@ const GOOGLE_SCOPES = {
2
2
  sheets: ['https://www.googleapis.com/auth/spreadsheets'],
3
3
  drive: ['https://www.googleapis.com/auth/drive'],
4
4
  calendar: ['https://www.googleapis.com/auth/calendar'],
5
+ analytics: ['https://www.googleapis.com/auth/analytics'],
5
6
  };
6
7
 
7
8
  module.exports = {
@@ -108,6 +108,26 @@ const getGoogleCalendar = async (
108
108
  };
109
109
  };
110
110
 
111
+ const getGoogleAnalyticsData = async (
112
+ credsPayload,
113
+ {
114
+ subject,
115
+ } = {},
116
+ ) => {
117
+ const { auth, error } = await getGoogleAuth(credsPayload, {
118
+ scopes: GOOGLE_SCOPES.analytics,
119
+ subject,
120
+ });
121
+
122
+ if (error) {
123
+ return { error };
124
+ }
125
+
126
+ return {
127
+ client: google.analyticsdata({ version: 'v1beta', auth }),
128
+ };
129
+ };
130
+
111
131
  const googleApiCall = async (promiseFn) => {
112
132
  try {
113
133
  const response = await promiseFn();
@@ -132,6 +152,7 @@ module.exports = {
132
152
  getGoogleSheets,
133
153
  getGoogleDrive,
134
154
  getGoogleCalendar,
155
+ getGoogleAnalyticsData,
135
156
  googleApiCall,
136
157
  invalidCredsResponse,
137
158
  };
@@ -0,0 +1,62 @@
1
+ // https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/getMetadata
2
+
3
+ const { ArgsWarden, objHasAny } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const { getGoogleAnalyticsData, googleApiCall } = require('../google/google.utils');
6
+
7
+ const propertyIdentifierValidator = (propertyIdentifier) => {
8
+ return objHasAny(propertyIdentifier, ['propertyId', 'property']);
9
+ };
10
+
11
+ const argsWarden = new ArgsWarden([
12
+ ['credsPayload', credsValidator],
13
+ ['propertyIdentifier', propertyIdentifierValidator],
14
+ ]);
15
+
16
+ const googleanalyticsMetadataGet = async (
17
+ credsPayload,
18
+ propertyIdentifier,
19
+ {
20
+ subject,
21
+ } = {},
22
+ ) => {
23
+
24
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
25
+ credsPayload,
26
+ propertyIdentifier,
27
+ });
28
+ if (rejectResponse) {
29
+ return rejectResponse;
30
+ }
31
+
32
+ const { propertyId, property } = propertyIdentifier;
33
+ const propertyName = property ?? `properties/${ propertyId }`;
34
+
35
+ const { client, error } = await getGoogleAnalyticsData(credsPayload, { subject });
36
+
37
+ if (error) {
38
+ return error;
39
+ }
40
+
41
+ return googleApiCall(() => client.properties.getMetadata({
42
+ name: `${ propertyName }/metadata`,
43
+ }));
44
+ };
45
+
46
+ const funcApiConfig = {
47
+ argsWarden,
48
+ };
49
+
50
+ module.exports = {
51
+ googleanalyticsMetadataGet,
52
+ funcApiConfig,
53
+ };
54
+
55
+ /*
56
+ curl -X POST "http://localhost:8000/googleanalyticsMetadataGet" \
57
+ -H "Content-Type: application/json" \
58
+ -d '{
59
+ "credsPayload": { "credsPath": "google" },
60
+ "propertyIdentifier": { "propertyId": "402247571" }
61
+ }'
62
+ */
@@ -0,0 +1,103 @@
1
+ // https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runRealtimeReport
2
+
3
+ const { ArgsWarden, objHasAny } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const { getGoogleAnalyticsData, googleApiCall } = require('../google/google.utils');
6
+
7
+ const propertyIdentifierValidator = (propertyIdentifier) => {
8
+ return objHasAny(propertyIdentifier, ['propertyId', 'property']);
9
+ };
10
+
11
+ const argsWarden = new ArgsWarden([
12
+ ['credsPayload', credsValidator],
13
+ ['propertyIdentifier', propertyIdentifierValidator],
14
+ ]);
15
+
16
+ const formatDimensions = (dimensions) => {
17
+ if (!dimensions) {
18
+ return undefined;
19
+ }
20
+ return dimensions.map((dim) => (typeof dim === 'string' ? { name: dim } : dim));
21
+ };
22
+
23
+ const formatMetrics = (metrics) => {
24
+ if (!metrics) {
25
+ return undefined;
26
+ }
27
+ return metrics.map((metric) => (typeof metric === 'string' ? { name: metric } : metric));
28
+ };
29
+
30
+ const googleanalyticsRealtimeReportRun = async (
31
+ credsPayload,
32
+ propertyIdentifier,
33
+ {
34
+ subject,
35
+ dimensions,
36
+ metrics,
37
+ dimensionFilter,
38
+ metricFilter,
39
+ limit,
40
+ metricAggregations,
41
+ orderBys,
42
+ returnPropertyQuota,
43
+ minuteRanges,
44
+ } = {},
45
+ ) => {
46
+
47
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
48
+ credsPayload,
49
+ propertyIdentifier,
50
+ });
51
+ if (rejectResponse) {
52
+ return rejectResponse;
53
+ }
54
+
55
+ const { propertyId, property } = propertyIdentifier;
56
+ const propertyName = property ?? `properties/${ propertyId }`;
57
+
58
+ const { client, error } = await getGoogleAnalyticsData(credsPayload, { subject });
59
+
60
+ if (error) {
61
+ return error;
62
+ }
63
+
64
+ const formattedDimensions = formatDimensions(dimensions);
65
+ const formattedMetrics = formatMetrics(metrics);
66
+
67
+ return googleApiCall(() => client.properties.runRealtimeReport({
68
+ property: propertyName,
69
+ requestBody: {
70
+ ...formattedDimensions && { dimensions: formattedDimensions },
71
+ ...formattedMetrics && { metrics: formattedMetrics },
72
+ ...dimensionFilter && { dimensionFilter },
73
+ ...metricFilter && { metricFilter },
74
+ ...limit !== undefined && { limit },
75
+ ...metricAggregations && { metricAggregations },
76
+ ...orderBys && { orderBys },
77
+ ...returnPropertyQuota !== undefined && { returnPropertyQuota },
78
+ ...minuteRanges && { minuteRanges },
79
+ },
80
+ }));
81
+ };
82
+
83
+ const funcApiConfig = {
84
+ argsWarden,
85
+ };
86
+
87
+ module.exports = {
88
+ googleanalyticsRealtimeReportRun,
89
+ funcApiConfig,
90
+ };
91
+
92
+ /*
93
+ curl -X POST "http://localhost:8000/googleanalyticsRealtimeReportRun" \
94
+ -H "Content-Type: application/json" \
95
+ -d '{
96
+ "credsPayload": { "credsPath": "google" },
97
+ "propertyIdentifier": { "propertyId": "402247571" },
98
+ "options": {
99
+ "dimensions": ["country"],
100
+ "metrics": ["activeUsers"]
101
+ }
102
+ }'
103
+ */
@@ -0,0 +1,114 @@
1
+ // https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runReport
2
+
3
+ const { ArgsWarden, objHasAny } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const { getGoogleAnalyticsData, googleApiCall } = require('../google/google.utils');
6
+
7
+ const propertyIdentifierValidator = (propertyIdentifier) => {
8
+ return objHasAny(propertyIdentifier, ['propertyId', 'property']);
9
+ };
10
+
11
+ const argsWarden = new ArgsWarden([
12
+ ['credsPayload', credsValidator],
13
+ ['propertyIdentifier', propertyIdentifierValidator],
14
+ ]);
15
+
16
+ const formatDimensions = (dimensions) => {
17
+ if (!dimensions) {
18
+ return undefined;
19
+ }
20
+ return dimensions.map((dim) => (typeof dim === 'string' ? { name: dim } : dim));
21
+ };
22
+
23
+ const formatMetrics = (metrics) => {
24
+ if (!metrics) {
25
+ return undefined;
26
+ }
27
+ return metrics.map((metric) => (typeof metric === 'string' ? { name: metric } : metric));
28
+ };
29
+
30
+ const googleanalyticsReportRun = async (
31
+ credsPayload,
32
+ propertyIdentifier,
33
+ {
34
+ subject,
35
+ dateRanges,
36
+ dimensions,
37
+ metrics,
38
+ dimensionFilter,
39
+ metricFilter,
40
+ offset,
41
+ limit,
42
+ metricAggregations,
43
+ orderBys,
44
+ currencyCode,
45
+ cohortSpec,
46
+ keepEmptyRows,
47
+ returnPropertyQuota,
48
+ comparisons,
49
+ } = {},
50
+ ) => {
51
+
52
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
53
+ credsPayload,
54
+ propertyIdentifier,
55
+ });
56
+ if (rejectResponse) {
57
+ return rejectResponse;
58
+ }
59
+
60
+ const { propertyId, property } = propertyIdentifier;
61
+ const propertyName = property ?? `properties/${ propertyId }`;
62
+
63
+ const { client, error } = await getGoogleAnalyticsData(credsPayload, { subject });
64
+
65
+ if (error) {
66
+ return error;
67
+ }
68
+
69
+ const formattedDimensions = formatDimensions(dimensions);
70
+ const formattedMetrics = formatMetrics(metrics);
71
+
72
+ return googleApiCall(() => client.properties.runReport({
73
+ property: propertyName,
74
+ requestBody: {
75
+ ...dateRanges && { dateRanges },
76
+ ...formattedDimensions && { dimensions: formattedDimensions },
77
+ ...formattedMetrics && { metrics: formattedMetrics },
78
+ ...dimensionFilter && { dimensionFilter },
79
+ ...metricFilter && { metricFilter },
80
+ ...offset !== undefined && { offset },
81
+ ...limit !== undefined && { limit },
82
+ ...metricAggregations && { metricAggregations },
83
+ ...orderBys && { orderBys },
84
+ ...currencyCode && { currencyCode },
85
+ ...cohortSpec && { cohortSpec },
86
+ ...keepEmptyRows !== undefined && { keepEmptyRows },
87
+ ...returnPropertyQuota !== undefined && { returnPropertyQuota },
88
+ ...comparisons && { comparisons },
89
+ },
90
+ }));
91
+ };
92
+
93
+ const funcApiConfig = {
94
+ argsWarden,
95
+ };
96
+
97
+ module.exports = {
98
+ googleanalyticsReportRun,
99
+ funcApiConfig,
100
+ };
101
+
102
+ /*
103
+ curl -X POST "http://localhost:8000/googleanalyticsReportRun" \
104
+ -H "Content-Type: application/json" \
105
+ -d '{
106
+ "credsPayload": { "credsPath": "google" },
107
+ "propertyIdentifier": { "propertyId": "402247571" },
108
+ "options": {
109
+ "dateRanges": [{ "startDate": "30daysAgo", "endDate": "today" }],
110
+ "dimensions": ["date"],
111
+ "metrics": ["activeUsers", "sessions"]
112
+ }
113
+ }'
114
+ */
@@ -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
+ };
@@ -0,0 +1,81 @@
1
+ // https://developer.paypal.com/api/rest/authentication/
2
+
3
+ const { credsValidator } = require('../validators');
4
+ const { ArgsWarden } = require('../utils');
5
+ const {
6
+ resolveAccessToken,
7
+ baseUrlForCreds,
8
+ getClientCredentialsToken,
9
+ } = require('../paypal/paypal.utils');
10
+ const { resolveCreds } = require('../pipelineSteps');
11
+ const { credsFromPayload } = require('../utils');
12
+
13
+ const argsWarden = new ArgsWarden([
14
+ ['credsPayload', credsValidator],
15
+ ]);
16
+
17
+ /**
18
+ * Resolve (and cache) a PayPal access token for the given creds.
19
+ * Useful for debugging auth or for callers that need the raw token.
20
+ */
21
+ const paypalAccessTokenGet = async (
22
+ credsPayload,
23
+ {
24
+ forceRefresh = false,
25
+ } = {},
26
+ ) => {
27
+
28
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
29
+ credsPayload,
30
+ });
31
+ if (rejectResponse) {
32
+ return rejectResponse;
33
+ }
34
+
35
+ try {
36
+ const creds = await credsFromPayload(credsPayload);
37
+
38
+ if (forceRefresh && creds?.CLIENT_ID) {
39
+ const { clientCredentialsTokenCache } = require('../paypal/paypal.utils');
40
+ const env = (creds.SANDBOX === true || creds.SANDBOX === 'true'
41
+ || creds.ENVIRONMENT === 'sandbox' || creds.MODE === 'sandbox')
42
+ ? 'sandbox'
43
+ : 'live';
44
+ clientCredentialsTokenCache.delete(`${ env }:${ creds.CLIENT_ID }`);
45
+ }
46
+
47
+ const accessToken = await resolveAccessToken(creds);
48
+ return {
49
+ ok: true,
50
+ data: {
51
+ access_token: accessToken,
52
+ base_url: baseUrlForCreds(creds),
53
+ },
54
+ };
55
+ } catch (err) {
56
+ return {
57
+ ok: false,
58
+ error: {
59
+ code: 'PAYPAL_AUTH_FAILED',
60
+ message: err.message || String(err),
61
+ },
62
+ };
63
+ }
64
+ };
65
+
66
+ const funcApiConfig = {
67
+ argsWarden,
68
+ };
69
+
70
+ module.exports = {
71
+ paypalAccessTokenGet,
72
+ funcApiConfig,
73
+ };
74
+
75
+ /*
76
+ curl -X POST "http://localhost:8000/paypalAccessTokenGet" \
77
+ -H "Content-Type: application/json" \
78
+ -d '{
79
+ "credsPayload": { "credsPath": "paypal" }
80
+ }'
81
+ */