@aifabrix/builder 2.6.3 → 2.8.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/.cursor/rules/project-rules.mdc +680 -0
- package/bin/aifabrix.js +4 -0
- package/lib/app-config.js +10 -0
- package/lib/app-deploy.js +18 -0
- package/lib/app-dockerfile.js +15 -0
- package/lib/app-prompts.js +172 -9
- package/lib/app-push.js +15 -0
- package/lib/app-register.js +14 -0
- package/lib/app-run.js +25 -0
- package/lib/app.js +30 -13
- package/lib/audit-logger.js +9 -4
- package/lib/build.js +8 -0
- package/lib/cli.js +99 -2
- package/lib/commands/datasource.js +94 -0
- package/lib/commands/login.js +40 -3
- package/lib/config.js +121 -114
- package/lib/datasource-deploy.js +182 -0
- package/lib/datasource-diff.js +73 -0
- package/lib/datasource-list.js +138 -0
- package/lib/datasource-validate.js +63 -0
- package/lib/diff.js +266 -0
- package/lib/environment-deploy.js +305 -0
- package/lib/external-system-deploy.js +262 -0
- package/lib/external-system-generator.js +187 -0
- package/lib/schema/application-schema.json +869 -698
- package/lib/schema/external-datasource.schema.json +512 -0
- package/lib/schema/external-system.schema.json +262 -0
- package/lib/schema/infrastructure-schema.json +1 -1
- package/lib/secrets.js +20 -1
- package/lib/templates.js +32 -1
- package/lib/utils/device-code.js +10 -2
- package/lib/utils/env-copy.js +24 -0
- package/lib/utils/env-endpoints.js +50 -11
- package/lib/utils/schema-loader.js +220 -0
- package/lib/utils/schema-resolver.js +174 -0
- package/lib/utils/secrets-helpers.js +65 -17
- package/lib/utils/token-encryption.js +68 -0
- package/lib/validate.js +299 -0
- package/lib/validator.js +47 -3
- package/package.json +1 -1
- package/tatus +181 -0
- package/templates/external-system/external-datasource.json.hbs +55 -0
- package/templates/external-system/external-system.json.hbs +37 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder - Datasource Commands
|
|
3
|
+
*
|
|
4
|
+
* Handles datasource validation, listing, comparison, and deployment
|
|
5
|
+
* Commands: datasource validate, datasource list, datasource diff, datasource deploy
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Datasource management commands for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const logger = require('../utils/logger');
|
|
14
|
+
const { validateDatasourceFile } = require('../datasource-validate');
|
|
15
|
+
const { listDatasources } = require('../datasource-list');
|
|
16
|
+
const { compareDatasources } = require('../datasource-diff');
|
|
17
|
+
const { deployDatasource } = require('../datasource-deploy');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Setup datasource management commands
|
|
21
|
+
* @param {Command} program - Commander program instance
|
|
22
|
+
*/
|
|
23
|
+
function setupDatasourceCommands(program) {
|
|
24
|
+
const datasource = program
|
|
25
|
+
.command('datasource')
|
|
26
|
+
.description('Manage external data sources');
|
|
27
|
+
|
|
28
|
+
// Validate command
|
|
29
|
+
datasource
|
|
30
|
+
.command('validate <file>')
|
|
31
|
+
.description('Validate external datasource JSON file')
|
|
32
|
+
.action(async(file) => {
|
|
33
|
+
try {
|
|
34
|
+
const result = await validateDatasourceFile(file);
|
|
35
|
+
if (result.valid) {
|
|
36
|
+
logger.log(chalk.green(`\n✓ Datasource file is valid: ${file}`));
|
|
37
|
+
} else {
|
|
38
|
+
logger.log(chalk.red(`\n✗ Datasource file has errors: ${file}`));
|
|
39
|
+
result.errors.forEach(error => {
|
|
40
|
+
logger.log(chalk.red(` • ${error}`));
|
|
41
|
+
});
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.error(chalk.red('❌ Validation failed:'), error.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// List command
|
|
51
|
+
datasource
|
|
52
|
+
.command('list')
|
|
53
|
+
.description('List datasources from environment')
|
|
54
|
+
.requiredOption('-e, --environment <env>', 'Environment ID or key')
|
|
55
|
+
.action(async(options) => {
|
|
56
|
+
try {
|
|
57
|
+
await listDatasources(options);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.error(chalk.red('❌ Failed to list datasources:'), error.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Diff command
|
|
65
|
+
datasource
|
|
66
|
+
.command('diff <file1> <file2>')
|
|
67
|
+
.description('Compare two datasource configuration files (for dataplane)')
|
|
68
|
+
.action(async(file1, file2) => {
|
|
69
|
+
try {
|
|
70
|
+
await compareDatasources(file1, file2);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.error(chalk.red('❌ Diff failed:'), error.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Deploy command
|
|
78
|
+
datasource
|
|
79
|
+
.command('deploy <myapp> <file>')
|
|
80
|
+
.description('Deploy datasource to dataplane')
|
|
81
|
+
.requiredOption('--controller <url>', 'Controller URL')
|
|
82
|
+
.requiredOption('-e, --environment <env>', 'Environment (miso, dev, tst, pro)')
|
|
83
|
+
.action(async(myapp, file, options) => {
|
|
84
|
+
try {
|
|
85
|
+
await deployDatasource(myapp, file, options);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error(chalk.red('❌ Deployment failed:'), error.message);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { setupDatasourceCommands };
|
|
94
|
+
|
package/lib/commands/login.js
CHANGED
|
@@ -302,20 +302,51 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Build scope string from options
|
|
307
|
+
* @param {boolean} [offline] - Whether to request offline_access
|
|
308
|
+
* @param {string} [customScope] - Custom scope string
|
|
309
|
+
* @returns {string} Scope string
|
|
310
|
+
*/
|
|
311
|
+
function buildScope(offline, customScope) {
|
|
312
|
+
const defaultScope = 'openid profile email';
|
|
313
|
+
|
|
314
|
+
if (customScope) {
|
|
315
|
+
// If custom scope provided, use it and optionally add offline_access
|
|
316
|
+
if (offline && !customScope.includes('offline_access')) {
|
|
317
|
+
return `${customScope} offline_access`;
|
|
318
|
+
}
|
|
319
|
+
return customScope;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Default scope with optional offline_access
|
|
323
|
+
if (offline) {
|
|
324
|
+
return `${defaultScope} offline_access`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return defaultScope;
|
|
328
|
+
}
|
|
329
|
+
|
|
305
330
|
/**
|
|
306
331
|
* Handle device code flow login
|
|
307
332
|
* @async
|
|
308
333
|
* @param {string} controllerUrl - Controller URL
|
|
309
334
|
* @param {string} [environment] - Environment key from options
|
|
335
|
+
* @param {boolean} [offline] - Whether to request offline_access scope
|
|
336
|
+
* @param {string} [scope] - Custom scope string
|
|
310
337
|
* @returns {Promise<{token: string, environment: string}>} Token and environment
|
|
311
338
|
*/
|
|
312
|
-
async function handleDeviceCodeLogin(controllerUrl, environment) {
|
|
339
|
+
async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope) {
|
|
313
340
|
const envKey = await getEnvironmentKey(environment);
|
|
341
|
+
const requestScope = buildScope(offline, scope);
|
|
314
342
|
|
|
315
343
|
logger.log(chalk.blue('\n📱 Initiating device code flow...\n'));
|
|
344
|
+
if (offline) {
|
|
345
|
+
logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
|
|
346
|
+
}
|
|
316
347
|
|
|
317
348
|
try {
|
|
318
|
-
const deviceCodeResponse = await initiateDeviceCodeFlow(controllerUrl, envKey);
|
|
349
|
+
const deviceCodeResponse = await initiateDeviceCodeFlow(controllerUrl, envKey, requestScope);
|
|
319
350
|
|
|
320
351
|
displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
|
|
321
352
|
|
|
@@ -369,6 +400,12 @@ async function handleLogin(options) {
|
|
|
369
400
|
let token;
|
|
370
401
|
let expiresAt;
|
|
371
402
|
|
|
403
|
+
// Validate scope options - only applicable to device flow
|
|
404
|
+
if (method === 'credentials' && (options.offline || options.scope)) {
|
|
405
|
+
logger.log(chalk.yellow('⚠️ Warning: --offline and --scope options are only available for device flow'));
|
|
406
|
+
logger.log(chalk.gray(' These options will be ignored for credentials method\n'));
|
|
407
|
+
}
|
|
408
|
+
|
|
372
409
|
if (method === 'credentials') {
|
|
373
410
|
if (!options.app) {
|
|
374
411
|
logger.error(chalk.red('❌ --app is required for credentials login method'));
|
|
@@ -379,7 +416,7 @@ async function handleLogin(options) {
|
|
|
379
416
|
expiresAt = loginResult.expiresAt;
|
|
380
417
|
await saveCredentialsLoginConfig(controllerUrl, token, expiresAt, environment, options.app);
|
|
381
418
|
} else if (method === 'device') {
|
|
382
|
-
const result = await handleDeviceCodeLogin(controllerUrl, options.environment);
|
|
419
|
+
const result = await handleDeviceCodeLogin(controllerUrl, options.environment, options.offline, options.scope);
|
|
383
420
|
token = result.token;
|
|
384
421
|
environment = result.environment;
|
|
385
422
|
return; // Early return for device flow (already saved config)
|
package/lib/config.js
CHANGED
|
@@ -12,6 +12,7 @@ const fs = require('fs').promises;
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const yaml = require('js-yaml');
|
|
14
14
|
const os = require('os');
|
|
15
|
+
const { encryptToken, decryptToken, isTokenEncrypted } = require('./utils/token-encryption');
|
|
15
16
|
// Avoid importing paths here to prevent circular dependency.
|
|
16
17
|
// Config location is always under OS home at ~/.aifabrix/config.yaml
|
|
17
18
|
|
|
@@ -26,11 +27,6 @@ const RUNTIME_CONFIG_FILE = path.join(RUNTIME_CONFIG_DIR, 'config.yaml');
|
|
|
26
27
|
// Cache for developer ID - loaded when getConfig() is first called
|
|
27
28
|
let cachedDeveloperId = null;
|
|
28
29
|
|
|
29
|
-
/**
|
|
30
|
-
* Get stored configuration
|
|
31
|
-
* Loads developer ID and caches it as a property for easy access
|
|
32
|
-
* @returns {Promise<Object>} Configuration object with new structure
|
|
33
|
-
*/
|
|
34
30
|
async function getConfig() {
|
|
35
31
|
try {
|
|
36
32
|
const configContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
|
|
@@ -156,61 +152,37 @@ async function getDeveloperId() {
|
|
|
156
152
|
*/
|
|
157
153
|
async function setDeveloperId(developerId) {
|
|
158
154
|
const DEV_ID_DIGITS_REGEX = /^[0-9]+$/;
|
|
155
|
+
const errorMsg = 'Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)';
|
|
159
156
|
let devIdString;
|
|
160
157
|
if (typeof developerId === 'number') {
|
|
161
|
-
if (!Number.isFinite(developerId) || developerId < 0)
|
|
162
|
-
throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
|
|
163
|
-
}
|
|
158
|
+
if (!Number.isFinite(developerId) || developerId < 0) throw new Error(errorMsg);
|
|
164
159
|
devIdString = String(developerId);
|
|
165
160
|
} else if (typeof developerId === 'string') {
|
|
166
|
-
if (!DEV_ID_DIGITS_REGEX.test(developerId))
|
|
167
|
-
throw new Error('Developer ID must be a non-negative digit string or number (0 = default infra, > 0 = developer-specific)');
|
|
168
|
-
}
|
|
161
|
+
if (!DEV_ID_DIGITS_REGEX.test(developerId)) throw new Error(errorMsg);
|
|
169
162
|
devIdString = developerId;
|
|
170
163
|
} else {
|
|
171
|
-
throw new Error(
|
|
164
|
+
throw new Error(errorMsg);
|
|
172
165
|
}
|
|
173
|
-
// Clear cache first to ensure we get fresh data from file
|
|
174
166
|
cachedDeveloperId = null;
|
|
175
|
-
// Read file directly to avoid any caching issues
|
|
176
167
|
const config = await getConfig();
|
|
177
|
-
// Update developer ID
|
|
178
168
|
config['developer-id'] = devIdString;
|
|
179
|
-
// Update cache before saving
|
|
180
169
|
cachedDeveloperId = devIdString;
|
|
181
|
-
// Save the entire config object to ensure all fields are preserved
|
|
182
170
|
await saveConfig(config);
|
|
183
|
-
// Verify the file was saved correctly by reading it back
|
|
184
|
-
// This ensures the file system has written the data
|
|
185
|
-
// Add a small delay to ensure file system has flushed the write
|
|
186
171
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
187
|
-
// Read file again with fresh file handle to avoid OS caching
|
|
188
172
|
const savedContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
|
|
189
173
|
const savedConfig = yaml.load(savedContent);
|
|
190
|
-
// YAML may parse numbers as numbers, so convert to string for comparison
|
|
191
174
|
const savedDevIdString = String(savedConfig['developer-id']);
|
|
192
175
|
if (savedDevIdString !== devIdString) {
|
|
193
176
|
throw new Error(`Failed to save developer ID: expected ${devIdString}, got ${savedDevIdString}. File content: ${savedContent.substring(0, 200)}`);
|
|
194
177
|
}
|
|
195
|
-
// Clear the cache to force reload from file on next getDeveloperId() call
|
|
196
|
-
// This ensures we get the value that was actually saved to disk
|
|
197
178
|
cachedDeveloperId = null;
|
|
198
179
|
}
|
|
199
180
|
|
|
200
|
-
/**
|
|
201
|
-
* Get current environment from root-level config
|
|
202
|
-
* @returns {Promise<string>} Current environment (defaults to 'dev')
|
|
203
|
-
*/
|
|
204
181
|
async function getCurrentEnvironment() {
|
|
205
182
|
const config = await getConfig();
|
|
206
183
|
return config.environment || 'dev';
|
|
207
184
|
}
|
|
208
185
|
|
|
209
|
-
/**
|
|
210
|
-
* Set current environment in root-level config
|
|
211
|
-
* @param {string} environment - Environment to set (e.g., 'miso', 'dev', 'tst', 'pro')
|
|
212
|
-
* @returns {Promise<void>}
|
|
213
|
-
*/
|
|
214
186
|
async function setCurrentEnvironment(environment) {
|
|
215
187
|
if (!environment || typeof environment !== 'string') {
|
|
216
188
|
throw new Error('Environment must be a non-empty string');
|
|
@@ -220,31 +192,45 @@ async function setCurrentEnvironment(environment) {
|
|
|
220
192
|
await saveConfig(config);
|
|
221
193
|
}
|
|
222
194
|
|
|
223
|
-
/**
|
|
224
|
-
* Check if token is expired
|
|
225
|
-
* @param {string} expiresAt - ISO timestamp string
|
|
226
|
-
* @returns {boolean} True if token is expired
|
|
227
|
-
*/
|
|
228
195
|
function isTokenExpired(expiresAt) {
|
|
229
196
|
if (!expiresAt) return true;
|
|
230
197
|
const expirationTime = new Date(expiresAt).getTime();
|
|
231
198
|
const now = Date.now();
|
|
232
|
-
return now >= (expirationTime - 5 * 60 * 1000);
|
|
199
|
+
return now >= (expirationTime - 5 * 60 * 1000);
|
|
233
200
|
}
|
|
234
201
|
|
|
235
|
-
/**
|
|
236
|
-
* Check if token should be refreshed proactively (within 15 minutes of expiry)
|
|
237
|
-
* Helps keep Keycloak sessions alive by refreshing before SSO Session Idle timeout (30 minutes)
|
|
238
|
-
* @param {string} expiresAt - ISO timestamp string
|
|
239
|
-
* @returns {boolean} True if token should be refreshed proactively
|
|
240
|
-
*/
|
|
241
202
|
function shouldRefreshToken(expiresAt) {
|
|
242
203
|
if (!expiresAt) return true;
|
|
243
204
|
const expirationTime = new Date(expiresAt).getTime();
|
|
244
205
|
const now = Date.now();
|
|
245
|
-
return now >= (expirationTime - 15 * 60 * 1000);
|
|
206
|
+
return now >= (expirationTime - 15 * 60 * 1000);
|
|
207
|
+
}
|
|
208
|
+
async function encryptTokenValue(value) {
|
|
209
|
+
if (!value || typeof value !== 'string') return value;
|
|
210
|
+
try {
|
|
211
|
+
const encryptionKey = await getSecretsEncryptionKey();
|
|
212
|
+
if (!encryptionKey) return value;
|
|
213
|
+
if (isTokenEncrypted(value)) return value;
|
|
214
|
+
const encrypted = encryptToken(value, encryptionKey);
|
|
215
|
+
// Ensure we never return undefined for valid inputs
|
|
216
|
+
return encrypted !== undefined && encrypted !== null ? encrypted : value;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function decryptTokenValue(value) {
|
|
222
|
+
if (!value || typeof value !== 'string') return value;
|
|
223
|
+
try {
|
|
224
|
+
const encryptionKey = await getSecretsEncryptionKey();
|
|
225
|
+
if (!encryptionKey) return value;
|
|
226
|
+
if (!isTokenEncrypted(value)) return value;
|
|
227
|
+
const decrypted = decryptToken(value, encryptionKey);
|
|
228
|
+
// Ensure we never return undefined for valid inputs
|
|
229
|
+
return decrypted !== undefined && decrypted !== null ? decrypted : value;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
246
233
|
}
|
|
247
|
-
|
|
248
234
|
/**
|
|
249
235
|
* Get device token for controller
|
|
250
236
|
* @param {string} controllerUrl - Controller URL
|
|
@@ -254,10 +240,37 @@ async function getDeviceToken(controllerUrl) {
|
|
|
254
240
|
const config = await getConfig();
|
|
255
241
|
if (!config.device || !config.device[controllerUrl]) return null;
|
|
256
242
|
const deviceToken = config.device[controllerUrl];
|
|
243
|
+
|
|
244
|
+
// Migration: If tokens are plain text and encryption key exists, encrypt them first
|
|
245
|
+
const encryptionKey = await getSecretsEncryptionKey();
|
|
246
|
+
if (encryptionKey) {
|
|
247
|
+
let needsSave = false;
|
|
248
|
+
|
|
249
|
+
if (deviceToken.token && !isTokenEncrypted(deviceToken.token)) {
|
|
250
|
+
// Token is plain text, encrypt it
|
|
251
|
+
deviceToken.token = await encryptTokenValue(deviceToken.token);
|
|
252
|
+
needsSave = true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (deviceToken.refreshToken && !isTokenEncrypted(deviceToken.refreshToken)) {
|
|
256
|
+
// Refresh token is plain text, encrypt it
|
|
257
|
+
deviceToken.refreshToken = await encryptTokenValue(deviceToken.refreshToken);
|
|
258
|
+
needsSave = true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (needsSave) {
|
|
262
|
+
// Save encrypted tokens back to config
|
|
263
|
+
await saveConfig(config);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Decrypt tokens if encrypted (for return value)
|
|
267
|
+
const token = deviceToken.token ? await decryptTokenValue(deviceToken.token) : undefined;
|
|
268
|
+
const refreshToken = deviceToken.refreshToken ? await decryptTokenValue(deviceToken.refreshToken) : null;
|
|
269
|
+
|
|
257
270
|
return {
|
|
258
271
|
controller: controllerUrl,
|
|
259
|
-
token:
|
|
260
|
-
refreshToken:
|
|
272
|
+
token: token,
|
|
273
|
+
refreshToken: refreshToken,
|
|
261
274
|
expiresAt: deviceToken.expiresAt
|
|
262
275
|
};
|
|
263
276
|
}
|
|
@@ -272,7 +285,26 @@ async function getClientToken(environment, appName) {
|
|
|
272
285
|
const config = await getConfig();
|
|
273
286
|
if (!config.environments || !config.environments[environment]) return null;
|
|
274
287
|
if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) return null;
|
|
275
|
-
|
|
288
|
+
|
|
289
|
+
const clientToken = config.environments[environment].clients[appName];
|
|
290
|
+
|
|
291
|
+
// Migration: If token is plain text and encryption key exists, encrypt it first
|
|
292
|
+
const encryptionKey = await getSecretsEncryptionKey();
|
|
293
|
+
if (encryptionKey && clientToken.token && !isTokenEncrypted(clientToken.token)) {
|
|
294
|
+
// Token is plain text, encrypt it
|
|
295
|
+
clientToken.token = await encryptTokenValue(clientToken.token);
|
|
296
|
+
// Save encrypted token back to config
|
|
297
|
+
await saveConfig(config);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Decrypt token if encrypted (for return value)
|
|
301
|
+
const token = await decryptTokenValue(clientToken.token);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
controller: clientToken.controller,
|
|
305
|
+
token: token,
|
|
306
|
+
expiresAt: clientToken.expiresAt
|
|
307
|
+
};
|
|
276
308
|
}
|
|
277
309
|
|
|
278
310
|
/**
|
|
@@ -286,7 +318,16 @@ async function getClientToken(environment, appName) {
|
|
|
286
318
|
async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
|
|
287
319
|
const config = await getConfig();
|
|
288
320
|
if (!config.device) config.device = {};
|
|
289
|
-
|
|
321
|
+
|
|
322
|
+
// Encrypt tokens before saving
|
|
323
|
+
const encryptedToken = await encryptTokenValue(token);
|
|
324
|
+
const encryptedRefreshToken = refreshToken ? await encryptTokenValue(refreshToken) : null;
|
|
325
|
+
|
|
326
|
+
config.device[controllerUrl] = {
|
|
327
|
+
token: encryptedToken,
|
|
328
|
+
refreshToken: encryptedRefreshToken,
|
|
329
|
+
expiresAt
|
|
330
|
+
};
|
|
290
331
|
await saveConfig(config);
|
|
291
332
|
}
|
|
292
333
|
|
|
@@ -304,7 +345,15 @@ async function saveClientToken(environment, appName, controllerUrl, token, expir
|
|
|
304
345
|
if (!config.environments) config.environments = {};
|
|
305
346
|
if (!config.environments[environment]) config.environments[environment] = { clients: {} };
|
|
306
347
|
if (!config.environments[environment].clients) config.environments[environment].clients = {};
|
|
307
|
-
|
|
348
|
+
|
|
349
|
+
// Encrypt token before saving
|
|
350
|
+
const encryptedToken = await encryptTokenValue(token);
|
|
351
|
+
|
|
352
|
+
config.environments[environment].clients[appName] = {
|
|
353
|
+
controller: controllerUrl,
|
|
354
|
+
token: encryptedToken,
|
|
355
|
+
expiresAt
|
|
356
|
+
};
|
|
308
357
|
await saveConfig(config);
|
|
309
358
|
}
|
|
310
359
|
|
|
@@ -349,100 +398,56 @@ async function setSecretsEncryptionKey(key) {
|
|
|
349
398
|
await saveConfig(config);
|
|
350
399
|
}
|
|
351
400
|
|
|
352
|
-
/**
|
|
353
|
-
* Get general secrets path from configuration
|
|
354
|
-
* Returns aifabrix-secrets path from config.yaml if configured
|
|
355
|
-
* @returns {Promise<string|null>} Secrets path or null if not set
|
|
356
|
-
*/
|
|
357
401
|
async function getSecretsPath() {
|
|
358
402
|
const config = await getConfig();
|
|
359
|
-
// Backward compatibility: prefer new key, fallback to legacy
|
|
360
403
|
return config['aifabrix-secrets'] || config['secrets-path'] || null;
|
|
361
404
|
}
|
|
362
405
|
|
|
363
|
-
/**
|
|
364
|
-
* Set general secrets path in configuration
|
|
365
|
-
* @param {string} secretsPath - Path to general secrets file
|
|
366
|
-
* @returns {Promise<void>}
|
|
367
|
-
*/
|
|
368
406
|
async function setSecretsPath(secretsPath) {
|
|
369
407
|
if (!secretsPath || typeof secretsPath !== 'string') {
|
|
370
408
|
throw new Error('Secrets path is required and must be a string');
|
|
371
409
|
}
|
|
372
|
-
|
|
373
410
|
const config = await getConfig();
|
|
374
|
-
// Store under new canonical key
|
|
375
411
|
config['aifabrix-secrets'] = secretsPath;
|
|
376
412
|
await saveConfig(config);
|
|
377
413
|
}
|
|
378
414
|
|
|
379
|
-
|
|
380
|
-
* Get aifabrix-home override from configuration
|
|
381
|
-
* @returns {Promise<string|null>} Home override path or null if not set
|
|
382
|
-
*/
|
|
383
|
-
async function getAifabrixHomeOverride() {
|
|
415
|
+
async function getPathConfig(key) {
|
|
384
416
|
const config = await getConfig();
|
|
385
|
-
return config[
|
|
417
|
+
return config[key] || null;
|
|
386
418
|
}
|
|
387
419
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
* @returns {Promise<void>}
|
|
392
|
-
*/
|
|
393
|
-
async function setAifabrixHomeOverride(homePath) {
|
|
394
|
-
if (!homePath || typeof homePath !== 'string') {
|
|
395
|
-
throw new Error('Home path is required and must be a string');
|
|
420
|
+
async function setPathConfig(key, value, errorMsg) {
|
|
421
|
+
if (!value || typeof value !== 'string') {
|
|
422
|
+
throw new Error(errorMsg);
|
|
396
423
|
}
|
|
397
424
|
const config = await getConfig();
|
|
398
|
-
config[
|
|
425
|
+
config[key] = value;
|
|
399
426
|
await saveConfig(config);
|
|
400
427
|
}
|
|
401
428
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
429
|
+
async function getAifabrixHomeOverride() {
|
|
430
|
+
return getPathConfig('aifabrix-home');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function setAifabrixHomeOverride(homePath) {
|
|
434
|
+
await setPathConfig('aifabrix-home', homePath, 'Home path is required and must be a string');
|
|
435
|
+
}
|
|
436
|
+
|
|
406
437
|
async function getAifabrixSecretsPath() {
|
|
407
|
-
|
|
408
|
-
return config['aifabrix-secrets'] || null;
|
|
438
|
+
return getPathConfig('aifabrix-secrets');
|
|
409
439
|
}
|
|
410
440
|
|
|
411
|
-
/**
|
|
412
|
-
* Set aifabrix-secrets path in configuration (canonical)
|
|
413
|
-
* @param {string} secretsPath - Path to default secrets file
|
|
414
|
-
* @returns {Promise<void>}
|
|
415
|
-
*/
|
|
416
441
|
async function setAifabrixSecretsPath(secretsPath) {
|
|
417
|
-
|
|
418
|
-
throw new Error('Secrets path is required and must be a string');
|
|
419
|
-
}
|
|
420
|
-
const config = await getConfig();
|
|
421
|
-
config['aifabrix-secrets'] = secretsPath;
|
|
422
|
-
await saveConfig(config);
|
|
442
|
+
await setPathConfig('aifabrix-secrets', secretsPath, 'Secrets path is required and must be a string');
|
|
423
443
|
}
|
|
424
444
|
|
|
425
|
-
/**
|
|
426
|
-
* Get aifabrix-env-config path from configuration
|
|
427
|
-
* @returns {Promise<string|null>} Env config path or null if not set
|
|
428
|
-
*/
|
|
429
445
|
async function getAifabrixEnvConfigPath() {
|
|
430
|
-
|
|
431
|
-
return config['aifabrix-env-config'] || null;
|
|
446
|
+
return getPathConfig('aifabrix-env-config');
|
|
432
447
|
}
|
|
433
448
|
|
|
434
|
-
/**
|
|
435
|
-
* Set aifabrix-env-config path in configuration
|
|
436
|
-
* @param {string} envConfigPath - Path to user env-config file
|
|
437
|
-
* @returns {Promise<void>}
|
|
438
|
-
*/
|
|
439
449
|
async function setAifabrixEnvConfigPath(envConfigPath) {
|
|
440
|
-
|
|
441
|
-
throw new Error('Env config path is required and must be a string');
|
|
442
|
-
}
|
|
443
|
-
const config = await getConfig();
|
|
444
|
-
config['aifabrix-env-config'] = envConfigPath;
|
|
445
|
-
await saveConfig(config);
|
|
450
|
+
await setPathConfig('aifabrix-env-config', envConfigPath, 'Env config path is required and must be a string');
|
|
446
451
|
}
|
|
447
452
|
|
|
448
453
|
// Create exports object
|
|
@@ -461,6 +466,8 @@ const exportsObj = {
|
|
|
461
466
|
getClientToken,
|
|
462
467
|
saveDeviceToken,
|
|
463
468
|
saveClientToken,
|
|
469
|
+
encryptTokenValue,
|
|
470
|
+
decryptTokenValue,
|
|
464
471
|
getSecretsEncryptionKey,
|
|
465
472
|
setSecretsEncryptionKey,
|
|
466
473
|
getSecretsPath,
|