@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.
- package/.cursor/rules/docs-rules.mdc +30 -0
- package/README.md +7 -5
- package/integration/hubspot/README.md +8 -4
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +2 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +34 -1
- package/lib/app/config.js +44 -11
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +12 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +9 -6
- package/lib/app/run-env-compose.js +264 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show-display.js +1 -1
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +172 -15
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +206 -16
- package/lib/cli/setup-environment.js +16 -6
- package/lib/cli/setup-external-system.js +89 -24
- package/lib/cli/setup-infra.js +82 -15
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +129 -24
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +347 -0
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +507 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +96 -40
- package/lib/commands/wizard-core-helpers.js +226 -4
- package/lib/commands/wizard-core.js +67 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +44 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +86 -64
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/config.js +7 -1
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +176 -89
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/deployer.js +7 -5
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +188 -203
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +56 -19
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +1 -1
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +177 -25
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +294 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +88 -0
- package/lib/generator/wizard.js +155 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +98 -12
- package/lib/infrastructure/services.js +88 -22
- package/lib/schema/application-schema.json +32 -8
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +509 -411
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +41 -13
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +77 -76
- package/lib/utils/compose-handlebars-helpers.js +54 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +357 -0
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +103 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -2
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +56 -29
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +16 -2
- package/lib/utils/infra-status.js +80 -45
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +128 -37
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +114 -6
- package/lib/utils/secrets-helpers.js +108 -114
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +72 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +8 -3
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +6 -5
- package/templates/applications/dataplane/env.template +15 -10
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +12 -10
- package/templates/external-system/README.md.hbs +65 -25
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
- 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
|
|
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
|
-
*
|
|
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
|
-
|
|
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 (
|
|
35
|
-
* Used for
|
|
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
|
|
44
|
-
if (typeof
|
|
45
|
-
return
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
87
|
-
* Returns null when file is missing or neither
|
|
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
|
|
98
|
-
if (typeof
|
|
99
|
-
return
|
|
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 =
|
|
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
|
-
|
|
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-
|
|
335
|
+
redis-url: "redis://\${REDIS_HOST}:\${REDIS_PORT}"
|
|
231
336
|
|
|
232
337
|
# Keycloak Secrets
|
|
233
338
|
keycloak-admin-passwordKeyVault: "admin123"
|
|
234
|
-
keycloak-
|
|
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
|
};
|