@aifabrix/builder 2.8.0 → 2.10.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 (46) hide show
  1. package/integration/hubspot/README.md +136 -0
  2. package/integration/hubspot/env.template +9 -0
  3. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  4. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  5. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  6. package/integration/hubspot/hubspot-deploy.json +91 -0
  7. package/integration/hubspot/variables.yaml +17 -0
  8. package/lib/app-config.js +4 -3
  9. package/lib/app-deploy.js +8 -20
  10. package/lib/app-dockerfile.js +7 -9
  11. package/lib/app-prompts.js +6 -5
  12. package/lib/app-push.js +9 -9
  13. package/lib/app-register.js +23 -5
  14. package/lib/app-rotate-secret.js +10 -0
  15. package/lib/app-run.js +5 -11
  16. package/lib/app.js +42 -14
  17. package/lib/build.js +20 -16
  18. package/lib/cli.js +61 -2
  19. package/lib/commands/login.js +7 -1
  20. package/lib/datasource-deploy.js +14 -20
  21. package/lib/external-system-deploy.js +123 -40
  22. package/lib/external-system-download.js +431 -0
  23. package/lib/external-system-generator.js +13 -10
  24. package/lib/external-system-test.js +446 -0
  25. package/lib/generator-builders.js +323 -0
  26. package/lib/generator.js +200 -292
  27. package/lib/schema/application-schema.json +853 -852
  28. package/lib/schema/env-config.yaml +9 -1
  29. package/lib/schema/external-datasource.schema.json +823 -49
  30. package/lib/schema/external-system.schema.json +96 -78
  31. package/lib/templates.js +36 -5
  32. package/lib/utils/api-error-handler.js +12 -12
  33. package/lib/utils/cli-utils.js +4 -4
  34. package/lib/utils/device-code.js +65 -2
  35. package/lib/utils/env-template.js +5 -4
  36. package/lib/utils/external-system-display.js +159 -0
  37. package/lib/utils/external-system-validators.js +245 -0
  38. package/lib/utils/paths.js +151 -1
  39. package/lib/utils/schema-resolver.js +7 -2
  40. package/lib/validator.js +5 -2
  41. package/package.json +1 -1
  42. package/templates/applications/keycloak/env.template +8 -2
  43. package/templates/applications/keycloak/variables.yaml +3 -3
  44. package/templates/applications/miso-controller/env.template +23 -10
  45. package/templates/applications/miso-controller/rbac.yaml +263 -213
  46. package/templates/applications/miso-controller/variables.yaml +3 -3
@@ -3,7 +3,6 @@
3
3
  "$id": "https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-system.schema.json",
4
4
  "title": "AI Fabrix External System Configuration Schema",
5
5
  "description": "Schema for configuring an external system connected to the AI Fabrix Dataplane. This defines authentication, OpenAPI/MCP bindings, field mappings defaults, metadata handling and portal inputs.",
6
-
7
6
  "metadata": {
8
7
  "key": "external-system-schema",
9
8
  "name": "External System Configuration Schema",
@@ -19,7 +18,13 @@
19
18
  "maxVersion": "2.0.0",
20
19
  "deprecated": false
21
20
  },
22
- "tags": ["schema", "external-system", "dataplane", "integration", "validation"],
21
+ "tags": [
22
+ "schema",
23
+ "external-system",
24
+ "dataplane",
25
+ "integration",
26
+ "validation"
27
+ ],
23
28
  "dependencies": [],
24
29
  "changelog": [
25
30
  {
@@ -36,9 +41,7 @@
36
41
  }
37
42
  ]
38
43
  },
39
-
40
44
  "type": "object",
41
-
42
45
  "required": [
43
46
  "key",
44
47
  "displayName",
@@ -46,7 +49,6 @@
46
49
  "type",
47
50
  "authentication"
48
51
  ],
49
-
50
52
  "properties": {
51
53
  "key": {
52
54
  "type": "string",
@@ -55,32 +57,31 @@
55
57
  "minLength": 3,
56
58
  "maxLength": 40
57
59
  },
58
-
59
60
  "displayName": {
60
61
  "type": "string",
61
62
  "description": "Human-readable name for the external system",
62
63
  "minLength": 1,
63
64
  "maxLength": 100
64
65
  },
65
-
66
66
  "description": {
67
67
  "type": "string",
68
68
  "description": "Description of this external system integration",
69
69
  "minLength": 1,
70
70
  "maxLength": 500
71
71
  },
72
-
73
72
  "type": {
74
73
  "type": "string",
75
- "enum": ["openapi", "mcp", "custom"],
74
+ "enum": [
75
+ "openapi",
76
+ "mcp",
77
+ "custom"
78
+ ],
76
79
  "description": "Integration type: OpenAPI-driven, MCP-driven, or custom Python connector."
77
80
  },
78
-
79
81
  "enabled": {
80
82
  "type": "boolean",
81
83
  "default": true
82
84
  },
83
-
84
85
  "environment": {
85
86
  "type": "object",
86
87
  "description": "Environment-level configuration values used by dataplane and external data sources.",
@@ -97,95 +98,80 @@
97
98
  },
98
99
  "additionalProperties": true
99
100
  },
100
-
101
101
  "authentication": {
102
102
  "type": "object",
103
- "description": "Authentication configuration for the external system.",
104
- "required": ["type"],
103
+ "description": "Authentication configuration for external system",
104
+ "required": [
105
+ "type"
106
+ ],
105
107
  "properties": {
106
108
  "type": {
107
109
  "type": "string",
108
- "enum": ["oauth2", "apikey", "basic", "aad", "none"],
109
- "description": "Authentication method used for API access."
110
+ "enum": [
111
+ "oauth2",
112
+ "apikey",
113
+ "basic",
114
+ "aad",
115
+ "none"
116
+ ],
117
+ "description": "Authentication type"
110
118
  },
111
-
112
119
  "oauth2": {
113
120
  "type": "object",
114
- "description": "OAuth2 configuration used when type == 'oauth2'. All secrets are stored in Key Vault.",
115
- "properties": {
116
- "tokenUrl": {
117
- "type": "string",
118
- "pattern": "^(http|https)://.*$"
119
- },
120
- "clientId": {
121
- "type": "string"
122
- },
123
- "clientSecret": {
124
- "type": "string"
125
- },
126
- "scopes": {
127
- "type": "array",
128
- "items": { "type": "string" }
129
- }
130
- },
131
- "additionalProperties": false
121
+ "description": "OAuth2 authentication configuration",
122
+ "additionalProperties": true
132
123
  },
133
-
134
124
  "apikey": {
135
125
  "type": "object",
136
- "properties": {
137
- "headerName": { "type": "string" },
138
- "key": { "type": "string" }
139
- },
140
- "additionalProperties": false
126
+ "description": "API key authentication configuration",
127
+ "additionalProperties": true
141
128
  },
142
-
143
129
  "basic": {
144
130
  "type": "object",
145
- "properties": {
146
- "username": { "type": "string" },
147
- "password": { "type": "string" }
148
- },
149
- "additionalProperties": false
131
+ "description": "Basic authentication configuration",
132
+ "additionalProperties": true
133
+ },
134
+ "aad": {
135
+ "type": "object",
136
+ "description": "Azure AD authentication configuration",
137
+ "additionalProperties": true
150
138
  }
151
139
  },
152
- "additionalProperties": false
140
+ "additionalProperties": true
153
141
  },
154
-
155
142
  "openapi": {
156
143
  "type": "object",
157
- "description": "OpenAPI integration configuration.",
144
+ "description": "OpenAPI integration configuration",
158
145
  "properties": {
159
146
  "documentKey": {
160
147
  "type": "string",
161
- "description": "Reference to OpenAPI spec registered via builder. Example: 'hubspot-v3'."
148
+ "description": "Key of the OpenAPI document in the registry"
162
149
  },
163
- "autoDiscoverEntities": {
164
- "type": "boolean",
165
- "default": false,
166
- "description": "Automatically discover resources/entities from OpenAPI schema."
150
+ "specUrl": {
151
+ "type": "string",
152
+ "description": "URL to the OpenAPI specification",
153
+ "pattern": "^(http|https)://.*$"
167
154
  }
168
155
  },
169
- "additionalProperties": false
156
+ "additionalProperties": true
170
157
  },
171
-
172
158
  "mcp": {
173
159
  "type": "object",
174
- "description": "Model Context Protocol integration config.",
160
+ "description": "Model Context Protocol integration configuration",
175
161
  "properties": {
176
162
  "serverUrl": {
177
163
  "type": "string",
164
+ "description": "MCP server URL",
178
165
  "pattern": "^(http|https)://.*$"
179
166
  },
180
167
  "toolPrefix": {
181
168
  "type": "string",
182
- "pattern": "^[a-z0-9-]+$",
183
- "description": "Prefix for MCP tool names generated from this system."
169
+ "description": "Prefix for MCP tool names",
170
+ "pattern": "^[a-zA-Z0-9_-]+$"
184
171
  }
185
172
  },
186
- "additionalProperties": false
173
+ "additionalProperties": true
187
174
  },
188
-
189
175
  "dataSources": {
190
176
  "type": "array",
191
177
  "description": "List of data source keys belonging to this external system. Each must match external-datasource.schema.json.",
@@ -195,13 +181,17 @@
195
181
  },
196
182
  "uniqueItems": true
197
183
  },
198
-
199
184
  "configuration": {
200
185
  "type": "array",
201
186
  "description": "External system configuration variables (same pattern as application-schema).",
202
187
  "items": {
203
188
  "type": "object",
204
- "required": ["name", "value", "location", "required"],
189
+ "required": [
190
+ "name",
191
+ "value",
192
+ "location",
193
+ "required"
194
+ ],
205
195
  "properties": {
206
196
  "name": {
207
197
  "type": "string",
@@ -213,33 +203,61 @@
213
203
  },
214
204
  "location": {
215
205
  "type": "string",
216
- "enum": ["variable", "keyvault"]
206
+ "enum": [
207
+ "variable",
208
+ "keyvault"
209
+ ]
217
210
  },
218
211
  "required": {
219
212
  "type": "boolean"
220
213
  },
221
214
  "portalInput": {
222
215
  "type": "object",
223
- "required": ["field", "label"],
216
+ "required": [
217
+ "field",
218
+ "label"
219
+ ],
224
220
  "properties": {
225
221
  "field": {
226
222
  "type": "string",
227
- "enum": ["password", "text", "textarea", "select", "json"]
223
+ "enum": [
224
+ "password",
225
+ "text",
226
+ "textarea",
227
+ "select",
228
+ "json"
229
+ ]
230
+ },
231
+ "label": {
232
+ "type": "string"
233
+ },
234
+ "placeholder": {
235
+ "type": "string"
228
236
  },
229
- "label": { "type": "string" },
230
- "placeholder": { "type": "string" },
231
237
  "options": {
232
238
  "type": "array",
233
- "items": { "type": "string" }
239
+ "items": {
240
+ "type": "string"
241
+ }
242
+ },
243
+ "masked": {
244
+ "type": "boolean"
234
245
  },
235
- "masked": { "type": "boolean" },
236
246
  "validation": {
237
247
  "type": "object",
238
248
  "properties": {
239
- "minLength": { "type": "integer" },
240
- "maxLength": { "type": "integer" },
241
- "pattern": { "type": "string" },
242
- "required": { "type": "boolean" }
249
+ "minLength": {
250
+ "type": "integer"
251
+ },
252
+ "maxLength": {
253
+ "type": "integer"
254
+ },
255
+ "pattern": {
256
+ "type": "string"
257
+ },
258
+ "required": {
259
+ "type": "boolean"
260
+ }
243
261
  },
244
262
  "additionalProperties": false
245
263
  }
@@ -250,13 +268,13 @@
250
268
  "additionalProperties": false
251
269
  }
252
270
  },
253
-
254
271
  "tags": {
255
272
  "type": "array",
256
273
  "description": "Optional tags for search / filtering",
257
- "items": { "type": "string" }
274
+ "items": {
275
+ "type": "string"
276
+ }
258
277
  }
259
278
  },
260
-
261
279
  "additionalProperties": false
262
280
  }
package/lib/templates.js CHANGED
@@ -32,7 +32,7 @@ function generateVariablesYaml(appName, config) {
32
32
  environment: 'dev'
33
33
  },
34
34
  externalIntegration: {
35
- schemaBasePath: './schemas',
35
+ schemaBasePath: './',
36
36
  systems: [],
37
37
  dataSources: [],
38
38
  autopublish: true,
@@ -134,13 +134,31 @@ function generateVariablesYaml(appName, config) {
134
134
  */
135
135
  function buildCoreEnv(config) {
136
136
  return {
137
- 'NODE_ENV': 'development',
137
+ 'NODE_ENV': '${NODE_ENV}',
138
138
  'PORT': config.port || 3000,
139
139
  'APP_NAME': config.appName || 'myapp',
140
140
  'LOG_LEVEL': 'info'
141
141
  };
142
142
  }
143
143
 
144
+ /**
145
+ * Builds Python-specific environment variables
146
+ * @param {Object} config - Configuration options
147
+ * @returns {Object} Python environment variables
148
+ */
149
+ function buildPythonEnv(config) {
150
+ const language = config.language || 'typescript';
151
+ if (language !== 'python') {
152
+ return {};
153
+ }
154
+
155
+ return {
156
+ 'PYTHONUNBUFFERED': '${PYTHONUNBUFFERED}',
157
+ 'PYTHONDONTWRITEBYTECODE': '${PYTHONDONTWRITEBYTECODE}',
158
+ 'PYTHONIOENCODING': '${PYTHONIOENCODING}'
159
+ };
160
+ }
161
+
144
162
  function buildDatabaseEnv(config) {
145
163
  if (!config.database) {
146
164
  return {};
@@ -207,8 +225,9 @@ function buildMonitoringEnv(config) {
207
225
  return {
208
226
  'MISO_CONTROLLER_URL': config.controllerUrl || 'https://controller.aifabrix.ai',
209
227
  'MISO_ENVIRONMENT': 'dev',
210
- 'MISO_CLIENTID': 'kv://miso-clientid',
211
- 'MISO_CLIENTSECRET': 'kv://miso-clientsecret'
228
+ 'MISO_CLIENTID': 'kv://miso-controller-client-idKeyVault',
229
+ 'MISO_CLIENTSECRET': 'kv://miso-controller-client-secretKeyVault',
230
+ 'MISO_WEB_SERVER_URL': 'kv://miso-controller-web-server-url'
212
231
  };
213
232
  }
214
233
 
@@ -220,10 +239,17 @@ function buildMonitoringEnv(config) {
220
239
  function addCoreVariables(lines, envVars) {
221
240
  Object.entries(envVars).forEach(([key, value]) => {
222
241
  if (key.startsWith('NODE_ENV') || key.startsWith('PORT') ||
223
- key.startsWith('APP_NAME') || key.startsWith('LOG_LEVEL')) {
242
+ key.startsWith('APP_NAME') || key.startsWith('LOG_LEVEL') ||
243
+ key.startsWith('PYTHON')) {
224
244
  lines.push(`${key}=${value}`);
225
245
  }
226
246
  });
247
+
248
+ // Add ALLOWED_ORIGINS and WEB_SERVER_URL after PORT variable
249
+ // ALLOWED_ORIGINS: My application public address
250
+ lines.push('ALLOWED_ORIGINS=http://localhost:*,');
251
+ // WEB_SERVER_URL: Miso public address (uses ${PORT} template variable)
252
+ lines.push('WEB_SERVER_URL=http://localhost:${PORT},');
227
253
  }
228
254
 
229
255
  function addMonitoringSection(lines, envVars) {
@@ -232,6 +258,10 @@ function addMonitoringSection(lines, envVars) {
232
258
  lines.push(`MISO_ENVIRONMENT=${envVars['MISO_ENVIRONMENT']}`);
233
259
  lines.push(`MISO_CLIENTID=${envVars['MISO_CLIENTID']}`);
234
260
  lines.push(`MISO_CLIENTSECRET=${envVars['MISO_CLIENTSECRET']}`);
261
+ // MISO_WEB_SERVER_URL: Miso public address
262
+ if (envVars['MISO_WEB_SERVER_URL']) {
263
+ lines.push(`MISO_WEB_SERVER_URL=${envVars['MISO_WEB_SERVER_URL']}`);
264
+ }
235
265
  }
236
266
 
237
267
  function addDatabaseSection(lines, envVars) {
@@ -316,6 +346,7 @@ function addAuthenticationSection(lines, envVars) {
316
346
  function generateEnvTemplate(config, existingEnv = {}) {
317
347
  const envVars = {
318
348
  ...buildCoreEnv(config),
349
+ ...buildPythonEnv(config),
319
350
  ...buildDatabaseEnv(config),
320
351
  ...buildRedisEnv(config),
321
352
  ...buildStorageEnv(config),
@@ -73,13 +73,11 @@ function formatPermissionError(errorData) {
73
73
 
74
74
  const requiredPerms = extractRequiredPermissions(errorData);
75
75
  addPermissionList(lines, requiredPerms, 'Required permissions');
76
-
77
76
  const requestUrl = errorData.instance || errorData.url;
78
77
  const method = errorData.method || 'POST';
79
78
  if (requestUrl) {
80
79
  lines.push(chalk.gray(`Request: ${method} ${requestUrl}`));
81
80
  }
82
-
83
81
  if (errorData.correlationId) {
84
82
  lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
85
83
  }
@@ -97,18 +95,27 @@ function formatValidationError(errorData) {
97
95
  lines.push(chalk.red('❌ Validation Error\n'));
98
96
 
99
97
  // Handle RFC 7807 Problem Details format
100
- // Priority: detail > title > message
98
+ // Priority: detail > title > errorDescription > message > error
101
99
  if (errorData.detail) {
102
100
  lines.push(chalk.yellow(errorData.detail));
103
101
  lines.push('');
104
102
  } else if (errorData.title) {
105
103
  lines.push(chalk.yellow(errorData.title));
106
104
  lines.push('');
105
+ } else if (errorData.errorDescription) {
106
+ // Handle Keycloak-style error format
107
+ lines.push(chalk.yellow(errorData.errorDescription));
108
+ if (errorData.error) {
109
+ lines.push(chalk.gray(`Error code: ${errorData.error}`));
110
+ }
111
+ lines.push('');
107
112
  } else if (errorData.message) {
108
113
  lines.push(chalk.yellow(errorData.message));
109
114
  lines.push('');
115
+ } else if (errorData.error) {
116
+ lines.push(chalk.yellow(errorData.error));
117
+ lines.push('');
110
118
  }
111
-
112
119
  // Handle errors array - this is the most important part
113
120
  if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) {
114
121
  lines.push(chalk.yellow('Validation errors:'));
@@ -124,17 +131,14 @@ function formatValidationError(errorData) {
124
131
  });
125
132
  lines.push('');
126
133
  }
127
-
128
134
  // Show instance (endpoint) if available (RFC 7807)
129
135
  if (errorData.instance) {
130
136
  lines.push(chalk.gray(`Endpoint: ${errorData.instance}`));
131
137
  }
132
-
133
138
  // Show correlation ID if available
134
139
  if (errorData.correlationId) {
135
140
  lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
136
141
  }
137
-
138
142
  return lines.join('\n');
139
143
  }
140
144
  /**
@@ -145,7 +149,6 @@ function formatValidationError(errorData) {
145
149
  function formatAuthenticationError(errorData) {
146
150
  const lines = [];
147
151
  lines.push(chalk.red('❌ Authentication Failed\n'));
148
-
149
152
  if (errorData.message) {
150
153
  lines.push(chalk.yellow(errorData.message));
151
154
  } else {
@@ -153,11 +156,9 @@ function formatAuthenticationError(errorData) {
153
156
  }
154
157
  lines.push('');
155
158
  lines.push(chalk.gray('Run: aifabrix login'));
156
-
157
159
  if (errorData.correlationId) {
158
160
  lines.push(chalk.gray(`Correlation ID: ${errorData.correlationId}`));
159
161
  }
160
-
161
162
  return lines.join('\n');
162
163
  }
163
164
  /**
@@ -379,7 +380,7 @@ function createErrorResult(type, message, formatted, data) {
379
380
  * @returns {string} Error message
380
381
  */
381
382
  function getErrorMessage(errorData, defaultMessage) {
382
- return errorData.detail || errorData.title || errorData.message || defaultMessage;
383
+ return errorData.detail || errorData.title || errorData.errorDescription || errorData.message || errorData.error || defaultMessage;
383
384
  }
384
385
 
385
386
  /**
@@ -486,7 +487,6 @@ function formatApiError(apiResponse) {
486
487
  const parsed = parseErrorResponse(errorResponse, statusCode, isNetworkError);
487
488
  return parsed.formatted;
488
489
  }
489
-
490
490
  module.exports = {
491
491
  parseErrorResponse,
492
492
  formatApiError,
@@ -57,7 +57,7 @@ function formatError(error) {
57
57
  } else if (errorMsg.includes('Docker') && (errorMsg.includes('not running') || errorMsg.includes('not installed') || errorMsg.includes('Cannot connect'))) {
58
58
  messages.push(' Docker is not running or not installed.');
59
59
  messages.push(' Please start Docker Desktop and try again.');
60
- } else if (errorMsg.includes('port')) {
60
+ } else if (errorMsg.toLowerCase().includes('port') && (errorMsg.includes('already in use') || errorMsg.includes('in use') || errorMsg.includes('conflict'))) {
61
61
  messages.push(' Port conflict detected.');
62
62
  messages.push(' Run "aifabrix doctor" to check which ports are in use.');
63
63
  } else if ((errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) && !errorMsg.includes('permissions/') && !errorMsg.includes('Field "permissions')) {
@@ -69,13 +69,13 @@ function formatError(error) {
69
69
  messages.push(' Azure CLI is not installed or not working properly.');
70
70
  messages.push(' Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
71
71
  messages.push(' Run: az login');
72
+ } else if (errorMsg.includes('Invalid ACR URL') || errorMsg.includes('Invalid registry URL') || errorMsg.includes('Expected format')) {
73
+ messages.push(' Invalid registry URL format.');
74
+ messages.push(' Use format: *.azurecr.io (e.g., myacr.azurecr.io)');
72
75
  } else if (errorMsg.includes('authenticate') || errorMsg.includes('ACR') || errorMsg.includes('Authentication required')) {
73
76
  messages.push(' Azure Container Registry authentication failed.');
74
77
  messages.push(' Run: az acr login --name <registry-name>');
75
78
  messages.push(' Or login to Azure: az login');
76
- } else if (errorMsg.includes('Invalid ACR URL') || errorMsg.includes('Expected format')) {
77
- messages.push(' Invalid registry URL format.');
78
- messages.push(' Use format: *.azurecr.io (e.g., myacr.azurecr.io)');
79
79
  } else if (errorMsg.includes('Registry URL is required')) {
80
80
  messages.push(' Registry URL is required.');
81
81
  messages.push(' Provide via --registry flag or configure in variables.yaml under image.registry');
@@ -140,15 +140,61 @@ function parseTokenResponse(response) {
140
140
  };
141
141
  }
142
142
 
143
+ /**
144
+ * Creates a validation error with detailed information
145
+ * @function createValidationError
146
+ * @param {Object} response - Full API response object
147
+ * @returns {Error} Validation error with formattedError and errorData attached
148
+ */
149
+ function createValidationError(response) {
150
+ const validationError = new Error('Token polling failed: Validation error');
151
+
152
+ // Attach formatted error if available (includes detailed validation info with ANSI colors)
153
+ if (response && response.formattedError) {
154
+ validationError.formattedError = response.formattedError;
155
+ validationError.message = `Token polling failed:\n${response.formattedError}`;
156
+ }
157
+
158
+ // Attach error data for programmatic access
159
+ if (response && response.errorData) {
160
+ validationError.errorData = response.errorData;
161
+ validationError.errorType = response.errorType || 'validation';
162
+
163
+ // Build detailed message if formattedError not available
164
+ if (!validationError.formattedError) {
165
+ const errorData = response.errorData;
166
+ const detail = errorData.detail || errorData.title || errorData.message || 'Validation error';
167
+ let errorMsg = `Token polling failed: ${detail}`;
168
+ // Add validation errors if available
169
+ if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) {
170
+ errorMsg += '\n\nValidation errors:';
171
+ errorData.errors.forEach(err => {
172
+ const field = err.field || err.path || 'validation';
173
+ const message = err.message || 'Invalid value';
174
+ if (field === 'validation' || field === 'unknown') {
175
+ errorMsg += `\n • ${message}`;
176
+ } else {
177
+ errorMsg += `\n • ${field}: ${message}`;
178
+ }
179
+ });
180
+ }
181
+ validationError.message = errorMsg;
182
+ }
183
+ }
184
+
185
+ return validationError;
186
+ }
187
+
143
188
  /**
144
189
  * Handles polling errors
145
190
  * @function handlePollingErrors
146
191
  * @param {string} error - Error code
147
192
  * @param {number} status - HTTP status code
193
+ * @param {Object} response - Full API response object (for accessing formattedError and errorData)
148
194
  * @throws {Error} For fatal errors
149
195
  * @returns {boolean} True if should continue polling
150
196
  */
151
- function handlePollingErrors(error, status) {
197
+ function handlePollingErrors(error, status, response) {
152
198
  if (error === 'authorization_pending' || status === 202) {
153
199
  return true;
154
200
  }
@@ -166,6 +212,11 @@ function handlePollingErrors(error, status) {
166
212
  return true;
167
213
  }
168
214
 
215
+ // Handle validation errors with detailed message
216
+ if (error === 'validation_error' || status === 400) {
217
+ throw createValidationError(response);
218
+ }
219
+
169
220
  throw new Error(`Token polling failed: ${error}`);
170
221
  }
171
222
 
@@ -187,6 +238,18 @@ async function waitForNextPoll(interval, slowDown) {
187
238
  * @returns {string} Error code or 'Unknown error'
188
239
  */
189
240
  function extractPollingError(response) {
241
+ // Check for structured error data first (from api-error-handler)
242
+ if (response.errorData) {
243
+ const errorData = response.errorData;
244
+ // For validation errors, return the error type so we can handle it specially
245
+ if (response.errorType === 'validation') {
246
+ return 'validation_error';
247
+ }
248
+ // Return the error message from structured error
249
+ return errorData.detail || errorData.title || errorData.message || errorData.error || response.error || 'Unknown error';
250
+ }
251
+
252
+ // Fallback to original extraction logic
190
253
  const apiResponse = response.data || {};
191
254
  const errorData = typeof apiResponse === 'object' ? apiResponse : {};
192
255
  return errorData.error || response.error || 'Unknown error';
@@ -229,7 +292,7 @@ async function processPollingResponse(response, interval) {
229
292
  }
230
293
 
231
294
  const error = extractPollingError(response);
232
- const shouldContinue = handlePollingErrors(error, response.status);
295
+ const shouldContinue = handlePollingErrors(error, response.status, response);
233
296
 
234
297
  if (shouldContinue) {
235
298
  const slowDown = error === 'slow_down';
@@ -20,10 +20,11 @@ const logger = require('../utils/logger');
20
20
  * @param {string} appKey - Application key
21
21
  * @param {string} clientIdKey - Secret key for client ID (e.g., 'myapp-client-idKeyVault')
22
22
  * @param {string} clientSecretKey - Secret key for client secret (e.g., 'myapp-client-secretKeyVault')
23
- * @param {string} controllerUrl - Controller URL (e.g., 'http://localhost:3010' or 'https://controller.aifabrix.ai')
23
+ * @param {string} _controllerUrl - Controller URL (e.g., 'http://localhost:3010' or 'https://controller.aifabrix.ai')
24
+ * Note: This parameter is accepted for compatibility but the template format http://${MISO_HOST}:${MISO_PORT} is used instead
24
25
  * @returns {Promise<void>} Resolves when template is updated
25
26
  */
26
- async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey, controllerUrl) {
27
+ async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey, _controllerUrl) {
27
28
  const envTemplatePath = path.join(process.cwd(), 'builder', appKey, 'env.template');
28
29
 
29
30
  if (!fsSync.existsSync(envTemplatePath)) {
@@ -49,7 +50,7 @@ async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey, controlle
49
50
  }
50
51
 
51
52
  if (hasControllerUrl) {
52
- content = content.replace(/^MISO_CONTROLLER_URL\s*=.*$/m, `MISO_CONTROLLER_URL=${controllerUrl}`);
53
+ content = content.replace(/^MISO_CONTROLLER_URL\s*=.*$/m, 'MISO_CONTROLLER_URL=http://${MISO_HOST}:${MISO_PORT}');
53
54
  }
54
55
 
55
56
  // Add missing entries
@@ -62,7 +63,7 @@ async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey, controlle
62
63
  missingEntries.push(`MISO_CLIENTSECRET=kv://${clientSecretKey}`);
63
64
  }
64
65
  if (!hasControllerUrl) {
65
- missingEntries.push(`MISO_CONTROLLER_URL=${controllerUrl}`);
66
+ missingEntries.push('MISO_CONTROLLER_URL=http://${MISO_HOST}:${MISO_PORT}');
66
67
  }
67
68
 
68
69
  const misoSection = `# MISO Application Client Credentials (per application)