@suitegeezus/suitecloud-cli 3.1.4
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/CONTRIBUTING.md +7 -0
- package/DELTA.md +235 -0
- package/README.md +107 -0
- package/docs/Command.md +299 -0
- package/messages.json +317 -0
- package/package.json +51 -0
- package/postinstall.js +8 -0
- package/resources/FUTC-LICENSE.txt +115 -0
- package/src/ApplicationConstants.js +87 -0
- package/src/CLI.js +202 -0
- package/src/CLIException.js +21 -0
- package/src/ExecutionEnvironmentContext.js +39 -0
- package/src/SdkExecutionContext.js +90 -0
- package/src/SdkExecutor.js +160 -0
- package/src/commands/Command.js +158 -0
- package/src/commands/account/manageauth/ManageAccountAction.js +126 -0
- package/src/commands/account/manageauth/ManageAccountCommand.js +20 -0
- package/src/commands/account/manageauth/ManageAccountInputHandler.js +215 -0
- package/src/commands/account/manageauth/ManageAccountOutputHandler.js +30 -0
- package/src/commands/account/setup/SetupAction.js +39 -0
- package/src/commands/account/setup/SetupCommand.js +21 -0
- package/src/commands/account/setup/SetupInputHandler.js +169 -0
- package/src/commands/account/setup/SetupOutputHandler.js +65 -0
- package/src/commands/account/setupci/AccountSetupCiAction.js +58 -0
- package/src/commands/account/setupci/AccountSetupCiCommand.js +20 -0
- package/src/commands/account/setupci/AccountSetupCiConstants.js +21 -0
- package/src/commands/account/setupci/AccountSetupCiOutputHandler.js +40 -0
- package/src/commands/account/setupci/AccountSetupCiValidation.js +100 -0
- package/src/commands/base/BaseAction.js +37 -0
- package/src/commands/base/BaseInputHandler.js +24 -0
- package/src/commands/base/BaseOutputHandler.js +30 -0
- package/src/commands/custom/hello/Action.js +58 -0
- package/src/commands/custom/hello/Command.js +20 -0
- package/src/commands/custom/hello/Handler.js +26 -0
- package/src/commands/custom/hello/README.md +78 -0
- package/src/commands/custom/hook/Action.js +28 -0
- package/src/commands/custom/hook/Command.js +22 -0
- package/src/commands/custom/hook/InputHandler.js +8 -0
- package/src/commands/custom/hook/OutputHandler.js +8 -0
- package/src/commands/custom/hook/README.md +32 -0
- package/src/commands/file/create/CreateFileAction.js +89 -0
- package/src/commands/file/create/CreateFileCommand.js +20 -0
- package/src/commands/file/create/CreateFileInputHandler.js +175 -0
- package/src/commands/file/create/CreateFileOutputHandler.js +22 -0
- package/src/commands/file/import/ImportFilesAction.js +112 -0
- package/src/commands/file/import/ImportFilesCommand.js +22 -0
- package/src/commands/file/import/ImportFilesInputHandler.js +130 -0
- package/src/commands/file/import/ImportFilesOutputHandler.js +53 -0
- package/src/commands/file/list/ListFilesAction.js +55 -0
- package/src/commands/file/list/ListFilesCommand.js +20 -0
- package/src/commands/file/list/ListFilesInputHandler.js +55 -0
- package/src/commands/file/list/ListFilesOutputHandler.js +24 -0
- package/src/commands/file/upload/UploadFilesAction.js +67 -0
- package/src/commands/file/upload/UploadFilesCommand.js +20 -0
- package/src/commands/file/upload/UploadFilesInputHandler.js +125 -0
- package/src/commands/file/upload/UploadFilesOutputHandler.js +49 -0
- package/src/commands/object/create/CreateObjectAction.js +33 -0
- package/src/commands/object/create/CreateObjectCommand.js +19 -0
- package/src/commands/object/create/CreateObjectInputHandler.js +82 -0
- package/src/commands/object/import/ImportObjectsAction.js +225 -0
- package/src/commands/object/import/ImportObjectsCommand.js +20 -0
- package/src/commands/object/import/ImportObjectsInputHandler.js +310 -0
- package/src/commands/object/import/ImportObjectsOutputHandler.js +114 -0
- package/src/commands/object/list/ListObjectsAction.js +62 -0
- package/src/commands/object/list/ListObjectsCommand.js +20 -0
- package/src/commands/object/list/ListObjectsInputHandler.js +148 -0
- package/src/commands/object/list/ListObjectsOutputHandler.js +29 -0
- package/src/commands/object/update/UpdateAction.js +138 -0
- package/src/commands/object/update/UpdateCommand.js +20 -0
- package/src/commands/object/update/UpdateInputHandler.js +170 -0
- package/src/commands/object/update/UpdateOutputHandler.js +61 -0
- package/src/commands/project/adddependencies/AddDependenciesAction.js +55 -0
- package/src/commands/project/adddependencies/AddDependenciesCommand.js +19 -0
- package/src/commands/project/adddependencies/AddDependenciesOutputHandler.js +114 -0
- package/src/commands/project/create/CreateProjectAction.js +370 -0
- package/src/commands/project/create/CreateProjectCommand.js +20 -0
- package/src/commands/project/create/CreateProjectInputHandler.js +169 -0
- package/src/commands/project/create/CreateProjectOutputHandler.js +36 -0
- package/src/commands/project/deploy/DeployAction.js +161 -0
- package/src/commands/project/deploy/DeployCommand.js +20 -0
- package/src/commands/project/deploy/DeployInputHandler.js +100 -0
- package/src/commands/project/deploy/DeployOutputHandler.js +49 -0
- package/src/commands/project/package/PackageAction.js +59 -0
- package/src/commands/project/package/PackageCommand.js +18 -0
- package/src/commands/project/package/PackageOutputHandler.js +18 -0
- package/src/commands/project/validate/ValidateAction.js +106 -0
- package/src/commands/project/validate/ValidateCommand.js +20 -0
- package/src/commands/project/validate/ValidateInputHandler.js +92 -0
- package/src/commands/project/validate/ValidateOutputHandler.js +74 -0
- package/src/core/CommandActionExecutor.js +347 -0
- package/src/core/CommandAuthentication.js +13 -0
- package/src/core/CommandOptionsValidator.js +42 -0
- package/src/core/CommandRegistrationService.js +130 -0
- package/src/core/CommandsMetadataService.js +104 -0
- package/src/core/extensibility/CLIConfigurationService.js +192 -0
- package/src/core/extensibility/CommandUserExtension.js +64 -0
- package/src/core/sdksetup/SdkDownloadService.js +109 -0
- package/src/core/sdksetup/SdkLicense.js +39 -0
- package/src/core/sdksetup/SdkProperties.js +51 -0
- package/src/loggers/ConsoleLogger.js +32 -0
- package/src/loggers/LoggerFontFormatter.mjs +17 -0
- package/src/loggers/LoggerOsConstants.js +12 -0
- package/src/loggers/NodeConsoleLogger.js +47 -0
- package/src/metadata/CommandGenerators.json +92 -0
- package/src/metadata/NodeCommandsMetadata.json +139 -0
- package/src/metadata/ObjectTypesMetadata.js +615 -0
- package/src/metadata/SdkCommandsMetadata.json +846 -0
- package/src/metadata/SdkCommandsMetadataPatch.json +130 -0
- package/src/metadata/SuiteScriptModulesMetadata.js +152 -0
- package/src/metadata/SuiteScriptTypesMetadata.js +64 -0
- package/src/services/AccountFileCabinetService.js +86 -0
- package/src/services/EnvironmentInformationService.js +31 -0
- package/src/services/ExecutionContextService.js +108 -0
- package/src/services/FileCabinetService.js +65 -0
- package/src/services/FileSystemService.js +245 -0
- package/src/services/NodeTranslationService.js +22 -0
- package/src/services/NpmInstallRunner.js +33 -0
- package/src/services/ProjectInfoService.js +209 -0
- package/src/services/SuiteCloudAuthProxyService.js +469 -0
- package/src/services/TranslationKeys.js +506 -0
- package/src/services/TranslationService.js +30 -0
- package/src/services/actionresult/ActionResult.js +129 -0
- package/src/services/actionresult/AuthenticateActionResult.js +85 -0
- package/src/services/actionresult/CreateProjectActionResult.js +100 -0
- package/src/services/actionresult/DeployActionResult.js +69 -0
- package/src/services/actionresult/HelloActionResult.js +13 -0
- package/src/services/actionresult/ManageAccountActionResult.js +70 -0
- package/src/services/settings/CLISettings.js +46 -0
- package/src/services/settings/CLISettingsService.js +132 -0
- package/src/suitecloud.js +33 -0
- package/src/templates/TemplateKeys.js +25 -0
- package/src/templates/objects/commerceextension.xml +9 -0
- package/src/templates/projectconfigs/default_gitignore.template +47 -0
- package/src/templates/projectconfigs/sdf.config.js +4 -0
- package/src/templates/projectconfigs/suitecloud.config.js +4 -0
- package/src/templates/scripts/blankscript.js +3 -0
- package/src/templates/unittest/jest.config.js.template +7 -0
- package/src/templates/unittest/jsconfig.json.template +5 -0
- package/src/templates/unittest/package.json.template +12 -0
- package/src/templates/unittest/sample-test.js.template +37 -0
- package/src/templates/unittest/suitecloud.config.js.template +15 -0
- package/src/ui/CliSpinner.js +34 -0
- package/src/utils/AccountCredentialsFormatter.js +62 -0
- package/src/utils/AccountSpecificValuesUtils.js +55 -0
- package/src/utils/ActionResultUtils.js +47 -0
- package/src/utils/ApplyInstallationPreferencesUtils.js +41 -0
- package/src/utils/AuthenticationUtils.js +262 -0
- package/src/utils/CommandUtils.js +50 -0
- package/src/utils/CryptoUtils.js +41 -0
- package/src/utils/ExceptionUtils.js +33 -0
- package/src/utils/FileUtils.js +43 -0
- package/src/utils/SdkOperationResult.js +68 -0
- package/src/utils/SdkOperationResultUtils.js +20 -0
- package/src/utils/ValidationErrorsFormatter.js +23 -0
- package/src/utils/http/HttpConstants.js +39 -0
- package/src/utils/http/ProxyAgent.js +110 -0
- package/src/validation/InteractiveAnswersValidator.js +205 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
|
|
3
|
+
** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
/** http libraries */
|
|
8
|
+
const http = require('node:http');
|
|
9
|
+
const https = require('node:https');
|
|
10
|
+
const EventEmitter = require('events');
|
|
11
|
+
|
|
12
|
+
/** Events */
|
|
13
|
+
const EVENTS = {
|
|
14
|
+
PROXY_ERROR: {
|
|
15
|
+
DEFAULT: 'proxyError',
|
|
16
|
+
MANUAL_AUTH_REFRESH_REQUIRED: 'manualAuthRefreshRequired'
|
|
17
|
+
},
|
|
18
|
+
REQUEST_ERROR: {
|
|
19
|
+
PATH_NOT_ALLOWED: 'requestPathNotAllowed',
|
|
20
|
+
UNAUTHORIZED: 'unauthorizedProxyRequest'
|
|
21
|
+
},
|
|
22
|
+
SERVER_ERROR: {
|
|
23
|
+
DEFAULT: 'serverError',
|
|
24
|
+
ON_AUTH_REFRESH: 'serverErrorOnRefresh',
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Authentication methods */
|
|
29
|
+
const {
|
|
30
|
+
getAuthIds,
|
|
31
|
+
checkIfReauthorizationIsNeeded,
|
|
32
|
+
forceRefreshAuthorization,
|
|
33
|
+
} = require('../utils/AuthenticationUtils');
|
|
34
|
+
const {
|
|
35
|
+
AUTHORIZATION_PROPERTIES_KEYS,
|
|
36
|
+
HTTP_RESPONSE_CODE,
|
|
37
|
+
} = require('../ApplicationConstants');
|
|
38
|
+
|
|
39
|
+
/** Message literal service method */
|
|
40
|
+
const NodeTranslationService = require('./NodeTranslationService');
|
|
41
|
+
const {
|
|
42
|
+
SUITECLOUD_AUTH_PROXY_SERVICE,
|
|
43
|
+
} = require('./TranslationKeys');
|
|
44
|
+
|
|
45
|
+
const MAX_RETRY_ATTEMPTS = 1;
|
|
46
|
+
const LOCAL_HOSTNAME = '127.0.0.1';
|
|
47
|
+
const DEVASSIST_CHAT_COMPLETIONS_PATH = '/api/internal/devassist/chat/completions'
|
|
48
|
+
|
|
49
|
+
/** Target server port */
|
|
50
|
+
const TARGET_SERVER_PORT = 443;
|
|
51
|
+
|
|
52
|
+
class SuiteCloudAuthProxyService extends EventEmitter {
|
|
53
|
+
|
|
54
|
+
constructor(sdkPath, executionEnvironmentContext, allowedPathPrefix, apiKey) {
|
|
55
|
+
super();
|
|
56
|
+
this._sdkPath = sdkPath;
|
|
57
|
+
this._executionEnvironmentContext = executionEnvironmentContext;
|
|
58
|
+
this._allowedPathPrefix = allowedPathPrefix;
|
|
59
|
+
this._apiKey = apiKey;
|
|
60
|
+
/** These are the variables we are going to use to store instance data */
|
|
61
|
+
this._accessToken = undefined;
|
|
62
|
+
this._localProxy = undefined;
|
|
63
|
+
this._targetHost = undefined;
|
|
64
|
+
this._authId = undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* starts the listener.
|
|
69
|
+
* It can return an error, for instance when it cannot connect to the auth server or the parameters being incorrect
|
|
70
|
+
* @public
|
|
71
|
+
* @param authId
|
|
72
|
+
* @param proxyPort
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
async start(authId, proxyPort) {
|
|
76
|
+
|
|
77
|
+
//Parameters validation
|
|
78
|
+
this._evalInputParameters(authId, proxyPort);
|
|
79
|
+
this._authId = authId;
|
|
80
|
+
|
|
81
|
+
//Retrieve from authId accessToken and target host
|
|
82
|
+
const { accessToken, hostName } = await this._retrieveCredentials();
|
|
83
|
+
this._targetHost = hostName;
|
|
84
|
+
this._accessToken = accessToken;
|
|
85
|
+
|
|
86
|
+
await this.stop();
|
|
87
|
+
this._localProxy = http.createServer();
|
|
88
|
+
|
|
89
|
+
this._localProxy.addListener('request', async (request, response) => {
|
|
90
|
+
|
|
91
|
+
// Validate incoming request (Api Key & Request Path)
|
|
92
|
+
if (!this._isValidAndFilterIncomingRequest(request, response)) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const requestOptions = this._buildRequestOptions(request);
|
|
97
|
+
|
|
98
|
+
// Save body
|
|
99
|
+
const bodyChunks = [];
|
|
100
|
+
request.on('data', function (chunk) {
|
|
101
|
+
bodyChunks.push(chunk);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
request.on('end', async () => {
|
|
105
|
+
const body = Buffer.concat(bodyChunks);
|
|
106
|
+
const proxyRequest = await this._createProxyRequest(requestOptions, body, response, 0);
|
|
107
|
+
proxyRequest.write(body);
|
|
108
|
+
proxyRequest.end();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
this._localProxy.listen(proxyPort, LOCAL_HOSTNAME, () => {
|
|
113
|
+
const localURL = `http://${LOCAL_HOSTNAME}:${proxyPort}`;
|
|
114
|
+
console.log(`SuiteCloud Proxy server listening on ${localURL}`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this._localProxy.on('error', (error) => {
|
|
118
|
+
const errorMessage = (error.code === 'EADDRINUSE') ?
|
|
119
|
+
NodeTranslationService.getMessage(SUITECLOUD_AUTH_PROXY_SERVICE.ALREADY_USED_PORT, proxyPort, error.message ?? '')
|
|
120
|
+
: NodeTranslationService.getMessage(SUITECLOUD_AUTH_PROXY_SERVICE.INTERNAL_PROXY_SERVER_ERROR, proxyPort, error.message ?? '');
|
|
121
|
+
this._emitEventWithData(EVENTS.PROXY_ERROR.DEFAULT, errorMessage);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Public method that stops the proxy and returns a Promise resolved when it's fully closed
|
|
127
|
+
* @public
|
|
128
|
+
*/
|
|
129
|
+
async stop() {
|
|
130
|
+
// when having a "listen EADDRINUSE: address already in use 127.0.0.1:49285" the server instance exists but is not listening
|
|
131
|
+
// to avoid "Error [ERR_SERVER_NOT_RUNNING]: Server is not running." at close time there is the need to check if server is activelly listening
|
|
132
|
+
if (this._localProxy && this._localProxy.listening) {
|
|
133
|
+
// Wrap the close callback in a Promise
|
|
134
|
+
const closePromise = new Promise((resolve, reject) => {
|
|
135
|
+
this._localProxy.close(error => {
|
|
136
|
+
if (error) {
|
|
137
|
+
console.error('Error occurred while stopping SuiteCloud Auth Proxy server.')
|
|
138
|
+
console.error(error)
|
|
139
|
+
reject(error);
|
|
140
|
+
} else {
|
|
141
|
+
console.log('SuiteCloud Auth Proxy server stopped.');
|
|
142
|
+
resolve();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await closePromise;
|
|
148
|
+
} else {
|
|
149
|
+
console.log('No server instance to stop.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this._localProxy = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Updates the stored API key, which is used to authenticate and filter incoming requests to the local server.
|
|
157
|
+
*
|
|
158
|
+
* @public
|
|
159
|
+
* @param {string} newApiKey - The new API key to set for request filtering.
|
|
160
|
+
* @returns {void}
|
|
161
|
+
*/
|
|
162
|
+
updateApiKey(newApiKey) {
|
|
163
|
+
this._apiKey = newApiKey;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* For being used after manual authentication. It refreshes the access token from credentials.
|
|
169
|
+
* @public
|
|
170
|
+
* @returns {Promise<void>}
|
|
171
|
+
*/
|
|
172
|
+
async reloadAccessToken() {
|
|
173
|
+
const { accessToken } = await this._retrieveCredentials();
|
|
174
|
+
this._accessToken = accessToken;
|
|
175
|
+
console.log('access token refreshed');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Emits an event with a structured data object.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} eventName - The name of the event to emit.
|
|
182
|
+
* @param {string} errorMessage - The error message to include in the data object.
|
|
183
|
+
* @param {string} [requestUrl] - (Optional) The URL associated with the event, if applicable.
|
|
184
|
+
* @returns {void}
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
_emitEventWithData(eventName, errorMessage, requestUrl) {
|
|
188
|
+
const emitData = {
|
|
189
|
+
authId: this._authId,
|
|
190
|
+
message: errorMessage,
|
|
191
|
+
...(requestUrl && { requestUrl })
|
|
192
|
+
}
|
|
193
|
+
console.error({ eventName, emitData });
|
|
194
|
+
this.emit(eventName, emitData);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* It validates the input parameters
|
|
199
|
+
* @param authId
|
|
200
|
+
* @param proxyPort
|
|
201
|
+
* @private
|
|
202
|
+
*/
|
|
203
|
+
_evalInputParameters(authId, proxyPort) {
|
|
204
|
+
if (!authId) {
|
|
205
|
+
throw NodeTranslationService.getMessage(SUITECLOUD_AUTH_PROXY_SERVICE.MISSING_AUTH_ID);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!proxyPort) {
|
|
209
|
+
throw NodeTranslationService.getMessage(SUITECLOUD_AUTH_PROXY_SERVICE.MISSING_PORT);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isNaN(proxyPort)) {
|
|
213
|
+
throw NodeTranslationService.getMessage(SUITECLOUD_AUTH_PROXY_SERVICE.PORT_MUST_BE_NUMBER);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* This method retrieves the credentials and returns the hostname and the accessToken
|
|
219
|
+
* @returns {Promise<{hostName: string, accessToken: string}>}
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
async _retrieveCredentials() {
|
|
223
|
+
const authIDActionResult = await getAuthIds(this._sdkPath);
|
|
224
|
+
|
|
225
|
+
if (!authIDActionResult.isSuccess()) {
|
|
226
|
+
throw authIDActionResult.errorMessages;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!authIDActionResult.data.hasOwnProperty(this._authId)) {
|
|
230
|
+
throw NodeTranslationService.getMessage(SUITECLOUD_AUTH_PROXY_SERVICE.NOT_EXISTING_AUTH_ID, this._authId);
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
accessToken: authIDActionResult.data[this._authId].token.accessToken,
|
|
234
|
+
hostName: authIDActionResult.data[this._authId].hostInfo.hostName,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Validates an incoming proxy request for apiKey & allowed path.
|
|
240
|
+
* Responds and emits the correct events on failure.
|
|
241
|
+
* @param {http.IncomingMessage} request
|
|
242
|
+
* @param {http.ServerResponse} response
|
|
243
|
+
* @returns {boolean} true if valid, false if rejected
|
|
244
|
+
*/
|
|
245
|
+
_isValidAndFilterIncomingRequest(request, response) {
|
|
246
|
+
const requestValidationFunctions = [
|
|
247
|
+
this._checkAuthenticationHeader.bind(this),
|
|
248
|
+
this._checkRequestPath.bind(this)
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
// check all validations are true or stops in the first failing one and returns false
|
|
252
|
+
return requestValidationFunctions.every((validateFunction => validateFunction(request, response)));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Authentication filter: check authorization header if an API key is configured.
|
|
257
|
+
* Manages response and emit event on failure
|
|
258
|
+
*/
|
|
259
|
+
_checkAuthenticationHeader(request, response) {
|
|
260
|
+
if (this._apiKey) {
|
|
261
|
+
const authHeader = request.headers['authorization'];
|
|
262
|
+
if (authHeader !== `Bearer ${this._apiKey}`) {
|
|
263
|
+
const unauthorizedMessage = NodeTranslationService.getMessage(SUITECLOUD_AUTH_PROXY_SERVICE.UNAUTHORIZED_PROXY_REQUEST);
|
|
264
|
+
// using 401-Unauthorized http response code as CLINE won't activate the retry mechanism with it
|
|
265
|
+
// not using 407-Proxy Authentication Required as CLINE activates the retry mechanism with it
|
|
266
|
+
this._writeResponseMessage(response, HTTP_RESPONSE_CODE.UNAUTHORIZED, unauthorizedMessage);
|
|
267
|
+
this._emitEventWithData(EVENTS.REQUEST_ERROR.UNAUTHORIZED, unauthorizedMessage, request.url)
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Allowed path filter: check allowed prefix if configured.
|
|
276
|
+
* Manages response and emit event on failure
|
|
277
|
+
*/
|
|
278
|
+
_checkRequestPath(request, response) {
|
|
279
|
+
// Allowed path filter: check allowed prefix if configured
|
|
280
|
+
if (this._allowedPathPrefix && !request.url.startsWith(this._allowedPathPrefix)) {
|
|
281
|
+
const errorMessage = NodeTranslationService.
|
|
282
|
+
getMessage(SUITECLOUD_AUTH_PROXY_SERVICE.REQUEST_PATH_NOT_ALLOWED_ERROR, this._allowedPathPrefix);
|
|
283
|
+
this._writeResponseMessage(response, HTTP_RESPONSE_CODE.FORBIDDEN, errorMessage);
|
|
284
|
+
this._emitEventWithData(EVENTS.REQUEST_ERROR.PATH_NOT_ALLOWED, errorMessage);
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Builds request options
|
|
292
|
+
* @param request request
|
|
293
|
+
* @returns {{path: *, headers: *&{authorization: string}, hostname: *, method: *, port: number}}
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
_buildRequestOptions(request) {
|
|
297
|
+
const authorization = 'Bearer ' + this._accessToken;
|
|
298
|
+
const host = this._targetHost;
|
|
299
|
+
|
|
300
|
+
const requestOptions = {
|
|
301
|
+
hostname: this._targetHost,
|
|
302
|
+
port: TARGET_SERVER_PORT,
|
|
303
|
+
path: request.url,
|
|
304
|
+
method: request.method,
|
|
305
|
+
headers: { ...request.headers, host, authorization },
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// added to get "stream responses" from netsuite devassist backend
|
|
309
|
+
if (request?.url.startsWith(DEVASSIST_CHAT_COMPLETIONS_PATH)) {
|
|
310
|
+
requestOptions.headers = {
|
|
311
|
+
...requestOptions.headers,
|
|
312
|
+
Accept: 'text/event-stream'
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Add agent for insecure connections when connecting to runboxes
|
|
317
|
+
if (this._targetHost && this._targetHost.includes('vm.eng')) {
|
|
318
|
+
requestOptions.agent = new https.Agent({
|
|
319
|
+
rejectUnauthorized: false,
|
|
320
|
+
});
|
|
321
|
+
requestOptions.rejectUnauthorized = false;
|
|
322
|
+
}
|
|
323
|
+
return requestOptions;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async _createProxyRequest(requestOptions, body, response, attempts) {
|
|
327
|
+
const proxyRequest = https.request(requestOptions, async (proxyResponse) => {
|
|
328
|
+
if (proxyResponse.statusCode === HTTP_RESPONSE_CODE.UNAUTHORIZED && attempts <= MAX_RETRY_ATTEMPTS) {
|
|
329
|
+
proxyResponse.resume();
|
|
330
|
+
const refreshOperationResult = await this._tryRefreshOperation();
|
|
331
|
+
|
|
332
|
+
if (refreshOperationResult.accessTokenHasBeenUpdated) {
|
|
333
|
+
this._updateRequestAuthorizationHeader(requestOptions);
|
|
334
|
+
const newProxyRequest = await this._createProxyRequest(requestOptions, body, response, attempts + 1);
|
|
335
|
+
newProxyRequest.write(body);
|
|
336
|
+
newProxyRequest.end();
|
|
337
|
+
} else {
|
|
338
|
+
this._emitEventWithData(refreshOperationResult.emitEventName, refreshOperationResult.errorMessage);
|
|
339
|
+
//Message shown to cline
|
|
340
|
+
this._writeResponseMessage(response, refreshOperationResult.responseStatusCode, refreshOperationResult.errorMessage);
|
|
341
|
+
proxyResponse.pipe(response, { end: true });
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
response.writeHead(proxyResponse.statusCode || HTTP_RESPONSE_CODE.INTERNAL_SERVER_ERROR, proxyResponse.headers);
|
|
345
|
+
proxyResponse.pipe(response, { end: true });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
proxyRequest.on('error', (error) => {
|
|
351
|
+
console.error('Proxy request error:', error);
|
|
352
|
+
this._emitEventWithData(EVENTS.SERVER_ERROR.DEFAULT, error.message)
|
|
353
|
+
response.writeHead(HTTP_RESPONSE_CODE.INTERNAL_SERVER_ERROR);
|
|
354
|
+
//TODO Review this message and see confluence error pages and review with the tech writers
|
|
355
|
+
response.end('SuiteCloud Proxy error: ' + error.message);
|
|
356
|
+
});
|
|
357
|
+
return proxyRequest;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* This method refreshes authorization.
|
|
362
|
+
* If successful returns true and updates this._accessToken
|
|
363
|
+
* If not successful returns false and returns the http response and the info to generate the emit message.
|
|
364
|
+
* @returns {Promise<*>}
|
|
365
|
+
* @private
|
|
366
|
+
*/
|
|
367
|
+
async _tryRefreshOperation() {
|
|
368
|
+
const refreshInfo = {
|
|
369
|
+
accessTokenHasBeenUpdated: false,//Whether the token has been updated or not.
|
|
370
|
+
emitEventName: undefined, //Event to be thrown.
|
|
371
|
+
responseStatusCode: undefined, //HTTP response status code.
|
|
372
|
+
errorMessage: undefined //Error message, used both for emit data and http response.
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const inspectAuthOperationResult = await checkIfReauthorizationIsNeeded(this._authId, this._sdkPath, this._executionEnvironmentContext);
|
|
376
|
+
|
|
377
|
+
//Not being able to execute the reauth if needed, it can be vpn disconnected, network problems...
|
|
378
|
+
if (!inspectAuthOperationResult.isSuccess()) {
|
|
379
|
+
const errorMsg = this._cleanText(inspectAuthOperationResult.errorMessages.join('. '));
|
|
380
|
+
|
|
381
|
+
refreshInfo.emitEventName = EVENTS.SERVER_ERROR.ON_AUTH_REFRESH;
|
|
382
|
+
refreshInfo.errorMessage = errorMsg;
|
|
383
|
+
refreshInfo.responseStatusCode = HTTP_RESPONSE_CODE.FORBIDDEN;
|
|
384
|
+
|
|
385
|
+
return Object.freeze(refreshInfo);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
//Needs manual reauthorization
|
|
389
|
+
const inspectAuthData = inspectAuthOperationResult.data;
|
|
390
|
+
if (inspectAuthData[AUTHORIZATION_PROPERTIES_KEYS.NEEDS_REAUTHORIZATION]) {
|
|
391
|
+
const errorMsg = NodeTranslationService.getMessage(SUITECLOUD_AUTH_PROXY_SERVICE.NEED_TO_REAUTHENTICATE);
|
|
392
|
+
|
|
393
|
+
refreshInfo.emitEventName = EVENTS.PROXY_ERROR.MANUAL_AUTH_REFRESH_REQUIRED;
|
|
394
|
+
refreshInfo.errorMessage = errorMsg;
|
|
395
|
+
refreshInfo.responseStatusCode = HTTP_RESPONSE_CODE.FORBIDDEN;
|
|
396
|
+
|
|
397
|
+
return Object.freeze(refreshInfo);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
//force refresh
|
|
401
|
+
const forceRefreshOperationResult = await forceRefreshAuthorization(this._authId, this._sdkPath, this._executionEnvironmentContext);
|
|
402
|
+
if (!forceRefreshOperationResult.isSuccess()) {
|
|
403
|
+
//Refresh unsuccessful
|
|
404
|
+
const errorMsg = this._cleanText(forceRefreshOperationResult.errorMessages.join('. '));
|
|
405
|
+
|
|
406
|
+
refreshInfo.emitEventName = EVENTS.PROXY_ERROR.MANUAL_AUTH_REFRESH_REQUIRED;
|
|
407
|
+
refreshInfo.errorMessage = errorMsg;
|
|
408
|
+
refreshInfo.responseStatusCode = HTTP_RESPONSE_CODE.FORBIDDEN;
|
|
409
|
+
|
|
410
|
+
return Object.freeze(refreshInfo);
|
|
411
|
+
} else {
|
|
412
|
+
//If the refresh has been successful
|
|
413
|
+
//Return operation forceRefreshOperationResult as true and update the accessToken
|
|
414
|
+
refreshInfo.accessTokenHasBeenUpdated = true;
|
|
415
|
+
this._accessToken = forceRefreshOperationResult.data.accessToken;
|
|
416
|
+
|
|
417
|
+
return Object.freeze(refreshInfo);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Method to clear output messages.
|
|
423
|
+
* The reason for this is the output do not show properly \n and \r
|
|
424
|
+
* So they are replaced by . and made some extra adjustments.
|
|
425
|
+
* @param input
|
|
426
|
+
* @returns {*}
|
|
427
|
+
*/
|
|
428
|
+
_cleanText(input) {
|
|
429
|
+
let result = input.replace(/\r/g, ''); // Remove \r
|
|
430
|
+
result = result.replace(/\n/g, '. '); // Replace \n with ". "
|
|
431
|
+
result = result.replace(/,\./g, '.'); // Replace ",." with "."
|
|
432
|
+
result = result.replace(/"/g, '\''); //Replace \" by ' since \" also show problems.
|
|
433
|
+
while (result.includes(' ')) { // Replace double spaces with single space
|
|
434
|
+
result = result.replace(/ /g, ' ');
|
|
435
|
+
}
|
|
436
|
+
while (result.includes('..')) { // Replace ".." with "."
|
|
437
|
+
result = result.replace(/\.\./g, '.');
|
|
438
|
+
}
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Update requestOptions access token without need to rebuild the requestOptions
|
|
444
|
+
* @param requestOptions
|
|
445
|
+
* @private
|
|
446
|
+
*/
|
|
447
|
+
_updateRequestAuthorizationHeader(requestOptions) {
|
|
448
|
+
if (requestOptions.headers && requestOptions.headers.authorization) {
|
|
449
|
+
requestOptions.headers.authorization = 'Bearer ' + this._accessToken;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Write JSON response message
|
|
456
|
+
* @param response
|
|
457
|
+
* @param responseCode
|
|
458
|
+
* @param responseMessage
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
_writeResponseMessage(response, responseCode, responseMessage) {
|
|
462
|
+
response.writeHead(responseCode, { 'Content-Type': 'application/json' });
|
|
463
|
+
const message = { error: responseMessage };
|
|
464
|
+
response.end(JSON.stringify(message));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
module.exports = { SuiteCloudAuthProxyService, EVENTS };
|