@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,348 @@
1
+ /**
2
+ * Helpers for repairing env.template from system config (KV_* names, path-style kv://).
3
+ * @fileoverview Repair env.template to match system file
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const { systemKeyToKvPrefix, kvEnvKeyToPath, securityKeyToVar } = require('../utils/credential-secrets-env');
13
+ const { extractEnvTemplate } = require('../generator/split');
14
+
15
+ /**
16
+ * Normalizes a keyvault config entry to canonical KV_* name and path-style value.
17
+ * Path format: kv://<system-key>/<variable> (e.g. kv://microsoft-teams/clientId).
18
+ * @param {Object} entry - Config entry with name, value, location
19
+ * @param {string} prefix - KV prefix (e.g. MICROSOFT_TEAMS)
20
+ * @param {string} systemKey - System key (e.g. microsoft-teams) for path namespace
21
+ * @returns {{ name: string, value: string }|null}
22
+ */
23
+ function normalizeKeyvaultEntry(entry, prefix, systemKey) {
24
+ const afterPrefix = entry.name.startsWith(`KV_${prefix}_`)
25
+ ? entry.name.slice(`KV_${prefix}_`.length)
26
+ : entry.name.replace(/^KV_[A-Z0-9]+_/, '');
27
+ const normalizedVar = afterPrefix.replace(/_/g, '').toUpperCase();
28
+ const normalizedName = `KV_${prefix}_${normalizedVar}`;
29
+ const pathVal = kvEnvKeyToPath(normalizedName, systemKey);
30
+ if (!pathVal) return null;
31
+ return {
32
+ name: normalizedName,
33
+ value: pathVal.replace(/^kv:\/\//, ''),
34
+ location: 'keyvault'
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Adds effective config entries from system configuration array.
40
+ * @param {Array} effective - Mutable result array
41
+ * @param {Array} config - System configuration array
42
+ * @param {string} prefix - KV prefix
43
+ * @param {Set<string>} seenNames - Mutable set of names already added
44
+ * @param {string} systemKey - System key for path format kv://systemKey/variable
45
+ */
46
+ function addFromConfiguration(effective, config, prefix, seenNames, systemKey) {
47
+ for (const entry of config) {
48
+ if (!entry || !entry.name) continue;
49
+ if (entry.location === 'keyvault') {
50
+ const normalized = normalizeKeyvaultEntry(entry, prefix, systemKey);
51
+ if (normalized && !seenNames.has(normalized.name)) {
52
+ effective.push(normalized);
53
+ seenNames.add(normalized.name);
54
+ }
55
+ } else if (!seenNames.has(entry.name)) {
56
+ effective.push({
57
+ name: entry.name,
58
+ value: String(entry.value ?? ''),
59
+ location: entry.location
60
+ });
61
+ seenNames.add(entry.name);
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Adds effective config entries from authentication.security.
68
+ * Path format: kv://<system-key>/<variable> (e.g. kv://microsoft-teams/clientId).
69
+ * @param {Array} effective - Mutable result array
70
+ * @param {Object} systemParsed - Parsed system config
71
+ * @param {string} prefix - KV prefix
72
+ * @param {Set<string>} seenNames - Mutable set of names already added
73
+ * @param {string} systemKey - System key for path namespace
74
+ */
75
+ function addFromAuthSecurity(effective, systemParsed, prefix, seenNames, systemKey) {
76
+ const security = systemParsed.authentication?.security;
77
+ if (!security || typeof security !== 'object') return;
78
+ for (const key of Object.keys(security)) {
79
+ const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
80
+ if (seenNames.has(envName)) continue;
81
+ const pathVal = kvEnvKeyToPath(envName, systemKey);
82
+ if (pathVal) {
83
+ effective.push({
84
+ name: envName,
85
+ value: pathVal.replace(/^kv:\/\//, ''),
86
+ location: 'keyvault'
87
+ });
88
+ seenNames.add(envName);
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Builds effective configuration array from system (KV_* names, path-style kv://).
95
+ * @param {Object} systemParsed - Parsed system config
96
+ * @param {string} systemKey - System key (e.g. 'hubspot')
97
+ * @returns {Array<{ name: string, value: string, location?: string }>}
98
+ */
99
+ function buildEffectiveConfiguration(systemParsed, systemKey) {
100
+ const effective = [];
101
+ const prefix = systemKeyToKvPrefix(systemKey);
102
+ if (!prefix) return effective;
103
+ const seenNames = new Set();
104
+ const config = Array.isArray(systemParsed.configuration) ? systemParsed.configuration : [];
105
+ addFromConfiguration(effective, config, prefix, seenNames, systemKey);
106
+ addFromAuthSecurity(effective, systemParsed, prefix, seenNames, systemKey);
107
+ return effective;
108
+ }
109
+
110
+ /**
111
+ * Builds map of env key -> full line (KEY=value) from effective config.
112
+ * @param {Array} effective - Effective configuration array
113
+ * @returns {Map<string, string>}
114
+ */
115
+ function buildExpectedByKey(effective) {
116
+ const expectedByKey = new Map();
117
+ const lines = extractEnvTemplate(effective).split('\n').filter(Boolean);
118
+ for (const line of lines) {
119
+ const eq = line.indexOf('=');
120
+ if (eq > 0) {
121
+ const key = line.substring(0, eq).trim();
122
+ expectedByKey.set(key, `${key}=${line.substring(eq + 1)}`);
123
+ }
124
+ }
125
+ return expectedByKey;
126
+ }
127
+
128
+ /**
129
+ * Creates env.template when missing. Returns true if created (and change pushed).
130
+ * @param {string} envPath - Path to env.template
131
+ * @param {Map<string, string>} expectedByKey - Expected key->line map
132
+ * @param {boolean} dryRun - If true, do not write
133
+ * @param {string[]} changes - Array to append to
134
+ * @returns {boolean}
135
+ */
136
+ function createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes) {
137
+ if (fs.existsSync(envPath)) return false;
138
+ const lines = Array.from(expectedByKey.values());
139
+ const content = lines.join('\n');
140
+ if (!content) return false;
141
+ if (!dryRun) {
142
+ fs.writeFileSync(envPath, content + '\n', { mode: 0o644, encoding: 'utf8' });
143
+ }
144
+ changes.push('Created env.template from system configuration');
145
+ return true;
146
+ }
147
+
148
+ /**
149
+ * Extracts env key from a commented line (e.g. "# KEY=value" or "#KEY=value"). Returns null if not key=value.
150
+ * @param {string} trimmed - Trimmed line (starts with #)
151
+ * @returns {string|null} Key part after # and before =, or null
152
+ */
153
+ function keyFromCommentedLine(trimmed) {
154
+ const afterHash = trimmed.slice(1).trim();
155
+ if (!afterHash.includes('=')) return null;
156
+ const eq = afterHash.indexOf('=');
157
+ if (eq <= 0) return null;
158
+ const key = afterHash.substring(0, eq).trim();
159
+ return key || null;
160
+ }
161
+
162
+ /**
163
+ * Processes one line: keep as-is or replace with expected. Mutates updatedLines, keysWritten, changed.
164
+ * Preserves existing key=value in env.template: if a key already has a value, that value is never overwritten.
165
+ * Treats commented key=value (e.g. # KV_X=kv://...) as "already present" so repair does not add it again.
166
+ * Only missing keys from expectedByKey are added at the end.
167
+ * @param {string} line - Current line
168
+ * @param {Map<string, string>} expectedByKey - Expected key->line map
169
+ * @param {string[]} updatedLines - Mutable output lines
170
+ * @param {Set<string>} keysWritten - Mutable set of keys written
171
+ * @param {{ value: boolean }} _changedRef - Mutable changed flag (caller tracks changes)
172
+ */
173
+ function processLine(line, expectedByKey, updatedLines, keysWritten, _changedRef) {
174
+ const trimmed = line.trim();
175
+ if (!trimmed || trimmed.startsWith('#')) {
176
+ if (trimmed.startsWith('#')) {
177
+ const commentedKey = keyFromCommentedLine(trimmed);
178
+ if (commentedKey && expectedByKey.has(commentedKey)) keysWritten.add(commentedKey);
179
+ }
180
+ updatedLines.push(line);
181
+ return;
182
+ }
183
+ const eq = line.indexOf('=');
184
+ if (eq <= 0) {
185
+ updatedLines.push(line);
186
+ return;
187
+ }
188
+ const key = line.substring(0, eq).trim();
189
+ if (expectedByKey.has(key)) {
190
+ // Keep existing value; do not overwrite with expected (user may have set kv:// or custom value)
191
+ updatedLines.push(line);
192
+ keysWritten.add(key);
193
+ } else {
194
+ updatedLines.push(line);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Merges existing env.template content with expected key=value lines.
200
+ * Preserves comments, blank lines, and vars not in expectedByKey.
201
+ * @param {string} content - Current file content
202
+ * @param {Map<string, string>} expectedByKey - Expected key->line map
203
+ * @returns {{ output: string, changed: boolean }}
204
+ */
205
+ function mergeEnvTemplateContent(content, expectedByKey) {
206
+ const lines = content.split(/\r?\n/);
207
+ const updatedLines = [];
208
+ const keysWritten = new Set();
209
+ const changedRef = { value: false };
210
+
211
+ for (const line of lines) {
212
+ processLine(line, expectedByKey, updatedLines, keysWritten, changedRef);
213
+ }
214
+ for (const key of expectedByKey.keys()) {
215
+ if (!keysWritten.has(key)) {
216
+ updatedLines.push(expectedByKey.get(key));
217
+ changedRef.value = true;
218
+ }
219
+ }
220
+
221
+ const output = updatedLines.join('\n') + (updatedLines.length > 0 ? '\n' : '');
222
+ return { output, changed: changedRef.value };
223
+ }
224
+
225
+ /**
226
+ * Repairs env.template so KV_ names and path-style kv:// values match the system file.
227
+ * @param {string} appPath - Application directory path
228
+ * @param {Object} systemParsed - Parsed system config
229
+ * @param {string} systemKey - System key
230
+ * @param {boolean} dryRun - If true, do not write
231
+ * @param {string[]} changes - Array to append change descriptions to
232
+ * @returns {boolean} True if env.template was repaired or created
233
+ */
234
+ function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes) {
235
+ const effective = buildEffectiveConfiguration(systemParsed, systemKey);
236
+ const expectedByKey = buildExpectedByKey(effective);
237
+ const envPath = path.join(appPath, 'env.template');
238
+
239
+ if (createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes)) {
240
+ return true;
241
+ }
242
+ if (!fs.existsSync(envPath)) {
243
+ return false;
244
+ }
245
+
246
+ const content = fs.readFileSync(envPath, 'utf8');
247
+ const { output, changed } = mergeEnvTemplateContent(content, expectedByKey);
248
+
249
+ if (changed && !dryRun) {
250
+ fs.writeFileSync(envPath, output, { mode: 0o644, encoding: 'utf8' });
251
+ }
252
+ if (changed) {
253
+ changes.push('Repaired env.template (KV_* names and path-style kv:// values)');
254
+ return true;
255
+ }
256
+ return false;
257
+ }
258
+
259
+ /**
260
+ * Returns true if a kv value looks like legacy format (KeyVault suffix or no path segments).
261
+ * @param {string} val - Value from authentication.security or configuration
262
+ * @returns {boolean}
263
+ */
264
+ function isLegacyKvValue(val) {
265
+ if (typeof val !== 'string' || !val.trim().toLowerCase().startsWith('kv://')) return false;
266
+ const after = val.trim().slice(5); // after 'kv://'
267
+ return after.includes('KeyVault') || !after.includes('/');
268
+ }
269
+
270
+ /**
271
+ * Normalizes authentication.security values to path-style kv:// (kv://systemKey/variable).
272
+ * @param {Object} security - authentication.security object (mutated)
273
+ * @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
274
+ * @param {string} systemKey - System key for path namespace
275
+ * @param {string[]} changes - Array to append change descriptions to
276
+ * @returns {boolean} True if any change was made
277
+ */
278
+ function normalizeSecuritySection(security, prefix, systemKey, changes) {
279
+ let updated = false;
280
+ for (const key of Object.keys(security)) {
281
+ const val = security[key];
282
+ if (typeof val !== 'string' || !isLegacyKvValue(val)) continue;
283
+ const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
284
+ const pathVal = kvEnvKeyToPath(envName, systemKey);
285
+ if (pathVal) {
286
+ security[key] = pathVal;
287
+ changes.push(`authentication.security.${key}: normalized to path-style ${pathVal}`);
288
+ updated = true;
289
+ }
290
+ }
291
+ return updated;
292
+ }
293
+
294
+ /**
295
+ * Normalizes configuration array keyvault entries to canonical KV_* names and path-style values.
296
+ * @param {Object[]} config - configuration array (mutated)
297
+ * @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
298
+ * @param {string} systemKey - System key for path namespace
299
+ * @param {string[]} changes - Array to append change descriptions to
300
+ * @returns {boolean} True if any change was made
301
+ */
302
+ function normalizeConfigurationSection(config, prefix, systemKey, changes) {
303
+ let updated = false;
304
+ for (let i = 0; i < config.length; i++) {
305
+ const entry = config[i];
306
+ if (!entry || !entry.name || (entry.location !== 'keyvault' && !String(entry.name).startsWith('KV_'))) continue;
307
+ const afterPrefix = entry.name.startsWith(`KV_${prefix}_`)
308
+ ? entry.name.slice(`KV_${prefix}_`.length)
309
+ : entry.name.replace(/^KV_[A-Z0-9]+_/, '');
310
+ const normalizedVar = afterPrefix.replace(/_/g, '').toUpperCase();
311
+ const canonicalName = `KV_${prefix}_${normalizedVar}`;
312
+ const pathVal = kvEnvKeyToPath(canonicalName, systemKey);
313
+ if (!pathVal) continue;
314
+ const pathValWithoutPrefix = pathVal.replace(/^kv:\/\//, '');
315
+ const valueLegacy = typeof entry.value === 'string' && (entry.value.includes('KeyVault') || !entry.value.includes('/'));
316
+ if (entry.name !== canonicalName || (valueLegacy && entry.value !== pathValWithoutPrefix)) {
317
+ config[i] = { ...entry, name: canonicalName, value: pathValWithoutPrefix, location: 'keyvault' };
318
+ changes.push(`configuration: normalized ${entry.name} → ${canonicalName}, value → path-style`);
319
+ updated = true;
320
+ }
321
+ }
322
+ return updated;
323
+ }
324
+
325
+ /**
326
+ * Normalizes system file authentication.security and configuration keyvault entries.
327
+ * @param {Object} systemParsed - Parsed system config (mutated)
328
+ * @param {string} systemKey - System key (e.g. 'hubspot')
329
+ * @param {string[]} changes - Array to append change descriptions to
330
+ * @returns {boolean} True if any change was made
331
+ */
332
+ function normalizeSystemFileAuthAndConfig(systemParsed, systemKey, changes) {
333
+ const prefix = systemKeyToKvPrefix(systemKey);
334
+ if (!prefix) return false;
335
+ const security = systemParsed.authentication?.security;
336
+ let updated = (security && typeof security === 'object' && normalizeSecuritySection(security, prefix, systemKey, changes));
337
+ const config = systemParsed.configuration;
338
+ if (Array.isArray(config)) {
339
+ updated = normalizeConfigurationSection(config, prefix, systemKey, changes) || updated;
340
+ }
341
+ return updated;
342
+ }
343
+
344
+ module.exports = {
345
+ buildEffectiveConfiguration,
346
+ repairEnvTemplate,
347
+ normalizeSystemFileAuthAndConfig
348
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Internal helpers for repair command: file discovery and datasource list building.
3
+ *
4
+ * @fileoverview Repair discovery and datasource helpers
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
+
16
+ /** Matches *-datasource-*.(yaml|yml|json) or datasource-*.(yaml|yml|json) */
17
+ function isDatasourceFileName(name) {
18
+ if (!/\.(yaml|yml|json)$/i.test(name)) return false;
19
+ return /-datasource-.+\.(yaml|yml|json)$/i.test(name) || /^datasource-.+\.(yaml|yml|json)$/i.test(name);
20
+ }
21
+
22
+ /**
23
+ * Discovers system and datasource files in app directory
24
+ * @param {string} appPath - Application directory path
25
+ * @returns {{ systemFiles: string[], datasourceFiles: string[] }}
26
+ */
27
+ function discoverIntegrationFiles(appPath) {
28
+ if (!fs.existsSync(appPath)) {
29
+ return { systemFiles: [], datasourceFiles: [] };
30
+ }
31
+ const entries = fs.readdirSync(appPath);
32
+ const systemFiles = [];
33
+ const datasourceFiles = [];
34
+ for (const name of entries) {
35
+ if (!/^[a-z0-9_.-]+\.(yaml|yml|json)$/i.test(name)) continue;
36
+ if (/-system\.(yaml|yml|json)$/i.test(name)) {
37
+ systemFiles.push(name);
38
+ } else if (isDatasourceFileName(name)) {
39
+ datasourceFiles.push(name);
40
+ }
41
+ }
42
+ systemFiles.sort();
43
+ datasourceFiles.sort();
44
+ return { systemFiles, datasourceFiles };
45
+ }
46
+
47
+ /**
48
+ * Builds the effective datasource file list from application.yaml and discovered files.
49
+ * @param {string} appPath - Application directory path
50
+ * @param {string[]} discoveredDatasourceFiles - Filenames from discoverIntegrationFiles
51
+ * @param {string[]} [existingDataSources] - externalIntegration.dataSources from application.yaml
52
+ * @returns {string[]} Sorted, deduplicated list of datasource filenames
53
+ */
54
+ function buildEffectiveDatasourceFiles(appPath, discoveredDatasourceFiles, existingDataSources) {
55
+ const existing = Array.isArray(existingDataSources) ? existingDataSources : [];
56
+ const seen = new Set();
57
+ const result = [];
58
+ for (const name of existing) {
59
+ if (!name || typeof name !== 'string') continue;
60
+ const trimmed = name.trim();
61
+ if (!trimmed || seen.has(trimmed)) continue;
62
+ const filePath = path.join(appPath, trimmed);
63
+ if (fs.existsSync(filePath)) {
64
+ result.push(trimmed);
65
+ seen.add(trimmed);
66
+ } else {
67
+ logger.log(chalk.yellow(`⚠ Datasource file referenced in application.yaml not found: ${trimmed}`));
68
+ }
69
+ }
70
+ for (const name of discoveredDatasourceFiles) {
71
+ if (seen.has(name)) continue;
72
+ const filePath = path.join(appPath, name);
73
+ if (fs.existsSync(filePath)) {
74
+ result.push(name);
75
+ seen.add(name);
76
+ }
77
+ }
78
+ result.sort();
79
+ return result;
80
+ }
81
+
82
+ module.exports = {
83
+ discoverIntegrationFiles,
84
+ buildEffectiveDatasourceFiles
85
+ };
@@ -0,0 +1,158 @@
1
+ /**
2
+ * RBAC merge for repair: build permissions from datasources and ensure default roles.
3
+ *
4
+ * @fileoverview Repair RBAC merge from datasource resourceType/capabilities
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 yaml = require('js-yaml');
14
+ const chalk = require('chalk');
15
+ const logger = require('../utils/logger');
16
+ const { loadConfigFile } = require('../utils/config-format');
17
+
18
+ const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
19
+
20
+ /**
21
+ * Resolves capabilities from datasource (array or legacy object).
22
+ * @param {Object} parsed - Parsed datasource
23
+ * @returns {string[]}
24
+ */
25
+ function getCapabilitiesFromDatasource(parsed) {
26
+ const cap = parsed?.capabilities;
27
+ if (Array.isArray(cap)) return cap.filter(c => typeof c === 'string');
28
+ if (cap && typeof cap === 'object') {
29
+ return Object.keys(cap).filter(k => cap[k] === true);
30
+ }
31
+ return [...DEFAULT_CAPABILITIES];
32
+ }
33
+
34
+ /**
35
+ * Collects permission names (resourceType:capability) from datasource files.
36
+ * @param {string} appPath - Application path
37
+ * @param {string[]} datasourceFiles - Datasource file names
38
+ * @returns {Set<string>}
39
+ */
40
+ function collectPermissionNames(appPath, datasourceFiles) {
41
+ const permissionNames = new Set();
42
+ for (const fileName of datasourceFiles || []) {
43
+ const filePath = path.join(appPath, fileName);
44
+ if (!fs.existsSync(filePath)) continue;
45
+ try {
46
+ const parsed = loadConfigFile(filePath);
47
+ const resourceType = parsed?.resourceType || 'document';
48
+ const caps = getCapabilitiesFromDatasource(parsed);
49
+ for (const cap of caps) permissionNames.add(`${resourceType}:${cap}`);
50
+ } catch (err) {
51
+ logger.log(chalk.yellow(`⚠ Could not load ${fileName} for RBAC: ${err.message}`));
52
+ }
53
+ }
54
+ return permissionNames;
55
+ }
56
+
57
+ /**
58
+ * Loads existing rbac or creates empty structure. Uses extractRbacFromSystem when provided.
59
+ * @param {string} appPath - Application path
60
+ * @param {Object} [systemParsed] - Parsed system for extractRbacFromSystem
61
+ * @param {Function} extractRbacFromSystem - (system) => rbac or null
62
+ * @returns {{ rbac: Object, rbacPath: string, rbacYmlPath: string }}
63
+ */
64
+ function loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem) {
65
+ const rbacPath = path.join(appPath, 'rbac.yaml');
66
+ const rbacYmlPath = path.join(appPath, 'rbac.yml');
67
+ let rbac;
68
+ if (fs.existsSync(rbacPath)) {
69
+ rbac = loadConfigFile(rbacPath);
70
+ } else if (fs.existsSync(rbacYmlPath)) {
71
+ rbac = loadConfigFile(rbacYmlPath);
72
+ } else {
73
+ rbac = extractRbacFromSystem(systemParsed) || { roles: [], permissions: [] };
74
+ if (!Array.isArray(rbac.roles)) rbac.roles = [];
75
+ if (!Array.isArray(rbac.permissions)) rbac.permissions = [];
76
+ }
77
+ return { rbac, rbacPath, rbacYmlPath };
78
+ }
79
+
80
+ /**
81
+ * Adds missing permissions to rbac.permissions. Mutates rbac.
82
+ * @param {Object} rbac - RBAC object (mutated)
83
+ * @param {Set<string>} permissionNames - Desired permission names
84
+ * @param {string[]} changes - Array to append to
85
+ * @returns {boolean}
86
+ */
87
+ function addMissingPermissions(rbac, permissionNames, changes) {
88
+ const existing = new Set((rbac.permissions || []).map(p => p?.name).filter(Boolean));
89
+ let updated = false;
90
+ for (const name of permissionNames) {
91
+ if (existing.has(name)) continue;
92
+ rbac.permissions = rbac.permissions || [];
93
+ rbac.permissions.push({ name, roles: [], description: `Permission: ${name}` });
94
+ existing.add(name);
95
+ changes.push(`Added RBAC permission: ${name}`);
96
+ updated = true;
97
+ }
98
+ return updated;
99
+ }
100
+
101
+ /**
102
+ * Ensures default Admin and Reader roles if rbac.roles is empty. Mutates rbac.
103
+ * @param {Object} rbac - RBAC object (mutated)
104
+ * @param {string} systemKey - System key
105
+ * @param {string} displayName - Display name
106
+ * @param {string[]} changes - Array to append to
107
+ * @returns {boolean}
108
+ */
109
+ function ensureDefaultRoles(rbac, systemKey, displayName, changes) {
110
+ const hasRoles = Array.isArray(rbac.roles) && rbac.roles.length > 0;
111
+ if (hasRoles) return false;
112
+ const adminValue = `${systemKey}-admin`;
113
+ const readerValue = `${systemKey}-reader`;
114
+ rbac.roles = [
115
+ { name: `${displayName} Admin`, value: adminValue, description: `Full access to all ${displayName} operations`, groups: [] },
116
+ { name: `${displayName} Reader`, value: readerValue, description: `Read-only access to all ${displayName} data`, groups: [] }
117
+ ];
118
+ const listGetPerms = (rbac.permissions || [])
119
+ .filter(p => p?.name && (p.name.split(':')[1] === 'list' || p.name.split(':')[1] === 'get'))
120
+ .map(p => p.name);
121
+ for (const p of rbac.permissions || []) {
122
+ if (!p.roles) p.roles = [];
123
+ if (p.name && listGetPerms.includes(p.name) && !p.roles.includes(readerValue)) p.roles.push(readerValue);
124
+ if (!p.roles.includes(adminValue)) p.roles.push(adminValue);
125
+ }
126
+ changes.push('Added default Admin and Reader roles to rbac.yaml');
127
+ return true;
128
+ }
129
+
130
+ /**
131
+ * Merges RBAC from datasources: ensures permission per resourceType:capability, adds Admin/Reader roles if none.
132
+ * @param {string} appPath - Application path
133
+ * @param {Object} systemParsed - Parsed system (key, displayName)
134
+ * @param {string[]} datasourceFiles - Datasource file names
135
+ * @param {Function} extractRbacFromSystem - (system) => rbac or null
136
+ * @param {boolean} dryRun - If true, do not write
137
+ * @param {string[]} changes - Array to append change descriptions to
138
+ * @returns {boolean} True if rbac was updated (or would be in dry-run)
139
+ */
140
+ function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, dryRun, changes) {
141
+ const permissionNames = collectPermissionNames(appPath, datasourceFiles);
142
+ if (permissionNames.size === 0) return false;
143
+ const systemKey = systemParsed?.key || 'system';
144
+ const displayName = systemParsed?.displayName || systemKey;
145
+ const { rbac, rbacPath, rbacYmlPath } = loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem);
146
+ let updated = addMissingPermissions(rbac, permissionNames, changes);
147
+ updated = ensureDefaultRoles(rbac, systemKey, displayName, changes) || updated;
148
+ if (updated && !dryRun) {
149
+ const outPath = fs.existsSync(rbacPath) ? rbacPath : (fs.existsSync(rbacYmlPath) ? rbacYmlPath : rbacPath);
150
+ fs.writeFileSync(outPath, yaml.dump(rbac, { indent: 2, lineWidth: -1 }), { mode: 0o644, encoding: 'utf8' });
151
+ }
152
+ return updated;
153
+ }
154
+
155
+ module.exports = {
156
+ getCapabilitiesFromDatasource,
157
+ mergeRbacFromDatasources
158
+ };