zapier-platform-core 11.2.0 → 11.3.2

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": "11.2.0",
3
+ "version": "11.3.2",
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/",
@@ -47,10 +47,10 @@
47
47
  "form-data": "4.0.0",
48
48
  "lodash": "4.17.21",
49
49
  "mime-types": "2.1.34",
50
- "node-fetch": "2.6.6",
50
+ "node-fetch": "2.6.7",
51
51
  "oauth-sign": "0.9.0",
52
52
  "semver": "7.3.5",
53
- "zapier-platform-schema": "11.2.0"
53
+ "zapier-platform-schema": "11.3.2"
54
54
  },
55
55
  "devDependencies": {
56
56
  "adm-zip": "0.5.5",
@@ -14,7 +14,24 @@ const addQueryParams = (req) => {
14
14
 
15
15
  normalizeEmptyParamFields(req);
16
16
 
17
- const stringifiedParams = querystring.stringify(req.params);
17
+ let stringifiedParams = querystring.stringify(req.params);
18
+
19
+ // it goes against spec, but for compatibility, some APIs want certain
20
+ // characters (mostly $) unencoded
21
+ if (req.skipEncodingChars) {
22
+ for (let i = 0; i < req.skipEncodingChars.length; i++) {
23
+ const char = req.skipEncodingChars.charAt(i);
24
+ const valToReplace = querystring.escape(char);
25
+ if (valToReplace === char) {
26
+ continue;
27
+ }
28
+ // no replaceAll in JS yet, coming in a node version soon!
29
+ stringifiedParams = stringifiedParams.replace(
30
+ new RegExp(valToReplace, 'g'),
31
+ char
32
+ );
33
+ }
34
+ }
18
35
 
19
36
  if (stringifiedParams) {
20
37
  req.url += `${splitter}${stringifiedParams}`;
@@ -158,7 +158,7 @@ const isEmptyQueryParam = (value) =>
158
158
  value === '' ||
159
159
  value === null ||
160
160
  value === undefined ||
161
- isCurlies.test(value);
161
+ (typeof value === 'string' && value.search(isCurlies) >= 0);
162
162
 
163
163
  const normalizeEmptyParamFields = normalizeEmptyRequestFields.bind(
164
164
  null,
@@ -167,7 +167,7 @@ const normalizeEmptyParamFields = normalizeEmptyRequestFields.bind(
167
167
  );
168
168
  const normalizeEmptyBodyFields = normalizeEmptyRequestFields.bind(
169
169
  null,
170
- (v) => isCurlies.test(v),
170
+ (v) => typeof v === 'string' && v.search(isCurlies) >= 0,
171
171
  'body'
172
172
  );
173
173
 
@@ -3,13 +3,13 @@ const _ = require('lodash');
3
3
  const constants = require('../constants');
4
4
 
5
5
  const createHttpPatch = (event) => {
6
- const createLogger = require('./create-logger');
7
- const logBuffer = [];
8
- const logger = createLogger(event, { logBuffer });
9
-
10
- const httpPatch = (object) => {
6
+ const httpPatch = (object, logger) => {
11
7
  const originalRequest = object.request;
12
8
 
9
+ // Important not to reuse logger between calls, because we always destroy
10
+ // the logger at the end of a Lambda call.
11
+ object.zapierLogger = logger;
12
+
13
13
  // Avoids multiple patching and memory leaks (mostly when running tests locally)
14
14
  if (object.patchedByZapier) {
15
15
  return;
@@ -21,13 +21,15 @@ const createHttpPatch = (event) => {
21
21
  object.request = (options, callback) => {
22
22
  // `options` can be an object or a string. If options is a string, it is
23
23
  // automatically parsed with url.parse().
24
- // See https://nodejs.org/docs/latest-v6.x/api/http.html#http_http_request_options_callback
24
+ // See https://nodejs.org/docs/latest-v14.x/api/http.html#http_http_request_options_callback
25
25
  let requestUrl;
26
26
  if (typeof options === 'string') {
27
27
  requestUrl = options;
28
28
  } else if (typeof options.url === 'string') {
29
- // XXX: Somehow options.url is available for some requests although http.request doesn't really accept it.
30
- // Without this else-if, many HTTP requests don't work. Should take a deeper look at this weirdness.
29
+ // XXX: Somehow options.url is available for some requests although
30
+ // http.request doesn't really accept it. Without this else-if, many
31
+ // HTTP requests don't work. Should take a deeper look at this
32
+ // weirdness.
31
33
  requestUrl = options.url;
32
34
  } else {
33
35
  requestUrl =
@@ -67,7 +69,7 @@ const createHttpPatch = (event) => {
67
69
  response_content: responseBody,
68
70
  };
69
71
 
70
- logger(
72
+ object.zapierLogger(
71
73
  `${logData.response_status_code} ${logData.request_method} ${logData.request_url}`,
72
74
  logData
73
75
  );
@@ -147,37 +147,36 @@ const createLambdaHandler = (appRawOrPath) => {
147
147
 
148
148
  environmentTools.cleanEnvironment();
149
149
 
150
+ // Copy bundle environment into process.env *before* creating the logger and
151
+ // loading app code, so that the logger gets the endpoint from process.env,
152
+ // and top level app code can get bundle environment vars via process.env.
153
+ environmentTools.applyEnvironment(event);
154
+
150
155
  // Create logger outside of domain, so we can use in both error and run callbacks.
151
156
  const logBuffer = [];
152
157
  const logger = createLogger(event, { logBuffer });
153
158
 
154
159
  let isCallbackCalled = false;
155
160
  const callbackOnce = (err, resp) => {
156
- if (!isCallbackCalled) {
157
- isCallbackCalled = true;
158
- callback(err, resp);
159
- }
161
+ logger.end().finally(() => {
162
+ if (!isCallbackCalled) {
163
+ isCallbackCalled = true;
164
+ callback(err, resp);
165
+ }
166
+ });
160
167
  };
161
168
 
162
169
  const logErrorAndCallbackOnce = (logMsg, logData, err) => {
163
- // Wait for logger to complete before callback. This isn't
164
- // strictly necessary because callbacksWaitsForEmptyLoop is
165
- // the default behavior with callbacks anyway, but don't want
166
- // to rely on that.
167
- logger(logMsg, logData).then(() => {
168
- // Check for `.message` in case someone did `throw "My Error"`
169
- if (
170
- !constants.IS_TESTING &&
171
- err &&
172
- !err.doNotContextify &&
173
- err.message
174
- ) {
175
- err.message += `\n\nConsole logs:\n${logBuffer
176
- .map((s) => ` ${s.message}`)
177
- .join('')}`;
178
- }
179
- callbackOnce(err);
180
- });
170
+ logger(logMsg, logData);
171
+
172
+ // Check for `.message` in case someone did `throw "My Error"`
173
+ if (!constants.IS_TESTING && err && !err.doNotContextify && err.message) {
174
+ err.message += `\n\nConsole logs:\n${logBuffer
175
+ .map((s) => ` ${s.message}`)
176
+ .join('')}`;
177
+ }
178
+
179
+ callbackOnce(err);
181
180
  };
182
181
 
183
182
  const handlerDomain = domain.create();
@@ -191,10 +190,6 @@ const createLambdaHandler = (appRawOrPath) => {
191
190
  });
192
191
 
193
192
  handlerDomain.run(() => {
194
- // Copy bundle environment into process.env *before* loading app code,
195
- // so that top level app code can get bundle environment vars via process.env.
196
- environmentTools.applyEnvironment(event);
197
-
198
193
  const rpc = createRpcClient(event);
199
194
 
200
195
  return loadApp(event, rpc, appRawOrPath)
@@ -203,10 +198,10 @@ const createLambdaHandler = (appRawOrPath) => {
203
198
 
204
199
  const { skipHttpPatch } = appRaw.flags || {};
205
200
  // Adds logging for _all_ kinds of http(s) requests, no matter the library
206
- if (!skipHttpPatch) {
201
+ if (!skipHttpPatch && !event.calledFromCli) {
207
202
  const httpPatch = createHttpPatch(event);
208
- httpPatch(require('http'));
209
- httpPatch(require('https')); // 'https' needs to be patched separately
203
+ httpPatch(require('http'), logger);
204
+ httpPatch(require('https'), logger); // 'https' needs to be patched separately
210
205
  }
211
206
 
212
207
  // TODO: Avoid calling prepareApp(appRaw) repeatedly here as createApp()
@@ -8,8 +8,11 @@ const semver = require('semver');
8
8
  const createLegacyScriptingRunner = (z, input) => {
9
9
  const app = _.get(input, '_zapier.app');
10
10
 
11
- let source =
12
- _.get(app, 'legacy.scriptingSource') || app.legacyScriptingSource;
11
+ // once we have node 14 everywhere, this can be:
12
+ // let source = _.get(app, 'legacy.scriptingSource') ?? app.legacyScriptingSource;
13
+ let source = _.get(app, 'legacy.scriptingSource');
14
+ source = source === undefined ? app.legacyScriptingSource : source;
15
+
13
16
  if (source === undefined) {
14
17
  // Don't initialize z.legacyScripting for a pure CLI app
15
18
  return null;
@@ -26,8 +29,8 @@ const createLegacyScriptingRunner = (z, input) => {
26
29
  let LegacyScriptingRunner, version;
27
30
  try {
28
31
  LegacyScriptingRunner = require('zapier-platform-legacy-scripting-runner');
29
- version = require('zapier-platform-legacy-scripting-runner/package.json')
30
- .version;
32
+ version =
33
+ require('zapier-platform-legacy-scripting-runner/package.json').version;
31
34
  } catch (e) {
32
35
  // Find it in cwd, in case we're developing legacy-scripting-runner itself
33
36
  const cwd = process.cwd();
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { Transform } = require('stream');
4
+
3
5
  const _ = require('lodash');
4
6
 
5
7
  const request = require('./request-client-internal');
6
8
  const { simpleTruncate, recurseReplace } = require('./data');
7
- const ZapierPromise = require('./promise');
8
9
  const {
9
10
  DEFAULT_LOGGING_HTTP_API_KEY,
10
11
  DEFAULT_LOGGING_HTTP_ENDPOINT,
@@ -19,6 +20,10 @@ const {
19
20
  // not really a public function, but it came from here originally
20
21
  const { isUrlWithSecrets } = require('@zapier/secret-scrubber/lib/convenience');
21
22
 
23
+ // The payload size per request to stream logs. This should be slighly lower
24
+ // than the limit (16 MB) on the server side.
25
+ const LOG_STREAM_BYTES_LIMIT = 15 * 1024 * 1024;
26
+
22
27
  const isUrl = (url) => {
23
28
  try {
24
29
  // eslint-disable-next-line no-new
@@ -121,7 +126,75 @@ const buildSensitiveValues = (event, data) => {
121
126
  ];
122
127
  };
123
128
 
124
- const sendLog = (options, event, message, data) => {
129
+ class LogStream extends Transform {
130
+ constructor(options) {
131
+ super(options);
132
+ this.bytesWritten = 0;
133
+ this.request = this._newRequest(options.url, options.token);
134
+ }
135
+
136
+ _newRequest(url, token) {
137
+ const httpOptions = {
138
+ url,
139
+ method: 'POST',
140
+ headers: {
141
+ 'Content-Type': 'application/x-ndjson',
142
+ 'X-Token': token,
143
+ },
144
+ body: this,
145
+ };
146
+ return request(httpOptions).catch((err) => {
147
+ // Swallow logging errors. This will show up in AWS logs at least.
148
+ console.error(
149
+ 'Error making log request:',
150
+ err,
151
+ 'http options:',
152
+ httpOptions
153
+ );
154
+ });
155
+ }
156
+
157
+ _transform(chunk, encoding, callback) {
158
+ this.push(chunk);
159
+ this.bytesWritten += Buffer.byteLength(chunk, encoding);
160
+ callback();
161
+ }
162
+ }
163
+
164
+ // Implements singleton for LogStream. The goal is for every sendLog() call we
165
+ // reuse the same request until the request body grows too big and exceeds
166
+ // LOG_STREAM_BYTES_LIMIT.
167
+ class LogStreamFactory {
168
+ constructor() {
169
+ this._logStream = null;
170
+ }
171
+
172
+ getOrCreate(url, token) {
173
+ if (this._logStream) {
174
+ if (this._logStream.bytesWritten < LOG_STREAM_BYTES_LIMIT) {
175
+ // Reuse the same request for efficiency
176
+ return this._logStream;
177
+ }
178
+
179
+ // End this one before creating another
180
+ this._logStream.end();
181
+ }
182
+
183
+ this._logStream = new LogStream({ url, token });
184
+ return this._logStream;
185
+ }
186
+
187
+ async end() {
188
+ if (this._logStream) {
189
+ this._logStream.end();
190
+ const response = await this._logStream.request;
191
+ this._logStream = null;
192
+ return response;
193
+ }
194
+ }
195
+ }
196
+
197
+ const sendLog = async (logStreamFactory, options, event, message, data) => {
125
198
  data = _.extend({}, data || {}, event.logExtra || {});
126
199
  data.log_type = data.log_type || 'console';
127
200
 
@@ -148,20 +221,6 @@ const sendLog = (options, event, message, data) => {
148
221
  safeData.request_headers = formatHeaders(safeData.request_headers);
149
222
  safeData.response_headers = formatHeaders(safeData.response_headers);
150
223
 
151
- const body = {
152
- message: safeMessage,
153
- data: safeData,
154
- token: options.token,
155
- };
156
-
157
- const httpOptions = {
158
- url: options.endpoint,
159
- method: 'POST',
160
- headers: { 'Content-Type': 'application/json' },
161
- body: JSON.stringify(body),
162
- timeout: 3000,
163
- };
164
-
165
224
  if (event.logToStdout) {
166
225
  toStdout(event, message, unsafeData);
167
226
  }
@@ -172,24 +231,36 @@ const sendLog = (options, event, message, data) => {
172
231
  }
173
232
 
174
233
  if (options.token) {
175
- return request(httpOptions).catch((err) => {
176
- // Swallow logging errors.
177
- // This will show up in AWS logs at least:
178
- console.error(
179
- 'Error making log request:',
180
- err,
181
- 'http options:',
182
- httpOptions
183
- );
184
- });
185
- } else {
186
- return ZapierPromise.resolve();
234
+ const logStream = logStreamFactory.getOrCreate(
235
+ options.endpoint,
236
+ options.token
237
+ );
238
+ logStream.write(
239
+ // JSON Lines format: It's important the serialized JSON object itself has
240
+ // no line breaks, and after an object it ends with a line break.
241
+ JSON.stringify({ message: safeMessage, data: safeData }) + '\n'
242
+ );
187
243
  }
188
244
  };
189
245
 
190
246
  /*
191
247
  Creates low level logging function that POSTs to endpoint (GL by default).
192
248
  Use internally; do not expose to devs.
249
+
250
+ Usage:
251
+
252
+ const logger = createLogger(event, options);
253
+
254
+ // These will reuse the same request to the log server
255
+ logger('log message here', { log_type: 'console' });
256
+ logger('another log', { log_type: 'console' });
257
+ logger('200 GET https://example.com', { log_type: 'http' });
258
+
259
+ // After an invocation, the Lambda handler MUST call logger.end() to close
260
+ // the log stream. Otherwise, it will hang!
261
+ logger.end().finally(() => {
262
+ // anything else you want to do to finish an invocation
263
+ });
193
264
  */
194
265
  const createLogger = (event, options) => {
195
266
  options = options || {};
@@ -201,7 +272,13 @@ const createLogger = (event, options) => {
201
272
  token: process.env.LOGGING_TOKEN || event.token,
202
273
  });
203
274
 
204
- return sendLog.bind(undefined, options, event);
275
+ const logStreamFactory = new LogStreamFactory();
276
+ const logger = sendLog.bind(undefined, logStreamFactory, options, event);
277
+
278
+ logger.end = async () => {
279
+ return logStreamFactory.end();
280
+ };
281
+ return logger;
205
282
  };
206
283
 
207
284
  module.exports = createLogger;