@aifabrix/builder 2.41.0 → 2.42.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.
Files changed (142) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +2 -2
  3. package/integration/hubspot/README.md +11 -5
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +36 -2
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +5 -3
  24. package/lib/app/prompts.js +46 -31
  25. package/lib/app/readme.js +11 -4
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +45 -14
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/auth-config.js +22 -12
  37. package/lib/commands/credential-env.js +162 -0
  38. package/lib/commands/credential-list.js +17 -22
  39. package/lib/commands/credential-push.js +96 -0
  40. package/lib/commands/datasource.js +77 -6
  41. package/lib/commands/dev-init.js +39 -1
  42. package/lib/commands/repair-auth-config.js +99 -0
  43. package/lib/commands/repair-datasource-keys.js +208 -0
  44. package/lib/commands/repair-datasource.js +235 -0
  45. package/lib/commands/repair-env-template.js +348 -0
  46. package/lib/commands/repair-internal.js +85 -0
  47. package/lib/commands/repair-rbac.js +158 -0
  48. package/lib/commands/repair.js +518 -0
  49. package/lib/commands/secrets-set.js +6 -0
  50. package/lib/commands/test-e2e-external.js +165 -0
  51. package/lib/commands/up-dataplane.js +90 -6
  52. package/lib/commands/upload.js +71 -40
  53. package/lib/commands/wizard-core-helpers.js +230 -5
  54. package/lib/commands/wizard-core.js +68 -29
  55. package/lib/commands/wizard-dataplane.js +1 -1
  56. package/lib/commands/wizard-entity-selection.js +43 -0
  57. package/lib/commands/wizard-headless.js +49 -5
  58. package/lib/commands/wizard-helpers.js +7 -3
  59. package/lib/commands/wizard.js +93 -64
  60. package/lib/core/config.js +7 -1
  61. package/lib/core/secrets.js +33 -12
  62. package/lib/datasource/deploy.js +12 -3
  63. package/lib/datasource/test-e2e.js +219 -0
  64. package/lib/datasource/test-integration.js +154 -0
  65. package/lib/deployment/deployer.js +7 -5
  66. package/lib/external-system/download-helpers.js +3 -1
  67. package/lib/external-system/download.js +182 -204
  68. package/lib/external-system/generator.js +204 -56
  69. package/lib/external-system/test-execution.js +2 -1
  70. package/lib/external-system/test-system-level.js +73 -0
  71. package/lib/external-system/test.js +51 -18
  72. package/lib/generator/external-controller-manifest.js +29 -2
  73. package/lib/generator/external-schema-utils.js +4 -2
  74. package/lib/generator/external.js +10 -3
  75. package/lib/generator/index.js +4 -1
  76. package/lib/generator/split-readme.js +1 -0
  77. package/lib/generator/split-variables.js +7 -1
  78. package/lib/generator/split.js +194 -54
  79. package/lib/generator/wizard-prompts-secondary.js +326 -0
  80. package/lib/generator/wizard-prompts.js +105 -106
  81. package/lib/generator/wizard-readme.js +91 -0
  82. package/lib/generator/wizard.js +180 -179
  83. package/lib/infrastructure/compose.js +11 -1
  84. package/lib/infrastructure/index.js +11 -3
  85. package/lib/infrastructure/services.js +22 -11
  86. package/lib/schema/application-schema.json +8 -5
  87. package/lib/schema/external-datasource.schema.json +49 -26
  88. package/lib/schema/external-system.schema.json +82 -6
  89. package/lib/schema/wizard-config.schema.json +23 -1
  90. package/lib/utils/api.js +38 -10
  91. package/lib/utils/auth-headers.js +8 -7
  92. package/lib/utils/compose-generator.js +1 -1
  93. package/lib/utils/compose-handlebars-helpers.js +11 -0
  94. package/lib/utils/config-format-preference.js +51 -0
  95. package/lib/utils/config-format.js +36 -0
  96. package/lib/utils/configuration-env-resolver.js +179 -0
  97. package/lib/utils/credential-display.js +83 -0
  98. package/lib/utils/credential-secrets-env.js +115 -25
  99. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  100. package/lib/utils/deployment-validation-helpers.js +4 -4
  101. package/lib/utils/dev-ca-install.js +139 -0
  102. package/lib/utils/env-copy.js +23 -3
  103. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  104. package/lib/utils/error-formatters/permission-errors.js +0 -1
  105. package/lib/utils/error-formatters/validation-errors.js +0 -1
  106. package/lib/utils/external-readme.js +89 -30
  107. package/lib/utils/external-system-display.js +59 -1
  108. package/lib/utils/external-system-test-helpers.js +21 -8
  109. package/lib/utils/external-system-validators.js +3 -0
  110. package/lib/utils/file-upload.js +20 -50
  111. package/lib/utils/help-builder.js +1 -0
  112. package/lib/utils/infra-status.js +50 -44
  113. package/lib/utils/local-secrets.js +5 -5
  114. package/lib/utils/paths.js +85 -4
  115. package/lib/utils/secrets-canonical.js +93 -0
  116. package/lib/utils/secrets-generator.js +20 -0
  117. package/lib/utils/secrets-helpers.js +75 -89
  118. package/lib/utils/test-log-writer.js +56 -0
  119. package/lib/utils/token-manager.js +24 -32
  120. package/lib/validation/env-template-auth.js +157 -0
  121. package/lib/validation/env-template-kv.js +41 -0
  122. package/lib/validation/external-manifest-validator.js +25 -0
  123. package/lib/validation/external-system-auth-rules.js +86 -0
  124. package/lib/validation/validate-batch.js +149 -0
  125. package/lib/validation/validate-datasource-keys-api.js +33 -0
  126. package/lib/validation/validate-display.js +94 -16
  127. package/lib/validation/validate.js +25 -12
  128. package/lib/validation/validator.js +7 -9
  129. package/lib/validation/wizard-datasource-validation.js +50 -0
  130. package/package.json +7 -2
  131. package/templates/applications/dataplane/application.yaml +1 -1
  132. package/templates/applications/dataplane/env.template +5 -5
  133. package/templates/applications/dataplane/rbac.yaml +2 -2
  134. package/templates/applications/miso-controller/env.template +1 -1
  135. package/templates/external-system/README.md.hbs +75 -22
  136. package/templates/external-system/deploy.js.hbs +4 -2
  137. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  138. package/templates/external-system/external-system.json.hbs +1 -18
  139. package/templates/infra/compose.yaml.hbs +6 -0
  140. package/templates/python/docker-compose.hbs +4 -4
  141. package/templates/typescript/docker-compose.hbs +4 -4
  142. package/integration/hubspot/application.yaml +0 -37
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Repair external integration config: fix drift between config and files on disk.
3
+ *
4
+ * Aligns application.yaml with actual system/datasource files, fixes system key mismatch,
5
+ * creates missing externalIntegration block, extracts rbac.yaml from system when needed,
6
+ * and regenerates the deployment manifest.
7
+ *
8
+ * @fileoverview Repair external integration drift
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+ /* eslint-disable max-lines -- Repair flow with auth, normalization, and steps */
13
+
14
+ 'use strict';
15
+
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+ const chalk = require('chalk');
19
+ const yaml = require('js-yaml');
20
+ const { detectAppType } = require('../utils/paths');
21
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
22
+ const { loadConfigFile, writeConfigFile, writeYamlPreservingComments, isYamlPath } = require('../utils/config-format');
23
+ const { systemKeyToKvPrefix, securityKeyToVar } = require('../utils/credential-secrets-env');
24
+ const logger = require('../utils/logger');
25
+ const generator = require('../generator');
26
+ const { repairEnvTemplate, normalizeSystemFileAuthAndConfig } = require('./repair-env-template');
27
+ const { repairDatasourceFile } = require('./repair-datasource');
28
+ const { mergeRbacFromDatasources } = require('./repair-rbac');
29
+ const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
30
+ const { normalizeDatasourceKeysAndFilenames } = require('./repair-datasource-keys');
31
+
32
+ /** Allowed authentication methods for repair --auth (matches external-system schema) */
33
+ const ALLOWED_AUTH = ['oauth2', 'aad', 'apikey', 'basic', 'queryParam', 'oidc', 'hmac', 'none'];
34
+
35
+ /**
36
+ * Extracts roles and permissions from system object for rbac.yaml
37
+ * @param {Object} system - Parsed system config
38
+ * @returns {Object|null} RBAC object or null
39
+ */
40
+ function extractRbacFromSystem(system) {
41
+ if (!system || typeof system !== 'object') return null;
42
+ const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
43
+ const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
44
+ if (!hasRoles && !hasPermissions) return null;
45
+ const rbac = {};
46
+ if (hasRoles) rbac.roles = system.roles;
47
+ if (hasPermissions) rbac.permissions = system.permissions;
48
+ return rbac;
49
+ }
50
+
51
+ /**
52
+ * Loads first system file and returns parsed object with key
53
+ * @param {string} appPath - Application path
54
+ * @param {string} systemFileName - System file name
55
+ * @returns {Object} Parsed system config
56
+ */
57
+ function loadFirstSystemFile(appPath, systemFileName) {
58
+ const systemPath = path.join(appPath, systemFileName);
59
+ if (!fs.existsSync(systemPath)) {
60
+ throw new Error(`System file not found: ${systemPath}`);
61
+ }
62
+ return loadConfigFile(systemPath);
63
+ }
64
+
65
+ function resolveSystemContext(appPath, systemFiles) {
66
+ const systemFilePath = path.join(appPath, systemFiles[0]);
67
+ const systemParsed = loadFirstSystemFile(appPath, systemFiles[0]);
68
+ const systemKey = systemParsed.key ||
69
+ path.basename(systemFiles[0], path.extname(systemFiles[0])).replace(/-system$/, '');
70
+ return { systemFilePath, systemParsed, systemKey };
71
+ }
72
+
73
+ function ensureExternalIntegrationBlock(variables, systemFiles, datasourceFiles, changes) {
74
+ const extInt = variables.externalIntegration;
75
+ let updated = false;
76
+ if (!extInt) {
77
+ variables.externalIntegration = {
78
+ schemaBasePath: './',
79
+ systems: systemFiles,
80
+ dataSources: datasourceFiles,
81
+ autopublish: true,
82
+ version: '1.0.0'
83
+ };
84
+ changes.push('Created externalIntegration block from discovered files');
85
+ updated = true;
86
+ } else {
87
+ const prevSystems = extInt.systems || [];
88
+ const prevDataSources = extInt.dataSources || [];
89
+ if (JSON.stringify(prevSystems) !== JSON.stringify(systemFiles)) {
90
+ changes.push(`systems: [${prevSystems.join(', ')}] → [${systemFiles.join(', ')}]`);
91
+ extInt.systems = systemFiles;
92
+ updated = true;
93
+ }
94
+ if (JSON.stringify(prevDataSources) !== JSON.stringify(datasourceFiles)) {
95
+ changes.push(`dataSources: [${prevDataSources.join(', ')}] → [${datasourceFiles.join(', ')}]`);
96
+ extInt.dataSources = datasourceFiles;
97
+ updated = true;
98
+ }
99
+ }
100
+ return updated;
101
+ }
102
+
103
+ function alignAppKeyWithSystem(variables, systemKey, systemParsed, changes) {
104
+ const appKey = variables.app?.key;
105
+ if (!appKey || appKey === systemKey) return false;
106
+ if (!variables.app) variables.app = {};
107
+ variables.app.key = systemKey;
108
+ changes.push(`app.key: ${appKey} → ${systemKey}`);
109
+ return true;
110
+ }
111
+
112
+ /**
113
+ * Aligns datasource systemKey values to match the system key.
114
+ * Updates each datasource file whose systemKey differs from the system key.
115
+ *
116
+ * @param {string} appPath - Application directory path
117
+ * @param {string[]} datasourceFiles - Datasource file names
118
+ * @param {string} systemKey - Expected system key
119
+ * @param {boolean} dryRun - If true, report changes but do not write
120
+ * @param {string[]} changes - Array to append change descriptions to
121
+ * @returns {boolean} True if any file was updated (or would be in dry-run)
122
+ */
123
+ function alignDatasourceSystemKeys(appPath, datasourceFiles, systemKey, dryRun, changes) {
124
+ if (!datasourceFiles || datasourceFiles.length === 0) return false;
125
+ let updated = false;
126
+ for (const datasourceFile of datasourceFiles) {
127
+ const datasourcePath = path.join(appPath, datasourceFile);
128
+ if (!fs.existsSync(datasourcePath)) continue;
129
+ const parsed = loadConfigFile(datasourcePath);
130
+ const old = parsed.systemKey;
131
+ if (old !== systemKey) {
132
+ parsed.systemKey = systemKey;
133
+ if (!dryRun) {
134
+ writeConfigFile(datasourcePath, parsed);
135
+ }
136
+ changes.push(`${datasourceFile}: systemKey ${old} → ${systemKey}`);
137
+ updated = true;
138
+ }
139
+ }
140
+ return updated;
141
+ }
142
+
143
+ /**
144
+ * Derives a datasource key from filename when the file has no key.
145
+ * - hubspot-datasource-company.json → hubspot-company
146
+ * - datasource-companies.json → {systemKey}-companies (e.g. test-hubspot-companies)
147
+ *
148
+ * @param {string} fileName - Datasource file name
149
+ * @param {string} systemKey - System key (e.g. test-hubspot), used for datasource-*.json style names
150
+ * @returns {string}
151
+ */
152
+ function deriveDatasourceKeyFromFileName(fileName, systemKey) {
153
+ const base = path.basename(fileName, path.extname(fileName));
154
+ if (/^datasource-/.test(base)) {
155
+ const suffix = base.slice('datasource-'.length);
156
+ return systemKey && typeof systemKey === 'string' ? `${systemKey}-${suffix}` : base;
157
+ }
158
+ return base.replace(/-datasource-/, '-');
159
+ }
160
+
161
+ /**
162
+ * Aligns system file dataSources array to match datasource keys from discovered files.
163
+ * The system file holds logical keys (not filenames): each key comes from that datasource
164
+ * file's "key" property, or is derived from the filename when missing (e.g. datasource-companies.json → {systemKey}-companies).
165
+ *
166
+ * @param {string} appPath - Application directory path
167
+ * @param {Object} systemParsed - Parsed system config (mutated)
168
+ * @param {string[]} datasourceFiles - Datasource file names
169
+ * @param {string} systemKey - System key for deriving key when missing
170
+ * @param {boolean} dryRun - If true, report changes but do not write
171
+ * @param {string[]} changes - Array to append change descriptions to
172
+ * @returns {boolean} True if dataSources was updated (or would be in dry-run)
173
+ */
174
+ function alignSystemFileDataSources(appPath, systemParsed, datasourceFiles, systemKey, dryRun, changes) {
175
+ const keys = [];
176
+ for (const fileName of datasourceFiles) {
177
+ const filePath = path.join(appPath, fileName);
178
+ if (!fs.existsSync(filePath)) continue;
179
+ try {
180
+ const parsed = loadConfigFile(filePath);
181
+ const key = parsed && typeof parsed.key === 'string' && parsed.key.trim()
182
+ ? parsed.key.trim()
183
+ : deriveDatasourceKeyFromFileName(fileName, systemKey);
184
+ keys.push(key);
185
+ } catch (err) {
186
+ logger.log(chalk.yellow(`⚠ Could not load datasource file ${fileName}: ${err.message}; using derived key`));
187
+ keys.push(deriveDatasourceKeyFromFileName(fileName, systemKey));
188
+ }
189
+ }
190
+ keys.sort();
191
+ const prev = Array.isArray(systemParsed.dataSources) ? [...systemParsed.dataSources].sort() : [];
192
+ if (JSON.stringify(prev) === JSON.stringify(keys)) return false;
193
+ systemParsed.dataSources = keys;
194
+ changes.push(`dataSources: [${prev.join(', ') || '(none)'}] → [${keys.join(', ')}] (keys from each datasource file's "key" or filename)`);
195
+ return true;
196
+ }
197
+
198
+ /**
199
+ * Builds the set of auth variable names (UPPERCASE, no underscores) from authentication.variables and authentication.security.
200
+ * Used to detect configuration entries that belong only in the authentication section.
201
+ * Canonical keys per method are in lib/schema/external-system.schema.json $defs.authenticationVariablesByMethod
202
+ * (e.g. oauth2: variables baseUrl, tokenUrl, scope; security clientId, clientSecret).
203
+ *
204
+ * @param {Object} systemParsed - Parsed system config
205
+ * @param {string} _systemKey - System key (unused; for API consistency)
206
+ * @returns {Set<string>}
207
+ */
208
+ function buildAuthVarNames(systemParsed, _systemKey) {
209
+ const names = new Set();
210
+ const auth = systemParsed.authentication;
211
+ if (auth && typeof auth.variables === 'object') {
212
+ for (const k of Object.keys(auth.variables)) {
213
+ names.add(String(k).toUpperCase().replace(/_/g, ''));
214
+ }
215
+ }
216
+ if (auth && typeof auth.security === 'object') {
217
+ for (const k of Object.keys(auth.security)) {
218
+ names.add(securityKeyToVar(k));
219
+ }
220
+ }
221
+ return names;
222
+ }
223
+
224
+ /**
225
+ * Removes from system configuration any entry that represents standard auth variables
226
+ * (BASEURL, CLIENTID, CLIENTSECRET, TOKENURL, APIKEY, USERNAME, PASSWORD, etc.).
227
+ * These are supplied from the selected credential at runtime; the configuration array
228
+ * should contain only custom variables. Removes both plain and keyvault auth entries.
229
+ *
230
+ * @param {Object} systemParsed - Parsed system config (mutated)
231
+ * @param {string} systemKey - System key for naming consistency
232
+ * @param {boolean} dryRun - If true, report changes but do not write
233
+ * @param {string[]} changes - Array to append change descriptions to
234
+ * @returns {boolean} True if any entry was removed
235
+ */
236
+ /**
237
+ * Derives the normalized auth variable part from a config entry name (for matching against authNames).
238
+ * E.g. KV_HUBSPOT_CLIENTID → CLIENTID, BASEURL → BASEURL.
239
+ * @param {string} name - Entry name
240
+ * @param {string} systemKey - System key for KV_ prefix
241
+ * @returns {string}
242
+ */
243
+ function normalizedAuthPartFromConfigName(name, systemKey) {
244
+ const n = String(name).trim();
245
+ if (!n) return '';
246
+ const prefix = systemKeyToKvPrefix(systemKey);
247
+ const kvPrefix = `KV_${prefix}_`;
248
+ if (n.toUpperCase().startsWith(kvPrefix)) {
249
+ const rest = n.slice(kvPrefix.length);
250
+ return rest.toUpperCase().replace(/_/g, '');
251
+ }
252
+ return n.toUpperCase().replace(/_/g, '');
253
+ }
254
+
255
+ function removeAuthVarsFromConfiguration(systemParsed, systemKey, dryRun, changes) {
256
+ const config = systemParsed.configuration;
257
+ if (!Array.isArray(config)) return false;
258
+ const authNames = buildAuthVarNames(systemParsed, systemKey);
259
+ if (authNames.size === 0) return false;
260
+ const removed = [];
261
+ const filtered = config.filter((entry) => {
262
+ if (!entry || !entry.name) return true;
263
+ const authPart = normalizedAuthPartFromConfigName(entry.name, systemKey);
264
+ if (authNames.has(authPart)) {
265
+ removed.push(entry.name);
266
+ return false;
267
+ }
268
+ return true;
269
+ });
270
+ if (removed.length === 0) return false;
271
+ systemParsed.configuration = filtered;
272
+ changes.push(`Removed authentication variable(s) from configuration: ${removed.join(', ')}`);
273
+ return true;
274
+ }
275
+
276
+ function createRbacFromSystemIfNeeded(appPath, systemFilePath, systemParsed, dryRun, changes) {
277
+ const rbacPath = path.join(appPath, 'rbac.yaml');
278
+ const rbacYmlPath = path.join(appPath, 'rbac.yml');
279
+ if (fs.existsSync(rbacPath) || fs.existsSync(rbacYmlPath)) return false;
280
+ const rbacFromSystem = extractRbacFromSystem(systemParsed);
281
+ if (!rbacFromSystem) return false;
282
+ if (!dryRun) {
283
+ const rbacYaml = yaml.dump(rbacFromSystem, { indent: 2, lineWidth: -1 });
284
+ fs.writeFileSync(rbacPath, rbacYaml, { mode: 0o644, encoding: 'utf8' });
285
+ delete systemParsed.roles;
286
+ delete systemParsed.permissions;
287
+ writeConfigFile(systemFilePath, systemParsed);
288
+ }
289
+ changes.push('Created rbac.yaml from system roles/permissions');
290
+ changes.push('Removed roles/permissions from system file (now in rbac.yaml)');
291
+ return true;
292
+ }
293
+
294
+ /**
295
+ * Runs datasource repair for each file (dimensions, metadataSchema, optional expose/sync/test).
296
+ * @param {string} appPath - Application path
297
+ * @param {string[]} datasourceFiles - Datasource file names
298
+ * @param {Object} options - { expose?: boolean, sync?: boolean, test?: boolean }
299
+ * @param {boolean} dryRun - If true, do not write
300
+ * @param {string[]} changes - Array to append change descriptions to
301
+ * @returns {boolean} True if any datasource was updated
302
+ */
303
+ function runDatasourceRepairs(appPath, datasourceFiles, options, dryRun, changes) {
304
+ if (!datasourceFiles || datasourceFiles.length === 0) return false;
305
+ let updated = false;
306
+ for (const fileName of datasourceFiles) {
307
+ const filePath = path.join(appPath, fileName);
308
+ if (!fs.existsSync(filePath)) continue;
309
+ try {
310
+ const parsed = loadConfigFile(filePath);
311
+ const { updated: fileUpdated, changes: fileChanges } = repairDatasourceFile(parsed, {
312
+ expose: options.expose,
313
+ sync: options.sync,
314
+ test: options.test
315
+ });
316
+ if (fileUpdated) {
317
+ updated = true;
318
+ fileChanges.forEach(c => changes.push(`${fileName}: ${c}`));
319
+ if (!dryRun) {
320
+ writeConfigFile(filePath, parsed);
321
+ }
322
+ }
323
+ } catch (err) {
324
+ logger.log(chalk.yellow(`⚠ Could not repair datasource ${fileName}: ${err.message}`));
325
+ }
326
+ }
327
+ return updated;
328
+ }
329
+
330
+ async function regenerateManifest(appName, appPath, changes) {
331
+ try {
332
+ const deployPath = await generator.generateDeployJson(appName, { appPath });
333
+ changes.push(`Regenerated ${path.basename(deployPath)}`);
334
+ return true;
335
+ } catch (err) {
336
+ logger.log(chalk.yellow(`⚠ Manifest regeneration skipped: ${err.message}`));
337
+ return false;
338
+ }
339
+ }
340
+
341
+ function persistChangesAndRegenerate(configPath, variables, appName, appPath, changes, originalYamlContent) {
342
+ if (originalYamlContent !== null && originalYamlContent !== undefined && typeof originalYamlContent === 'string' && isYamlPath(configPath)) {
343
+ writeYamlPreservingComments(configPath, originalYamlContent, variables);
344
+ } else {
345
+ writeConfigFile(configPath, variables);
346
+ }
347
+ logger.log(chalk.green(`✓ Updated ${path.basename(configPath)}`));
348
+ changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
349
+ return regenerateManifest(appName, appPath, changes);
350
+ }
351
+
352
+ /**
353
+ * Apply --auth: replace system file authentication with canonical block for the given method.
354
+ * Preserves existing authentication.variables (e.g. baseUrl, tokenUrl) from the current system file.
355
+ * @param {Object} ctx - Context with systemParsed, systemKey, auth, dryRun, changes
356
+ * @returns {boolean} True if auth was replaced
357
+ */
358
+ function applyAuthMethod(ctx) {
359
+ if (!ctx.auth || typeof ctx.auth !== 'string') return false;
360
+ const method = ctx.auth.trim().toLowerCase();
361
+ if (!ALLOWED_AUTH.includes(method)) {
362
+ throw new Error(
363
+ `Invalid --auth "${ctx.auth}". Allowed methods: ${ALLOWED_AUTH.join(', ')}`
364
+ );
365
+ }
366
+ const existingAuth = ctx.systemParsed.authentication || ctx.systemParsed.auth || {};
367
+ const { buildAuthenticationFromMethod } = require('../external-system/generator');
368
+ const newAuth = buildAuthenticationFromMethod(ctx.systemKey, method);
369
+ const existingVars = existingAuth.variables && typeof existingAuth.variables === 'object' ? existingAuth.variables : {};
370
+ const mergedVariables = { ...newAuth.variables, ...existingVars };
371
+ ctx.systemParsed.authentication = {
372
+ ...newAuth,
373
+ variables: Object.keys(mergedVariables).length ? mergedVariables : newAuth.variables
374
+ };
375
+ if (existingAuth.displayName !== undefined) {
376
+ ctx.systemParsed.authentication.displayName = existingAuth.displayName;
377
+ }
378
+ ctx.changes.push(`Set authentication method to ${method}`);
379
+ return true;
380
+ }
381
+
382
+ /**
383
+ * Runs all repair steps (integration block, system dataSources, auth/config, app key, datasource keys, rbac, env.template).
384
+ * @param {Object} ctx - Context with appPath, configPath, variables, systemFilePath, systemParsed, systemKey, systemFiles, datasourceFiles, dryRun, changes, auth?
385
+ * @returns {{ updated: boolean, appKeyFixed: boolean, datasourceKeysFixed: boolean, rbacFileCreated: boolean, envTemplateRepaired: boolean }}
386
+ */
387
+ function runRepairSteps(ctx) {
388
+ let updated = ensureExternalIntegrationBlock(
389
+ ctx.variables, ctx.systemFiles, ctx.datasourceFiles, ctx.changes
390
+ );
391
+ const authReplaced = applyAuthMethod(ctx);
392
+ updated = updated || authReplaced;
393
+ const systemAuthConfigNormalized = normalizeSystemFileAuthAndConfig(
394
+ ctx.systemParsed, ctx.systemKey, ctx.changes
395
+ );
396
+ const systemDataSourcesAligned = alignSystemFileDataSources(
397
+ ctx.appPath, ctx.systemParsed, ctx.datasourceFiles, ctx.systemKey, ctx.dryRun, ctx.changes
398
+ );
399
+ const authVarsRemoved = removeAuthVarsFromConfiguration(
400
+ ctx.systemParsed, ctx.systemKey, ctx.dryRun, ctx.changes
401
+ );
402
+ if ((authReplaced || systemAuthConfigNormalized || systemDataSourcesAligned || authVarsRemoved) && !ctx.dryRun) {
403
+ writeConfigFile(ctx.systemFilePath, ctx.systemParsed);
404
+ }
405
+ updated = updated || systemAuthConfigNormalized || systemDataSourcesAligned || authVarsRemoved;
406
+ const appKeyFixed = alignAppKeyWithSystem(
407
+ ctx.variables, ctx.systemKey, ctx.systemParsed, ctx.changes
408
+ );
409
+ const datasourceKeysFixed = alignDatasourceSystemKeys(
410
+ ctx.appPath, ctx.datasourceFiles, ctx.systemKey, ctx.dryRun, ctx.changes
411
+ );
412
+ const rbacFileCreated = createRbacFromSystemIfNeeded(
413
+ ctx.appPath, ctx.systemFilePath, ctx.systemParsed, ctx.dryRun, ctx.changes
414
+ );
415
+ const envTemplateRepaired = repairEnvTemplate(
416
+ ctx.appPath, ctx.systemParsed, ctx.systemKey, ctx.dryRun, ctx.changes
417
+ );
418
+ updated = updated || appKeyFixed || datasourceKeysFixed || rbacFileCreated || envTemplateRepaired;
419
+ return {
420
+ updated,
421
+ appKeyFixed,
422
+ datasourceKeysFixed,
423
+ rbacFileCreated,
424
+ envTemplateRepaired
425
+ };
426
+ }
427
+
428
+ /**
429
+ * Repairs external integration config: syncs files, aligns keys, creates rbac, regenerates manifest
430
+ * @async
431
+ * @param {string} appName - Application/integration name
432
+ * @param {Object} [options] - Options
433
+ * @param {boolean} [options.dryRun] - If true, only report changes; do not write
434
+ * @returns {Promise<{ updated: boolean, changes: string[], systemFiles: string[], datasourceFiles: string[], appKeyFixed?: boolean, datasourceKeysFixed?: boolean, rbacFileCreated?: boolean, envTemplateRepaired?: boolean, manifestRegenerated?: boolean }>}
435
+ */
436
+ /**
437
+ * Loads application config and discovers integration files; validates at least one system file exists.
438
+ * @param {string} appPath - Application path
439
+ * @param {string} configPath - Path to application config
440
+ * @returns {{ variables: Object, originalYamlContent: string|null, systemFiles: string[], datasourceFiles: string[] }}
441
+ */
442
+ function loadConfigAndDiscover(appPath, configPath) {
443
+ const variables = loadConfigFile(configPath);
444
+ let originalYamlContent = null;
445
+ if (isYamlPath(configPath)) {
446
+ originalYamlContent = fs.readFileSync(configPath, 'utf8');
447
+ }
448
+ const { systemFiles, datasourceFiles: discoveredDatasourceFiles } = discoverIntegrationFiles(appPath);
449
+ if (systemFiles.length === 0) {
450
+ throw new Error(`No system file found in ${appPath}. Expected *-system.yaml or *-system.json`);
451
+ }
452
+ const datasourceFiles = buildEffectiveDatasourceFiles(appPath, discoveredDatasourceFiles, variables.externalIntegration?.dataSources);
453
+ return { variables, originalYamlContent, systemFiles, datasourceFiles };
454
+ }
455
+
456
+ async function repairExternalIntegration(appName, options = {}) {
457
+ if (!appName || typeof appName !== 'string') throw new Error('App name is required');
458
+ const { dryRun = false, auth: authOption } = options;
459
+ if (authOption !== undefined && authOption !== null && typeof authOption !== 'string') {
460
+ throw new Error('Option --auth must be a string');
461
+ }
462
+ const { appPath, isExternal } = await detectAppType(appName);
463
+ if (!isExternal) throw new Error(`App '${appName}' is not an external integration`);
464
+ const configPath = resolveApplicationConfigPath(appPath);
465
+ if (!fs.existsSync(configPath)) throw new Error(`Application config not found: ${configPath}`);
466
+
467
+ const { variables, originalYamlContent, systemFiles, datasourceFiles: initialDatasourceFiles } = loadConfigAndDiscover(appPath, configPath);
468
+ const changes = [];
469
+ const { systemFilePath, systemParsed, systemKey } = resolveSystemContext(appPath, systemFiles);
470
+ const { updated: keysNormalized, datasourceFiles } = normalizeDatasourceKeysAndFilenames(
471
+ appPath,
472
+ initialDatasourceFiles,
473
+ systemKey,
474
+ variables,
475
+ dryRun,
476
+ changes
477
+ );
478
+ const steps = runRepairSteps({
479
+ appPath,
480
+ configPath,
481
+ variables,
482
+ systemFilePath,
483
+ systemParsed,
484
+ systemKey,
485
+ systemFiles,
486
+ datasourceFiles,
487
+ dryRun,
488
+ changes,
489
+ auth: authOption
490
+ });
491
+
492
+ const opts = { expose: Boolean(options.expose), sync: Boolean(options.sync), test: Boolean(options.test) };
493
+ const datasourceRepairUpdated = runDatasourceRepairs(appPath, datasourceFiles, opts, dryRun, changes);
494
+ const rbacMergeUpdated = options.rbac
495
+ ? mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, dryRun, changes)
496
+ : false;
497
+ const anyUpdated = keysNormalized || steps.updated || datasourceRepairUpdated || rbacMergeUpdated;
498
+ const manifestRegenerated = (anyUpdated && !dryRun)
499
+ ? await persistChangesAndRegenerate(configPath, variables, appName, appPath, changes, originalYamlContent)
500
+ : false;
501
+
502
+ return {
503
+ updated: anyUpdated,
504
+ changes,
505
+ systemFiles,
506
+ datasourceFiles,
507
+ appKeyFixed: steps.appKeyFixed,
508
+ datasourceKeysFixed: steps.datasourceKeysFixed,
509
+ rbacFileCreated: steps.rbacFileCreated,
510
+ envTemplateRepaired: steps.envTemplateRepaired,
511
+ manifestRegenerated
512
+ };
513
+ }
514
+
515
+ module.exports = {
516
+ repairExternalIntegration,
517
+ discoverIntegrationFiles
518
+ };
@@ -60,6 +60,12 @@ async function handleSecretsSet(key, value, options) {
60
60
  throw new Error('Secret key is required and must be a string');
61
61
  }
62
62
 
63
+ if (key.startsWith('kv://')) {
64
+ throw new Error(
65
+ 'Secret key must not start with kv://. Use the key path without the prefix (e.g. my-app/clientSecret or hubspot/apiKey).'
66
+ );
67
+ }
68
+
63
69
  if (value === undefined || value === null || value === '') {
64
70
  throw new Error('Secret value is required');
65
71
  }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Run E2E tests for all datasources of an external system.
3
+ *
4
+ * @fileoverview test-e2e <external system> – run E2E for every datasource
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const chalk = require('chalk');
14
+ const logger = require('../utils/logger');
15
+ const { getIntegrationPath } = require('../utils/paths');
16
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
17
+ const { loadConfigFile } = require('../utils/config-format');
18
+ const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
19
+ const { runDatasourceTestE2E } = require('../datasource/test-e2e');
20
+
21
+ /**
22
+ * Derives datasource key from filename when file has no key (same logic as repair).
23
+ * @param {string} fileName - Datasource file name
24
+ * @param {string} systemKey - System key
25
+ * @returns {string}
26
+ */
27
+ function deriveDatasourceKeyFromFileName(fileName, systemKey) {
28
+ const base = path.basename(fileName, path.extname(fileName));
29
+ if (/^datasource-/.test(base)) {
30
+ const suffix = base.slice('datasource-'.length);
31
+ return systemKey && typeof systemKey === 'string' ? `${systemKey}-${suffix}` : base;
32
+ }
33
+ return base.replace(/-datasource-/, '-');
34
+ }
35
+
36
+ /* eslint-disable max-statements -- Key resolution from system or files */
37
+ /**
38
+ * Resolves the list of datasource keys for an external system (from system file or discovered files).
39
+ * @param {string} appPath - Integration app path
40
+ * @param {string} configPath - Application config path
41
+ * @param {Object} variables - Loaded application variables (externalIntegration.dataSources = filenames)
42
+ * @param {string} systemKey - System key from system file
43
+ * @param {Object} systemParsed - Parsed system config (may have dataSources array of keys)
44
+ * @param {string[]} datasourceFiles - Discovered datasource filenames
45
+ * @returns {string[]} Sorted list of datasource keys
46
+ */
47
+ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemParsed, datasourceFiles) {
48
+ const fromSystem = Array.isArray(systemParsed.dataSources) && systemParsed.dataSources.length > 0
49
+ ? systemParsed.dataSources
50
+ : null;
51
+ const keys = [];
52
+ const seen = new Set();
53
+ if (fromSystem) {
54
+ fromSystem.forEach(k => {
55
+ if (k && typeof k === 'string' && !seen.has(k)) {
56
+ keys.push(k.trim());
57
+ seen.add(k.trim());
58
+ }
59
+ });
60
+ keys.sort();
61
+ return keys;
62
+ }
63
+ for (const fileName of datasourceFiles) {
64
+ const filePath = path.join(appPath, fileName);
65
+ if (!fs.existsSync(filePath)) continue;
66
+ try {
67
+ const parsed = loadConfigFile(filePath);
68
+ const key = parsed && typeof parsed.key === 'string' && parsed.key.trim()
69
+ ? parsed.key.trim()
70
+ : deriveDatasourceKeyFromFileName(fileName, systemKey);
71
+ if (key && !seen.has(key)) {
72
+ keys.push(key);
73
+ seen.add(key);
74
+ }
75
+ } catch {
76
+ const key = deriveDatasourceKeyFromFileName(fileName, systemKey);
77
+ if (key && !seen.has(key)) {
78
+ keys.push(key);
79
+ seen.add(key);
80
+ }
81
+ }
82
+ }
83
+ keys.sort();
84
+ return keys;
85
+ }
86
+
87
+ /* eslint-disable max-lines-per-function, max-statements -- Load context, then loop over keys */
88
+ /**
89
+ * Runs E2E for all datasources of an external system. Uses each datasource's payloadTemplate (no extra params required).
90
+ *
91
+ * @async
92
+ * @param {string} externalSystem - System key (e.g. hubspot-demo)
93
+ * @param {Object} options - Options passed to each runDatasourceTestE2E
94
+ * @param {string} [options.env] - Environment (dev, tst, pro)
95
+ * @param {boolean} [options.debug] - Include debug, write log
96
+ * @param {boolean} [options.verbose] - Verbose output
97
+ * @param {boolean} [options.async] - If false, sync mode (default true)
98
+ * @returns {Promise<{ success: boolean, results: Array<{ key: string, success: boolean, error?: string }> }>}
99
+ */
100
+ async function runTestE2EForExternalSystem(externalSystem, options = {}) {
101
+ if (!externalSystem || typeof externalSystem !== 'string') {
102
+ throw new Error('External system name is required');
103
+ }
104
+ const appPath = getIntegrationPath(externalSystem);
105
+ if (!fs.existsSync(appPath)) {
106
+ throw new Error(`Integration path not found: ${appPath}`);
107
+ }
108
+ const configPath = resolveApplicationConfigPath(appPath);
109
+ let variables = {};
110
+ if (fs.existsSync(configPath)) {
111
+ variables = loadConfigFile(configPath);
112
+ }
113
+ const { systemFiles, datasourceFiles: discovered } = discoverIntegrationFiles(appPath);
114
+ if (systemFiles.length === 0) {
115
+ throw new Error(`No system file found in ${appPath}. Expected *-system.yaml or *-system.json`);
116
+ }
117
+ const datasourceFiles = buildEffectiveDatasourceFiles(
118
+ appPath,
119
+ discovered,
120
+ variables.externalIntegration?.dataSources
121
+ );
122
+ const systemPath = path.join(appPath, systemFiles[0]);
123
+ const systemParsed = loadConfigFile(systemPath);
124
+ const systemKey = systemParsed.key ||
125
+ path.basename(systemFiles[0], path.extname(systemFiles[0])).replace(/-system$/, '');
126
+
127
+ const keys = getDatasourceKeys(
128
+ appPath,
129
+ configPath,
130
+ variables,
131
+ systemKey,
132
+ systemParsed,
133
+ datasourceFiles
134
+ );
135
+ if (keys.length === 0) {
136
+ logger.log(chalk.yellow(`No datasources found for ${externalSystem}. Add datasource files and run aifabrix repair.`));
137
+ return { success: true, results: [] };
138
+ }
139
+
140
+ const results = [];
141
+ const opts = {
142
+ app: externalSystem,
143
+ environment: options.env,
144
+ debug: options.debug,
145
+ verbose: options.verbose,
146
+ async: options.async !== false
147
+ };
148
+ for (const key of keys) {
149
+ try {
150
+ const data = await runDatasourceTestE2E(key, opts);
151
+ const steps = data.steps || data.completedActions || [];
152
+ const failed = data.success === false || steps.some(s => s.success === false || s.error);
153
+ results.push({ key, success: !failed });
154
+ } catch (err) {
155
+ results.push({ key, success: false, error: err.message });
156
+ }
157
+ }
158
+ const success = results.every(r => r.success);
159
+ return { success, results };
160
+ }
161
+
162
+ module.exports = {
163
+ runTestE2EForExternalSystem,
164
+ getDatasourceKeys
165
+ };