@aifabrix/builder 2.45.5 → 2.46.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 (231) hide show
  1. package/.cursor/rules/cli-layout.mdc +2 -0
  2. package/.cursor/rules/cli-product-neutral.mdc +43 -0
  3. package/.cursor/rules/project-rules.mdc +1 -0
  4. package/README.md +43 -17
  5. package/integration/hubspot-test/test-artifacts/wizard-hubspot-credential-real.yaml +1 -1
  6. package/integration/hubspot-test/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
  7. package/jest.config.default.js +2 -1
  8. package/jest.config.isolated.js +2 -1
  9. package/jest.global-live-fabrix-hooks.js +36 -0
  10. package/jest.isolated-projects.js +130 -0
  11. package/jest.projects.js +41 -136
  12. package/lib/api/api-response-errors.js +124 -0
  13. package/lib/api/governance-scenario-pack.api.js +159 -0
  14. package/lib/api/index.js +43 -3
  15. package/lib/api/lifecycle.api.js +77 -0
  16. package/lib/api/pipeline.api.js +3 -1
  17. package/lib/api/records-bulk.api.js +14 -1
  18. package/lib/api/validation-runner.js +3 -1
  19. package/lib/app/deploy-config.js +4 -1
  20. package/lib/app/down.js +20 -1
  21. package/lib/app/push.js +4 -5
  22. package/lib/app/register.js +7 -4
  23. package/lib/app/run-env-compose.js +84 -30
  24. package/lib/build/index.js +5 -0
  25. package/lib/certification/sync-system-certification.js +13 -6
  26. package/lib/cli/index.js +4 -0
  27. package/lib/cli/infra-guided-auth.js +109 -0
  28. package/lib/cli/infra-guided.js +84 -81
  29. package/lib/cli/setup-app.help.js +0 -31
  30. package/lib/cli/setup-app.js +28 -2
  31. package/lib/cli/setup-app.test-commands.js +5 -57
  32. package/lib/cli/setup-auth.js +25 -0
  33. package/lib/cli/setup-certification-commands.js +147 -0
  34. package/lib/cli/setup-certification.help.js +141 -0
  35. package/lib/cli/setup-environment.js +19 -2
  36. package/lib/cli/setup-external-system-help.js +62 -0
  37. package/lib/cli/setup-external-system.js +25 -3
  38. package/lib/cli/setup-infra.js +9 -1
  39. package/lib/cli/setup-platform.js +6 -1
  40. package/lib/cli/setup-secrets.js +33 -1
  41. package/lib/cli/setup-utility-help-after.js +22 -2
  42. package/lib/cli/setup-utility-repair.js +198 -0
  43. package/lib/cli/setup-utility-resolve.js +40 -4
  44. package/lib/cli/setup-utility-validate.js +197 -0
  45. package/lib/cli/setup-utility.js +4 -239
  46. package/lib/commands/auth-status-client-tokens.js +214 -0
  47. package/lib/commands/auth-status-dataplane-version.js +58 -20
  48. package/lib/commands/auth-status.js +41 -47
  49. package/lib/commands/datasource-test-trust-cli.js +27 -23
  50. package/lib/commands/datasource-unified-test-cli.js +6 -3
  51. package/lib/commands/datasource-unified-test-cli.options.js +5 -1
  52. package/lib/commands/datasource.js +7 -7
  53. package/lib/commands/dev-show-display.js +8 -1
  54. package/lib/commands/governance-cli-shared.js +94 -0
  55. package/lib/commands/governance-command-action.js +132 -0
  56. package/lib/commands/governance-download-external.js +34 -0
  57. package/lib/commands/governance-generate-external.js +71 -0
  58. package/lib/commands/governance-upload-external.js +36 -0
  59. package/lib/commands/governance-verify-external.js +64 -0
  60. package/lib/commands/governance.js +144 -0
  61. package/lib/commands/lifecycle-command-action.js +123 -0
  62. package/lib/commands/lifecycle-external.js +268 -0
  63. package/lib/commands/login.js +11 -2
  64. package/lib/commands/repair-auth-apply.js +127 -0
  65. package/lib/commands/repair-changed-files.js +41 -0
  66. package/lib/commands/repair-datasource-keys.js +4 -1
  67. package/lib/commands/repair-datasource-run.js +59 -0
  68. package/lib/commands/repair-datasource-test-payload.js +259 -0
  69. package/lib/commands/repair-datasource.js +1 -51
  70. package/lib/commands/repair-env-template.js +64 -13
  71. package/lib/commands/repair-external-run.js +216 -0
  72. package/lib/commands/repair-openapi-sync.js +23 -0
  73. package/lib/commands/repair-persist.js +4 -0
  74. package/lib/commands/repair-rbac.js +52 -1
  75. package/lib/commands/repair-steps.js +145 -0
  76. package/lib/commands/repair.js +29 -296
  77. package/lib/commands/secrets-get.js +184 -0
  78. package/lib/commands/secrets-remove-all.js +3 -1
  79. package/lib/commands/setup-image-refresh.js +146 -0
  80. package/lib/commands/setup-modes.js +158 -162
  81. package/lib/commands/setup-platform-auth.js +76 -0
  82. package/lib/commands/setup-prompts-admin.js +244 -0
  83. package/lib/commands/setup-prompts-platform-mode.js +67 -0
  84. package/lib/commands/setup-prompts-validators.js +36 -0
  85. package/lib/commands/setup-prompts.js +24 -76
  86. package/lib/commands/setup-run-context.js +45 -0
  87. package/lib/commands/setup-spinners.js +39 -0
  88. package/lib/commands/setup.js +27 -5
  89. package/lib/commands/teardown.js +39 -0
  90. package/lib/commands/test-e2e-external.js +24 -8
  91. package/lib/commands/test-governance-external.js +3 -24
  92. package/lib/commands/test-trust-external.js +20 -21
  93. package/lib/commands/up-common.js +25 -13
  94. package/lib/commands/up-dataplane.js +50 -13
  95. package/lib/commands/upload-publish-context.js +75 -0
  96. package/lib/commands/upload-sidecar-followups.js +89 -0
  97. package/lib/commands/upload.js +90 -108
  98. package/lib/commands/verify-governance-combined-external.js +61 -0
  99. package/lib/commands/verify-governance-command-action.js +64 -0
  100. package/lib/commands/verify-governance-db.js +102 -0
  101. package/lib/commands/verify-governance-external.js +77 -0
  102. package/lib/commands/verify-governance-result-builders.js +71 -0
  103. package/lib/commands/verify-operations-command-action.js +68 -0
  104. package/lib/commands/verify-operations-external.js +268 -0
  105. package/lib/commands/verify-operations-persist.js +75 -0
  106. package/lib/commands/verify-operations-step-plan.js +68 -0
  107. package/lib/commands/verify-operations-steps.js +111 -0
  108. package/lib/commands/verify-trust-command-action.js +70 -0
  109. package/lib/commands/verify-trust-external.js +67 -0
  110. package/lib/commands/wizard-config-normalizer.js +43 -23
  111. package/lib/commands/wizard-core.js +2 -0
  112. package/lib/core/admin-password-generator.js +38 -0
  113. package/lib/core/admin-secrets-bundle.js +111 -0
  114. package/lib/core/admin-secrets-env-overlay.js +62 -0
  115. package/lib/core/admin-secrets.js +32 -4
  116. package/lib/core/config-attach-extensions.js +3 -0
  117. package/lib/core/config-platform-controller.js +44 -0
  118. package/lib/core/config-setup-installation-profile.js +42 -0
  119. package/lib/core/config-setup-platform-mode.js +46 -0
  120. package/lib/core/config.js +46 -36
  121. package/lib/core/secrets-admin-env.js +6 -2
  122. package/lib/core/secrets-ensure.js +4 -0
  123. package/lib/core/secrets-env-content.js +54 -48
  124. package/lib/core/secrets-env-write.js +10 -1
  125. package/lib/core/secrets-infra-placeholder-sync.js +2 -0
  126. package/lib/core/setup-installation-profile.js +43 -0
  127. package/lib/core/setup-platform-mode.js +55 -0
  128. package/lib/datasource/agent-trust-debug-log.js +1 -1
  129. package/lib/datasource/agent-trust-run.js +5 -3
  130. package/lib/datasource/audit-evidence-extract-debug.js +30 -18
  131. package/lib/datasource/audit-evidence-extract.js +7 -4
  132. package/lib/datasource/audit-evidence-verify-matrix.js +54 -26
  133. package/lib/datasource/bulk-loader-run-batches.js +15 -3
  134. package/lib/datasource/datasource-load-export-context.js +3 -0
  135. package/lib/datasource/log-cleaner.js +2 -2
  136. package/lib/datasource/log-viewer.js +8 -8
  137. package/lib/datasource/test-e2e.js +4 -1
  138. package/lib/datasource/unified-validation-run-body.js +3 -1
  139. package/lib/datasource/unified-validation-run.js +5 -2
  140. package/lib/deployment/environment-poll-status.js +138 -0
  141. package/lib/deployment/environment.js +42 -34
  142. package/lib/external-system/download.js +21 -20
  143. package/lib/external-system/generator.js +5 -1
  144. package/lib/external-system/integration-auth-context.js +45 -0
  145. package/lib/external-system/test-auth.js +12 -5
  146. package/lib/external-system/test.js +6 -6
  147. package/lib/generator/external-controller-manifest.js +15 -0
  148. package/lib/governance/governance-cli-errors.js +96 -0
  149. package/lib/governance/governance-download-display.js +38 -0
  150. package/lib/governance/governance-run-display.js +215 -25
  151. package/lib/infrastructure/admin-secrets-apply.js +84 -0
  152. package/lib/infrastructure/admin-secrets-ensure.js +130 -0
  153. package/lib/infrastructure/admin-secrets-resolve.js +95 -0
  154. package/lib/infrastructure/admin-secrets-sync.js +91 -0
  155. package/lib/infrastructure/helpers.js +23 -129
  156. package/lib/infrastructure/index.js +5 -6
  157. package/lib/infrastructure/orphan-infra-docker-teardown.js +11 -5
  158. package/lib/lifecycle/certification-progress.js +231 -0
  159. package/lib/lifecycle/dataplane-ui-url.js +145 -0
  160. package/lib/lifecycle/exit-codes.js +24 -0
  161. package/lib/lifecycle/json-envelope.js +25 -0
  162. package/lib/lifecycle/lifecycle-governance-options.js +51 -0
  163. package/lib/lifecycle/logger-quiet.js +29 -0
  164. package/lib/lifecycle/open-api-published.js +59 -0
  165. package/lib/lifecycle/operations-readiness-metrics.js +101 -0
  166. package/lib/lifecycle/product-model.js +196 -0
  167. package/lib/lifecycle/report-display.js +471 -0
  168. package/lib/lifecycle/scenario-upload.js +191 -0
  169. package/lib/lifecycle/verify-step-progress.js +87 -0
  170. package/lib/parameters/infra-kv-discovery.js +27 -2
  171. package/lib/protection/auth-context.js +10 -4
  172. package/lib/schema/application-schema.json +1 -1
  173. package/lib/schema/datasource-test-run.schema.json +89 -6
  174. package/lib/schema/external-datasource.schema.json +2636 -2622
  175. package/lib/schema/external-system.schema.json +1089 -761
  176. package/lib/schema/infra.parameter.yaml +45 -0
  177. package/lib/schema/type/credential-field-registry.json +103 -97
  178. package/lib/utils/aifabrix-test-runtime-guard.js +125 -0
  179. package/lib/utils/api.js +55 -31
  180. package/lib/utils/app-relative-path-env.js +112 -0
  181. package/lib/utils/cli-secrets-error-format.js +83 -21
  182. package/lib/utils/cli-utils.js +16 -4
  183. package/lib/utils/compose-generate-docker-compose.js +4 -0
  184. package/lib/utils/compose-traefik-ingress-base.js +16 -1
  185. package/lib/utils/config-secrets-path.js +46 -0
  186. package/lib/utils/controller-url.js +88 -1
  187. package/lib/utils/credential-secrets-env.js +137 -9
  188. package/lib/utils/dataplane-resolver.js +129 -2
  189. package/lib/utils/declarative-url-ports.js +3 -3
  190. package/lib/utils/deployment-auth-mode.js +41 -0
  191. package/lib/utils/developer-volume-id.js +33 -0
  192. package/lib/utils/env-copy.js +14 -9
  193. package/lib/utils/env-kv-orphan-lines.js +43 -0
  194. package/lib/utils/error-formatters/network-errors.js +13 -4
  195. package/lib/utils/external-env-template.js +1 -0
  196. package/lib/utils/external-system-readiness-core.js +10 -0
  197. package/lib/utils/external-system-readiness-display.js +80 -23
  198. package/lib/utils/health-check.js +4 -1
  199. package/lib/utils/help-builder.js +11 -8
  200. package/lib/utils/keycloak-realm-ready.js +101 -0
  201. package/lib/utils/load-cip-capacity-display-config.js +61 -15
  202. package/lib/utils/manifest-source-emit.js +2 -0
  203. package/lib/utils/pipeline-upload-auth-retry.js +82 -0
  204. package/lib/utils/platform-controller-url.js +156 -0
  205. package/lib/utils/postgres-platform-bootstrap.js +199 -0
  206. package/lib/utils/postgres-wipe.js +9 -2
  207. package/lib/utils/resolve-env-mode.js +47 -0
  208. package/lib/utils/secrets-generator.js +31 -25
  209. package/lib/utils/secrets-missing-error.js +164 -0
  210. package/lib/utils/service-url-label.js +81 -0
  211. package/lib/utils/setup-quiet-output.js +25 -0
  212. package/lib/utils/template-helpers.js +15 -0
  213. package/lib/utils/token-manager-client-credentials.js +201 -0
  214. package/lib/utils/token-manager-messages.js +8 -0
  215. package/lib/utils/token-manager-refresh.js +12 -2
  216. package/lib/utils/token-manager.js +86 -105
  217. package/lib/utils/upload-pipeline-ui.js +12 -5
  218. package/lib/utils/upload-sync-options.js +42 -0
  219. package/lib/utils/url-declarative-public-base.js +21 -3
  220. package/lib/utils/url-declarative-vdir-inactive-env.js +14 -4
  221. package/lib/utils/validation-poll-ui.js +8 -0
  222. package/lib/utils/validation-run-stage.js +45 -0
  223. package/package.json +1 -1
  224. package/templates/applications/dataplane/application.yaml +2 -1
  225. package/templates/applications/dataplane/env.template +24 -1
  226. package/templates/applications/miso-controller/env.template +16 -0
  227. package/templates/external-system/README.md.hbs +74 -17
  228. package/templates/python/docker-compose.hbs +19 -18
  229. package/templates/typescript/docker-compose.hbs +18 -17
  230. package/lib/commands/test-governance-command-action.js +0 -55
  231. package/lib/commands/test-trust-command-action.js +0 -118
@@ -280,21 +280,146 @@ function storedCountFromResponse(res, fallback) {
280
280
  * @param {Array<{ key: string, value: string }>} items - Items to send
281
281
  * @returns {Promise<{ pushed: number, warning?: string }>}
282
282
  */
283
+ /**
284
+ * For bearerToken auth, credential template resolves security.token only.
285
+ * Legacy .env keys KV_*_APIKEY with a plain token wrongly map to kv://.../apiKey.
286
+ * Prefer token path; drop duplicate apiKey when both would be pushed.
287
+ *
288
+ * @param {Map<string, string>} itemsByKey
289
+ * @param {string} [systemKey]
290
+ * @param {Object} [authConfig]
291
+ */
292
+ function reconcileBearerTokenSecretPaths(itemsByKey, systemKey, authConfig) {
293
+ const method = (
294
+ authConfig?.method ||
295
+ authConfig?.type ||
296
+ ''
297
+ )
298
+ .toString()
299
+ .toLowerCase()
300
+ .replace(/_/g, '');
301
+ if (method !== 'bearertoken' || !systemKey) return;
302
+ const tokenPath = `kv://${systemKey}/token`;
303
+ const apiKeyPath = `kv://${systemKey}/apiKey`;
304
+ if (!itemsByKey.has(apiKeyPath)) return;
305
+ if (!itemsByKey.has(tokenPath)) {
306
+ itemsByKey.set(tokenPath, itemsByKey.get(apiKeyPath));
307
+ }
308
+ itemsByKey.delete(apiKeyPath);
309
+ }
310
+
311
+ /**
312
+ * Optional dev/lab dataplane API key (Bearer bypass) for credential secret push only.
313
+ * Does not alter pipeline publish auth when AIFABRIX_DEPLOYMENT_AUTH=client-credentials.
314
+ * @returns {string|null}
315
+ */
316
+ function resolveLabDataplaneApiKey() {
317
+ const candidates = [
318
+ process.env.AIFABRIX_DATAPLANE_API_KEY,
319
+ process.env.DATAPLANE_API_KEY,
320
+ process.env.API_KEY
321
+ ];
322
+ for (const raw of candidates) {
323
+ const trimmed = typeof raw === 'string' ? raw.trim() : '';
324
+ if (trimmed) return trimmed;
325
+ }
326
+ return null;
327
+ }
328
+
329
+ /**
330
+ * @param {string} token
331
+ * @returns {{ type: string, token: string }}
332
+ */
333
+ function apiKeyAuthConfig(token) {
334
+ return { type: 'bearer', token };
335
+ }
336
+
337
+ const PERMISSION_DENIED_WARNING =
338
+ 'Could not push credential secrets (permission denied or unauthenticated). Ensure dataplane role has credential:create if you use KV_* in .env, or set AIFABRIX_DATAPLANE_API_KEY for local lab bypass.';
339
+
340
+ /**
341
+ * @param {Object} res
342
+ * @returns {boolean}
343
+ */
344
+ function isStoreResponseFailed(res) {
345
+ return Boolean(res && (res.success === false || res.data?.success === false));
346
+ }
347
+
348
+ /**
349
+ * @param {Object} res
350
+ * @returns {number|undefined}
351
+ */
352
+ function storeResponseStatus(res) {
353
+ return res?.status ?? res?.statusCode;
354
+ }
355
+
356
+ /**
357
+ * @param {Object} res
358
+ * @returns {{ pushed: number, warning: string }}
359
+ */
360
+ function storeErrorWarning(res) {
361
+ const errMsg =
362
+ res.formattedError ||
363
+ res.data?.formattedError ||
364
+ res.error ||
365
+ res.data?.error ||
366
+ 'Failed to push credential secrets to dataplane.';
367
+ return { pushed: 0, warning: errMsg };
368
+ }
369
+
370
+ /**
371
+ * @param {string} dataplaneUrl
372
+ * @param {Object} authConfig
373
+ * @param {Array<{ key: string, value: string }>} items
374
+ * @returns {Promise<{ pushed: number }|null>}
375
+ */
376
+ async function tryLabApiKeyRetry(dataplaneUrl, authConfig, items) {
377
+ const labKey = resolveLabDataplaneApiKey();
378
+ if (!labKey || authConfig?.token === labKey) {
379
+ return null;
380
+ }
381
+ const retry = await storeCredentialSecrets(
382
+ dataplaneUrl,
383
+ apiKeyAuthConfig(labKey),
384
+ items
385
+ );
386
+ if (isStoreResponseFailed(retry)) {
387
+ return null;
388
+ }
389
+ return { pushed: storedCountFromResponse(retry, items.length) };
390
+ }
391
+
392
+ /**
393
+ * @param {string} dataplaneUrl
394
+ * @param {Object} authConfig
395
+ * @param {Array<{ key: string, value: string }>} items
396
+ * @param {Object} res
397
+ * @returns {Promise<{ pushed: number, warning?: string }>}
398
+ */
399
+ async function handleFailedStoreResponse(dataplaneUrl, authConfig, items, res) {
400
+ const status = storeResponseStatus(res);
401
+ if (status === 403 || status === 401) {
402
+ const retryResult = await tryLabApiKeyRetry(dataplaneUrl, authConfig, items);
403
+ if (retryResult) {
404
+ return retryResult;
405
+ }
406
+ return { pushed: 0, warning: PERMISSION_DENIED_WARNING };
407
+ }
408
+ return storeErrorWarning(res);
409
+ }
410
+
283
411
  async function sendCredentialSecrets(dataplaneUrl, authConfig, items) {
284
412
  try {
285
413
  const res = await storeCredentialSecrets(dataplaneUrl, authConfig, items);
286
- const failed = res && (res.success === false || res.data?.success === false);
287
- if (failed) {
288
- const status = res.status ?? res.statusCode;
289
- if (status === 403 || status === 401) {
290
- 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.' };
291
- }
292
- const errMsg = res.formattedError || res.data?.formattedError || res.error || res.data?.error || 'Failed to push credential secrets to dataplane.';
293
- return { pushed: 0, warning: errMsg };
414
+ if (isStoreResponseFailed(res)) {
415
+ return handleFailedStoreResponse(dataplaneUrl, authConfig, items, res);
294
416
  }
295
417
  return { pushed: storedCountFromResponse(res, items.length) };
296
418
  } catch (err) {
297
- return { pushed: 0, warning: err.message || 'Failed to push credential secrets to dataplane.' };
419
+ return {
420
+ pushed: 0,
421
+ warning: err.message || 'Failed to push credential secrets to dataplane.'
422
+ };
298
423
  }
299
424
  }
300
425
 
@@ -322,6 +447,7 @@ async function pushCredentialSecrets(dataplaneUrl, authConfig, options = {}) {
322
447
  const itemsByKey = new Map();
323
448
  buildItemsFromEnv(envFilePath, secrets, itemsByKey, appName);
324
449
  buildItemsFromPayload(payload, secrets, itemsByKey);
450
+ reconcileBearerTokenSecretPaths(itemsByKey, appName, authConfig);
325
451
 
326
452
  const items = Array.from(itemsByKey.entries())
327
453
  .filter(([k]) => isValidKvPath(k))
@@ -362,6 +488,8 @@ module.exports = {
362
488
  collectKvEnvVarsAsSecretItems,
363
489
  collectKvRefsFromPayload,
364
490
  pushCredentialSecrets,
491
+ reconcileBearerTokenSecretPaths,
492
+ resolveLabDataplaneApiKey,
365
493
  kvEnvKeyToPath,
366
494
  getKvPathSegmentForSecurityKey,
367
495
  systemKeyToKvPrefix,
@@ -8,7 +8,127 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const config = require('../core/config');
11
15
  const { discoverDataplaneUrl } = require('../commands/wizard-dataplane');
16
+ const pathsUtil = require('./paths');
17
+ const { loadConfigFile } = require('./config-format');
18
+
19
+ /** Matches `templates/applications/dataplane/application.yaml` and `calculateComposePort`. */
20
+ const DEFAULT_DATAPLANE_MANIFEST_PORT = 3001;
21
+
22
+ /**
23
+ * @param {string} url
24
+ * @returns {number|null}
25
+ */
26
+ function localhostPortFromUrl(url) {
27
+ if (!url || typeof url !== 'string') return null;
28
+ const normalized = url.replace(/127\.0\.0\.1/g, 'localhost');
29
+ const match = normalized.match(/localhost:(\d+)/);
30
+ if (!match) return null;
31
+ const port = parseInt(match[1], 10);
32
+ return Number.isFinite(port) ? port : null;
33
+ }
34
+
35
+ /**
36
+ * Read dataplane `port` from materialized builder config or bundled template.
37
+ *
38
+ * @returns {number}
39
+ */
40
+ function loadDataplaneManifestListenPort() {
41
+ const tryLoad = (configPath) => {
42
+ if (!fs.existsSync(configPath)) {
43
+ return null;
44
+ }
45
+ const variables = loadConfigFile(configPath);
46
+ const port = variables && variables.port !== undefined ? Number(variables.port) : NaN;
47
+ return Number.isFinite(port) && port > 0 ? port : null;
48
+ };
49
+
50
+ try {
51
+ const builderPath = pathsUtil.getBuilderPath('dataplane');
52
+ if (fs.existsSync(builderPath)) {
53
+ try {
54
+ const fromBuilder = tryLoad(pathsUtil.resolveApplicationConfigPath(builderPath));
55
+ if (fromBuilder) {
56
+ return fromBuilder;
57
+ }
58
+ } catch {
59
+ // Fall through to bundled template when builder dir exists but config is missing.
60
+ }
61
+ }
62
+ } catch {
63
+ // ignore
64
+ }
65
+
66
+ const templatePath = path.join(
67
+ pathsUtil.getProjectRoot(),
68
+ 'templates',
69
+ 'applications',
70
+ 'dataplane',
71
+ 'application.yaml'
72
+ );
73
+ const fromTemplate = tryLoad(templatePath);
74
+ return fromTemplate || DEFAULT_DATAPLANE_MANIFEST_PORT;
75
+ }
76
+
77
+ /**
78
+ * Local Docker host port: `application.yaml` `port` (default 3001) + developerId×100.
79
+ * Same rule as {@link calculateComposePort} / `aifabrix up-platform` — not
80
+ * `APP_SERVICE_ENV_DEFAULTS_LOCAL.DATAPLANE_PUBLIC_PORT` (3011), which is for other local profiles.
81
+ *
82
+ * @returns {Promise<string>}
83
+ */
84
+ async function expectedLocalDataplaneBaseUrl() {
85
+ const developerIdRaw = await config.getDeveloperId();
86
+ const idNum =
87
+ typeof developerIdRaw === 'string'
88
+ ? parseInt(developerIdRaw, 10)
89
+ : Number(developerIdRaw);
90
+ const devId = Number.isFinite(idNum) && idNum >= 0 ? idNum : 0;
91
+ const basePort = loadDataplaneManifestListenPort();
92
+ const hostPort = devId === 0 ? basePort : basePort + devId * 100;
93
+ return `http://localhost:${hostPort}`;
94
+ }
95
+
96
+ /**
97
+ * Controller application.url can lag after dev-id changes; prefer builder host port formula for localhost.
98
+ * @param {string} discoveredUrl
99
+ * @returns {Promise<string>}
100
+ */
101
+ async function alignDiscoveredDataplaneUrl(discoveredUrl) {
102
+ if (!discoveredUrl || !/localhost|127\.0\.0\.1/i.test(discoveredUrl)) {
103
+ return discoveredUrl;
104
+ }
105
+ try {
106
+ const expectedUrl = await expectedLocalDataplaneBaseUrl();
107
+ const discoveredPort = localhostPortFromUrl(discoveredUrl);
108
+ const expectedPort = localhostPortFromUrl(expectedUrl);
109
+ if (
110
+ discoveredPort !== null &&
111
+ expectedPort !== null &&
112
+ discoveredPort !== expectedPort
113
+ ) {
114
+ return expectedUrl;
115
+ }
116
+ } catch {
117
+ // Keep controller-discovered URL when developer config is unavailable.
118
+ }
119
+ return discoveredUrl;
120
+ }
121
+
122
+ /**
123
+ * @returns {string|null}
124
+ */
125
+ function dataplaneUrlFromProcessEnv() {
126
+ const raw = process.env.DP || process.env.DATAPLANE_URL;
127
+ if (!raw || typeof raw !== 'string') {
128
+ return null;
129
+ }
130
+ return raw.trim().replace(/\/$/, '');
131
+ }
12
132
 
13
133
  /**
14
134
  * Resolve dataplane URL by discovering from controller
@@ -22,9 +142,16 @@ const { discoverDataplaneUrl } = require('../commands/wizard-dataplane');
22
142
  * @throws {Error} If dataplane URL cannot be resolved
23
143
  */
24
144
  async function resolveDataplaneUrl(controllerUrl, environment, authConfig, opts = {}) {
25
- return await discoverDataplaneUrl(controllerUrl, environment, authConfig, opts);
145
+ const envUrl = dataplaneUrlFromProcessEnv();
146
+ if (envUrl) {
147
+ return envUrl;
148
+ }
149
+ const discovered = await discoverDataplaneUrl(controllerUrl, environment, authConfig, opts);
150
+ return alignDiscoveredDataplaneUrl(discovered);
26
151
  }
27
152
 
28
153
  module.exports = {
29
- resolveDataplaneUrl
154
+ resolveDataplaneUrl,
155
+ expectedLocalDataplaneBaseUrl,
156
+ alignDiscoveredDataplaneUrl
30
157
  };
@@ -21,7 +21,7 @@ function parseDeveloperIdNum(raw) {
21
21
  }
22
22
 
23
23
  /**
24
- * Docker published host port: port + devId*100 (dev 0 → base port).
24
+ * Host port for compose publish, localhost URLs, and health checks: port + devId×100 (dev 0 → base port).
25
25
  * @param {number} appPort - Manifest listen port
26
26
  * @param {number} developerIdNum
27
27
  * @returns {number}
@@ -31,13 +31,13 @@ function publishedHostPort(appPort, developerIdNum) {
31
31
  }
32
32
 
33
33
  /**
34
- * Local workstation host port: port + 10 + devId*100 (dev 0 → port+10).
34
+ * Same as {@link publishedHostPort} (legacy name kept for call sites).
35
35
  * @param {number} appPort
36
36
  * @param {number} developerIdNum
37
37
  * @returns {number}
38
38
  */
39
39
  function localHostPort(appPort, developerIdNum) {
40
- return appPort + 10 + developerIdNum * 100;
40
+ return publishedHostPort(appPort, developerIdNum);
41
41
  }
42
42
 
43
43
  module.exports = {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Deployment auth mode for pipeline publish / external integration CLI.
3
+ * @fileoverview AIFABRIX_DEPLOYMENT_AUTH and options.deploymentAuth parsing
4
+ * @author AI Fabrix Team
5
+ * @version 1.0.0
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ * @typedef {'auto'|'client-credentials'|'device'} DeploymentAuthMode
12
+ */
13
+
14
+ /**
15
+ * @param {Object} [options] - CLI or caller options
16
+ * @returns {DeploymentAuthMode}
17
+ */
18
+ function getDeploymentAuthMode(options = {}) {
19
+ const raw = options.deploymentAuth || process.env.AIFABRIX_DEPLOYMENT_AUTH || 'auto';
20
+ const mode = String(raw).trim().toLowerCase().replace(/_/g, '-');
21
+ if (mode === 'client-credentials' || mode === 'client') {
22
+ return 'client-credentials';
23
+ }
24
+ if (mode === 'device' || mode === 'device-token' || mode === 'bearer') {
25
+ return 'device';
26
+ }
27
+ return 'auto';
28
+ }
29
+
30
+ /**
31
+ * @param {DeploymentAuthMode} mode
32
+ * @returns {boolean}
33
+ */
34
+ function shouldUseDeviceTokenForDeployment(mode) {
35
+ return mode !== 'client-credentials';
36
+ }
37
+
38
+ module.exports = {
39
+ getDeploymentAuthMode,
40
+ shouldUseDeviceTokenForDeployment
41
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Developer ID suffix variants for Docker volume names (`dev6` vs `dev06`).
3
+ *
4
+ * @fileoverview Volume name suffix normalization for multi-dev Docker
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * @param {string|number} developerId
13
+ * @returns {string[]}
14
+ */
15
+ function collectDeveloperVolumeIdSuffixes(developerId) {
16
+ const raw = String(developerId).trim();
17
+ const idNum = parseInt(raw, 10);
18
+ if (Number.isNaN(idNum) || idNum === 0) {
19
+ return ['0'];
20
+ }
21
+ const suffixes = new Set([String(idNum), raw]);
22
+ if (raw.length === 1) {
23
+ suffixes.add(raw.padStart(2, '0'));
24
+ }
25
+ if (String(idNum).length === 1) {
26
+ suffixes.add(String(idNum).padStart(2, '0'));
27
+ }
28
+ return [...suffixes];
29
+ }
30
+
31
+ module.exports = {
32
+ collectDeveloperVolumeIdSuffixes
33
+ };
@@ -272,14 +272,16 @@ async function patchEnvContentForLocal(envContent, variables) {
272
272
  * @param {string} outputPath - Resolved `build.envOutputPath`
273
273
  * @param {string} envOutputPathLabel - Label for log (e.g. raw `build.envOutputPath` value)
274
274
  */
275
- async function syncWrittenBuilderEnvToOutputPath(envPath, outputPath, envOutputPathLabel) {
275
+ async function syncWrittenBuilderEnvToOutputPath(envPath, outputPath, envOutputPathLabel, copyOptions = {}) {
276
276
  const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
277
277
  const raw = fs.readFileSync(envPath, 'utf8');
278
278
  const synced = substituteMntDataForLocal(raw, outputPath);
279
279
  let toWrite = synced;
280
- if (fs.existsSync(outputPath)) {
280
+ if (!copyOptions.freshEnv && fs.existsSync(outputPath)) {
281
281
  const existingContent = fs.readFileSync(outputPath, 'utf8');
282
- toWrite = mergeEnvMapIntoContent(existingContent, parseEnvContentToMap(synced));
282
+ toWrite = mergeEnvMapIntoContent(existingContent, parseEnvContentToMap(synced), {
283
+ appendMissingFromNewMap: true
284
+ });
283
285
  }
284
286
  fs.writeFileSync(outputPath, toWrite, { mode: 0o600 });
285
287
  logger.log(formatSuccessLine(`Copied resolved .env to envOutputPath (${envOutputPathLabel})`));
@@ -300,10 +302,10 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
300
302
  let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false, genOpts);
301
303
  localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath);
302
304
  let toWrite = localEnvContent;
303
- if (fs.existsSync(outputPath)) {
305
+ if (!extraOpts.freshEnv && fs.existsSync(outputPath)) {
304
306
  const existingContent = fs.readFileSync(outputPath, 'utf8');
305
307
  const localMap = parseEnvContentToMap(localEnvContent);
306
- toWrite = mergeEnvMapIntoContent(existingContent, localMap);
308
+ toWrite = mergeEnvMapIntoContent(existingContent, localMap, { appendMissingFromNewMap: true });
307
309
  }
308
310
  fs.writeFileSync(outputPath, toWrite, { mode: 0o600 });
309
311
  logger.log(formatSuccessLine(`Generated local .env at: ${envOutputPathLabel}`));
@@ -350,15 +352,16 @@ async function copyOrRegenerateEnvForNamedApp(opts) {
350
352
  secretsPath,
351
353
  label,
352
354
  preferLocal,
353
- appPathForLocal
355
+ appPathForLocal,
356
+ freshEnv
354
357
  } = opts;
355
- const localOpts = { appPath: appPathForLocal || undefined };
358
+ const localOpts = { appPath: appPathForLocal || undefined, freshEnv: freshEnv === true };
356
359
  if (preferLocal) {
357
360
  await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);
358
361
  return;
359
362
  }
360
363
  if (fs.existsSync(envPath)) {
361
- await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label);
364
+ await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label, { freshEnv: freshEnv === true });
362
365
  return;
363
366
  }
364
367
  await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);
@@ -386,6 +389,7 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath,
386
389
  (variablesPath ? path.dirname(variablesPath) : null);
387
390
  const preferLocal =
388
391
  copyOptions && typeof copyOptions === 'object' && copyOptions.preferLocalEnvOutputPath === true;
392
+ const freshEnv = copyOptions && typeof copyOptions === 'object' && copyOptions.freshEnv === true;
389
393
  if (appName) {
390
394
  await copyOrRegenerateEnvForNamedApp({
391
395
  envPath,
@@ -394,7 +398,8 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath,
394
398
  secretsPath,
395
399
  label,
396
400
  preferLocal,
397
- appPathForLocal
401
+ appPathForLocal,
402
+ freshEnv
398
403
  });
399
404
  } else {
400
405
  await writePatchedEnvToOutputPath(envPath, outputPath, variables, label);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Comment active KV_* env lines that are not in the expected key set (stale auth vars).
3
+ *
4
+ * @fileoverview Shared orphan KV line handling for env.template repair and resolve merge
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * Comments active KV_* lines whose env key is not in the expected set.
13
+ *
14
+ * @param {string} content - .env or env.template content
15
+ * @param {Set<string>|Map<string, unknown>} expectedKeys - Keys that should stay active
16
+ * @returns {{ content: string, changed: boolean }}
17
+ */
18
+ function commentOrphanKvEnvLines(content, expectedKeys) {
19
+ const expected =
20
+ expectedKeys instanceof Set ? expectedKeys : new Set(expectedKeys.keys());
21
+ const changedRef = { value: false };
22
+ const out = (content || '').split(/\r?\n/).map((line) => {
23
+ const trimmed = line.trim();
24
+ if (!trimmed || trimmed.startsWith('#')) {
25
+ return line;
26
+ }
27
+ const eq = line.indexOf('=');
28
+ if (eq <= 0 || !/^KV_/i.test(trimmed)) {
29
+ return line;
30
+ }
31
+ const key = line.substring(0, eq).trim();
32
+ if (expected.has(key)) {
33
+ return line;
34
+ }
35
+ changedRef.value = true;
36
+ return `${line.match(/^\s*/)[0]}# ${trimmed}`;
37
+ });
38
+ const joined = out.join('\n');
39
+ const normalized = joined.length > 0 && !joined.endsWith('\n') ? `${joined}\n` : joined;
40
+ return { content: normalized, changed: changedRef.value };
41
+ }
42
+
43
+ module.exports = { commentOrphanKvEnvLines };
@@ -10,6 +10,7 @@ const { formatBlockingError } = require('../cli-test-layout-chalk');
10
10
  */
11
11
 
12
12
  const chalk = require('chalk');
13
+ const { serviceLabelDisplayName } = require('../service-url-label');
13
14
 
14
15
  /**
15
16
  * Normalizes error message to string
@@ -56,9 +57,16 @@ function addControllerUrlToMessage(lines, errorData) {
56
57
  * @param {Object} errorData - Error response data
57
58
  */
58
59
  function formatConnectionRefusedError(lines, errorData) {
59
- lines.push(chalk.yellow('Cannot connect to controller.'));
60
+ const label = serviceLabelDisplayName(errorData?.serviceLabel || 'service');
61
+ lines.push(chalk.yellow(`Cannot connect to ${label}.`));
60
62
  addControllerUrlToMessage(lines, errorData);
61
- lines.push(chalk.gray('Check if the controller is running.'));
63
+ if (label === 'dataplane') {
64
+ lines.push(chalk.gray('Check if the dataplane is running (e.g. aifabrix run dataplane --reload).'));
65
+ } else if (label === 'controller') {
66
+ lines.push(chalk.gray('Check if the controller is running.'));
67
+ } else {
68
+ lines.push(chalk.gray('Check the service URL and network.'));
69
+ }
62
70
  }
63
71
 
64
72
  /**
@@ -68,9 +76,10 @@ function formatConnectionRefusedError(lines, errorData) {
68
76
  * @param {Object} errorData - Error response data
69
77
  */
70
78
  function formatHostnameNotFoundError(lines, errorData) {
71
- lines.push(chalk.yellow('Controller hostname not found.'));
79
+ const label = serviceLabelDisplayName(errorData?.serviceLabel || 'service');
80
+ lines.push(chalk.yellow(`${label.charAt(0).toUpperCase()}${label.slice(1)} hostname not found.`));
72
81
  addControllerUrlToMessage(lines, errorData);
73
- lines.push(chalk.gray('Check your controller URL.'));
82
+ lines.push(chalk.gray(`Check your ${label} URL (see aifabrix auth status).`));
74
83
  }
75
84
 
76
85
  /**
@@ -43,6 +43,7 @@ const FALLBACK_SECURITY_BY_AUTH = {
43
43
  aad: ['clientId', 'clientSecret'],
44
44
  apikey: ['apiKey'],
45
45
  apiKey: ['apiKey'],
46
+ bearertoken: ['token'],
46
47
  basic: ['username', 'password'],
47
48
  queryParam: ['paramValue'],
48
49
  oidc: [],
@@ -28,6 +28,15 @@ function isPublicationResultShape(obj) {
28
28
  return !!(obj && typeof obj === 'object' && (obj.uploadId || obj.system) && Array.isArray(obj.datasources));
29
29
  }
30
30
 
31
+ /**
32
+ * True when dataplane skipped publish because manifest matches running config.
33
+ * @param {*} publication - Parsed PublicationResult
34
+ * @returns {boolean}
35
+ */
36
+ function isPublicationUnchanged(publication) {
37
+ return !!(publication && publication.uploadStatus === 'unchanged');
38
+ }
39
+
31
40
  /**
32
41
  * Returns publication payload from upload API response.
33
42
  * @param {*} res - Response from uploadApplicationViaPipeline
@@ -432,6 +441,7 @@ module.exports = {
432
441
  unwrapApiData,
433
442
  unwrapPublicationResult,
434
443
  isPublicationResultShape,
444
+ isPublicationUnchanged,
435
445
  classifyDatasourceTierADetail,
436
446
  classifyDatasourceTierA,
437
447
  formatTierAPartialHint,