@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
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const yaml = require('js-yaml');
|
|
17
|
+
const YAML = require('yaml');
|
|
17
18
|
|
|
18
19
|
const YAML_EXTENSIONS = ['.yaml', '.yml'];
|
|
19
20
|
const JSON_EXTENSIONS = ['.json'];
|
|
@@ -144,11 +145,46 @@ function writeConfigFile(filePath, object, format) {
|
|
|
144
145
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Writes application config YAML by updating only repaired keys in the original content,
|
|
150
|
+
* so comments and formatting on other keys are preserved. Use for repair flows that
|
|
151
|
+
* only change externalIntegration and/or app.key.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} filePath - Absolute path to the YAML file
|
|
154
|
+
* @param {string} originalContent - Original file content (with comments)
|
|
155
|
+
* @param {Object} repairedVariables - Repaired config object; only externalIntegration and app are written
|
|
156
|
+
* @throws {Error} If parsing or write fails
|
|
157
|
+
*/
|
|
158
|
+
function writeYamlPreservingComments(filePath, originalContent, repairedVariables) {
|
|
159
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
160
|
+
throw new Error('writeYamlPreservingComments requires a non-empty file path');
|
|
161
|
+
}
|
|
162
|
+
if (typeof originalContent !== 'string') {
|
|
163
|
+
throw new Error('writeYamlPreservingComments requires original content string');
|
|
164
|
+
}
|
|
165
|
+
const doc = YAML.parseDocument(originalContent);
|
|
166
|
+
if (doc.errors && doc.errors.length > 0) {
|
|
167
|
+
const first = doc.errors[0];
|
|
168
|
+
throw new Error(`Invalid YAML: ${first.message}`);
|
|
169
|
+
}
|
|
170
|
+
if (!doc.contents) {
|
|
171
|
+
doc.contents = doc.createNode({});
|
|
172
|
+
}
|
|
173
|
+
if (repairedVariables.externalIntegration !== undefined) {
|
|
174
|
+
doc.set('externalIntegration', doc.createNode(repairedVariables.externalIntegration));
|
|
175
|
+
}
|
|
176
|
+
if (repairedVariables.app !== undefined) {
|
|
177
|
+
doc.set('app', doc.createNode(repairedVariables.app));
|
|
178
|
+
}
|
|
179
|
+
fs.writeFileSync(filePath, String(doc), 'utf8');
|
|
180
|
+
}
|
|
181
|
+
|
|
147
182
|
module.exports = {
|
|
148
183
|
yamlToJson,
|
|
149
184
|
jsonToYaml,
|
|
150
185
|
loadConfigFile,
|
|
151
186
|
writeConfigFile,
|
|
187
|
+
writeYamlPreservingComments,
|
|
152
188
|
isYamlPath,
|
|
153
189
|
isJsonPath
|
|
154
190
|
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves configuration section values for upload (variable → .env, keyvault → secrets)
|
|
3
|
+
* and re-templates configuration on download from env.template.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Configuration env resolution for external system upload/download
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const { getIntegrationPath } = require('./paths');
|
|
13
|
+
const { parseEnvToMap, resolveKvValue } = require('./credential-secrets-env');
|
|
14
|
+
const { loadSecrets, resolveKvReferences } = require('../core/secrets');
|
|
15
|
+
const { loadEnvTemplate } = require('./secrets-helpers');
|
|
16
|
+
const { getActualSecretsPath } = require('./secrets-path');
|
|
17
|
+
|
|
18
|
+
const VAR_PLACEHOLDER_REGEX = /\{\{([^}]+)\}\}/g;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds resolved env map and secrets for an integration app (for configuration resolution).
|
|
22
|
+
* If .env exists, parses it; otherwise resolves env.template with secrets and parses the result.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} systemKey - External system key (e.g. 'my-sharepoint')
|
|
25
|
+
* @returns {Promise<{ envMap: Object.<string, string>, secrets: Object }>} envMap for {{VAR}} substitution, secrets for kv://
|
|
26
|
+
* @throws {Error} If env.template is missing when .env is missing (only when building from template)
|
|
27
|
+
*/
|
|
28
|
+
async function buildResolvedEnvMapForIntegration(systemKey) {
|
|
29
|
+
if (!systemKey || typeof systemKey !== 'string') {
|
|
30
|
+
throw new Error('systemKey is required and must be a string');
|
|
31
|
+
}
|
|
32
|
+
const integrationPath = getIntegrationPath(systemKey);
|
|
33
|
+
const envPath = path.join(integrationPath, '.env');
|
|
34
|
+
const envTemplatePath = path.join(integrationPath, 'env.template');
|
|
35
|
+
|
|
36
|
+
let secrets = {};
|
|
37
|
+
try {
|
|
38
|
+
secrets = await loadSecrets(undefined, systemKey);
|
|
39
|
+
} catch {
|
|
40
|
+
secrets = {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let envMap = {};
|
|
44
|
+
if (fs.existsSync(envPath)) {
|
|
45
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
46
|
+
envMap = parseEnvToMap(content);
|
|
47
|
+
} else if (fs.existsSync(envTemplatePath)) {
|
|
48
|
+
const templateContent = loadEnvTemplate(envTemplatePath);
|
|
49
|
+
const secretsPaths = await getActualSecretsPath(undefined, systemKey);
|
|
50
|
+
const resolvedContent = await resolveKvReferences(
|
|
51
|
+
templateContent,
|
|
52
|
+
secrets,
|
|
53
|
+
'local',
|
|
54
|
+
secretsPaths,
|
|
55
|
+
systemKey
|
|
56
|
+
);
|
|
57
|
+
envMap = parseEnvToMap(resolvedContent);
|
|
58
|
+
}
|
|
59
|
+
return { envMap, secrets };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolves {{VAR}} in a string using envMap. Throws if any variable is missing.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} value - Value that may contain {{VAR}}
|
|
66
|
+
* @param {Object.<string, string>} envMap - Resolved env key-value map
|
|
67
|
+
* @param {string} [systemKey] - System key for error message
|
|
68
|
+
* @returns {string} Value with {{VAR}} replaced
|
|
69
|
+
* @throws {Error} If a {{VAR}} is missing from envMap
|
|
70
|
+
*/
|
|
71
|
+
function substituteVarPlaceholders(value, envMap, systemKey) {
|
|
72
|
+
const hint = systemKey ? ` Run 'aifabrix resolve ${systemKey}' or set the variable in .env.` : '';
|
|
73
|
+
return value.replace(VAR_PLACEHOLDER_REGEX, (match, varName) => {
|
|
74
|
+
const key = varName.trim();
|
|
75
|
+
if (envMap[key] === undefined || envMap[key] === null) {
|
|
76
|
+
throw new Error(`Missing configuration env var: ${key}.${hint}`);
|
|
77
|
+
}
|
|
78
|
+
return String(envMap[key]);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves configuration array values in place by location: variable → {{VAR}} from envMap;
|
|
84
|
+
* keyvault → kv:// from secrets. Does not log or expose secret values.
|
|
85
|
+
*
|
|
86
|
+
* @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
|
|
87
|
+
* @param {Object.<string, string>} envMap - Resolved env map from buildResolvedEnvMapForIntegration
|
|
88
|
+
* @param {Object} secrets - Loaded secrets for kv:// resolution
|
|
89
|
+
* @param {string} [systemKey] - System key for error messages
|
|
90
|
+
* @throws {Error} If variable env is missing or keyvault secret unresolved (message never contains secret values)
|
|
91
|
+
*/
|
|
92
|
+
function resolveConfigurationValues(configArray, envMap, secrets, systemKey) {
|
|
93
|
+
if (!Array.isArray(configArray)) return;
|
|
94
|
+
const hint = systemKey ? ` Run 'aifabrix resolve ${systemKey}' and ensure the key exists in the secrets file.` : '';
|
|
95
|
+
for (const item of configArray) {
|
|
96
|
+
if (!item || typeof item.value !== 'string') continue;
|
|
97
|
+
const location = (item.location || '').toLowerCase();
|
|
98
|
+
if (location === 'variable') {
|
|
99
|
+
if (item.value.trim().startsWith('kv://')) {
|
|
100
|
+
throw new Error(`Configuration entry '${item.name || 'unknown'}' has location 'variable' but value is kv://. Use location 'keyvault' for secrets.`);
|
|
101
|
+
}
|
|
102
|
+
item.value = substituteVarPlaceholders(item.value, envMap, systemKey);
|
|
103
|
+
} else if (location === 'keyvault') {
|
|
104
|
+
const resolved = resolveKvValue(secrets, item.value);
|
|
105
|
+
if (resolved === null || resolved === undefined) {
|
|
106
|
+
throw new Error(`Unresolved keyvault reference for configuration '${item.name || 'unknown'}'.${hint}`);
|
|
107
|
+
}
|
|
108
|
+
item.value = resolved;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns the set of variable names (keys) defined in env.template content.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} envTemplateContent - Raw env.template content
|
|
117
|
+
* @returns {Set<string>} Set of variable names
|
|
118
|
+
*/
|
|
119
|
+
function getEnvTemplateVariableNames(envTemplateContent) {
|
|
120
|
+
const names = new Set();
|
|
121
|
+
if (!envTemplateContent || typeof envTemplateContent !== 'string') return names;
|
|
122
|
+
const lines = envTemplateContent.split(/\r?\n/);
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
126
|
+
const eq = trimmed.indexOf('=');
|
|
127
|
+
if (eq > 0) {
|
|
128
|
+
const key = trimmed.substring(0, eq).trim();
|
|
129
|
+
if (key) names.add(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return names;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Re-templates configuration from env.template: for each entry with location === 'variable'
|
|
137
|
+
* whose name matches a key in env.template, sets value to {{name}}. Mutates configArray in place.
|
|
138
|
+
*
|
|
139
|
+
* @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
|
|
140
|
+
* @param {Set<string>} envTemplateVariableNames - Variable names present in env.template
|
|
141
|
+
*/
|
|
142
|
+
function retemplateConfigurationFromEnvTemplate(configArray, envTemplateVariableNames) {
|
|
143
|
+
if (!Array.isArray(configArray) || !envTemplateVariableNames || !envTemplateVariableNames.size) return;
|
|
144
|
+
for (const item of configArray) {
|
|
145
|
+
if (!item || (item.location || '').toLowerCase() !== 'variable') continue;
|
|
146
|
+
const name = item.name && String(item.name).trim();
|
|
147
|
+
if (name && envTemplateVariableNames.has(name)) {
|
|
148
|
+
item.value = `{{${name}}}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Reads env.template at integration path, re-templates the given configuration array,
|
|
155
|
+
* and returns the updated array (mutates in place). If env.template is missing, does nothing.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} systemKey - External system key
|
|
158
|
+
* @param {Array<{ name?: string, value?: string, location?: string }>} configArray - Configuration array (mutated)
|
|
159
|
+
* @returns {Promise<boolean>} True if re-templating was applied (env.template existed)
|
|
160
|
+
*/
|
|
161
|
+
async function retemplateConfigurationForDownload(systemKey, configArray) {
|
|
162
|
+
if (!systemKey || typeof systemKey !== 'string' || !Array.isArray(configArray)) return false;
|
|
163
|
+
const integrationPath = getIntegrationPath(systemKey);
|
|
164
|
+
const envTemplatePath = path.join(integrationPath, 'env.template');
|
|
165
|
+
if (!fs.existsSync(envTemplatePath)) return false;
|
|
166
|
+
const content = fs.readFileSync(envTemplatePath, 'utf8');
|
|
167
|
+
const names = getEnvTemplateVariableNames(content);
|
|
168
|
+
retemplateConfigurationFromEnvTemplate(configArray, names);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
buildResolvedEnvMapForIntegration,
|
|
174
|
+
resolveConfigurationValues,
|
|
175
|
+
getEnvTemplateVariableNames,
|
|
176
|
+
retemplateConfigurationFromEnvTemplate,
|
|
177
|
+
retemplateConfigurationForDownload,
|
|
178
|
+
substituteVarPlaceholders
|
|
179
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential display utilities – status icons and formatting for CLI output
|
|
3
|
+
* Aligns with dataplane credential status lifecycle (pending, verified, failed, expired).
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Credential status formatter with icons and chalk colors
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
/** @type {{ verified: string, pending: string, failed: string, expired: string }} */
|
|
13
|
+
const STATUS_ICONS = {
|
|
14
|
+
verified: ' ✓',
|
|
15
|
+
pending: ' ○',
|
|
16
|
+
failed: ' ✗',
|
|
17
|
+
expired: ' ⊘'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** @type {{ verified: string, pending: string, failed: string, expired: string }} */
|
|
21
|
+
const STATUS_LABELS = {
|
|
22
|
+
verified: 'Valid',
|
|
23
|
+
pending: 'Not tested',
|
|
24
|
+
failed: 'Connection failed',
|
|
25
|
+
expired: 'Token expired'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Chalk color functions per status
|
|
30
|
+
* @type {{ verified: Function, pending: Function, failed: Function, expired: Function }}
|
|
31
|
+
*/
|
|
32
|
+
const STATUS_CHALK = {
|
|
33
|
+
verified: chalk.green,
|
|
34
|
+
pending: chalk.gray,
|
|
35
|
+
failed: chalk.red,
|
|
36
|
+
expired: chalk.yellow
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const VALID_STATUSES = ['verified', 'pending', 'failed', 'expired'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format credential status for display (icon + optional label)
|
|
43
|
+
* @param {string} [status] - Credential status (pending | verified | failed | expired)
|
|
44
|
+
* @returns {{ icon: string, color: Function, label: string } | null} Status info or null when missing/invalid
|
|
45
|
+
*/
|
|
46
|
+
function formatCredentialStatus(status) {
|
|
47
|
+
if (!status || typeof status !== 'string') return null;
|
|
48
|
+
const s = status.toLowerCase();
|
|
49
|
+
if (!VALID_STATUSES.includes(s)) return null;
|
|
50
|
+
return {
|
|
51
|
+
icon: STATUS_ICONS[s],
|
|
52
|
+
color: STATUS_CHALK[s],
|
|
53
|
+
label: STATUS_LABELS[s]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Format credential with status for CLI display
|
|
59
|
+
* @param {Object} credential - Credential object from API
|
|
60
|
+
* @param {string} [credential.key]
|
|
61
|
+
* @param {string} [credential.id]
|
|
62
|
+
* @param {string} [credential.credentialKey]
|
|
63
|
+
* @param {string} [credential.displayName]
|
|
64
|
+
* @param {string} [credential.name]
|
|
65
|
+
* @param {string} [credential.status]
|
|
66
|
+
* @returns {{ key: string, name: string, statusFormatted: string, statusLabel: string }}
|
|
67
|
+
*/
|
|
68
|
+
function formatCredentialWithStatus(credential) {
|
|
69
|
+
const key = credential?.key ?? credential?.id ?? credential?.credentialKey ?? '-';
|
|
70
|
+
const name = credential?.displayName ?? credential?.name ?? key;
|
|
71
|
+
const statusInfo = formatCredentialStatus(credential?.status);
|
|
72
|
+
const statusFormatted = statusInfo ? statusInfo.color(statusInfo.icon) : '';
|
|
73
|
+
const statusLabel = statusInfo ? ` (${statusInfo.label})` : '';
|
|
74
|
+
return { key, name, statusFormatted, statusLabel };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
STATUS_ICONS,
|
|
79
|
+
STATUS_LABELS,
|
|
80
|
+
STATUS_CHALK,
|
|
81
|
+
formatCredentialStatus,
|
|
82
|
+
formatCredentialWithStatus
|
|
83
|
+
};
|
|
@@ -16,24 +16,107 @@ const KV_PREFIX = 'KV_';
|
|
|
16
16
|
const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Converts
|
|
20
|
-
* @param {string}
|
|
21
|
-
* @returns {string
|
|
19
|
+
* Converts systemKey to KV_* prefix (e.g. hubspot -> HUBSPOT, my-hubspot -> MY_HUBSPOT).
|
|
20
|
+
* @param {string} systemKey - System key
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function systemKeyToKvPrefix(systemKey) {
|
|
24
|
+
if (!systemKey || typeof systemKey !== 'string') return '';
|
|
25
|
+
return systemKey.replace(/-/g, '_').toUpperCase();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps authentication security key (camelCase) to env VAR (UPPERCASE, no underscores).
|
|
30
|
+
* Used for canonical KV_<APPKEY>_<VAR> names (e.g. clientId → CLIENTID).
|
|
31
|
+
* @param {string} securityKey - Security key (e.g. 'clientId', 'clientSecret')
|
|
32
|
+
* @returns {string}
|
|
22
33
|
*/
|
|
23
|
-
function
|
|
24
|
-
if (!
|
|
25
|
-
|
|
34
|
+
function securityKeyToVar(securityKey) {
|
|
35
|
+
if (!securityKey || typeof securityKey !== 'string') return '';
|
|
36
|
+
return securityKey.replace(/_/g, '').toUpperCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Known single-segment variable suffixes (uppercase) for inferring var vs namespace in env keys */
|
|
40
|
+
const VAR_SUFFIXES = new Set(['ID', 'SECRET', 'KEY', 'TOKEN', 'URL', 'USERNAME', 'PASSWORD']);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Converts var segment(s) from env key to path-style camelCase (e.g. CLIENT_ID → clientId, CLIENTID → clientId).
|
|
44
|
+
* @param {string[]} varSegments - One or two segments (e.g. ['CLIENT', 'ID'], ['CLIENTID'])
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function varSegmentsToCamelCase(varSegments) {
|
|
48
|
+
if (!varSegments || varSegments.length === 0) return '';
|
|
49
|
+
if (varSegments.length === 1) {
|
|
50
|
+
const s = varSegments[0].toLowerCase();
|
|
51
|
+
if (s.endsWith('id') && s.length > 2) return s.slice(0, -2) + 'Id';
|
|
52
|
+
if (s.endsWith('secret') && s.length > 6) return s.slice(0, -6) + 'Secret';
|
|
53
|
+
if (s.endsWith('key') && s.length > 3) return s.slice(0, -3) + 'Key';
|
|
54
|
+
if (s.endsWith('token') && s.length > 5) return s.slice(0, -5) + 'Token';
|
|
55
|
+
if (s.endsWith('url') && s.length > 3) return s.slice(0, -3) + 'Url';
|
|
56
|
+
return s;
|
|
26
57
|
}
|
|
27
|
-
|
|
58
|
+
return varSegments.map((seg, i) => {
|
|
59
|
+
const lower = seg.toLowerCase();
|
|
60
|
+
return i === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
61
|
+
}).join('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Builds kv path from segments when systemKey is provided (path = kv://systemKey/variable).
|
|
66
|
+
* @param {string[]} segments - Parsed segments after KV_ prefix
|
|
67
|
+
* @param {string} systemKey - System key (e.g. 'microsoft-teams')
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
function kvPathWithSystemKey(segments, systemKey) {
|
|
71
|
+
const prefixInKey = systemKey.replace(/-/g, '_').toUpperCase();
|
|
72
|
+
const prefixSegs = prefixInKey.split('_').filter(Boolean);
|
|
73
|
+
if (segments.length <= prefixSegs.length) return null;
|
|
74
|
+
const prefixMatch = prefixSegs.every((p, i) => segments[i] === p);
|
|
75
|
+
if (!prefixMatch) return null;
|
|
76
|
+
const varSegments = segments.slice(prefixSegs.length);
|
|
77
|
+
const pathVar = varSegmentsToCamelCase(varSegments);
|
|
78
|
+
return pathVar ? `kv://${systemKey}/${pathVar}` : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Builds kv path from segments when systemKey is not provided (infers namespace and variable).
|
|
83
|
+
* @param {string[]} segments - Parsed segments after KV_ prefix
|
|
84
|
+
* @returns {string|null}
|
|
85
|
+
*/
|
|
86
|
+
function kvPathInferred(segments) {
|
|
87
|
+
if (segments.length === 1) return `kv://${segments[0].toLowerCase()}`;
|
|
88
|
+
const varSegmentCount = (segments.length >= 2 && VAR_SUFFIXES.has(segments[segments.length - 1])) ? 2 : 1;
|
|
89
|
+
const namespace = segments.slice(0, -varSegmentCount).map(s => s.toLowerCase()).join('-');
|
|
90
|
+
const varSegments = segments.slice(-varSegmentCount);
|
|
91
|
+
const pathVar = varSegmentsToCamelCase(varSegments);
|
|
92
|
+
return (namespace && pathVar) ? `kv://${namespace}/${pathVar}` : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Converts KV_* env key to kv:// path in format kv://<system-key>/<variable>.
|
|
97
|
+
* System-key uses hyphens (e.g. microsoft-teams); variable is camelCase (e.g. clientId).
|
|
98
|
+
* When systemKey is provided, uses it as the path namespace; otherwise infers from segments.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} envKey - Env var name (e.g. KV_MICROSOFT_TEAMS_CLIENT_ID)
|
|
101
|
+
* @param {string} [systemKey] - Optional system key (e.g. 'microsoft-teams'); when provided, path is kv://systemKey/variable
|
|
102
|
+
* @returns {string|null} kv:// path or null if invalid
|
|
103
|
+
*/
|
|
104
|
+
function kvEnvKeyToPath(envKey, systemKey) {
|
|
105
|
+
if (!envKey || typeof envKey !== 'string' || !envKey.toUpperCase().startsWith(KV_PREFIX)) return null;
|
|
106
|
+
const rest = envKey.slice(KV_PREFIX.length).trim();
|
|
28
107
|
if (!rest) return null;
|
|
29
108
|
const segments = rest.split('_').filter(Boolean);
|
|
30
|
-
|
|
31
|
-
|
|
109
|
+
if (segments.length === 0) return null;
|
|
110
|
+
|
|
111
|
+
const hasSystemKey = typeof systemKey === 'string' && systemKey.length > 0;
|
|
112
|
+
if (hasSystemKey) return kvPathWithSystemKey(segments, systemKey);
|
|
113
|
+
return kvPathInferred(segments);
|
|
32
114
|
}
|
|
33
115
|
|
|
34
116
|
/**
|
|
35
117
|
* Collects KV_* entries from env map as secret items (key = kv path, value = raw).
|
|
36
|
-
*
|
|
118
|
+
* When value is a kv:// path, uses it as the key so it matches payload paths (e.g. kv://microsoft-teams/clientId).
|
|
119
|
+
* Otherwise derives path from env key via kvEnvKeyToPath(envKey).
|
|
37
120
|
*
|
|
38
121
|
* @param {Object.<string, string>} envMap - Key-value map from .env
|
|
39
122
|
* @returns {Array<{ key: string, value: string }>} Items (key = kv://..., value = raw)
|
|
@@ -46,7 +129,11 @@ function collectKvEnvVarsAsSecretItems(envMap) {
|
|
|
46
129
|
for (const [envKey, rawValue] of Object.entries(envMap)) {
|
|
47
130
|
const value = typeof rawValue === 'string' ? rawValue.trim() : '';
|
|
48
131
|
if (value === '') continue;
|
|
49
|
-
|
|
132
|
+
let kvPath = null;
|
|
133
|
+
if (value.startsWith('kv://') && isValidKvPath(value)) {
|
|
134
|
+
kvPath = value;
|
|
135
|
+
}
|
|
136
|
+
if (!kvPath) kvPath = kvEnvKeyToPath(envKey);
|
|
50
137
|
if (!kvPath) continue;
|
|
51
138
|
items.push({ key: kvPath, value });
|
|
52
139
|
}
|
|
@@ -70,10 +157,7 @@ function resolveKvValue(secrets, value) {
|
|
|
70
157
|
const pathMatch = trimmed.match(/^kv:\/\/([a-zA-Z0-9_\-/]+)$/);
|
|
71
158
|
if (!pathMatch) return null;
|
|
72
159
|
const pathKey = pathMatch[1];
|
|
73
|
-
|
|
74
|
-
if (resolved === undefined && pathKey.includes('/')) {
|
|
75
|
-
resolved = secrets[pathKey.replace(/\//g, '-')];
|
|
76
|
-
}
|
|
160
|
+
const resolved = secrets[pathKey];
|
|
77
161
|
if (resolved === undefined) return null;
|
|
78
162
|
return typeof resolved === 'string' ? resolved : String(resolved);
|
|
79
163
|
}
|
|
@@ -156,10 +240,7 @@ function buildItemsFromPayload(payload, secrets, itemsByKey) {
|
|
|
156
240
|
for (const ref of refs) {
|
|
157
241
|
if (existingKeys.has(ref)) continue;
|
|
158
242
|
const pathKey = ref.replace(/^kv:\/\//, '');
|
|
159
|
-
|
|
160
|
-
if (resolved === undefined && pathKey.includes('/')) {
|
|
161
|
-
resolved = secrets[pathKey.replace(/\//g, '-')];
|
|
162
|
-
}
|
|
243
|
+
const resolved = secrets[pathKey];
|
|
163
244
|
if (resolved !== null && resolved !== undefined && isValidKvPath(ref)) {
|
|
164
245
|
itemsByKey.set(ref, typeof resolved === 'string' ? resolved : String(resolved));
|
|
165
246
|
}
|
|
@@ -188,12 +269,14 @@ function storedCountFromResponse(res, fallback) {
|
|
|
188
269
|
async function sendCredentialSecrets(dataplaneUrl, authConfig, items) {
|
|
189
270
|
try {
|
|
190
271
|
const res = await storeCredentialSecrets(dataplaneUrl, authConfig, items);
|
|
191
|
-
|
|
272
|
+
const failed = res && (res.success === false || res.data?.success === false);
|
|
273
|
+
if (failed) {
|
|
192
274
|
const status = res.status ?? res.statusCode;
|
|
193
275
|
if (status === 403 || status === 401) {
|
|
194
276
|
return { pushed: 0, warning: 'Could not push credential secrets (permission denied or unauthenticated). Ensure dataplane role has credential:create if you use KV_* in .env.' };
|
|
195
277
|
}
|
|
196
|
-
|
|
278
|
+
const errMsg = res.formattedError || res.data?.formattedError || res.error || res.data?.error || 'Failed to push credential secrets to dataplane.';
|
|
279
|
+
return { pushed: 0, warning: errMsg };
|
|
197
280
|
}
|
|
198
281
|
return { pushed: storedCountFromResponse(res, items.length) };
|
|
199
282
|
} catch (err) {
|
|
@@ -212,7 +295,7 @@ async function sendCredentialSecrets(dataplaneUrl, authConfig, items) {
|
|
|
212
295
|
* @param {string} [options.envFilePath] - Path to .env (integration/<systemKey>/.env)
|
|
213
296
|
* @param {string} [options.appName] - App/system name for loadSecrets context
|
|
214
297
|
* @param {Object} [options.payload] - Upload payload { application, dataSources } for kv scan
|
|
215
|
-
* @returns {Promise<{ pushed: number, warning?: string }>} Count pushed
|
|
298
|
+
* @returns {Promise<{ pushed: number, keys?: string[], skipped?: boolean, warning?: string }>} Count pushed, keys (on success), skipped (when nothing to push), optional warning
|
|
216
299
|
*/
|
|
217
300
|
async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
|
|
218
301
|
const { envFilePath, appName, payload } = options;
|
|
@@ -230,8 +313,12 @@ async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
|
|
|
230
313
|
.filter(([k]) => isValidKvPath(k))
|
|
231
314
|
.map(([key, value]) => ({ key, value }));
|
|
232
315
|
|
|
233
|
-
if (items.length === 0) return { pushed: 0 };
|
|
234
|
-
|
|
316
|
+
if (items.length === 0) return { pushed: 0, skipped: true };
|
|
317
|
+
const sendResult = await sendCredentialSecrets(dataplaneUrl, authConfig, items);
|
|
318
|
+
if (sendResult.pushed > 0) {
|
|
319
|
+
sendResult.keys = items.map(i => i.key.replace(/^kv:\/\//, ''));
|
|
320
|
+
}
|
|
321
|
+
return sendResult;
|
|
235
322
|
}
|
|
236
323
|
|
|
237
324
|
/**
|
|
@@ -262,6 +349,9 @@ module.exports = {
|
|
|
262
349
|
collectKvRefsFromPayload,
|
|
263
350
|
pushCredentialSecrets,
|
|
264
351
|
kvEnvKeyToPath,
|
|
352
|
+
systemKeyToKvPrefix,
|
|
353
|
+
securityKeyToVar,
|
|
265
354
|
isValidKvPath,
|
|
266
|
-
resolveKvValue
|
|
355
|
+
resolveKvValue,
|
|
356
|
+
parseEnvToMap
|
|
267
357
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared warning message for Dataplane pipeline API usage (upload / validate / publish).
|
|
3
|
+
* Used by upload command and datasource upload so users know configuration is sent to Dataplane.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Dataplane pipeline usage warning
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('./logger');
|
|
12
|
+
|
|
13
|
+
/** Message shown when CLI is about to call Dataplane pipeline upload or publish APIs. */
|
|
14
|
+
const DATAPLANE_PIPELINE_WARNING =
|
|
15
|
+
'Configuration will be sent to the Dataplane pipeline API. Ensure you are targeting the correct environment and have the required permissions.';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Log the Dataplane pipeline warning (yellow) to the console.
|
|
19
|
+
* Call before uploadApplicationViaPipeline or publishDatasourceViaPipeline.
|
|
20
|
+
*/
|
|
21
|
+
function logDataplanePipelineWarning() {
|
|
22
|
+
logger.log(chalk.yellow(`⚠ ${DATAPLANE_PIPELINE_WARNING}`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
DATAPLANE_PIPELINE_WARNING,
|
|
27
|
+
logDataplanePipelineWarning
|
|
28
|
+
};
|
|
@@ -34,7 +34,7 @@ function processSuccessfulValidation(responseData) {
|
|
|
34
34
|
function processValidationFailure(responseData) {
|
|
35
35
|
const errorMessage = responseData.errors && responseData.errors.length > 0
|
|
36
36
|
? `Validation failed: ${responseData.errors.join(', ')}`
|
|
37
|
-
: 'Validation failed: Invalid configuration';
|
|
37
|
+
: (responseData.error || responseData.formattedError || 'Validation failed: Invalid configuration');
|
|
38
38
|
const error = new Error(errorMessage);
|
|
39
39
|
error.status = 400;
|
|
40
40
|
error.data = responseData;
|
|
@@ -78,13 +78,13 @@ function handleValidationResponse(response) {
|
|
|
78
78
|
if (responseData.valid === true) {
|
|
79
79
|
return processSuccessfulValidation(responseData);
|
|
80
80
|
}
|
|
81
|
-
// Handle validation failure (valid: false)
|
|
82
|
-
if (responseData.valid === false) {
|
|
81
|
+
// Handle validation failure (valid: false or success: false in body)
|
|
82
|
+
if (responseData.valid === false || responseData.success === false) {
|
|
83
83
|
processValidationFailure(responseData);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
// Handle validation errors (non-success responses)
|
|
87
|
+
// Handle validation errors (non-success HTTP responses)
|
|
88
88
|
if (!response.success) {
|
|
89
89
|
processValidationError(response);
|
|
90
90
|
}
|