@aifabrix/builder 2.40.2 → 2.42.0

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 (198) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +7 -5
  3. package/integration/hubspot/README.md +8 -4
  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/integration/hubspot/test.js +1 -1
  16. package/jest.config.manual.js +2 -1
  17. package/lib/api/credential.api.js +40 -0
  18. package/lib/api/dev.api.js +423 -0
  19. package/lib/api/external-test.api.js +111 -0
  20. package/lib/api/index.js +42 -19
  21. package/lib/api/pipeline.api.js +66 -120
  22. package/lib/api/types/credential.types.js +23 -0
  23. package/lib/api/types/dev.types.js +140 -0
  24. package/lib/api/types/pipeline.types.js +37 -0
  25. package/lib/api/wizard-platform.api.js +61 -0
  26. package/lib/api/wizard.api.js +34 -1
  27. package/lib/app/config.js +44 -11
  28. package/lib/app/down.js +2 -1
  29. package/lib/app/index.js +12 -1
  30. package/lib/app/prompts.js +44 -29
  31. package/lib/app/push.js +36 -12
  32. package/lib/app/readme.js +9 -6
  33. package/lib/app/run-env-compose.js +264 -0
  34. package/lib/app/run-helpers.js +121 -118
  35. package/lib/app/run.js +148 -28
  36. package/lib/app/show-display.js +1 -1
  37. package/lib/app/show.js +5 -2
  38. package/lib/build/index.js +11 -3
  39. package/lib/cli/setup-app.js +172 -15
  40. package/lib/cli/setup-credential-deployment.js +31 -6
  41. package/lib/cli/setup-dev.js +206 -16
  42. package/lib/cli/setup-environment.js +16 -6
  43. package/lib/cli/setup-external-system.js +89 -24
  44. package/lib/cli/setup-infra.js +82 -15
  45. package/lib/cli/setup-secrets.js +52 -5
  46. package/lib/cli/setup-utility.js +129 -24
  47. package/lib/commands/app-install.js +172 -0
  48. package/lib/commands/app-shell.js +75 -0
  49. package/lib/commands/app-test.js +282 -0
  50. package/lib/commands/app.js +1 -1
  51. package/lib/commands/credential-env.js +162 -0
  52. package/lib/commands/credential-list.js +17 -22
  53. package/lib/commands/credential-push.js +96 -0
  54. package/lib/commands/datasource.js +77 -6
  55. package/lib/commands/dev-cli-handlers.js +141 -0
  56. package/lib/commands/dev-down.js +114 -0
  57. package/lib/commands/dev-init.js +347 -0
  58. package/lib/commands/repair-auth-config.js +99 -0
  59. package/lib/commands/repair-datasource-keys.js +208 -0
  60. package/lib/commands/repair-datasource.js +235 -0
  61. package/lib/commands/repair-env-template.js +348 -0
  62. package/lib/commands/repair-internal.js +85 -0
  63. package/lib/commands/repair-rbac.js +158 -0
  64. package/lib/commands/repair.js +507 -0
  65. package/lib/commands/secrets-list.js +118 -0
  66. package/lib/commands/secrets-remove.js +97 -0
  67. package/lib/commands/secrets-set.js +30 -17
  68. package/lib/commands/secrets-validate.js +50 -0
  69. package/lib/commands/test-e2e-external.js +165 -0
  70. package/lib/commands/up-dataplane.js +2 -2
  71. package/lib/commands/up-miso.js +0 -25
  72. package/lib/commands/upload.js +96 -40
  73. package/lib/commands/wizard-core-helpers.js +226 -4
  74. package/lib/commands/wizard-core.js +67 -29
  75. package/lib/commands/wizard-dataplane.js +1 -1
  76. package/lib/commands/wizard-entity-selection.js +43 -0
  77. package/lib/commands/wizard-headless.js +44 -5
  78. package/lib/commands/wizard-helpers.js +7 -3
  79. package/lib/commands/wizard.js +86 -64
  80. package/lib/core/admin-secrets.js +96 -0
  81. package/lib/core/config.js +7 -1
  82. package/lib/core/secrets-ensure.js +378 -0
  83. package/lib/core/secrets-env-write.js +157 -0
  84. package/lib/core/secrets.js +176 -89
  85. package/lib/datasource/deploy.js +12 -3
  86. package/lib/datasource/field-reference-validator.js +91 -0
  87. package/lib/datasource/test-e2e.js +219 -0
  88. package/lib/datasource/test-integration.js +154 -0
  89. package/lib/datasource/validate.js +21 -3
  90. package/lib/deployment/deployer.js +7 -5
  91. package/lib/deployment/environment-config.js +137 -0
  92. package/lib/deployment/environment.js +21 -98
  93. package/lib/deployment/push.js +32 -2
  94. package/lib/external-system/download.js +188 -203
  95. package/lib/external-system/generator.js +204 -56
  96. package/lib/external-system/test-auth.js +7 -3
  97. package/lib/external-system/test-execution.js +2 -1
  98. package/lib/external-system/test-system-level.js +73 -0
  99. package/lib/external-system/test.js +56 -19
  100. package/lib/generator/external-controller-manifest.js +29 -2
  101. package/lib/generator/external-schema-utils.js +1 -1
  102. package/lib/generator/external.js +10 -3
  103. package/lib/generator/index.js +177 -25
  104. package/lib/generator/split-readme.js +1 -0
  105. package/lib/generator/split-variables.js +7 -1
  106. package/lib/generator/split.js +194 -54
  107. package/lib/generator/wizard-prompts-secondary.js +294 -0
  108. package/lib/generator/wizard-prompts.js +105 -106
  109. package/lib/generator/wizard-readme.js +88 -0
  110. package/lib/generator/wizard.js +155 -158
  111. package/lib/infrastructure/compose.js +11 -1
  112. package/lib/infrastructure/helpers.js +103 -20
  113. package/lib/infrastructure/index.js +98 -12
  114. package/lib/infrastructure/services.js +88 -22
  115. package/lib/schema/application-schema.json +32 -8
  116. package/lib/schema/external-datasource.schema.json +49 -26
  117. package/lib/schema/external-system.schema.json +509 -411
  118. package/lib/schema/wizard-config.schema.json +16 -0
  119. package/lib/utils/api.js +41 -13
  120. package/lib/utils/app-register-auth.js +25 -3
  121. package/lib/utils/auth-headers.js +8 -7
  122. package/lib/utils/cli-utils.js +20 -0
  123. package/lib/utils/compose-generator.js +77 -76
  124. package/lib/utils/compose-handlebars-helpers.js +54 -0
  125. package/lib/utils/compose-vector-helper.js +18 -0
  126. package/lib/utils/config-format-preference.js +51 -0
  127. package/lib/utils/config-format.js +36 -0
  128. package/lib/utils/config-paths.js +127 -2
  129. package/lib/utils/configuration-env-resolver.js +179 -0
  130. package/lib/utils/credential-display.js +83 -0
  131. package/lib/utils/credential-secrets-env.js +357 -0
  132. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  133. package/lib/utils/deployment-validation-helpers.js +4 -4
  134. package/lib/utils/dev-ca-install.js +139 -0
  135. package/lib/utils/dev-cert-helper.js +122 -0
  136. package/lib/utils/device-code-helpers.js +224 -0
  137. package/lib/utils/device-code.js +37 -336
  138. package/lib/utils/docker-build.js +40 -8
  139. package/lib/utils/env-copy.js +103 -13
  140. package/lib/utils/env-map.js +35 -5
  141. package/lib/utils/env-template.js +6 -5
  142. package/lib/utils/error-formatters/http-status-errors.js +20 -2
  143. package/lib/utils/error-formatters/permission-errors.js +0 -1
  144. package/lib/utils/error-formatters/validation-errors.js +0 -1
  145. package/lib/utils/external-readme.js +56 -29
  146. package/lib/utils/external-system-display.js +59 -1
  147. package/lib/utils/external-system-test-helpers.js +21 -8
  148. package/lib/utils/external-system-validators.js +3 -0
  149. package/lib/utils/file-upload.js +20 -50
  150. package/lib/utils/help-builder.js +16 -2
  151. package/lib/utils/infra-status.js +80 -45
  152. package/lib/utils/local-secrets.js +7 -52
  153. package/lib/utils/mutagen-install.js +195 -0
  154. package/lib/utils/mutagen.js +146 -0
  155. package/lib/utils/paths.js +128 -37
  156. package/lib/utils/port-resolver.js +28 -16
  157. package/lib/utils/remote-dev-auth.js +38 -0
  158. package/lib/utils/remote-docker-env.js +43 -0
  159. package/lib/utils/remote-secrets-loader.js +60 -0
  160. package/lib/utils/secrets-canonical.js +93 -0
  161. package/lib/utils/secrets-generator.js +114 -6
  162. package/lib/utils/secrets-helpers.js +108 -114
  163. package/lib/utils/secrets-path.js +2 -2
  164. package/lib/utils/secrets-utils.js +52 -1
  165. package/lib/utils/secrets-validation.js +84 -0
  166. package/lib/utils/ssh-key-helper.js +116 -0
  167. package/lib/utils/test-log-writer.js +56 -0
  168. package/lib/utils/token-manager-messages.js +90 -0
  169. package/lib/utils/token-manager.js +29 -36
  170. package/lib/utils/variable-transformer.js +3 -3
  171. package/lib/validation/env-template-auth.js +157 -0
  172. package/lib/validation/env-template-kv.js +41 -0
  173. package/lib/validation/external-manifest-validator.js +25 -0
  174. package/lib/validation/external-system-auth-rules.js +86 -0
  175. package/lib/validation/validate-batch.js +149 -0
  176. package/lib/validation/validate-datasource-keys-api.js +33 -0
  177. package/lib/validation/validate-display.js +94 -16
  178. package/lib/validation/validate.js +25 -12
  179. package/lib/validation/validator.js +72 -9
  180. package/lib/validation/wizard-datasource-validation.js +50 -0
  181. package/package.json +8 -3
  182. package/scripts/install-local.js +34 -15
  183. package/templates/README.md +0 -1
  184. package/templates/applications/README.md.hbs +4 -4
  185. package/templates/applications/dataplane/application.yaml +6 -5
  186. package/templates/applications/dataplane/env.template +15 -10
  187. package/templates/applications/dataplane/rbac.yaml +2 -2
  188. package/templates/applications/keycloak/env.template +2 -0
  189. package/templates/applications/miso-controller/application.yaml +1 -0
  190. package/templates/applications/miso-controller/env.template +12 -10
  191. package/templates/external-system/README.md.hbs +65 -25
  192. package/templates/external-system/deploy.js.hbs +4 -2
  193. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  194. package/templates/external-system/external-system.json.hbs +1 -18
  195. package/templates/infra/compose.yaml.hbs +6 -0
  196. package/templates/python/docker-compose.hbs +49 -23
  197. package/templates/typescript/docker-compose.hbs +48 -22
  198. package/integration/hubspot/application.yaml +0 -37
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Collect KV_* env vars as secret store items and push to dataplane.
3
+ * Reads integration .env, resolves kv:// in values, scans payload for kv:// refs,
4
+ * and pushes plain values to POST /api/v1/credential/secret.
5
+ *
6
+ * @fileoverview Credential secrets push from .env and payload (Dataplane)
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const { loadSecrets } = require('../core/secrets');
13
+ const { storeCredentialSecrets } = require('../api/credential.api');
14
+
15
+ const KV_PREFIX = 'KV_';
16
+ const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
17
+
18
+ /**
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}
33
+ */
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;
57
+ }
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();
107
+ if (!rest) return null;
108
+ const segments = rest.split('_').filter(Boolean);
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);
114
+ }
115
+
116
+ /**
117
+ * Collects KV_* entries from env map as secret items (key = kv path, value = raw).
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).
120
+ *
121
+ * @param {Object.<string, string>} envMap - Key-value map from .env
122
+ * @returns {Array<{ key: string, value: string }>} Items (key = kv://..., value = raw)
123
+ */
124
+ function collectKvEnvVarsAsSecretItems(envMap) {
125
+ if (!envMap || typeof envMap !== 'object') {
126
+ return [];
127
+ }
128
+ const items = [];
129
+ for (const [envKey, rawValue] of Object.entries(envMap)) {
130
+ const value = typeof rawValue === 'string' ? rawValue.trim() : '';
131
+ if (value === '') continue;
132
+ let kvPath = null;
133
+ if (value.startsWith('kv://') && isValidKvPath(value)) {
134
+ kvPath = value;
135
+ }
136
+ if (!kvPath) kvPath = kvEnvKeyToPath(envKey);
137
+ if (!kvPath) continue;
138
+ items.push({ key: kvPath, value });
139
+ }
140
+ return items;
141
+ }
142
+
143
+ /**
144
+ * Resolves a single value if it is a kv:// reference using secrets map.
145
+ * Supports path-style keys (e.g. secrets/foo) and hyphen-style (secrets-foo).
146
+ *
147
+ * @param {Object} secrets - Loaded secrets object
148
+ * @param {string} value - Value (may be "kv://..." or plain)
149
+ * @returns {string|null} Resolved plain value or null if unresolved
150
+ */
151
+ function resolveKvValue(secrets, value) {
152
+ if (typeof value !== 'string') return null;
153
+ const trimmed = value.trim();
154
+ if (!trimmed.startsWith('kv://')) {
155
+ return trimmed;
156
+ }
157
+ const pathMatch = trimmed.match(/^kv:\/\/([a-zA-Z0-9_\-/]+)$/);
158
+ if (!pathMatch) return null;
159
+ const pathKey = pathMatch[1];
160
+ const resolved = secrets[pathKey];
161
+ if (resolved === undefined) return null;
162
+ return typeof resolved === 'string' ? resolved : String(resolved);
163
+ }
164
+
165
+ /**
166
+ * Recursively collects all kv:// references from a JSON-serializable payload.
167
+ *
168
+ * @param {Object|Array|string|number|boolean|null} obj - Upload payload (application + dataSources)
169
+ * @param {Set<string>} [acc] - Accumulator set (internal)
170
+ * @returns {string[]} Unique kv:// refs (e.g. ["kv://secrets/foo"])
171
+ */
172
+ function collectKvRefsFromPayload(obj, acc = new Set()) {
173
+ if (obj === null || obj === undefined) {
174
+ return Array.from(acc);
175
+ }
176
+ if (typeof obj === 'string') {
177
+ let m;
178
+ KV_REF_PATTERN.lastIndex = 0;
179
+ while ((m = KV_REF_PATTERN.exec(obj)) !== null) {
180
+ acc.add(`kv://${m[1]}`);
181
+ }
182
+ return Array.from(acc);
183
+ }
184
+ if (Array.isArray(obj)) {
185
+ for (const item of obj) {
186
+ collectKvRefsFromPayload(item, acc);
187
+ }
188
+ return Array.from(acc);
189
+ }
190
+ if (typeof obj === 'object') {
191
+ for (const v of Object.values(obj)) {
192
+ collectKvRefsFromPayload(v, acc);
193
+ }
194
+ return Array.from(acc);
195
+ }
196
+ return Array.from(acc);
197
+ }
198
+
199
+ /**
200
+ * Validates that key is a well-formed kv path (kv:// with alphanumeric/hyphen/slash segments).
201
+ *
202
+ * @param {string} key - kv:// path
203
+ * @returns {boolean}
204
+ */
205
+ function isValidKvPath(key) {
206
+ return typeof key === 'string' && /^kv:\/\/[a-z0-9][a-z0-9\-/]*$/i.test(key.trim());
207
+ }
208
+
209
+ /**
210
+ * Builds secret items from .env file (KV_* vars, values resolved).
211
+ * @param {string} envFilePath - Path to .env
212
+ * @param {Object} secrets - Loaded secrets
213
+ * @param {Map<string, string>} itemsByKey - Mutable map to add items to
214
+ */
215
+ function buildItemsFromEnv(envFilePath, secrets, itemsByKey) {
216
+ if (!envFilePath || typeof envFilePath !== 'string' || !fs.existsSync(envFilePath)) return;
217
+ try {
218
+ const content = fs.readFileSync(envFilePath, 'utf8');
219
+ const envMap = parseEnvToMap(content);
220
+ const fromEnv = collectKvEnvVarsAsSecretItems(envMap);
221
+ for (const { key, value } of fromEnv) {
222
+ const resolved = resolveKvValue(secrets, value);
223
+ if (resolved !== null && resolved !== undefined && isValidKvPath(key)) itemsByKey.set(key, resolved);
224
+ }
225
+ } catch {
226
+ // Best-effort: continue without .env items
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Builds secret items from payload kv:// refs not already in itemsByKey.
232
+ * @param {Object} payload - Upload payload
233
+ * @param {Object} secrets - Loaded secrets
234
+ * @param {Map<string, string>} itemsByKey - Mutable map to add items to
235
+ */
236
+ function buildItemsFromPayload(payload, secrets, itemsByKey) {
237
+ if (!payload || typeof payload !== 'object') return;
238
+ const refs = collectKvRefsFromPayload(payload);
239
+ const existingKeys = new Set(itemsByKey.keys());
240
+ for (const ref of refs) {
241
+ if (existingKeys.has(ref)) continue;
242
+ const pathKey = ref.replace(/^kv:\/\//, '');
243
+ const resolved = secrets[pathKey];
244
+ if (resolved !== null && resolved !== undefined && isValidKvPath(ref)) {
245
+ itemsByKey.set(ref, typeof resolved === 'string' ? resolved : String(resolved));
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Derives stored count from API response.
252
+ * @param {Object} res - Response from storeCredentialSecrets
253
+ * @param {number} fallback - Fallback when stored not in response
254
+ * @returns {number}
255
+ */
256
+ function storedCountFromResponse(res, fallback) {
257
+ if (res && typeof res.stored === 'number') return res.stored;
258
+ if (res && res.data && res.data.stored !== undefined && res.data.stored !== null) return res.data.stored;
259
+ return fallback;
260
+ }
261
+
262
+ /**
263
+ * Sends items to dataplane credential API; returns pushed count or warning.
264
+ * @param {string} dataplaneUrl - Dataplane URL
265
+ * @param {Object} authConfig - Auth config
266
+ * @param {Array<{ key: string, value: string }>} items - Items to send
267
+ * @returns {Promise<{ pushed: number, warning?: string }>}
268
+ */
269
+ async function sendCredentialSecrets(dataplaneUrl, authConfig, items) {
270
+ try {
271
+ const res = await storeCredentialSecrets(dataplaneUrl, authConfig, items);
272
+ const failed = res && (res.success === false || res.data?.success === false);
273
+ if (failed) {
274
+ const status = res.status ?? res.statusCode;
275
+ if (status === 403 || status === 401) {
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.' };
277
+ }
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 };
280
+ }
281
+ return { pushed: storedCountFromResponse(res, items.length) };
282
+ } catch (err) {
283
+ return { pushed: 0, warning: err.message || 'Failed to push credential secrets to dataplane.' };
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Pushes credential secrets to dataplane: from .env (KV_*) and from payload kv:// refs.
289
+ * Resolves all kv:// in values via loadSecrets; sends only plain values. Best-effort:
290
+ * on 403/401 logs warning and returns; never logs secret values.
291
+ *
292
+ * @param {string} dataplaneUrl - Dataplane base URL
293
+ * @param {Object} authConfig - Auth config (Bearer)
294
+ * @param {Object} options - Options
295
+ * @param {string} [options.envFilePath] - Path to .env (integration/<systemKey>/.env)
296
+ * @param {string} [options.appName] - App/system name for loadSecrets context
297
+ * @param {Object} [options.payload] - Upload payload { application, dataSources } for kv scan
298
+ * @returns {Promise<{ pushed: number, keys?: string[], skipped?: boolean, warning?: string }>} Count pushed, keys (on success), skipped (when nothing to push), optional warning
299
+ */
300
+ async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
301
+ const { envFilePath, appName, payload } = options;
302
+ let secrets;
303
+ try {
304
+ secrets = await loadSecrets(undefined, appName);
305
+ } catch {
306
+ secrets = {};
307
+ }
308
+ const itemsByKey = new Map();
309
+ buildItemsFromEnv(envFilePath, secrets, itemsByKey);
310
+ buildItemsFromPayload(payload, secrets, itemsByKey);
311
+
312
+ const items = Array.from(itemsByKey.entries())
313
+ .filter(([k]) => isValidKvPath(k))
314
+ .map(([key, value]) => ({ key, value }));
315
+
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;
322
+ }
323
+
324
+ /**
325
+ * Parses .env-style content into key-value map (first = separates key and value).
326
+ *
327
+ * @param {string} content - Raw .env content
328
+ * @returns {Object.<string, string>}
329
+ */
330
+ function parseEnvToMap(content) {
331
+ if (!content || typeof content !== 'string') return {};
332
+ const map = {};
333
+ const lines = content.split(/\r?\n/);
334
+ for (const line of lines) {
335
+ const trimmed = line.trim();
336
+ if (!trimmed || trimmed.startsWith('#')) continue;
337
+ const eq = trimmed.indexOf('=');
338
+ if (eq > 0) {
339
+ const key = trimmed.substring(0, eq).trim();
340
+ const value = trimmed.substring(eq + 1);
341
+ map[key] = value;
342
+ }
343
+ }
344
+ return map;
345
+ }
346
+
347
+ module.exports = {
348
+ collectKvEnvVarsAsSecretItems,
349
+ collectKvRefsFromPayload,
350
+ pushCredentialSecrets,
351
+ kvEnvKeyToPath,
352
+ systemKeyToKvPrefix,
353
+ securityKeyToVar,
354
+ isValidKvPath,
355
+ resolveKvValue,
356
+ parseEnvToMap
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
  }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Dev CA Install – SSL untrusted detection, fetch CA from Builder Server, install into OS trust store.
3
+ * Used by `aifabrix dev init` when the server certificate is self-signed. Only /install-ca uses
4
+ * rejectUnauthorized: false; all other requests use default TLS verification.
5
+ *
6
+ * @fileoverview CA install utilities for development Builder Server
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+ const https = require('https');
14
+ const os = require('os');
15
+ const { execFileSync } = require('child_process');
16
+ const readline = require('readline');
17
+ const chalk = require('chalk');
18
+
19
+ const SSL_UNTRUSTED_CODES = [
20
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
21
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
22
+ 'CERT_UNTRUSTED',
23
+ 'SELF_SIGNED_CERT_IN_CHAIN',
24
+ 'UNABLE_TO_GET_ISSUER_CERT',
25
+ 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'
26
+ ];
27
+
28
+ /**
29
+ * Returns true if the error indicates an untrusted/self-signed server certificate.
30
+ * @param {Error} err - Thrown error (e.g. from devApi.getHealth)
31
+ * @returns {boolean}
32
+ */
33
+ function isSslUntrustedError(err) {
34
+ const code = err?.code || err?.cause?.code;
35
+ const msg = (err?.message || '').toUpperCase();
36
+ return SSL_UNTRUSTED_CODES.some(c => code === c || msg.includes(c));
37
+ }
38
+
39
+ /**
40
+ * Fetch CA PEM from Builder Server via GET {baseUrl}/install-ca.
41
+ * Uses rejectUnauthorized: false only for this endpoint (dev setup).
42
+ * @param {string} baseUrl - Builder Server base URL (no trailing slash)
43
+ * @returns {Promise<Buffer>} CA certificate PEM
44
+ */
45
+ function fetchInstallCa(baseUrl) {
46
+ return new Promise((resolve, reject) => {
47
+ const url = `${baseUrl.replace(/\/+$/, '')}/install-ca`;
48
+ const urlObj = new URL(url);
49
+ if (urlObj.protocol !== 'https:') {
50
+ reject(new Error('install-ca requires https URL'));
51
+ return;
52
+ }
53
+ const agent = new https.Agent({ rejectUnauthorized: false });
54
+ const req = https.get(
55
+ url,
56
+ { agent },
57
+ (res) => {
58
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
59
+ req.destroy();
60
+ fetchInstallCa(res.headers.location).then(resolve).catch(reject);
61
+ return;
62
+ }
63
+ const chunks = [];
64
+ res.on('data', c => chunks.push(c));
65
+ res.on('end', () => {
66
+ const body = Buffer.concat(chunks).toString('utf8');
67
+ if (!body || !body.includes('-----BEGIN CERTIFICATE-----')) {
68
+ reject(new Error('Invalid CA response: expected PEM certificate'));
69
+ return;
70
+ }
71
+ resolve(Buffer.from(body, 'utf8'));
72
+ });
73
+ }
74
+ );
75
+ req.on('error', reject);
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Install CA PEM into OS trust store (platform-specific).
81
+ * @param {Buffer|string} caPem - CA certificate PEM
82
+ * @param {string} baseUrl - Builder Server base URL (for help link)
83
+ * @returns {Promise<void>}
84
+ */
85
+ async function installCaPlatform(caPem, baseUrl) {
86
+ const pem = Buffer.isBuffer(caPem) ? caPem.toString('utf8') : String(caPem);
87
+ const tmpDir = os.tmpdir();
88
+ const tmpPath = path.join(tmpDir, 'aifabrix-root-ca.crt');
89
+ await fs.writeFile(tmpPath, pem, { mode: 0o644 });
90
+
91
+ try {
92
+ if (process.platform === 'win32') {
93
+ execFileSync('certutil', ['-addstore', '-user', 'ROOT', tmpPath], { stdio: 'inherit' });
94
+ } else if (process.platform === 'darwin') {
95
+ const keychain = path.join(os.homedir(), 'Library', 'Keychains', 'login.keychain-db');
96
+ execFileSync('security', ['add-trusted-cert', '-d', '-r', 'trustRoot', '-k', keychain, tmpPath], { stdio: 'inherit' });
97
+ } else if (process.platform === 'linux') {
98
+ const certPath = '/usr/local/share/ca-certificates/aifabrix-root-ca.crt';
99
+ try {
100
+ await fs.writeFile(certPath, pem, { mode: 0o644 });
101
+ execFileSync('update-ca-certificates', [], { stdio: 'inherit' });
102
+ } catch (e) {
103
+ if (e.code === 'EACCES' || (e.status !== undefined && e.status !== null && e.status !== 0)) {
104
+ const helpUrl = `${baseUrl.replace(/\/+$/, '')}/install-ca-help`;
105
+ throw new Error(
106
+ `Linux CA install requires sudo. Save CA manually from ${helpUrl} to ${certPath} and run: sudo update-ca-certificates`
107
+ );
108
+ }
109
+ throw e;
110
+ }
111
+ } else {
112
+ throw new Error(`Unsupported platform: ${process.platform}`);
113
+ }
114
+ } finally {
115
+ await fs.unlink(tmpPath).catch(() => {});
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Prompt user: "Download and install the development CA? (y/n)"
121
+ * @returns {Promise<boolean>}
122
+ */
123
+ function promptInstallCa() {
124
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
125
+ return new Promise(resolve => {
126
+ rl.question(chalk.yellow('Server certificate not trusted. Download and install the development CA? (y/n) '), answer => {
127
+ rl.close();
128
+ const normalized = (answer || '').trim().toLowerCase();
129
+ resolve(normalized === 'y' || normalized === 'yes');
130
+ });
131
+ });
132
+ }
133
+
134
+ module.exports = {
135
+ isSslUntrustedError,
136
+ fetchInstallCa,
137
+ installCaPlatform,
138
+ promptInstallCa
139
+ };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @fileoverview Helper for generating CSR and saving dev certificates (Builder Server onboarding)
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+ const os = require('os');
11
+
12
+ /**
13
+ * Generate a key pair and CSR for developer certificate. Uses OpenSSL when available.
14
+ * CN in CSR is set to dev-<developerId> per Builder Server convention.
15
+ * @param {string} developerId - Developer ID (e.g. "01")
16
+ * @returns {{ csrPem: string, keyPem: string }} PEM-encoded CSR and private key
17
+ * @throws {Error} If OpenSSL is not available or generation fails
18
+ */
19
+ function generateCSR(developerId) {
20
+ if (!developerId || typeof developerId !== 'string') {
21
+ throw new Error('developerId is required and must be a string');
22
+ }
23
+ const cn = `dev-${developerId}`;
24
+ const tmpDir = path.join(os.tmpdir(), `aifabrix-csr-${Date.now()}`);
25
+ fs.mkdirSync(tmpDir, { recursive: true });
26
+ const keyPath = path.join(tmpDir, 'key.pem');
27
+ const csrPath = path.join(tmpDir, 'csr.pem');
28
+ try {
29
+ execSync(
30
+ `openssl req -new -newkey rsa:2048 -keyout "${keyPath}" -nodes -subj "/CN=${cn}" -out "${csrPath}"`,
31
+ { stdio: 'pipe', encoding: 'utf8' }
32
+ );
33
+ const keyPem = fs.readFileSync(keyPath, 'utf8');
34
+ const csrPem = fs.readFileSync(csrPath, 'utf8');
35
+ return { csrPem, keyPem };
36
+ } catch (err) {
37
+ if (err.message && (err.message.includes('openssl') || err.message.includes('ENOENT'))) {
38
+ throw new Error(
39
+ 'OpenSSL is required for certificate generation. Install OpenSSL and ensure it is on PATH, or use a system that provides it (e.g. Git for Windows).'
40
+ );
41
+ }
42
+ throw new Error(`CSR generation failed: ${err.message}`);
43
+ } finally {
44
+ try {
45
+ fs.unlinkSync(keyPath);
46
+ fs.unlinkSync(csrPath);
47
+ fs.rmdirSync(tmpDir);
48
+ } catch {
49
+ // ignore cleanup errors
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Return path to the directory where dev certificates are stored for a developer.
56
+ * @param {string} configDir - Config directory (e.g. from getConfigDirForPaths())
57
+ * @param {string} developerId - Developer ID
58
+ * @returns {string} Absolute path to certs/<developerId>/
59
+ */
60
+ function getCertDir(configDir, developerId) {
61
+ return path.join(configDir, 'certs', developerId);
62
+ }
63
+
64
+ /**
65
+ * Read client certificate PEM from cert dir (cert.pem).
66
+ * @param {string} certDir - Directory containing cert.pem
67
+ * @returns {string|null} PEM content or null if not found
68
+ */
69
+ function readClientCertPem(certDir) {
70
+ const certPath = path.join(certDir, 'cert.pem');
71
+ try {
72
+ return fs.readFileSync(certPath, 'utf8');
73
+ } catch (e) {
74
+ if (e.code === 'ENOENT') return null;
75
+ throw e;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Read client private key PEM from cert dir (key.pem). Used for mTLS (e.g. getSettings).
81
+ * @param {string} certDir - Directory containing key.pem
82
+ * @returns {string|null} PEM content or null if not found
83
+ */
84
+ function readClientKeyPem(certDir) {
85
+ const keyPath = path.join(certDir, 'key.pem');
86
+ try {
87
+ return fs.readFileSync(keyPath, 'utf8');
88
+ } catch (e) {
89
+ if (e.code === 'ENOENT') return null;
90
+ throw e;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get certificate validity end (notAfter) from cert.pem in certDir using OpenSSL.
96
+ * @param {string} certDir - Directory containing cert.pem
97
+ * @returns {Date|null} Expiry date or null if cert missing/invalid or OpenSSL fails
98
+ */
99
+ function getCertValidNotAfter(certDir) {
100
+ const certPath = path.join(certDir, 'cert.pem');
101
+ try {
102
+ if (!fs.existsSync(certPath)) return null;
103
+ const out = execSync(
104
+ `openssl x509 -enddate -noout -in "${certPath}"`,
105
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
106
+ );
107
+ const match = out.match(/notAfter=(.+)/);
108
+ if (!match) return null;
109
+ const date = new Date(match[1].trim());
110
+ return isNaN(date.getTime()) ? null : date;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ generateCSR,
118
+ getCertDir,
119
+ readClientCertPem,
120
+ readClientKeyPem,
121
+ getCertValidNotAfter
122
+ };