@aifabrix/builder 2.10.1 → 2.11.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.
@@ -48,13 +48,13 @@ aifabrix validate hubspot
48
48
 
49
49
  ```bash
50
50
  # Login to controller
51
- aifabrix login --controller https://controller.aifabrix.ai --method device --environment dev
51
+ aifabrix login --controller http://localhost:3100 --method device --environment dev
52
52
 
53
53
  # Register application
54
54
  aifabrix app register hubspot --environment dev
55
55
 
56
56
  # Deploy entire system
57
- aifabrix deploy hubspot --controller https://controller.aifabrix.ai --environment dev
57
+ aifabrix deploy hubspot --controller http://localhost:3100 --environment dev
58
58
 
59
59
  # Or deploy individual datasources for testing
60
60
  aifabrix datasource deploy hubspot-company --environment dev --file integration/hubspot/hubspot-deploy-company.json
@@ -58,7 +58,11 @@
58
58
  "field": "select",
59
59
  "label": "HubSpot API Version",
60
60
  "placeholder": "Select API version",
61
- "options": ["v1", "v2", "v3"],
61
+ "options": [
62
+ "v1",
63
+ "v2",
64
+ "v3"
65
+ ],
62
66
  "validation": {
63
67
  "required": false
64
68
  }
@@ -86,6 +90,10 @@
86
90
  "documentKey": "hubspot-v3",
87
91
  "autoDiscoverEntities": false
88
92
  },
89
- "tags": ["crm", "sales", "marketing", "hubspot"]
90
- }
91
-
93
+ "tags": [
94
+ "crm",
95
+ "sales",
96
+ "marketing",
97
+ "hubspot"
98
+ ]
99
+ }
@@ -8,376 +8,98 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
- const fs = require('fs').promises;
12
- const path = require('path');
13
11
  const chalk = require('chalk');
14
- const yaml = require('js-yaml');
15
- const { getConfig } = require('./config');
16
- const { authenticatedApiCall } = require('./utils/api');
17
- const { formatApiError } = require('./utils/api-error-handler');
18
12
  const logger = require('./utils/logger');
19
13
  const { saveLocalSecret, isLocalhost } = require('./utils/local-secrets');
20
14
  const { updateEnvTemplate } = require('./utils/env-template');
21
- const { getOrRefreshDeviceToken } = require('./utils/token-manager');
22
- const { detectAppType } = require('./utils/paths');
23
15
  const { generateEnvFile } = require('./secrets');
24
- const applicationSchema = require('./schema/application-schema.json');
25
-
26
- // Import createApp to auto-generate config if missing
27
- let createApp;
28
- try {
29
- createApp = require('./app').createApp;
30
- } catch {
31
- createApp = null;
32
- }
33
-
34
- // Extract valid enum values from application schema
35
- const validTypes = applicationSchema.properties.type.enum || [];
36
- const validRegistryModes = applicationSchema.properties.registryMode.enum || [];
37
- const portConstraints = {
38
- minimum: applicationSchema.properties.port?.minimum || 1,
39
- maximum: applicationSchema.properties.port?.maximum || 65535
40
- };
41
-
42
- /**
43
- * Validation schema for application registration
44
- * Validates according to application-schema.json
45
- */
46
- const registerApplicationSchema = {
47
- environmentId: (val) => {
48
- if (!val || val.length < 1) {
49
- throw new Error('Invalid environment ID format');
50
- }
51
- return val;
52
- },
53
- key: (val) => {
54
- if (!val || val.length < 1) {
55
- throw new Error('Application key is required');
56
- }
57
- const keyPattern = applicationSchema.properties.key.pattern;
58
- const keyMaxLength = applicationSchema.properties.key.maxLength || 50;
59
- if (val.length > keyMaxLength) {
60
- throw new Error(`Application key must be at most ${keyMaxLength} characters`);
61
- }
62
- if (keyPattern && !new RegExp(keyPattern).test(val)) {
63
- throw new Error('Application key must contain only lowercase letters, numbers, and hyphens');
64
- }
65
- return val;
66
- },
67
- displayName: (val) => {
68
- if (!val || val.length < 1) {
69
- throw new Error('Display name is required');
70
- }
71
- const displayNameMaxLength = applicationSchema.properties.displayName.maxLength || 100;
72
- if (val.length > displayNameMaxLength) {
73
- throw new Error(`Display name must be at most ${displayNameMaxLength} characters`);
74
- }
75
- return val;
76
- },
77
- description: (val) => val || undefined,
78
- configuration: (val) => {
79
- if (!val || !val.type || !validTypes.includes(val.type)) {
80
- throw new Error(`Configuration type must be one of: ${validTypes.join(', ')}`);
81
- }
82
- if (!val.registryMode || !validRegistryModes.includes(val.registryMode)) {
83
- throw new Error(`Registry mode must be one of: ${validRegistryModes.join(', ')}`);
84
- }
85
- // Port validation: skip for external type (external systems don't need ports)
86
- // For other types, port is required and must be valid
87
- if (val.type !== 'external') {
88
- if (val.port === undefined || val.port === null) {
89
- throw new Error('Port is required for non-external application types');
90
- }
91
- if (!Number.isInteger(val.port) || val.port < portConstraints.minimum || val.port > portConstraints.maximum) {
92
- throw new Error(`Port must be an integer between ${portConstraints.minimum} and ${portConstraints.maximum}`);
93
- }
94
- }
95
- return val;
96
- }
97
- };
16
+ const { registerApplicationSchema, validateAppRegistrationData } = require('./utils/app-register-validator');
17
+ const {
18
+ loadVariablesYaml,
19
+ createMinimalAppIfNeeded,
20
+ extractAppConfiguration
21
+ } = require('./utils/app-register-config');
22
+ const { checkAuthentication } = require('./utils/app-register-auth');
23
+ const { callRegisterApi } = require('./utils/app-register-api');
24
+ const { displayRegistrationResults, getEnvironmentPrefix } = require('./utils/app-register-display');
98
25
 
99
26
  /**
100
- * Load variables.yaml file for an application
101
- * @async
102
- * @param {string} appKey - Application key
103
- * @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
104
- */
105
- async function loadVariablesYaml(appKey) {
106
- // Detect app type and get correct path (integration or builder)
107
- const { appPath } = await detectAppType(appKey);
108
- const variablesPath = path.join(appPath, 'variables.yaml');
109
-
110
- try {
111
- const variablesContent = await fs.readFile(variablesPath, 'utf-8');
112
- return { variables: yaml.load(variablesContent), created: false };
113
- } catch (error) {
114
- if (error.code === 'ENOENT') {
115
- logger.log(chalk.yellow(`āš ļø variables.yaml not found for ${appKey}`));
116
- logger.log(chalk.yellow('šŸ“ Creating minimal configuration...\n'));
117
- return { variables: null, created: true };
118
- }
119
- throw new Error(`Failed to read variables.yaml: ${error.message}`);
120
- }
121
- }
122
-
123
- /**
124
- * Create minimal application configuration if needed
125
- * @async
126
- * @param {string} appKey - Application key
27
+ * Build registration data payload from app configuration
28
+ * @param {Object} appConfig - Application configuration
127
29
  * @param {Object} options - Registration options
128
- * @returns {Promise<Object>} Variables after creation
30
+ * @returns {Object} Registration data payload
129
31
  */
130
- async function createMinimalAppIfNeeded(appKey, options) {
131
- if (!createApp) {
132
- throw new Error('Cannot auto-create application: createApp function not available');
133
- }
134
-
135
- await createApp(appKey, {
136
- port: options.port,
137
- language: 'typescript',
138
- database: false,
139
- redis: false,
140
- storage: false,
141
- authentication: false
142
- });
143
-
144
- // Detect app type and get correct path (integration or builder)
145
- const { appPath } = await detectAppType(appKey);
146
- const variablesPath = path.join(appPath, 'variables.yaml');
147
- const variablesContent = await fs.readFile(variablesPath, 'utf-8');
148
- return yaml.load(variablesContent);
149
- }
150
-
151
- /**
152
- * Extract application configuration from variables.yaml
153
- * @param {Object} variables - Variables from YAML file
154
- * @param {string} appKey - Application key
155
- * @param {Object} options - Registration options
156
- * @returns {Object} Extracted configuration
157
- */
158
- function extractAppConfiguration(variables, appKey, options) {
159
- const appKeyFromFile = variables.app?.key || appKey;
160
- const displayName = variables.app?.name || options.name || appKey;
161
- const description = variables.app?.description || '';
162
-
163
- // Handle external type
164
- if (variables.app?.type === 'external') {
165
- return {
166
- appKey: appKeyFromFile,
167
- displayName,
168
- description,
169
- appType: 'external',
170
- registryMode: 'external',
171
- port: null, // External systems don't need ports
172
- language: null // External systems don't need language
173
- };
174
- }
175
-
176
- const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
177
- const registryMode = 'external';
178
- const port = variables.build?.port || options.port || 3000;
179
- const language = variables.build?.language || 'typescript';
180
-
181
- return {
182
- appKey: appKeyFromFile,
183
- displayName,
184
- description,
185
- appType,
186
- registryMode,
187
- port,
188
- language
32
+ function buildRegistrationData(appConfig, options) {
33
+ const registrationData = {
34
+ key: appConfig.appKey,
35
+ displayName: appConfig.displayName,
36
+ type: appConfig.appType
189
37
  };
190
- }
191
-
192
- /**
193
- * Validate application registration data
194
- * @async
195
- * @param {Object} config - Application configuration
196
- * @param {string} originalAppKey - Original app key for error messages
197
- * @throws {Error} If validation fails
198
- */
199
- async function validateAppRegistrationData(config, originalAppKey) {
200
- const missingFields = [];
201
- if (!config.appKey) missingFields.push('app.key');
202
- if (!config.displayName) missingFields.push('app.name');
203
-
204
- if (missingFields.length > 0) {
205
- logger.error(chalk.red('āŒ Missing required fields in variables.yaml:'));
206
- missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
207
- // Detect app type to show correct path
208
- const { appPath } = await detectAppType(originalAppKey);
209
- const relativePath = path.relative(process.cwd(), appPath);
210
- logger.error(chalk.red(`\n Please update ${relativePath}/variables.yaml and try again.`));
211
- process.exit(1);
212
- }
213
38
 
214
- try {
215
- registerApplicationSchema.key(config.appKey);
216
- registerApplicationSchema.displayName(config.displayName);
217
- registerApplicationSchema.configuration({
218
- type: config.appType,
219
- registryMode: config.registryMode,
220
- port: config.port
221
- });
222
- } catch (error) {
223
- logger.error(chalk.red(`āŒ Invalid configuration: ${error.message}`));
224
- process.exit(1);
39
+ // Add optional fields only if they have values
40
+ if (appConfig.description || options.description) {
41
+ registrationData.description = appConfig.description || options.description;
225
42
  }
226
- }
227
-
228
- /**
229
- * Check if user is authenticated and get token
230
- * @async
231
- * @param {string} [controllerUrl] - Optional controller URL from variables.yaml
232
- * @param {string} [environment] - Optional environment key
233
- * @returns {Promise<{apiUrl: string, token: string}>} Configuration with API URL and token
234
- */
235
- async function checkAuthentication(controllerUrl, environment) {
236
- const config = await getConfig();
237
-
238
- // Try to get controller URL from parameter, config, or device tokens
239
- let finalControllerUrl = controllerUrl;
240
- let token = null;
241
43
 
242
- // If controller URL provided, try to get device token
243
- if (finalControllerUrl) {
244
- const deviceToken = await getOrRefreshDeviceToken(finalControllerUrl);
245
- if (deviceToken && deviceToken.token) {
246
- token = deviceToken.token;
247
- finalControllerUrl = deviceToken.controller;
44
+ // Handle external type vs non-external types differently
45
+ if (appConfig.appType === 'external') {
46
+ // For external type: include externalIntegration, exclude registryMode/port/image
47
+ if (appConfig.externalIntegration) {
48
+ registrationData.externalIntegration = appConfig.externalIntegration;
248
49
  }
249
- }
50
+ } else {
51
+ // For non-external types: include registryMode, port, image
52
+ registrationData.registryMode = appConfig.registryMode;
250
53
 
251
- // If no token yet, try to find any device token in config
252
- if (!token && config.device) {
253
- const deviceUrls = Object.keys(config.device);
254
- if (deviceUrls.length > 0) {
255
- // Use first available device token
256
- finalControllerUrl = deviceUrls[0];
257
- const deviceToken = await getOrRefreshDeviceToken(finalControllerUrl);
258
- if (deviceToken && deviceToken.token) {
259
- token = deviceToken.token;
260
- finalControllerUrl = deviceToken.controller;
261
- }
54
+ // Port is required for non-external types
55
+ if (appConfig.port) {
56
+ registrationData.port = appConfig.port;
262
57
  }
263
- }
264
-
265
- // If still no token, check for client token (requires environment and app)
266
- if (!token && environment) {
267
- // For app register, we don't have an app yet, so client tokens won't work
268
- // This is expected - device tokens should be used for registration
269
- }
270
58
 
271
- if (!token || !finalControllerUrl) {
272
- logger.error(chalk.red('āŒ Not logged in. Run: aifabrix login'));
273
- logger.error(chalk.gray(' Use device code flow: aifabrix login --method device --controller <url>'));
274
- process.exit(1);
59
+ // Image is required for non-external types
60
+ if (appConfig.image) {
61
+ registrationData.image = appConfig.image;
62
+ }
275
63
  }
276
64
 
277
- return {
278
- apiUrl: finalControllerUrl,
279
- token: token
280
- };
65
+ return registrationData;
281
66
  }
282
67
 
283
68
  /**
284
- * Call registration API
69
+ * Save credentials to local secrets if localhost
285
70
  * @async
71
+ * @param {Object} responseData - Registration response data
286
72
  * @param {string} apiUrl - API URL
287
- * @param {string} token - Authentication token
288
- * @param {string} environment - Environment ID
289
- * @param {Object} registrationData - Registration data
290
- * @returns {Promise<Object>} API response
291
73
  */
292
- async function callRegisterApi(apiUrl, token, environment, registrationData) {
293
- const response = await authenticatedApiCall(
294
- `${apiUrl}/api/v1/environments/${encodeURIComponent(environment)}/applications/register`,
295
- {
296
- method: 'POST',
297
- body: JSON.stringify(registrationData)
298
- },
299
- token
300
- );
301
-
302
- if (!response.success) {
303
- const formattedError = response.formattedError || formatApiError(response);
304
- logger.error(formattedError);
305
- process.exit(1);
74
+ async function saveLocalCredentials(responseData, apiUrl) {
75
+ if (!isLocalhost(apiUrl)) {
76
+ return;
306
77
  }
307
78
 
308
- // Handle API response structure:
309
- // makeApiCall returns: { success: true, data: <API response> }
310
- // API response can be:
311
- // 1. Direct format: { application: {...}, credentials: {...} }
312
- // 2. Wrapped format: { success: true, data: { application: {...}, credentials: {...} } }
313
- const apiResponse = response.data;
314
- if (apiResponse && apiResponse.data && apiResponse.data.application) {
315
- // Wrapped format: use apiResponse.data
316
- return apiResponse.data;
317
- } else if (apiResponse && apiResponse.application) {
318
- // Direct format: use apiResponse directly
319
- return apiResponse;
320
- }
321
- // Fallback: return apiResponse as-is (shouldn't happen, but handle gracefully)
322
- logger.error(chalk.red('āŒ Invalid response: missing application data'));
323
- logger.error(chalk.gray('\nFull response for debugging:'));
324
- logger.error(chalk.gray(JSON.stringify(response, null, 2)));
325
- process.exit(1);
326
-
327
- }
328
-
329
- /**
330
- * Get environment prefix for GitHub Secrets
331
- * @param {string} environment - Environment key (e.g., 'dev', 'tst', 'pro', 'miso')
332
- * @returns {string} Uppercase prefix (e.g., 'DEV', 'TST', 'PRO', 'MISO')
333
- */
334
- function getEnvironmentPrefix(environment) {
335
- if (!environment) {
336
- return 'DEV';
337
- }
338
- // Convert to uppercase and handle common variations
339
- const env = environment.toLowerCase();
340
- if (env === 'dev' || env === 'development') {
341
- return 'DEV';
342
- }
343
- if (env === 'tst' || env === 'test' || env === 'staging') {
344
- return 'TST';
345
- }
346
- if (env === 'pro' || env === 'prod' || env === 'production') {
347
- return 'PRO';
348
- }
349
- // For other environments (e.g., 'miso'), uppercase the entire string
350
- // Use full string if 4 characters or less, otherwise use first 4 characters
351
- const upper = environment.toUpperCase();
352
- return upper.length <= 4 ? upper : upper.substring(0, 4);
353
- }
79
+ const registeredAppKey = responseData.application.key;
80
+ const clientIdKey = `${registeredAppKey}-client-idKeyVault`;
81
+ const clientSecretKey = `${registeredAppKey}-client-secretKeyVault`;
354
82
 
355
- /**
356
- * Display registration success and credentials
357
- * @param {Object} data - Registration response data
358
- * @param {string} apiUrl - API URL
359
- * @param {string} environment - Environment key
360
- */
361
- function displayRegistrationResults(data, apiUrl, environment) {
362
- logger.log(chalk.green('āœ… Application registered successfully!\n'));
363
- logger.log(chalk.bold('šŸ“‹ Application Details:'));
364
- logger.log(` ID: ${data.application.id}`);
365
- logger.log(` Key: ${data.application.key}`);
366
- logger.log(` Display Name: ${data.application.displayName}\n`);
83
+ try {
84
+ await saveLocalSecret(clientIdKey, responseData.credentials.clientId);
85
+ await saveLocalSecret(clientSecretKey, responseData.credentials.clientSecret);
367
86
 
368
- logger.log(chalk.bold.yellow('šŸ”‘ CREDENTIALS (save these immediately):'));
369
- logger.log(chalk.yellow(` Client ID: ${data.credentials.clientId}`));
370
- logger.log(chalk.yellow(` Client Secret: ${data.credentials.clientSecret}\n`));
87
+ // Update env.template
88
+ await updateEnvTemplate(registeredAppKey, clientIdKey, clientSecretKey, apiUrl);
371
89
 
372
- logger.log(chalk.red('āš ļø IMPORTANT: Client Secret will not be shown again!\n'));
90
+ // Regenerate .env file with updated credentials
91
+ try {
92
+ await generateEnvFile(registeredAppKey, null, 'local');
93
+ logger.log(chalk.green('āœ“ .env file updated with new credentials'));
94
+ } catch (error) {
95
+ logger.warn(chalk.yellow(`āš ļø Could not regenerate .env file: ${error.message}`));
96
+ }
373
97
 
374
- const envPrefix = getEnvironmentPrefix(environment);
375
- logger.log(chalk.bold('šŸ“ Add to GitHub Secrets:'));
376
- logger.log(chalk.cyan(' Repository level:'));
377
- logger.log(chalk.cyan(` MISO_CONTROLLER_URL = ${apiUrl}`));
378
- logger.log(chalk.cyan(`\n Environment level (${environment}):`));
379
- logger.log(chalk.cyan(` ${envPrefix}_MISO_CLIENTID = ${data.credentials.clientId}`));
380
- logger.log(chalk.cyan(` ${envPrefix}_MISO_CLIENTSECRET = ${data.credentials.clientSecret}\n`));
98
+ logger.log(chalk.green('\nāœ“ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
99
+ logger.log(chalk.green('āœ“ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
100
+ } catch (error) {
101
+ logger.warn(chalk.yellow(`āš ļø Could not save credentials locally: ${error.message}`));
102
+ }
381
103
  }
382
104
 
383
105
  /**
@@ -396,49 +118,21 @@ async function registerApplication(appKey, options) {
396
118
 
397
119
  // Load variables.yaml
398
120
  const { variables, created } = await loadVariablesYaml(appKey);
399
- let finalVariables = variables;
400
-
401
- // Create minimal app if needed
402
- if (created) {
403
- finalVariables = await createMinimalAppIfNeeded(appKey, options);
404
- }
121
+ const finalVariables = created
122
+ ? await createMinimalAppIfNeeded(appKey, options)
123
+ : variables;
405
124
 
406
- // Extract configuration
407
- const appConfig = extractAppConfiguration(finalVariables, appKey, options);
408
-
409
- // Validate configuration (pass original appKey for error messages)
125
+ // Extract and validate configuration
126
+ const appConfig = await extractAppConfiguration(finalVariables, appKey, options);
410
127
  await validateAppRegistrationData(appConfig, appKey);
411
128
 
412
- // Get controller URL from variables.yaml if available
129
+ // Authenticate and get API configuration
413
130
  const controllerUrl = finalVariables?.deployment?.controllerUrl;
414
-
415
- // Check authentication (try device token first, supports registration flow)
416
131
  const authConfig = await checkAuthentication(controllerUrl, options.environment);
417
-
418
- // Validate environment
419
132
  const environment = registerApplicationSchema.environmentId(options.environment);
420
133
 
421
- // Prepare registration data to match OpenAPI RegisterApplicationRequest schema
422
- // Schema: { key, displayName, description?, configuration: { type, registryMode, port?, image? } }
423
- const registrationData = {
424
- key: appConfig.appKey,
425
- displayName: appConfig.displayName,
426
- configuration: {
427
- type: appConfig.appType,
428
- registryMode: appConfig.registryMode
429
- }
430
- };
431
-
432
- // Add optional fields only if they have values
433
- if (appConfig.description || options.description) {
434
- registrationData.description = appConfig.description || options.description;
435
- }
436
-
437
- if (appConfig.port) {
438
- registrationData.configuration.port = appConfig.port;
439
- }
440
-
441
134
  // Register application
135
+ const registrationData = buildRegistrationData(appConfig, options);
442
136
  const responseData = await callRegisterApi(
443
137
  authConfig.apiUrl,
444
138
  authConfig.token,
@@ -446,35 +140,8 @@ async function registerApplication(appKey, options) {
446
140
  registrationData
447
141
  );
448
142
 
449
- // Save credentials to local secrets if localhost
450
- if (isLocalhost(authConfig.apiUrl)) {
451
- const registeredAppKey = responseData.application.key;
452
- const clientIdKey = `${registeredAppKey}-client-idKeyVault`;
453
- const clientSecretKey = `${registeredAppKey}-client-secretKeyVault`;
454
-
455
- try {
456
- await saveLocalSecret(clientIdKey, responseData.credentials.clientId);
457
- await saveLocalSecret(clientSecretKey, responseData.credentials.clientSecret);
458
-
459
- // Update env.template
460
- await updateEnvTemplate(registeredAppKey, clientIdKey, clientSecretKey, authConfig.apiUrl);
461
-
462
- // Regenerate .env file with updated credentials
463
- try {
464
- await generateEnvFile(registeredAppKey, null, 'local');
465
- logger.log(chalk.green('āœ“ .env file updated with new credentials'));
466
- } catch (error) {
467
- logger.warn(chalk.yellow(`āš ļø Could not regenerate .env file: ${error.message}`));
468
- }
469
-
470
- logger.log(chalk.green('\nāœ“ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
471
- logger.log(chalk.green('āœ“ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
472
- } catch (error) {
473
- logger.warn(chalk.yellow(`āš ļø Could not save credentials locally: ${error.message}`));
474
- }
475
- }
476
-
477
- // Display results
143
+ // Save credentials and display results
144
+ await saveLocalCredentials(responseData, authConfig.apiUrl);
478
145
  displayRegistrationResults(responseData, authConfig.apiUrl, environment);
479
146
  }
480
147