zapier-platform-core 12.0.0 → 12.0.3
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zapier-platform-core",
|
|
3
|
-
"version": "12.0.
|
|
3
|
+
"version": "12.0.3",
|
|
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/",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"node-fetch": "2.6.7",
|
|
51
51
|
"oauth-sign": "0.9.0",
|
|
52
52
|
"semver": "7.3.5",
|
|
53
|
-
"zapier-platform-schema": "12.0.
|
|
53
|
+
"zapier-platform-schema": "12.0.3"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"adm-zip": "0.5.5",
|
package/src/execute.js
CHANGED
|
@@ -11,7 +11,18 @@ const ZapierPromise = require('./tools/promise');
|
|
|
11
11
|
const constants = require('./constants');
|
|
12
12
|
|
|
13
13
|
const executeHttpRequest = (input, options) => {
|
|
14
|
-
options = _.extend(
|
|
14
|
+
options = _.extend(
|
|
15
|
+
{},
|
|
16
|
+
// shorthand requests should always throw _unless_ the object specifically opts out
|
|
17
|
+
// this covers godzilla devs who use shorthand requests (most of them) that rely on the throwing behavior
|
|
18
|
+
// when we set the app-wide skip for everyone, we don't want their behavior to change
|
|
19
|
+
// so, this line takes precedence over the global setting, but not the local one (`options`)
|
|
20
|
+
{
|
|
21
|
+
skipThrowForStatus: false,
|
|
22
|
+
},
|
|
23
|
+
options,
|
|
24
|
+
constants.REQUEST_OBJECT_SHORTHAND_OPTIONS
|
|
25
|
+
);
|
|
15
26
|
return input.z.request(options).then((response) => {
|
|
16
27
|
if (response.data === undefined) {
|
|
17
28
|
throw new Error(
|
package/src/tools/cleaner.js
CHANGED
|
@@ -64,6 +64,11 @@ const recurseReplaceBank = (obj, bank = {}) => {
|
|
|
64
64
|
|
|
65
65
|
Object.keys(bank).forEach((key) => {
|
|
66
66
|
const matchesKey = matchesKeyRegexMap[key];
|
|
67
|
+
// RegExp.test modifies internal state of the regex object
|
|
68
|
+
// since we're re-using regexes, we have to reset that state between calls
|
|
69
|
+
// or the second time in a row that the key should match, it misses instead
|
|
70
|
+
// see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex
|
|
71
|
+
matchesKey.lastIndex = 0;
|
|
67
72
|
if (!matchesKey.test(maybeChangedString)) {
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
@@ -16,7 +16,7 @@ const oauth1SignRequest = require('../http-middlewares/before/oauth1-sign-reques
|
|
|
16
16
|
const prepareRequest = require('../http-middlewares/before/prepare-request');
|
|
17
17
|
|
|
18
18
|
// after middles
|
|
19
|
-
const logResponse = require('../http-middlewares/after/log-response');
|
|
19
|
+
const { logResponse } = require('../http-middlewares/after/log-response');
|
|
20
20
|
const prepareResponse = require('../http-middlewares/after/prepare-response');
|
|
21
21
|
const throwForStaleAuth = require('../http-middlewares/after/throw-for-stale-auth');
|
|
22
22
|
const throwForStatusMiddleware = require('../http-middlewares/after/throw-for-status');
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { Transform } = require('stream');
|
|
4
|
+
const { parse: querystringParse } = require('querystring');
|
|
4
5
|
|
|
5
6
|
const _ = require('lodash');
|
|
6
7
|
|
|
7
8
|
const request = require('./request-client-internal');
|
|
8
|
-
const { simpleTruncate, recurseReplace } = require('./data');
|
|
9
|
+
const { simpleTruncate, recurseReplace, truncateData } = require('./data');
|
|
9
10
|
const {
|
|
10
11
|
DEFAULT_LOGGING_HTTP_API_KEY,
|
|
11
12
|
DEFAULT_LOGGING_HTTP_ENDPOINT,
|
|
@@ -18,7 +19,10 @@ const {
|
|
|
18
19
|
recurseExtract,
|
|
19
20
|
} = require('@zapier/secret-scrubber');
|
|
20
21
|
// not really a public function, but it came from here originally
|
|
21
|
-
const {
|
|
22
|
+
const {
|
|
23
|
+
isUrlWithSecrets,
|
|
24
|
+
isSensitiveKey,
|
|
25
|
+
} = require('@zapier/secret-scrubber/lib/convenience');
|
|
22
26
|
|
|
23
27
|
// The payload size per request to stream logs. This should be slighly lower
|
|
24
28
|
// than the limit (16 MB) on the server side.
|
|
@@ -34,7 +38,8 @@ const isUrl = (url) => {
|
|
|
34
38
|
}
|
|
35
39
|
};
|
|
36
40
|
|
|
37
|
-
const
|
|
41
|
+
const MAX_LENGTH = 3500;
|
|
42
|
+
const truncateString = (str) => simpleTruncate(str, MAX_LENGTH, ' [...]');
|
|
38
43
|
|
|
39
44
|
const formatHeaders = (headers = {}) => {
|
|
40
45
|
if (_.isEmpty(headers)) {
|
|
@@ -70,7 +75,7 @@ const httpDetailsLogMessage = (data) => {
|
|
|
70
75
|
(result, value, key) => {
|
|
71
76
|
result[key] = value;
|
|
72
77
|
if (typeof value === 'string') {
|
|
73
|
-
result[key] =
|
|
78
|
+
result[key] = truncateString(value);
|
|
74
79
|
}
|
|
75
80
|
return result;
|
|
76
81
|
},
|
|
@@ -108,22 +113,54 @@ const toStdout = (event, msg, data) => {
|
|
|
108
113
|
}
|
|
109
114
|
};
|
|
110
115
|
|
|
116
|
+
// try to parse json; if successful, find secrets in it
|
|
117
|
+
const attemptFindSecretsInStr = (s) => {
|
|
118
|
+
let parsedRespContent;
|
|
119
|
+
try {
|
|
120
|
+
parsedRespContent = JSON.parse(s);
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
return findSensitiveValues(parsedRespContent);
|
|
125
|
+
};
|
|
126
|
+
|
|
111
127
|
const buildSensitiveValues = (event, data) => {
|
|
112
128
|
const bundle = event.bundle || {};
|
|
113
129
|
const authData = bundle.authData || {};
|
|
114
130
|
// for the most part, we should censor all the values from authData
|
|
115
131
|
// the exception is safe urls, which should be filtered out - we want those to be logged
|
|
132
|
+
// but, we _should_ censor-no-matter-what sensitive keys, even if their value is a safe url
|
|
133
|
+
// this covers the case where someone's password is a valid url ¯\_(ツ)_/¯
|
|
116
134
|
const sensitiveAuthData = recurseExtract(authData, (key, value) => {
|
|
135
|
+
if (isSensitiveKey(key)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
117
139
|
if (isUrl(value) && !isUrlWithSecrets(value)) {
|
|
118
140
|
return false;
|
|
119
141
|
}
|
|
120
142
|
return true;
|
|
121
143
|
});
|
|
122
|
-
|
|
144
|
+
|
|
145
|
+
const result = [
|
|
123
146
|
...sensitiveAuthData,
|
|
124
147
|
...findSensitiveValues(process.env),
|
|
125
148
|
...findSensitiveValues(data),
|
|
126
149
|
];
|
|
150
|
+
|
|
151
|
+
// for our http logs (genrated by prepareRequestLog), make sure that we try to parse the content to find any new strings
|
|
152
|
+
// (such as what comes back in the response during an auth refresh)
|
|
153
|
+
|
|
154
|
+
for (const prop of ['response_content', 'request_data']) {
|
|
155
|
+
if (data[prop]) {
|
|
156
|
+
result.push(...attemptFindSecretsInStr(data[prop]));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (data.request_params) {
|
|
160
|
+
result.push(...findSensitiveValues(querystringParse(data.request_params)));
|
|
161
|
+
}
|
|
162
|
+
// unique- no point in duplicates
|
|
163
|
+
return [...new Set(result)];
|
|
127
164
|
};
|
|
128
165
|
|
|
129
166
|
class LogStream extends Transform {
|
|
@@ -207,15 +244,21 @@ const sendLog = async (logStreamFactory, options, event, message, data) => {
|
|
|
207
244
|
data.response_headers = unheader(data.response_headers);
|
|
208
245
|
|
|
209
246
|
const sensitiveValues = buildSensitiveValues(event, data);
|
|
247
|
+
|
|
248
|
+
// data.input and data.output have the ability to grow unbounded; the following caps the size to a reasonable amount
|
|
249
|
+
if (data.log_type === 'bundle') {
|
|
250
|
+
data.input = truncateData(data.input, MAX_LENGTH);
|
|
251
|
+
data.output = truncateData(data.output, MAX_LENGTH);
|
|
252
|
+
}
|
|
210
253
|
// scrub throws an error if there are no secrets
|
|
211
|
-
const safeMessage =
|
|
254
|
+
const safeMessage = truncateString(
|
|
212
255
|
sensitiveValues.length ? scrub(message, sensitiveValues) : message
|
|
213
256
|
);
|
|
214
257
|
const safeData = recurseReplace(
|
|
215
258
|
sensitiveValues.length ? scrub(data, sensitiveValues) : data,
|
|
216
|
-
|
|
259
|
+
truncateString
|
|
217
260
|
);
|
|
218
|
-
const unsafeData = recurseReplace(data,
|
|
261
|
+
const unsafeData = recurseReplace(data, truncateString);
|
|
219
262
|
// Keep safe log keys uncensored
|
|
220
263
|
Object.keys(safeData).forEach((key) => {
|
|
221
264
|
if (SAFE_LOG_KEYS.includes(key)) {
|
package/src/tools/data.js
CHANGED
|
@@ -172,6 +172,162 @@ const simpleTruncate = (string, length, suffix) => {
|
|
|
172
172
|
return string;
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Adds an item to an object or array.
|
|
177
|
+
* If the parent is an object, the value will be set at the specified key.
|
|
178
|
+
* If the parent is an array, the value will be added to the end of the array (and the key will be ignored).
|
|
179
|
+
* Used by truncateData.
|
|
180
|
+
*
|
|
181
|
+
* @param {object | any[]} parent An object or array.
|
|
182
|
+
* @param {string} key The key to set the value at (objects only; ignored for arrays).
|
|
183
|
+
* @param {any} value The value to add.
|
|
184
|
+
*/
|
|
185
|
+
const _addItem = (parent, key, value) => {
|
|
186
|
+
if (Array.isArray(parent)) {
|
|
187
|
+
parent.push(value);
|
|
188
|
+
} else {
|
|
189
|
+
parent[key] = value;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Examines an object entry or array entry (`item`) and determines its "cost" (how many characters will it take to add it to `parent`).
|
|
195
|
+
* If the item is a string, `availableSpace` is used to determine if the string should be truncated.
|
|
196
|
+
* If the item is an object or array, its entries / values are added to `queue`.
|
|
197
|
+
* Used by truncateData.
|
|
198
|
+
*
|
|
199
|
+
* @param {any[]} queue
|
|
200
|
+
* @param {object | any[]} parent
|
|
201
|
+
* @param {string} key
|
|
202
|
+
* @param {any} item
|
|
203
|
+
* @param {number} availableSpace
|
|
204
|
+
* @returns
|
|
205
|
+
*/
|
|
206
|
+
const _processItem = (queue, parent, key, item, availableSpace) => {
|
|
207
|
+
let itemLength = 0;
|
|
208
|
+
let itemToAdd = item;
|
|
209
|
+
let wasTruncated = false;
|
|
210
|
+
|
|
211
|
+
itemLength += key.length; // array keys are empty strings, so this is a noop
|
|
212
|
+
itemLength += key.length ? 3 : 0; // objects get +2 for "" around the key and +1 for the :
|
|
213
|
+
itemLength += 1; // arrays and objects both have +1 for commas between entries
|
|
214
|
+
if (parent && typeof parent === 'object' && _.isEmpty(parent)) {
|
|
215
|
+
itemLength -= 1; // this is the first entry for an object or array; remove the count for a comma
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (typeof item === 'number' || typeof item === 'boolean' || item == null) {
|
|
219
|
+
itemLength += String(item).length;
|
|
220
|
+
} else if (typeof item === 'string') {
|
|
221
|
+
const overhead = itemLength + 2; // the minimum amount of space needed after truncation
|
|
222
|
+
if (item.length + overhead > availableSpace) {
|
|
223
|
+
// this string is going to push us over the edge; truncate it
|
|
224
|
+
itemToAdd = simpleTruncate(item, availableSpace - overhead, ' [...]');
|
|
225
|
+
wasTruncated = true;
|
|
226
|
+
}
|
|
227
|
+
itemLength += itemToAdd.length + 2; // 2 for quotes around the string value
|
|
228
|
+
} else if (typeof item === 'object') {
|
|
229
|
+
itemLength += 2; // '{}' or '[]'
|
|
230
|
+
|
|
231
|
+
let entries;
|
|
232
|
+
if (Array.isArray(item)) {
|
|
233
|
+
const newArr = [];
|
|
234
|
+
itemToAdd = newArr;
|
|
235
|
+
entries = item.map((subValue) => [newArr, '', subValue]);
|
|
236
|
+
} else {
|
|
237
|
+
const newObj = {};
|
|
238
|
+
itemToAdd = newObj;
|
|
239
|
+
entries = Object.entries(item).map(([subKey, subValue]) => [
|
|
240
|
+
newObj,
|
|
241
|
+
subKey,
|
|
242
|
+
subValue,
|
|
243
|
+
]);
|
|
244
|
+
}
|
|
245
|
+
queue.unshift(...entries);
|
|
246
|
+
} else {
|
|
247
|
+
// JSON.stringify doesn't usually really do anything for any other typeofs
|
|
248
|
+
// we're just going to use `undefined` and hope for the best
|
|
249
|
+
itemLength += 'undefined'.length;
|
|
250
|
+
itemToAdd = undefined;
|
|
251
|
+
}
|
|
252
|
+
return [itemLength, itemToAdd, wasTruncated];
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Takes a given `data` object or array and copies pieces of that data into `output` until its stringified length fits in `maxLength` characters.
|
|
257
|
+
*
|
|
258
|
+
* In general, output should track with `JSON.stringify(item).substring(0, maxLength)` (i.e. depth-first traversal of arrays and object entries), but in a JSON-aware way.
|
|
259
|
+
* If the item's initial stringified length is less than or equal to `maxLength`, the item is returned as-is.
|
|
260
|
+
* @param {object | any[]} data The JSON object or array to be truncated.
|
|
261
|
+
* @param {number} maxLength The maximum length of JSON.stringify(output). Note that this may not be the exact output length, but it serves as an upper bound. Minimum value is 40.
|
|
262
|
+
* @returns {object | any[]} The truncated object or array.
|
|
263
|
+
*/
|
|
264
|
+
const truncateData = (data, maxLength) => {
|
|
265
|
+
if (!data || typeof data !== 'object') {
|
|
266
|
+
// the following code is only meant to work on objects and arrays
|
|
267
|
+
return data;
|
|
268
|
+
}
|
|
269
|
+
if (JSON.stringify(data).length <= maxLength) {
|
|
270
|
+
// no need to truncate
|
|
271
|
+
return data;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const root = Array.isArray(data) ? [] : {};
|
|
275
|
+
let length = 2; // '{}' or '[]'
|
|
276
|
+
let dataWasTruncated = false; // used during iteration to track if a string was truncated
|
|
277
|
+
const truncateMessageSize = 39; // the overhead required to add a message about truncating data
|
|
278
|
+
|
|
279
|
+
if (maxLength < 40) {
|
|
280
|
+
// adding the truncate message takes 39 characters, but the minimum output (i.e. just the message wrapped in an object or array)
|
|
281
|
+
// is 40 characters due to the overhead of the {} or [] characters (+2) minus the comma (-1)
|
|
282
|
+
throw new Error(`maxLength must be at least 40`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const queue = Array.isArray(data)
|
|
286
|
+
? data.map((value) => [root, '', value])
|
|
287
|
+
: Object.entries(data).map(([key, value]) => [root, key, value]);
|
|
288
|
+
|
|
289
|
+
// iterate over the queue
|
|
290
|
+
while (queue.length > 0) {
|
|
291
|
+
const [parent, key, item] = queue.shift();
|
|
292
|
+
const [itemLength, processedItem, itemWasTruncated] = _processItem(
|
|
293
|
+
queue,
|
|
294
|
+
parent,
|
|
295
|
+
key,
|
|
296
|
+
item,
|
|
297
|
+
maxLength - length - truncateMessageSize
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (itemWasTruncated) {
|
|
301
|
+
// if a string was truncated, we mark the total data as truncated for messaging purposes
|
|
302
|
+
dataWasTruncated = true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (length + itemLength + truncateMessageSize < maxLength) {
|
|
306
|
+
// we're still under the max length, add this and keep going
|
|
307
|
+
_addItem(parent, key, processedItem);
|
|
308
|
+
length += itemLength;
|
|
309
|
+
} else {
|
|
310
|
+
if (length + itemLength + truncateMessageSize === maxLength) {
|
|
311
|
+
// we can fit this item + the truncate message, so let's add it before we stop
|
|
312
|
+
_addItem(parent, key, processedItem);
|
|
313
|
+
}
|
|
314
|
+
dataWasTruncated = true;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// we can hit the following even if we got through all the items in the queue in the case that any strings were truncated
|
|
320
|
+
if (dataWasTruncated) {
|
|
321
|
+
if (Array.isArray(root)) {
|
|
322
|
+
root.push('NOTE : This data has been truncated.');
|
|
323
|
+
} else {
|
|
324
|
+
root.NOTE = 'This data has been truncated.';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return root;
|
|
329
|
+
};
|
|
330
|
+
|
|
175
331
|
const genId = () => parseInt(Math.random() * 100000000);
|
|
176
332
|
|
|
177
333
|
module.exports = {
|
|
@@ -186,4 +342,5 @@ module.exports = {
|
|
|
186
342
|
memoizedFindMapDeep,
|
|
187
343
|
recurseReplace,
|
|
188
344
|
simpleTruncate,
|
|
345
|
+
truncateData,
|
|
189
346
|
};
|