@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.
- package/.cursor/rules/docs-rules.mdc +30 -0
- package/README.md +2 -2
- package/integration/hubspot/README.md +11 -5
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/jest.config.manual.js +2 -1
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +36 -2
- package/lib/app/config.js +23 -11
- package/lib/app/index.js +5 -3
- package/lib/app/prompts.js +46 -31
- package/lib/app/readme.js +11 -4
- package/lib/app/run-env-compose.js +64 -1
- package/lib/app/run-helpers.js +1 -1
- package/lib/app/show-display.js +1 -1
- package/lib/cli/setup-app.js +45 -14
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +27 -0
- package/lib/cli/setup-environment.js +12 -4
- package/lib/cli/setup-external-system.js +19 -4
- package/lib/cli/setup-infra.js +54 -14
- package/lib/cli/setup-utility.js +117 -21
- package/lib/commands/auth-config.js +22 -12
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-init.js +39 -1
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +518 -0
- package/lib/commands/secrets-set.js +6 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +90 -6
- package/lib/commands/upload.js +71 -40
- package/lib/commands/wizard-core-helpers.js +230 -5
- package/lib/commands/wizard-core.js +68 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +49 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +93 -64
- package/lib/core/config.js +7 -1
- package/lib/core/secrets.js +33 -12
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/deployment/deployer.js +7 -5
- package/lib/external-system/download-helpers.js +3 -1
- package/lib/external-system/download.js +182 -204
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +51 -18
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +4 -2
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +4 -1
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +326 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +91 -0
- package/lib/generator/wizard.js +180 -179
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/index.js +11 -3
- package/lib/infrastructure/services.js +22 -11
- package/lib/schema/application-schema.json +8 -5
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +82 -6
- package/lib/schema/wizard-config.schema.json +23 -1
- package/lib/utils/api.js +38 -10
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/compose-generator.js +1 -1
- package/lib/utils/compose-handlebars-helpers.js +11 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +115 -25
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/env-copy.js +23 -3
- package/lib/utils/error-formatters/http-status-errors.js +0 -1
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +89 -30
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +1 -0
- package/lib/utils/infra-status.js +50 -44
- package/lib/utils/local-secrets.js +5 -5
- package/lib/utils/paths.js +85 -4
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +20 -0
- package/lib/utils/secrets-helpers.js +75 -89
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager.js +24 -32
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +7 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +7 -2
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +5 -5
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/README.md.hbs +75 -22
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +4 -4
- package/templates/typescript/docker-compose.hbs +4 -4
- 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
|
+
};
|