@dishantlangayan/sc-cli-core 0.3.1 → 0.5.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/README.md CHANGED
@@ -20,8 +20,13 @@ The ScCommand abstract class extends [@oclif/core's Command class](https://githu
20
20
  ## ScConnection Class
21
21
  The ScConnection class provide abstraction functions for Solace Cloud API REST calls. It handles the access token and base URL for each REST call, avoiding the need to set these on each Command.
22
22
 
23
+ ## OrgManager
24
+ The OrgManager class provides utility functions to store and retrieve Solace Cloud authentication information from user's home directory: `~/.sc/` or `%USERPROFILE%\sc\`. The implementation uses AES-256-GCM for authenticated encryption and provides machine-bound encryption that combines OS-level security (keychain) with machine-specific identifiers, making credentials non-transferable between machines.
25
+
26
+ Supports changing of the Solace Cloud REST API base url using the environment variable `SC_BASE_URL` and API version using `SC_API_VERSION`.
27
+
23
28
  ## BrokerAuthManager
24
- The BrokerAuthManager class provides utility functions to store and retrieve broker SEMP management authentication information from user's home directory: `~/.sc/` or `%USERPROFILE%\sc\`. The implementation uses AES-256-GCM for authenticated encryption and provides machine-bound encryption that combines OS-level security (keychain) with machine-specific identifiers, making credentials non-transferable between machines.
29
+ The BrokerAuthManager class provides utility functions to store and retrieve broker SEMP management authentication information similar to the `OrgManager` class. It supports Basic and OAuth authentication schemes.
25
30
 
26
31
  # Contributing
27
32
  Contributions are encouraged! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
@@ -12,12 +12,12 @@ export declare class BrokerAuthEncryption {
12
12
  private static readonly SALT_LENGTH;
13
13
  private static readonly TAG_LENGTH;
14
14
  /**
15
- * Decrypt broker storage data
15
+ * Decrypt storage data
16
16
  * @param encryptedData - Encrypted data to decrypt
17
17
  * @param key - Decryption key
18
- * @returns Decrypted broker storage
18
+ * @returns Decrypted storage
19
19
  */
20
- static decrypt(encryptedData: EncryptedData, key: Buffer): Promise<BrokerAuthStorage>;
20
+ static decrypt<T = BrokerAuthStorage>(encryptedData: EncryptedData, key: Buffer): Promise<T>;
21
21
  /**
22
22
  * Derive encryption key from password using PBKDF2
23
23
  * @param password - User password
@@ -26,12 +26,12 @@ export declare class BrokerAuthEncryption {
26
26
  */
27
27
  static deriveKey(password: string, salt: Buffer): Promise<Buffer>;
28
28
  /**
29
- * Encrypt broker storage data
30
- * @param data - Broker storage to encrypt
29
+ * Encrypt storage data
30
+ * @param data - Storage to encrypt
31
31
  * @param key - Encryption key
32
32
  * @returns Encrypted data with metadata
33
33
  */
34
- static encrypt(data: BrokerAuthStorage, key: Buffer): Promise<EncryptedData>;
34
+ static encrypt<T = BrokerAuthStorage>(data: T, key: Buffer): Promise<EncryptedData>;
35
35
  /**
36
36
  * Generate cryptographically secure random salt
37
37
  * @returns Random salt buffer
@@ -15,10 +15,10 @@ export class BrokerAuthEncryption {
15
15
  static SALT_LENGTH = 32;
16
16
  static TAG_LENGTH = 16;
17
17
  /**
18
- * Decrypt broker storage data
18
+ * Decrypt storage data
19
19
  * @param encryptedData - Encrypted data to decrypt
20
20
  * @param key - Decryption key
21
- * @returns Decrypted broker storage
21
+ * @returns Decrypted storage
22
22
  */
23
23
  static async decrypt(encryptedData, key) {
24
24
  try {
@@ -60,8 +60,8 @@ export class BrokerAuthEncryption {
60
60
  }
61
61
  }
62
62
  /**
63
- * Encrypt broker storage data
64
- * @param data - Broker storage to encrypt
63
+ * Encrypt storage data
64
+ * @param data - Storage to encrypt
65
65
  * @param key - Encryption key
66
66
  * @returns Encrypted data with metadata
67
67
  */
@@ -85,8 +85,11 @@ export class BrokerAuthManager {
85
85
  }
86
86
  const baseURL = `${broker.sempEndpoint}:${broker.sempPort}`;
87
87
  const accessToken = broker.authType === AuthType.OAUTH ? broker.accessToken : broker.encodedCredentials;
88
- const isBasic = broker.authType === AuthType.BASIC;
89
- return new ScConnection(baseURL, accessToken, timeout, isBasic);
88
+ return new ScConnection(baseURL, accessToken, {
89
+ apiType: 'semp',
90
+ authType: broker.authType === AuthType.BASIC ? 'basic' : 'bearer',
91
+ timeout,
92
+ });
90
93
  }
91
94
  /**
92
95
  * Get all broker configurations
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Broker authentication management module
3
- * Provides encrypted storage for SEMP broker credentials
2
+ * Authentication management module
3
+ * Provides encrypted storage for broker and organization credentials
4
4
  */
5
5
  export { BrokerAuthEncryption } from './auth-encryption.js';
6
6
  export { BrokerAuthManager } from './auth-manager.js';
7
7
  export { AuthType, type BasicBrokerAuth, type BrokerAuth, type BrokerAuthBase, BrokerAuthError, BrokerAuthErrorCode, type BrokerAuthStorage, type EncryptedData, type EncryptionMetadata, type OAuthBrokerAuth, } from './auth-types.js';
8
+ export { OrgManager } from './org-manager.js';
9
+ export { type OrgConfig, OrgError, OrgErrorCode, type OrgStorage } from './org-types.js';
package/lib/auth/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Broker authentication management module
3
- * Provides encrypted storage for SEMP broker credentials
2
+ * Authentication management module
3
+ * Provides encrypted storage for broker and organization credentials
4
4
  */
5
5
  export { BrokerAuthEncryption } from './auth-encryption.js';
6
6
  export { BrokerAuthManager } from './auth-manager.js';
7
7
  export { AuthType, BrokerAuthError, BrokerAuthErrorCode, } from './auth-types.js';
8
+ export { OrgManager } from './org-manager.js';
9
+ export { OrgError, OrgErrorCode } from './org-types.js';
@@ -65,7 +65,6 @@ export class KeychainService {
65
65
  */
66
66
  loadMachineId() {
67
67
  // Use require for CommonJS module (node-machine-id)
68
- // eslint-disable-next-line @typescript-eslint/no-require-imports
69
68
  return require('node-machine-id');
70
69
  }
71
70
  }
@@ -0,0 +1,112 @@
1
+ import { ScConnection } from '../util/sc-connection.js';
2
+ import { KeychainService } from './keychain.js';
3
+ import { type OrgConfig } from './org-types.js';
4
+ /**
5
+ * Manager for organization storage
6
+ * Handles encrypted storage of Solace Cloud organization credentials
7
+ */
8
+ export declare class OrgManager {
9
+ private static instance;
10
+ private readonly configDir;
11
+ private readonly configFile;
12
+ private encryptionKey;
13
+ private readonly keychainService;
14
+ private machineId;
15
+ private masterKey;
16
+ private storage;
17
+ private constructor();
18
+ /**
19
+ * Get singleton instance
20
+ * @param keychainService - Optional keychain service for testing
21
+ */
22
+ static getInstance(keychainService?: KeychainService): OrgManager;
23
+ /**
24
+ * Add a new organization configuration
25
+ * @param org - Organization configuration
26
+ */
27
+ addOrg(org: OrgConfig): Promise<void>;
28
+ /**
29
+ * Clear all organization configurations
30
+ */
31
+ clearAll(): Promise<void>;
32
+ /**
33
+ * Create ScConnection instance from stored org config
34
+ * @param identifier - Organization ID or alias
35
+ * @param timeout - Optional timeout override (default: 10000ms)
36
+ * @returns Configured ScConnection instance
37
+ */
38
+ createConnection(identifier: string, timeout?: number): Promise<ScConnection>;
39
+ /**
40
+ * Get all organization configurations
41
+ * @returns Array of all organization configurations
42
+ */
43
+ getAllOrgs(): Promise<OrgConfig[]>;
44
+ /**
45
+ * Get the default organization
46
+ * @returns Default organization or null if no default is set
47
+ */
48
+ getDefaultOrg(): Promise<null | OrgConfig>;
49
+ /**
50
+ * Get organization configuration by orgId or alias
51
+ * @param identifier - Organization ID or alias
52
+ * @returns Organization configuration or null if not found
53
+ */
54
+ getOrg(identifier: string): Promise<null | OrgConfig>;
55
+ /**
56
+ * Initialize the org manager with encryption key derived from OS keychain and machine ID
57
+ */
58
+ initialize(): Promise<void>;
59
+ /**
60
+ * List all organization identifiers (orgId or alias if available)
61
+ * @returns Array of organization identifiers
62
+ */
63
+ listOrgs(): Promise<string[]>;
64
+ /**
65
+ * Check if organization exists
66
+ * @param identifier - Organization ID or alias
67
+ * @returns true if organization exists
68
+ */
69
+ orgExists(identifier: string): Promise<boolean>;
70
+ /**
71
+ * Remove organization configuration
72
+ * @param identifier - Organization ID or alias to remove
73
+ */
74
+ removeOrg(identifier: string): Promise<void>;
75
+ /**
76
+ * Set an organization as the default
77
+ * @param identifier - Organization ID or alias to set as default
78
+ */
79
+ setDefaultOrg(identifier: string): Promise<void>;
80
+ /**
81
+ * Update existing organization configuration
82
+ * @param identifier - Organization ID or alias to update
83
+ * @param updates - Partial updates to apply
84
+ */
85
+ updateOrg(identifier: string, updates: Partial<Omit<OrgConfig, 'orgId'>>): Promise<void>;
86
+ /**
87
+ * Ensure manager is initialized
88
+ */
89
+ private ensureInitialized;
90
+ /**
91
+ * Check if config file exists
92
+ */
93
+ private fileExists;
94
+ /**
95
+ * Load storage from encrypted file
96
+ * @param combinedKey - Combined master key and machine ID for decryption
97
+ */
98
+ private loadStorage;
99
+ /**
100
+ * Save storage to encrypted file
101
+ */
102
+ private saveStorage;
103
+ /**
104
+ * Unset the default flag on all organizations
105
+ */
106
+ private unsetAllDefaults;
107
+ /**
108
+ * Validate organization configuration
109
+ * @param org - Organization to validate
110
+ */
111
+ private validateOrg;
112
+ }
@@ -0,0 +1,380 @@
1
+ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { DefaultBaseUrl, EnvironmentVariable, envVars } from '../config/env-vars.js';
5
+ import { ScConnection } from '../util/sc-connection.js';
6
+ import { BrokerAuthEncryption } from './auth-encryption.js';
7
+ import { KeychainService } from './keychain.js';
8
+ import { OrgError, OrgErrorCode } from './org-types.js';
9
+ const SERVICE_NAME = 'local';
10
+ const KEY_NAME = 'sc-cli';
11
+ /**
12
+ * Manager for organization storage
13
+ * Handles encrypted storage of Solace Cloud organization credentials
14
+ */
15
+ export class OrgManager {
16
+ static instance = null;
17
+ configDir;
18
+ configFile;
19
+ encryptionKey = null;
20
+ keychainService;
21
+ machineId = null;
22
+ masterKey = null;
23
+ storage = null;
24
+ constructor(keychainService) {
25
+ const homeDirectory = homedir();
26
+ this.configDir = join(homeDirectory, '.sc');
27
+ this.configFile = join(this.configDir, 'orgs.json');
28
+ this.keychainService = keychainService ?? new KeychainService();
29
+ }
30
+ /**
31
+ * Get singleton instance
32
+ * @param keychainService - Optional keychain service for testing
33
+ */
34
+ static getInstance(keychainService) {
35
+ if (!OrgManager.instance) {
36
+ OrgManager.instance = new OrgManager(keychainService);
37
+ }
38
+ return OrgManager.instance;
39
+ }
40
+ /**
41
+ * Add a new organization configuration
42
+ * @param org - Organization configuration
43
+ */
44
+ async addOrg(org) {
45
+ this.ensureInitialized();
46
+ // Validate organization
47
+ this.validateOrg(org);
48
+ // Check if organization already exists (by orgId or alias)
49
+ const existingByOrgId = this.storage.orgs.find((o) => o.orgId === org.orgId);
50
+ if (existingByOrgId) {
51
+ throw new OrgError(`Organization '${org.orgId}' already exists`, OrgErrorCode.ORG_ALREADY_EXISTS);
52
+ }
53
+ if (org.alias) {
54
+ const existingByAlias = this.storage.orgs.find((o) => o.alias === org.alias);
55
+ if (existingByAlias) {
56
+ throw new OrgError(`Organization with alias '${org.alias}' already exists`, OrgErrorCode.ORG_ALREADY_EXISTS);
57
+ }
58
+ }
59
+ // If this org is being set as default, unset any existing default
60
+ if (org.isDefault) {
61
+ this.unsetAllDefaults();
62
+ }
63
+ // Add organization
64
+ this.storage.orgs.push(org);
65
+ // Save to file
66
+ await this.saveStorage();
67
+ }
68
+ /**
69
+ * Clear all organization configurations
70
+ */
71
+ async clearAll() {
72
+ this.ensureInitialized();
73
+ this.storage.orgs = [];
74
+ await this.saveStorage();
75
+ }
76
+ /**
77
+ * Create ScConnection instance from stored org config
78
+ * @param identifier - Organization ID or alias
79
+ * @param timeout - Optional timeout override (default: 10000ms)
80
+ * @returns Configured ScConnection instance
81
+ */
82
+ async createConnection(identifier, timeout = 10_000) {
83
+ this.ensureInitialized();
84
+ const org = await this.getOrg(identifier);
85
+ if (!org) {
86
+ throw new OrgError(`Organization '${identifier}' not found`, OrgErrorCode.ORG_NOT_FOUND);
87
+ }
88
+ // Get base URL: org config → env var → default
89
+ const baseURL = org.baseUrl ?? envVars.getString(EnvironmentVariable.SC_BASE_URL, DefaultBaseUrl);
90
+ // Get API version: org config → env var → default 'v2'
91
+ const apiVersion = org.apiVersion ?? envVars.getString(EnvironmentVariable.SC_API_VERSION, 'v2');
92
+ return new ScConnection(baseURL, org.accessToken, {
93
+ apiType: 'cloud',
94
+ apiVersion,
95
+ authType: 'bearer',
96
+ timeout,
97
+ });
98
+ }
99
+ /**
100
+ * Get all organization configurations
101
+ * @returns Array of all organization configurations
102
+ */
103
+ async getAllOrgs() {
104
+ this.ensureInitialized();
105
+ return [...this.storage.orgs];
106
+ }
107
+ /**
108
+ * Get the default organization
109
+ * @returns Default organization or null if no default is set
110
+ */
111
+ async getDefaultOrg() {
112
+ this.ensureInitialized();
113
+ const org = this.storage.orgs.find((o) => o.isDefault === true);
114
+ return org ?? null;
115
+ }
116
+ /**
117
+ * Get organization configuration by orgId or alias
118
+ * @param identifier - Organization ID or alias
119
+ * @returns Organization configuration or null if not found
120
+ */
121
+ async getOrg(identifier) {
122
+ this.ensureInitialized();
123
+ const org = this.storage.orgs.find((o) => o.orgId === identifier || o.alias === identifier);
124
+ return org ?? null;
125
+ }
126
+ /**
127
+ * Initialize the org manager with encryption key derived from OS keychain and machine ID
128
+ */
129
+ async initialize() {
130
+ try {
131
+ // Get machine ID
132
+ this.machineId = this.keychainService.getMachineId();
133
+ // Get or create master key from OS keychain
134
+ this.masterKey = await this.keychainService.getPassword(KEY_NAME, SERVICE_NAME);
135
+ if (!this.masterKey) {
136
+ // Generate new master key and store in OS keychain
137
+ this.masterKey = this.keychainService.generateMasterKey();
138
+ await this.keychainService.setPassword(KEY_NAME, SERVICE_NAME, this.masterKey);
139
+ }
140
+ // Combine master key with machine ID for encryption
141
+ const combinedKey = `${this.masterKey}:${this.machineId}`;
142
+ // Try to load existing storage
143
+ const fileExists = await this.fileExists();
144
+ if (fileExists) {
145
+ // Load existing file and derive key from stored salt
146
+ await this.loadStorage(combinedKey);
147
+ }
148
+ else {
149
+ // Create new storage with new salt
150
+ const salt = BrokerAuthEncryption.generateSalt();
151
+ this.encryptionKey = await BrokerAuthEncryption.deriveKey(combinedKey, salt);
152
+ this.storage = {
153
+ orgs: [],
154
+ version: '1.0.0',
155
+ };
156
+ }
157
+ }
158
+ catch (error) {
159
+ if (error instanceof OrgError) {
160
+ throw error;
161
+ }
162
+ throw new OrgError('Failed to initialize org manager', OrgErrorCode.NOT_INITIALIZED, error);
163
+ }
164
+ }
165
+ /**
166
+ * List all organization identifiers (orgId or alias if available)
167
+ * @returns Array of organization identifiers
168
+ */
169
+ async listOrgs() {
170
+ this.ensureInitialized();
171
+ return this.storage.orgs.map((o) => o.alias ?? o.orgId);
172
+ }
173
+ /**
174
+ * Check if organization exists
175
+ * @param identifier - Organization ID or alias
176
+ * @returns true if organization exists
177
+ */
178
+ async orgExists(identifier) {
179
+ this.ensureInitialized();
180
+ return this.storage.orgs.some((o) => o.orgId === identifier || o.alias === identifier);
181
+ }
182
+ /**
183
+ * Remove organization configuration
184
+ * @param identifier - Organization ID or alias to remove
185
+ */
186
+ async removeOrg(identifier) {
187
+ this.ensureInitialized();
188
+ const index = this.storage.orgs.findIndex((o) => o.orgId === identifier || o.alias === identifier);
189
+ if (index === -1) {
190
+ throw new OrgError(`Organization '${identifier}' not found`, OrgErrorCode.ORG_NOT_FOUND);
191
+ }
192
+ // Remove organization
193
+ this.storage.orgs.splice(index, 1);
194
+ // Save to file
195
+ await this.saveStorage();
196
+ }
197
+ /**
198
+ * Set an organization as the default
199
+ * @param identifier - Organization ID or alias to set as default
200
+ */
201
+ async setDefaultOrg(identifier) {
202
+ this.ensureInitialized();
203
+ const index = this.storage.orgs.findIndex((o) => o.orgId === identifier || o.alias === identifier);
204
+ if (index === -1) {
205
+ throw new OrgError(`Organization '${identifier}' not found`, OrgErrorCode.ORG_NOT_FOUND);
206
+ }
207
+ // Unset all existing defaults
208
+ this.unsetAllDefaults();
209
+ // Set this org as default
210
+ this.storage.orgs[index].isDefault = true;
211
+ // Save to file
212
+ await this.saveStorage();
213
+ }
214
+ /**
215
+ * Update existing organization configuration
216
+ * @param identifier - Organization ID or alias to update
217
+ * @param updates - Partial updates to apply
218
+ */
219
+ async updateOrg(identifier, updates) {
220
+ this.ensureInitialized();
221
+ const index = this.storage.orgs.findIndex((o) => o.orgId === identifier || o.alias === identifier);
222
+ if (index === -1) {
223
+ throw new OrgError(`Organization '${identifier}' not found`, OrgErrorCode.ORG_NOT_FOUND);
224
+ }
225
+ const currentOrg = this.storage.orgs[index];
226
+ // Check if new alias conflicts with another org
227
+ if (updates.alias && updates.alias !== currentOrg.alias) {
228
+ const conflictingOrg = this.storage.orgs.find((o) => o.alias === updates.alias && o.orgId !== currentOrg.orgId);
229
+ if (conflictingOrg) {
230
+ throw new OrgError(`Organization with alias '${updates.alias}' already exists`, OrgErrorCode.ORG_ALREADY_EXISTS);
231
+ }
232
+ }
233
+ // If setting this org as default, unset any existing default
234
+ if (updates.isDefault === true) {
235
+ this.unsetAllDefaults();
236
+ }
237
+ // Merge updates
238
+ const updated = {
239
+ ...currentOrg,
240
+ ...updates,
241
+ };
242
+ // Validate updated organization
243
+ this.validateOrg(updated);
244
+ // Update organization
245
+ this.storage.orgs[index] = updated;
246
+ // Save to file
247
+ await this.saveStorage();
248
+ }
249
+ /**
250
+ * Ensure manager is initialized
251
+ */
252
+ ensureInitialized() {
253
+ if (!this.encryptionKey || !this.storage) {
254
+ throw new OrgError('OrgManager not initialized. Call initialize() first.', OrgErrorCode.NOT_INITIALIZED);
255
+ }
256
+ }
257
+ /**
258
+ * Check if config file exists
259
+ */
260
+ async fileExists() {
261
+ try {
262
+ await readFile(this.configFile);
263
+ return true;
264
+ }
265
+ catch {
266
+ return false;
267
+ }
268
+ }
269
+ /**
270
+ * Load storage from encrypted file
271
+ * @param combinedKey - Combined master key and machine ID for decryption
272
+ */
273
+ async loadStorage(combinedKey) {
274
+ try {
275
+ const fileContent = await readFile(this.configFile, 'utf8');
276
+ const encryptedData = JSON.parse(fileContent);
277
+ // Derive key from combined key and stored salt
278
+ const salt = Buffer.from(encryptedData.salt, 'base64');
279
+ this.encryptionKey = await BrokerAuthEncryption.deriveKey(combinedKey, salt);
280
+ // Decrypt storage
281
+ this.storage = await BrokerAuthEncryption.decrypt(encryptedData, this.encryptionKey);
282
+ }
283
+ catch (error) {
284
+ if (error instanceof OrgError) {
285
+ throw error;
286
+ }
287
+ throw new OrgError('Failed to load organization storage', OrgErrorCode.FILE_READ_ERROR, error);
288
+ }
289
+ }
290
+ /**
291
+ * Save storage to encrypted file
292
+ */
293
+ async saveStorage() {
294
+ try {
295
+ if (!this.masterKey || !this.machineId) {
296
+ throw new OrgError('Org manager not initialized', OrgErrorCode.NOT_INITIALIZED);
297
+ }
298
+ // Ensure directory exists
299
+ await mkdir(this.configDir, { mode: 0o700, recursive: true });
300
+ // Generate new salt and derive key for THIS save
301
+ const combinedKey = `${this.masterKey}:${this.machineId}`;
302
+ const newSalt = BrokerAuthEncryption.generateSalt();
303
+ const newKey = await BrokerAuthEncryption.deriveKey(combinedKey, newSalt);
304
+ // Encrypt data with the new key
305
+ const encrypted = await BrokerAuthEncryption.encrypt(this.storage, newKey);
306
+ // Update the salt in encrypted data to match the salt we used for key derivation
307
+ encrypted.salt = newSalt.toString('base64');
308
+ // Store the new key for next operation
309
+ this.encryptionKey = newKey;
310
+ // Write to temp file first (atomic write)
311
+ const jsonData = JSON.stringify(encrypted, null, 2);
312
+ const tempFile = `${this.configFile}.tmp`;
313
+ await writeFile(tempFile, jsonData, { mode: 0o600 });
314
+ // Atomic rename
315
+ await rename(tempFile, this.configFile);
316
+ // Set restrictive permissions (Unix only)
317
+ if (process.platform !== 'win32') {
318
+ // Already set via writeFile mode option
319
+ }
320
+ }
321
+ catch (error) {
322
+ // Clean up temp file if it exists
323
+ try {
324
+ await unlink(`${this.configFile}.tmp`);
325
+ }
326
+ catch {
327
+ // Ignore cleanup errors
328
+ }
329
+ if (error instanceof OrgError) {
330
+ throw error;
331
+ }
332
+ throw new OrgError('Failed to save organization storage', OrgErrorCode.FILE_WRITE_ERROR, error);
333
+ }
334
+ }
335
+ /**
336
+ * Unset the default flag on all organizations
337
+ */
338
+ unsetAllDefaults() {
339
+ for (const org of this.storage.orgs) {
340
+ org.isDefault = false;
341
+ }
342
+ }
343
+ /**
344
+ * Validate organization configuration
345
+ * @param org - Organization to validate
346
+ */
347
+ validateOrg(org) {
348
+ // Validate orgId
349
+ if (!org.orgId || org.orgId.trim() === '') {
350
+ throw new OrgError('Organization ID is required', OrgErrorCode.INVALID_ORG_ID);
351
+ }
352
+ // Validate accessToken
353
+ if (!org.accessToken || org.accessToken.trim() === '') {
354
+ throw new OrgError('Access token is required', OrgErrorCode.INVALID_ACCESS_TOKEN);
355
+ }
356
+ // Validate alias if provided
357
+ if (org.alias !== undefined && org.alias.trim() === '') {
358
+ throw new OrgError('Alias cannot be empty if provided', OrgErrorCode.INVALID_ORG_ID);
359
+ }
360
+ // Validate apiVersion if provided
361
+ if (org.apiVersion !== undefined && org.apiVersion.trim() === '') {
362
+ throw new OrgError('API version cannot be empty if provided', OrgErrorCode.INVALID_API_VERSION);
363
+ }
364
+ // Validate baseUrl if provided
365
+ if (org.baseUrl !== undefined) {
366
+ // Must not be empty or whitespace
367
+ if (org.baseUrl.trim() === '') {
368
+ throw new OrgError('Base URL cannot be empty if provided', OrgErrorCode.INVALID_BASE_URL);
369
+ }
370
+ // Must start with http:// or https://
371
+ if (!(org.baseUrl.startsWith('http://') || org.baseUrl.startsWith('https://'))) {
372
+ throw new OrgError('Base URL must start with http:// or https://', OrgErrorCode.INVALID_BASE_URL);
373
+ }
374
+ // Should not end with trailing slash
375
+ if (org.baseUrl.endsWith('/')) {
376
+ throw new OrgError('Base URL should not end with a slash', OrgErrorCode.INVALID_BASE_URL);
377
+ }
378
+ }
379
+ }
380
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Organization configuration
3
+ */
4
+ export interface OrgConfig {
5
+ accessToken: string;
6
+ alias?: string;
7
+ apiVersion?: string;
8
+ baseUrl?: string;
9
+ isDefault?: boolean;
10
+ orgId: string;
11
+ }
12
+ /**
13
+ * Storage format for organization configurations
14
+ */
15
+ export interface OrgStorage {
16
+ orgs: OrgConfig[];
17
+ version: string;
18
+ }
19
+ /**
20
+ * Error codes for organization operations
21
+ */
22
+ export declare enum OrgErrorCode {
23
+ DECRYPTION_FAILED = "DECRYPTION_FAILED",
24
+ ENCRYPTION_FAILED = "ENCRYPTION_FAILED",
25
+ FILE_READ_ERROR = "FILE_READ_ERROR",
26
+ FILE_WRITE_ERROR = "FILE_WRITE_ERROR",
27
+ INVALID_ACCESS_TOKEN = "INVALID_ACCESS_TOKEN",
28
+ INVALID_API_VERSION = "INVALID_API_VERSION",
29
+ INVALID_BASE_URL = "INVALID_BASE_URL",
30
+ INVALID_ORG_ID = "INVALID_ORG_ID",
31
+ NOT_INITIALIZED = "NOT_INITIALIZED",
32
+ ORG_ALREADY_EXISTS = "ORG_ALREADY_EXISTS",
33
+ ORG_NOT_FOUND = "ORG_NOT_FOUND"
34
+ }
35
+ /**
36
+ * Custom error class for organization operations
37
+ */
38
+ export declare class OrgError extends Error {
39
+ readonly code: OrgErrorCode;
40
+ readonly cause?: Error | undefined;
41
+ constructor(message: string, code: OrgErrorCode, cause?: Error | undefined);
42
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Error codes for organization operations
3
+ */
4
+ export var OrgErrorCode;
5
+ (function (OrgErrorCode) {
6
+ OrgErrorCode["DECRYPTION_FAILED"] = "DECRYPTION_FAILED";
7
+ OrgErrorCode["ENCRYPTION_FAILED"] = "ENCRYPTION_FAILED";
8
+ OrgErrorCode["FILE_READ_ERROR"] = "FILE_READ_ERROR";
9
+ OrgErrorCode["FILE_WRITE_ERROR"] = "FILE_WRITE_ERROR";
10
+ OrgErrorCode["INVALID_ACCESS_TOKEN"] = "INVALID_ACCESS_TOKEN";
11
+ OrgErrorCode["INVALID_API_VERSION"] = "INVALID_API_VERSION";
12
+ OrgErrorCode["INVALID_BASE_URL"] = "INVALID_BASE_URL";
13
+ OrgErrorCode["INVALID_ORG_ID"] = "INVALID_ORG_ID";
14
+ OrgErrorCode["NOT_INITIALIZED"] = "NOT_INITIALIZED";
15
+ OrgErrorCode["ORG_ALREADY_EXISTS"] = "ORG_ALREADY_EXISTS";
16
+ OrgErrorCode["ORG_NOT_FOUND"] = "ORG_NOT_FOUND";
17
+ })(OrgErrorCode || (OrgErrorCode = {}));
18
+ /**
19
+ * Custom error class for organization operations
20
+ */
21
+ export class OrgError extends Error {
22
+ code;
23
+ cause;
24
+ constructor(message, code, cause) {
25
+ super(message);
26
+ this.code = code;
27
+ this.cause = cause;
28
+ this.name = 'OrgError';
29
+ }
30
+ }
@@ -1,8 +1,7 @@
1
1
  export declare enum EnvironmentVariable {
2
2
  'SC_ACCESS_TOKEN' = "SC_ACCESS_TOKEN",
3
3
  'SC_API_VERSION' = "SC_API_VERSION",
4
- 'SC_BASE_URL' = "SC_BASE_URL",
5
- 'SEMP_API_VERSION' = "SEMP_API_VERSION"
4
+ 'SC_BASE_URL' = "SC_BASE_URL"
6
5
  }
7
6
  export declare const DefaultBaseUrl = "https://api.solace.cloud";
8
7
  /**
@@ -3,7 +3,6 @@ export var EnvironmentVariable;
3
3
  EnvironmentVariable["SC_ACCESS_TOKEN"] = "SC_ACCESS_TOKEN";
4
4
  EnvironmentVariable["SC_API_VERSION"] = "SC_API_VERSION";
5
5
  EnvironmentVariable["SC_BASE_URL"] = "SC_BASE_URL";
6
- EnvironmentVariable["SEMP_API_VERSION"] = "SEMP_API_VERSION";
7
6
  })(EnvironmentVariable || (EnvironmentVariable = {}));
8
7
  export const DefaultBaseUrl = 'https://api.solace.cloud';
9
8
  /**
package/lib/exported.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { AuthType, type BasicBrokerAuth, type BrokerAuth, BrokerAuthError, BrokerAuthErrorCode, BrokerAuthManager, type OAuthBrokerAuth, } from './auth/index.js';
1
+ export { AuthType, type BasicBrokerAuth, type BrokerAuth, BrokerAuthError, BrokerAuthErrorCode, BrokerAuthManager, type OAuthBrokerAuth, type OrgConfig, OrgError, OrgErrorCode, OrgManager, type OrgStorage, } from './auth/index.js';
2
2
  export { EnvironmentVariable, envVars } from './config/env-vars.js';
3
3
  export { ScCommand } from './sc-command.js';
4
- export { ScConnection } from './util/sc-connection.js';
4
+ export { type ApiType, type HttpAuthType, ScConnection, type ScConnectionOptions } from './util/sc-connection.js';
5
5
  export { sleep } from './util/util.js';
6
6
  export * from './ux/table.js';
package/lib/exported.js CHANGED
@@ -1,4 +1,4 @@
1
- export { AuthType, BrokerAuthError, BrokerAuthErrorCode, BrokerAuthManager, } from './auth/index.js';
1
+ export { AuthType, BrokerAuthError, BrokerAuthErrorCode, BrokerAuthManager, OrgError, OrgErrorCode, OrgManager, } from './auth/index.js';
2
2
  export { EnvironmentVariable, envVars } from './config/env-vars.js';
3
3
  export { ScCommand } from './sc-command.js';
4
4
  export { ScConnection } from './util/sc-connection.js';
@@ -1,4 +1,6 @@
1
1
  import { Command, Interfaces } from '@oclif/core';
2
+ import { BrokerAuthManager } from './auth/auth-manager.js';
3
+ import { OrgManager } from './auth/org-manager.js';
2
4
  export type Flags<T extends typeof Command> = Interfaces.InferredFlags<(typeof ScCommand)['baseFlags'] & T['flags']>;
3
5
  export type Args<T extends typeof Command> = Interfaces.InferredArgs<T['args']>;
4
6
  /**
@@ -14,9 +16,21 @@ export declare abstract class ScCommand<T extends typeof Command> extends Comman
14
16
  static enableJsonFlag: boolean;
15
17
  protected args: Args<T>;
16
18
  protected flags: Flags<T>;
19
+ private _brokerAuthManager?;
20
+ private _orgManager?;
17
21
  protected catch(err: Error & {
18
22
  exitCode?: number;
19
23
  }): Promise<unknown>;
20
24
  protected finally(_: Error | undefined): Promise<unknown>;
25
+ /**
26
+ * Get BrokerAuthManager instance with lazy initialization
27
+ * @returns Initialized BrokerAuthManager instance
28
+ */
29
+ protected getBrokerAuthManager(): Promise<BrokerAuthManager>;
30
+ /**
31
+ * Get OrgManager instance with lazy initialization
32
+ * @returns Initialized OrgManager instance
33
+ */
34
+ protected getOrgManager(): Promise<OrgManager>;
21
35
  init(): Promise<void>;
22
36
  }
package/lib/sc-command.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
+ import { BrokerAuthManager } from './auth/auth-manager.js';
3
+ import { OrgManager } from './auth/org-manager.js';
2
4
  import { DefaultBaseUrl, EnvironmentVariable, envVars } from './config/env-vars.js';
3
5
  /**
4
6
  * A base command that provided common functionality for all sc commands.
@@ -20,6 +22,8 @@ export class ScCommand extends Command {
20
22
  static enableJsonFlag = true;
21
23
  args;
22
24
  flags;
25
+ _brokerAuthManager;
26
+ _orgManager;
23
27
  async catch(err) {
24
28
  // add any custom logic to handle errors from the command
25
29
  // or simply return the parent class error handling
@@ -29,6 +33,28 @@ export class ScCommand extends Command {
29
33
  // called after run and catch regardless of whether or not the command errored
30
34
  return super.finally(_);
31
35
  }
36
+ /**
37
+ * Get BrokerAuthManager instance with lazy initialization
38
+ * @returns Initialized BrokerAuthManager instance
39
+ */
40
+ async getBrokerAuthManager() {
41
+ if (!this._brokerAuthManager) {
42
+ this._brokerAuthManager = BrokerAuthManager.getInstance();
43
+ await this._brokerAuthManager.initialize();
44
+ }
45
+ return this._brokerAuthManager;
46
+ }
47
+ /**
48
+ * Get OrgManager instance with lazy initialization
49
+ * @returns Initialized OrgManager instance
50
+ */
51
+ async getOrgManager() {
52
+ if (!this._orgManager) {
53
+ this._orgManager = OrgManager.getInstance();
54
+ await this._orgManager.initialize();
55
+ }
56
+ return this._orgManager;
57
+ }
32
58
  async init() {
33
59
  await super.init();
34
60
  const { args, flags } = await this.parse({
@@ -1,8 +1,16 @@
1
1
  import { AxiosRequestConfig } from 'axios';
2
+ export type HttpAuthType = 'basic' | 'bearer';
3
+ export type ApiType = 'cloud' | 'semp';
4
+ export interface ScConnectionOptions {
5
+ apiType?: ApiType;
6
+ apiVersion?: string;
7
+ authType?: HttpAuthType;
8
+ timeout?: number;
9
+ }
2
10
  export declare class ScConnection {
3
11
  private axiosInstance;
4
12
  private endpointUrl;
5
- constructor(baseURL?: string, accessToken?: string, timeout?: number, basic?: boolean);
13
+ constructor(baseURL: string, accessToken: string, options?: ScConnectionOptions);
6
14
  delete<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
7
15
  get<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
8
16
  patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>;
@@ -1,23 +1,33 @@
1
1
  import axios from 'axios';
2
- import { DefaultBaseUrl, EnvironmentVariable, envVars } from '../config/env-vars.js';
3
2
  export class ScConnection {
4
3
  axiosInstance;
5
4
  endpointUrl = '';
6
- constructor(baseURL = envVars.getString(EnvironmentVariable.SC_BASE_URL, DefaultBaseUrl), accessToken = envVars.getString(EnvironmentVariable.SC_ACCESS_TOKEN, ''), timeout = 10_000, basic = false) {
7
- const apiVersion = envVars.getString(EnvironmentVariable.SC_API_VERSION, 'v2');
8
- const sempApiVersion = envVars.getString(EnvironmentVariable.SEMP_API_VERSION, 'v2');
9
- this.endpointUrl = basic
10
- ? this.joinPaths(baseURL, `/SEMP/${sempApiVersion}`)
11
- : this.joinPaths(baseURL, `/api/${apiVersion}`);
5
+ constructor(baseURL, accessToken, options) {
6
+ // Validate required parameters
7
+ if (!baseURL || baseURL.trim() === '') {
8
+ throw new Error('baseURL is required and cannot be empty');
9
+ }
10
+ if (!accessToken || accessToken.trim() === '') {
11
+ throw new Error('accessToken is required and cannot be empty');
12
+ }
13
+ // Extract options with defaults
14
+ const { apiType = 'cloud', apiVersion = 'v2', authType = 'bearer', timeout = 10_000 } = options ?? {};
15
+ // Build endpoint URL based on apiType
16
+ // For SEMP: use baseURL directly (version specified in actual API calls)
17
+ // For cloud: append /api/{version}
18
+ const apiPath = apiType === 'semp' ? '' : `/api/${apiVersion}`;
19
+ this.endpointUrl = apiPath ? this.joinPaths(baseURL, apiPath) : baseURL;
20
+ // Build authorization header based on authType
21
+ const authHeader = authType === 'basic' ? `Basic ${accessToken}` : `Bearer ${accessToken}`;
12
22
  this.axiosInstance = axios.create({
13
23
  baseURL: this.endpointUrl,
14
24
  headers: {
15
- Authorization: basic ? `Basic ${accessToken}` : `Bearer ${accessToken}`,
25
+ Authorization: authHeader,
16
26
  'Content-Type': 'application/json',
17
27
  },
18
28
  timeout,
19
29
  });
20
- // Add interceptors if needed
30
+ // Add interceptors
21
31
  this.axiosInstance.interceptors.response.use((response) => response, (error) => {
22
32
  console.error('API Error:', error.response?.data || error.message);
23
33
  return Promise.reject(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dishantlangayan/sc-cli-core",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Base library for the Solace Cloud CLI and plugins",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dishant Langayan",