@aifabrix/builder 2.1.7 → 2.3.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.
Files changed (43) hide show
  1. package/lib/app-deploy.js +73 -29
  2. package/lib/app-list.js +132 -0
  3. package/lib/app-readme.js +11 -4
  4. package/lib/app-register.js +435 -0
  5. package/lib/app-rotate-secret.js +164 -0
  6. package/lib/app-run.js +98 -84
  7. package/lib/app.js +13 -0
  8. package/lib/audit-logger.js +195 -15
  9. package/lib/build.js +155 -42
  10. package/lib/cli.js +104 -8
  11. package/lib/commands/app.js +8 -391
  12. package/lib/commands/login.js +130 -36
  13. package/lib/commands/secure.js +260 -0
  14. package/lib/config.js +315 -4
  15. package/lib/deployer.js +221 -183
  16. package/lib/infra.js +177 -112
  17. package/lib/push.js +34 -7
  18. package/lib/secrets.js +89 -23
  19. package/lib/templates.js +1 -1
  20. package/lib/utils/api-error-handler.js +465 -0
  21. package/lib/utils/api.js +165 -16
  22. package/lib/utils/auth-headers.js +84 -0
  23. package/lib/utils/build-copy.js +162 -0
  24. package/lib/utils/cli-utils.js +49 -3
  25. package/lib/utils/compose-generator.js +57 -16
  26. package/lib/utils/deployment-errors.js +90 -0
  27. package/lib/utils/deployment-validation.js +60 -0
  28. package/lib/utils/dev-config.js +83 -0
  29. package/lib/utils/docker-build.js +24 -0
  30. package/lib/utils/env-template.js +30 -10
  31. package/lib/utils/health-check.js +18 -1
  32. package/lib/utils/infra-containers.js +101 -0
  33. package/lib/utils/local-secrets.js +0 -2
  34. package/lib/utils/secrets-encryption.js +203 -0
  35. package/lib/utils/secrets-path.js +22 -3
  36. package/lib/utils/token-manager.js +381 -0
  37. package/package.json +2 -2
  38. package/templates/applications/README.md.hbs +155 -23
  39. package/templates/applications/miso-controller/Dockerfile +7 -119
  40. package/templates/infra/compose.yaml.hbs +93 -0
  41. package/templates/python/docker-compose.hbs +25 -17
  42. package/templates/typescript/docker-compose.hbs +25 -17
  43. package/test-output.txt +0 -5431
@@ -0,0 +1,260 @@
1
+ /**
2
+ * AI Fabrix Builder - Secure Command
3
+ *
4
+ * Handles encryption of secrets in secrets.local.yaml files
5
+ * Sets encryption key in config.yaml and encrypts all secret values
6
+ *
7
+ * @fileoverview Secure command implementation for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const yaml = require('js-yaml');
16
+ const inquirer = require('inquirer');
17
+ const chalk = require('chalk');
18
+ const logger = require('../utils/logger');
19
+ const { setSecretsEncryptionKey, getSecretsEncryptionKey } = require('../config');
20
+ const { encryptSecret, isEncrypted, validateEncryptionKey } = require('../utils/secrets-encryption');
21
+
22
+ /**
23
+ * Finds all secrets.local.yaml files to encrypt
24
+ * Includes user secrets file and build secrets from all apps
25
+ *
26
+ * @async
27
+ * @function findSecretsFiles
28
+ * @returns {Promise<Array<{path: string, type: string}>>} Array of secrets file paths
29
+ */
30
+ async function findSecretsFiles() {
31
+ const files = [];
32
+
33
+ // User's secrets file
34
+ const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
35
+ if (fs.existsSync(userSecretsPath)) {
36
+ files.push({ path: userSecretsPath, type: 'user' });
37
+ }
38
+
39
+ // Find all apps and check for build.secrets
40
+ // Scan builder directory for apps
41
+ try {
42
+ const builderDir = path.join(process.cwd(), 'builder');
43
+ if (fs.existsSync(builderDir)) {
44
+ const entries = fs.readdirSync(builderDir, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ if (entry.isDirectory()) {
47
+ const appName = entry.name;
48
+ const variablesPath = path.join(builderDir, appName, 'variables.yaml');
49
+ if (fs.existsSync(variablesPath)) {
50
+ try {
51
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
52
+ const variables = yaml.load(variablesContent);
53
+
54
+ if (variables?.build?.secrets) {
55
+ const buildSecretsPath = path.resolve(
56
+ path.dirname(variablesPath),
57
+ variables.build.secrets
58
+ );
59
+ if (fs.existsSync(buildSecretsPath)) {
60
+ files.push({ path: buildSecretsPath, type: `app:${appName}` });
61
+ }
62
+ }
63
+ } catch (error) {
64
+ // Ignore errors, continue
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ } catch (error) {
71
+ // Ignore errors, continue
72
+ }
73
+
74
+ // Check config.yaml for general secrets-path
75
+ try {
76
+ const { getSecretsPath } = require('../config');
77
+ const generalSecretsPath = await getSecretsPath();
78
+ if (generalSecretsPath) {
79
+ const resolvedPath = path.isAbsolute(generalSecretsPath)
80
+ ? generalSecretsPath
81
+ : path.resolve(process.cwd(), generalSecretsPath);
82
+ if (fs.existsSync(resolvedPath) && !files.some(f => f.path === resolvedPath)) {
83
+ files.push({ path: resolvedPath, type: 'general' });
84
+ }
85
+ }
86
+ } catch (error) {
87
+ // Ignore errors, continue
88
+ }
89
+
90
+ return files;
91
+ }
92
+
93
+ /**
94
+ * Encrypts all non-encrypted values in a secrets file
95
+ * Preserves YAML structure and comments
96
+ *
97
+ * @async
98
+ * @function encryptSecretsFile
99
+ * @param {string} filePath - Path to secrets file
100
+ * @param {string} encryptionKey - Encryption key
101
+ * @returns {Promise<{encrypted: number, total: number}>} Count of encrypted values
102
+ */
103
+ async function encryptSecretsFile(filePath, encryptionKey) {
104
+ const content = fs.readFileSync(filePath, 'utf8');
105
+ const secrets = yaml.load(content);
106
+
107
+ if (!secrets || typeof secrets !== 'object') {
108
+ throw new Error(`Invalid secrets file format: ${filePath}`);
109
+ }
110
+
111
+ let encryptedCount = 0;
112
+ let totalCount = 0;
113
+ const updatedSecrets = {};
114
+
115
+ for (const [key, value] of Object.entries(secrets)) {
116
+ totalCount++;
117
+ if (typeof value === 'string' && value.trim() !== '') {
118
+ if (isEncrypted(value)) {
119
+ // Already encrypted, keep as-is
120
+ updatedSecrets[key] = value;
121
+ } else {
122
+ // Encrypt the value
123
+ updatedSecrets[key] = encryptSecret(value, encryptionKey);
124
+ encryptedCount++;
125
+ }
126
+ } else {
127
+ // Non-string or empty value, keep as-is
128
+ updatedSecrets[key] = value;
129
+ }
130
+ }
131
+
132
+ // Write back to file with same formatting
133
+ // Use yaml.dump with appropriate options to preserve structure
134
+ const yamlContent = yaml.dump(updatedSecrets, {
135
+ indent: 2,
136
+ lineWidth: -1,
137
+ noRefs: true,
138
+ sortKeys: false
139
+ });
140
+
141
+ fs.writeFileSync(filePath, yamlContent, { mode: 0o600 });
142
+
143
+ return { encrypted: encryptedCount, total: totalCount };
144
+ }
145
+
146
+ /**
147
+ * Prompt for encryption key if not provided
148
+ *
149
+ * @async
150
+ * @function promptForEncryptionKey
151
+ * @returns {Promise<string>} Encryption key
152
+ */
153
+ async function promptForEncryptionKey() {
154
+ const answer = await inquirer.prompt([{
155
+ type: 'password',
156
+ name: 'key',
157
+ message: 'Enter encryption key (32 bytes, hex or base64):',
158
+ mask: '*',
159
+ validate: (input) => {
160
+ if (!input || input.trim().length === 0) {
161
+ return 'Encryption key is required';
162
+ }
163
+ try {
164
+ validateEncryptionKey(input.trim());
165
+ return true;
166
+ } catch (error) {
167
+ return error.message;
168
+ }
169
+ }
170
+ }]);
171
+
172
+ return answer.key.trim();
173
+ }
174
+
175
+ /**
176
+ * Handle secure command action
177
+ * Sets encryption key and encrypts all secrets files
178
+ *
179
+ * @async
180
+ * @function handleSecure
181
+ * @param {Object} options - Command options
182
+ * @param {string} [options.secretsEncryption] - Encryption key (optional, will prompt if not provided)
183
+ * @returns {Promise<void>} Resolves when encryption completes
184
+ * @throws {Error} If encryption fails
185
+ */
186
+ async function handleSecure(options) {
187
+ logger.log(chalk.blue('\n🔐 Securing secrets files...\n'));
188
+
189
+ // Get or prompt for encryption key
190
+ let encryptionKey = options.secretsEncryption || options['secrets-encryption'];
191
+ if (!encryptionKey) {
192
+ // Check if key already exists in config
193
+ const existingKey = await getSecretsEncryptionKey();
194
+ if (existingKey) {
195
+ logger.log(chalk.yellow('⚠️ Encryption key already configured in config.yaml'));
196
+ const useExisting = await inquirer.prompt([{
197
+ type: 'confirm',
198
+ name: 'use',
199
+ message: 'Use existing encryption key?',
200
+ default: true
201
+ }]);
202
+ if (useExisting.use) {
203
+ encryptionKey = existingKey;
204
+ } else {
205
+ encryptionKey = await promptForEncryptionKey();
206
+ await setSecretsEncryptionKey(encryptionKey);
207
+ logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
208
+ }
209
+ } else {
210
+ encryptionKey = await promptForEncryptionKey();
211
+ await setSecretsEncryptionKey(encryptionKey);
212
+ logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
213
+ }
214
+ } else {
215
+ // Validate and save the provided key
216
+ validateEncryptionKey(encryptionKey);
217
+ await setSecretsEncryptionKey(encryptionKey);
218
+ logger.log(chalk.green('✓ Encryption key saved to config.yaml'));
219
+ }
220
+
221
+ // Find all secrets files
222
+ const secretsFiles = await findSecretsFiles();
223
+
224
+ if (secretsFiles.length === 0) {
225
+ logger.log(chalk.yellow('⚠️ No secrets files found to encrypt'));
226
+ logger.log(chalk.gray(' Create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml'));
227
+ return;
228
+ }
229
+
230
+ logger.log(chalk.gray(`Found ${secretsFiles.length} secrets file(s) to process:\n`));
231
+
232
+ // Encrypt each file
233
+ let totalEncrypted = 0;
234
+ let totalValues = 0;
235
+
236
+ for (const file of secretsFiles) {
237
+ try {
238
+ logger.log(chalk.gray(`Processing: ${file.path} (${file.type})`));
239
+ const result = await encryptSecretsFile(file.path, encryptionKey);
240
+ totalEncrypted += result.encrypted;
241
+ totalValues += result.total;
242
+
243
+ if (result.encrypted > 0) {
244
+ logger.log(chalk.green(` ✓ Encrypted ${result.encrypted} of ${result.total} values`));
245
+ } else {
246
+ logger.log(chalk.gray(` - All values already encrypted (${result.total} total)`));
247
+ }
248
+ } catch (error) {
249
+ logger.log(chalk.red(` ✗ Error: ${error.message}`));
250
+ }
251
+ }
252
+
253
+ logger.log(chalk.green('\n✅ Encryption complete!'));
254
+ logger.log(chalk.gray(` Files processed: ${secretsFiles.length}`));
255
+ logger.log(chalk.gray(` Values encrypted: ${totalEncrypted} of ${totalValues} total`));
256
+ logger.log(chalk.gray(' Encryption key stored in: ~/.aifabrix/config.yaml\n'));
257
+ }
258
+
259
+ module.exports = { handleSecure };
260
+
package/lib/config.js CHANGED
@@ -17,17 +17,53 @@ const yaml = require('js-yaml');
17
17
  const CONFIG_DIR = path.join(os.homedir(), '.aifabrix');
18
18
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.yaml');
19
19
 
20
+ // Cache for developer ID - loaded when getConfig() is first called
21
+ let cachedDeveloperId = null;
22
+
20
23
  /**
21
24
  * Get stored configuration
22
- * @returns {Promise<Object>} Configuration object with apiUrl and token
25
+ * Loads developer ID and caches it as a property for easy access
26
+ * @returns {Promise<Object>} Configuration object with new structure
23
27
  */
24
28
  async function getConfig() {
25
29
  try {
26
30
  const configContent = await fs.readFile(CONFIG_FILE, 'utf8');
27
- return yaml.load(configContent);
31
+ let config = yaml.load(configContent);
32
+
33
+ // Handle empty file or null/undefined result from yaml.load
34
+ if (!config || typeof config !== 'object') {
35
+ config = {};
36
+ }
37
+
38
+ // Ensure developerId defaults to 0 if not set
39
+ if (typeof config['developer-id'] === 'undefined') {
40
+ config['developer-id'] = 0;
41
+ }
42
+ // Ensure environment defaults to 'dev' if not set
43
+ if (typeof config.environment === 'undefined') {
44
+ config.environment = 'dev';
45
+ }
46
+ // Ensure environments object exists
47
+ if (typeof config.environments !== 'object' || config.environments === null) {
48
+ config.environments = {};
49
+ }
50
+ // Ensure device object exists at root level
51
+ if (typeof config.device !== 'object' || config.device === null) {
52
+ config.device = {};
53
+ }
54
+ // Cache developer ID as property for easy access (use nullish coalescing to allow 0)
55
+ cachedDeveloperId = config['developer-id'] !== undefined ? config['developer-id'] : 0;
56
+ return config;
28
57
  } catch (error) {
29
58
  if (error.code === 'ENOENT') {
30
- return { apiUrl: null, token: null };
59
+ // Default developer ID is 0, default environment is 'dev'
60
+ cachedDeveloperId = 0;
61
+ return {
62
+ 'developer-id': 0,
63
+ environment: 'dev',
64
+ environments: {},
65
+ device: {}
66
+ };
31
67
  }
32
68
  throw new Error(`Failed to read config: ${error.message}`);
33
69
  }
@@ -45,10 +81,19 @@ async function saveConfig(data) {
45
81
 
46
82
  // Set secure permissions
47
83
  const configContent = yaml.dump(data);
84
+ // Write file first
48
85
  await fs.writeFile(CONFIG_FILE, configContent, {
49
86
  mode: 0o600,
50
87
  flag: 'w'
51
88
  });
89
+ // Open file descriptor and fsync to ensure write is flushed to disk
90
+ // This is critical on Windows where file writes may be cached
91
+ const fd = await fs.open(CONFIG_FILE, 'r+');
92
+ try {
93
+ await fd.sync();
94
+ } finally {
95
+ await fd.close();
96
+ }
52
97
  } catch (error) {
53
98
  throw new Error(`Failed to save config: ${error.message}`);
54
99
  }
@@ -68,11 +113,277 @@ async function clearConfig() {
68
113
  }
69
114
  }
70
115
 
71
- module.exports = {
116
+ /**
117
+ * Get developer ID from configuration
118
+ * Loads config if not already cached, then returns cached developer ID
119
+ * Developer ID: 0 = default infra, > 0 = developer-specific
120
+ * @returns {Promise<number>} Developer ID (defaults to 0)
121
+ */
122
+ async function getDeveloperId() {
123
+ // Always reload from file to ensure we have the latest value
124
+ // This ensures the cache matches what's actually in the file
125
+ // Clear cache first to force a fresh read
126
+ cachedDeveloperId = null;
127
+ await getConfig();
128
+ return cachedDeveloperId;
129
+ }
130
+
131
+ /**
132
+ * Set developer ID in configuration
133
+ * @param {number} developerId - Developer ID to set (0 = default infra, > 0 = developer-specific)
134
+ * @returns {Promise<void>}
135
+ */
136
+ async function setDeveloperId(developerId) {
137
+ if (typeof developerId !== 'number' || developerId < 0) {
138
+ throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
139
+ }
140
+ // Clear cache first to ensure we get fresh data from file
141
+ cachedDeveloperId = null;
142
+ // Read file directly to avoid any caching issues
143
+ const config = await getConfig();
144
+ // Update developer ID
145
+ config['developer-id'] = developerId;
146
+ // Update cache before saving
147
+ cachedDeveloperId = developerId;
148
+ // Save the entire config object to ensure all fields are preserved
149
+ await saveConfig(config);
150
+ // Verify the file was saved correctly by reading it back
151
+ // This ensures the file system has written the data
152
+ // Add a small delay to ensure file system has flushed the write
153
+ await new Promise(resolve => setTimeout(resolve, 100));
154
+ // Read file again with fresh file handle to avoid OS caching
155
+ const savedContent = await fs.readFile(CONFIG_FILE, 'utf8');
156
+ const savedConfig = yaml.load(savedContent);
157
+ if (savedConfig['developer-id'] !== developerId) {
158
+ throw new Error(`Failed to save developer ID: expected ${developerId}, got ${savedConfig['developer-id']}. File content: ${savedContent.substring(0, 200)}`);
159
+ }
160
+ // Clear the cache to force reload from file on next getDeveloperId() call
161
+ // This ensures we get the value that was actually saved to disk
162
+ cachedDeveloperId = null;
163
+ }
164
+
165
+ /**
166
+ * Get current environment from root-level config
167
+ * @returns {Promise<string>} Current environment (defaults to 'dev')
168
+ */
169
+ async function getCurrentEnvironment() {
170
+ const config = await getConfig();
171
+ return config.environment || 'dev';
172
+ }
173
+
174
+ /**
175
+ * Set current environment in root-level config
176
+ * @param {string} environment - Environment to set (e.g., 'miso', 'dev', 'tst', 'pro')
177
+ * @returns {Promise<void>}
178
+ */
179
+ async function setCurrentEnvironment(environment) {
180
+ if (!environment || typeof environment !== 'string') {
181
+ throw new Error('Environment must be a non-empty string');
182
+ }
183
+ const config = await getConfig();
184
+ config.environment = environment;
185
+ await saveConfig(config);
186
+ }
187
+
188
+ /**
189
+ * Check if token is expired
190
+ * @param {string} expiresAt - ISO timestamp string
191
+ * @returns {boolean} True if token is expired
192
+ */
193
+ function isTokenExpired(expiresAt) {
194
+ if (!expiresAt) {
195
+ return true;
196
+ }
197
+ const expirationTime = new Date(expiresAt).getTime();
198
+ const now = Date.now();
199
+ // Add 5 minute buffer to refresh before actual expiration
200
+ return now >= (expirationTime - 5 * 60 * 1000);
201
+ }
202
+
203
+ /**
204
+ * Get device token for controller
205
+ * @param {string} controllerUrl - Controller URL
206
+ * @returns {Promise<{controller: string, token: string, refreshToken: string, expiresAt: string}|null>} Device token info or null
207
+ */
208
+ async function getDeviceToken(controllerUrl) {
209
+ const config = await getConfig();
210
+ if (!config.device || !config.device[controllerUrl]) {
211
+ return null;
212
+ }
213
+ const deviceToken = config.device[controllerUrl];
214
+ return {
215
+ controller: controllerUrl,
216
+ token: deviceToken.token,
217
+ refreshToken: deviceToken.refreshToken,
218
+ expiresAt: deviceToken.expiresAt
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Get client token for environment and app
224
+ * @param {string} environment - Environment key
225
+ * @param {string} appName - Application name
226
+ * @returns {Promise<{controller: string, token: string, expiresAt: string}|null>} Client token info or null
227
+ */
228
+ async function getClientToken(environment, appName) {
229
+ const config = await getConfig();
230
+ if (!config.environments || !config.environments[environment]) {
231
+ return null;
232
+ }
233
+ if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) {
234
+ return null;
235
+ }
236
+ return config.environments[environment].clients[appName];
237
+ }
238
+
239
+ /**
240
+ * Save device token for controller (root level)
241
+ * @param {string} controllerUrl - Controller URL (used as key)
242
+ * @param {string} token - Device access token
243
+ * @param {string} refreshToken - Refresh token for token renewal
244
+ * @param {string} expiresAt - ISO timestamp string
245
+ * @returns {Promise<void>}
246
+ */
247
+ async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
248
+ const config = await getConfig();
249
+ if (!config.device) {
250
+ config.device = {};
251
+ }
252
+ config.device[controllerUrl] = {
253
+ token: token,
254
+ refreshToken: refreshToken,
255
+ expiresAt: expiresAt
256
+ };
257
+ await saveConfig(config);
258
+ }
259
+
260
+ /**
261
+ * Save client token for environment and app
262
+ * @param {string} environment - Environment key
263
+ * @param {string} appName - Application name
264
+ * @param {string} controllerUrl - Controller URL
265
+ * @param {string} token - Client token
266
+ * @param {string} expiresAt - ISO timestamp string
267
+ * @returns {Promise<void>}
268
+ */
269
+ async function saveClientToken(environment, appName, controllerUrl, token, expiresAt) {
270
+ const config = await getConfig();
271
+ if (!config.environments) {
272
+ config.environments = {};
273
+ }
274
+ if (!config.environments[environment]) {
275
+ config.environments[environment] = { clients: {} };
276
+ }
277
+ if (!config.environments[environment].clients) {
278
+ config.environments[environment].clients = {};
279
+ }
280
+ config.environments[environment].clients[appName] = {
281
+ controller: controllerUrl,
282
+ token: token,
283
+ expiresAt: expiresAt
284
+ };
285
+ await saveConfig(config);
286
+ }
287
+
288
+ /**
289
+ * Initialize and load developer ID
290
+ * Call this to ensure developerId is loaded and cached
291
+ * @returns {Promise<number>} Developer ID
292
+ */
293
+ async function loadDeveloperId() {
294
+ if (cachedDeveloperId === null) {
295
+ await getConfig();
296
+ }
297
+ return cachedDeveloperId;
298
+ }
299
+
300
+ /**
301
+ * Get secrets encryption key from configuration
302
+ * @returns {Promise<string|null>} Encryption key or null if not set
303
+ */
304
+ async function getSecretsEncryptionKey() {
305
+ const config = await getConfig();
306
+ return config['secrets-encryption'] || null;
307
+ }
308
+
309
+ /**
310
+ * Set secrets encryption key in configuration
311
+ * @param {string} key - Encryption key (32 bytes, hex or base64)
312
+ * @returns {Promise<void>}
313
+ * @throws {Error} If key format is invalid
314
+ */
315
+ async function setSecretsEncryptionKey(key) {
316
+ if (!key || typeof key !== 'string') {
317
+ throw new Error('Encryption key is required and must be a string');
318
+ }
319
+
320
+ // Validate key format using encryption utilities
321
+ const { validateEncryptionKey } = require('./utils/secrets-encryption');
322
+ validateEncryptionKey(key);
323
+
324
+ const config = await getConfig();
325
+ config['secrets-encryption'] = key;
326
+ await saveConfig(config);
327
+ }
328
+
329
+ /**
330
+ * Get general secrets path from configuration
331
+ * Used as fallback when build.secrets is not set in variables.yaml
332
+ * @returns {Promise<string|null>} Secrets path or null if not set
333
+ */
334
+ async function getSecretsPath() {
335
+ const config = await getConfig();
336
+ return config['secrets-path'] || null;
337
+ }
338
+
339
+ /**
340
+ * Set general secrets path in configuration
341
+ * @param {string} secretsPath - Path to general secrets file
342
+ * @returns {Promise<void>}
343
+ */
344
+ async function setSecretsPath(secretsPath) {
345
+ if (!secretsPath || typeof secretsPath !== 'string') {
346
+ throw new Error('Secrets path is required and must be a string');
347
+ }
348
+
349
+ const config = await getConfig();
350
+ config['secrets-path'] = secretsPath;
351
+ await saveConfig(config);
352
+ }
353
+
354
+ // Create exports object
355
+ const exportsObj = {
72
356
  getConfig,
73
357
  saveConfig,
74
358
  clearConfig,
359
+ getDeveloperId,
360
+ setDeveloperId,
361
+ loadDeveloperId,
362
+ getCurrentEnvironment,
363
+ setCurrentEnvironment,
364
+ isTokenExpired,
365
+ getDeviceToken,
366
+ getClientToken,
367
+ saveDeviceToken,
368
+ saveClientToken,
369
+ getSecretsEncryptionKey,
370
+ setSecretsEncryptionKey,
371
+ getSecretsPath,
372
+ setSecretsPath,
75
373
  CONFIG_DIR,
76
374
  CONFIG_FILE
77
375
  };
78
376
 
377
+ // Add developerId as a property getter for direct access
378
+ // After getConfig() or getDeveloperId() is called, config.developerId will be available
379
+ // Developer ID: 0 = default infra, > 0 = developer-specific
380
+ Object.defineProperty(exportsObj, 'developerId', {
381
+ get() {
382
+ return cachedDeveloperId !== null ? cachedDeveloperId : 0; // Default to 0 if not loaded yet
383
+ },
384
+ enumerable: true,
385
+ configurable: true
386
+ });
387
+
388
+ module.exports = exportsObj;
389
+