zapier-platform-cli 18.0.5 → 18.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -3
- package/src/oclif/commands/build.js +7 -6
- package/src/oclif/commands/invoke/action.js +91 -0
- package/src/oclif/commands/invoke/auth/index.js +6 -0
- package/src/oclif/commands/invoke/auth/label.js +22 -0
- package/src/oclif/commands/invoke/auth/refresh.js +87 -0
- package/src/oclif/commands/invoke/auth/start.js +290 -0
- package/src/oclif/commands/invoke/auth/test.js +34 -0
- package/src/oclif/commands/invoke/env.js +62 -0
- package/src/oclif/commands/invoke/index.js +481 -0
- package/src/oclif/commands/invoke/input-types.js +240 -0
- package/src/oclif/commands/invoke/logger.js +13 -0
- package/src/oclif/commands/invoke/prompts.js +288 -0
- package/src/oclif/commands/invoke/relay.js +97 -0
- package/src/oclif/commands/push.js +4 -4
- package/src/utils/build.js +18 -8
- package/src/utils/misc.js +1 -1
- package/src/utils/package-manager.js +31 -14
- package/oclif.manifest.json +0 -2559
- package/src/oclif/commands/invoke.js +0 -1526
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zapier-platform-cli",
|
|
3
|
-
"version": "18.0.
|
|
3
|
+
"version": "18.0.7",
|
|
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/",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"semver": "7.7.2",
|
|
54
54
|
"string-length": "4.0.2",
|
|
55
55
|
"through2": "4.0.2",
|
|
56
|
-
"tmp": "0.2.
|
|
56
|
+
"tmp": "0.2.5",
|
|
57
57
|
"traverse": "0.6.11",
|
|
58
58
|
"update-notifier": "7.3.1",
|
|
59
59
|
"yeoman-environment": "4.4.3",
|
|
@@ -76,7 +76,20 @@
|
|
|
76
76
|
"zapier-platform": "./src/bin/run"
|
|
77
77
|
},
|
|
78
78
|
"oclif": {
|
|
79
|
-
"commands":
|
|
79
|
+
"commands": {
|
|
80
|
+
"strategy": "pattern",
|
|
81
|
+
"target": "./src/oclif/commands",
|
|
82
|
+
"globPatterns": [
|
|
83
|
+
"**/*.js",
|
|
84
|
+
"!invoke/action.js",
|
|
85
|
+
"!invoke/env.js",
|
|
86
|
+
"!invoke/input-types.js",
|
|
87
|
+
"!invoke/logger.js",
|
|
88
|
+
"!invoke/prompts.js",
|
|
89
|
+
"!invoke/relay.js",
|
|
90
|
+
"!invoke/auth/**"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
80
93
|
"additionalHelpFlags": [
|
|
81
94
|
"-h"
|
|
82
95
|
],
|
|
@@ -12,11 +12,11 @@ const colors = require('colors/safe');
|
|
|
12
12
|
|
|
13
13
|
class BuildCommand extends BaseCommand {
|
|
14
14
|
async perform() {
|
|
15
|
-
const
|
|
15
|
+
const skipDepInstall = this.flags['skip-dep-install'];
|
|
16
16
|
await buildAndOrUpload(
|
|
17
17
|
{ build: true },
|
|
18
18
|
{
|
|
19
|
-
|
|
19
|
+
skipDepInstall,
|
|
20
20
|
disableDependencyDetection: this.flags['disable-dependency-detection'],
|
|
21
21
|
skipValidation: this.flags['skip-validation'],
|
|
22
22
|
},
|
|
@@ -27,9 +27,9 @@ class BuildCommand extends BaseCommand {
|
|
|
27
27
|
`Now you can upload them with the ${colors.bold.underline('zapier upload')} command.`,
|
|
28
28
|
);
|
|
29
29
|
|
|
30
|
-
if (!
|
|
30
|
+
if (!skipDepInstall) {
|
|
31
31
|
this.log(
|
|
32
|
-
`\nTip: Try ${colors.bold.underline('zapier build --skip-
|
|
32
|
+
`\nTip: Try ${colors.bold.underline('zapier build --skip-dep-install')} for faster builds.`,
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -40,9 +40,10 @@ BuildCommand.flags = buildFlags({
|
|
|
40
40
|
'disable-dependency-detection': Flags.boolean({
|
|
41
41
|
description: `Disable "smart" file inclusion. By default, Zapier only includes files that are required by your entry point (\`index.js\` by default). If you (or your dependencies) require files dynamically (such as with \`require(someVar)\`), then you may see "Cannot find module" errors. Disabling this may make your \`build.zip\` too large. If that's the case, try using the \`includeInBuild\` option in your \`${CURRENT_APP_FILE}\`. See the docs about \`includeInBuild\` for more info.`,
|
|
42
42
|
}),
|
|
43
|
-
'skip-
|
|
43
|
+
'skip-dep-install': Flags.boolean({
|
|
44
|
+
aliases: ['skip-npm-install'],
|
|
44
45
|
description:
|
|
45
|
-
'
|
|
46
|
+
'[alias: --skip-npm-install]\nSkips installing a fresh copy of dependencies for shorter build time. Helpful for using yarn, pnpm, or local copies of dependencies.',
|
|
46
47
|
}),
|
|
47
48
|
'skip-validation': Flags.boolean({
|
|
48
49
|
description:
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const debug = require('debug')('zapier:invoke');
|
|
2
|
+
|
|
3
|
+
const { startSpinner, endSpinner } = require('../../../utils/display');
|
|
4
|
+
const { customLogger } = require('./logger');
|
|
5
|
+
const { localAppCommandWithRelayErrorHandler } = require('./relay');
|
|
6
|
+
const { promptForFields } = require('./prompts');
|
|
7
|
+
const resolveInputDataTypes = require('./input-types');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Invokes a trigger, create, or search action locally.
|
|
11
|
+
* Handles the full flow: prompting for input fields, resolving types, and executing the perform method.
|
|
12
|
+
* @param {import('../../ZapierBaseCommand')} command - The command instance for prompting
|
|
13
|
+
* @param {Object} context - The execution context containing app definition, auth data, input data, etc.
|
|
14
|
+
* @returns {Promise<*>} The action output
|
|
15
|
+
*/
|
|
16
|
+
const invokeAction = async (command, context) => {
|
|
17
|
+
// Do these in order:
|
|
18
|
+
// 1. Prompt for static input fields that alter dynamic fields
|
|
19
|
+
// 2. {actionTypePlural}.{actionKey}.operation.inputFields
|
|
20
|
+
// 3. Prompt for input fields again
|
|
21
|
+
// 4. {actionTypePlural}.{actionKey}.operation.perform
|
|
22
|
+
const action =
|
|
23
|
+
context.appDefinition[context.actionTypePlural][context.actionKey];
|
|
24
|
+
const staticInputFields = (action.operation.inputFields || []).filter(
|
|
25
|
+
(f) => f.key,
|
|
26
|
+
);
|
|
27
|
+
debug('staticInputFields:', staticInputFields);
|
|
28
|
+
|
|
29
|
+
await promptForFields(command, context, staticInputFields, invokeAction);
|
|
30
|
+
|
|
31
|
+
let methodName = `${context.actionTypePlural}.${action.key}.operation.inputFields`;
|
|
32
|
+
startSpinner(`Invoking ${methodName}`);
|
|
33
|
+
|
|
34
|
+
const inputFields = await localAppCommandWithRelayErrorHandler({
|
|
35
|
+
command: 'execute',
|
|
36
|
+
method: methodName,
|
|
37
|
+
bundle: {
|
|
38
|
+
inputData: context.inputData,
|
|
39
|
+
inputDataRaw: context.inputData, // At this point, inputData hasn't been transformed yet
|
|
40
|
+
authData: context.authData,
|
|
41
|
+
meta: context.meta,
|
|
42
|
+
},
|
|
43
|
+
zcacheTestObj: context.zcacheTestObj,
|
|
44
|
+
cursorTestObj: context.cursorTestObj,
|
|
45
|
+
customLogger,
|
|
46
|
+
calledFromCliInvoke: true,
|
|
47
|
+
appId: context.appId,
|
|
48
|
+
deployKey: context.deployKey,
|
|
49
|
+
relayAuthenticationId: context.authId,
|
|
50
|
+
});
|
|
51
|
+
endSpinner();
|
|
52
|
+
|
|
53
|
+
debug('inputFields:', inputFields);
|
|
54
|
+
|
|
55
|
+
if (inputFields.length !== staticInputFields.length) {
|
|
56
|
+
await promptForFields(command, context, inputFields, invokeAction);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Preserve original inputData as inputDataRaw before type resolution
|
|
60
|
+
const inputDataRaw = { ...context.inputData };
|
|
61
|
+
const inputData = resolveInputDataTypes(
|
|
62
|
+
context.inputData,
|
|
63
|
+
inputFields,
|
|
64
|
+
context.timezone,
|
|
65
|
+
);
|
|
66
|
+
methodName = `${context.actionTypePlural}.${action.key}.operation.perform`;
|
|
67
|
+
|
|
68
|
+
startSpinner(`Invoking ${methodName}`);
|
|
69
|
+
const output = await localAppCommandWithRelayErrorHandler({
|
|
70
|
+
command: 'execute',
|
|
71
|
+
method: methodName,
|
|
72
|
+
bundle: {
|
|
73
|
+
inputData,
|
|
74
|
+
inputDataRaw,
|
|
75
|
+
authData: context.authData,
|
|
76
|
+
meta: context.meta,
|
|
77
|
+
},
|
|
78
|
+
zcacheTestObj: context.zcacheTestObj,
|
|
79
|
+
cursorTestObj: context.cursorTestObj,
|
|
80
|
+
customLogger,
|
|
81
|
+
calledFromCliInvoke: true,
|
|
82
|
+
appId: context.appId,
|
|
83
|
+
deployKey: context.deployKey,
|
|
84
|
+
relayAuthenticationId: context.authId,
|
|
85
|
+
});
|
|
86
|
+
endSpinner();
|
|
87
|
+
|
|
88
|
+
return output;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
module.exports = { invokeAction };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
|
|
3
|
+
const { testAuth } = require('./test');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets the connection label by running authentication.test and rendering the label template.
|
|
7
|
+
* @param {Object} context - The execution context
|
|
8
|
+
* @returns {Promise<string>} The rendered connection label
|
|
9
|
+
*/
|
|
10
|
+
const getAuthLabel = async (context) => {
|
|
11
|
+
const testResult = await testAuth(context);
|
|
12
|
+
const labelTemplate = (
|
|
13
|
+
context.appDefinition.authentication.connectionLabel ?? ''
|
|
14
|
+
).replaceAll('__', '.');
|
|
15
|
+
const tpl = _.template(labelTemplate, { interpolate: /{{([\s\S]+?)}}/g });
|
|
16
|
+
return tpl({
|
|
17
|
+
...testResult,
|
|
18
|
+
bundle: { authData: context.authData, inputData: testResult },
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
module.exports = { getAuthLabel };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
|
|
3
|
+
const { localAppCommand } = require('../../../../utils/local');
|
|
4
|
+
const { startSpinner, endSpinner } = require('../../../../utils/display');
|
|
5
|
+
const { customLogger } = require('../logger');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Refreshes OAuth2 access token using the refresh token.
|
|
9
|
+
* @param {Object} context - The execution context with current authData
|
|
10
|
+
* @returns {Promise<Object>} New auth data with refreshed tokens
|
|
11
|
+
*/
|
|
12
|
+
const refreshOAuth2 = async (context) => {
|
|
13
|
+
startSpinner('Invoking authentication.oauth2Config.refreshAccessToken');
|
|
14
|
+
|
|
15
|
+
const newAuthData = await localAppCommand({
|
|
16
|
+
command: 'execute',
|
|
17
|
+
method: 'authentication.oauth2Config.refreshAccessToken',
|
|
18
|
+
bundle: {
|
|
19
|
+
authData: context.authData,
|
|
20
|
+
},
|
|
21
|
+
zcacheTestObj: context.zcacheTestObj,
|
|
22
|
+
customLogger,
|
|
23
|
+
calledFromCliInvoke: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
endSpinner();
|
|
27
|
+
return newAuthData;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Refreshes session authentication by calling the session config perform method.
|
|
32
|
+
* @param {Object} context - The execution context with current authData
|
|
33
|
+
* @returns {Promise<Object>} New session data
|
|
34
|
+
*/
|
|
35
|
+
const refreshSessionAuth = async (context) => {
|
|
36
|
+
startSpinner('Invoking authentication.sessionConfig.perform');
|
|
37
|
+
|
|
38
|
+
const sessionData = await localAppCommand({
|
|
39
|
+
command: 'execute',
|
|
40
|
+
method: 'authentication.sessionConfig.perform',
|
|
41
|
+
bundle: {
|
|
42
|
+
authData: context.authData,
|
|
43
|
+
},
|
|
44
|
+
zcacheTestObj: context.zcacheTestObj,
|
|
45
|
+
customLogger,
|
|
46
|
+
calledFromCliInvoke: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
endSpinner();
|
|
50
|
+
return sessionData;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Main entry point for refreshing authentication.
|
|
55
|
+
* Routes to the appropriate refresh handler based on authentication type.
|
|
56
|
+
* @param {Object} context - The execution context
|
|
57
|
+
* @returns {Promise<Object|null>} New auth data or null if no authentication needed
|
|
58
|
+
* @throws {Error} If auth type doesn't support refresh or no auth data exists
|
|
59
|
+
*/
|
|
60
|
+
const refreshAuth = async (context) => {
|
|
61
|
+
const authentication = context.appDefinition.authentication;
|
|
62
|
+
if (!authentication) {
|
|
63
|
+
console.warn(
|
|
64
|
+
"Your integration doesn't seem to need authentication. " +
|
|
65
|
+
"If that isn't true, the app definition should have " +
|
|
66
|
+
'an `authentication` object at the root level.',
|
|
67
|
+
);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (_.isEmpty(context.authData)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'No auth data found in the .env file. Run `zapier invoke auth start` first to initialize the auth data.',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
switch (authentication.type) {
|
|
76
|
+
case 'oauth2':
|
|
77
|
+
return refreshOAuth2(context);
|
|
78
|
+
case 'session':
|
|
79
|
+
return refreshSessionAuth(context);
|
|
80
|
+
default:
|
|
81
|
+
throw new Error(
|
|
82
|
+
`This command doesn't support refreshing authentication type "${authentication.type}".`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
module.exports = { refreshAuth };
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const http = require('node:http');
|
|
3
|
+
|
|
4
|
+
const _ = require('lodash');
|
|
5
|
+
const debug = require('debug')('zapier:invoke');
|
|
6
|
+
|
|
7
|
+
const { localAppCommand } = require('../../../../utils/local');
|
|
8
|
+
const { startSpinner, endSpinner } = require('../../../../utils/display');
|
|
9
|
+
const { appendEnv } = require('../env');
|
|
10
|
+
const { customLogger } = require('../logger');
|
|
11
|
+
const { formatFieldDisplay } = require('../prompts');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Prompts the user for authentication field values.
|
|
15
|
+
* Handles password fields with hidden input.
|
|
16
|
+
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
|
17
|
+
* @param {Array<Object>} authFields - Array of auth field definitions
|
|
18
|
+
* @returns {Promise<Object>} Object containing field keys and user-provided values
|
|
19
|
+
*/
|
|
20
|
+
const promptForAuthFields = async (command, authFields) => {
|
|
21
|
+
const authData = {};
|
|
22
|
+
for (const field of authFields) {
|
|
23
|
+
if (field.computed) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const message = formatFieldDisplay(field) + ':';
|
|
27
|
+
let value;
|
|
28
|
+
if (field.type === 'password') {
|
|
29
|
+
value = await command.promptHidden(message, true);
|
|
30
|
+
} else {
|
|
31
|
+
value = await command.prompt(message, { useStderr: true });
|
|
32
|
+
}
|
|
33
|
+
authData[field.key] = value;
|
|
34
|
+
}
|
|
35
|
+
return authData;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initializes basic authentication by prompting for username and password.
|
|
40
|
+
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
|
41
|
+
* @param {Object} context - The execution context
|
|
42
|
+
* @returns {Promise<Object>} Auth data with username and password
|
|
43
|
+
* @throws {Error} If in non-interactive mode
|
|
44
|
+
*/
|
|
45
|
+
const startBasicAuth = async (command, context) => {
|
|
46
|
+
if (context.nonInteractive) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'The `auth start` subcommand for "basic" authentication type only works in interactive mode.',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return promptForAuthFields(command, [
|
|
52
|
+
{
|
|
53
|
+
key: 'username',
|
|
54
|
+
label: 'Username',
|
|
55
|
+
required: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: 'password',
|
|
59
|
+
label: 'Password',
|
|
60
|
+
required: true,
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Initializes custom authentication by prompting for configured auth fields.
|
|
67
|
+
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
|
68
|
+
* @param {Object} context - The execution context
|
|
69
|
+
* @returns {Promise<Object>} Auth data with field values
|
|
70
|
+
* @throws {Error} If in non-interactive mode
|
|
71
|
+
*/
|
|
72
|
+
const startCustomAuth = async (command, context) => {
|
|
73
|
+
if (context.nonInteractive) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'The `auth start` subcommand for "custom" authentication type only works in interactive mode.',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return promptForAuthFields(
|
|
79
|
+
command,
|
|
80
|
+
context.appDefinition.authentication.fields,
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Initializes OAuth2 authentication.
|
|
86
|
+
* Prompts for CLIENT_ID/SECRET if needed, starts a local HTTP server,
|
|
87
|
+
* opens the browser for authorization, and exchanges the code for tokens.
|
|
88
|
+
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
|
89
|
+
* @param {Object} context - The execution context
|
|
90
|
+
* @returns {Promise<Object>} Auth data with access token and other OAuth2 fields
|
|
91
|
+
*/
|
|
92
|
+
const startOAuth2 = async (command, context) => {
|
|
93
|
+
const env = {};
|
|
94
|
+
|
|
95
|
+
if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) {
|
|
96
|
+
if (context.nonInteractive) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'CLIENT_ID and CLIENT_SECRET must be set in the .env file in non-interactive mode.',
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
console.warn(
|
|
102
|
+
'CLIENT_ID and CLIENT_SECRET are required for OAuth2, ' +
|
|
103
|
+
"but they are not found in the .env file. I'll prompt you for them now.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!process.env.CLIENT_ID) {
|
|
109
|
+
env.CLIENT_ID = await command.prompt('CLIENT_ID:', { useStderr: true });
|
|
110
|
+
process.env.CLIENT_ID = env.CLIENT_ID;
|
|
111
|
+
}
|
|
112
|
+
if (!process.env.CLIENT_SECRET) {
|
|
113
|
+
env.CLIENT_SECRET = await command.prompt('CLIENT_SECRET:', {
|
|
114
|
+
useStderr: true,
|
|
115
|
+
});
|
|
116
|
+
process.env.CLIENT_SECRET = env.CLIENT_SECRET;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!_.isEmpty(env)) {
|
|
120
|
+
// Save envs so the user won't have to re-enter them if the command fails
|
|
121
|
+
await appendEnv(env);
|
|
122
|
+
console.warn('CLIENT_ID and CLIENT_SECRET saved to .env file.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
startSpinner('Invoking authentication.oauth2Config.authorizeUrl');
|
|
126
|
+
|
|
127
|
+
const stateParam = crypto.randomBytes(20).toString('hex');
|
|
128
|
+
let authorizeUrl = await localAppCommand({
|
|
129
|
+
command: 'execute',
|
|
130
|
+
method: 'authentication.oauth2Config.authorizeUrl',
|
|
131
|
+
bundle: {
|
|
132
|
+
inputData: {
|
|
133
|
+
response_type: 'code',
|
|
134
|
+
redirect_uri: context.redirectUri,
|
|
135
|
+
state: stateParam,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
zcacheTestObj: context.zcacheTestObj,
|
|
139
|
+
customLogger,
|
|
140
|
+
calledFromCliInvoke: true,
|
|
141
|
+
});
|
|
142
|
+
if (!authorizeUrl.includes('&scope=')) {
|
|
143
|
+
const scope = context.appDefinition.authentication.oauth2Config.scope;
|
|
144
|
+
if (scope) {
|
|
145
|
+
authorizeUrl += `&scope=${encodeURIComponent(scope)}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
debug('authorizeUrl:', authorizeUrl);
|
|
149
|
+
|
|
150
|
+
endSpinner();
|
|
151
|
+
startSpinner('Starting local HTTP server');
|
|
152
|
+
|
|
153
|
+
let resolveCode;
|
|
154
|
+
const codePromise = new Promise((resolve) => {
|
|
155
|
+
resolveCode = resolve;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const server = http.createServer((req, res) => {
|
|
159
|
+
// Parse the request URL to extract the query parameters
|
|
160
|
+
const code = new URL(req.url, context.redirectUri).searchParams.get('code');
|
|
161
|
+
if (code) {
|
|
162
|
+
resolveCode(code);
|
|
163
|
+
debug(`Received code '${code}' from ${req.headers.referer}`);
|
|
164
|
+
|
|
165
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
166
|
+
res.end(
|
|
167
|
+
'Parameter `code` received successfully. Go back to the terminal to continue.',
|
|
168
|
+
);
|
|
169
|
+
} else {
|
|
170
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
171
|
+
res.end(
|
|
172
|
+
'Error: Did not receive `code` query parameter. ' +
|
|
173
|
+
'Did you have the right CLIENT_ID and CLIENT_SECRET? ' +
|
|
174
|
+
'Or did your server respond properly?',
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await new Promise((resolve) => {
|
|
180
|
+
server.listen(context.port, resolve);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
endSpinner();
|
|
184
|
+
startSpinner('Opening browser to authorize (press Ctrl-C to exit on error)');
|
|
185
|
+
|
|
186
|
+
const { default: open } = await import('open');
|
|
187
|
+
open(authorizeUrl);
|
|
188
|
+
|
|
189
|
+
const code = await codePromise;
|
|
190
|
+
endSpinner();
|
|
191
|
+
|
|
192
|
+
startSpinner('Closing local HTTP server');
|
|
193
|
+
await new Promise((resolve) => {
|
|
194
|
+
server.close(resolve);
|
|
195
|
+
});
|
|
196
|
+
debug('Local HTTP server closed');
|
|
197
|
+
|
|
198
|
+
endSpinner();
|
|
199
|
+
startSpinner('Invoking authentication.oauth2Config.getAccessToken');
|
|
200
|
+
|
|
201
|
+
const authData = await localAppCommand({
|
|
202
|
+
command: 'execute',
|
|
203
|
+
method: 'authentication.oauth2Config.getAccessToken',
|
|
204
|
+
bundle: {
|
|
205
|
+
authData: {},
|
|
206
|
+
inputData: {
|
|
207
|
+
code,
|
|
208
|
+
redirect_uri: context.redirectUri,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
zcacheTestObj: context.zcacheTestObj,
|
|
212
|
+
customLogger,
|
|
213
|
+
calledFromCliInvoke: true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
endSpinner();
|
|
217
|
+
return authData;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Initializes session authentication.
|
|
222
|
+
* Prompts for auth fields and then calls the session config perform method.
|
|
223
|
+
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
|
224
|
+
* @param {Object} context - The execution context
|
|
225
|
+
* @returns {Promise<Object>} Combined auth data and session data
|
|
226
|
+
* @throws {Error} If in non-interactive mode
|
|
227
|
+
*/
|
|
228
|
+
const startSessionAuth = async (command, context) => {
|
|
229
|
+
if (context.nonInteractive) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
'The `auth start` subcommand for "session" authentication type only works in interactive mode.',
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const authData = await promptForAuthFields(
|
|
235
|
+
command,
|
|
236
|
+
context.appDefinition.authentication.fields,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
startSpinner('Invoking authentication.sessionConfig.perform');
|
|
240
|
+
const sessionData = await localAppCommand({
|
|
241
|
+
command: 'execute',
|
|
242
|
+
method: 'authentication.sessionConfig.perform',
|
|
243
|
+
bundle: {
|
|
244
|
+
authData,
|
|
245
|
+
},
|
|
246
|
+
zcacheTestObj: context.zcacheTestObj,
|
|
247
|
+
customLogger,
|
|
248
|
+
calledFromCliInvoke: true,
|
|
249
|
+
});
|
|
250
|
+
endSpinner();
|
|
251
|
+
|
|
252
|
+
return { ...authData, ...sessionData };
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Main entry point for initializing authentication.
|
|
257
|
+
* Routes to the appropriate auth type handler based on the app definition.
|
|
258
|
+
* @param {import('../../../ZapierBaseCommand')} command - The command instance for prompting
|
|
259
|
+
* @param {Object} context - The execution context
|
|
260
|
+
* @returns {Promise<Object|null>} Auth data or null if no authentication needed
|
|
261
|
+
* @throws {Error} If the authentication type is not supported
|
|
262
|
+
*/
|
|
263
|
+
const startAuth = async (command, context) => {
|
|
264
|
+
const authentication = context.appDefinition.authentication;
|
|
265
|
+
if (!authentication) {
|
|
266
|
+
console.warn(
|
|
267
|
+
"Your integration doesn't seem to need authentication. " +
|
|
268
|
+
"If that isn't true, the app definition should have " +
|
|
269
|
+
'an `authentication` object at the root level.',
|
|
270
|
+
);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
switch (authentication.type) {
|
|
274
|
+
case 'basic':
|
|
275
|
+
return startBasicAuth(command, context);
|
|
276
|
+
case 'custom':
|
|
277
|
+
return startCustomAuth(command, context);
|
|
278
|
+
case 'oauth2':
|
|
279
|
+
return startOAuth2(command, context);
|
|
280
|
+
case 'session':
|
|
281
|
+
return startSessionAuth(command, context);
|
|
282
|
+
default:
|
|
283
|
+
// TODO: Add support for 'digest' and 'oauth1'
|
|
284
|
+
throw new Error(
|
|
285
|
+
`This command doesn't support authentication type "${authentication.type}".`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
module.exports = { startAuth };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { startSpinner, endSpinner } = require('../../../../utils/display');
|
|
2
|
+
const { customLogger } = require('../logger');
|
|
3
|
+
const { localAppCommandWithRelayErrorHandler } = require('../relay');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests authentication by invoking the authentication.test method.
|
|
7
|
+
* Supports both local auth data and relay mode with production credentials.
|
|
8
|
+
* @param {Object} context - The execution context with authData and optional authId
|
|
9
|
+
* @returns {Promise<*>} The test result from the authentication.test method
|
|
10
|
+
*/
|
|
11
|
+
const testAuth = async (context) => {
|
|
12
|
+
startSpinner('Invoking authentication.test');
|
|
13
|
+
const result = await localAppCommandWithRelayErrorHandler({
|
|
14
|
+
command: 'execute',
|
|
15
|
+
method: 'authentication.test',
|
|
16
|
+
bundle: {
|
|
17
|
+
authData: context.authData,
|
|
18
|
+
meta: {
|
|
19
|
+
...context.meta,
|
|
20
|
+
isTestingAuth: true,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
zcacheTestObj: context.zcacheTestObj,
|
|
24
|
+
customLogger,
|
|
25
|
+
calledFromCliInvoke: true,
|
|
26
|
+
appId: context.appId,
|
|
27
|
+
deployKey: context.deployKey,
|
|
28
|
+
relayAuthenticationId: context.authId,
|
|
29
|
+
});
|
|
30
|
+
endSpinner();
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = { testAuth };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const fsP = require('node:fs/promises');
|
|
2
|
+
|
|
3
|
+
/** Prefix used for auth data fields in the .env file */
|
|
4
|
+
const AUTH_FIELD_ENV_PREFIX = 'authData_';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Loads authData from environment variables.
|
|
8
|
+
* Looks for variables prefixed with AUTH_FIELD_ENV_PREFIX and parses JSON values.
|
|
9
|
+
* @returns {Object} An object containing auth field keys and their values
|
|
10
|
+
*/
|
|
11
|
+
const loadAuthDataFromEnv = () => {
|
|
12
|
+
return Object.entries(process.env)
|
|
13
|
+
.filter(([k, v]) => k.startsWith(AUTH_FIELD_ENV_PREFIX))
|
|
14
|
+
.reduce((authData, [k, v]) => {
|
|
15
|
+
const fieldKey = k.substr(AUTH_FIELD_ENV_PREFIX.length);
|
|
16
|
+
// Try to parse as JSON if it looks like JSON, otherwise keep as string
|
|
17
|
+
try {
|
|
18
|
+
authData[fieldKey] =
|
|
19
|
+
v.startsWith('{') || v.startsWith('[') ? JSON.parse(v) : v;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// If JSON parsing fails, keep as string
|
|
22
|
+
authData[fieldKey] = v;
|
|
23
|
+
}
|
|
24
|
+
return authData;
|
|
25
|
+
}, {});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Appends variables to the .env file.
|
|
30
|
+
* Handles proper formatting and ensures newline separation.
|
|
31
|
+
* @param {Object} vars - Key-value pairs to append
|
|
32
|
+
* @param {string} [prefix=''] - Prefix to add to each variable name
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
*/
|
|
35
|
+
const appendEnv = async (vars, prefix = '') => {
|
|
36
|
+
const envFile = '.env';
|
|
37
|
+
let content = Object.entries(vars)
|
|
38
|
+
.filter(([k, v]) => v !== undefined)
|
|
39
|
+
.map(
|
|
40
|
+
([k, v]) =>
|
|
41
|
+
`${prefix}${k}='${typeof v === 'object' && v !== null ? JSON.stringify(v) : v || ''}'\n`,
|
|
42
|
+
)
|
|
43
|
+
.join('');
|
|
44
|
+
|
|
45
|
+
// Check if .env file exists and doesn't end with newline
|
|
46
|
+
try {
|
|
47
|
+
const existingContent = await fsP.readFile(envFile, 'utf8');
|
|
48
|
+
if (existingContent.length > 0 && !existingContent.endsWith('\n')) {
|
|
49
|
+
content = '\n' + content;
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// File doesn't exist or can't be read, proceed as normal
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await fsP.appendFile(envFile, content);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
AUTH_FIELD_ENV_PREFIX,
|
|
60
|
+
loadAuthDataFromEnv,
|
|
61
|
+
appendEnv,
|
|
62
|
+
};
|