@aifabrix/builder 2.32.3 → 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 (123) 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/types/wizard.types.js +176 -38
  40. package/lib/api/wizard.api.js +161 -23
  41. package/lib/app/deploy.js +116 -54
  42. package/lib/app/display.js +6 -5
  43. package/lib/app/dockerfile.js +2 -1
  44. package/lib/app/list.js +17 -10
  45. package/lib/app/readme.js +41 -112
  46. package/lib/app/register.js +44 -9
  47. package/lib/app/rotate-secret.js +48 -31
  48. package/lib/cli.js +219 -70
  49. package/lib/commands/app.js +4 -9
  50. package/lib/commands/auth-config.js +125 -0
  51. package/lib/commands/auth-status.js +7 -8
  52. package/lib/commands/datasource.js +3 -6
  53. package/lib/commands/login-credentials.js +4 -4
  54. package/lib/commands/login-device.js +26 -17
  55. package/lib/commands/login.js +12 -10
  56. package/lib/commands/wizard-config-normalizer.js +92 -0
  57. package/lib/commands/wizard-core.js +515 -0
  58. package/lib/commands/wizard-dataplane.js +122 -0
  59. package/lib/commands/wizard-headless.js +115 -0
  60. package/lib/commands/wizard.js +110 -332
  61. package/lib/core/config.js +46 -0
  62. package/lib/core/secrets.js +3 -22
  63. package/lib/core/templates-env.js +1 -1
  64. package/lib/datasource/deploy.js +29 -21
  65. package/lib/datasource/list.js +8 -6
  66. package/lib/deployment/deployer.js +25 -0
  67. package/lib/deployment/environment.js +10 -13
  68. package/lib/external-system/delete.js +151 -0
  69. package/lib/external-system/deploy.js +53 -378
  70. package/lib/external-system/download-helpers.js +45 -65
  71. package/lib/external-system/download.js +33 -13
  72. package/lib/external-system/generator.js +11 -7
  73. package/lib/external-system/test-auth.js +4 -3
  74. package/lib/generator/builders.js +3 -1
  75. package/lib/generator/external-controller-manifest.js +157 -0
  76. package/lib/generator/external-schema-utils.js +236 -0
  77. package/lib/generator/external.js +55 -3
  78. package/lib/generator/index.js +22 -10
  79. package/lib/generator/wizard-prompts.js +33 -10
  80. package/lib/generator/wizard.js +69 -86
  81. package/lib/infrastructure/compose.js +100 -0
  82. package/lib/infrastructure/helpers.js +139 -0
  83. package/lib/infrastructure/index.js +52 -311
  84. package/lib/infrastructure/services.js +168 -0
  85. package/lib/schema/application-schema.json +23 -4
  86. package/lib/schema/external-datasource.schema.json +2 -2
  87. package/lib/schema/wizard-config.schema.json +234 -0
  88. package/lib/utils/api.js +32 -50
  89. package/lib/utils/app-existence.js +42 -0
  90. package/lib/utils/app-register-config.js +7 -2
  91. package/lib/utils/auth-config-validator.js +92 -0
  92. package/lib/utils/command-header.js +43 -0
  93. package/lib/utils/compose-generator.js +113 -70
  94. package/lib/utils/controller-url.js +65 -17
  95. package/lib/utils/dataplane-health.js +115 -0
  96. package/lib/utils/dataplane-resolver.js +29 -0
  97. package/lib/utils/dev-config.js +6 -2
  98. package/lib/utils/env-copy.js +2 -1
  99. package/lib/utils/env-ports.js +2 -1
  100. package/lib/utils/env-template.js +1 -1
  101. package/lib/utils/error-formatter.js +49 -0
  102. package/lib/utils/external-readme.js +125 -0
  103. package/lib/utils/help-builder.js +190 -0
  104. package/lib/utils/infra-status.js +13 -3
  105. package/lib/utils/paths.js +17 -2
  106. package/lib/utils/port-resolver.js +111 -0
  107. package/lib/utils/secrets-helpers.js +3 -15
  108. package/lib/utils/secrets-utils.js +2 -2
  109. package/lib/utils/token-manager.js +9 -4
  110. package/lib/utils/variable-transformer.js +7 -2
  111. package/lib/validation/external-manifest-validator.js +202 -0
  112. package/lib/validation/validate-display.js +406 -0
  113. package/lib/validation/validate.js +159 -123
  114. package/lib/validation/validator.js +36 -3
  115. package/lib/validation/wizard-config-validator.js +267 -0
  116. package/package.json +4 -2
  117. package/templates/applications/README.md.hbs +18 -16
  118. package/templates/applications/miso-controller/env.template +1 -1
  119. package/templates/applications/miso-controller/rbac.yaml +7 -7
  120. package/templates/external-system/README.md.hbs +99 -0
  121. package/templates/infra/compose.yaml.hbs +35 -0
  122. package/templates/python/docker-compose.hbs +26 -0
  123. package/templates/typescript/docker-compose.hbs +26 -0
@@ -0,0 +1,234 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/wizard-config.schema.json",
4
+ "title": "AI Fabrix Wizard Configuration Schema",
5
+ "description": "Schema for validating wizard.yaml configuration files for headless external system creation",
6
+ "type": "object",
7
+ "required": ["appName", "mode", "source"],
8
+ "properties": {
9
+ "appName": {
10
+ "type": "string",
11
+ "description": "Application name (must be lowercase alphanumeric with hyphens and underscores)",
12
+ "pattern": "^[a-z0-9-_]+$",
13
+ "minLength": 1,
14
+ "maxLength": 50
15
+ },
16
+ "mode": {
17
+ "type": "string",
18
+ "description": "Wizard mode",
19
+ "enum": ["create-system", "add-datasource"]
20
+ },
21
+ "systemIdOrKey": {
22
+ "type": "string",
23
+ "description": "Existing system ID or key (required when mode='add-datasource')",
24
+ "pattern": "^[a-z0-9-]+$",
25
+ "minLength": 1,
26
+ "maxLength": 50
27
+ },
28
+ "source": {
29
+ "type": "object",
30
+ "description": "Source configuration for the wizard",
31
+ "required": ["type"],
32
+ "properties": {
33
+ "type": {
34
+ "type": "string",
35
+ "description": "Source type",
36
+ "enum": ["openapi-file", "openapi-url", "mcp-server", "known-platform"]
37
+ },
38
+ "filePath": {
39
+ "type": "string",
40
+ "description": "Path to OpenAPI file (for openapi-file type)",
41
+ "minLength": 1
42
+ },
43
+ "url": {
44
+ "type": "string",
45
+ "description": "OpenAPI URL (for openapi-url type)",
46
+ "pattern": "^https?://.*$"
47
+ },
48
+ "serverUrl": {
49
+ "type": "string",
50
+ "description": "MCP server URL (for mcp-server type)",
51
+ "pattern": "^https?://.*$"
52
+ },
53
+ "token": {
54
+ "type": "string",
55
+ "description": "MCP server authentication token (supports ${ENV_VAR} syntax)",
56
+ "minLength": 1
57
+ },
58
+ "platform": {
59
+ "type": "string",
60
+ "description": "Known platform identifier (for known-platform type)",
61
+ "enum": ["hubspot", "salesforce", "zendesk", "slack", "microsoft365"]
62
+ }
63
+ },
64
+ "allOf": [
65
+ {
66
+ "if": {
67
+ "properties": { "type": { "const": "openapi-file" } }
68
+ },
69
+ "then": {
70
+ "required": ["filePath"]
71
+ }
72
+ },
73
+ {
74
+ "if": {
75
+ "properties": { "type": { "const": "openapi-url" } }
76
+ },
77
+ "then": {
78
+ "required": ["url"]
79
+ }
80
+ },
81
+ {
82
+ "if": {
83
+ "properties": { "type": { "const": "mcp-server" } }
84
+ },
85
+ "then": {
86
+ "required": ["serverUrl", "token"]
87
+ }
88
+ },
89
+ {
90
+ "if": {
91
+ "properties": { "type": { "const": "known-platform" } }
92
+ },
93
+ "then": {
94
+ "required": ["platform"]
95
+ }
96
+ }
97
+ ]
98
+ },
99
+ "credential": {
100
+ "type": "object",
101
+ "description": "Credential configuration for the wizard (optional)",
102
+ "required": ["action"],
103
+ "properties": {
104
+ "action": {
105
+ "type": "string",
106
+ "description": "Credential action",
107
+ "enum": ["create", "select", "skip"]
108
+ },
109
+ "credentialIdOrKey": {
110
+ "type": "string",
111
+ "description": "Credential ID or key (required when action='select')",
112
+ "minLength": 1
113
+ },
114
+ "config": {
115
+ "type": "object",
116
+ "description": "Credential configuration (required when action='create')",
117
+ "properties": {
118
+ "key": {
119
+ "type": "string",
120
+ "description": "Credential key",
121
+ "pattern": "^[a-z0-9-]+$",
122
+ "minLength": 1
123
+ },
124
+ "displayName": {
125
+ "type": "string",
126
+ "description": "Credential display name",
127
+ "minLength": 1
128
+ },
129
+ "type": {
130
+ "type": "string",
131
+ "description": "Credential type",
132
+ "enum": ["OAUTH2", "API_KEY", "BASIC", "BEARER"]
133
+ },
134
+ "config": {
135
+ "type": "object",
136
+ "description": "Credential-specific configuration",
137
+ "additionalProperties": true
138
+ }
139
+ },
140
+ "required": ["key", "displayName", "type"]
141
+ }
142
+ },
143
+ "allOf": [
144
+ {
145
+ "if": {
146
+ "properties": { "action": { "const": "select" } }
147
+ },
148
+ "then": {
149
+ "required": ["credentialIdOrKey"]
150
+ }
151
+ },
152
+ {
153
+ "if": {
154
+ "properties": { "action": { "const": "create" } }
155
+ },
156
+ "then": {
157
+ "required": ["config"]
158
+ }
159
+ }
160
+ ]
161
+ },
162
+ "preferences": {
163
+ "type": "object",
164
+ "description": "Generation preferences",
165
+ "properties": {
166
+ "intent": {
167
+ "type": "string",
168
+ "description": "User intent (any descriptive text, e.g., 'sales-focused CRM integration')",
169
+ "minLength": 1,
170
+ "maxLength": 500
171
+ },
172
+ "fieldOnboardingLevel": {
173
+ "type": "string",
174
+ "description": "Field onboarding level",
175
+ "enum": ["full", "standard", "minimal"],
176
+ "default": "full"
177
+ },
178
+ "enableOpenAPIGeneration": {
179
+ "type": "boolean",
180
+ "description": "Enable OpenAPI operation generation",
181
+ "default": true
182
+ },
183
+ "enableMCP": {
184
+ "type": "boolean",
185
+ "description": "Enable Model Context Protocol",
186
+ "default": false
187
+ },
188
+ "enableABAC": {
189
+ "type": "boolean",
190
+ "description": "Enable Attribute-Based Access Control",
191
+ "default": false
192
+ },
193
+ "enableRBAC": {
194
+ "type": "boolean",
195
+ "description": "Enable Role-Based Access Control",
196
+ "default": false
197
+ }
198
+ }
199
+ },
200
+ "deployment": {
201
+ "type": "object",
202
+ "description": "Deployment settings (optional overrides)",
203
+ "properties": {
204
+ "controller": {
205
+ "type": "string",
206
+ "description": "Controller URL",
207
+ "pattern": "^https?://.*$"
208
+ },
209
+ "environment": {
210
+ "type": "string",
211
+ "description": "Environment key",
212
+ "enum": ["dev", "tst", "pro", "miso"],
213
+ "default": "dev"
214
+ },
215
+ "dataplane": {
216
+ "type": "string",
217
+ "description": "Dataplane URL (overrides controller lookup)",
218
+ "pattern": "^https?://.*$"
219
+ }
220
+ }
221
+ }
222
+ },
223
+ "allOf": [
224
+ {
225
+ "if": {
226
+ "properties": { "mode": { "const": "add-datasource" } }
227
+ },
228
+ "then": {
229
+ "required": ["systemIdOrKey"]
230
+ }
231
+ }
232
+ ],
233
+ "additionalProperties": false
234
+ }
package/lib/utils/api.js CHANGED
@@ -13,6 +13,9 @@
13
13
  const { parseErrorResponse } = require('./api-error-handler');
14
14
  const auditLogger = require('../core/audit-logger');
15
15
 
16
+ /** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. */
17
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5000;
18
+
16
19
  /**
17
20
  * Logs API request performance metrics and errors to audit log
18
21
  * @param {Object} params - Performance logging parameters
@@ -166,15 +169,20 @@ async function handleNetworkError(error, url, options, duration) {
166
169
 
167
170
  /**
168
171
  * Make an API call with proper error handling
172
+ * Uses a 15s timeout to avoid hanging when the controller is unreachable.
169
173
  * @param {string} url - API endpoint URL
170
- * @param {Object} options - Fetch options
174
+ * @param {Object} options - Fetch options (signal, method, headers, body, etc.)
171
175
  * @returns {Promise<Object>} Response object with success flag
172
176
  */
173
177
  async function makeApiCall(url, options = {}) {
174
178
  const startTime = Date.now();
179
+ const fetchOptions = { ...options };
180
+ if (!fetchOptions.signal) {
181
+ fetchOptions.signal = AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS);
182
+ }
175
183
 
176
184
  try {
177
- const response = await fetch(url, options);
185
+ const response = await fetch(url, fetchOptions);
178
186
  const duration = Date.now() - startTime;
179
187
 
180
188
  if (!response.ok) {
@@ -182,8 +190,13 @@ async function makeApiCall(url, options = {}) {
182
190
  }
183
191
 
184
192
  return await handleSuccessResponse(response, url, options, duration);
185
- } catch (error) {
193
+ } catch (err) {
186
194
  const duration = Date.now() - startTime;
195
+ const error = err?.name === 'AbortError'
196
+ ? new Error(
197
+ `Request timed out after ${DEFAULT_REQUEST_TIMEOUT_MS / 1000} seconds. The controller may be unreachable. Check the URL and network.`
198
+ )
199
+ : err;
187
200
  return await handleNetworkError(error, url, options, duration);
188
201
  }
189
202
  }
@@ -214,20 +227,16 @@ function extractControllerUrl(url) {
214
227
  * @param {string} [tokenOrAuthConfig.controller] - Controller URL for token refresh (if object)
215
228
  * @returns {Promise<Object>} Response object
216
229
  */
230
+ // eslint-disable-next-line max-statements
217
231
  async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
218
- // Support both string token (backward compat) and authConfig object
219
- const token = typeof tokenOrAuthConfig === 'string'
220
- ? tokenOrAuthConfig
221
- : tokenOrAuthConfig?.token;
222
- const authControllerUrl = typeof tokenOrAuthConfig === 'object'
223
- ? tokenOrAuthConfig?.controller
224
- : null;
225
-
226
- const headers = {
227
- 'Content-Type': 'application/json',
228
- ...options.headers
229
- };
230
-
232
+ const isStringToken = typeof tokenOrAuthConfig === 'string';
233
+ const token = isStringToken ? tokenOrAuthConfig : tokenOrAuthConfig?.token;
234
+ const authControllerUrl = isStringToken ? null : tokenOrAuthConfig?.controller;
235
+ const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData;
236
+ const headers = { ...options.headers };
237
+ if (!isFormData && !headers['Content-Type']) {
238
+ headers['Content-Type'] = 'application/json';
239
+ }
231
240
  if (token) {
232
241
  headers['Authorization'] = `Bearer ${token}`;
233
242
  }
@@ -237,46 +246,19 @@ async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
237
246
  headers
238
247
  });
239
248
 
240
- // Handle 401 errors with automatic token refresh for device tokens
241
249
  if (!response.success && response.status === 401) {
242
250
  try {
243
- // Use controller URL from authConfig if available, otherwise extract from request URL
244
- // This is important when the request URL is a dataplane URL but the token
245
- // is stored under the controller URL
246
- const controllerUrl = authControllerUrl || extractControllerUrl(url);
247
-
248
- // Try to force refresh device token on 401 (regardless of local expiry time)
249
- // because the server rejected the token
250
251
  const { forceRefreshDeviceToken } = require('./token-manager');
251
- const refreshedToken = await forceRefreshDeviceToken(controllerUrl);
252
-
253
- if (refreshedToken && refreshedToken.token) {
254
- // Retry request with new token
252
+ const refreshedToken = await forceRefreshDeviceToken(authControllerUrl || extractControllerUrl(url));
253
+ if (refreshedToken?.token) {
255
254
  headers['Authorization'] = `Bearer ${refreshedToken.token}`;
256
- const retryResponse = await makeApiCall(url, {
257
- ...options,
258
- headers
259
- });
260
- return retryResponse;
261
- }
262
-
263
- // Token refresh failed or no refresh token available
264
- // Return a more helpful error message
265
- if (!refreshedToken) {
266
- return {
267
- ...response,
268
- error: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login',
269
- formattedError: 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login'
270
- };
255
+ return await makeApiCall(url, { ...options, headers });
271
256
  }
257
+ const authError = 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login';
258
+ return { ...response, error: authError, formattedError: authError };
272
259
  } catch (refreshError) {
273
- // Refresh failed, return original 401 error with additional context
274
- const errorMessage = refreshError.message || String(refreshError);
275
- return {
276
- ...response,
277
- error: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`,
278
- formattedError: `Authentication failed: ${errorMessage}. Please login again using: aifabrix login`
279
- };
260
+ const authError = `Authentication failed: ${refreshError.message || String(refreshError)}. Please login again using: aifabrix login`;
261
+ return { ...response, error: authError, formattedError: authError };
280
262
  }
281
263
  }
282
264
 
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Application Existence Check Utility
3
+ *
4
+ * Checks if an application exists in an environment before deployment.
5
+ *
6
+ * @fileoverview Application existence checking for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const { getEnvironmentApplication } = require('../api/environments.api');
12
+
13
+ /**
14
+ * Check if application exists in the environment
15
+ * Uses device token auth to check existence (doesn't require app credentials)
16
+ * @async
17
+ * @function checkApplicationExists
18
+ * @param {string} appKey - Application key
19
+ * @param {string} controllerUrl - Controller URL
20
+ * @param {string} envKey - Environment key
21
+ * @param {Object} authConfig - Authentication configuration (device token)
22
+ * @returns {Promise<boolean>} True if application exists, false otherwise
23
+ */
24
+ async function checkApplicationExists(appKey, controllerUrl, envKey, authConfig) {
25
+ try {
26
+ // Use device token auth (bearer token) to check if app exists
27
+ // This doesn't require app credentials, so it works even if credentials are wrong
28
+ const deviceAuthConfig = { type: 'bearer', token: authConfig.token };
29
+ const response = await getEnvironmentApplication(controllerUrl, envKey, appKey, deviceAuthConfig);
30
+ return response.success && response.data !== null && response.data !== undefined;
31
+ } catch (error) {
32
+ // If 404, application doesn't exist
33
+ if (error.status === 404 || (error.response && error.response.status === 404)) {
34
+ return false;
35
+ }
36
+ // For other errors (including 401 if device token is invalid), we can't determine existence
37
+ // Return false to avoid blocking deployment - the validation step will catch credential issues
38
+ return false;
39
+ }
40
+ }
41
+
42
+ module.exports = { checkApplicationExists };
@@ -14,6 +14,7 @@ const chalk = require('chalk');
14
14
  const yaml = require('js-yaml');
15
15
  const logger = require('./logger');
16
16
  const { detectAppType } = require('./paths');
17
+ const { getContainerPort, getLocalPort } = require('./port-resolver');
17
18
 
18
19
  // createApp is imported dynamically in createMinimalAppIfNeeded to handle test mocking
19
20
 
@@ -238,9 +239,11 @@ async function extractExternalAppConfiguration(appKey, variables, appKeyFromFile
238
239
  function extractWebappConfiguration(variables, appKeyFromFile, displayName, description, options) {
239
240
  const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
240
241
  const registryMode = variables.image?.registryMode || 'external';
241
- const port = variables.build?.port || options.port || 3000;
242
+ const port = options.port ?? getContainerPort(variables, 3000);
243
+ const localPort = getLocalPort(variables, port);
242
244
  const language = variables.build?.language || 'typescript';
243
245
  const image = buildImageReference(variables, appKeyFromFile);
246
+ const url = variables.app?.url || variables.deployment?.dataplaneUrl || variables.deployment?.appUrl || null;
244
247
 
245
248
  return {
246
249
  appKey: appKeyFromFile,
@@ -249,8 +252,10 @@ function extractWebappConfiguration(variables, appKeyFromFile, displayName, desc
249
252
  appType,
250
253
  registryMode,
251
254
  port,
255
+ localPort,
252
256
  image,
253
- language
257
+ language,
258
+ url
254
259
  };
255
260
  }
256
261
 
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Authentication Configuration Validators
3
+ *
4
+ * Provides validation functions for authentication configuration commands
5
+ *
6
+ * @fileoverview Authentication configuration validators
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const { getControllerUrlFromLoggedInUser } = require('./controller-url');
12
+
13
+ /**
14
+ * Validate controller URL format
15
+ * @function validateControllerUrl
16
+ * @param {string} url - Controller URL to validate
17
+ * @throws {Error} If URL format is invalid
18
+ */
19
+ function validateControllerUrl(url) {
20
+ if (!url || typeof url !== 'string') {
21
+ throw new Error('Controller URL is required and must be a string');
22
+ }
23
+
24
+ const trimmed = url.trim();
25
+ if (trimmed.length === 0) {
26
+ throw new Error('Controller URL cannot be empty');
27
+ }
28
+
29
+ // Basic URL validation - must start with http:// or https://
30
+ if (!trimmed.match(/^https?:\/\//)) {
31
+ throw new Error('Controller URL must start with http:// or https://');
32
+ }
33
+
34
+ try {
35
+ // Use URL constructor for more thorough validation
36
+ new URL(trimmed);
37
+ } catch (error) {
38
+ throw new Error(`Invalid controller URL format: ${error.message}`);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Validate environment key format
44
+ * @function validateEnvironment
45
+ * @param {string} env - Environment key to validate
46
+ * @throws {Error} If environment format is invalid
47
+ */
48
+ function validateEnvironment(env) {
49
+ if (!env || typeof env !== 'string') {
50
+ throw new Error('Environment is required and must be a string');
51
+ }
52
+
53
+ const trimmed = env.trim();
54
+ if (trimmed.length === 0) {
55
+ throw new Error('Environment cannot be empty');
56
+ }
57
+
58
+ // Environment key must contain only letters, numbers, hyphens, and underscores
59
+ if (!/^[a-z0-9-_]+$/i.test(trimmed)) {
60
+ throw new Error('Environment must contain only letters, numbers, hyphens, and underscores');
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Check if user is logged in to a controller
66
+ * @async
67
+ * @function checkUserLoggedIn
68
+ * @param {string} controllerUrl - Controller URL to check
69
+ * @returns {Promise<boolean>} True if user has device token for this controller
70
+ */
71
+ async function checkUserLoggedIn(controllerUrl) {
72
+ if (!controllerUrl) {
73
+ return false;
74
+ }
75
+
76
+ const normalizedUrl = controllerUrl.trim().replace(/\/+$/, '');
77
+ const loggedInControllerUrl = await getControllerUrlFromLoggedInUser();
78
+
79
+ if (!loggedInControllerUrl) {
80
+ return false;
81
+ }
82
+
83
+ // Normalize both URLs for comparison
84
+ const normalizedLoggedIn = loggedInControllerUrl.trim().replace(/\/+$/, '');
85
+ return normalizedLoggedIn === normalizedUrl;
86
+ }
87
+
88
+ module.exports = {
89
+ validateControllerUrl,
90
+ validateEnvironment,
91
+ checkUserLoggedIn
92
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Command Header Display Utility
3
+ *
4
+ * Displays active configuration (controller, environment, dataplane) at top of commands
5
+ *
6
+ * @fileoverview Command header display utility
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const chalk = require('chalk');
12
+ const logger = require('./logger');
13
+
14
+ /**
15
+ * Display command header with active configuration
16
+ * @function displayCommandHeader
17
+ * @param {string} controllerUrl - Controller URL
18
+ * @param {string} environment - Environment key
19
+ * @param {string} [dataplaneUrl] - Dataplane URL (optional)
20
+ */
21
+ function displayCommandHeader(controllerUrl, environment, dataplaneUrl) {
22
+ const parts = [];
23
+
24
+ if (controllerUrl) {
25
+ parts.push(`Controller: ${chalk.cyan(controllerUrl)}`);
26
+ }
27
+
28
+ if (environment) {
29
+ parts.push(`Environment: ${chalk.cyan(environment)}`);
30
+ }
31
+
32
+ if (dataplaneUrl) {
33
+ parts.push(`Dataplane: ${chalk.cyan(dataplaneUrl)}`);
34
+ }
35
+
36
+ if (parts.length > 0) {
37
+ logger.log(chalk.gray(`\n${parts.join(' | ')}\n`));
38
+ }
39
+ }
40
+
41
+ module.exports = {
42
+ displayCommandHeader
43
+ };