@dishantlangayan/sc-plugin-broker 0.1.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/LICENSE.txt +202 -0
- package/README.md +268 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/broker/login/basic.d.ts +54 -0
- package/dist/commands/broker/login/basic.js +293 -0
- package/dist/commands/broker/login/cloud.d.ts +56 -0
- package/dist/commands/broker/login/cloud.js +333 -0
- package/dist/commands/broker/login/list.d.ts +10 -0
- package/dist/commands/broker/login/list.js +36 -0
- package/dist/commands/broker/logout.d.ts +15 -0
- package/dist/commands/broker/logout.js +111 -0
- package/dist/commands/broker/queue/create.d.ts +28 -0
- package/dist/commands/broker/queue/create.js +116 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/broker-utils.d.ts +25 -0
- package/dist/lib/broker-utils.js +62 -0
- package/dist/types/cloud-api.d.ts +102 -0
- package/dist/types/cloud-api.js +5 -0
- package/dist/types/msgvpn-queue.d.ts +49 -0
- package/dist/types/msgvpn-queue.js +4 -0
- package/oclif.manifest.json +475 -0
- package/package.json +75 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { AuthType, BrokerAuthError, BrokerAuthErrorCode, ScCommand, } from '@dishantlangayan/sc-cli-core';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import * as process from 'node:process';
|
|
4
|
+
import * as readline from 'node:readline';
|
|
5
|
+
export default class BrokerLoginBasic extends ScCommand {
|
|
6
|
+
static args = {};
|
|
7
|
+
static description = `Login to a Solace Event Broker using Basic authentication.
|
|
8
|
+
|
|
9
|
+
Stores broker credentials securely using encrypted local storage.
|
|
10
|
+
Credentials are base64-encoded and encrypted before storage.
|
|
11
|
+
|
|
12
|
+
If a broker with the same name already exists, you'll be prompted to overwrite.
|
|
13
|
+
|
|
14
|
+
Required SEMP permissions: Varies by operations you intend to perform`;
|
|
15
|
+
static examples = [
|
|
16
|
+
'<%= config.bin %> <%= command.id %> --broker-name=dev-broker --semp-url=https://localhost --semp-port=8080',
|
|
17
|
+
'<%= config.bin %> <%= command.id %> --broker-name=ci-broker --semp-url=http://192.168.1.100 --semp-port=8080 --no-prompt',
|
|
18
|
+
'<%= config.bin %> <%= command.id %> --broker-name=default-broker --semp-url=https://broker.example.com --semp-port=943 --set-default',
|
|
19
|
+
];
|
|
20
|
+
static flags = {
|
|
21
|
+
'broker-name': Flags.string({
|
|
22
|
+
char: 'b',
|
|
23
|
+
description: 'Name/identifier for the broker',
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
26
|
+
'no-prompt': Flags.boolean({
|
|
27
|
+
default: false,
|
|
28
|
+
description: 'Read credentials from SC_SEMP_USERNAME and SC_SEMP_PASSWORD environment variables',
|
|
29
|
+
}),
|
|
30
|
+
'semp-port': Flags.integer({
|
|
31
|
+
char: 'p',
|
|
32
|
+
description: 'SEMP port number (1-65535)',
|
|
33
|
+
required: true,
|
|
34
|
+
}),
|
|
35
|
+
'semp-url': Flags.string({
|
|
36
|
+
char: 'u',
|
|
37
|
+
description: 'SEMP endpoint URL (must start with http:// or https://)',
|
|
38
|
+
required: true,
|
|
39
|
+
}),
|
|
40
|
+
'set-default': Flags.boolean({
|
|
41
|
+
char: 'd',
|
|
42
|
+
default: false,
|
|
43
|
+
description: 'Set this broker as the default',
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
async run() {
|
|
47
|
+
const { flags } = await this.parse(BrokerLoginBasic);
|
|
48
|
+
try {
|
|
49
|
+
// Get broker auth manager
|
|
50
|
+
const brokerAuthManager = await this.getBrokerAuthManager();
|
|
51
|
+
// Validate inputs and get broker name
|
|
52
|
+
this.validateInputs(flags);
|
|
53
|
+
const brokerName = flags['broker-name'];
|
|
54
|
+
// Handle existing broker
|
|
55
|
+
const isUpdate = await this.handleExistingBroker(brokerAuthManager, brokerName, flags['no-prompt']);
|
|
56
|
+
// Obtain credentials
|
|
57
|
+
const { password, username } = await this.obtainCredentials(flags['no-prompt']);
|
|
58
|
+
// Encode credentials and create broker config
|
|
59
|
+
const accessToken = this.encodeBasicAuth(username, password);
|
|
60
|
+
const brokerAuth = this.createBrokerAuth(flags, brokerName, accessToken);
|
|
61
|
+
// Store broker configuration
|
|
62
|
+
await (isUpdate ? brokerAuthManager.updateBroker(brokerName, brokerAuth) : brokerAuthManager.addBroker(brokerAuth));
|
|
63
|
+
// Set as default if requested
|
|
64
|
+
if (flags['set-default']) {
|
|
65
|
+
await brokerAuthManager.setDefaultBroker(brokerName);
|
|
66
|
+
}
|
|
67
|
+
// Display success message
|
|
68
|
+
this.displaySuccessMessage(isUpdate, brokerName, flags['set-default']);
|
|
69
|
+
return brokerAuth;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
this.handleLoginError(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates broker authentication configuration object
|
|
77
|
+
*/
|
|
78
|
+
createBrokerAuth(flags, brokerName, accessToken) {
|
|
79
|
+
return {
|
|
80
|
+
accessToken,
|
|
81
|
+
authType: AuthType.BASIC,
|
|
82
|
+
name: brokerName,
|
|
83
|
+
sempEndpoint: flags['semp-url'],
|
|
84
|
+
sempPort: flags['semp-port'],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Displays success message after login
|
|
89
|
+
*/
|
|
90
|
+
displaySuccessMessage(isUpdate, brokerName, setDefault) {
|
|
91
|
+
const action = isUpdate ? 'updated' : 'logged in to';
|
|
92
|
+
this.log(`Successfully ${action} broker '${brokerName}'`);
|
|
93
|
+
if (setDefault) {
|
|
94
|
+
this.log('Set as default broker.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Encodes username and password to base64 for basic authentication
|
|
99
|
+
*/
|
|
100
|
+
encodeBasicAuth(username, password) {
|
|
101
|
+
const credentials = `${username}:${password}`;
|
|
102
|
+
return Buffer.from(credentials, 'utf8').toString('base64');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Checks if broker exists and handles overwrite confirmation
|
|
106
|
+
*/
|
|
107
|
+
async handleExistingBroker(brokerAuthManager, brokerName, noPrompt) {
|
|
108
|
+
const exists = await brokerAuthManager.brokerExists(brokerName);
|
|
109
|
+
if (!exists) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
// If no-prompt, assume yes to overwrite
|
|
113
|
+
if (noPrompt) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const shouldOverwrite = await this.promptForConfirmation(`Broker '${brokerName}' already exists. Do you want to overwrite the credentials?`);
|
|
117
|
+
if (!shouldOverwrite) {
|
|
118
|
+
this.log('Login cancelled.');
|
|
119
|
+
this.exit(0);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Handles errors during login process
|
|
125
|
+
*/
|
|
126
|
+
handleLoginError(error) {
|
|
127
|
+
if (error instanceof BrokerAuthError) {
|
|
128
|
+
switch (error.code) {
|
|
129
|
+
case BrokerAuthErrorCode.FILE_WRITE_ERROR: {
|
|
130
|
+
this.error('Failed to save broker configuration. Please check file permissions.');
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case BrokerAuthErrorCode.INVALID_ACCESS_TOKEN: {
|
|
134
|
+
this.error('Failed to encode credentials. Please check your username and password.');
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case BrokerAuthErrorCode.INVALID_ENDPOINT: {
|
|
138
|
+
this.error('Invalid SEMP endpoint URL format. URL must start with http:// or https://');
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case BrokerAuthErrorCode.INVALID_NAME: {
|
|
142
|
+
this.error('Invalid broker name. Please provide a valid broker name.');
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case BrokerAuthErrorCode.INVALID_PORT: {
|
|
146
|
+
this.error('Invalid port number. Port must be between 1 and 65535.');
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case BrokerAuthErrorCode.NOT_INITIALIZED: {
|
|
150
|
+
this.error('Failed to initialize broker authentication manager.');
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
default: {
|
|
154
|
+
this.error(`Login failed: ${error.message}`);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (error instanceof Error && error.message === 'Cancelled by user') {
|
|
160
|
+
this.error('Login cancelled.');
|
|
161
|
+
}
|
|
162
|
+
// Re-throw unexpected errors
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Obtains credentials from environment variables or user prompts
|
|
167
|
+
*/
|
|
168
|
+
async obtainCredentials(noPrompt) {
|
|
169
|
+
if (noPrompt) {
|
|
170
|
+
const username = process.env.SC_SEMP_USERNAME || '';
|
|
171
|
+
const password = process.env.SC_SEMP_PASSWORD || '';
|
|
172
|
+
if (!username || !password) {
|
|
173
|
+
this.error('SC_SEMP_USERNAME and SC_SEMP_PASSWORD environment variables must be set when using --no-prompt flag.');
|
|
174
|
+
}
|
|
175
|
+
return { password, username };
|
|
176
|
+
}
|
|
177
|
+
const username = await this.promptForUsername();
|
|
178
|
+
const password = await this.promptForPassword();
|
|
179
|
+
return { password, username };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Prompts user for confirmation with Y/n (default Yes)
|
|
183
|
+
*/
|
|
184
|
+
async promptForConfirmation(message) {
|
|
185
|
+
const rl = readline.createInterface({
|
|
186
|
+
input: process.stdin,
|
|
187
|
+
output: process.stdout,
|
|
188
|
+
});
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
rl.question(`${message} (Y/n): `, (answer) => {
|
|
191
|
+
rl.close();
|
|
192
|
+
const normalized = answer.trim().toLowerCase();
|
|
193
|
+
// Default to yes if empty, accept y/yes as confirmation
|
|
194
|
+
const confirmed = normalized === '' || normalized === 'y' || normalized === 'yes';
|
|
195
|
+
resolve(confirmed);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Prompts user for password with hidden input
|
|
201
|
+
*/
|
|
202
|
+
async promptForPassword() {
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const rl = readline.createInterface({
|
|
205
|
+
input: process.stdin,
|
|
206
|
+
output: process.stdout,
|
|
207
|
+
});
|
|
208
|
+
process.stdout.write('SEMP Password: ');
|
|
209
|
+
let password = '';
|
|
210
|
+
const { stdin } = process;
|
|
211
|
+
// Hide input by setting raw mode
|
|
212
|
+
if (stdin.isTTY) {
|
|
213
|
+
stdin.setRawMode(true);
|
|
214
|
+
}
|
|
215
|
+
const onData = (char) => {
|
|
216
|
+
const charStr = char.toString();
|
|
217
|
+
// Enter key
|
|
218
|
+
if (charStr === '\n' || charStr === '\r' || charStr === '\r\n') {
|
|
219
|
+
process.stdout.write('\n');
|
|
220
|
+
if (stdin.isTTY) {
|
|
221
|
+
stdin.setRawMode(false);
|
|
222
|
+
}
|
|
223
|
+
stdin.removeListener('data', onData);
|
|
224
|
+
rl.close();
|
|
225
|
+
if (!password) {
|
|
226
|
+
reject(new Error('Password cannot be empty'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
resolve(password);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Ctrl+C
|
|
233
|
+
if (charStr === '\u0003') {
|
|
234
|
+
process.stdout.write('\n');
|
|
235
|
+
if (stdin.isTTY) {
|
|
236
|
+
stdin.setRawMode(false);
|
|
237
|
+
}
|
|
238
|
+
stdin.removeListener('data', onData);
|
|
239
|
+
rl.close();
|
|
240
|
+
reject(new Error('Cancelled by user'));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// Backspace/Delete
|
|
244
|
+
if (charStr === '\u007F' || charStr === '\b') {
|
|
245
|
+
if (password.length > 0) {
|
|
246
|
+
password = password.slice(0, -1);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Printable characters only
|
|
251
|
+
if (charStr >= ' ') {
|
|
252
|
+
password += charStr;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
stdin.on('data', onData);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Prompts user for username
|
|
260
|
+
*/
|
|
261
|
+
async promptForUsername() {
|
|
262
|
+
const rl = readline.createInterface({
|
|
263
|
+
input: process.stdin,
|
|
264
|
+
output: process.stdout,
|
|
265
|
+
});
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
rl.question('SEMP Username: ', (answer) => {
|
|
268
|
+
rl.close();
|
|
269
|
+
const username = answer.trim();
|
|
270
|
+
if (!username) {
|
|
271
|
+
reject(new Error('Username cannot be empty'));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
resolve(username);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Validates command inputs
|
|
280
|
+
*/
|
|
281
|
+
validateInputs(flags) {
|
|
282
|
+
// Validate SEMP URL format
|
|
283
|
+
const sempUrl = flags['semp-url'];
|
|
284
|
+
if (!sempUrl.startsWith('http://') && !sempUrl.startsWith('https://')) {
|
|
285
|
+
this.error('SEMP URL must start with http:// or https://');
|
|
286
|
+
}
|
|
287
|
+
// Validate port range
|
|
288
|
+
const port = flags['semp-port'];
|
|
289
|
+
if (port < 1 || port > 65_535) {
|
|
290
|
+
this.error('SEMP port must be between 1 and 65535.');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type BrokerAuth, ScCommand } from '@dishantlangayan/sc-cli-core';
|
|
2
|
+
export default class BrokerLoginCloud extends ScCommand<typeof BrokerLoginCloud> {
|
|
3
|
+
static args: {};
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
'broker-name': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'no-prompt': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
'org-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'set-default': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<BrokerAuth>;
|
|
13
|
+
/**
|
|
14
|
+
* Creates broker authentication configuration object
|
|
15
|
+
*/
|
|
16
|
+
private createBrokerAuth;
|
|
17
|
+
/**
|
|
18
|
+
* Displays success message after login
|
|
19
|
+
*/
|
|
20
|
+
private displaySuccessMessage;
|
|
21
|
+
/**
|
|
22
|
+
* Encodes username and password to base64 for basic authentication
|
|
23
|
+
*/
|
|
24
|
+
private encodeBasicAuth;
|
|
25
|
+
/**
|
|
26
|
+
* Extract SEMP connection details from service response
|
|
27
|
+
*/
|
|
28
|
+
private extractSempDetails;
|
|
29
|
+
/**
|
|
30
|
+
* Find service ID by broker name using Cloud API
|
|
31
|
+
* Uses RSQL filter: name==brokerName
|
|
32
|
+
*/
|
|
33
|
+
private findServiceIdByName;
|
|
34
|
+
/**
|
|
35
|
+
* Get authenticated Cloud API connection
|
|
36
|
+
* Validates that user is logged in to Solace Cloud
|
|
37
|
+
*/
|
|
38
|
+
private getCloudConnection;
|
|
39
|
+
/**
|
|
40
|
+
* Get detailed service information with expanded fields
|
|
41
|
+
*/
|
|
42
|
+
private getServiceDetails;
|
|
43
|
+
/**
|
|
44
|
+
* Checks if broker exists and handles overwrite confirmation
|
|
45
|
+
* Same pattern as broker:login:basic
|
|
46
|
+
*/
|
|
47
|
+
private handleExistingBroker;
|
|
48
|
+
/**
|
|
49
|
+
* Handles errors during login process
|
|
50
|
+
*/
|
|
51
|
+
private handleLoginError;
|
|
52
|
+
/**
|
|
53
|
+
* Prompts user for confirmation with Y/n (default Yes)
|
|
54
|
+
*/
|
|
55
|
+
private promptForConfirmation;
|
|
56
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { AuthType, BrokerAuthError, BrokerAuthErrorCode, OrgError, OrgErrorCode, ScCommand, } from '@dishantlangayan/sc-cli-core';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import * as process from 'node:process';
|
|
4
|
+
import * as readline from 'node:readline';
|
|
5
|
+
export default class BrokerLoginCloud extends ScCommand {
|
|
6
|
+
static args = {};
|
|
7
|
+
static description = `Login to a Solace Cloud Event Broker using Cloud API credentials.
|
|
8
|
+
|
|
9
|
+
Retrieves SEMP credentials automatically from Solace Cloud REST API.
|
|
10
|
+
Requires prior authentication to Solace Cloud (org:login).
|
|
11
|
+
|
|
12
|
+
The command will:
|
|
13
|
+
1. Look up the broker by name in your Solace Cloud organization
|
|
14
|
+
2. Retrieve SEMP endpoint details and credentials from Cloud API
|
|
15
|
+
3. Store encrypted broker credentials locally for future use
|
|
16
|
+
|
|
17
|
+
Required Cloud API permissions: Read access to Event Broker Services`;
|
|
18
|
+
static examples = [
|
|
19
|
+
'<%= config.bin %> <%= command.id %> --broker-name=production-broker',
|
|
20
|
+
'<%= config.bin %> <%= command.id %> --broker-name=dev-broker --set-default',
|
|
21
|
+
'<%= config.bin %> <%= command.id %> --broker-name=staging-broker --org-name=my-org',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> --broker-name=prod --no-prompt',
|
|
23
|
+
];
|
|
24
|
+
static flags = {
|
|
25
|
+
'broker-name': Flags.string({
|
|
26
|
+
char: 'b',
|
|
27
|
+
description: 'Name of the broker in Solace Cloud',
|
|
28
|
+
required: true,
|
|
29
|
+
}),
|
|
30
|
+
'no-prompt': Flags.boolean({
|
|
31
|
+
default: false,
|
|
32
|
+
description: 'Skip confirmation prompts for overwriting existing broker',
|
|
33
|
+
}),
|
|
34
|
+
'org-name': Flags.string({
|
|
35
|
+
char: 'o',
|
|
36
|
+
description: 'Solace Cloud organization name (uses default org if not specified)',
|
|
37
|
+
}),
|
|
38
|
+
'set-default': Flags.boolean({
|
|
39
|
+
char: 'd',
|
|
40
|
+
default: false,
|
|
41
|
+
description: 'Set this broker as the default',
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
async run() {
|
|
45
|
+
const { flags } = await this.parse(BrokerLoginCloud);
|
|
46
|
+
try {
|
|
47
|
+
// Step 1: Get managers
|
|
48
|
+
const brokerAuthManager = await this.getBrokerAuthManager();
|
|
49
|
+
const orgManager = await this.getOrgManager();
|
|
50
|
+
// Step 2: Validate Cloud API authentication
|
|
51
|
+
const cloudConnection = await this.getCloudConnection(orgManager, flags['org-name']);
|
|
52
|
+
// Step 3: Handle existing broker
|
|
53
|
+
const brokerName = flags['broker-name'];
|
|
54
|
+
const isUpdate = await this.handleExistingBroker(brokerAuthManager, brokerName, flags['no-prompt']);
|
|
55
|
+
// Step 4: Fetch broker details from Cloud API
|
|
56
|
+
this.log(`Fetching broker details for '${brokerName}' from Solace Cloud...`);
|
|
57
|
+
const serviceId = await this.findServiceIdByName(cloudConnection, brokerName);
|
|
58
|
+
const serviceDetails = await this.getServiceDetails(cloudConnection, serviceId);
|
|
59
|
+
// Step 5: Extract SEMP credentials
|
|
60
|
+
const { msgVpnName, password, sempEndpoint, sempPort, username } = this.extractSempDetails(serviceDetails);
|
|
61
|
+
// Step 6: Create broker auth configuration
|
|
62
|
+
const accessToken = this.encodeBasicAuth(username, password);
|
|
63
|
+
const brokerAuth = this.createBrokerAuth({
|
|
64
|
+
accessToken,
|
|
65
|
+
brokerName,
|
|
66
|
+
msgVpnName,
|
|
67
|
+
sempEndpoint,
|
|
68
|
+
sempPort,
|
|
69
|
+
});
|
|
70
|
+
// Step 7: Store broker configuration
|
|
71
|
+
await (isUpdate ? brokerAuthManager.updateBroker(brokerName, brokerAuth) : brokerAuthManager.addBroker(brokerAuth));
|
|
72
|
+
// Step 8: Set as default if requested
|
|
73
|
+
if (flags['set-default']) {
|
|
74
|
+
await brokerAuthManager.setDefaultBroker(brokerName);
|
|
75
|
+
}
|
|
76
|
+
// Step 9: Display success message
|
|
77
|
+
this.displaySuccessMessage(isUpdate, brokerName, flags['set-default']);
|
|
78
|
+
return brokerAuth;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.handleLoginError(error, flags['broker-name']);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Creates broker authentication configuration object
|
|
86
|
+
*/
|
|
87
|
+
createBrokerAuth(params) {
|
|
88
|
+
return {
|
|
89
|
+
accessToken: params.accessToken,
|
|
90
|
+
authType: AuthType.BASIC,
|
|
91
|
+
isSolaceCloud: true,
|
|
92
|
+
msgVpnName: params.msgVpnName,
|
|
93
|
+
name: params.brokerName,
|
|
94
|
+
sempEndpoint: params.sempEndpoint,
|
|
95
|
+
sempPort: params.sempPort,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Displays success message after login
|
|
100
|
+
*/
|
|
101
|
+
displaySuccessMessage(isUpdate, brokerName, setDefault) {
|
|
102
|
+
const action = isUpdate ? 'updated' : 'logged in to';
|
|
103
|
+
this.log(`Successfully ${action} broker '${brokerName}'`);
|
|
104
|
+
if (setDefault) {
|
|
105
|
+
this.log('Set as default broker.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Encodes username and password to base64 for basic authentication
|
|
110
|
+
*/
|
|
111
|
+
encodeBasicAuth(username, password) {
|
|
112
|
+
const credentials = `${username}:${password}`;
|
|
113
|
+
return Buffer.from(credentials, 'utf8').toString('base64');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extract SEMP connection details from service response
|
|
117
|
+
*/
|
|
118
|
+
extractSempDetails(service) {
|
|
119
|
+
// Validate required fields
|
|
120
|
+
if (!service.msgVpnName) {
|
|
121
|
+
this.error('Missing msgVpnName in broker details.', { exit: 1 });
|
|
122
|
+
}
|
|
123
|
+
if (!service.defaultManagementHostname) {
|
|
124
|
+
this.error('Missing defaultManagementHostname in broker details.', { exit: 1 });
|
|
125
|
+
}
|
|
126
|
+
if (!service.serviceConnectionEndpoints || service.serviceConnectionEndpoints.length === 0) {
|
|
127
|
+
this.error('No service connection endpoints found for this broker.', { exit: 1 });
|
|
128
|
+
}
|
|
129
|
+
if (!service.broker?.msgVpns || service.broker.msgVpns.length === 0) {
|
|
130
|
+
this.error('Missing broker message VPN configuration.', { exit: 1 });
|
|
131
|
+
}
|
|
132
|
+
const msgVpn = service.broker.msgVpns[0];
|
|
133
|
+
if (!msgVpn.missionControlManagerLoginCredential) {
|
|
134
|
+
this.error('Missing SEMP credentials in broker details.', { exit: 1 });
|
|
135
|
+
}
|
|
136
|
+
// Find SEMP TLS port from all service connection endpoints
|
|
137
|
+
let sempPort;
|
|
138
|
+
for (const endpoint of service.serviceConnectionEndpoints) {
|
|
139
|
+
if (!endpoint.ports)
|
|
140
|
+
continue;
|
|
141
|
+
const sempPortConfig = endpoint.ports.find((p) => p.protocol === 'serviceManagementTlsListenPort');
|
|
142
|
+
if (sempPortConfig && sempPortConfig.port > 0) {
|
|
143
|
+
sempPort = sempPortConfig.port;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!sempPort) {
|
|
148
|
+
this.error('SEMP TLS endpoint not found. Ensure the broker has SEMP over TLS enabled.', { exit: 1 });
|
|
149
|
+
}
|
|
150
|
+
// Extract credentials
|
|
151
|
+
const { password, username } = msgVpn.missionControlManagerLoginCredential;
|
|
152
|
+
if (!username || !password) {
|
|
153
|
+
this.error('Incomplete SEMP credentials in broker details.', { exit: 1 });
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
msgVpnName: service.msgVpnName,
|
|
157
|
+
password,
|
|
158
|
+
sempEndpoint: `https://${service.defaultManagementHostname}`,
|
|
159
|
+
sempPort,
|
|
160
|
+
username,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Find service ID by broker name using Cloud API
|
|
165
|
+
* Uses RSQL filter: name==brokerName
|
|
166
|
+
*/
|
|
167
|
+
async findServiceIdByName(cloudConnection, brokerName) {
|
|
168
|
+
try {
|
|
169
|
+
// Use customAttributes with RSQL filter to search by name
|
|
170
|
+
const endpoint = `/missionControl/eventBrokerServices?customAttributes=name==${brokerName}`;
|
|
171
|
+
const response = await cloudConnection.get(endpoint);
|
|
172
|
+
if (!response.data || response.data.length === 0) {
|
|
173
|
+
this.error(`Broker '${brokerName}' not found in Solace Cloud. Please check the broker name and try again.`, { exit: 1 });
|
|
174
|
+
}
|
|
175
|
+
if (response.data.length > 1) {
|
|
176
|
+
this.warn(`Multiple brokers found with name '${brokerName}'. Using the first match.`);
|
|
177
|
+
}
|
|
178
|
+
return response.data[0].id;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error instanceof Error && error.message.includes('401')) {
|
|
182
|
+
this.error('Cloud API authentication failed. Please run \'org:login\' again.', { exit: 1 });
|
|
183
|
+
}
|
|
184
|
+
if (error instanceof Error && error.message.includes('403')) {
|
|
185
|
+
this.error('Insufficient permissions to access broker information in Solace Cloud.', { exit: 1 });
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get authenticated Cloud API connection
|
|
192
|
+
* Validates that user is logged in to Solace Cloud
|
|
193
|
+
*/
|
|
194
|
+
async getCloudConnection(orgManager, orgName) {
|
|
195
|
+
try {
|
|
196
|
+
// If org-name provided, use that org; otherwise use default
|
|
197
|
+
const orgIdentifier = orgName ?? (await orgManager.getDefaultOrg())?.orgId;
|
|
198
|
+
if (!orgIdentifier) {
|
|
199
|
+
this.error('No Solace Cloud organization found. Please run \'org:login\' first to authenticate to Solace Cloud.', { exit: 1 });
|
|
200
|
+
}
|
|
201
|
+
// Create connection using org credentials
|
|
202
|
+
return await orgManager.createConnection(orgIdentifier);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof OrgError) {
|
|
206
|
+
if (error.code === OrgErrorCode.NOT_INITIALIZED) {
|
|
207
|
+
this.error('Solace Cloud authentication not initialized. Please run \'org:login\' first.', { exit: 1 });
|
|
208
|
+
}
|
|
209
|
+
if (error.code === OrgErrorCode.ORG_NOT_FOUND) {
|
|
210
|
+
this.error(`Organization '${orgName}' not found. Please run 'org:login' first.`, { exit: 1 });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get detailed service information with expanded fields
|
|
218
|
+
*/
|
|
219
|
+
async getServiceDetails(cloudConnection, serviceId) {
|
|
220
|
+
try {
|
|
221
|
+
const endpoint = `/missionControl/eventBrokerServices/${serviceId}?expand=broker,serviceConnectionEndpoints`;
|
|
222
|
+
const response = await cloudConnection.get(endpoint);
|
|
223
|
+
if (!response.data) {
|
|
224
|
+
this.error('Failed to retrieve broker details from Solace Cloud.', { exit: 1 });
|
|
225
|
+
}
|
|
226
|
+
return response.data;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (error instanceof Error && error.message.includes('404')) {
|
|
230
|
+
this.error('Broker not found in Solace Cloud.', { exit: 1 });
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Checks if broker exists and handles overwrite confirmation
|
|
237
|
+
* Same pattern as broker:login:basic
|
|
238
|
+
*/
|
|
239
|
+
async handleExistingBroker(brokerAuthManager, brokerName, noPrompt) {
|
|
240
|
+
const exists = await brokerAuthManager.brokerExists(brokerName);
|
|
241
|
+
if (!exists) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
// If no-prompt, assume yes to overwrite
|
|
245
|
+
if (noPrompt) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
const shouldOverwrite = await this.promptForConfirmation(`Broker '${brokerName}' already exists. Do you want to overwrite the credentials?`);
|
|
249
|
+
if (!shouldOverwrite) {
|
|
250
|
+
this.log('Login cancelled.');
|
|
251
|
+
this.exit(0);
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Handles errors during login process
|
|
257
|
+
*/
|
|
258
|
+
handleLoginError(error, brokerName) {
|
|
259
|
+
if (error instanceof BrokerAuthError) {
|
|
260
|
+
switch (error.code) {
|
|
261
|
+
case BrokerAuthErrorCode.FILE_WRITE_ERROR: {
|
|
262
|
+
this.error('Failed to save broker configuration. Please check file permissions.');
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case BrokerAuthErrorCode.INVALID_ACCESS_TOKEN: {
|
|
266
|
+
this.error('Failed to encode credentials.');
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case BrokerAuthErrorCode.INVALID_ENDPOINT: {
|
|
270
|
+
this.error('Invalid SEMP endpoint URL format.');
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
default: {
|
|
274
|
+
this.error(`Login failed: ${error.message}`);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (error instanceof OrgError) {
|
|
280
|
+
switch (error.code) {
|
|
281
|
+
case OrgErrorCode.INVALID_ACCESS_TOKEN: {
|
|
282
|
+
this.error('Cloud API token expired or invalid. Please run \'org:login\' again.');
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case OrgErrorCode.NOT_INITIALIZED: {
|
|
286
|
+
this.error('Solace Cloud authentication not initialized. Please run \'org:login\' first.');
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case OrgErrorCode.ORG_NOT_FOUND: {
|
|
290
|
+
this.error('Solace Cloud organization not found. Please run \'org:login\' first.');
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
default: {
|
|
294
|
+
this.error(`Cloud API error: ${error.message}`);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (error instanceof Error) {
|
|
300
|
+
// Check for network errors
|
|
301
|
+
if (error.message.includes('ENOTFOUND') || error.message.includes('ECONNREFUSED')) {
|
|
302
|
+
this.error('Failed to connect to Solace Cloud API. Please check your network connection.');
|
|
303
|
+
}
|
|
304
|
+
// Check for Cloud API errors
|
|
305
|
+
if (error.message.includes('404')) {
|
|
306
|
+
this.error(`Broker '${brokerName}' not found in Solace Cloud.`);
|
|
307
|
+
}
|
|
308
|
+
if (error.message.includes('401') || error.message.includes('403')) {
|
|
309
|
+
this.error('Cloud API authentication failed. Please run \'org:login\' again.');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Re-throw unexpected errors
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Prompts user for confirmation with Y/n (default Yes)
|
|
317
|
+
*/
|
|
318
|
+
async promptForConfirmation(message) {
|
|
319
|
+
const rl = readline.createInterface({
|
|
320
|
+
input: process.stdin,
|
|
321
|
+
output: process.stdout,
|
|
322
|
+
});
|
|
323
|
+
return new Promise((resolve) => {
|
|
324
|
+
rl.question(`${message} (Y/n): `, (answer) => {
|
|
325
|
+
rl.close();
|
|
326
|
+
const normalized = answer.trim().toLowerCase();
|
|
327
|
+
// Default to yes if empty, accept y/yes as confirmation
|
|
328
|
+
const confirmed = normalized === '' || normalized === 'y' || normalized === 'yes';
|
|
329
|
+
resolve(confirmed);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BrokerAuth, ScCommand } from '@dishantlangayan/sc-cli-core';
|
|
2
|
+
export default class BrokerLoginList extends ScCommand<typeof BrokerLoginList> {
|
|
3
|
+
static args: {};
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {};
|
|
7
|
+
run(): Promise<{
|
|
8
|
+
data: BrokerAuth[];
|
|
9
|
+
}>;
|
|
10
|
+
}
|