@aifabrix/builder 2.32.2 → 2.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.cursor/rules/project-rules.mdc +8 -0
  2. package/README.md +36 -8
  3. package/bin/aifabrix.js +6 -8
  4. package/integration/hubspot/README.md +8 -7
  5. package/integration/hubspot/companies.json +2048 -0
  6. package/integration/hubspot/create-hubspot.js +665 -0
  7. package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
  8. package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
  9. package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
  10. package/integration/hubspot/hubspot-deploy.json +832 -81
  11. package/integration/hubspot/hubspot-system.json +99 -0
  12. package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
  13. package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
  14. package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
  15. package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
  16. package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
  17. package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
  18. package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
  19. package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
  20. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
  22. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
  23. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
  24. package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
  25. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
  26. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
  29. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
  30. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
  31. package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
  32. package/integration/hubspot/test-dataplane-down-tests.js +419 -0
  33. package/integration/hubspot/test-dataplane-down.js +157 -0
  34. package/integration/hubspot/test.js +1517 -0
  35. package/integration/hubspot/variables.yaml +4 -4
  36. package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
  37. package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
  38. package/lib/api/applications.api.js +1 -0
  39. package/lib/api/index.js +10 -5
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +207 -38
  42. package/lib/app/deploy.js +116 -54
  43. package/lib/app/display.js +6 -5
  44. package/lib/app/dockerfile.js +2 -1
  45. package/lib/app/list.js +78 -37
  46. package/lib/app/prompts.js +9 -5
  47. package/lib/app/readme.js +41 -112
  48. package/lib/app/register.js +44 -9
  49. package/lib/app/rotate-secret.js +50 -32
  50. package/lib/cli.js +243 -65
  51. package/lib/commands/app.js +4 -9
  52. package/lib/commands/auth-config.js +125 -0
  53. package/lib/commands/auth-status.js +261 -0
  54. package/lib/commands/datasource.js +3 -6
  55. package/lib/commands/login-credentials.js +4 -4
  56. package/lib/commands/login-device.js +43 -29
  57. package/lib/commands/login.js +22 -13
  58. package/lib/commands/wizard-config-normalizer.js +92 -0
  59. package/lib/commands/wizard-core.js +515 -0
  60. package/lib/commands/wizard-dataplane.js +122 -0
  61. package/lib/commands/wizard-headless.js +115 -0
  62. package/lib/commands/wizard.js +129 -357
  63. package/lib/core/config.js +46 -0
  64. package/lib/core/secrets.js +3 -22
  65. package/lib/core/templates-env.js +1 -1
  66. package/lib/datasource/deploy.js +34 -23
  67. package/lib/datasource/list.js +8 -6
  68. package/lib/deployment/deployer.js +25 -0
  69. package/lib/deployment/environment.js +10 -13
  70. package/lib/external-system/delete.js +151 -0
  71. package/lib/external-system/deploy.js +54 -378
  72. package/lib/external-system/download-helpers.js +45 -65
  73. package/lib/external-system/download.js +34 -13
  74. package/lib/external-system/generator.js +11 -7
  75. package/lib/external-system/test-auth.js +5 -3
  76. package/lib/generator/builders.js +3 -1
  77. package/lib/generator/external-controller-manifest.js +157 -0
  78. package/lib/generator/external-schema-utils.js +236 -0
  79. package/lib/generator/external.js +55 -3
  80. package/lib/generator/index.js +22 -10
  81. package/lib/generator/wizard-prompts.js +33 -10
  82. package/lib/generator/wizard.js +69 -86
  83. package/lib/infrastructure/compose.js +100 -0
  84. package/lib/infrastructure/helpers.js +139 -0
  85. package/lib/infrastructure/index.js +52 -311
  86. package/lib/infrastructure/services.js +168 -0
  87. package/lib/schema/application-schema.json +24 -5
  88. package/lib/schema/external-datasource.schema.json +303 -17
  89. package/lib/schema/external-system.schema.json +1 -1
  90. package/lib/schema/wizard-config.schema.json +234 -0
  91. package/lib/utils/api.js +37 -42
  92. package/lib/utils/app-existence.js +42 -0
  93. package/lib/utils/app-register-config.js +7 -2
  94. package/lib/utils/app-register-display.js +2 -1
  95. package/lib/utils/auth-config-validator.js +92 -0
  96. package/lib/utils/cli-utils.js +3 -1
  97. package/lib/utils/command-header.js +43 -0
  98. package/lib/utils/compose-generator.js +113 -70
  99. package/lib/utils/controller-url.js +115 -0
  100. package/lib/utils/dataplane-health.js +115 -0
  101. package/lib/utils/dataplane-resolver.js +29 -0
  102. package/lib/utils/dev-config.js +6 -2
  103. package/lib/utils/env-copy.js +2 -1
  104. package/lib/utils/env-map.js +2 -1
  105. package/lib/utils/env-ports.js +2 -1
  106. package/lib/utils/env-template.js +1 -1
  107. package/lib/utils/error-formatter.js +149 -28
  108. package/lib/utils/external-readme.js +125 -0
  109. package/lib/utils/help-builder.js +190 -0
  110. package/lib/utils/infra-status.js +13 -3
  111. package/lib/utils/paths.js +17 -2
  112. package/lib/utils/port-resolver.js +111 -0
  113. package/lib/utils/secrets-helpers.js +3 -15
  114. package/lib/utils/secrets-utils.js +2 -2
  115. package/lib/utils/token-manager.js +69 -4
  116. package/lib/utils/variable-transformer.js +7 -2
  117. package/lib/validation/external-manifest-validator.js +202 -0
  118. package/lib/validation/validate-display.js +406 -0
  119. package/lib/validation/validate.js +159 -123
  120. package/lib/validation/validator.js +38 -4
  121. package/lib/validation/wizard-config-validator.js +267 -0
  122. package/package.json +4 -2
  123. package/templates/applications/README.md.hbs +19 -17
  124. package/templates/applications/miso-controller/env.template +1 -1
  125. package/templates/applications/miso-controller/rbac.yaml +7 -7
  126. package/templates/external-system/README.md.hbs +99 -0
  127. package/templates/external-system/external-system.json.hbs +1 -1
  128. package/templates/infra/compose.yaml.hbs +35 -0
  129. package/templates/python/docker-compose.hbs +26 -0
  130. package/templates/typescript/docker-compose.hbs +26 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * AI Fabrix Builder - Auth Status Command
3
+ *
4
+ * Displays authentication status for the current controller and environment
5
+ *
6
+ * @fileoverview Authentication status command implementation
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const chalk = require('chalk');
12
+ const logger = require('../utils/logger');
13
+ const config = require('../core/config');
14
+ const { getConfig } = config;
15
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
16
+ const { getAuthUser } = require('../api/auth.api');
17
+ const { resolveControllerUrl } = require('../utils/controller-url');
18
+
19
+ /**
20
+ * Format expiration date for display
21
+ * @param {string} expiresAt - ISO 8601 expiration timestamp
22
+ * @returns {string} Formatted expiration string
23
+ */
24
+ function formatExpiration(expiresAt) {
25
+ if (!expiresAt) {
26
+ return 'Unknown';
27
+ }
28
+ try {
29
+ const date = new Date(expiresAt);
30
+ return date.toISOString();
31
+ } catch {
32
+ return expiresAt;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Check and validate device token
38
+ * @async
39
+ * @param {string} controllerUrl - Controller URL
40
+ * @returns {Promise<Object|null>} Token validation result or null
41
+ */
42
+ async function checkDeviceToken(controllerUrl) {
43
+ const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
44
+ if (!deviceToken || !deviceToken.token) {
45
+ return null;
46
+ }
47
+
48
+ try {
49
+ const authConfig = { type: 'bearer', token: deviceToken.token };
50
+ // Use getAuthUser instead of validateToken - it's more reliable and tests actual API access
51
+ const { getAuthUser } = require('../api/auth.api');
52
+ const response = await getAuthUser(controllerUrl, authConfig);
53
+
54
+ if (response.success && response.data) {
55
+ return {
56
+ type: 'Device Token',
57
+ token: deviceToken.token,
58
+ authenticated: response.data.authenticated !== false,
59
+ user: response.data.user,
60
+ expiresAt: deviceToken.expiresAt
61
+ };
62
+ }
63
+
64
+ return {
65
+ type: 'Device Token',
66
+ token: deviceToken.token,
67
+ authenticated: false,
68
+ error: response.error || response.formattedError || 'Token validation failed'
69
+ };
70
+ } catch (error) {
71
+ return {
72
+ type: 'Device Token',
73
+ token: deviceToken.token,
74
+ authenticated: false,
75
+ error: error.message || 'Token validation error'
76
+ };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Decrypt token if encrypted
82
+ * @async
83
+ * @param {string} token - Token to decrypt
84
+ * @returns {Promise<string>} Decrypted token
85
+ */
86
+ async function decryptTokenIfNeeded(token) {
87
+ const { decryptToken, isTokenEncrypted } = require('../utils/token-encryption');
88
+ const encryptionKey = await config.getSecretsEncryptionKey();
89
+
90
+ if (encryptionKey && isTokenEncrypted(token)) {
91
+ return await decryptToken(token, encryptionKey);
92
+ }
93
+ return token;
94
+ }
95
+
96
+ /**
97
+ * Validate client token and return result
98
+ * @async
99
+ * @param {string} token - Token to validate
100
+ * @param {string} controllerUrl - Controller URL
101
+ * @param {string} environment - Environment key
102
+ * @param {string} appName - Application name
103
+ * @param {string} expiresAt - Token expiration
104
+ * @returns {Promise<Object>} Token validation result
105
+ */
106
+ async function validateClientToken(token, controllerUrl, environment, appName, expiresAt) {
107
+ try {
108
+ const authConfig = { type: 'bearer', token: token };
109
+ // Use getAuthUser instead of validateToken - it's more reliable and tests actual API access
110
+ const response = await getAuthUser(controllerUrl, authConfig);
111
+
112
+ if (response.success && response.data) {
113
+ return {
114
+ type: 'Client Token',
115
+ token: token,
116
+ authenticated: response.data.authenticated !== false,
117
+ user: response.data.user,
118
+ expiresAt: expiresAt,
119
+ appName: appName
120
+ };
121
+ }
122
+
123
+ return {
124
+ type: 'Client Token',
125
+ token: token,
126
+ authenticated: false,
127
+ error: response.error || response.formattedError || 'Token validation failed',
128
+ appName: appName
129
+ };
130
+ } catch (error) {
131
+ return {
132
+ type: 'Client Token',
133
+ token: '***',
134
+ authenticated: false,
135
+ error: error.message || 'Token validation error',
136
+ appName: appName
137
+ };
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Check and validate client token
143
+ * @async
144
+ * @param {string} controllerUrl - Controller URL
145
+ * @param {string} environment - Environment key
146
+ * @returns {Promise<Object|null>} Token validation result or null
147
+ */
148
+ async function checkClientToken(controllerUrl, environment) {
149
+ const configData = await getConfig();
150
+ const environments = configData.environments || {};
151
+ const envConfig = environments[environment];
152
+
153
+ if (!envConfig || !envConfig.clients) {
154
+ return null;
155
+ }
156
+
157
+ for (const [appName, tokenData] of Object.entries(envConfig.clients)) {
158
+ if (tokenData.controller === controllerUrl && tokenData.token) {
159
+ const token = await decryptTokenIfNeeded(tokenData.token);
160
+ return await validateClientToken(token, controllerUrl, environment, appName, tokenData.expiresAt);
161
+ }
162
+ }
163
+
164
+ return null;
165
+ }
166
+
167
+ /**
168
+ * Display user information
169
+ * @param {Object} user - User object
170
+ */
171
+ function displayUserInfo(user) {
172
+ if (!user) {
173
+ return;
174
+ }
175
+
176
+ logger.log('');
177
+ logger.log(chalk.bold('User Information:'));
178
+ if (user.email) {
179
+ logger.log(` Email: ${chalk.cyan(user.email)}`);
180
+ }
181
+ if (user.username) {
182
+ logger.log(` Username: ${chalk.cyan(user.username)}`);
183
+ }
184
+ if (user.id) {
185
+ logger.log(` ID: ${chalk.gray(user.id)}`);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Display token information
191
+ * @param {Object} tokenInfo - Token information
192
+ */
193
+ function displayTokenInfo(tokenInfo) {
194
+ const statusIcon = tokenInfo.authenticated ? chalk.green('✓') : chalk.red('✗');
195
+ const statusText = tokenInfo.authenticated ? 'Authenticated' : 'Not authenticated';
196
+
197
+ logger.log(`Status: ${statusIcon} ${statusText}`);
198
+ logger.log(`Token Type: ${chalk.cyan(tokenInfo.type)}`);
199
+
200
+ if (tokenInfo.appName) {
201
+ logger.log(`Application: ${chalk.cyan(tokenInfo.appName)}`);
202
+ }
203
+
204
+ if (tokenInfo.expiresAt) {
205
+ logger.log(`Expires: ${chalk.gray(formatExpiration(tokenInfo.expiresAt))}`);
206
+ }
207
+
208
+ if (tokenInfo.error) {
209
+ logger.log(`Error: ${chalk.red(tokenInfo.error)}`);
210
+ }
211
+
212
+ displayUserInfo(tokenInfo.user);
213
+ }
214
+
215
+ /**
216
+ * Display authentication status
217
+ * @param {string} controllerUrl - Controller URL
218
+ * @param {string} environment - Environment key
219
+ * @param {Object|null} tokenInfo - Token information
220
+ */
221
+ function displayStatus(controllerUrl, environment, tokenInfo) {
222
+ logger.log(chalk.bold('\n🔐 Authentication Status\n'));
223
+ logger.log(`Controller: ${chalk.cyan(controllerUrl)}`);
224
+ logger.log(`Environment: ${chalk.cyan(environment || 'Not specified')}\n`);
225
+
226
+ if (!tokenInfo) {
227
+ logger.log(`Status: ${chalk.red('✗ Not authenticated')}`);
228
+ logger.log(`Token Type: ${chalk.gray('None')}\n`);
229
+ logger.log(chalk.yellow('💡 Run "aifabrix login" to authenticate\n'));
230
+ return;
231
+ }
232
+
233
+ displayTokenInfo(tokenInfo);
234
+ logger.log('');
235
+ }
236
+
237
+ /**
238
+ * Handle auth status command
239
+ * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
240
+ * @async
241
+ * @function handleAuthStatus
242
+ * @param {Object} _options - Command options (unused; controller/environment from config only)
243
+ * @returns {Promise<void>} Resolves when status is displayed
244
+ */
245
+ async function handleAuthStatus(_options) {
246
+ const { resolveEnvironment } = require('../core/config');
247
+ const controllerUrl = await resolveControllerUrl();
248
+ const environment = await resolveEnvironment();
249
+
250
+ // Check device token first (preferred)
251
+ let tokenInfo = await checkDeviceToken(controllerUrl);
252
+
253
+ // If no device token, check client token
254
+ if (!tokenInfo) {
255
+ tokenInfo = await checkClientToken(controllerUrl, environment);
256
+ }
257
+
258
+ displayStatus(controllerUrl, environment, tokenInfo);
259
+ }
260
+
261
+ module.exports = { handleAuthStatus };
@@ -50,11 +50,10 @@ function setupDatasourceCommands(program) {
50
50
  // List command
51
51
  datasource
52
52
  .command('list')
53
- .description('List datasources from environment')
54
- .requiredOption('-e, --environment <env>', 'Environment ID or key')
55
- .action(async(options) => {
53
+ .description('List datasources from environment (uses environment from config.yaml)')
54
+ .action(async() => {
56
55
  try {
57
- await listDatasources(options);
56
+ await listDatasources({});
58
57
  } catch (error) {
59
58
  logger.error(chalk.red('❌ Failed to list datasources:'), error.message);
60
59
  process.exit(1);
@@ -78,8 +77,6 @@ function setupDatasourceCommands(program) {
78
77
  datasource
79
78
  .command('deploy <myapp> <file>')
80
79
  .description('Deploy datasource to dataplane')
81
- .requiredOption('--controller <url>', 'Controller URL')
82
- .requiredOption('-e, --environment <env>', 'Environment (miso, dev, tst, pro)')
83
80
  .action(async(myapp, file, options) => {
84
81
  try {
85
82
  await deployDatasource(myapp, file, options);
@@ -54,7 +54,7 @@ async function promptForCredentials(clientId, clientSecret) {
54
54
  message: 'Client ID:',
55
55
  default: clientId || '',
56
56
  validate: (input) => {
57
- const value = input.trim();
57
+ const value = input ? input.trim() : '';
58
58
  if (!value || value.length === 0) {
59
59
  return 'Client ID is required';
60
60
  }
@@ -68,7 +68,7 @@ async function promptForCredentials(clientId, clientSecret) {
68
68
  default: clientSecret || '',
69
69
  mask: '*',
70
70
  validate: (input) => {
71
- const value = input.trim();
71
+ const value = input ? input.trim() : '';
72
72
  if (!value || value.length === 0) {
73
73
  return 'Client Secret is required';
74
74
  }
@@ -78,8 +78,8 @@ async function promptForCredentials(clientId, clientSecret) {
78
78
  ]);
79
79
 
80
80
  return {
81
- clientId: credentials.clientId.trim(),
82
- clientSecret: credentials.clientSecret.trim()
81
+ clientId: (credentials.clientId || '').trim(),
82
+ clientSecret: (credentials.clientSecret || '').trim()
83
83
  };
84
84
  }
85
85
 
@@ -11,7 +11,7 @@
11
11
  const inquirer = require('inquirer');
12
12
  const chalk = require('chalk');
13
13
  const ora = require('ora');
14
- const { setCurrentEnvironment, saveDeviceToken } = require('../core/config');
14
+ const { setCurrentEnvironment, saveDeviceToken, setControllerUrl } = require('../core/config');
15
15
  const { initiateDeviceCodeFlow } = require('../api/auth.api');
16
16
  const { pollDeviceCodeToken, displayDeviceCodeInfo } = require('../utils/api');
17
17
  const logger = require('../utils/logger');
@@ -68,6 +68,30 @@ async function saveDeviceLoginConfig(controllerUrl, token, refreshToken, expires
68
68
  await saveDeviceToken(controllerUrl, token, refreshToken, expiresAt);
69
69
  }
70
70
 
71
+ /**
72
+ * Save token configuration and display success message
73
+ * @async
74
+ * @param {string} controllerUrl - Controller URL
75
+ * @param {string} token - Access token
76
+ * @param {string} refreshToken - Refresh token
77
+ * @param {string} expiresAt - Token expiration time
78
+ * @param {string} envKey - Environment key
79
+ * @returns {Promise<void>}
80
+ */
81
+ async function saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, expiresAt, envKey) {
82
+ await saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt);
83
+ await setControllerUrl(controllerUrl);
84
+ if (envKey) {
85
+ await setCurrentEnvironment(envKey);
86
+ }
87
+ logger.log(chalk.green('\n✅ Successfully logged in!'));
88
+ logger.log(chalk.gray(`Controller: ${controllerUrl}`));
89
+ if (envKey) {
90
+ logger.log(chalk.gray(`Environment: ${envKey}`));
91
+ }
92
+ logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
93
+ }
94
+
71
95
  /**
72
96
  * Poll for device code token and save configuration
73
97
  * @async
@@ -105,23 +129,8 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
105
129
  const refreshToken = tokenResponse.refresh_token;
106
130
  const expiresAt = new Date(Date.now() + (tokenResponse.expires_in * 1000)).toISOString();
107
131
 
108
- // Save device token at root level (controller-specific, not environment-specific)
109
- await saveDeviceLoginConfig(controllerUrl, token, refreshToken, expiresAt);
110
-
111
- // Still set current environment if provided (for other purposes)
112
- if (envKey) {
113
- await setCurrentEnvironment(envKey);
114
- }
115
-
116
- logger.log(chalk.green('\n✅ Successfully logged in!'));
117
- logger.log(chalk.gray(`Controller: ${controllerUrl}`));
118
- if (envKey) {
119
- logger.log(chalk.gray(`Environment: ${envKey}`));
120
- }
121
- logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
122
-
132
+ await saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, expiresAt, envKey);
123
133
  return { token, environment: envKey };
124
-
125
134
  } catch (pollError) {
126
135
  spinner.fail('Authentication failed');
127
136
  throw pollError;
@@ -130,27 +139,32 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
130
139
 
131
140
  /**
132
141
  * Build scope string from options
133
- * @param {boolean} [offline] - Whether to request offline_access
142
+ * @param {boolean} [online] - Whether to exclude offline_access (default: false, meaning offline tokens are default)
134
143
  * @param {string} [customScope] - Custom scope string
135
144
  * @returns {string} Scope string
136
145
  */
137
- function buildScope(offline, customScope) {
146
+ function buildScope(online, customScope) {
138
147
  const defaultScope = 'openid profile email';
139
148
 
140
149
  if (customScope) {
141
- // If custom scope provided, use it and optionally add offline_access
142
- if (offline && !customScope.includes('offline_access')) {
150
+ // If custom scope provided, use it as-is
151
+ // If --online flag is used and scope contains offline_access, remove it
152
+ if (online && customScope.includes('offline_access')) {
153
+ return customScope.replace(/\s*offline_access\s*/g, ' ').trim().replace(/\s+/g, ' ');
154
+ }
155
+ // If not --online and scope doesn't have offline_access, add it (default behavior)
156
+ if (!online && !customScope.includes('offline_access')) {
143
157
  return `${customScope} offline_access`;
144
158
  }
145
159
  return customScope;
146
160
  }
147
161
 
148
- // Default scope with optional offline_access
149
- if (offline) {
150
- return `${defaultScope} offline_access`;
162
+ // Default scope: include offline_access unless --online is specified
163
+ if (online) {
164
+ return defaultScope;
151
165
  }
152
166
 
153
- return defaultScope;
167
+ return `${defaultScope} offline_access`;
154
168
  }
155
169
 
156
170
  /**
@@ -200,16 +214,16 @@ function convertDeviceCodeResponse(apiResponse) {
200
214
  * @async
201
215
  * @param {string} controllerUrl - Controller URL
202
216
  * @param {string} [environment] - Environment key from options
203
- * @param {boolean} [offline] - Whether to request offline_access scope
217
+ * @param {boolean} [online] - Whether to exclude offline_access scope (default: false, meaning offline tokens are default)
204
218
  * @param {string} [scope] - Custom scope string
205
219
  * @returns {Promise<{token: string, environment: string}>} Token and environment
206
220
  */
207
- async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope) {
221
+ async function handleDeviceCodeLogin(controllerUrl, environment, online, scope) {
208
222
  const envKey = await getEnvironmentKey(environment);
209
- const requestScope = buildScope(offline, scope);
223
+ const requestScope = buildScope(online, scope);
210
224
 
211
225
  logger.log(chalk.blue('\n📱 Initiating device code flow...\n'));
212
- if (offline) {
226
+ if (!online && requestScope.includes('offline_access')) {
213
227
  logger.log(chalk.gray(`Requesting offline token (scope: ${requestScope})\n`));
214
228
  }
215
229
 
@@ -11,10 +11,11 @@
11
11
 
12
12
  const inquirer = require('inquirer');
13
13
  const chalk = require('chalk');
14
- const { setCurrentEnvironment, saveClientToken } = require('../core/config');
14
+ const { setCurrentEnvironment, saveClientToken, setControllerUrl } = require('../core/config');
15
15
  const logger = require('../utils/logger');
16
16
  const { handleCredentialsLogin } = require('./login-credentials');
17
17
  const { handleDeviceCodeLogin } = require('./login-device');
18
+ const { resolveControllerUrl } = require('../utils/controller-url');
18
19
 
19
20
  /**
20
21
  * Determine and validate authentication method
@@ -61,8 +62,8 @@ async function saveCredentialsLoginConfig(controllerUrl, token, expiresAt, envir
61
62
  * @async
62
63
  * @function handleLogin
63
64
  * @param {Object} options - Login options
64
- * @param {string} [options.controller] - Controller URL (default: 'http://localhost:3000')
65
- * @param {string} [options.method] - Authentication method ('device' or 'credentials')
65
+ * @param {string} [options.controller] - Controller URL (default: from config, device tokens, or developer ID)
66
+ * @param {string} [options.method] - Authentication method ('device' or 'credentials', default: 'device')
66
67
  * @param {string} [options.app] - Application name (for credentials method, reads from secrets.local.yaml)
67
68
  * @param {string} [options.clientId] - Client ID (for credentials method, overrides secrets.local.yaml)
68
69
  * @param {string} [options.clientSecret] - Client Secret (for credentials method, overrides secrets.local.yaml)
@@ -71,13 +72,20 @@ async function saveCredentialsLoginConfig(controllerUrl, token, expiresAt, envir
71
72
  * @throws {Error} If login fails
72
73
  */
73
74
  /**
74
- * Normalizes and logs controller URL
75
+ * Resolves and logs controller URL from --controller, config, device tokens, or developer-ID default
76
+ * @async
75
77
  * @function normalizeControllerUrl
76
78
  * @param {Object} options - Login options
77
- * @returns {string} Normalized controller URL
79
+ * @returns {Promise<string>} Normalized controller URL
78
80
  */
79
- function normalizeControllerUrl(options) {
80
- const controllerUrl = (options.controller || options.url || 'http://localhost:3000').replace(/\/$/, '');
81
+ async function normalizeControllerUrl(options) {
82
+ let controllerUrl = options.controller || options.url;
83
+ if (!controllerUrl) {
84
+ controllerUrl = await resolveControllerUrl();
85
+ }
86
+ controllerUrl = String(controllerUrl).replace(/\/+$/, '');
87
+ // Save controller URL to config
88
+ await setControllerUrl(controllerUrl);
81
89
  logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
82
90
  return controllerUrl;
83
91
  }
@@ -108,8 +116,8 @@ async function handleEnvironmentConfig(options) {
108
116
  * @param {Object} options - Login options
109
117
  */
110
118
  function validateScopeOptions(method, options) {
111
- if (method === 'credentials' && (options.offline || options.scope)) {
112
- logger.log(chalk.yellow('⚠️ Warning: --offline and --scope options are only available for device flow'));
119
+ if (method === 'credentials' && (options.online || options.scope)) {
120
+ logger.log(chalk.yellow('⚠️ Warning: --online and --scope options are only available for device flow'));
113
121
  logger.log(chalk.gray(' These options will be ignored for credentials method\n'));
114
122
  }
115
123
  }
@@ -137,17 +145,18 @@ async function handleCredentialsLoginFlow(controllerUrl, environment, options) {
137
145
  * @async
138
146
  * @function handleDeviceCodeLoginFlow
139
147
  * @param {string} controllerUrl - Controller URL
148
+ * @param {string} environment - Resolved environment key (from config or -e/--environment)
140
149
  * @param {Object} options - Login options
141
150
  * @returns {Promise<{token: string, environment: string}>} Login result
142
151
  */
143
- async function handleDeviceCodeLoginFlow(controllerUrl, options) {
144
- return await handleDeviceCodeLogin(controllerUrl, options.environment, options.offline, options.scope);
152
+ async function handleDeviceCodeLoginFlow(controllerUrl, environment, options) {
153
+ return await handleDeviceCodeLogin(controllerUrl, environment, options.online, options.scope);
145
154
  }
146
155
 
147
156
  async function handleLogin(options) {
148
157
  logger.log(chalk.blue('\n🔐 Logging in to Miso Controller...\n'));
149
158
 
150
- const controllerUrl = normalizeControllerUrl(options);
159
+ const controllerUrl = await normalizeControllerUrl(options);
151
160
  const environment = await handleEnvironmentConfig(options);
152
161
  const method = await determineAuthMethod(options.method);
153
162
 
@@ -156,7 +165,7 @@ async function handleLogin(options) {
156
165
  if (method === 'credentials') {
157
166
  await handleCredentialsLoginFlow(controllerUrl, environment, options);
158
167
  } else if (method === 'device') {
159
- await handleDeviceCodeLoginFlow(controllerUrl, options);
168
+ await handleDeviceCodeLoginFlow(controllerUrl, environment, options);
160
169
  return; // Early return for device flow (already saved config)
161
170
  }
162
171
 
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @fileoverview Normalize wizard-generated configs before validation
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const ENTITY_TYPE_FALLBACK = 'record-storage';
8
+ const VALID_ENTITY_TYPES = new Set([
9
+ 'document-storage',
10
+ 'documentStorage',
11
+ 'vector-store',
12
+ 'vectorStore',
13
+ 'record-storage',
14
+ 'recordStorage',
15
+ 'message-service',
16
+ 'messageService',
17
+ 'none'
18
+ ]);
19
+ const VALID_PORTAL_FIELDS = new Set(['text', 'textarea', 'select', 'json', 'boolean', 'number']);
20
+
21
+ /**
22
+ * Normalize system config fields to schema constraints
23
+ * @function normalizeSystemConfig
24
+ * @param {Object} systemConfig - External system config
25
+ * @returns {Object} Normalized config
26
+ */
27
+ function normalizeSystemConfig(systemConfig) {
28
+ if (!systemConfig || typeof systemConfig !== 'object') {
29
+ return systemConfig;
30
+ }
31
+ if (typeof systemConfig.description === 'string' && systemConfig.description.length > 500) {
32
+ systemConfig.description = systemConfig.description.slice(0, 500);
33
+ }
34
+ return systemConfig;
35
+ }
36
+
37
+ /**
38
+ * Normalize datasource config fields to schema constraints
39
+ * @function normalizeDatasourceConfig
40
+ * @param {Object} datasourceConfig - Datasource config
41
+ * @returns {Object} Normalized config
42
+ */
43
+ function normalizeDatasourceConfig(datasourceConfig) {
44
+ if (!datasourceConfig || typeof datasourceConfig !== 'object') {
45
+ return datasourceConfig;
46
+ }
47
+ if (datasourceConfig.entityType && !VALID_ENTITY_TYPES.has(datasourceConfig.entityType)) {
48
+ datasourceConfig.entityType = ENTITY_TYPE_FALLBACK;
49
+ }
50
+ if (Array.isArray(datasourceConfig.portalInput)) {
51
+ datasourceConfig.portalInput = datasourceConfig.portalInput.filter(item => {
52
+ if (!item || typeof item !== 'object') {
53
+ return false;
54
+ }
55
+ if (!item.name || !item.field || !item.label) {
56
+ return false;
57
+ }
58
+ return VALID_PORTAL_FIELDS.has(item.field);
59
+ });
60
+ }
61
+ if (datasourceConfig.execution?.cip?.operations) {
62
+ for (const operation of Object.values(datasourceConfig.execution.cip.operations)) {
63
+ if (!operation || !Array.isArray(operation.steps)) {
64
+ continue;
65
+ }
66
+ for (const step of operation.steps) {
67
+ if (step?.output?.mode && step.output.mode !== 'records') {
68
+ step.output.mode = 'records';
69
+ }
70
+ }
71
+ }
72
+ }
73
+ return datasourceConfig;
74
+ }
75
+
76
+ /**
77
+ * Normalize system and datasource configs
78
+ * @function normalizeWizardConfigs
79
+ * @param {Object} systemConfig - System config
80
+ * @param {Object|Object[]} datasourceConfigs - Datasource config(s)
81
+ * @returns {{ systemConfig: Object, datasourceConfigs: Object[] }} Normalized configs
82
+ */
83
+ function normalizeWizardConfigs(systemConfig, datasourceConfigs) {
84
+ const normalizedSystem = normalizeSystemConfig(systemConfig);
85
+ const configs = Array.isArray(datasourceConfigs) ? datasourceConfigs : [datasourceConfigs];
86
+ const normalizedDatasources = configs.map(normalizeDatasourceConfig);
87
+ return { systemConfig: normalizedSystem, datasourceConfigs: normalizedDatasources };
88
+ }
89
+
90
+ module.exports = {
91
+ normalizeWizardConfigs
92
+ };