@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
@@ -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 KV_* env key to kv:// path (e.g. KV_SECRETS_FOO kv://secrets/foo).
20
- * @param {string} envKey - Env var name (e.g. KV_SECRETS_CLIENT_SECRET)
21
- * @returns {string|null} kv:// path or null if invalid
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 kvEnvKeyToPath(envKey) {
24
- if (!envKey || typeof envKey !== 'string' || !envKey.toUpperCase().startsWith(KV_PREFIX)) {
25
- return null;
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
- const rest = envKey.slice(KV_PREFIX.length);
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://&lt;system-key&gt;/&lt;variable&gt;.
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
- const pathPart = segments.map(s => s.toLowerCase()).join('/');
31
- return pathPart ? `kv://${pathPart}` : null;
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
- * Does not resolve values; empty values are skipped.
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
- const kvPath = kvEnvKeyToPath(envKey);
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
- let resolved = secrets[pathKey];
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
- let resolved = secrets[pathKey];
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
- if (res && res.success === false) {
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
- return { pushed: 0, warning: res.formattedError || res.error || 'Failed to push credential secrets to dataplane.' };
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 and optional warning
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
- return sendCredentialSecrets(dataplaneUrl, authConfig, items);
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
  }