@aifabrix/builder 2.33.0 → 2.33.3
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 +13 -0
- package/integration/hubspot/README.md +7 -7
- package/lib/api/index.js +6 -2
- package/lib/app/deploy-config.js +161 -0
- package/lib/app/deploy.js +28 -153
- package/lib/app/register.js +6 -5
- package/lib/app/run-helpers.js +23 -17
- package/lib/cli.js +31 -1
- package/lib/commands/logout.js +3 -4
- package/lib/commands/up-common.js +72 -0
- package/lib/commands/up-dataplane.js +109 -0
- package/lib/commands/up-miso.js +134 -0
- package/lib/core/config.js +32 -9
- package/lib/core/secrets-docker-env.js +88 -0
- package/lib/core/secrets.js +142 -115
- package/lib/datasource/deploy.js +31 -3
- package/lib/datasource/list.js +102 -15
- package/lib/infrastructure/helpers.js +82 -1
- package/lib/infrastructure/index.js +2 -0
- package/lib/schema/env-config.yaml +7 -0
- package/lib/utils/api.js +70 -2
- package/lib/utils/compose-generator.js +13 -13
- package/lib/utils/config-paths.js +13 -0
- package/lib/utils/device-code.js +2 -2
- package/lib/utils/env-endpoints.js +2 -5
- package/lib/utils/env-map.js +4 -5
- package/lib/utils/error-formatters/network-errors.js +13 -3
- package/lib/utils/parse-image-ref.js +27 -0
- package/lib/utils/paths.js +28 -4
- package/lib/utils/secrets-generator.js +34 -12
- package/lib/utils/secrets-helpers.js +1 -2
- package/lib/utils/token-manager-refresh.js +5 -0
- package/package.json +1 -1
- package/templates/applications/dataplane/Dockerfile +16 -0
- package/templates/applications/dataplane/README.md +205 -0
- package/templates/applications/dataplane/env.template +143 -0
- package/templates/applications/dataplane/rbac.yaml +283 -0
- package/templates/applications/dataplane/variables.yaml +143 -0
- package/templates/applications/keycloak/Dockerfile +1 -1
- package/templates/applications/keycloak/README.md +193 -0
- package/templates/applications/keycloak/variables.yaml +5 -6
- package/templates/applications/miso-controller/Dockerfile +8 -8
- package/templates/applications/miso-controller/README.md +369 -0
- package/templates/applications/miso-controller/env.template +114 -6
- package/templates/applications/miso-controller/rbac.yaml +74 -0
- package/templates/applications/miso-controller/variables.yaml +93 -5
- package/templates/github/ci.yaml.hbs +44 -1
- package/templates/github/release.yaml.hbs +44 -0
- package/templates/infra/compose.yaml.hbs +2 -1
- package/templates/applications/miso-controller/test.yaml +0 -1
|
@@ -18,6 +18,9 @@ environments:
|
|
|
18
18
|
MISO_PORT: 3000 # Internal port (container-to-container). MISO_PUBLIC_PORT calculated automatically.
|
|
19
19
|
KEYCLOAK_HOST: keycloak
|
|
20
20
|
KEYCLOAK_PORT: 8082 # Internal port (container-to-container). KEYCLOAK_PUBLIC_PORT calculated automatically.
|
|
21
|
+
KEYCLOAK_PUBLIC_PORT: 8082
|
|
22
|
+
DATAPLANE_HOST: dataplane
|
|
23
|
+
DATAPLANE_PORT: 3001
|
|
21
24
|
NODE_ENV: production
|
|
22
25
|
PYTHONUNBUFFERED: 1
|
|
23
26
|
PYTHONDONTWRITEBYTECODE: 1
|
|
@@ -30,8 +33,12 @@ environments:
|
|
|
30
33
|
REDIS_PORT: 6379
|
|
31
34
|
MISO_HOST: localhost
|
|
32
35
|
MISO_PORT: 3010
|
|
36
|
+
MISO_PUBLIC_PORT: 3010
|
|
33
37
|
KEYCLOAK_HOST: localhost
|
|
34
38
|
KEYCLOAK_PORT: 8082
|
|
39
|
+
KEYCLOAK_PUBLIC_PORT: 8082
|
|
40
|
+
DATAPLANE_HOST: localhost
|
|
41
|
+
DATAPLANE_PORT: 3011
|
|
35
42
|
NODE_ENV: development
|
|
36
43
|
PYTHONUNBUFFERED: 1
|
|
37
44
|
PYTHONDONTWRITEBYTECODE: 1
|
package/lib/utils/api.js
CHANGED
|
@@ -131,6 +131,32 @@ async function handleSuccessResponse(response, url, options, duration) {
|
|
|
131
131
|
return { success: true, data: text, status: response.status };
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Validates that a URL is not empty or missing
|
|
136
|
+
* @function validateUrl
|
|
137
|
+
* @param {string} url - URL to validate
|
|
138
|
+
* @param {string} [urlType='URL'] - Type of URL for error message (e.g., 'Dataplane URL', 'Controller URL')
|
|
139
|
+
* @returns {void}
|
|
140
|
+
* @throws {Error} If URL is empty, null, undefined, whitespace-only, or malformed
|
|
141
|
+
*/
|
|
142
|
+
function validateUrl(url, urlType = 'URL') {
|
|
143
|
+
if (!url || typeof url !== 'string') {
|
|
144
|
+
throw new Error(`${urlType} is required and must be a string (received: ${JSON.stringify(url)})`);
|
|
145
|
+
}
|
|
146
|
+
const trimmedUrl = url.trim();
|
|
147
|
+
if (!trimmedUrl) {
|
|
148
|
+
throw new Error(`${urlType} cannot be empty. Please provide a valid URL.`);
|
|
149
|
+
}
|
|
150
|
+
// Check for common invalid URL patterns
|
|
151
|
+
if (trimmedUrl === 'undefined' || trimmedUrl === 'null' || trimmedUrl === 'NaN') {
|
|
152
|
+
throw new Error(`${urlType} is invalid: "${trimmedUrl}". Please provide a valid URL.`);
|
|
153
|
+
}
|
|
154
|
+
// Basic URL format validation - must start with http:// or https://
|
|
155
|
+
if (!trimmedUrl.match(/^https?:\/\//i)) {
|
|
156
|
+
throw new Error(`${urlType} must be a valid HTTP/HTTPS URL (received: "${trimmedUrl}")`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
/**
|
|
135
161
|
* Handles network error from API call
|
|
136
162
|
* @async
|
|
@@ -142,7 +168,34 @@ async function handleSuccessResponse(response, url, options, duration) {
|
|
|
142
168
|
* @returns {Promise<Object>} Error response object
|
|
143
169
|
*/
|
|
144
170
|
async function handleNetworkError(error, url, options, duration) {
|
|
145
|
-
|
|
171
|
+
// Enhance error message with URL information if URL is missing or invalid
|
|
172
|
+
let errorMessage = error.message;
|
|
173
|
+
if (errorMessage && (errorMessage.includes('cannot be empty') || errorMessage.includes('is required'))) {
|
|
174
|
+
// Add URL context to validation errors
|
|
175
|
+
if (!url || !url.trim()) {
|
|
176
|
+
errorMessage = `${errorMessage} (URL was: ${JSON.stringify(url)})`;
|
|
177
|
+
} else {
|
|
178
|
+
errorMessage = `${errorMessage} (URL was: ${url})`;
|
|
179
|
+
}
|
|
180
|
+
} else if (!url || !url.trim()) {
|
|
181
|
+
// If URL is empty but error doesn't mention it, add context
|
|
182
|
+
errorMessage = `Invalid or missing URL. ${errorMessage} (URL was: ${JSON.stringify(url)})`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const parsedError = parseErrorResponse(errorMessage, 0, true);
|
|
186
|
+
|
|
187
|
+
// Extract controller URL from full URL for error data
|
|
188
|
+
let controllerUrl = null;
|
|
189
|
+
const endpointUrl = url;
|
|
190
|
+
if (url && typeof url === 'string' && url.trim()) {
|
|
191
|
+
try {
|
|
192
|
+
const urlObj = new URL(url);
|
|
193
|
+
controllerUrl = `${urlObj.protocol}//${urlObj.host}`;
|
|
194
|
+
} catch {
|
|
195
|
+
// If URL parsing fails, use the full URL as endpoint
|
|
196
|
+
controllerUrl = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
146
199
|
|
|
147
200
|
await logApiPerformance({
|
|
148
201
|
url,
|
|
@@ -157,10 +210,17 @@ async function handleNetworkError(error, url, options, duration) {
|
|
|
157
210
|
}
|
|
158
211
|
});
|
|
159
212
|
|
|
213
|
+
// Include both controller URL and full endpoint URL in error data
|
|
214
|
+
const errorData = {
|
|
215
|
+
...parsedError.data,
|
|
216
|
+
controllerUrl: controllerUrl,
|
|
217
|
+
endpointUrl: endpointUrl
|
|
218
|
+
};
|
|
219
|
+
|
|
160
220
|
return {
|
|
161
221
|
success: false,
|
|
162
222
|
error: parsedError.message,
|
|
163
|
-
errorData:
|
|
223
|
+
errorData: errorData,
|
|
164
224
|
errorType: parsedError.type,
|
|
165
225
|
formattedError: parsedError.formatted,
|
|
166
226
|
network: true
|
|
@@ -175,6 +235,14 @@ async function handleNetworkError(error, url, options, duration) {
|
|
|
175
235
|
* @returns {Promise<Object>} Response object with success flag
|
|
176
236
|
*/
|
|
177
237
|
async function makeApiCall(url, options = {}) {
|
|
238
|
+
// Validate URL before attempting request
|
|
239
|
+
try {
|
|
240
|
+
validateUrl(url, 'API endpoint URL');
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const duration = 0;
|
|
243
|
+
return await handleNetworkError(error, url || '', options, duration);
|
|
244
|
+
}
|
|
245
|
+
|
|
178
246
|
const startTime = Date.now();
|
|
179
247
|
const fetchOptions = { ...options };
|
|
180
248
|
if (!fetchOptions.signal) {
|
|
@@ -17,6 +17,7 @@ const config = require('../core/config');
|
|
|
17
17
|
const buildCopy = require('./build-copy');
|
|
18
18
|
const { formatMissingDbPasswordError } = require('./error-formatter');
|
|
19
19
|
const { getContainerPort } = require('./port-resolver');
|
|
20
|
+
const { parseImageOverride } = require('./parse-image-ref');
|
|
20
21
|
|
|
21
22
|
// Register commonly used helpers
|
|
22
23
|
handlebars.registerHelper('eq', (a, b) => a === b);
|
|
@@ -129,9 +130,14 @@ function buildAppConfig(appName, config) {
|
|
|
129
130
|
* Builds image configuration section
|
|
130
131
|
* @param {Object} config - Application configuration
|
|
131
132
|
* @param {string} appName - Application name
|
|
133
|
+
* @param {string} [imageOverride] - Optional full image reference (registry/name:tag) to use instead of config
|
|
132
134
|
* @returns {Object} Image configuration
|
|
133
135
|
*/
|
|
134
|
-
function buildImageConfig(config, appName) {
|
|
136
|
+
function buildImageConfig(config, appName, imageOverride) {
|
|
137
|
+
const parsed = imageOverride ? parseImageOverride(imageOverride) : null;
|
|
138
|
+
if (parsed) {
|
|
139
|
+
return { name: parsed.name, tag: parsed.tag };
|
|
140
|
+
}
|
|
135
141
|
const imageName = getImageName(config, appName);
|
|
136
142
|
const imageTag = config.image?.tag || 'latest';
|
|
137
143
|
return {
|
|
@@ -237,14 +243,15 @@ function buildRequiresConfig(config) {
|
|
|
237
243
|
* @param {Object} config - Application configuration
|
|
238
244
|
* @param {number} port - Application port
|
|
239
245
|
* @param {string|number} devId - Developer ID
|
|
246
|
+
* @param {string} [imageOverride] - Optional full image reference for run (e.g. from --image)
|
|
240
247
|
* @returns {Object} Service configuration
|
|
241
248
|
*/
|
|
242
|
-
function buildServiceConfig(appName, config, port, devId) {
|
|
249
|
+
function buildServiceConfig(appName, config, port, devId, imageOverride) {
|
|
243
250
|
const containerPortValue = getContainerPort(config, 3000);
|
|
244
251
|
const hostPort = port;
|
|
245
252
|
return {
|
|
246
253
|
app: buildAppConfig(appName, config),
|
|
247
|
-
image: buildImageConfig(config, appName),
|
|
254
|
+
image: buildImageConfig(config, appName, imageOverride),
|
|
248
255
|
port: containerPortValue, // Container port (for health check and template)
|
|
249
256
|
containerPort: containerPortValue, // Container port (always set, equals containerPort if exists, else port)
|
|
250
257
|
hostPort: hostPort, // Host port (options.port if provided, else config.port)
|
|
@@ -275,14 +282,7 @@ function buildNetworksConfig(config) {
|
|
|
275
282
|
return { databases: config.requires?.databases || config.databases || [] };
|
|
276
283
|
}
|
|
277
284
|
|
|
278
|
-
/**
|
|
279
|
-
* Reads and parses .env file
|
|
280
|
-
* @async
|
|
281
|
-
* @function readEnvFile
|
|
282
|
-
* @param {string} envPath - Path to .env file
|
|
283
|
-
* @returns {Promise<Object>} Object with environment variables
|
|
284
|
-
* @throws {Error} If file not found or read fails
|
|
285
|
-
*/
|
|
285
|
+
/** Reads and parses .env file. @param {string} envPath - Path to .env file. @returns {Promise<Object>} env vars. @throws {Error} If file not found. */
|
|
286
286
|
async function readEnvFile(envPath) {
|
|
287
287
|
if (!fsSync.existsSync(envPath)) {
|
|
288
288
|
throw new Error(`.env file not found: ${envPath}`);
|
|
@@ -458,9 +458,10 @@ async function generateDockerCompose(appName, appConfig, options) {
|
|
|
458
458
|
const language = appConfig.build?.language || appConfig.language || 'typescript';
|
|
459
459
|
const template = loadDockerComposeTemplate(language);
|
|
460
460
|
const port = options.port || appConfig.port || 3000;
|
|
461
|
+
const imageOverride = options.image || options.imageOverride;
|
|
461
462
|
const { devId, idNum } = await getDeveloperIdAndNumeric();
|
|
462
463
|
const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
|
|
463
|
-
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId);
|
|
464
|
+
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, imageOverride);
|
|
464
465
|
const volumesConfig = buildVolumesConfig(appName);
|
|
465
466
|
const networksConfig = buildNetworksConfig(appConfig);
|
|
466
467
|
|
|
@@ -495,4 +496,3 @@ module.exports = {
|
|
|
495
496
|
buildTraefikConfig,
|
|
496
497
|
buildDevUsername
|
|
497
498
|
};
|
|
498
|
-
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @version 2.0.0
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Get path configuration value
|
|
13
15
|
* @async
|
|
@@ -102,6 +104,17 @@ function createPathConfigFunctions(getConfigFn, saveConfigFn) {
|
|
|
102
104
|
*/
|
|
103
105
|
async setAifabrixEnvConfigPath(envConfigPath) {
|
|
104
106
|
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-env-config', envConfigPath, 'Env config path is required and must be a string');
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get builder root directory (dirname of aifabrix-env-config when set).
|
|
111
|
+
* When set, app dirs and generated .env use this instead of cwd/builder.
|
|
112
|
+
* @async
|
|
113
|
+
* @returns {Promise<string|null>} Builder root path or null to use cwd/builder
|
|
114
|
+
*/
|
|
115
|
+
async getAifabrixBuilderDir() {
|
|
116
|
+
const envConfigPath = await getPathConfig(getConfigFn, 'aifabrix-env-config');
|
|
117
|
+
return envConfigPath && typeof envConfigPath === 'string' ? path.dirname(envConfigPath) : null;
|
|
105
118
|
}
|
|
106
119
|
};
|
|
107
120
|
}
|
package/lib/utils/device-code.js
CHANGED
|
@@ -469,12 +469,13 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
|
|
472
|
+
// Send both refresh_token (OAuth2 RFC 6749 / Keycloak) and refreshToken (camelCase) so controller accepts either
|
|
472
473
|
const response = await getMakeApiCall()(url, {
|
|
473
474
|
method: 'POST',
|
|
474
475
|
headers: {
|
|
475
476
|
'Content-Type': 'application/json'
|
|
476
477
|
},
|
|
477
|
-
body: JSON.stringify({ refreshToken })
|
|
478
|
+
body: JSON.stringify({ refresh_token: refreshToken, refreshToken })
|
|
478
479
|
});
|
|
479
480
|
|
|
480
481
|
if (!response.success) {
|
|
@@ -490,7 +491,6 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
|
490
491
|
|
|
491
492
|
return tokenResponse;
|
|
492
493
|
}
|
|
493
|
-
|
|
494
494
|
module.exports = {
|
|
495
495
|
initiateDeviceCodeFlow,
|
|
496
496
|
pollDeviceCodeToken,
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
12
|
const yaml = require('js-yaml');
|
|
14
13
|
const config = require('../core/config');
|
|
15
14
|
const devConfig = require('../utils/dev-config');
|
|
@@ -49,8 +48,7 @@ function splitHost(value) {
|
|
|
49
48
|
function getLocalhostOverride(context) {
|
|
50
49
|
if (context !== 'local') return null;
|
|
51
50
|
try {
|
|
52
|
-
const
|
|
53
|
-
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
51
|
+
const cfgPath = config.CONFIG_FILE;
|
|
54
52
|
if (fs.existsSync(cfgPath)) {
|
|
55
53
|
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
56
54
|
const cfg = yaml.load(cfgContent) || {};
|
|
@@ -310,8 +308,7 @@ async function rewriteInfraEndpoints(envContent, context, devPorts, adjustedHost
|
|
|
310
308
|
|
|
311
309
|
// Apply config.yaml → environments.{context} override (if exists)
|
|
312
310
|
try {
|
|
313
|
-
const
|
|
314
|
-
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
311
|
+
const cfgPath = config.CONFIG_FILE;
|
|
315
312
|
if (fs.existsSync(cfgPath)) {
|
|
316
313
|
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
317
314
|
const cfg = yaml.load(cfgContent) || {};
|
package/lib/utils/env-map.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
12
|
const yaml = require('js-yaml');
|
|
14
13
|
const { loadEnvConfig } = require('./env-config-loader');
|
|
15
14
|
const config = require('../core/config');
|
|
@@ -38,9 +37,9 @@ async function loadBaseVars(context) {
|
|
|
38
37
|
* @param {Object} os - OS module instance
|
|
39
38
|
* @returns {Object} Override environment variables
|
|
40
39
|
*/
|
|
41
|
-
function loadOverrideVars(context,
|
|
40
|
+
function loadOverrideVars(context, _os) {
|
|
42
41
|
try {
|
|
43
|
-
const cfgPath =
|
|
42
|
+
const cfgPath = config.CONFIG_FILE;
|
|
44
43
|
if (fs.existsSync(cfgPath)) {
|
|
45
44
|
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
46
45
|
const cfg = yaml.load(cfgContent) || {};
|
|
@@ -60,9 +59,9 @@ function loadOverrideVars(context, os) {
|
|
|
60
59
|
* @param {Object} os - OS module instance
|
|
61
60
|
* @returns {string|null} Localhost override value or null
|
|
62
61
|
*/
|
|
63
|
-
function getLocalhostOverride(
|
|
62
|
+
function getLocalhostOverride(_os) {
|
|
64
63
|
try {
|
|
65
|
-
const cfgPath =
|
|
64
|
+
const cfgPath = config.CONFIG_FILE;
|
|
66
65
|
if (fs.existsSync(cfgPath)) {
|
|
67
66
|
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
68
67
|
const cfg = yaml.load(cfgContent) || {};
|
|
@@ -40,7 +40,10 @@ function addControllerUrlHeader(lines, errorData) {
|
|
|
40
40
|
* @param {Object} errorData - Error response data
|
|
41
41
|
*/
|
|
42
42
|
function addControllerUrlToMessage(lines, errorData) {
|
|
43
|
-
|
|
43
|
+
// Prefer showing full endpoint URL if available
|
|
44
|
+
if (errorData && errorData.endpointUrl) {
|
|
45
|
+
lines.push(chalk.gray(`Endpoint URL: ${errorData.endpointUrl}`));
|
|
46
|
+
} else if (errorData && errorData.controllerUrl) {
|
|
44
47
|
lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -77,8 +80,15 @@ function formatHostnameNotFoundError(lines, errorData) {
|
|
|
77
80
|
*/
|
|
78
81
|
function formatTimeoutError(lines, errorData) {
|
|
79
82
|
lines.push(chalk.yellow('Request timed out.'));
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
|
|
84
|
+
// Show full endpoint URL if available, otherwise show controller URL
|
|
85
|
+
if (errorData && errorData.endpointUrl) {
|
|
86
|
+
lines.push(chalk.gray(`Endpoint URL: ${errorData.endpointUrl}`));
|
|
87
|
+
} else if (errorData && errorData.controllerUrl) {
|
|
88
|
+
lines.push(chalk.gray(`Controller URL: ${errorData.controllerUrl}`));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
lines.push(chalk.gray('The endpoint may not exist, the controller may be overloaded, or there may be a network issue.'));
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse full image reference (registry/name:tag or name:tag) into { name, tag }
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Image reference parsing for compose and run overrides
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses full image reference (registry/name:tag or name:tag) into { name, tag }
|
|
11
|
+
* @param {string} imageRef - Full image reference (e.g. myreg/keycloak:v1 or keycloak:latest)
|
|
12
|
+
* @returns {{ name: string, tag: string }|null} Parsed name and tag, or null if invalid
|
|
13
|
+
*/
|
|
14
|
+
function parseImageOverride(imageRef) {
|
|
15
|
+
if (!imageRef || typeof imageRef !== 'string') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const lastColon = imageRef.lastIndexOf(':');
|
|
19
|
+
if (lastColon <= 0) {
|
|
20
|
+
return { name: imageRef.trim(), tag: 'latest' };
|
|
21
|
+
}
|
|
22
|
+
const name = imageRef.substring(0, lastColon).trim();
|
|
23
|
+
const tag = imageRef.substring(lastColon + 1).trim() || 'latest';
|
|
24
|
+
return { name, tag };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { parseImageOverride };
|
package/lib/utils/paths.js
CHANGED
|
@@ -30,18 +30,33 @@ function safeHomedir() {
|
|
|
30
30
|
return process.env.HOME || process.env.USERPROFILE || '/';
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Returns the path to the config file (AIFABRIX_HOME env or ~/.aifabrix).
|
|
35
|
+
* Used so getAifabrixHome can read from the same location as config.js.
|
|
36
|
+
* @returns {string} Absolute path to config directory
|
|
37
|
+
*/
|
|
38
|
+
function getConfigDirForPaths() {
|
|
39
|
+
if (process.env.AIFABRIX_HOME && typeof process.env.AIFABRIX_HOME === 'string') {
|
|
40
|
+
return path.resolve(process.env.AIFABRIX_HOME.trim());
|
|
41
|
+
}
|
|
42
|
+
return path.join(safeHomedir(), '.aifabrix');
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/**
|
|
34
46
|
* Returns the base AI Fabrix directory.
|
|
35
|
-
*
|
|
36
|
-
* Falls back to ~/.aifabrix when not specified.
|
|
47
|
+
* Priority: AIFABRIX_HOME env → config.yaml `aifabrix-home` (from AIFABRIX_HOME or ~/.aifabrix) → ~/.aifabrix.
|
|
37
48
|
*
|
|
38
49
|
* @returns {string} Absolute path to the AI Fabrix home directory
|
|
39
50
|
*/
|
|
40
51
|
function getAifabrixHome() {
|
|
52
|
+
if (process.env.AIFABRIX_HOME && typeof process.env.AIFABRIX_HOME === 'string') {
|
|
53
|
+
return path.resolve(process.env.AIFABRIX_HOME.trim());
|
|
54
|
+
}
|
|
41
55
|
const isTestEnv = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
|
|
42
56
|
if (!isTestEnv) {
|
|
43
57
|
try {
|
|
44
|
-
const
|
|
58
|
+
const configDir = getConfigDirForPaths();
|
|
59
|
+
const configPath = path.join(configDir, 'config.yaml');
|
|
45
60
|
if (fs.existsSync(configPath)) {
|
|
46
61
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
47
62
|
const config = yaml.load(content) || {};
|
|
@@ -278,7 +293,9 @@ function getIntegrationPath(appName) {
|
|
|
278
293
|
}
|
|
279
294
|
|
|
280
295
|
/**
|
|
281
|
-
* Gets the builder folder path for regular applications
|
|
296
|
+
* Gets the builder folder path for regular applications.
|
|
297
|
+
* When AIFABRIX_BUILDER_DIR is set (e.g. by up-miso/up-dataplane from config aifabrix-env-config),
|
|
298
|
+
* uses that as builder root instead of cwd/builder.
|
|
282
299
|
* @param {string} appName - Application name
|
|
283
300
|
* @returns {string} Absolute path to builder directory
|
|
284
301
|
*/
|
|
@@ -286,6 +303,12 @@ function getBuilderPath(appName) {
|
|
|
286
303
|
if (!appName || typeof appName !== 'string') {
|
|
287
304
|
throw new Error('App name is required and must be a string');
|
|
288
305
|
}
|
|
306
|
+
const builderRoot = process.env.AIFABRIX_BUILDER_DIR && typeof process.env.AIFABRIX_BUILDER_DIR === 'string'
|
|
307
|
+
? process.env.AIFABRIX_BUILDER_DIR.trim()
|
|
308
|
+
: null;
|
|
309
|
+
if (builderRoot) {
|
|
310
|
+
return path.join(builderRoot, appName);
|
|
311
|
+
}
|
|
289
312
|
return path.join(process.cwd(), 'builder', appName);
|
|
290
313
|
}
|
|
291
314
|
|
|
@@ -442,6 +465,7 @@ async function detectAppType(appName, options = {}) {
|
|
|
442
465
|
|
|
443
466
|
module.exports = {
|
|
444
467
|
getAifabrixHome,
|
|
468
|
+
getConfigDirForPaths,
|
|
445
469
|
getApplicationsBaseDir,
|
|
446
470
|
getDevDirectory,
|
|
447
471
|
getAppPath,
|
|
@@ -41,6 +41,36 @@ function findMissingSecretKeys(envTemplate, existingSecrets) {
|
|
|
41
41
|
return missingKeys;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Generate database password value for a key (databases-*-passwordKeyVault)
|
|
46
|
+
* @param {string} key - Secret key name
|
|
47
|
+
* @returns {string|null} Password string or null if key does not match
|
|
48
|
+
*/
|
|
49
|
+
function generateDbPasswordValue(key) {
|
|
50
|
+
const dbPasswordMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-passwordKeyVault$/i);
|
|
51
|
+
if (!dbPasswordMatch) return null;
|
|
52
|
+
const appName = dbPasswordMatch[1];
|
|
53
|
+
if (appName === 'miso-controller') return 'miso_pass123';
|
|
54
|
+
const dbName = appName.replace(/-/g, '_');
|
|
55
|
+
return `${dbName}_pass123`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate database URL value for a key (databases-*-urlKeyVault)
|
|
60
|
+
* @param {string} key - Secret key name
|
|
61
|
+
* @returns {string|null} URL string or null if key does not match
|
|
62
|
+
*/
|
|
63
|
+
function generateDbUrlValue(key) {
|
|
64
|
+
const dbUrlMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-urlKeyVault$/i);
|
|
65
|
+
if (!dbUrlMatch) return null;
|
|
66
|
+
const appName = dbUrlMatch[1];
|
|
67
|
+
if (appName === 'miso-controller') {
|
|
68
|
+
return 'postgresql://miso_user:miso_pass123@${DB_HOST}:${DB_PORT}/miso';
|
|
69
|
+
}
|
|
70
|
+
const dbName = appName.replace(/-/g, '_');
|
|
71
|
+
return `postgresql://${dbName}_user:${dbName}_pass123@\${DB_HOST}:\${DB_PORT}/${dbName}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
/**
|
|
45
75
|
* Generates secret value based on key name
|
|
46
76
|
* @function generateSecretValue
|
|
@@ -51,22 +81,14 @@ function generateSecretValue(key) {
|
|
|
51
81
|
const keyLower = key.toLowerCase();
|
|
52
82
|
|
|
53
83
|
if (keyLower.includes('password')) {
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
const appName = dbPasswordMatch[1];
|
|
57
|
-
const dbName = appName.replace(/-/g, '_');
|
|
58
|
-
return `${dbName}_pass123`;
|
|
59
|
-
}
|
|
84
|
+
const dbPassword = generateDbPasswordValue(key);
|
|
85
|
+
if (dbPassword !== null) return dbPassword;
|
|
60
86
|
return crypto.randomBytes(32).toString('base64');
|
|
61
87
|
}
|
|
62
88
|
|
|
63
89
|
if (keyLower.includes('url') || keyLower.includes('uri')) {
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
const appName = dbUrlMatch[1];
|
|
67
|
-
const dbName = appName.replace(/-/g, '_');
|
|
68
|
-
return `postgresql://${dbName}_user:${dbName}_pass123@\${DB_HOST}:\${DB_PORT}/${dbName}`;
|
|
69
|
-
}
|
|
90
|
+
const dbUrl = generateDbUrlValue(key);
|
|
91
|
+
if (dbUrl !== null) return dbUrl;
|
|
70
92
|
return '';
|
|
71
93
|
}
|
|
72
94
|
|
|
@@ -263,8 +263,7 @@ async function getLocalEnvWithOverrides() {
|
|
|
263
263
|
let localEnv = await getEnvHosts('local');
|
|
264
264
|
|
|
265
265
|
try {
|
|
266
|
-
const
|
|
267
|
-
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
266
|
+
const cfgPath = config.CONFIG_FILE;
|
|
268
267
|
if (fs.existsSync(cfgPath)) {
|
|
269
268
|
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
270
269
|
const cfg = yaml.load(cfgContent) || {};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const config = require('../core/config');
|
|
12
12
|
const { refreshDeviceToken: apiRefreshDeviceToken } = require('./api');
|
|
13
|
+
const { isTokenEncrypted } = require('./token-encryption');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Validates refresh token parameters
|
|
@@ -138,6 +139,10 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
|
138
139
|
if (!refreshToken || typeof refreshToken !== 'string') {
|
|
139
140
|
throw new Error('Refresh token is required');
|
|
140
141
|
}
|
|
142
|
+
// Never send encrypted token to the API (causes 401). Decryption should happen in getDeviceToken; this is a safeguard.
|
|
143
|
+
if (isTokenEncrypted(refreshToken)) {
|
|
144
|
+
throw new Error('Refresh token is still encrypted; decryption may have failed. Run "aifabrix login" to authenticate again.');
|
|
145
|
+
}
|
|
141
146
|
|
|
142
147
|
try {
|
|
143
148
|
// Call API refresh endpoint
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# AI Fabrix Dataplane - Build from base image
|
|
2
|
+
# This repo has no application source; use the published image as baseline.
|
|
3
|
+
# Build: docker build -t aifabrix/dataplane:local .
|
|
4
|
+
# Or use the image directly: docker run aifabrix/dataplane:latest
|
|
5
|
+
|
|
6
|
+
FROM aifabrix/dataplane:latest
|
|
7
|
+
|
|
8
|
+
# Expose port (documentation; base image may already set this)
|
|
9
|
+
EXPOSE 3001
|
|
10
|
+
|
|
11
|
+
# Health check (documentation; base image may already set this)
|
|
12
|
+
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
|
13
|
+
CMD curl -f http://localhost:3001/health || exit 1
|
|
14
|
+
|
|
15
|
+
# CMD inherited from base image; override only if needed
|
|
16
|
+
# CMD inherited: uvicorn app.main:app --host 0.0.0.0 --port 3001
|