@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
@@ -5,7 +5,7 @@
5
5
  * Use getContainerPort for container/Docker/deployment/registration; use getLocalPort
6
6
  * for local .env and dev-id–adjusted host port.
7
7
  *
8
- * @fileoverview Port resolution from variables (port, build.containerPort, build.localPort)
8
+ * @fileoverview Port resolution from variables (port, build.containerPort)
9
9
  * @author AI Fabrix Team
10
10
  * @version 2.0.0
11
11
  */
@@ -17,8 +17,8 @@ const yaml = require('js-yaml');
17
17
 
18
18
  /**
19
19
  * Resolve container port from variables object.
20
- * Precedence: build.containerPort → port → defaultPort.
21
- * Used for: Dockerfile, container .env PORT, compose, deployment, app register, variable-transformer, builders, secrets-utils.
20
+ * Precedence: build.containerPort (if set and non-empty) → port (main port) → defaultPort.
21
+ * When containerPort is empty or missing, main port is used (e.g. keycloak 8082:8080 vs miso 3000:3000).
22
22
  *
23
23
  * @param {Object} variables - Parsed application config (or subset with build, port)
24
24
  * @param {number} [defaultPort=3000] - Default when neither build.containerPort nor port is set
@@ -26,13 +26,19 @@ const yaml = require('js-yaml');
26
26
  */
27
27
  function getContainerPort(variables, defaultPort = 3000) {
28
28
  const v = variables || {};
29
- return v.build?.containerPort ?? v.port ?? defaultPort;
29
+ const containerPort = v.build?.containerPort;
30
+ const useMain = containerPort === undefined || containerPort === null ||
31
+ (typeof containerPort === 'string' && containerPort.trim() === '');
32
+ if (!useMain && typeof containerPort === 'number' && containerPort > 0) {
33
+ return containerPort;
34
+ }
35
+ return v.port ?? defaultPort;
30
36
  }
31
37
 
32
38
  /**
33
39
  * Resolve local (development) port from variables object.
34
- * Precedence: build.localPort (if number and > 0) → port → defaultPort.
35
- * Used for: env-copy, env-ports, and as base for getLocalPortFromPath (secrets-helpers).
40
+ * Precedence: build.localPort (when positive integer) → port → defaultPort.
41
+ * Used for env-copy, env-ports, getLocalPortFromPath, run compose host port.
36
42
  *
37
43
  * @param {Object} variables - Parsed application config
38
44
  * @param {number} [defaultPort=3000] - Default when neither build.localPort nor port is set
@@ -40,9 +46,9 @@ function getContainerPort(variables, defaultPort = 3000) {
40
46
  */
41
47
  function getLocalPort(variables, defaultPort = 3000) {
42
48
  const v = variables || {};
43
- const local = v.build?.localPort;
44
- if (typeof local === 'number' && local > 0) {
45
- return local;
49
+ const localPort = v.build?.localPort;
50
+ if (typeof localPort === 'number' && localPort > 0) {
51
+ return localPort;
46
52
  }
47
53
  return v.port ?? defaultPort;
48
54
  }
@@ -67,7 +73,7 @@ function loadVariablesFromPath(variablesPath) {
67
73
 
68
74
  /**
69
75
  * Resolve container port from application config path.
70
- * Returns null when file is missing or neither build.containerPort nor port is set (for chaining with other sources).
76
+ * When containerPort is empty or missing, returns main port. Null only when file missing or no port set.
71
77
  *
72
78
  * @param {string} variablesPath - Path to application config
73
79
  * @returns {number|null} Container port or null
@@ -77,14 +83,20 @@ function getContainerPortFromPath(variablesPath) {
77
83
  if (!v) {
78
84
  return null;
79
85
  }
80
- const p = v.build?.containerPort ?? v.port;
86
+ const containerPort = v.build?.containerPort;
87
+ const useMain = containerPort === undefined || containerPort === null ||
88
+ (typeof containerPort === 'string' && containerPort.trim() === '');
89
+ if (!useMain && typeof containerPort === 'number' && containerPort > 0) {
90
+ return containerPort;
91
+ }
92
+ const p = v.port;
81
93
  return (p !== undefined && p !== null) ? p : null;
82
94
  }
83
95
 
84
96
  /**
85
97
  * Resolve local port from application config path.
86
- * Matches legacy getPortFromVariablesFile: build.localPort (if number and > 0) else variables.port or null.
87
- * Returns null when file is missing or neither is set (for calculateAppPort chain).
98
+ * Same rule as getLocalPort: build.localPort (when positive integer) port.
99
+ * Returns null when file is missing or neither localPort nor port is set.
88
100
  *
89
101
  * @param {string} variablesPath - Path to application config
90
102
  * @returns {number|null} Local port or null
@@ -94,9 +106,9 @@ function getLocalPortFromPath(variablesPath) {
94
106
  if (!v) {
95
107
  return null;
96
108
  }
97
- const local = v.build?.localPort;
98
- if (typeof local === 'number' && local > 0) {
99
- return local;
109
+ const localPort = v.build?.localPort;
110
+ if (typeof localPort === 'number' && localPort > 0) {
111
+ return localPort;
100
112
  }
101
113
  const p = v.port;
102
114
  return (p !== undefined && p !== null) ? p : null;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @fileoverview Resolve Builder Server URL and client cert for cert-authenticated dev API calls
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const config = require('../core/config');
8
+ const { getCertDir, readClientCertPem } = require('./dev-cert-helper');
9
+ const { getConfigDirForPaths } = require('./paths');
10
+
11
+ /**
12
+ * Check if a string is an http(s) URL (for aifabrix-secrets remote mode).
13
+ * @param {string} value - Config value
14
+ * @returns {boolean}
15
+ */
16
+ function isRemoteSecretsUrl(value) {
17
+ return typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'));
18
+ }
19
+
20
+ /**
21
+ * Get Builder Server URL and client cert PEM when remote is configured; otherwise null.
22
+ * Use for cert-authenticated dev API calls (settings, users, ssh-keys, secrets).
23
+ * @returns {Promise<{ serverUrl: string, clientCertPem: string }|null>}
24
+ */
25
+ async function getRemoteDevAuth() {
26
+ const serverUrl = await config.getRemoteServer();
27
+ if (!serverUrl) return null;
28
+ const devId = await config.getDeveloperId();
29
+ const certDir = getCertDir(getConfigDirForPaths(), devId);
30
+ const clientCertPem = readClientCertPem(certDir);
31
+ if (!clientCertPem) return null;
32
+ return { serverUrl, clientCertPem };
33
+ }
34
+
35
+ module.exports = {
36
+ isRemoteSecretsUrl,
37
+ getRemoteDevAuth
38
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Remote Docker environment – DOCKER_HOST and TLS cert path when docker-endpoint is set.
3
+ *
4
+ * @fileoverview Env overlay for Docker CLI when using remote Builder Server
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const path = require('path');
10
+ const config = require('../core/config');
11
+ const { getCertDir } = require('./dev-cert-helper');
12
+ const { getConfigDirForPaths } = require('./paths');
13
+
14
+ /**
15
+ * If remote Docker is configured (docker-endpoint + cert.pem, key.pem, and ca.pem present),
16
+ * returns env vars for Docker CLI: DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH.
17
+ * Docker requires ca.pem in DOCKER_CERT_PATH for TLS; if it is missing we return {} so
18
+ * the CLI uses local Docker and avoids "open ca.pem: no such file or directory".
19
+ *
20
+ * @returns {Promise<Object>} Env overlay (may be empty)
21
+ */
22
+ async function getRemoteDockerEnv() {
23
+ const endpoint = await config.getDockerEndpoint();
24
+ if (!endpoint || typeof endpoint !== 'string' || !endpoint.trim()) {
25
+ return {};
26
+ }
27
+ const devId = await config.getDeveloperId();
28
+ const certDir = getCertDir(getConfigDirForPaths(), devId);
29
+ const certPath = path.join(certDir, 'cert.pem');
30
+ const keyPath = path.join(certDir, 'key.pem');
31
+ const caPath = path.join(certDir, 'ca.pem');
32
+ const fs = require('fs');
33
+ if (!fs.existsSync(certPath) || !fs.existsSync(keyPath) || !fs.existsSync(caPath)) {
34
+ return {};
35
+ }
36
+ return {
37
+ DOCKER_HOST: endpoint.trim(),
38
+ DOCKER_TLS_VERIFY: '1',
39
+ DOCKER_CERT_PATH: certDir
40
+ };
41
+ }
42
+
43
+ module.exports = { getRemoteDockerEnv };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Load shared secrets from Builder Server when aifabrix-secrets is an http(s) URL.
3
+ * Used for .env resolution only; values are never persisted to disk.
4
+ *
5
+ * @fileoverview Remote shared secrets loader for .env generation
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const config = require('../core/config');
11
+
12
+ /**
13
+ * Fetches shared secrets from Builder Server when aifabrix-secrets is an http(s) URL.
14
+ * @returns {Promise<Object|null>} Key-value secrets from API or null
15
+ */
16
+ async function loadRemoteSharedSecrets() {
17
+ const { isRemoteSecretsUrl, getRemoteDevAuth } = require('./remote-dev-auth');
18
+ const devApi = require('../api/dev.api');
19
+ const configSecretsPath = await config.getSecretsPath();
20
+ if (!configSecretsPath || !isRemoteSecretsUrl(configSecretsPath)) {
21
+ return null;
22
+ }
23
+ const auth = await getRemoteDevAuth();
24
+ if (!auth) return null;
25
+ try {
26
+ const items = await devApi.listSecrets(auth.serverUrl, auth.clientCertPem);
27
+ if (!Array.isArray(items)) return null;
28
+ const obj = {};
29
+ for (const item of items) {
30
+ if (item && typeof item.name === 'string' && item.value !== undefined) {
31
+ obj[item.name] = String(item.value);
32
+ }
33
+ }
34
+ return obj;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Merges remote shared secrets with user secrets. User wins on same key.
42
+ * @param {Object} userSecrets - User secrets object
43
+ * @param {Object} remoteSecrets - Remote API secrets (key-value)
44
+ * @returns {Object} Merged object
45
+ */
46
+ function mergeUserWithRemoteSecrets(userSecrets, remoteSecrets) {
47
+ const merged = { ...userSecrets };
48
+ if (!remoteSecrets || typeof remoteSecrets !== 'object') return merged;
49
+ for (const key of Object.keys(remoteSecrets)) {
50
+ if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
51
+ merged[key] = remoteSecrets[key];
52
+ }
53
+ }
54
+ return merged;
55
+ }
56
+
57
+ module.exports = {
58
+ loadRemoteSharedSecrets,
59
+ mergeUserWithRemoteSecrets
60
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Canonical secrets path and YAML helpers
3
+ *
4
+ * @fileoverview Read/merge canonical secrets from config path
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const yaml = require('js-yaml');
14
+ const config = require('../core/config');
15
+
16
+ /**
17
+ * Read a YAML file and return parsed object
18
+ * @function readYamlAtPath
19
+ * @param {string} filePath - Absolute file path
20
+ * @returns {Object} Parsed YAML object
21
+ */
22
+ function readYamlAtPath(filePath) {
23
+ const content = fs.readFileSync(filePath, 'utf8');
24
+ return yaml.load(content);
25
+ }
26
+
27
+ /**
28
+ * Merge a single secret value from canonical into result
29
+ * @function mergeSecretValue
30
+ * @param {Object} result - Result object to merge into
31
+ * @param {string} key - Secret key
32
+ * @param {*} canonicalValue - Value from canonical secrets
33
+ */
34
+ function mergeSecretValue(result, key, canonicalValue) {
35
+ const currentValue = result[key];
36
+ // Fill missing, empty, or undefined values
37
+ if (!(key in result) || currentValue === undefined || currentValue === null || currentValue === '') {
38
+ result[key] = canonicalValue;
39
+ return;
40
+ }
41
+ // Only replace values that are encrypted (have secure:// prefix)
42
+ // Plaintext values (no secure://) are used as-is
43
+ if (typeof currentValue === 'string' && typeof canonicalValue === 'string') {
44
+ if (currentValue.startsWith('secure://')) {
45
+ result[key] = canonicalValue;
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Apply canonical secrets path override if configured and file exists
52
+ * @async
53
+ * @function applyCanonicalSecretsOverride
54
+ * @param {Object} currentSecrets - Current secrets map
55
+ * @returns {Promise<Object>} Possibly overridden secrets
56
+ */
57
+ async function applyCanonicalSecretsOverride(currentSecrets) {
58
+ let mergedSecrets = currentSecrets || {};
59
+ try {
60
+ const canonicalPath = await config.getSecretsPath();
61
+ if (!canonicalPath) {
62
+ return mergedSecrets;
63
+ }
64
+ const resolvedCanonical = path.isAbsolute(canonicalPath)
65
+ ? canonicalPath
66
+ : path.resolve(process.cwd(), canonicalPath);
67
+ if (!fs.existsSync(resolvedCanonical)) {
68
+ return mergedSecrets;
69
+ }
70
+ const configSecrets = readYamlAtPath(resolvedCanonical);
71
+ if (!configSecrets || typeof configSecrets !== 'object') {
72
+ return mergedSecrets;
73
+ }
74
+ // Apply canonical secrets as a fallback source:
75
+ // - Do NOT override any existing keys from user/build
76
+ // - Add only missing keys from canonical path
77
+ // - Also fill in empty/undefined values from canonical path
78
+ // - Replace encrypted values (secure://) with canonical plaintext
79
+ const result = { ...mergedSecrets };
80
+ for (const [key, canonicalValue] of Object.entries(configSecrets)) {
81
+ mergeSecretValue(result, key, canonicalValue);
82
+ }
83
+ mergedSecrets = result;
84
+ } catch {
85
+ // ignore and fall through
86
+ }
87
+ return mergedSecrets;
88
+ }
89
+
90
+ module.exports = {
91
+ readYamlAtPath,
92
+ applyCanonicalSecretsOverride
93
+ };
@@ -17,6 +17,54 @@ const crypto = require('crypto');
17
17
  const logger = require('./logger');
18
18
  const pathsUtil = require('./paths');
19
19
 
20
+ /**
21
+ * Parse key-value pairs from YAML-like lines (last occurrence wins per key).
22
+ * @param {string} content - Raw YAML content
23
+ * @returns {Object} Parsed object
24
+ */
25
+ function parseYamlKeyValueLines(content) {
26
+ const result = {};
27
+ const keyValueRe = /^\s*([^#:]+):\s*(.*)$/;
28
+ const lines = content.split(/\r?\n/);
29
+ for (const line of lines) {
30
+ const trimmed = line.trim();
31
+ if (trimmed === '' || trimmed.startsWith('#')) continue;
32
+ const m = line.match(keyValueRe);
33
+ if (!m) continue;
34
+ const key = m[1].trim();
35
+ let value = m[2].trim();
36
+ if ((value.startsWith('\'') && value.endsWith('\'')) || (value.startsWith('"') && value.endsWith('"'))) {
37
+ value = value.slice(1, -1).replace(/\\'/g, '\'').replace(/\\"/g, '"');
38
+ }
39
+ result[key] = value;
40
+ }
41
+ return result;
42
+ }
43
+
44
+ /**
45
+ * Parse YAML content tolerating duplicate keys (last occurrence wins).
46
+ * Use for secrets files that may have been appended to repeatedly.
47
+ * Tries yaml.load first; on "duplicate key" error falls back to line-by-line parse.
48
+ *
49
+ * @param {string} content - Raw YAML content
50
+ * @returns {Object} Parsed object (last value wins for duplicate keys)
51
+ */
52
+ function loadYamlTolerantOfDuplicateKeys(content) {
53
+ if (!content || typeof content !== 'string') {
54
+ return {};
55
+ }
56
+ try {
57
+ const parsed = yaml.load(content);
58
+ return parsed && typeof parsed === 'object' ? parsed : {};
59
+ } catch (err) {
60
+ const msg = err.message || '';
61
+ if (!msg.includes('duplicate') && !msg.includes('duplicated mapping')) {
62
+ throw err;
63
+ }
64
+ }
65
+ return parseYamlKeyValueLines(content);
66
+ }
67
+
20
68
  /**
21
69
  * Skips commented or empty lines when scanning env.template
22
70
  * @param {string} line - Single line
@@ -127,7 +175,7 @@ function loadExistingSecrets(resolvedPath) {
127
175
 
128
176
  try {
129
177
  const content = fs.readFileSync(resolvedPath, 'utf8');
130
- const secrets = yaml.load(content) || {};
178
+ const secrets = loadYamlTolerantOfDuplicateKeys(content);
131
179
  return typeof secrets === 'object' ? secrets : {};
132
180
  } catch (error) {
133
181
  logger.warn(`Warning: Could not read existing secrets file: ${error.message}`);
@@ -136,7 +184,7 @@ function loadExistingSecrets(resolvedPath) {
136
184
  }
137
185
 
138
186
  /**
139
- * Saves secrets file
187
+ * Saves secrets file (full overwrite). Use appendSecretsToFile to add keys without changing existing content.
140
188
  * @function saveSecretsFile
141
189
  * @param {string} resolvedPath - Path to secrets file
142
190
  * @param {Object} secrets - Secrets object to save
@@ -158,6 +206,64 @@ function saveSecretsFile(resolvedPath, secrets) {
158
206
  fs.writeFileSync(resolvedPath, yamlContent, { mode: 0o600 });
159
207
  }
160
208
 
209
+ const YAML_DUMP_OPTS = { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false };
210
+
211
+ /**
212
+ * Merges secret keys into the secrets file (load existing, merge, overwrite file).
213
+ * Use when setting or updating keys so that existing keys are updated in place instead of duplicated.
214
+ * Creates the file if it does not exist. Tolerant of duplicate keys in existing file (last wins when loading).
215
+ *
216
+ * @function mergeSecretsIntoFile
217
+ * @param {string} resolvedPath - Path to secrets file
218
+ * @param {Object} secrets - Key-value object to merge (overwrites existing same keys)
219
+ * @throws {Error} If write fails
220
+ */
221
+ function mergeSecretsIntoFile(resolvedPath, secrets) {
222
+ if (!secrets || typeof secrets !== 'object' || Object.keys(secrets).length === 0) {
223
+ return;
224
+ }
225
+ const existing = loadExistingSecrets(resolvedPath);
226
+ const merged = { ...existing, ...secrets };
227
+ saveSecretsFile(resolvedPath, merged);
228
+ }
229
+
230
+ /**
231
+ * Appends secret keys to the end of the secrets file without modifying existing content (preserves comments and structure).
232
+ * Creates the file if it does not exist. For existing files, new keys are appended.
233
+ * When the file has duplicate keys, use loadExistingSecrets (tolerant parse) to read; last occurrence wins.
234
+ *
235
+ * @function appendSecretsToFile
236
+ * @param {string} resolvedPath - Path to secrets file
237
+ * @param {Object} secrets - Key-value object to append (only these keys are written)
238
+ * @throws {Error} If write fails
239
+ */
240
+ function appendSecretsToFile(resolvedPath, secrets) {
241
+ if (!secrets || typeof secrets !== 'object' || Object.keys(secrets).length === 0) {
242
+ return;
243
+ }
244
+ const dir = path.dirname(resolvedPath);
245
+ if (!fs.existsSync(dir)) {
246
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
247
+ }
248
+
249
+ const appendContent = yaml.dump(secrets, YAML_DUMP_OPTS);
250
+
251
+ if (!fs.existsSync(resolvedPath)) {
252
+ fs.writeFileSync(resolvedPath, appendContent, { mode: 0o600 });
253
+ return;
254
+ }
255
+
256
+ let existing = '';
257
+ try {
258
+ const raw = fs.readFileSync(resolvedPath, 'utf8');
259
+ existing = typeof raw === 'string' ? raw : '';
260
+ } catch (err) {
261
+ logger.warn(`Could not read existing secrets file: ${err.message}; appending new keys only.`);
262
+ }
263
+ const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
264
+ fs.writeFileSync(resolvedPath, existing + separator + appendContent, { mode: 0o600 });
265
+ }
266
+
161
267
  /**
162
268
  * Generates missing secret keys in secrets file
163
269
  * Scans env.template for kv:// references and adds missing keys with secure defaults
@@ -188,8 +294,7 @@ async function generateMissingSecrets(envTemplate, secretsPath) {
188
294
  newSecrets[key] = generateSecretValue(key);
189
295
  }
190
296
 
191
- const updatedSecrets = { ...existingSecrets, ...newSecrets };
192
- saveSecretsFile(resolvedPath, updatedSecrets);
297
+ appendSecretsToFile(resolvedPath, newSecrets);
193
298
 
194
299
  logger.log(`✓ Generated ${missingKeys.length} missing secret key(s): ${missingKeys.join(', ')}`);
195
300
  return missingKeys;
@@ -227,11 +332,11 @@ postgres-passwordKeyVault: "admin123"
227
332
 
228
333
  # Redis Secrets
229
334
  redis-passwordKeyVault: ""
230
- redis-urlKeyVault: "redis://\${REDIS_HOST}:\${REDIS_PORT}"
335
+ redis-url: "redis://\${REDIS_HOST}:\${REDIS_PORT}"
231
336
 
232
337
  # Keycloak Secrets
233
338
  keycloak-admin-passwordKeyVault: "admin123"
234
- keycloak-auth-server-urlKeyVault: "http://\${KEYCLOAK_HOST}:\${KEYCLOAK_PORT}"
339
+ keycloak-server-url: "http://\${KEYCLOAK_HOST}:\${KEYCLOAK_PORT}"
235
340
  `;
236
341
 
237
342
  fs.writeFileSync(resolvedPath, defaultSecrets, { mode: 0o600 });
@@ -240,8 +345,11 @@ keycloak-auth-server-urlKeyVault: "http://\${KEYCLOAK_HOST}:\${KEYCLOAK_PORT}"
240
345
  module.exports = {
241
346
  findMissingSecretKeys,
242
347
  generateSecretValue,
348
+ loadYamlTolerantOfDuplicateKeys,
243
349
  loadExistingSecrets,
244
350
  saveSecretsFile,
351
+ mergeSecretsIntoFile,
352
+ appendSecretsToFile,
245
353
  generateMissingSecrets,
246
354
  createDefaultSecrets
247
355
  };