zapier-platform-core 17.0.4 → 17.2.0
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/package.json +3 -2
- package/src/app-middlewares/before/fetch-stashed-bundle.js +43 -0
- package/src/create-app.js +2 -0
- package/src/errors.js +1 -0
- package/src/tools/bundle-encryption.js +57 -0
- package/src/tools/create-response-stasher.js +1 -13
- package/src/tools/retry-utils.js +32 -0
- package/types/schemas.generated.d.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zapier-platform-core",
|
|
3
|
-
"version": "17.0
|
|
3
|
+
"version": "17.2.0",
|
|
4
4
|
"description": "The core SDK for CLI apps in the Zapier Developer Platform.",
|
|
5
5
|
"repository": "zapier/zapier-platform",
|
|
6
6
|
"homepage": "https://platform.zapier.com/",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@zapier/secret-scrubber": "^1.1.2",
|
|
56
56
|
"content-disposition": "0.5.4",
|
|
57
57
|
"dotenv": "16.5.0",
|
|
58
|
+
"fernet": "^0.4.0",
|
|
58
59
|
"form-data": "4.0.1",
|
|
59
60
|
"lodash": "4.17.21",
|
|
60
61
|
"mime-types": "2.1.35",
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
"node-fetch": "2.7.0",
|
|
63
64
|
"oauth-sign": "0.9.0",
|
|
64
65
|
"semver": "7.7.1",
|
|
65
|
-
"zapier-platform-schema": "17.0
|
|
66
|
+
"zapier-platform-schema": "17.2.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@types/node-fetch": "^2.6.11",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const fetch = require('../../tools/fetch');
|
|
5
|
+
const { StashedBundleError } = require('../../errors');
|
|
6
|
+
const { withRetry } = require('../../tools/retry-utils');
|
|
7
|
+
const { decryptBundleWithSecret } = require('../../tools/bundle-encryption');
|
|
8
|
+
|
|
9
|
+
const fetchStashedBundle = async (input) => {
|
|
10
|
+
const stashedBundleKey = _.get(
|
|
11
|
+
input,
|
|
12
|
+
'_zapier.event.stashedBundleKey',
|
|
13
|
+
undefined,
|
|
14
|
+
);
|
|
15
|
+
const rpc = _.get(input, '_zapier.rpc');
|
|
16
|
+
const secret = process.env._ZAPIER_ONE_TIME_SECRET;
|
|
17
|
+
if (stashedBundleKey && secret) {
|
|
18
|
+
// Use the RPC to get a presigned URL for downloading the data
|
|
19
|
+
const rpcResponse = await rpc(
|
|
20
|
+
'get_presigned_download_url',
|
|
21
|
+
stashedBundleKey,
|
|
22
|
+
);
|
|
23
|
+
const response = await withRetry(() => fetch(rpcResponse.url));
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const errorMessage = `Failed to read stashed bundle. Status: ${response.status} ${response.statusText}`;
|
|
26
|
+
throw new StashedBundleError(errorMessage);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const responseText = await response.text();
|
|
31
|
+
// Decrypt the bundle
|
|
32
|
+
const stashedBundle = decryptBundleWithSecret(responseText, secret);
|
|
33
|
+
|
|
34
|
+
// Set the bundle to the stashedBundle value
|
|
35
|
+
_.set(input, '_zapier.event.bundle', stashedBundle);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new StashedBundleError(error.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return input;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
module.exports = fetchStashedBundle;
|
package/src/create-app.js
CHANGED
|
@@ -7,6 +7,7 @@ const schemaTools = require('./tools/schema');
|
|
|
7
7
|
// before middles
|
|
8
8
|
const injectZObject = require('./app-middlewares/before/z-object');
|
|
9
9
|
const addAppContext = require('./app-middlewares/before/add-app-context');
|
|
10
|
+
const fetchStashedBundle = require('./app-middlewares/before/fetch-stashed-bundle');
|
|
10
11
|
|
|
11
12
|
// after middles
|
|
12
13
|
const checkOutput = require('./app-middlewares/after/checks');
|
|
@@ -27,6 +28,7 @@ const createApp = (appRaw) => {
|
|
|
27
28
|
|
|
28
29
|
// standard before middlewares
|
|
29
30
|
const befores = [
|
|
31
|
+
fetchStashedBundle,
|
|
30
32
|
addAppContext,
|
|
31
33
|
injectZObject,
|
|
32
34
|
...ensureArray(frozenCompiledApp.beforeApp),
|
package/src/errors.js
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fernet = require('fernet');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Decrypt a bundle using secret key
|
|
8
|
+
*
|
|
9
|
+
* This matches the backend:
|
|
10
|
+
* 1. Hash the secret with SHA256 to get 32 bytes
|
|
11
|
+
* 2. Base64url encode those bytes to make Fernet-compatible key
|
|
12
|
+
* 3. Use Fernet library to decrypt (handles all token parsing internally)
|
|
13
|
+
*
|
|
14
|
+
* @param {string} bundle - The bundle represented as an encrypted token
|
|
15
|
+
* @param {string} secret - The secret key for decryption
|
|
16
|
+
* @returns {Object} The decrypted bundle object
|
|
17
|
+
*/
|
|
18
|
+
const decryptBundleWithSecret = (bundle, secret) => {
|
|
19
|
+
try {
|
|
20
|
+
// Validate input
|
|
21
|
+
if (!bundle || typeof bundle !== 'string') {
|
|
22
|
+
throw new Error('Invalid object from s3 - must be a non-empty string');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!secret || typeof secret !== 'string') {
|
|
26
|
+
throw new Error('Invalid secret - must be a non-empty string');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create the same key as backend
|
|
30
|
+
// Hash the secret and take first 32 bytes, then base64url encode for Fernet
|
|
31
|
+
const keyHash = crypto.createHash('sha256').update(secret).digest();
|
|
32
|
+
const keyBytes = keyHash.subarray(0, 32); // Take first 32 bytes
|
|
33
|
+
const fernetKey = keyBytes.toString('base64url'); // Use built-in base64url encoding
|
|
34
|
+
|
|
35
|
+
// Use Fernet library to decrypt (handles all the token parsing)
|
|
36
|
+
const token = new fernet.Token({
|
|
37
|
+
secret: new fernet.Secret(fernetKey),
|
|
38
|
+
token: bundle,
|
|
39
|
+
ttl: 0,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const decrypted = token.decode();
|
|
43
|
+
|
|
44
|
+
// Parse JSON
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(decrypted);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new Error('Invalid JSON in decrypted bundle');
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`Bundle decryption failed: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
decryptBundleWithSecret,
|
|
57
|
+
};
|
|
@@ -3,19 +3,7 @@
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
const uploader = require('./uploader');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
-
|
|
7
|
-
const withRetry = async (fn, retries = 3, delay = 100, attempt = 0) => {
|
|
8
|
-
try {
|
|
9
|
-
return await fn();
|
|
10
|
-
} catch (error) {
|
|
11
|
-
if (attempt >= retries) {
|
|
12
|
-
throw error;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
16
|
-
return withRetry(fn, retries, delay, attempt + 1);
|
|
17
|
-
}
|
|
18
|
-
};
|
|
6
|
+
const { withRetry } = require('./retry-utils');
|
|
19
7
|
|
|
20
8
|
// responseStasher uploads the data and returns the URL that points to that data.
|
|
21
9
|
const stashResponse = async (input, response) => {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Retry a function with exponential backoff
|
|
5
|
+
* @param {Function} fn - The function to retry
|
|
6
|
+
* @param {number} retries - Maximum number of retries (default: 3)
|
|
7
|
+
* @param {number} delay - Initial delay in milliseconds (default: 100)
|
|
8
|
+
* @param {number} attempt - Current attempt number (internal use)
|
|
9
|
+
* @returns {Promise} The result of the function call
|
|
10
|
+
*/
|
|
11
|
+
const withRetry = async (fn, retries = 3, delay = 100, attempt = 0) => {
|
|
12
|
+
try {
|
|
13
|
+
return await fn();
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (attempt >= retries) {
|
|
16
|
+
// Create an enhanced error with retry information
|
|
17
|
+
const retryError = new Error(
|
|
18
|
+
`Request failed after ${retries + 1} attempts. ` +
|
|
19
|
+
`Last error: ${error.message}.`,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
throw retryError;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
26
|
+
return withRetry(fn, retries, delay, attempt + 1);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
withRetry,
|
|
32
|
+
};
|