@dishantlangayan/sc-cli-core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/lib/auth/auth-encryption.d.ts +6 -6
- package/lib/auth/auth-encryption.js +4 -4
- package/lib/auth/keychain.js +0 -1
- package/lib/auth/org-manager.d.ts +104 -0
- package/lib/auth/org-manager.js +336 -0
- package/lib/auth/org-types.d.ts +38 -0
- package/lib/auth/org-types.js +28 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,8 +20,11 @@ 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
|
+
|
|
23
26
|
## BrokerAuthManager
|
|
24
|
-
The BrokerAuthManager class provides utility functions to store and retrieve broker SEMP management authentication information
|
|
27
|
+
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
28
|
|
|
26
29
|
# Contributing
|
|
27
30
|
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
|
|
15
|
+
* Decrypt storage data
|
|
16
16
|
* @param encryptedData - Encrypted data to decrypt
|
|
17
17
|
* @param key - Decryption key
|
|
18
|
-
* @returns Decrypted
|
|
18
|
+
* @returns Decrypted storage
|
|
19
19
|
*/
|
|
20
|
-
static decrypt(encryptedData: EncryptedData, key: Buffer): Promise<
|
|
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
|
|
30
|
-
* @param data -
|
|
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:
|
|
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
|
|
18
|
+
* Decrypt storage data
|
|
19
19
|
* @param encryptedData - Encrypted data to decrypt
|
|
20
20
|
* @param key - Decryption key
|
|
21
|
-
* @returns Decrypted
|
|
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
|
|
64
|
-
* @param data -
|
|
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
|
*/
|
package/lib/auth/keychain.js
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { KeychainService } from './keychain.js';
|
|
2
|
+
import { type OrgConfig } from './org-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Manager for organization storage
|
|
5
|
+
* Handles encrypted storage of Solace Cloud organization credentials
|
|
6
|
+
*/
|
|
7
|
+
export declare class OrgManager {
|
|
8
|
+
private static instance;
|
|
9
|
+
private readonly configDir;
|
|
10
|
+
private readonly configFile;
|
|
11
|
+
private encryptionKey;
|
|
12
|
+
private readonly keychainService;
|
|
13
|
+
private machineId;
|
|
14
|
+
private masterKey;
|
|
15
|
+
private storage;
|
|
16
|
+
private constructor();
|
|
17
|
+
/**
|
|
18
|
+
* Get singleton instance
|
|
19
|
+
* @param keychainService - Optional keychain service for testing
|
|
20
|
+
*/
|
|
21
|
+
static getInstance(keychainService?: KeychainService): OrgManager;
|
|
22
|
+
/**
|
|
23
|
+
* Add a new organization configuration
|
|
24
|
+
* @param org - Organization configuration
|
|
25
|
+
*/
|
|
26
|
+
addOrg(org: OrgConfig): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Clear all organization configurations
|
|
29
|
+
*/
|
|
30
|
+
clearAll(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Get all organization configurations
|
|
33
|
+
* @returns Array of all organization configurations
|
|
34
|
+
*/
|
|
35
|
+
getAllOrgs(): Promise<OrgConfig[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Get the default organization
|
|
38
|
+
* @returns Default organization or null if no default is set
|
|
39
|
+
*/
|
|
40
|
+
getDefaultOrg(): Promise<null | OrgConfig>;
|
|
41
|
+
/**
|
|
42
|
+
* Get organization configuration by orgId or alias
|
|
43
|
+
* @param identifier - Organization ID or alias
|
|
44
|
+
* @returns Organization configuration or null if not found
|
|
45
|
+
*/
|
|
46
|
+
getOrg(identifier: string): Promise<null | OrgConfig>;
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the org manager with encryption key derived from OS keychain and machine ID
|
|
49
|
+
*/
|
|
50
|
+
initialize(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* List all organization identifiers (orgId or alias if available)
|
|
53
|
+
* @returns Array of organization identifiers
|
|
54
|
+
*/
|
|
55
|
+
listOrgs(): Promise<string[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Check if organization exists
|
|
58
|
+
* @param identifier - Organization ID or alias
|
|
59
|
+
* @returns true if organization exists
|
|
60
|
+
*/
|
|
61
|
+
orgExists(identifier: string): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Remove organization configuration
|
|
64
|
+
* @param identifier - Organization ID or alias to remove
|
|
65
|
+
*/
|
|
66
|
+
removeOrg(identifier: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Set an organization as the default
|
|
69
|
+
* @param identifier - Organization ID or alias to set as default
|
|
70
|
+
*/
|
|
71
|
+
setDefaultOrg(identifier: string): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Update existing organization configuration
|
|
74
|
+
* @param identifier - Organization ID or alias to update
|
|
75
|
+
* @param updates - Partial updates to apply
|
|
76
|
+
*/
|
|
77
|
+
updateOrg(identifier: string, updates: Partial<Omit<OrgConfig, 'orgId'>>): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Ensure manager is initialized
|
|
80
|
+
*/
|
|
81
|
+
private ensureInitialized;
|
|
82
|
+
/**
|
|
83
|
+
* Check if config file exists
|
|
84
|
+
*/
|
|
85
|
+
private fileExists;
|
|
86
|
+
/**
|
|
87
|
+
* Load storage from encrypted file
|
|
88
|
+
* @param combinedKey - Combined master key and machine ID for decryption
|
|
89
|
+
*/
|
|
90
|
+
private loadStorage;
|
|
91
|
+
/**
|
|
92
|
+
* Save storage to encrypted file
|
|
93
|
+
*/
|
|
94
|
+
private saveStorage;
|
|
95
|
+
/**
|
|
96
|
+
* Unset the default flag on all organizations
|
|
97
|
+
*/
|
|
98
|
+
private unsetAllDefaults;
|
|
99
|
+
/**
|
|
100
|
+
* Validate organization configuration
|
|
101
|
+
* @param org - Organization to validate
|
|
102
|
+
*/
|
|
103
|
+
private validateOrg;
|
|
104
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
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 { BrokerAuthEncryption } from './auth-encryption.js';
|
|
5
|
+
import { KeychainService } from './keychain.js';
|
|
6
|
+
import { OrgError, OrgErrorCode } from './org-types.js';
|
|
7
|
+
const SERVICE_NAME = 'local';
|
|
8
|
+
const KEY_NAME = 'sc-cli';
|
|
9
|
+
/**
|
|
10
|
+
* Manager for organization storage
|
|
11
|
+
* Handles encrypted storage of Solace Cloud organization credentials
|
|
12
|
+
*/
|
|
13
|
+
export class OrgManager {
|
|
14
|
+
static instance = null;
|
|
15
|
+
configDir;
|
|
16
|
+
configFile;
|
|
17
|
+
encryptionKey = null;
|
|
18
|
+
keychainService;
|
|
19
|
+
machineId = null;
|
|
20
|
+
masterKey = null;
|
|
21
|
+
storage = null;
|
|
22
|
+
constructor(keychainService) {
|
|
23
|
+
const homeDirectory = homedir();
|
|
24
|
+
this.configDir = join(homeDirectory, '.sc');
|
|
25
|
+
this.configFile = join(this.configDir, 'orgs.json');
|
|
26
|
+
this.keychainService = keychainService ?? new KeychainService();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get singleton instance
|
|
30
|
+
* @param keychainService - Optional keychain service for testing
|
|
31
|
+
*/
|
|
32
|
+
static getInstance(keychainService) {
|
|
33
|
+
if (!OrgManager.instance) {
|
|
34
|
+
OrgManager.instance = new OrgManager(keychainService);
|
|
35
|
+
}
|
|
36
|
+
return OrgManager.instance;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Add a new organization configuration
|
|
40
|
+
* @param org - Organization configuration
|
|
41
|
+
*/
|
|
42
|
+
async addOrg(org) {
|
|
43
|
+
this.ensureInitialized();
|
|
44
|
+
// Validate organization
|
|
45
|
+
this.validateOrg(org);
|
|
46
|
+
// Check if organization already exists (by orgId or alias)
|
|
47
|
+
const existingByOrgId = this.storage.orgs.find((o) => o.orgId === org.orgId);
|
|
48
|
+
if (existingByOrgId) {
|
|
49
|
+
throw new OrgError(`Organization '${org.orgId}' already exists`, OrgErrorCode.ORG_ALREADY_EXISTS);
|
|
50
|
+
}
|
|
51
|
+
if (org.alias) {
|
|
52
|
+
const existingByAlias = this.storage.orgs.find((o) => o.alias === org.alias);
|
|
53
|
+
if (existingByAlias) {
|
|
54
|
+
throw new OrgError(`Organization with alias '${org.alias}' already exists`, OrgErrorCode.ORG_ALREADY_EXISTS);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// If this org is being set as default, unset any existing default
|
|
58
|
+
if (org.isDefault) {
|
|
59
|
+
this.unsetAllDefaults();
|
|
60
|
+
}
|
|
61
|
+
// Add organization
|
|
62
|
+
this.storage.orgs.push(org);
|
|
63
|
+
// Save to file
|
|
64
|
+
await this.saveStorage();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Clear all organization configurations
|
|
68
|
+
*/
|
|
69
|
+
async clearAll() {
|
|
70
|
+
this.ensureInitialized();
|
|
71
|
+
this.storage.orgs = [];
|
|
72
|
+
await this.saveStorage();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get all organization configurations
|
|
76
|
+
* @returns Array of all organization configurations
|
|
77
|
+
*/
|
|
78
|
+
async getAllOrgs() {
|
|
79
|
+
this.ensureInitialized();
|
|
80
|
+
return [...this.storage.orgs];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get the default organization
|
|
84
|
+
* @returns Default organization or null if no default is set
|
|
85
|
+
*/
|
|
86
|
+
async getDefaultOrg() {
|
|
87
|
+
this.ensureInitialized();
|
|
88
|
+
const org = this.storage.orgs.find((o) => o.isDefault === true);
|
|
89
|
+
return org ?? null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get organization configuration by orgId or alias
|
|
93
|
+
* @param identifier - Organization ID or alias
|
|
94
|
+
* @returns Organization configuration or null if not found
|
|
95
|
+
*/
|
|
96
|
+
async getOrg(identifier) {
|
|
97
|
+
this.ensureInitialized();
|
|
98
|
+
const org = this.storage.orgs.find((o) => o.orgId === identifier || o.alias === identifier);
|
|
99
|
+
return org ?? null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Initialize the org manager with encryption key derived from OS keychain and machine ID
|
|
103
|
+
*/
|
|
104
|
+
async initialize() {
|
|
105
|
+
try {
|
|
106
|
+
// Get machine ID
|
|
107
|
+
this.machineId = this.keychainService.getMachineId();
|
|
108
|
+
// Get or create master key from OS keychain
|
|
109
|
+
this.masterKey = await this.keychainService.getPassword(KEY_NAME, SERVICE_NAME);
|
|
110
|
+
if (!this.masterKey) {
|
|
111
|
+
// Generate new master key and store in OS keychain
|
|
112
|
+
this.masterKey = this.keychainService.generateMasterKey();
|
|
113
|
+
await this.keychainService.setPassword(KEY_NAME, SERVICE_NAME, this.masterKey);
|
|
114
|
+
}
|
|
115
|
+
// Combine master key with machine ID for encryption
|
|
116
|
+
const combinedKey = `${this.masterKey}:${this.machineId}`;
|
|
117
|
+
// Try to load existing storage
|
|
118
|
+
const fileExists = await this.fileExists();
|
|
119
|
+
if (fileExists) {
|
|
120
|
+
// Load existing file and derive key from stored salt
|
|
121
|
+
await this.loadStorage(combinedKey);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Create new storage with new salt
|
|
125
|
+
const salt = BrokerAuthEncryption.generateSalt();
|
|
126
|
+
this.encryptionKey = await BrokerAuthEncryption.deriveKey(combinedKey, salt);
|
|
127
|
+
this.storage = {
|
|
128
|
+
orgs: [],
|
|
129
|
+
version: '1.0.0',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof OrgError) {
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
throw new OrgError('Failed to initialize org manager', OrgErrorCode.NOT_INITIALIZED, error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* List all organization identifiers (orgId or alias if available)
|
|
142
|
+
* @returns Array of organization identifiers
|
|
143
|
+
*/
|
|
144
|
+
async listOrgs() {
|
|
145
|
+
this.ensureInitialized();
|
|
146
|
+
return this.storage.orgs.map((o) => o.alias ?? o.orgId);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if organization exists
|
|
150
|
+
* @param identifier - Organization ID or alias
|
|
151
|
+
* @returns true if organization exists
|
|
152
|
+
*/
|
|
153
|
+
async orgExists(identifier) {
|
|
154
|
+
this.ensureInitialized();
|
|
155
|
+
return this.storage.orgs.some((o) => o.orgId === identifier || o.alias === identifier);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Remove organization configuration
|
|
159
|
+
* @param identifier - Organization ID or alias to remove
|
|
160
|
+
*/
|
|
161
|
+
async removeOrg(identifier) {
|
|
162
|
+
this.ensureInitialized();
|
|
163
|
+
const index = this.storage.orgs.findIndex((o) => o.orgId === identifier || o.alias === identifier);
|
|
164
|
+
if (index === -1) {
|
|
165
|
+
throw new OrgError(`Organization '${identifier}' not found`, OrgErrorCode.ORG_NOT_FOUND);
|
|
166
|
+
}
|
|
167
|
+
// Remove organization
|
|
168
|
+
this.storage.orgs.splice(index, 1);
|
|
169
|
+
// Save to file
|
|
170
|
+
await this.saveStorage();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Set an organization as the default
|
|
174
|
+
* @param identifier - Organization ID or alias to set as default
|
|
175
|
+
*/
|
|
176
|
+
async setDefaultOrg(identifier) {
|
|
177
|
+
this.ensureInitialized();
|
|
178
|
+
const index = this.storage.orgs.findIndex((o) => o.orgId === identifier || o.alias === identifier);
|
|
179
|
+
if (index === -1) {
|
|
180
|
+
throw new OrgError(`Organization '${identifier}' not found`, OrgErrorCode.ORG_NOT_FOUND);
|
|
181
|
+
}
|
|
182
|
+
// Unset all existing defaults
|
|
183
|
+
this.unsetAllDefaults();
|
|
184
|
+
// Set this org as default
|
|
185
|
+
this.storage.orgs[index].isDefault = true;
|
|
186
|
+
// Save to file
|
|
187
|
+
await this.saveStorage();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Update existing organization configuration
|
|
191
|
+
* @param identifier - Organization ID or alias to update
|
|
192
|
+
* @param updates - Partial updates to apply
|
|
193
|
+
*/
|
|
194
|
+
async updateOrg(identifier, updates) {
|
|
195
|
+
this.ensureInitialized();
|
|
196
|
+
const index = this.storage.orgs.findIndex((o) => o.orgId === identifier || o.alias === identifier);
|
|
197
|
+
if (index === -1) {
|
|
198
|
+
throw new OrgError(`Organization '${identifier}' not found`, OrgErrorCode.ORG_NOT_FOUND);
|
|
199
|
+
}
|
|
200
|
+
const currentOrg = this.storage.orgs[index];
|
|
201
|
+
// Check if new alias conflicts with another org
|
|
202
|
+
if (updates.alias && updates.alias !== currentOrg.alias) {
|
|
203
|
+
const conflictingOrg = this.storage.orgs.find((o) => o.alias === updates.alias && o.orgId !== currentOrg.orgId);
|
|
204
|
+
if (conflictingOrg) {
|
|
205
|
+
throw new OrgError(`Organization with alias '${updates.alias}' already exists`, OrgErrorCode.ORG_ALREADY_EXISTS);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// If setting this org as default, unset any existing default
|
|
209
|
+
if (updates.isDefault === true) {
|
|
210
|
+
this.unsetAllDefaults();
|
|
211
|
+
}
|
|
212
|
+
// Merge updates
|
|
213
|
+
const updated = {
|
|
214
|
+
...currentOrg,
|
|
215
|
+
...updates,
|
|
216
|
+
};
|
|
217
|
+
// Validate updated organization
|
|
218
|
+
this.validateOrg(updated);
|
|
219
|
+
// Update organization
|
|
220
|
+
this.storage.orgs[index] = updated;
|
|
221
|
+
// Save to file
|
|
222
|
+
await this.saveStorage();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Ensure manager is initialized
|
|
226
|
+
*/
|
|
227
|
+
ensureInitialized() {
|
|
228
|
+
if (!this.encryptionKey || !this.storage) {
|
|
229
|
+
throw new OrgError('OrgManager not initialized. Call initialize() first.', OrgErrorCode.NOT_INITIALIZED);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Check if config file exists
|
|
234
|
+
*/
|
|
235
|
+
async fileExists() {
|
|
236
|
+
try {
|
|
237
|
+
await readFile(this.configFile);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Load storage from encrypted file
|
|
246
|
+
* @param combinedKey - Combined master key and machine ID for decryption
|
|
247
|
+
*/
|
|
248
|
+
async loadStorage(combinedKey) {
|
|
249
|
+
try {
|
|
250
|
+
const fileContent = await readFile(this.configFile, 'utf8');
|
|
251
|
+
const encryptedData = JSON.parse(fileContent);
|
|
252
|
+
// Derive key from combined key and stored salt
|
|
253
|
+
const salt = Buffer.from(encryptedData.salt, 'base64');
|
|
254
|
+
this.encryptionKey = await BrokerAuthEncryption.deriveKey(combinedKey, salt);
|
|
255
|
+
// Decrypt storage
|
|
256
|
+
this.storage = await BrokerAuthEncryption.decrypt(encryptedData, this.encryptionKey);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error instanceof OrgError) {
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
throw new OrgError('Failed to load organization storage', OrgErrorCode.FILE_READ_ERROR, error);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Save storage to encrypted file
|
|
267
|
+
*/
|
|
268
|
+
async saveStorage() {
|
|
269
|
+
try {
|
|
270
|
+
if (!this.masterKey || !this.machineId) {
|
|
271
|
+
throw new OrgError('Org manager not initialized', OrgErrorCode.NOT_INITIALIZED);
|
|
272
|
+
}
|
|
273
|
+
// Ensure directory exists
|
|
274
|
+
await mkdir(this.configDir, { mode: 0o700, recursive: true });
|
|
275
|
+
// Generate new salt and derive key for THIS save
|
|
276
|
+
const combinedKey = `${this.masterKey}:${this.machineId}`;
|
|
277
|
+
const newSalt = BrokerAuthEncryption.generateSalt();
|
|
278
|
+
const newKey = await BrokerAuthEncryption.deriveKey(combinedKey, newSalt);
|
|
279
|
+
// Encrypt data with the new key
|
|
280
|
+
const encrypted = await BrokerAuthEncryption.encrypt(this.storage, newKey);
|
|
281
|
+
// Update the salt in encrypted data to match the salt we used for key derivation
|
|
282
|
+
encrypted.salt = newSalt.toString('base64');
|
|
283
|
+
// Store the new key for next operation
|
|
284
|
+
this.encryptionKey = newKey;
|
|
285
|
+
// Write to temp file first (atomic write)
|
|
286
|
+
const jsonData = JSON.stringify(encrypted, null, 2);
|
|
287
|
+
const tempFile = `${this.configFile}.tmp`;
|
|
288
|
+
await writeFile(tempFile, jsonData, { mode: 0o600 });
|
|
289
|
+
// Atomic rename
|
|
290
|
+
await rename(tempFile, this.configFile);
|
|
291
|
+
// Set restrictive permissions (Unix only)
|
|
292
|
+
if (process.platform !== 'win32') {
|
|
293
|
+
// Already set via writeFile mode option
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
// Clean up temp file if it exists
|
|
298
|
+
try {
|
|
299
|
+
await unlink(`${this.configFile}.tmp`);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// Ignore cleanup errors
|
|
303
|
+
}
|
|
304
|
+
if (error instanceof OrgError) {
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
throw new OrgError('Failed to save organization storage', OrgErrorCode.FILE_WRITE_ERROR, error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Unset the default flag on all organizations
|
|
312
|
+
*/
|
|
313
|
+
unsetAllDefaults() {
|
|
314
|
+
for (const org of this.storage.orgs) {
|
|
315
|
+
org.isDefault = false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Validate organization configuration
|
|
320
|
+
* @param org - Organization to validate
|
|
321
|
+
*/
|
|
322
|
+
validateOrg(org) {
|
|
323
|
+
// Validate orgId
|
|
324
|
+
if (!org.orgId || org.orgId.trim() === '') {
|
|
325
|
+
throw new OrgError('Organization ID is required', OrgErrorCode.INVALID_ORG_ID);
|
|
326
|
+
}
|
|
327
|
+
// Validate accessToken
|
|
328
|
+
if (!org.accessToken || org.accessToken.trim() === '') {
|
|
329
|
+
throw new OrgError('Access token is required', OrgErrorCode.INVALID_ACCESS_TOKEN);
|
|
330
|
+
}
|
|
331
|
+
// Validate alias if provided
|
|
332
|
+
if (org.alias !== undefined && org.alias.trim() === '') {
|
|
333
|
+
throw new OrgError('Alias cannot be empty if provided', OrgErrorCode.INVALID_ORG_ID);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organization configuration
|
|
3
|
+
*/
|
|
4
|
+
export interface OrgConfig {
|
|
5
|
+
accessToken: string;
|
|
6
|
+
alias?: string;
|
|
7
|
+
isDefault?: boolean;
|
|
8
|
+
orgId: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Storage format for organization configurations
|
|
12
|
+
*/
|
|
13
|
+
export interface OrgStorage {
|
|
14
|
+
orgs: OrgConfig[];
|
|
15
|
+
version: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Error codes for organization operations
|
|
19
|
+
*/
|
|
20
|
+
export declare enum OrgErrorCode {
|
|
21
|
+
DECRYPTION_FAILED = "DECRYPTION_FAILED",
|
|
22
|
+
ENCRYPTION_FAILED = "ENCRYPTION_FAILED",
|
|
23
|
+
FILE_READ_ERROR = "FILE_READ_ERROR",
|
|
24
|
+
FILE_WRITE_ERROR = "FILE_WRITE_ERROR",
|
|
25
|
+
INVALID_ACCESS_TOKEN = "INVALID_ACCESS_TOKEN",
|
|
26
|
+
INVALID_ORG_ID = "INVALID_ORG_ID",
|
|
27
|
+
NOT_INITIALIZED = "NOT_INITIALIZED",
|
|
28
|
+
ORG_ALREADY_EXISTS = "ORG_ALREADY_EXISTS",
|
|
29
|
+
ORG_NOT_FOUND = "ORG_NOT_FOUND"
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Custom error class for organization operations
|
|
33
|
+
*/
|
|
34
|
+
export declare class OrgError extends Error {
|
|
35
|
+
readonly code: OrgErrorCode;
|
|
36
|
+
readonly cause?: Error | undefined;
|
|
37
|
+
constructor(message: string, code: OrgErrorCode, cause?: Error | undefined);
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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_ORG_ID"] = "INVALID_ORG_ID";
|
|
12
|
+
OrgErrorCode["NOT_INITIALIZED"] = "NOT_INITIALIZED";
|
|
13
|
+
OrgErrorCode["ORG_ALREADY_EXISTS"] = "ORG_ALREADY_EXISTS";
|
|
14
|
+
OrgErrorCode["ORG_NOT_FOUND"] = "ORG_NOT_FOUND";
|
|
15
|
+
})(OrgErrorCode || (OrgErrorCode = {}));
|
|
16
|
+
/**
|
|
17
|
+
* Custom error class for organization operations
|
|
18
|
+
*/
|
|
19
|
+
export class OrgError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
cause;
|
|
22
|
+
constructor(message, code, cause) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.cause = cause;
|
|
26
|
+
this.name = 'OrgError';
|
|
27
|
+
}
|
|
28
|
+
}
|