zapier-platform-core 17.1.0 → 17.3.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/fetch-logger.js +5 -0
- package/src/tools/http.js +6 -3
- package/src/tools/retry-utils.js +32 -0
- package/types/custom.d.ts +6 -1
- package/types/functions.d.ts +8 -1
- package/types/functions.test-d.ts +18 -0
- package/types/schemas.generated.d.ts +56 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zapier-platform-core",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.3.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.3.3",
|
|
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.
|
|
66
|
+
"zapier-platform-schema": "17.3.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) => {
|
|
@@ -56,11 +56,16 @@ const isZapierUserAgent = (headers) =>
|
|
|
56
56
|
_.get(headers, 'user-agent', []).indexOf('Zapier') !== -1;
|
|
57
57
|
|
|
58
58
|
const shouldIncludeResponseContent = (contentType) => {
|
|
59
|
+
if (!contentType) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
for (const ctype of ALLOWED_HTTP_DATA_CONTENT_TYPES) {
|
|
60
64
|
if (contentType.includes(ctype)) {
|
|
61
65
|
return true;
|
|
62
66
|
}
|
|
63
67
|
}
|
|
68
|
+
|
|
64
69
|
return false;
|
|
65
70
|
};
|
|
66
71
|
|
package/src/tools/http.js
CHANGED
|
@@ -9,7 +9,8 @@ const HTML_TYPE = 'text/html';
|
|
|
9
9
|
const TEXT_TYPE = 'text/plain';
|
|
10
10
|
const TEXT_TYPE_UTF8 = 'text/plain; charset=utf-8';
|
|
11
11
|
const YAML_TYPE = 'application/yaml';
|
|
12
|
-
const
|
|
12
|
+
const XML_TEXT_TYPE = 'text/xml';
|
|
13
|
+
const XML_APPLICATION_TYPE = 'application/xml';
|
|
13
14
|
const JSONAPI_TYPE = 'application/vnd.api+json';
|
|
14
15
|
|
|
15
16
|
const ALLOWED_HTTP_DATA_CONTENT_TYPES = new Set([
|
|
@@ -20,7 +21,8 @@ const ALLOWED_HTTP_DATA_CONTENT_TYPES = new Set([
|
|
|
20
21
|
TEXT_TYPE,
|
|
21
22
|
TEXT_TYPE_UTF8,
|
|
22
23
|
YAML_TYPE,
|
|
23
|
-
|
|
24
|
+
XML_TEXT_TYPE,
|
|
25
|
+
XML_APPLICATION_TYPE,
|
|
24
26
|
JSONAPI_TYPE,
|
|
25
27
|
]);
|
|
26
28
|
|
|
@@ -122,7 +124,8 @@ module.exports = {
|
|
|
122
124
|
HTML_TYPE,
|
|
123
125
|
TEXT_TYPE,
|
|
124
126
|
YAML_TYPE,
|
|
125
|
-
|
|
127
|
+
XML_TEXT_TYPE,
|
|
128
|
+
XML_APPLICATION_TYPE,
|
|
126
129
|
JSONAPI_TYPE,
|
|
127
130
|
ALLOWED_HTTP_DATA_CONTENT_TYPES,
|
|
128
131
|
getContentType,
|
|
@@ -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
|
+
};
|
package/types/custom.d.ts
CHANGED
|
@@ -18,10 +18,15 @@ export const createAppTester: (
|
|
|
18
18
|
options?: { customStoreKey?: string },
|
|
19
19
|
) => <T, B extends Bundle>(
|
|
20
20
|
func: (z: ZObject, bundle: B) => T | Promise<T>,
|
|
21
|
-
bundle?:
|
|
21
|
+
bundle?: DeepPartial<B>, // partial so we don't have to make a full bundle in tests
|
|
22
22
|
clearZcacheBeforeUse?: boolean,
|
|
23
23
|
) => Promise<T>; // appTester always returns a promise
|
|
24
24
|
|
|
25
|
+
/** Recursively make all properties of an object optional. */
|
|
26
|
+
type DeepPartial<T> = T extends object
|
|
27
|
+
? { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] }
|
|
28
|
+
: T;
|
|
29
|
+
|
|
25
30
|
type HttpMethod =
|
|
26
31
|
| 'GET'
|
|
27
32
|
| 'POST'
|
package/types/functions.d.ts
CHANGED
|
@@ -2,9 +2,16 @@ import type { Bundle, ZObject } from './custom';
|
|
|
2
2
|
|
|
3
3
|
type DefaultInputData = Record<string, unknown>;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a `perform` function that is used to poll for data from an API.
|
|
7
|
+
* By default must return an array of objects with an `id` field, but
|
|
8
|
+
* when one or more output fields have `primary:true` set on them, this
|
|
9
|
+
* can be overridden by setting the second type parameter to a type with
|
|
10
|
+
* those keys.
|
|
11
|
+
*/
|
|
5
12
|
export type PollingTriggerPerform<
|
|
6
13
|
$InputData extends DefaultInputData = DefaultInputData,
|
|
7
|
-
$Return extends {
|
|
14
|
+
$Return extends {} = { id: string },
|
|
8
15
|
> = (z: ZObject, bundle: Bundle<$InputData>) => $Return[] | Promise<$Return[]>;
|
|
9
16
|
|
|
10
17
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expectAssignable } from 'tsd';
|
|
2
|
+
import type { PollingTriggerPerform } from './functions';
|
|
3
|
+
|
|
4
|
+
const simplePerform = (async (z, bundle) => {
|
|
5
|
+
return [{ id: '1', name: 'test' }];
|
|
6
|
+
}) satisfies PollingTriggerPerform;
|
|
7
|
+
|
|
8
|
+
expectAssignable<PollingTriggerPerform<{}, { id: string; name: string }>>(
|
|
9
|
+
simplePerform,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const primaryKeyOverridePerform = (async (z, bundle) => {
|
|
13
|
+
return [{ itemId: 123, name: 'test' }];
|
|
14
|
+
}) satisfies PollingTriggerPerform<{}, { itemId: number; name: string }>;
|
|
15
|
+
|
|
16
|
+
expectAssignable<PollingTriggerPerform<{}, { itemId: number; name: string }>>(
|
|
17
|
+
primaryKeyOverridePerform,
|
|
18
|
+
);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* files, and/or the schema-to-ts tool and run its CLI to regenerate
|
|
5
5
|
* these typings.
|
|
6
6
|
*
|
|
7
|
-
* zapier-platform-schema version: 17.
|
|
7
|
+
* zapier-platform-schema version: 17.2.0
|
|
8
8
|
* schema-to-ts compiler version: 0.1.0
|
|
9
9
|
*/
|
|
10
10
|
import type {
|
|
@@ -906,6 +906,12 @@ export interface BasicPollingOperation<
|
|
|
906
906
|
/** What should the form a user sees and configures look like? */
|
|
907
907
|
inputFields?: $InputFields;
|
|
908
908
|
|
|
909
|
+
/**
|
|
910
|
+
* Defines groups for organizing input fields in the UI. Each group
|
|
911
|
+
* can have a key, label, and emphasis styling.
|
|
912
|
+
*/
|
|
913
|
+
inputFieldGroups?: InputFieldGroups;
|
|
914
|
+
|
|
909
915
|
/**
|
|
910
916
|
* What fields of data will this return? Will use resource
|
|
911
917
|
* outputFields if missing, will also use sample if available.
|
|
@@ -995,6 +1001,12 @@ export interface BasicHookOperation<
|
|
|
995
1001
|
/** What should the form a user sees and configures look like? */
|
|
996
1002
|
inputFields?: $InputFields;
|
|
997
1003
|
|
|
1004
|
+
/**
|
|
1005
|
+
* Defines groups for organizing input fields in the UI. Each group
|
|
1006
|
+
* can have a key, label, and emphasis styling.
|
|
1007
|
+
*/
|
|
1008
|
+
inputFieldGroups?: InputFieldGroups;
|
|
1009
|
+
|
|
998
1010
|
/**
|
|
999
1011
|
* What fields of data will this return? Will use resource
|
|
1000
1012
|
* outputFields if missing, will also use sample if available.
|
|
@@ -1049,6 +1061,12 @@ export interface BasicHookToPollOperation<
|
|
|
1049
1061
|
/** What should the form a user sees and configures look like? */
|
|
1050
1062
|
inputFields?: $InputFields;
|
|
1051
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Defines groups for organizing input fields in the UI. Each group
|
|
1066
|
+
* can have a key, label, and emphasis styling.
|
|
1067
|
+
*/
|
|
1068
|
+
inputFieldGroups?: InputFieldGroups;
|
|
1069
|
+
|
|
1052
1070
|
/**
|
|
1053
1071
|
* What fields of data will this return? Will use resource
|
|
1054
1072
|
* outputFields if missing, will also use sample if available.
|
|
@@ -1103,6 +1121,12 @@ export interface BasicActionOperation {
|
|
|
1103
1121
|
/** What should the form a user sees and configures look like? */
|
|
1104
1122
|
inputFields?: InputFields;
|
|
1105
1123
|
|
|
1124
|
+
/**
|
|
1125
|
+
* Defines groups for organizing input fields in the UI. Each group
|
|
1126
|
+
* can have a key, label, and emphasis styling.
|
|
1127
|
+
*/
|
|
1128
|
+
inputFieldGroups?: InputFieldGroups;
|
|
1129
|
+
|
|
1106
1130
|
/**
|
|
1107
1131
|
* What fields of data will this return? Will use resource
|
|
1108
1132
|
* outputFields if missing, will also use sample if available.
|
|
@@ -1164,6 +1188,12 @@ export interface BasicSearchOperation<
|
|
|
1164
1188
|
/** What should the form a user sees and configures look like? */
|
|
1165
1189
|
inputFields?: $InputFields;
|
|
1166
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* Defines groups for organizing input fields in the UI. Each group
|
|
1193
|
+
* can have a key, label, and emphasis styling.
|
|
1194
|
+
*/
|
|
1195
|
+
inputFieldGroups?: InputFieldGroups;
|
|
1196
|
+
|
|
1167
1197
|
/**
|
|
1168
1198
|
* What fields of data will this return? Will use resource
|
|
1169
1199
|
* outputFields if missing, will also use sample if available.
|
|
@@ -1227,6 +1257,12 @@ export interface BasicCreateOperation<
|
|
|
1227
1257
|
/** What should the form a user sees and configures look like? */
|
|
1228
1258
|
inputFields?: $InputFields;
|
|
1229
1259
|
|
|
1260
|
+
/**
|
|
1261
|
+
* Defines groups for organizing input fields in the UI. Each group
|
|
1262
|
+
* can have a key, label, and emphasis styling.
|
|
1263
|
+
*/
|
|
1264
|
+
inputFieldGroups?: InputFieldGroups;
|
|
1265
|
+
|
|
1230
1266
|
/**
|
|
1231
1267
|
* What fields of data will this return? Will use resource
|
|
1232
1268
|
* outputFields if missing, will also use sample if available.
|
|
@@ -1305,6 +1341,12 @@ export interface BasicOperation {
|
|
|
1305
1341
|
/** What should the form a user sees and configures look like? */
|
|
1306
1342
|
inputFields?: InputFields;
|
|
1307
1343
|
|
|
1344
|
+
/**
|
|
1345
|
+
* Defines groups for organizing input fields in the UI. Each group
|
|
1346
|
+
* can have a key, label, and emphasis styling.
|
|
1347
|
+
*/
|
|
1348
|
+
inputFieldGroups?: InputFieldGroups;
|
|
1349
|
+
|
|
1308
1350
|
/**
|
|
1309
1351
|
* What fields of data will this return? Will use resource
|
|
1310
1352
|
* outputFields if missing, will also use sample if available.
|
|
@@ -1412,6 +1454,13 @@ export interface PlainOutputField {
|
|
|
1412
1454
|
steadyState?: boolean;
|
|
1413
1455
|
}
|
|
1414
1456
|
|
|
1457
|
+
/** An array or collection of input field groups. */
|
|
1458
|
+
export type InputFieldGroups = {
|
|
1459
|
+
key: Key;
|
|
1460
|
+
label: string;
|
|
1461
|
+
emphasize: boolean;
|
|
1462
|
+
}[];
|
|
1463
|
+
|
|
1415
1464
|
/**
|
|
1416
1465
|
* Zapier uses this configuration to ensure this action is performed
|
|
1417
1466
|
* one at a time per scope (avoid concurrency).
|
|
@@ -1613,6 +1662,12 @@ export interface PlainInputField {
|
|
|
1613
1662
|
* Supports simple key-values only (no sub-objects or arrays).
|
|
1614
1663
|
*/
|
|
1615
1664
|
meta?: FieldMeta;
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* References a group key from the operation's inputFieldGroups to
|
|
1668
|
+
* organize this field with others.
|
|
1669
|
+
*/
|
|
1670
|
+
group?: Key;
|
|
1616
1671
|
}
|
|
1617
1672
|
|
|
1618
1673
|
/**
|