@aifabrix/builder 2.61.2 → 2.63.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 (90) hide show
  1. package/lib/api/applications.api.js +22 -2
  2. package/lib/api/bootstrap-snapshot.api.js +115 -0
  3. package/lib/api/onboarding-status.api.js +117 -0
  4. package/lib/app/deploy.js +14 -5
  5. package/lib/app/register.js +24 -2
  6. package/lib/app/run-env-compose.js +44 -8
  7. package/lib/app/run-env-recovery.js +1 -0
  8. package/lib/app/run-helpers.js +1 -1
  9. package/lib/app/run.js +6 -4
  10. package/lib/build/standard-docker-build.js +4 -0
  11. package/lib/channels/channel-artifact-generator.js +13 -3
  12. package/lib/cli/infra-guided-footers.js +10 -8
  13. package/lib/cli/infra-guided.js +60 -12
  14. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  15. package/lib/cli/setup-platform.js +20 -3
  16. package/lib/commands/auth-config.js +2 -0
  17. package/lib/commands/login.js +4 -0
  18. package/lib/commands/repair-datasource-apply.js +3 -1
  19. package/lib/commands/repair-datasource-legacy.js +94 -0
  20. package/lib/commands/setup-image-refresh.js +26 -0
  21. package/lib/commands/setup-modes.js +9 -2
  22. package/lib/commands/setup-onboarding-readiness.js +83 -0
  23. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  24. package/lib/commands/setup.js +35 -10
  25. package/lib/commands/up-builder-api.js +24 -8
  26. package/lib/commands/up-common.js +16 -1
  27. package/lib/commands/up-dataplane-bootstrap.js +149 -16
  28. package/lib/commands/up-dataplane-credentials.js +21 -0
  29. package/lib/commands/up-dataplane.js +26 -34
  30. package/lib/commands/up-integration-server.js +112 -0
  31. package/lib/commands/wizard-config-normalizer.js +5 -1
  32. package/lib/core/admin-secrets-env-overlay.js +8 -3
  33. package/lib/core/environment-access-policy.js +121 -0
  34. package/lib/core/environment-mode-policy.js +177 -0
  35. package/lib/core/local-env-overrides.js +149 -0
  36. package/lib/core/secrets-ensure.js +4 -2
  37. package/lib/core/secrets-env-content.js +12 -9
  38. package/lib/core/secrets-env-write.js +1 -1
  39. package/lib/datasource/capability/capability-manifest.js +1 -0
  40. package/lib/deployment/installation/index.js +14 -1
  41. package/lib/deployment/installation/infra-catalog.js +3 -0
  42. package/lib/deployment/installation/local-environment-stage.js +285 -0
  43. package/lib/deployment/installation/local-installation.js +76 -0
  44. package/lib/deployment/installation/stage-input.js +5 -1
  45. package/lib/generator/builders.js +3 -1
  46. package/lib/generator/index.js +32 -9
  47. package/lib/generator/split-variables.js +2 -2
  48. package/lib/parameters/database-adoption.js +125 -0
  49. package/lib/parameters/physical-database-naming.js +136 -0
  50. package/lib/parameters/physical-secret-name.js +194 -0
  51. package/lib/parameters/rsa-secret-values.js +165 -0
  52. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  53. package/lib/programmatic/run-channel-package.js +3 -1
  54. package/lib/programmatic/workspace-context.js +37 -2
  55. package/lib/schema/infra.parameter.yaml +61 -27
  56. package/lib/schema/infrastructure-schema.json +213 -120
  57. package/lib/utils/compose-generate-docker-compose.js +2 -2
  58. package/lib/utils/compose-generator.js +4 -7
  59. package/lib/utils/compose-miso-env.js +2 -2
  60. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  61. package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
  62. package/lib/utils/declarative-url-ports.js +3 -0
  63. package/lib/utils/env-environment-file-paths.js +105 -0
  64. package/lib/utils/environment-scoped-resources.js +48 -14
  65. package/lib/utils/log-redaction.js +4 -1
  66. package/lib/utils/paths-system-builder-keys.js +7 -1
  67. package/lib/utils/paths.js +7 -2
  68. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  69. package/lib/utils/redis-env-scope.js +21 -1
  70. package/lib/utils/registry-auth-sources.js +154 -0
  71. package/lib/utils/registry-credentials.js +131 -14
  72. package/lib/utils/secrets-generator.js +30 -21
  73. package/lib/utils/secrets-helpers.js +5 -7
  74. package/lib/utils/secrets-kv-scope.js +127 -28
  75. package/lib/utils/secrets-materialize-local.js +9 -0
  76. package/lib/utils/secrets-missing-error.js +35 -1
  77. package/lib/utils/token-manager.js +14 -8
  78. package/lib/utils/url-public-path-prefix.js +6 -10
  79. package/package.json +4 -2
  80. package/templates/README.md +6 -0
  81. package/templates/applications/builder-api/application.yaml +1 -1
  82. package/templates/applications/dataplane/application.yaml +2 -2
  83. package/templates/applications/dataplane/env.template +1 -1
  84. package/templates/applications/integration-server/application.yaml +91 -0
  85. package/templates/applications/integration-server/env.template +215 -0
  86. package/templates/applications/mirrored-templates.json +52 -0
  87. package/templates/applications/miso-controller/application.yaml +43 -1
  88. package/templates/applications/miso-controller/env.template +45 -39
  89. package/templates/marketplace/createUiDefinition.json +52 -4
  90. package/templates/marketplace/main.json +85 -7
@@ -22,7 +22,8 @@ const { isVectorDatabaseName } = require('./compose-vector-helper');
22
22
  const { resolveMisoEnvironment } = require('./compose-miso-env');
23
23
  const {
24
24
  buildScopedLocalContainerName,
25
- composeTraefikServiceKey
25
+ composeTraefikServiceKey,
26
+ isScopeActiveForOptions
26
27
  } = require('./environment-scoped-resources');
27
28
  const {
28
29
  derivePathFromPattern,
@@ -308,9 +309,7 @@ function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
308
309
  const hostPort = port;
309
310
  const useTraefikScope =
310
311
  scopeOpts &&
311
- scopeOpts.effectiveEnvironmentScopedResources &&
312
- scopeOpts.runEnvKey &&
313
- (scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst');
312
+ isScopeActiveForOptions(scopeOpts);
314
313
  const composeAppKey = useTraefikScope
315
314
  ? composeTraefikServiceKey(appName, scopeOpts.runEnvKey)
316
315
  : null;
@@ -380,9 +379,7 @@ function buildNetworkAndContainerNames(appName, devId, idNum, scopeOpts = null)
380
379
  let containerName;
381
380
  if (
382
381
  scopeOpts &&
383
- scopeOpts.effectiveEnvironmentScopedResources &&
384
- scopeOpts.runEnvKey &&
385
- (scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst')
382
+ isScopeActiveForOptions(scopeOpts)
386
383
  ) {
387
384
  containerName = buildScopedLocalContainerName(appName, devId, idNum, scopeOpts.runEnvKey);
388
385
  } else {
@@ -8,11 +8,11 @@
8
8
 
9
9
  /**
10
10
  * @param {Object} options - Run options
11
- * @returns {'dev'|'tst'|'pro'}
11
+ * @returns {'miso'|'dev'|'tst'|'pro'}
12
12
  */
13
13
  function resolveMisoEnvironment(options) {
14
14
  const env = (options.env && typeof options.env === 'string') ? options.env.toLowerCase() : 'dev';
15
- return (env === 'tst' || env === 'pro') ? env : 'dev';
15
+ return (env === 'miso' || env === 'tst' || env === 'pro') ? env : 'dev';
16
16
  }
17
17
 
18
18
  module.exports = { resolveMisoEnvironment };
@@ -7,7 +7,8 @@
7
7
  */
8
8
 
9
9
  const {
10
- buildEnvScopedTraefikPath
10
+ buildEnvScopedTraefikPath,
11
+ isScopeActiveForOptions
11
12
  } = require('./environment-scoped-resources');
12
13
  const { parseDeveloperIdNum } = require('./declarative-url-ports');
13
14
  const {
@@ -122,9 +123,7 @@ function buildTraefikIngressBase(config, devId, scopeOpts, remoteServer, develop
122
123
  let pathOut = basePath;
123
124
  if (
124
125
  scopeOpts &&
125
- scopeOpts.effectiveEnvironmentScopedResources &&
126
- scopeOpts.runEnvKey &&
127
- (scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst')
126
+ isScopeActiveForOptions(scopeOpts)
128
127
  ) {
129
128
  pathOut = buildEnvScopedTraefikPath(basePath, scopeOpts.runEnvKey);
130
129
  }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Resolve and materialize the Dataplane Dimension ABAC installer setting.
3
+ *
4
+ * @fileoverview Strict non-secret Dimension ABAC setting helpers
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const SETTING_NAME = 'DIMENSION_ABAC_ENFORCEMENT_ENABLED';
12
+ const CONFIG_KEY = 'dimensionAbacEnforcementEnabled';
13
+ const DATAPLANE_APP = 'dataplane';
14
+
15
+ function settingError(message) {
16
+ const error = new Error(message);
17
+ error.code = 'DIMENSION_ABAC_SETTING_INVALID';
18
+ return error;
19
+ }
20
+
21
+ function parseDimensionAbacOption(raw) {
22
+ if (raw === undefined || raw === null) return undefined;
23
+ if (raw === true || raw === false) return raw;
24
+ const value = String(raw).trim().toLowerCase();
25
+ if (value === 'on') return true;
26
+ if (value === 'off') return false;
27
+ throw settingError('Invalid --dimension-abac value. Use on or off.');
28
+ }
29
+
30
+ function readStoredValue(userConfig) {
31
+ const applications = userConfig && userConfig.applications;
32
+ if (applications === undefined || applications === null) return undefined;
33
+ if (typeof applications !== 'object' || Array.isArray(applications)) {
34
+ throw settingError('Invalid applications configuration; expected an object.');
35
+ }
36
+ const entry = applications[DATAPLANE_APP];
37
+ if (entry === undefined || entry === null) return undefined;
38
+ if (typeof entry !== 'object' || Array.isArray(entry)) {
39
+ throw settingError('Invalid applications.dataplane configuration; expected an object.');
40
+ }
41
+ if (!Object.prototype.hasOwnProperty.call(entry, CONFIG_KEY)) return undefined;
42
+ if (typeof entry[CONFIG_KEY] !== 'boolean') {
43
+ throw settingError(`Invalid applications.dataplane.${CONFIG_KEY}; expected a boolean.`);
44
+ }
45
+ return entry[CONFIG_KEY];
46
+ }
47
+
48
+ function resolveDimensionAbacSetting(userConfig, explicitValue) {
49
+ const stored = readStoredValue(userConfig);
50
+ if (explicitValue !== undefined && typeof explicitValue !== 'boolean') {
51
+ throw settingError('Explicit Dimension ABAC selection must be a boolean.');
52
+ }
53
+ if (explicitValue !== undefined) return { value: explicitValue, source: 'explicit' };
54
+ if (stored !== undefined) return { value: stored, source: 'stored' };
55
+ return { value: false, source: 'default' };
56
+ }
57
+
58
+ async function persistExplicitDimensionAbacSetting(configApi, value) {
59
+ if (value === undefined) return undefined;
60
+ if (!configApi || typeof configApi.getConfig !== 'function' || typeof configApi.saveConfig !== 'function') {
61
+ throw settingError('Configuration API is unavailable.');
62
+ }
63
+ if (typeof value !== 'boolean') {
64
+ throw settingError('Persisted Dimension ABAC selection must be a boolean.');
65
+ }
66
+ const userConfig = await configApi.getConfig();
67
+ readStoredValue(userConfig);
68
+ const applications = { ...(userConfig.applications || {}) };
69
+ const dataplane = { ...(applications[DATAPLANE_APP] || {}), [CONFIG_KEY]: value };
70
+ const nextConfig = { ...userConfig, applications: { ...applications, [DATAPLANE_APP]: dataplane } };
71
+ await configApi.saveConfig(nextConfig);
72
+ return nextConfig;
73
+ }
74
+
75
+ function toDimensionAbacEnvValue(value) {
76
+ if (typeof value !== 'boolean') {
77
+ throw settingError('Dimension ABAC value must be a boolean.');
78
+ }
79
+ return value ? 'true' : 'false';
80
+ }
81
+
82
+ function applyDimensionAbacToEnvMap(appName, envMap, value) {
83
+ if (appName !== DATAPLANE_APP) return envMap;
84
+ if (!envMap || typeof envMap !== 'object' || Array.isArray(envMap)) {
85
+ throw settingError('Dataplane environment map must be an object.');
86
+ }
87
+ envMap[SETTING_NAME] = toDimensionAbacEnvValue(value);
88
+ return envMap;
89
+ }
90
+
91
+ function applyDimensionAbacToConfiguration(appName, configuration, value) {
92
+ if (appName !== DATAPLANE_APP) return configuration;
93
+ if (!Array.isArray(configuration)) {
94
+ throw settingError('Dataplane deployment configuration must be an array.');
95
+ }
96
+ const matches = configuration.filter(item => item && item.name === SETTING_NAME);
97
+ if (matches.length > 1) {
98
+ throw settingError(`Dataplane deployment configuration contains duplicate ${SETTING_NAME} entries.`);
99
+ }
100
+ const entry = {
101
+ name: SETTING_NAME,
102
+ value: toDimensionAbacEnvValue(value),
103
+ location: 'variable',
104
+ required: false
105
+ };
106
+ if (matches.length === 0) configuration.push(entry);
107
+ else Object.assign(matches[0], entry);
108
+ return configuration;
109
+ }
110
+
111
+ module.exports = {
112
+ SETTING_NAME,
113
+ CONFIG_KEY,
114
+ parseDimensionAbacOption,
115
+ resolveDimensionAbacSetting,
116
+ persistExplicitDimensionAbacSetting,
117
+ toDimensionAbacEnvValue,
118
+ applyDimensionAbacToEnvMap,
119
+ applyDimensionAbacToConfiguration
120
+ };
@@ -59,6 +59,9 @@ function dataplaneDockerHostPort(appPort, developerIdNum, environmentKey = 'dev'
59
59
  if (env === 'tst') {
60
60
  return baseHostPort + 20;
61
61
  }
62
+ if (env === 'pro') {
63
+ return baseHostPort + 30;
64
+ }
62
65
  return id === 0 ? appPort : baseHostPort;
63
66
  }
64
67
 
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Per-environment env file names (plan 210.0).
3
+ *
4
+ * One host runs several environments, so the env file an environment is given has to be its
5
+ * own. The suffix order is **environment before profile**:
6
+ *
7
+ * ```text
8
+ * .env .env.local dev
9
+ * .env.tst .env.tst.local tst
10
+ * .env.pro .env.pro.local pro
11
+ * ```
12
+ *
13
+ * Two properties make that ordering the right one. `dev` keeps the names it has, so nothing
14
+ * existing is renamed and no tooling that looks for `.env` stops finding it. And `.local`
15
+ * stays the last segment, so the rule "a `.local` file is host-only and is never mounted
16
+ * into a container" is still readable from the name alone — `.env.local.tst` would have
17
+ * quietly broken that, since the profile would no longer be where every existing check looks
18
+ * for it.
19
+ *
20
+ * Pure: no filesystem access. The environment comes from the caller.
21
+ *
22
+ * @fileoverview Environment-suffixed env and env.local file paths
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ const path = require('path');
28
+ const { isPromotionOnlyEnvironment } = require('../core/environment-mode-policy');
29
+
30
+ /** The host-only profile suffix, which must stay last. */
31
+ const LOCAL_PROFILE_SUFFIX = '.local';
32
+
33
+ /**
34
+ * The environment segment for an env file name.
35
+ *
36
+ * Empty for `dev` and for an absent environment, which is what keeps `.env` meaning what it
37
+ * has always meant.
38
+ *
39
+ * @param {string} [envKey] - Environment key
40
+ * @returns {string} `''`, `.tst` or `.pro`
41
+ */
42
+ function environmentEnvFileSegment(envKey) {
43
+ return isPromotionOnlyEnvironment(envKey) ? `.${String(envKey).trim().toLowerCase()}` : '';
44
+ }
45
+
46
+ /**
47
+ * The env file an environment is given.
48
+ *
49
+ * @param {string} envOutputPath - Resolved path ending in `.env`
50
+ * @param {string} [envKey] - Environment key
51
+ * @returns {string} Path ending in `.env`, `.env.tst` or `.env.pro`
52
+ * @throws {Error} When no base path is given
53
+ */
54
+ function resolveEnvOutputPathForEnvironment(envOutputPath, envKey) {
55
+ const base = typeof envOutputPath === 'string' ? envOutputPath.trim() : '';
56
+ if (!base) {
57
+ throw new Error('envOutputPath is required to resolve an environment env file');
58
+ }
59
+ const segment = environmentEnvFileSegment(envKey);
60
+ if (!segment || base.endsWith(segment)) {
61
+ return base;
62
+ }
63
+ return `${base}${segment}`;
64
+ }
65
+
66
+ /**
67
+ * The host-only sibling of an environment's env file.
68
+ *
69
+ * Built from the environment path so the profile suffix is always last, whichever
70
+ * environment it belongs to.
71
+ *
72
+ * @param {string} envOutputPath - Resolved path ending in `.env`
73
+ * @param {string} [envKey] - Environment key
74
+ * @returns {string} Path ending in `.local`
75
+ * @throws {Error} When no base path is given
76
+ */
77
+ function resolveEnvLocalOutputPathForEnvironment(envOutputPath, envKey) {
78
+ const base = typeof envOutputPath === 'string' ? envOutputPath.trim() : '';
79
+ if (!base) {
80
+ throw new Error('envOutputPath is required to resolve an environment .local env file');
81
+ }
82
+ const withoutProfile = base.endsWith(LOCAL_PROFILE_SUFFIX)
83
+ ? base.slice(0, -LOCAL_PROFILE_SUFFIX.length)
84
+ : base;
85
+ const forEnvironment = resolveEnvOutputPathForEnvironment(withoutProfile, envKey);
86
+ return `${forEnvironment}${LOCAL_PROFILE_SUFFIX}`;
87
+ }
88
+
89
+ /**
90
+ * Whether a file name is the host-only profile, for any environment.
91
+ *
92
+ * @param {string} filePath - Path or file name
93
+ * @returns {boolean}
94
+ */
95
+ function isHostOnlyEnvFile(filePath) {
96
+ return path.basename(String(filePath ?? '')).endsWith(LOCAL_PROFILE_SUFFIX);
97
+ }
98
+
99
+ module.exports = {
100
+ LOCAL_PROFILE_SUFFIX,
101
+ environmentEnvFileSegment,
102
+ resolveEnvOutputPathForEnvironment,
103
+ resolveEnvLocalOutputPathForEnvironment,
104
+ isHostOnlyEnvFile
105
+ };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Environment-scoped resource naming (shared infra: dev/tst on same Postgres/Redis/Docker).
3
- * Effective only when user gate AND app flag AND run env is dev|tst.
3
+ * Effective only when user gate AND app flag AND run env is dev|tst|pro.
4
4
  *
5
5
  * @fileoverview environmentScopedResources + useEnvironmentScopedResources helpers
6
6
  * @author AI Fabrix Team
@@ -10,14 +10,44 @@
10
10
  'use strict';
11
11
 
12
12
  /**
13
- * Run/env keys that participate in resource prefixing (not pro).
13
+ * Run/env keys that participate in resource prefixing.
14
+ *
15
+ * `pro` is included (plan 210.0). It was excluded while `tst` was the only promotion target,
16
+ * which left `/pro/data` ungenerated and every pro resource sharing dev's name, Redis index
17
+ * and port. On a shared developer host that is a collision, not an absence.
18
+ *
19
+ * Callers must use this predicate rather than comparing keys inline: the same `dev || tst`
20
+ * test was written out in five other modules, and every one of them had to be found to add
21
+ * a single environment.
22
+ */
23
+ const SCOPED_RUN_ENVIRONMENTS = Object.freeze(['dev', 'tst', 'pro']);
24
+
25
+ /** Redis logical DB per environment on one shared instance. Distinct by construction. */
26
+ const REDIS_DB_INDEX_BY_ENV = Object.freeze({ dev: 0, tst: 1, pro: 2 });
27
+
28
+ /**
14
29
  * @param {string} envKey - Normalized lowercase env key
15
30
  * @returns {boolean}
16
31
  */
17
32
  function isScopedRunEnvironmentKey(envKey) {
18
33
  if (!envKey || typeof envKey !== 'string') return false;
19
- const k = envKey.toLowerCase();
20
- return k === 'dev' || k === 'tst';
34
+ return SCOPED_RUN_ENVIRONMENTS.includes(envKey.toLowerCase());
35
+ }
36
+
37
+ /**
38
+ * Whether a scope context carries both the gate and a scopable environment.
39
+ *
40
+ * The five call sites that need this each combined `effectiveEnvironmentScopedResources`
41
+ * with their own copy of the environment test; this is that combination, once.
42
+ *
43
+ * @param {{ effectiveEnvironmentScopedResources?: boolean, runEnvKey?: string }|null} scopeOpts
44
+ * @returns {boolean}
45
+ */
46
+ function isScopeActiveForOptions(scopeOpts) {
47
+ if (!scopeOpts || !scopeOpts.effectiveEnvironmentScopedResources) {
48
+ return false;
49
+ }
50
+ return isScopedRunEnvironmentKey(scopeOpts.runEnvKey);
21
51
  }
22
52
 
23
53
  /**
@@ -42,17 +72,18 @@ function computeEffectiveEnvironmentScopedResources(
42
72
 
43
73
  /**
44
74
  * Redis logical DB index when env-scoped resources are effective.
45
- * dev → 0, tst → 1 (single shared Redis instance).
75
+ * dev → 0, tst → 1, pro → 2 (single shared Redis instance).
76
+ *
77
+ * Returning null for a scoped environment would put it on DB 0 alongside dev, so this must
78
+ * cover every key in {@link SCOPED_RUN_ENVIRONMENTS}.
46
79
  *
47
- * @param {string} runEnvKey - dev | tst
80
+ * @param {string} runEnvKey - dev | tst | pro
48
81
  * @returns {number|null} Index or null if not applicable
49
82
  */
50
83
  function redisDbIndexForScopedRunEnv(runEnvKey) {
51
84
  if (!runEnvKey || typeof runEnvKey !== 'string') return null;
52
- const k = runEnvKey.toLowerCase();
53
- if (k === 'dev') return 0;
54
- if (k === 'tst') return 1;
55
- return null;
85
+ const index = REDIS_DB_INDEX_BY_ENV[runEnvKey.toLowerCase()];
86
+ return index === undefined ? null : index;
56
87
  }
57
88
 
58
89
  /**
@@ -61,7 +92,7 @@ function redisDbIndexForScopedRunEnv(runEnvKey) {
61
92
  * @param {string} appName - Application key
62
93
  * @param {string|number} devId - Developer id
63
94
  * @param {number} idNum - Parsed numeric developer id
64
- * @param {string} envKey - dev | tst
95
+ * @param {string} envKey - dev | tst | pro
65
96
  * @returns {string}
66
97
  */
67
98
  function buildScopedLocalContainerName(appName, devId, idNum, envKey) {
@@ -93,7 +124,7 @@ function buildDefaultLocalContainerName(appName, devId, idNum) {
93
124
  * @param {string} appName - Application key
94
125
  * @param {string|number} developerId - Developer id
95
126
  * @param {boolean} effectiveScoped - From {@link computeEffectiveEnvironmentScopedResources}
96
- * @param {string} [runEnvKey] - dev | tst (required when effectiveScoped)
127
+ * @param {string} [runEnvKey] - dev | tst | pro (required when effectiveScoped)
97
128
  * @returns {string}
98
129
  */
99
130
  function resolveRunContainerName(appName, developerId, effectiveScoped, runEnvKey) {
@@ -108,7 +139,7 @@ function resolveRunContainerName(appName, developerId, effectiveScoped, runEnvKe
108
139
  * Traefik PathPrefix: prefix /{envKey} before pattern base (e.g. /api → /dev/api).
109
140
  *
110
141
  * @param {string} basePath - From derivePathFromPattern
111
- * @param {string} envKey - dev | tst
142
+ * @param {string} envKey - dev | tst | pro
112
143
  * @returns {string}
113
144
  */
114
145
  function buildEnvScopedTraefikPath(basePath, envKey) {
@@ -125,7 +156,7 @@ function buildEnvScopedTraefikPath(basePath, envKey) {
125
156
  * Compose service + Traefik router key when env-scoped (unique per env on shared host).
126
157
  *
127
158
  * @param {string} appName - Application key
128
- * @param {string} envKey - dev | tst
159
+ * @param {string} envKey - dev | tst | pro
129
160
  * @returns {string}
130
161
  */
131
162
  function composeTraefikServiceKey(appName, envKey) {
@@ -133,7 +164,10 @@ function composeTraefikServiceKey(appName, envKey) {
133
164
  }
134
165
 
135
166
  module.exports = {
167
+ SCOPED_RUN_ENVIRONMENTS,
168
+ REDIS_DB_INDEX_BY_ENV,
136
169
  isScopedRunEnvironmentKey,
170
+ isScopeActiveForOptions,
137
171
  computeEffectiveEnvironmentScopedResources,
138
172
  redisDbIndexForScopedRunEnv,
139
173
  buildScopedLocalContainerName,
@@ -13,6 +13,9 @@
13
13
  /** Env key patterns that indicate a secret (mask value) — same rules as aifabrix logs <app>. */
14
14
  const SECRET_KEY_PATTERN = /password|secret|token|credential|api[_-]?key/i;
15
15
 
16
+ /** Bounded encryption/master-key names; avoids treating every `key` variable as secret. */
17
+ const ENCRYPTION_KEY_PATTERN = /(?:^|[_-])(?:encryption|master)[_-]?key(?:$|[_-])/i;
18
+
16
19
  /** Prefixes to strip before checking key (avoids masking KEYCLOAK_SERVER_URL etc.) */
17
20
  const KEY_PREFIXES_TO_STRIP = /^KEYCLOAK_|^KEY_VAULT_/;
18
21
 
@@ -50,7 +53,7 @@ function maskEnvLine(line) {
50
53
  const value = line.slice(eq + 1);
51
54
 
52
55
  const keyForCheck = key.replace(KEY_PREFIXES_TO_STRIP, '');
53
- const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck);
56
+ const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck) || ENCRYPTION_KEY_PATTERN.test(keyForCheck);
54
57
 
55
58
  const maskedValue = maskUrlEmbeddedCredentials(value);
56
59
  const hasUrlCredentials = maskedValue !== value;
@@ -8,7 +8,13 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- const SYSTEM_BUILDER_APP_KEYS = Object.freeze(['keycloak', 'miso-controller', 'dataplane', 'builder-api']);
11
+ const SYSTEM_BUILDER_APP_KEYS = Object.freeze([
12
+ 'keycloak',
13
+ 'miso-controller',
14
+ 'dataplane',
15
+ 'builder-api',
16
+ 'integration-server'
17
+ ]);
12
18
 
13
19
  /**
14
20
  * @param {string} appName
@@ -473,7 +473,13 @@ function getBuilderRoot() {
473
473
  * {@link getSystemBuilderRoot} (= {@link getAppsMaterializationParent}`/builder`), never the global CLI package tree.
474
474
  * @readonly
475
475
  */
476
- const SYSTEM_BUILDER_APP_KEYS = Object.freeze(['keycloak', 'miso-controller', 'dataplane', 'builder-api']);
476
+ const SYSTEM_BUILDER_APP_KEYS = Object.freeze([
477
+ 'keycloak',
478
+ 'miso-controller',
479
+ 'dataplane',
480
+ 'builder-api',
481
+ 'integration-server'
482
+ ]);
477
483
 
478
484
  /**
479
485
  * @param {string} appName
@@ -1156,4 +1162,3 @@ module.exports = {
1156
1162
  resolveAppWorkspace: require('./app-path-resolve').resolveAppWorkspace,
1157
1163
  resolveExternalSystemPath: require('./app-path-resolve').resolveExternalSystemPath
1158
1164
  };
1159
-
@@ -164,6 +164,35 @@ async function ensurePlatformDatabaseSlot(container, slot, secrets, adminPasswor
164
164
  return slot.dbName;
165
165
  }
166
166
 
167
+ /**
168
+ * Databases this server already has.
169
+ *
170
+ * Read before anything is created, so an environment can adopt a database an older
171
+ * installation left behind instead of renaming it — a rename here means dropping and
172
+ * recreating, which means losing the data.
173
+ *
174
+ * @async
175
+ * @param {string} container - Postgres container name
176
+ * @param {string} adminPassword - Superuser password
177
+ * @returns {Promise<string[]>} Database names, empty when the server cannot be listed
178
+ */
179
+ async function listExistingDatabaseNames(container, adminPassword) {
180
+ try {
181
+ const result = await dockerExec.execWithDockerEnv(
182
+ `docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d postgres -tAc "SELECT datname FROM pg_database WHERE datistemplate = false"`,
183
+ { env: { PGPASSWORD: adminPassword } }
184
+ );
185
+ return String((result && result.stdout) || '')
186
+ .split('\n')
187
+ .map((line) => line.trim())
188
+ .filter(Boolean);
189
+ } catch {
190
+ // An unreadable server adopts nothing and creates under the contract name. Failing here
191
+ // would block a fresh install, where there is nothing to list in the first place.
192
+ return [];
193
+ }
194
+ }
195
+
167
196
  /**
168
197
  * @async
169
198
  * @returns {Promise<{ container: string, adminPassword: string, secrets: Object }>}
@@ -202,5 +231,10 @@ module.exports = {
202
231
  PLATFORM_DATABASE_SLOTS,
203
232
  bootstrapPlatformPostgresDatabases,
204
233
  pgRoleName,
205
- extractPasswordFromUrlOrValue
234
+ extractPasswordFromUrlOrValue,
235
+ // Reused by the local environment stage, which needs the same primitives under
236
+ // environment-scoped names rather than the unprefixed defaults this module creates.
237
+ resolveBootstrapContext,
238
+ listExistingDatabaseNames,
239
+ ensureRoleAndDatabase
206
240
  };
@@ -59,4 +59,24 @@ function applyRedisDbIndexToEnvContent(content, dbIndex) {
59
59
  return out.join('\n');
60
60
  }
61
61
 
62
- module.exports = { applyRedisDbIndexToEnvContent, setRedisUrlDbIndex };
62
+ /**
63
+ * Point a scoped environment at its own Redis logical database.
64
+ *
65
+ * Here rather than at the call site because this module already owns the rewrite, and the
66
+ * caller is a 500-line file whose budget this would spend. Separating it is honest anyway:
67
+ * it is the only step that rewrites a resolved value instead of resolving one.
68
+ *
69
+ * @param {string} content - Resolved env content
70
+ * @param {string} runEnvKey - Run environment key
71
+ * @param {boolean} effective - Whether env-scoped resources apply
72
+ * @returns {string} Content, unchanged when scoping does not apply
73
+ */
74
+ function applyScopedRedisDbIndex(content, runEnvKey, effective) {
75
+ if (!effective) {
76
+ return content;
77
+ }
78
+ const { redisDbIndexForScopedRunEnv } = require('./environment-scoped-resources');
79
+ return applyRedisDbIndexToEnvContent(content, redisDbIndexForScopedRunEnv(runEnvKey));
80
+ }
81
+
82
+ module.exports = { applyRedisDbIndexToEnvContent, setRedisUrlDbIndex, applyScopedRedisDbIndex };