zapier-platform-cli 16.3.1 → 16.4.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.
@@ -178,11 +178,18 @@
178
178
  "required": true
179
179
  }
180
180
  },
181
- "description": "Mark a non-production version of your integration as deprecated, with removal by a certain date.\n\nUse this when an integration version will not be supported or start breaking at a known date.\n\nZapier will send an email warning users of the deprecation once a date is set, they'll start seeing it as \"Deprecated\" in the UI, and once the deprecation date arrives, if the Zaps weren't updated, they'll be paused and the users will be emailed again explaining what happened.\n\nAfter the deprecation date has passed it will be safe to delete that integration version.\n\nDo not use this if you have non-breaking changes, such as fixing help text.",
181
+ "description": "Mark a non-production version of your integration as deprecated, with removal by a certain date.\n\nUse this when an integration version will not be supported or start breaking at a known date.\n\nZapier will immediately send emails warning users of the deprecation if a date less than 30 days in the future is set, otherwise the emails will be sent exactly 30 days before the configured deprecation date.\n\nThere are other side effects: they'll start seeing it as \"Deprecated\" in the UI, and once the deprecation date arrives, if the Zaps weren't updated, they'll be paused and the users will be emailed again explaining what happened.\n\nDo not use deprecation if you only have non-breaking changes, such as:\n- Fixing help text\n- Adding new triggers/actions\n- Improving existing functionality\n- other bug fixes that don't break existing automations.",
182
182
  "examples": [
183
183
  "zapier deprecate 1.2.3 2011-10-01"
184
184
  ],
185
185
  "flags": {
186
+ "force": {
187
+ "char": "f",
188
+ "description": "Skip confirmation prompt. Use with caution.",
189
+ "name": "force",
190
+ "allowNo": false,
191
+ "type": "boolean"
192
+ },
186
193
  "debug": {
187
194
  "char": "d",
188
195
  "description": "Show extra debugging output.",
@@ -459,7 +466,7 @@
459
466
  "name": "actionKey"
460
467
  }
461
468
  },
462
- "description": "Invoke an auth operation, a trigger, or a create/search action locally.\n\nThis command emulates how Zapier production environment would invoke your integration. It runs code locally, so you can use this command to quickly test your integration without deploying it to Zapier. This is especially useful for debugging and development.\n\nThis command loads environment variables and `authData` from the `.env` file in the current directory. If you don't have a `.env` file yet, you can use the `zapier invoke auth start` command to help you initialize it, or you can manually create it.\n\nThe `zapier invoke auth start` subcommand will prompt you for the necessary auth fields and save them to the `.env` file. For OAuth2, it will start a local HTTP server, open the authorization URL in the browser, wait for the OAuth2 redirect, and get the access token.\n\nEach line in the `.env` file should follow one of these formats:\n\n* `VAR_NAME=VALUE` for environment variables\n* `authData_FIELD_KEY=VALUE` for auth data fields\n\nFor example, a `.env` file for an OAuth2 integration might look like this:\n\n```\nCLIENT_ID='your_client_id'\nCLIENT_SECRET='your_client_secret'\nauthData_access_token='1234567890'\nauthData_refresh_token='abcdefg'\nauthData_account_name='zapier'\n```\n\nTo test if the auth data is correct, run either one of these:\n\n```\nzapier invoke auth test # invokes authentication.test method\nzapier invoke auth label # invokes authentication.test and renders connection label\n```\n\nTo refresh stale auth data for OAuth2 or session auth, run `zapier invoke auth refresh`.\n\nOnce you have the correct auth data, you can test an trigger, a search, or a create action. For example, here's how you invoke a trigger with the key `new_recipe`:\n\n```\nzapier invoke trigger new_recipe\n```\n\nTo add input data, use the `--inputData` flag. The input data can come from the command directly, a file, or stdin. See **EXAMPLES** below.\n\nWhen you miss any command arguments, such as ACTIONTYPE or ACTIONKEY, the command will prompt you interactively. If you don't want to get interactive prompts, use the `--non-interactive` flag.\n\nThe `--debug` flag will show you the HTTP request logs and any console logs you have in your code.\n\nThe following is a non-exhaustive list of current limitations and may be supported in the future:\n\n- Hook triggers, including REST hook subscribe/unsubscribe\n- Line items\n- Output hydration\n- File upload\n- Dynamic dropdown pagination\n- Function-based connection label\n- Buffered create actions\n- Search-or-create actions\n- Search-powered fields\n- Field choices\n- autoRefresh for OAuth2 and session auth\n",
469
+ "description": "Invoke an auth operation, a trigger, or a create/search action locally.\n\nThis command emulates how Zapier production environment would invoke your integration. It runs code locally, so you can use this command to quickly test your integration without deploying it to Zapier. This is especially useful for debugging and development.\n\nWhy use this command?\n\n* Fast feedback loop: Write code and run this command to verify if it works immediately\n* Step-by-step debugging: Running locally means you can use a debugger to step through your code\n* Untruncated logs: View complete logs and errors in your terminal\n\n### Authentication\n\nYou can supply the authentcation data in two ways: Load from the local `.env` file or use the (experimental) `--authentication-id` flag.\n\n#### The local `.env` file\n\nThis command loads environment variables and `authData` from the `.env` file in the current directory. If you don't have a `.env` file yet, you can use the `zapier invoke auth start` command to help you initialize it, or you can manually create it.\n\nThe `zapier invoke auth start` subcommand will prompt you for the necessary auth fields and save them to the `.env` file. For OAuth2, it will start a local HTTP server, open the authorization URL in the browser, wait for the OAuth2 redirect, and get the access token.\n\nEach line in the `.env` file should follow one of these formats:\n\n* `VAR_NAME=VALUE` for environment variables\n* `authData_FIELD_KEY=VALUE` for auth data fields\n\nFor example, a `.env` file for an OAuth2 integration might look like this:\n\n```\nCLIENT_ID='your_client_id'\nCLIENT_SECRET='your_client_secret'\nauthData_access_token='1234567890'\nauthData_refresh_token='abcdefg'\nauthData_account_name='zapier'\n```\n\n\n#### The `--authentication-id` flag (EXPERIMENTAL)\n\nSetting up local auth data can be troublesome. You'd have to configure your app server to allow localhost redirect URIs or use a port forwarding tool. This is sometimes not easy to get right.\n\nThe `--authentication-id` flag (`-a` for short) gives you an alternative (and perhaps easier) way to supply your auth data. You can use `-a` to specify an existing production authentication/connection. The available authentications can be found at https://zapier.com/app/assets/connections. Check https://zpr.io/z8SjFTdnTFZ2 for more instructions.\n\nWhen `-a -` is specified, such as `zapier invoke auth test -a -`, the command will interactively prompt you to select one of your available authentications.\n\nIf you know your authentication ID, you can specify it directly, such as `zapier invoke auth test -a 123456`.\n\n#### Testing authentication\n\nTo test if the auth data is correct, run either one of these:\n\n```\nzapier invoke auth test # invokes authentication.test method\nzapier invoke auth label # invokes authentication.test and renders connection label\n```\n\nTo refresh stale auth data for OAuth2 or session auth, run `zapier invoke auth refresh`. Note that refreshing is only applicable for local auth data in the `.env` file.\n\n### Invoking a trigger or an action\n\nOnce you have the correct auth data, you can test an trigger, a search, or a create action. For example, here's how you invoke a trigger with the key `new_recipe`:\n\n```\nzapier invoke trigger new_recipe\n```\n\nTo add input data, use the `--inputData` flag (`-i` for short). The input data can come from the command directly, a file, or stdin. See **EXAMPLES** below.\n\nWhen you miss any command arguments, such as ACTIONTYPE or ACTIONKEY, the command will prompt you interactively. If you don't want to get interactive prompts, use the `--non-interactive` flag.\n\nThe `--debug` flag will show you the HTTP request logs and any console logs you have in your code.\n\n### Limitations\n\nThe following is a non-exhaustive list of current limitations and may be supported in the future:\n\n- Hook triggers, including REST hook subscribe/unsubscribe\n- Line items\n- Output hydration\n- File upload\n- Dynamic dropdown pagination\n- Function-based connection label\n- Buffered create actions\n- Search-or-create actions\n- Search-powered fields\n- Field choices\n- autoRefresh for OAuth2 and session auth\n",
463
470
  "examples": [
464
471
  "zapier invoke",
465
472
  "zapier invoke auth start",
@@ -468,8 +475,10 @@
468
475
  "zapier invoke auth label",
469
476
  "zapier invoke trigger new_recipe",
470
477
  "zapier invoke create add_recipe --inputData '{\"title\": \"Pancakes\"}'",
471
- "zapier invoke search find_recipe -i @file.json",
472
- "cat file.json | zapier invoke trigger new_recipe -i @-"
478
+ "zapier invoke search find_recipe -i @file.json --non-interactive",
479
+ "cat file.json | zapier invoke trigger new_recipe -i @-",
480
+ "zapier invoke search find_ticket --authentication-id 12345",
481
+ "zapier invoke create add_ticket -a -"
473
482
  ],
474
483
  "flags": {
475
484
  "inputData": {
@@ -546,6 +555,14 @@
546
555
  "multiple": false,
547
556
  "type": "option"
548
557
  },
558
+ "authentication-id": {
559
+ "char": "a",
560
+ "description": "EXPERIMENTAL: Instead of using the local .env file, use the production authentication data with the given authentication ID (aka the \"app connection\" on Zapier). Find them at https://zapier.com/app/assets/connections (https://zpr.io/z8SjFTdnTFZ2 for instructions) or specify '-' to interactively select one from your available authentications. When specified, the code will still run locally, but all outgoing requests will be proxied through Zapier with the production auth data.",
561
+ "name": "authentication-id",
562
+ "hasDynamicHelp": false,
563
+ "multiple": false,
564
+ "type": "option"
565
+ },
549
566
  "debug": {
550
567
  "char": "d",
551
568
  "description": "Show extra debugging output.",
@@ -2341,5 +2358,5 @@
2341
2358
  ]
2342
2359
  }
2343
2360
  },
2344
- "version": "16.3.1"
2361
+ "version": "16.4.0"
2345
2362
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zapier-platform-cli",
3
- "version": "16.3.1",
3
+ "version": "16.4.0",
4
4
  "description": "The CLI for managing integrations in Zapier Developer Platform.",
5
5
  "repository": "zapier/zapier-platform",
6
6
  "homepage": "https://platform.zapier.com/",
@@ -18,7 +18,7 @@ class ZapierBaseCommand extends Command {
18
18
 
19
19
  if (this.flags.debug) {
20
20
  this.debug.enabled = true; // enables this.debug on the command
21
- require('debug').enable('zapier:*'); // enables all further spawned functions, like API
21
+ require('debug').enable('zapier:*,oclif:zapier:*'); // enables all further spawned functions, like API
22
22
  }
23
23
 
24
24
  this.debug('argv is', this.argv);
@@ -1,15 +1,38 @@
1
1
  const BaseCommand = require('../ZapierBaseCommand');
2
- const { Args } = require('@oclif/core');
2
+ const { Args, Flags } = require('@oclif/core');
3
3
  const { buildFlags } = require('../buildFlags');
4
+ const colors = require('colors/safe');
4
5
 
5
- const { callAPI } = require('../../utils/api');
6
+ const { callAPI, getSpecificVersionInfo } = require('../../utils/api');
6
7
 
7
8
  class DeprecateCommand extends BaseCommand {
8
9
  async perform() {
9
10
  const app = await this.getWritableApp();
10
11
  const { version, date } = this.args;
12
+
13
+ const versionInfo = await getSpecificVersionInfo(version);
14
+ const hasActiveUsers = versionInfo.user_count && versionInfo.user_count > 0;
15
+
16
+ this.log(
17
+ `${colors.yellow('Warning: Deprecation is an irreversible action that will eventually block access to this version.')}\n` +
18
+ `${colors.yellow('If all your changes are non-breaking, use `zapier migrate` instead to move users over to a newer version.')}\n`,
19
+ );
20
+
21
+ if (
22
+ !this.flags.force &&
23
+ !(await this.confirm(
24
+ 'Are you sure you want to deprecate this version? This will notify users that their Zaps or other automations will stop working after the specified date.' +
25
+ (hasActiveUsers
26
+ ? `\n\nThis version has ${versionInfo.user_count} active user(s). Strongly consider migrating users to another version before deprecating!`
27
+ : ''),
28
+ ))
29
+ ) {
30
+ this.log('\nDeprecation cancelled.');
31
+ return;
32
+ }
33
+
11
34
  this.log(
12
- `Preparing to deprecate version ${version} your app "${app.title}".\n`,
35
+ `\nPreparing to deprecate version ${version} your app "${app.title}".\n`,
13
36
  );
14
37
  const url = `/apps/${app.id}/versions/${version}/deprecate`;
15
38
  this.startSpinner(`Deprecating ${version}`);
@@ -21,12 +44,19 @@ class DeprecateCommand extends BaseCommand {
21
44
  });
22
45
  this.stopSpinner();
23
46
  this.log(
24
- `\nWe'll let users know that this version is no longer recommended and will cease to work on ${date}.`,
47
+ `\nWe'll let users know that this version will cease to work on ${date}.`,
25
48
  );
26
49
  }
27
50
  }
28
51
 
29
- DeprecateCommand.flags = buildFlags();
52
+ DeprecateCommand.flags = buildFlags({
53
+ commandFlags: {
54
+ force: Flags.boolean({
55
+ char: 'f',
56
+ description: 'Skip confirmation prompt. Use with caution.',
57
+ }),
58
+ },
59
+ });
30
60
  DeprecateCommand.args = {
31
61
  version: Args.string({
32
62
  description: 'The version to deprecate.',
@@ -43,11 +73,15 @@ DeprecateCommand.description = `Mark a non-production version of your integratio
43
73
 
44
74
  Use this when an integration version will not be supported or start breaking at a known date.
45
75
 
46
- Zapier will send an email warning users of the deprecation once a date is set, they'll start seeing it as "Deprecated" in the UI, and once the deprecation date arrives, if the Zaps weren't updated, they'll be paused and the users will be emailed again explaining what happened.
76
+ Zapier will immediately send emails warning users of the deprecation if a date less than 30 days in the future is set, otherwise the emails will be sent exactly 30 days before the configured deprecation date.
47
77
 
48
- After the deprecation date has passed it will be safe to delete that integration version.
78
+ There are other side effects: they'll start seeing it as "Deprecated" in the UI, and once the deprecation date arrives, if the Zaps weren't updated, they'll be paused and the users will be emailed again explaining what happened.
49
79
 
50
- Do not use this if you have non-breaking changes, such as fixing help text.`;
80
+ Do not use deprecation if you only have non-breaking changes, such as:
81
+ - Fixing help text
82
+ - Adding new triggers/actions
83
+ - Improving existing functionality
84
+ - other bug fixes that don't break existing automations.`;
51
85
  DeprecateCommand.skipValidInstallCheck = true;
52
86
 
53
87
  module.exports = DeprecateCommand;
@@ -15,6 +15,12 @@ const BaseCommand = require('../ZapierBaseCommand');
15
15
  const { buildFlags } = require('../buildFlags');
16
16
  const { localAppCommand, getLocalAppHandler } = require('../../utils/local');
17
17
  const { startSpinner, endSpinner } = require('../../utils/display');
18
+ const {
19
+ getLinkedAppConfig,
20
+ listAuthentications,
21
+ readCredentials,
22
+ } = require('../../utils/api');
23
+ const { AUTH_KEY } = require('../../constants');
18
24
 
19
25
  const ACTION_TYPE_PLURALS = {
20
26
  trigger: 'triggers',
@@ -235,9 +241,88 @@ const appendEnv = async (vars, prefix = '') => {
235
241
  );
236
242
  };
237
243
 
238
- const testAuth = async (authData, meta, zcacheTestObj) => {
244
+ const replaceDoubleCurlies = async (request) => {
245
+ // Use lcurly-fieldName-rcurly instead of {{fieldName}} to bypass node-fetch's
246
+ // URL validation in case the variable is used in a URL.
247
+ if (request.url) {
248
+ request.url = request.url
249
+ .replaceAll('{{', 'lcurly-')
250
+ .replaceAll('}}', '-rcurly');
251
+ }
252
+
253
+ // The authorization header may confuse zapier.com and it's relay's job to add
254
+ // it, so we delete it here.
255
+ delete request.headers.authorization;
256
+ delete request.headers.Authorization;
257
+
258
+ return request;
259
+ };
260
+
261
+ const restoreDoubleCurlies = async (response) => {
262
+ if (response.url) {
263
+ response.url = response.url
264
+ .replaceAll('lcurly-', '{{')
265
+ .replaceAll('-rcurly', '}}');
266
+ }
267
+ if (response.request?.url) {
268
+ response.request.url = response.request.url
269
+ .replaceAll('lcurly-', '{{')
270
+ .replaceAll('-rcurly', '}}');
271
+ }
272
+ return response;
273
+ };
274
+
275
+ const localAppCommandWithRelayErrorHandler = async (args) => {
276
+ if (args.relayAuthenticationId) {
277
+ args = {
278
+ ...args,
279
+ beforeRequest: [replaceDoubleCurlies],
280
+ afterResponse: [restoreDoubleCurlies],
281
+ };
282
+ }
283
+
284
+ let output;
285
+ try {
286
+ output = await localAppCommand(args);
287
+ } catch (outerError) {
288
+ if (outerError.name === 'ResponseError') {
289
+ let response;
290
+ try {
291
+ response = JSON.parse(outerError.message);
292
+ } catch (innerError) {
293
+ throw outerError;
294
+ }
295
+ if (typeof response.content === 'string') {
296
+ const match = response.content.match(/domain filter `([^`]+)`/);
297
+ if (!match) {
298
+ throw outerError;
299
+ }
300
+ const domainFilter = match[1];
301
+ const requestUrl = response.request.url
302
+ .replaceAll('lcurly-', '{{')
303
+ .replaceAll('-rcurly', '}}');
304
+ throw new Error(
305
+ `Request to ${requestUrl} was blocked. ` +
306
+ `Only these domain names are allowed: ${domainFilter}. ` +
307
+ 'Contact Zapier team to verify your domain filter setting.',
308
+ );
309
+ }
310
+ }
311
+ throw outerError;
312
+ }
313
+ return output;
314
+ };
315
+
316
+ const testAuth = async (
317
+ authId,
318
+ authData,
319
+ meta,
320
+ zcacheTestObj,
321
+ appId,
322
+ deployKey,
323
+ ) => {
239
324
  startSpinner('Invoking authentication.test');
240
- const result = await localAppCommand({
325
+ const result = await localAppCommandWithRelayErrorHandler({
241
326
  command: 'execute',
242
327
  method: 'authentication.test',
243
328
  bundle: {
@@ -250,13 +335,31 @@ const testAuth = async (authData, meta, zcacheTestObj) => {
250
335
  zcacheTestObj,
251
336
  customLogger,
252
337
  calledFromCliInvoke: true,
338
+ appId,
339
+ deployKey,
340
+ relayAuthenticationId: authId,
253
341
  });
254
342
  endSpinner();
255
343
  return result;
256
344
  };
257
345
 
258
- const getAuthLabel = async (labelTemplate, authData, meta, zcacheTestObj) => {
259
- const testResult = await testAuth(authData, meta, zcacheTestObj);
346
+ const getAuthLabel = async (
347
+ labelTemplate,
348
+ authId,
349
+ authData,
350
+ meta,
351
+ zcacheTestObj,
352
+ appId,
353
+ deployKey,
354
+ ) => {
355
+ const testResult = await testAuth(
356
+ authId,
357
+ authData,
358
+ meta,
359
+ zcacheTestObj,
360
+ appId,
361
+ deployKey,
362
+ );
260
363
  labelTemplate = labelTemplate.replace('__', '.');
261
364
  const tpl = _.template(labelTemplate, { interpolate: /{{([\s\S]+?)}}/g });
262
365
  return tpl({ ...testResult, bundle: { authData, inputData: testResult } });
@@ -632,10 +735,13 @@ class InvokeCommand extends BaseCommand {
632
735
  field,
633
736
  appDefinition,
634
737
  inputData,
738
+ authId,
635
739
  authData,
636
740
  timezone,
637
741
  zcacheTestObj,
638
742
  cursorTestObj,
743
+ appId,
744
+ deployKey,
639
745
  ) {
640
746
  const message = formatFieldDisplay(field) + ':';
641
747
  if (field.dynamic) {
@@ -655,16 +761,19 @@ class InvokeCommand extends BaseCommand {
655
761
  'triggers',
656
762
  trigger,
657
763
  inputData,
764
+ authId,
658
765
  authData,
659
766
  meta,
660
767
  timezone,
661
768
  zcacheTestObj,
662
769
  cursorTestObj,
770
+ appId,
771
+ deployKey,
663
772
  );
664
773
  return this.promptWithList(
665
774
  message,
666
775
  choices.map((c) => {
667
- const id = c[idField] || 'null';
776
+ const id = c[idField] ?? 'null';
668
777
  const label = getLabelForDynamicDropdown(c, labelField, idField);
669
778
  return {
670
779
  name: `${label} (${id})`,
@@ -685,11 +794,14 @@ class InvokeCommand extends BaseCommand {
685
794
  inputData,
686
795
  inputFields,
687
796
  appDefinition,
797
+ authId,
688
798
  authData,
689
799
  meta,
690
800
  timezone,
691
801
  zcacheTestObj,
692
802
  cursorTestObj,
803
+ appId,
804
+ deployKey,
693
805
  ) {
694
806
  const missingFields = getMissingRequiredInputFields(inputData, inputFields);
695
807
  if (missingFields.length) {
@@ -704,10 +816,13 @@ class InvokeCommand extends BaseCommand {
704
816
  f,
705
817
  appDefinition,
706
818
  inputData,
819
+ authId,
707
820
  authData,
708
821
  timezone,
709
822
  zcacheTestObj,
710
823
  cursorTestObj,
824
+ appId,
825
+ deployKey,
711
826
  );
712
827
  }
713
828
  }
@@ -717,10 +832,13 @@ class InvokeCommand extends BaseCommand {
717
832
  inputData,
718
833
  inputFields,
719
834
  appDefinition,
835
+ authId,
720
836
  authData,
721
837
  timezone,
722
838
  zcacheTestObj,
723
839
  cursorTestObj,
840
+ appId,
841
+ deployKey,
724
842
  ) {
725
843
  inputFields = inputFields.filter((f) => f.key);
726
844
  if (!inputFields.length) {
@@ -768,10 +886,13 @@ class InvokeCommand extends BaseCommand {
768
886
  field,
769
887
  appDefinition,
770
888
  inputData,
889
+ authId,
771
890
  authData,
772
891
  timezone,
773
892
  zcacheTestObj,
774
893
  cursorTestObj,
894
+ appId,
895
+ deployKey,
775
896
  );
776
897
  }
777
898
  }
@@ -780,31 +901,40 @@ class InvokeCommand extends BaseCommand {
780
901
  inputData,
781
902
  inputFields,
782
903
  appDefinition,
904
+ authId,
783
905
  authData,
784
906
  meta,
785
907
  timezone,
786
908
  zcacheTestObj,
787
909
  cursorTestObj,
910
+ appId,
911
+ deployKey,
788
912
  ) {
789
913
  await this.promptOrErrorForRequiredInputFields(
790
914
  inputData,
791
915
  inputFields,
792
916
  appDefinition,
917
+ authId,
793
918
  authData,
794
919
  meta,
795
920
  timezone,
796
921
  zcacheTestObj,
797
922
  cursorTestObj,
923
+ appId,
924
+ deployKey,
798
925
  );
799
926
  if (!this.nonInteractive && !meta.isFillingDynamicDropdown) {
800
927
  await this.promptForInputFieldEdit(
801
928
  inputData,
802
929
  inputFields,
803
930
  appDefinition,
931
+ authId,
804
932
  authData,
805
933
  timezone,
806
934
  zcacheTestObj,
807
935
  cursorTestObj,
936
+ appId,
937
+ deployKey,
808
938
  );
809
939
  }
810
940
  }
@@ -814,11 +944,14 @@ class InvokeCommand extends BaseCommand {
814
944
  actionTypePlural,
815
945
  action,
816
946
  inputData,
947
+ authId,
817
948
  authData,
818
949
  meta,
819
950
  timezone,
820
951
  zcacheTestObj,
821
952
  cursorTestObj,
953
+ appId,
954
+ deployKey,
822
955
  ) {
823
956
  // Do these in order:
824
957
  // 1. Prompt for static input fields that alter dynamic fields
@@ -835,17 +968,20 @@ class InvokeCommand extends BaseCommand {
835
968
  inputData,
836
969
  staticInputFields,
837
970
  appDefinition,
971
+ authId,
838
972
  authData,
839
973
  meta,
840
974
  timezone,
841
975
  zcacheTestObj,
842
976
  cursorTestObj,
977
+ appId,
978
+ deployKey,
843
979
  );
844
980
 
845
981
  let methodName = `${actionTypePlural}.${action.key}.operation.inputFields`;
846
982
  startSpinner(`Invoking ${methodName}`);
847
983
 
848
- const inputFields = await localAppCommand({
984
+ const inputFields = await localAppCommandWithRelayErrorHandler({
849
985
  command: 'execute',
850
986
  method: methodName,
851
987
  bundle: {
@@ -857,6 +993,9 @@ class InvokeCommand extends BaseCommand {
857
993
  cursorTestObj,
858
994
  customLogger,
859
995
  calledFromCliInvoke: true,
996
+ appId,
997
+ deployKey,
998
+ relayAuthenticationId: authId,
860
999
  });
861
1000
  endSpinner();
862
1001
 
@@ -867,11 +1006,14 @@ class InvokeCommand extends BaseCommand {
867
1006
  inputData,
868
1007
  inputFields,
869
1008
  appDefinition,
1009
+ authId,
870
1010
  authData,
871
1011
  meta,
872
1012
  timezone,
873
1013
  zcacheTestObj,
874
1014
  cursorTestObj,
1015
+ appId,
1016
+ deployKey,
875
1017
  );
876
1018
  }
877
1019
 
@@ -879,7 +1021,7 @@ class InvokeCommand extends BaseCommand {
879
1021
  methodName = `${actionTypePlural}.${action.key}.operation.perform`;
880
1022
 
881
1023
  startSpinner(`Invoking ${methodName}`);
882
- const output = await localAppCommand({
1024
+ const output = await localAppCommandWithRelayErrorHandler({
883
1025
  command: 'execute',
884
1026
  method: methodName,
885
1027
  bundle: {
@@ -891,15 +1033,40 @@ class InvokeCommand extends BaseCommand {
891
1033
  cursorTestObj,
892
1034
  customLogger,
893
1035
  calledFromCliInvoke: true,
1036
+ appId,
1037
+ deployKey,
1038
+ relayAuthenticationId: authId,
894
1039
  });
895
1040
  endSpinner();
896
1041
 
897
1042
  return output;
898
1043
  }
899
1044
 
1045
+ async promptForAuthentication() {
1046
+ const auths = (await listAuthentications()).authentications;
1047
+ if (!auths || auths.length === 0) {
1048
+ throw new Error(
1049
+ 'No authentications/connections found for your integration. ' +
1050
+ 'Add a new connection at https://zapier.com/app/assets/connections ' +
1051
+ 'or use local auth data by removing the `--authentication-id` flag.',
1052
+ );
1053
+ }
1054
+ const authChoices = auths.map((auth) => ({
1055
+ name: `${auth.title} | ${auth.app_version} | ID: ${auth.id}`,
1056
+ value: auth.id,
1057
+ }));
1058
+ return this.promptWithList(
1059
+ 'Which authentication/connection would you like to use?',
1060
+ authChoices,
1061
+ { useStderr: true },
1062
+ );
1063
+ }
1064
+
900
1065
  async perform() {
1066
+ let authId = this.flags['authentication-id'];
1067
+
901
1068
  const dotenvResult = dotenv.config({ override: true });
902
- if (_.isEmpty(dotenvResult.parsed)) {
1069
+ if (!authId && _.isEmpty(dotenvResult.parsed)) {
903
1070
  console.warn(
904
1071
  'The .env file does not exist or is empty. ' +
905
1072
  'You may need to set some environment variables in there if your code uses process.env.',
@@ -955,10 +1122,37 @@ class InvokeCommand extends BaseCommand {
955
1122
  }
956
1123
  }
957
1124
 
958
- const authData = loadAuthDataFromEnv();
1125
+ const appId = (await getLinkedAppConfig(null, false))?.id;
1126
+ const deployKey = (await readCredentials(false))[AUTH_KEY];
1127
+
1128
+ if (authId === '-' || authId === '') {
1129
+ if (this.nonInteractive) {
1130
+ throw new Error(
1131
+ "You cannot specify '-' or an empty string for `--authentication-id` in non-interactive mode.",
1132
+ );
1133
+ }
1134
+ authId = (await this.promptForAuthentication()).toString();
1135
+ }
1136
+
959
1137
  const zcacheTestObj = {};
960
1138
  const cursorTestObj = {};
961
1139
 
1140
+ let authData = {};
1141
+ if (authId) {
1142
+ // Fill authData with curlies if we're in relay mode
1143
+ const authFields = appDefinition.authentication.fields || [];
1144
+ for (const field of authFields) {
1145
+ if (field.key) {
1146
+ authData[field.key] = `{{${field.key}}}`;
1147
+ }
1148
+ }
1149
+ }
1150
+
1151
+ // Load from .env as well even in relay mode, in case the integration code
1152
+ // assumes there are values in bundle.authData. Loading from .env at least
1153
+ // gives the developer an option to override the values in bundle.authData.
1154
+ authData = { ...authData, ...loadAuthDataFromEnv() };
1155
+
962
1156
  if (actionType === 'auth') {
963
1157
  const meta = {
964
1158
  isLoadingSample: false,
@@ -970,6 +1164,13 @@ class InvokeCommand extends BaseCommand {
970
1164
  };
971
1165
  switch (actionKey) {
972
1166
  case 'start': {
1167
+ if (authId) {
1168
+ throw new Error(
1169
+ 'The `--authentication-id` flag is not applicable. ' +
1170
+ 'The `auth start` subcommand is to initialize local auth data in the .env file, ' +
1171
+ 'whereas `--authentication-id` is for proxying requests using production auth data.',
1172
+ );
1173
+ }
973
1174
  const newAuthData = await this.startAuth(
974
1175
  appDefinition,
975
1176
  zcacheTestObj,
@@ -984,6 +1185,13 @@ class InvokeCommand extends BaseCommand {
984
1185
  return;
985
1186
  }
986
1187
  case 'refresh': {
1188
+ if (authId) {
1189
+ throw new Error(
1190
+ 'The `--authentication-id` flag is not applicable. ' +
1191
+ 'The `auth refresh` subcommand can only refresh your local auth data in the .env file. ' +
1192
+ 'You might want to run `auth test` instead, which tests and may refresh auth data with the specified authentication ID in production.',
1193
+ );
1194
+ }
987
1195
  const newAuthData = await this.refreshAuth(
988
1196
  appDefinition,
989
1197
  authData,
@@ -999,7 +1207,14 @@ class InvokeCommand extends BaseCommand {
999
1207
  return;
1000
1208
  }
1001
1209
  case 'test': {
1002
- const output = await testAuth(authData, meta, zcacheTestObj);
1210
+ const output = await testAuth(
1211
+ authId,
1212
+ authData,
1213
+ meta,
1214
+ zcacheTestObj,
1215
+ appId,
1216
+ deployKey,
1217
+ );
1003
1218
  console.log(JSON.stringify(output, null, 2));
1004
1219
  return;
1005
1220
  }
@@ -1009,14 +1224,24 @@ class InvokeCommand extends BaseCommand {
1009
1224
  console.warn(
1010
1225
  'Function-based connection label is not supported yet. Printing auth test result instead.',
1011
1226
  );
1012
- const output = await testAuth(authData, meta, zcacheTestObj);
1227
+ const output = await testAuth(
1228
+ authId,
1229
+ authData,
1230
+ meta,
1231
+ zcacheTestObj,
1232
+ appId,
1233
+ deployKey,
1234
+ );
1013
1235
  console.log(JSON.stringify(output, null, 2));
1014
1236
  } else {
1015
1237
  const output = await getAuthLabel(
1016
1238
  labelTemplate,
1239
+ authId,
1017
1240
  authData,
1018
1241
  meta,
1019
1242
  zcacheTestObj,
1243
+ appId,
1244
+ deployKey,
1020
1245
  );
1021
1246
  if (output) {
1022
1247
  console.log(output);
@@ -1085,11 +1310,14 @@ class InvokeCommand extends BaseCommand {
1085
1310
  actionTypePlural,
1086
1311
  action,
1087
1312
  inputData,
1313
+ authId,
1088
1314
  authData,
1089
1315
  meta,
1090
1316
  timezone,
1091
1317
  zcacheTestObj,
1092
1318
  cursorTestObj,
1319
+ appId,
1320
+ deployKey,
1093
1321
  );
1094
1322
  console.log(JSON.stringify(output, null, 2));
1095
1323
  }
@@ -1149,6 +1377,11 @@ InvokeCommand.flags = buildFlags({
1149
1377
  'Only used by `auth start` subcommand. The local port that will be used to start the local HTTP server to listen for the OAuth2 callback. This port can be different from the one in the redirect URI if you have port forwarding set up.',
1150
1378
  default: 9000,
1151
1379
  }),
1380
+ 'authentication-id': Flags.string({
1381
+ char: 'a',
1382
+ description:
1383
+ 'EXPERIMENTAL: Instead of using the local .env file, use the production authentication data with the given authentication ID (aka the "app connection" on Zapier). Find them at https://zapier.com/app/assets/connections (https://zpr.io/z8SjFTdnTFZ2 for instructions) or specify \'-\' to interactively select one from your available authentications. When specified, the code will still run locally, but all outgoing requests will be proxied through Zapier with the production auth data.',
1384
+ }),
1152
1385
  },
1153
1386
  });
1154
1387
 
@@ -1171,13 +1404,27 @@ InvokeCommand.examples = [
1171
1404
  'zapier invoke auth label',
1172
1405
  'zapier invoke trigger new_recipe',
1173
1406
  `zapier invoke create add_recipe --inputData '{"title": "Pancakes"}'`,
1174
- 'zapier invoke search find_recipe -i @file.json',
1407
+ 'zapier invoke search find_recipe -i @file.json --non-interactive',
1175
1408
  'cat file.json | zapier invoke trigger new_recipe -i @-',
1409
+ 'zapier invoke search find_ticket --authentication-id 12345',
1410
+ 'zapier invoke create add_ticket -a -',
1176
1411
  ];
1177
1412
  InvokeCommand.description = `Invoke an auth operation, a trigger, or a create/search action locally.
1178
1413
 
1179
1414
  This command emulates how Zapier production environment would invoke your integration. It runs code locally, so you can use this command to quickly test your integration without deploying it to Zapier. This is especially useful for debugging and development.
1180
1415
 
1416
+ Why use this command?
1417
+
1418
+ * Fast feedback loop: Write code and run this command to verify if it works immediately
1419
+ * Step-by-step debugging: Running locally means you can use a debugger to step through your code
1420
+ * Untruncated logs: View complete logs and errors in your terminal
1421
+
1422
+ ### Authentication
1423
+
1424
+ You can supply the authentcation data in two ways: Load from the local \`.env\` file or use the (experimental) \`--authentication-id\` flag.
1425
+
1426
+ #### The local \`.env\` file
1427
+
1181
1428
  This command loads environment variables and \`authData\` from the \`.env\` file in the current directory. If you don't have a \`.env\` file yet, you can use the \`zapier invoke auth start\` command to help you initialize it, or you can manually create it.
1182
1429
 
1183
1430
  The \`zapier invoke auth start\` subcommand will prompt you for the necessary auth fields and save them to the \`.env\` file. For OAuth2, it will start a local HTTP server, open the authorization URL in the browser, wait for the OAuth2 redirect, and get the access token.
@@ -1197,6 +1444,19 @@ authData_refresh_token='abcdefg'
1197
1444
  authData_account_name='zapier'
1198
1445
  \`\`\`
1199
1446
 
1447
+
1448
+ #### The \`--authentication-id\` flag (EXPERIMENTAL)
1449
+
1450
+ Setting up local auth data can be troublesome. You'd have to configure your app server to allow localhost redirect URIs or use a port forwarding tool. This is sometimes not easy to get right.
1451
+
1452
+ The \`--authentication-id\` flag (\`-a\` for short) gives you an alternative (and perhaps easier) way to supply your auth data. You can use \`-a\` to specify an existing production authentication/connection. The available authentications can be found at https://zapier.com/app/assets/connections. Check https://zpr.io/z8SjFTdnTFZ2 for more instructions.
1453
+
1454
+ When \`-a -\` is specified, such as \`zapier invoke auth test -a -\`, the command will interactively prompt you to select one of your available authentications.
1455
+
1456
+ If you know your authentication ID, you can specify it directly, such as \`zapier invoke auth test -a 123456\`.
1457
+
1458
+ #### Testing authentication
1459
+
1200
1460
  To test if the auth data is correct, run either one of these:
1201
1461
 
1202
1462
  \`\`\`
@@ -1204,7 +1464,9 @@ zapier invoke auth test # invokes authentication.test method
1204
1464
  zapier invoke auth label # invokes authentication.test and renders connection label
1205
1465
  \`\`\`
1206
1466
 
1207
- To refresh stale auth data for OAuth2 or session auth, run \`zapier invoke auth refresh\`.
1467
+ To refresh stale auth data for OAuth2 or session auth, run \`zapier invoke auth refresh\`. Note that refreshing is only applicable for local auth data in the \`.env\` file.
1468
+
1469
+ ### Invoking a trigger or an action
1208
1470
 
1209
1471
  Once you have the correct auth data, you can test an trigger, a search, or a create action. For example, here's how you invoke a trigger with the key \`new_recipe\`:
1210
1472
 
@@ -1212,12 +1474,14 @@ Once you have the correct auth data, you can test an trigger, a search, or a cre
1212
1474
  zapier invoke trigger new_recipe
1213
1475
  \`\`\`
1214
1476
 
1215
- To add input data, use the \`--inputData\` flag. The input data can come from the command directly, a file, or stdin. See **EXAMPLES** below.
1477
+ To add input data, use the \`--inputData\` flag (\`-i\` for short). The input data can come from the command directly, a file, or stdin. See **EXAMPLES** below.
1216
1478
 
1217
1479
  When you miss any command arguments, such as ACTIONTYPE or ACTIONKEY, the command will prompt you interactively. If you don't want to get interactive prompts, use the \`--non-interactive\` flag.
1218
1480
 
1219
1481
  The \`--debug\` flag will show you the HTTP request logs and any console logs you have in your code.
1220
1482
 
1483
+ ### Limitations
1484
+
1221
1485
  The following is a non-exhaustive list of current limitations and may be supported in the future:
1222
1486
 
1223
1487
  - Hook triggers, including REST hook subscribe/unsubscribe
package/src/utils/api.js CHANGED
@@ -293,6 +293,11 @@ const getVersionInfo = () => {
293
293
  });
294
294
  };
295
295
 
296
+ const getSpecificVersionInfo = async (version) => {
297
+ const app = await getWritableApp();
298
+ return callAPI(`/apps/${app.id}/versions/${version}`);
299
+ };
300
+
296
301
  // Intended to match logic of https://gitlab.com/zapier/team-developer-platform/dev-platform/-/blob/9fa28d8bacd04ebdad5937bd039c71aede4ede47/web/frontend/assets/app/entities/CliApp/CliApp.ts#L96
297
302
  const isPublished = (appStatus) => {
298
303
  const publishedStatuses = ['public', 'beta'];
@@ -363,6 +368,8 @@ const listEnv = (version) =>
363
368
 
364
369
  const listMigrations = () => listEndpoint('migrations');
365
370
 
371
+ const listAuthentications = () => listEndpoint('authentications');
372
+
366
373
  // the goal of this is to call `/check` with as much info as possible
367
374
  // if the app is registered and auth is available, then we can send app id
368
375
  // otherwise, we should just send the definition and get back checks about that
@@ -500,8 +507,10 @@ module.exports = {
500
507
  getLinkedAppConfig,
501
508
  getWritableApp,
502
509
  getVersionInfo,
510
+ getSpecificVersionInfo,
503
511
  isPublished,
504
512
  listApps,
513
+ listAuthentications,
505
514
  listCanaries,
506
515
  listEndpoint,
507
516
  listEndpointMulti,
@@ -2,8 +2,231 @@ const _ = require('lodash');
2
2
  const path = require('path');
3
3
 
4
4
  const { findCorePackageDir } = require('./misc');
5
+ const { BASE_ENDPOINT } = require('../constants');
5
6
 
6
- const getLocalAppHandler = ({ reload = false, baseEvent = {} } = {}) => {
7
+ /**
8
+ * Wraps Node's http.request() / https.request() so that all requests go via a relay URL.
9
+ * It decides whether to use the http or https module based on the relay URL's protocol.
10
+ *
11
+ * @param {Function} originalHttpRequest - The original http.request function.
12
+ * @param {Function} originalHttpsRequest - The original https.request function.
13
+ * @param {string} relayUrl - The base URL to which we relay. (e.g., 'http://my-relay.test')
14
+ * @param {Object} relayHeaders - Extra headers to add to each request sent to the relay.
15
+ * @returns {Function} A function with the same signature as http(s).request that relays instead.
16
+ *
17
+ * Usage:
18
+ * const http = require('http');
19
+ * const https = require('https');
20
+ *
21
+ * // Replace https.request with our wrapped version:
22
+ * https.request = wrapHttpRequestFuncWithRelay(
23
+ * http.request,
24
+ * https.request,
25
+ * 'https://my-relay.test',
26
+ * { 'X-Relayed-By': 'MyRelayProxy' }
27
+ * );
28
+ *
29
+ * // Now, calling https.request('https://example.com/hello') will actually
30
+ * // send a request to "https://my-relay.test/example.com/hello"
31
+ * // with X-Relayed-By header attached.
32
+ */
33
+ function wrapHttpRequestFuncWithRelay(
34
+ originalHttpRequest,
35
+ originalHttpsRequest,
36
+ relayUrl,
37
+ relayHeaders,
38
+ ) {
39
+ const parsedRelayUrl = new URL(relayUrl);
40
+
41
+ // Decide if the relay itself is HTTP or HTTPS
42
+ const isRelayHttps = parsedRelayUrl.protocol === 'https:';
43
+
44
+ // Pick which request function to use to talk to the relay
45
+ const relayRequestFunc = isRelayHttps
46
+ ? originalHttpsRequest
47
+ : originalHttpRequest;
48
+
49
+ /**
50
+ * The actual wrapped request function.
51
+ * Accepts the same arguments as http(s).request:
52
+ * (options[, callback]) or (url[, options][, callback])
53
+ */
54
+ return function wrappedRequest(originalOptions, originalCallback) {
55
+ let options;
56
+ let callback;
57
+
58
+ // 1. Normalize arguments (string URL vs. options object)
59
+ if (typeof originalOptions === 'string') {
60
+ // Called like request(urlString, [...])
61
+ try {
62
+ const parsedOriginalUrl = new URL(originalOptions);
63
+
64
+ if (typeof originalCallback === 'object') {
65
+ // request(urlString, optionsObject, callback)
66
+ options = { ...originalCallback };
67
+ callback = arguments[2];
68
+ } else {
69
+ // request(urlString, callback) or request(urlString)
70
+ options = {};
71
+ callback = originalCallback;
72
+ }
73
+
74
+ // Merge in the URL parts if not explicitly set in options
75
+ options.protocol = options.protocol || parsedOriginalUrl.protocol;
76
+ options.hostname = options.hostname || parsedOriginalUrl.hostname;
77
+ options.port = options.port || parsedOriginalUrl.port;
78
+ options.path =
79
+ options.path || parsedOriginalUrl.pathname + parsedOriginalUrl.search;
80
+ } catch (err) {
81
+ // If it's not a valid absolute URL, treat it as a path
82
+ // or re-throw if you prefer strictness.
83
+ options = {};
84
+ callback = originalCallback;
85
+ options.path = originalOptions;
86
+ }
87
+ } else {
88
+ // Called like request(optionsObject, [callback])
89
+ options = { ...originalOptions };
90
+ callback = originalCallback;
91
+ }
92
+
93
+ // 2. Default method and headers
94
+ if (!options.method) {
95
+ options.method = 'GET';
96
+ }
97
+ if (!options.headers) {
98
+ options.headers = {};
99
+ }
100
+
101
+ // 3. Decide whether to relay or not
102
+ // If the request is being sent to the same host:port as our relay,
103
+ // we do NOT want to relay again. We just call the original request.
104
+ const targetHost = (options.hostname || options.host)
105
+ .replaceAll('lcurly-', '{{')
106
+ .replaceAll('-rcurly', '}}');
107
+ const targetPort = options.port ? String(options.port) : '';
108
+ const relayHost = parsedRelayUrl.hostname;
109
+ const relayPort = parsedRelayUrl.port ? String(parsedRelayUrl.port) : '';
110
+
111
+ const isAlreadyRelay =
112
+ targetHost === relayHost &&
113
+ // If no port was specified, assume default port comparison as needed
114
+ (targetPort === relayPort || (!targetPort && !relayPort));
115
+
116
+ if (isAlreadyRelay) {
117
+ // Just call the original function; do *not* re-relay
118
+ const originalFn =
119
+ options.protocol === 'https:'
120
+ ? originalHttpsRequest
121
+ : originalHttpRequest;
122
+ return originalFn(options, callback);
123
+ }
124
+
125
+ // 4. Otherwise, build the path we want to relay to
126
+ let finalHost = targetHost;
127
+ if (targetPort && targetPort !== '443') {
128
+ finalHost += `:${targetPort}`;
129
+ }
130
+ const combinedPath = `${parsedRelayUrl.pathname}/${finalHost}${options.path}`;
131
+
132
+ // 5. Build final options for the relay request
133
+ const relayedOptions = {
134
+ protocol: parsedRelayUrl.protocol,
135
+ hostname: relayHost,
136
+ port: relayPort,
137
+ path: combinedPath,
138
+ method: options.method,
139
+ headers: {
140
+ ...options.headers,
141
+ ...relayHeaders,
142
+ },
143
+ };
144
+
145
+ // 6. Make the relay request
146
+ const relayReq = relayRequestFunc(relayedOptions, callback);
147
+ return relayReq;
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Wraps a fetch function so that all requests get relayed to a specified relay URL.
153
+ * The final relay URL includes: relayUrl + "/" + originalHost + originalPath
154
+ *
155
+ * @param {Function} fetchFunc - The original fetch function (e.g., global.fetch).
156
+ * @param {string} relayUrl - The base URL to which we relay. (e.g. 'https://my-relay.test')
157
+ * @param {Object} relayHeaders - Extra headers to add to each request sent to the relay.
158
+ * @returns {Function} A function with the same signature as `fetch(url, options)`.
159
+ *
160
+ * Usage:
161
+ * const wrappedFetch = wrapFetchWithRelay(
162
+ * fetch,
163
+ * 'https://my-relay.test',
164
+ * { 'X-Relayed-By': 'MyRelayProxy' },
165
+ * );
166
+ *
167
+ * // Now when you do:
168
+ * // wrappedFetch('https://example.com/api/user?id=123', { method: 'POST' });
169
+ * // it actually sends a request to:
170
+ * // https://my-relay.test/example.com/api/user?id=123
171
+ * // with "X-Relayed-By" header included.
172
+ */
173
+ function wrapFetchWithRelay(fetchFunc, relayUrl, relayHeaders) {
174
+ const parsedRelayUrl = new URL(relayUrl);
175
+
176
+ return async function wrappedFetch(originalUrl, originalOptions = {}) {
177
+ // Attempt to parse the originalUrl as an absolute URL
178
+ const parsedOriginalUrl = new URL(originalUrl);
179
+
180
+ // Build the portion that includes the original host (and port if present)
181
+ let host = parsedOriginalUrl.hostname;
182
+ // If there's a port that isn't 443 (for HTTPS) or 80 (for HTTP), append it
183
+ // (Adjust to your preferences; here we loosely check for 443 only.)
184
+ if (parsedOriginalUrl.port && parsedOriginalUrl.port.toString() !== '443') {
185
+ host += `:${parsedOriginalUrl.port}`;
186
+ }
187
+
188
+ const isAlreadyRelay =
189
+ parsedOriginalUrl.hostname === parsedRelayUrl.hostname &&
190
+ parsedOriginalUrl.port === parsedRelayUrl.port;
191
+ if (isAlreadyRelay) {
192
+ // Just call the original fetch function; do *not* re-relay
193
+ return fetchFunc(originalUrl, originalOptions);
194
+ }
195
+
196
+ // Combine the relay's pathname with the "host + path" from the original
197
+ // For example: relayUrl = http://my-relay.test
198
+ // => parsedRelayUrl.pathname might be '/'
199
+ // => combinedPath = '/example.com:8080/some/path'
200
+ const combinedPath = `${parsedRelayUrl.pathname}/${host}${parsedOriginalUrl.pathname}`;
201
+
202
+ // Merge in the search strings: the relay's own search (if any) plus the original URL's search
203
+ const finalUrl = `${parsedRelayUrl.origin}${combinedPath}${parsedRelayUrl.search}${parsedOriginalUrl.search}`;
204
+
205
+ // Merge the user's headers with the relayHeaders
206
+ const mergedHeaders = {
207
+ ...(originalOptions.headers || {}),
208
+ ...relayHeaders,
209
+ };
210
+
211
+ const finalOptions = {
212
+ ...originalOptions,
213
+ headers: mergedHeaders,
214
+ };
215
+
216
+ // Call the real fetch with our new URL and merged options
217
+ return fetchFunc(finalUrl, finalOptions);
218
+ };
219
+ }
220
+
221
+ const getLocalAppHandler = ({
222
+ reload = false,
223
+ baseEvent = {},
224
+ appId = null,
225
+ deployKey = null,
226
+ relayAuthenticationId = null,
227
+ beforeRequest = null,
228
+ afterResponse = null,
229
+ } = {}) => {
7
230
  const entryPath = `${process.cwd()}/index`;
8
231
  const rootPath = path.dirname(require.resolve(entryPath));
9
232
  const corePackageDir = findCorePackageDir();
@@ -24,6 +247,41 @@ const getLocalAppHandler = ({ reload = false, baseEvent = {} } = {}) => {
24
247
  // maybe we could do require('syntax-error') in the future
25
248
  return (event, ctx, callback) => callback(err);
26
249
  }
250
+
251
+ if (beforeRequest) {
252
+ appRaw.beforeRequest = [...(appRaw.beforeRequest || []), ...beforeRequest];
253
+ }
254
+ if (afterResponse) {
255
+ appRaw.afterResponse = [...afterResponse, ...(appRaw.afterResponse || [])];
256
+ }
257
+
258
+ if (appId && deployKey && relayAuthenticationId) {
259
+ const relayUrl = `${BASE_ENDPOINT}/api/platform/cli/apps/${appId}/relay`;
260
+ const relayHeaders = {
261
+ 'x-relay-authentication-id': relayAuthenticationId,
262
+ 'x-deploy-key': deployKey,
263
+ };
264
+
265
+ const http = require('http');
266
+ const https = require('https');
267
+ const origHttpRequest = http.request;
268
+ const origHttpsRequest = https.request;
269
+ http.request = wrapHttpRequestFuncWithRelay(
270
+ origHttpRequest,
271
+ origHttpsRequest,
272
+ relayUrl,
273
+ relayHeaders,
274
+ );
275
+ https.request = wrapHttpRequestFuncWithRelay(
276
+ origHttpRequest,
277
+ origHttpsRequest,
278
+ relayUrl,
279
+ relayHeaders,
280
+ );
281
+
282
+ global.fetch = wrapFetchWithRelay(global.fetch, relayUrl, relayHeaders);
283
+ }
284
+
27
285
  const handler = zapier.createAppHandler(appRaw);
28
286
  return (event, ctx, callback) => {
29
287
  event = _.merge(
@@ -40,7 +298,13 @@ const getLocalAppHandler = ({ reload = false, baseEvent = {} } = {}) => {
40
298
 
41
299
  // Runs a local app command (./index.js) like {command: 'validate'};
42
300
  const localAppCommand = (event) => {
43
- const handler = getLocalAppHandler();
301
+ const handler = getLocalAppHandler({
302
+ appId: event.appId,
303
+ deployKey: event.deployKey,
304
+ relayAuthenticationId: event.relayAuthenticationId,
305
+ beforeRequest: event.beforeRequest,
306
+ afterResponse: event.afterResponse,
307
+ });
44
308
  return new Promise((resolve, reject) => {
45
309
  handler(event, {}, (err, resp) => {
46
310
  if (err) {