@aifabrix/builder 2.32.3 → 2.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/project-rules.mdc +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +12 -11
- package/integration/hubspot/companies.json +2048 -0
- package/integration/hubspot/create-hubspot.js +665 -0
- package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
- package/integration/hubspot/hubspot-deploy.json +832 -81
- package/integration/hubspot/hubspot-system.json +99 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
- package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
- package/integration/hubspot/test-dataplane-down-tests.js +419 -0
- package/integration/hubspot/test-dataplane-down.js +157 -0
- package/integration/hubspot/test.js +1517 -0
- package/integration/hubspot/variables.yaml +4 -4
- package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
- package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
- package/lib/api/applications.api.js +1 -0
- package/lib/api/index.js +6 -2
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +161 -23
- package/lib/app/deploy.js +116 -54
- package/lib/app/display.js +6 -5
- package/lib/app/dockerfile.js +2 -1
- package/lib/app/list.js +17 -10
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +48 -31
- package/lib/cli.js +219 -70
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +7 -8
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +26 -17
- package/lib/commands/login.js +12 -10
- package/lib/commands/wizard-config-normalizer.js +92 -0
- package/lib/commands/wizard-core.js +515 -0
- package/lib/commands/wizard-dataplane.js +122 -0
- package/lib/commands/wizard-headless.js +115 -0
- package/lib/commands/wizard.js +110 -332
- package/lib/core/config.js +46 -0
- package/lib/core/secrets.js +3 -22
- package/lib/core/templates-env.js +1 -1
- package/lib/datasource/deploy.js +59 -23
- package/lib/datasource/list.js +108 -19
- package/lib/deployment/deployer.js +25 -0
- package/lib/deployment/environment.js +10 -13
- package/lib/external-system/delete.js +151 -0
- package/lib/external-system/deploy.js +53 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +33 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +4 -3
- package/lib/generator/builders.js +3 -1
- package/lib/generator/external-controller-manifest.js +157 -0
- package/lib/generator/external-schema-utils.js +236 -0
- package/lib/generator/external.js +55 -3
- package/lib/generator/index.js +22 -10
- package/lib/generator/wizard-prompts.js +33 -10
- package/lib/generator/wizard.js +69 -86
- package/lib/infrastructure/compose.js +100 -0
- package/lib/infrastructure/helpers.js +139 -0
- package/lib/infrastructure/index.js +52 -311
- package/lib/infrastructure/services.js +168 -0
- package/lib/schema/application-schema.json +23 -4
- package/lib/schema/external-datasource.schema.json +2 -2
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +102 -52
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +65 -17
- package/lib/utils/dataplane-health.js +115 -0
- package/lib/utils/dataplane-resolver.js +29 -0
- package/lib/utils/dev-config.js +6 -2
- package/lib/utils/env-copy.js +2 -1
- package/lib/utils/env-ports.js +2 -1
- package/lib/utils/env-template.js +1 -1
- package/lib/utils/error-formatter.js +49 -0
- package/lib/utils/error-formatters/network-errors.js +13 -3
- package/lib/utils/external-readme.js +125 -0
- package/lib/utils/help-builder.js +190 -0
- package/lib/utils/infra-status.js +13 -3
- package/lib/utils/paths.js +17 -2
- package/lib/utils/port-resolver.js +111 -0
- package/lib/utils/secrets-helpers.js +3 -15
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/token-manager.js +9 -4
- package/lib/utils/variable-transformer.js +7 -2
- package/lib/validation/external-manifest-validator.js +202 -0
- package/lib/validation/validate-display.js +406 -0
- package/lib/validation/validate.js +159 -123
- package/lib/validation/validator.js +36 -3
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +18 -16
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +7 -7
- package/templates/external-system/README.md.hbs +99 -0
- package/templates/github/ci.yaml.hbs +44 -1
- package/templates/github/release.yaml.hbs +44 -0
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- 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
|
|
@@ -128,6 +131,32 @@ async function handleSuccessResponse(response, url, options, duration) {
|
|
|
128
131
|
return { success: true, data: text, status: response.status };
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Validates that a URL is not empty or missing
|
|
136
|
+
* @function validateUrl
|
|
137
|
+
* @param {string} url - URL to validate
|
|
138
|
+
* @param {string} [urlType='URL'] - Type of URL for error message (e.g., 'Dataplane URL', 'Controller URL')
|
|
139
|
+
* @returns {void}
|
|
140
|
+
* @throws {Error} If URL is empty, null, undefined, whitespace-only, or malformed
|
|
141
|
+
*/
|
|
142
|
+
function validateUrl(url, urlType = 'URL') {
|
|
143
|
+
if (!url || typeof url !== 'string') {
|
|
144
|
+
throw new Error(`${urlType} is required and must be a string (received: ${JSON.stringify(url)})`);
|
|
145
|
+
}
|
|
146
|
+
const trimmedUrl = url.trim();
|
|
147
|
+
if (!trimmedUrl) {
|
|
148
|
+
throw new Error(`${urlType} cannot be empty. Please provide a valid URL.`);
|
|
149
|
+
}
|
|
150
|
+
// Check for common invalid URL patterns
|
|
151
|
+
if (trimmedUrl === 'undefined' || trimmedUrl === 'null' || trimmedUrl === 'NaN') {
|
|
152
|
+
throw new Error(`${urlType} is invalid: "${trimmedUrl}". Please provide a valid URL.`);
|
|
153
|
+
}
|
|
154
|
+
// Basic URL format validation - must start with http:// or https://
|
|
155
|
+
if (!trimmedUrl.match(/^https?:\/\//i)) {
|
|
156
|
+
throw new Error(`${urlType} must be a valid HTTP/HTTPS URL (received: "${trimmedUrl}")`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
131
160
|
/**
|
|
132
161
|
* Handles network error from API call
|
|
133
162
|
* @async
|
|
@@ -139,7 +168,34 @@ async function handleSuccessResponse(response, url, options, duration) {
|
|
|
139
168
|
* @returns {Promise<Object>} Error response object
|
|
140
169
|
*/
|
|
141
170
|
async function handleNetworkError(error, url, options, duration) {
|
|
142
|
-
|
|
171
|
+
// Enhance error message with URL information if URL is missing or invalid
|
|
172
|
+
let errorMessage = error.message;
|
|
173
|
+
if (errorMessage && (errorMessage.includes('cannot be empty') || errorMessage.includes('is required'))) {
|
|
174
|
+
// Add URL context to validation errors
|
|
175
|
+
if (!url || !url.trim()) {
|
|
176
|
+
errorMessage = `${errorMessage} (URL was: ${JSON.stringify(url)})`;
|
|
177
|
+
} else {
|
|
178
|
+
errorMessage = `${errorMessage} (URL was: ${url})`;
|
|
179
|
+
}
|
|
180
|
+
} else if (!url || !url.trim()) {
|
|
181
|
+
// If URL is empty but error doesn't mention it, add context
|
|
182
|
+
errorMessage = `Invalid or missing URL. ${errorMessage} (URL was: ${JSON.stringify(url)})`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const parsedError = parseErrorResponse(errorMessage, 0, true);
|
|
186
|
+
|
|
187
|
+
// Extract controller URL from full URL for error data
|
|
188
|
+
let controllerUrl = null;
|
|
189
|
+
const endpointUrl = url;
|
|
190
|
+
if (url && typeof url === 'string' && url.trim()) {
|
|
191
|
+
try {
|
|
192
|
+
const urlObj = new URL(url);
|
|
193
|
+
controllerUrl = `${urlObj.protocol}//${urlObj.host}`;
|
|
194
|
+
} catch {
|
|
195
|
+
// If URL parsing fails, use the full URL as endpoint
|
|
196
|
+
controllerUrl = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
143
199
|
|
|
144
200
|
await logApiPerformance({
|
|
145
201
|
url,
|
|
@@ -154,10 +210,17 @@ async function handleNetworkError(error, url, options, duration) {
|
|
|
154
210
|
}
|
|
155
211
|
});
|
|
156
212
|
|
|
213
|
+
// Include both controller URL and full endpoint URL in error data
|
|
214
|
+
const errorData = {
|
|
215
|
+
...parsedError.data,
|
|
216
|
+
controllerUrl: controllerUrl,
|
|
217
|
+
endpointUrl: endpointUrl
|
|
218
|
+
};
|
|
219
|
+
|
|
157
220
|
return {
|
|
158
221
|
success: false,
|
|
159
222
|
error: parsedError.message,
|
|
160
|
-
errorData:
|
|
223
|
+
errorData: errorData,
|
|
161
224
|
errorType: parsedError.type,
|
|
162
225
|
formattedError: parsedError.formatted,
|
|
163
226
|
network: true
|
|
@@ -166,15 +229,28 @@ async function handleNetworkError(error, url, options, duration) {
|
|
|
166
229
|
|
|
167
230
|
/**
|
|
168
231
|
* Make an API call with proper error handling
|
|
232
|
+
* Uses a 15s timeout to avoid hanging when the controller is unreachable.
|
|
169
233
|
* @param {string} url - API endpoint URL
|
|
170
|
-
* @param {Object} options - Fetch options
|
|
234
|
+
* @param {Object} options - Fetch options (signal, method, headers, body, etc.)
|
|
171
235
|
* @returns {Promise<Object>} Response object with success flag
|
|
172
236
|
*/
|
|
173
237
|
async function makeApiCall(url, options = {}) {
|
|
238
|
+
// Validate URL before attempting request
|
|
239
|
+
try {
|
|
240
|
+
validateUrl(url, 'API endpoint URL');
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const duration = 0;
|
|
243
|
+
return await handleNetworkError(error, url || '', options, duration);
|
|
244
|
+
}
|
|
245
|
+
|
|
174
246
|
const startTime = Date.now();
|
|
247
|
+
const fetchOptions = { ...options };
|
|
248
|
+
if (!fetchOptions.signal) {
|
|
249
|
+
fetchOptions.signal = AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS);
|
|
250
|
+
}
|
|
175
251
|
|
|
176
252
|
try {
|
|
177
|
-
const response = await fetch(url,
|
|
253
|
+
const response = await fetch(url, fetchOptions);
|
|
178
254
|
const duration = Date.now() - startTime;
|
|
179
255
|
|
|
180
256
|
if (!response.ok) {
|
|
@@ -182,8 +258,13 @@ async function makeApiCall(url, options = {}) {
|
|
|
182
258
|
}
|
|
183
259
|
|
|
184
260
|
return await handleSuccessResponse(response, url, options, duration);
|
|
185
|
-
} catch (
|
|
261
|
+
} catch (err) {
|
|
186
262
|
const duration = Date.now() - startTime;
|
|
263
|
+
const error = err?.name === 'AbortError'
|
|
264
|
+
? new Error(
|
|
265
|
+
`Request timed out after ${DEFAULT_REQUEST_TIMEOUT_MS / 1000} seconds. The controller may be unreachable. Check the URL and network.`
|
|
266
|
+
)
|
|
267
|
+
: err;
|
|
187
268
|
return await handleNetworkError(error, url, options, duration);
|
|
188
269
|
}
|
|
189
270
|
}
|
|
@@ -214,20 +295,16 @@ function extractControllerUrl(url) {
|
|
|
214
295
|
* @param {string} [tokenOrAuthConfig.controller] - Controller URL for token refresh (if object)
|
|
215
296
|
* @returns {Promise<Object>} Response object
|
|
216
297
|
*/
|
|
298
|
+
// eslint-disable-next-line max-statements
|
|
217
299
|
async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
|
|
218
|
-
|
|
219
|
-
const token =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const headers = {
|
|
227
|
-
'Content-Type': 'application/json',
|
|
228
|
-
...options.headers
|
|
229
|
-
};
|
|
230
|
-
|
|
300
|
+
const isStringToken = typeof tokenOrAuthConfig === 'string';
|
|
301
|
+
const token = isStringToken ? tokenOrAuthConfig : tokenOrAuthConfig?.token;
|
|
302
|
+
const authControllerUrl = isStringToken ? null : tokenOrAuthConfig?.controller;
|
|
303
|
+
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData;
|
|
304
|
+
const headers = { ...options.headers };
|
|
305
|
+
if (!isFormData && !headers['Content-Type']) {
|
|
306
|
+
headers['Content-Type'] = 'application/json';
|
|
307
|
+
}
|
|
231
308
|
if (token) {
|
|
232
309
|
headers['Authorization'] = `Bearer ${token}`;
|
|
233
310
|
}
|
|
@@ -237,46 +314,19 @@ async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
|
|
|
237
314
|
headers
|
|
238
315
|
});
|
|
239
316
|
|
|
240
|
-
// Handle 401 errors with automatic token refresh for device tokens
|
|
241
317
|
if (!response.success && response.status === 401) {
|
|
242
318
|
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
319
|
const { forceRefreshDeviceToken } = require('./token-manager');
|
|
251
|
-
const refreshedToken = await forceRefreshDeviceToken(
|
|
252
|
-
|
|
253
|
-
if (refreshedToken && refreshedToken.token) {
|
|
254
|
-
// Retry request with new token
|
|
320
|
+
const refreshedToken = await forceRefreshDeviceToken(authControllerUrl || extractControllerUrl(url));
|
|
321
|
+
if (refreshedToken?.token) {
|
|
255
322
|
headers['Authorization'] = `Bearer ${refreshedToken.token}`;
|
|
256
|
-
|
|
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
|
-
};
|
|
323
|
+
return await makeApiCall(url, { ...options, headers });
|
|
271
324
|
}
|
|
325
|
+
const authError = 'Authentication failed: Token expired and refresh failed. Please login again using: aifabrix login';
|
|
326
|
+
return { ...response, error: authError, formattedError: authError };
|
|
272
327
|
} catch (refreshError) {
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
};
|
|
328
|
+
const authError = `Authentication failed: ${refreshError.message || String(refreshError)}. Please login again using: aifabrix login`;
|
|
329
|
+
return { ...response, error: authError, formattedError: authError };
|
|
280
330
|
}
|
|
281
331
|
}
|
|
282
332
|
|
|
@@ -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 =
|
|
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
|
+
};
|