@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.
- package/lib/app-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +155 -42
- package/lib/cli.js +104 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/commands/secure.js +260 -0
- package/lib/config.js +315 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/push.js +34 -7
- package/lib/secrets.js +89 -23
- package/lib/templates.js +1 -1
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +162 -0
- package/lib/utils/cli-utils.js +49 -3
- package/lib/utils/compose-generator.js +57 -16
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/docker-build.js +24 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/secrets-encryption.js +203 -0
- package/lib/utils/secrets-path.js +22 -3
- package/lib/utils/token-manager.js +381 -0
- package/package.json +2 -2
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
- 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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|