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.
- package/oclif.manifest.json +22 -5
- package/package.json +1 -1
- package/src/oclif/ZapierBaseCommand.js +1 -1
- package/src/oclif/commands/deprecate.js +42 -8
- package/src/oclif/commands/invoke.js +278 -14
- package/src/utils/api.js +9 -0
- package/src/utils/local.js +266 -2
package/oclif.manifest.json
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
2361
|
+
"version": "16.4.0"
|
|
2345
2362
|
}
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
259
|
-
|
|
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]
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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,
|
package/src/utils/local.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|