@aifabrix/builder 2.61.2 → 2.62.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 (81) hide show
  1. package/lib/api/applications.api.js +22 -2
  2. package/lib/api/bootstrap-snapshot.api.js +115 -0
  3. package/lib/app/deploy.js +14 -5
  4. package/lib/app/run-env-compose.js +5 -6
  5. package/lib/app/run.js +6 -4
  6. package/lib/build/standard-docker-build.js +4 -0
  7. package/lib/channels/channel-artifact-generator.js +13 -3
  8. package/lib/cli/infra-guided-footers.js +10 -8
  9. package/lib/cli/infra-guided.js +30 -10
  10. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  11. package/lib/cli/setup-platform.js +2 -0
  12. package/lib/commands/auth-config.js +2 -0
  13. package/lib/commands/login.js +4 -0
  14. package/lib/commands/repair-datasource-apply.js +3 -1
  15. package/lib/commands/repair-datasource-legacy.js +94 -0
  16. package/lib/commands/setup-image-refresh.js +26 -0
  17. package/lib/commands/setup-modes.js +7 -1
  18. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  19. package/lib/commands/setup.js +3 -0
  20. package/lib/commands/up-builder-api.js +24 -8
  21. package/lib/commands/up-common.js +16 -1
  22. package/lib/commands/up-dataplane-bootstrap.js +70 -14
  23. package/lib/commands/up-dataplane-credentials.js +21 -0
  24. package/lib/commands/up-dataplane.js +37 -18
  25. package/lib/commands/up-integration-server.js +112 -0
  26. package/lib/commands/wizard-config-normalizer.js +5 -1
  27. package/lib/core/admin-secrets-env-overlay.js +8 -3
  28. package/lib/core/environment-access-policy.js +121 -0
  29. package/lib/core/environment-mode-policy.js +177 -0
  30. package/lib/core/local-env-overrides.js +149 -0
  31. package/lib/core/secrets-ensure.js +4 -2
  32. package/lib/core/secrets-env-content.js +12 -9
  33. package/lib/core/secrets-env-write.js +1 -1
  34. package/lib/datasource/capability/capability-manifest.js +1 -0
  35. package/lib/deployment/installation/index.js +14 -1
  36. package/lib/deployment/installation/infra-catalog.js +3 -0
  37. package/lib/deployment/installation/local-environment-stage.js +285 -0
  38. package/lib/deployment/installation/local-installation.js +76 -0
  39. package/lib/deployment/installation/stage-input.js +5 -1
  40. package/lib/generator/builders.js +3 -1
  41. package/lib/generator/split-variables.js +2 -2
  42. package/lib/parameters/database-adoption.js +125 -0
  43. package/lib/parameters/physical-database-naming.js +136 -0
  44. package/lib/parameters/physical-secret-name.js +194 -0
  45. package/lib/parameters/rsa-secret-values.js +165 -0
  46. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  47. package/lib/programmatic/run-channel-package.js +3 -1
  48. package/lib/programmatic/workspace-context.js +37 -2
  49. package/lib/schema/infra.parameter.yaml +61 -27
  50. package/lib/schema/infrastructure-schema.json +213 -120
  51. package/lib/utils/compose-generate-docker-compose.js +2 -2
  52. package/lib/utils/compose-generator.js +4 -7
  53. package/lib/utils/compose-miso-env.js +2 -2
  54. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  55. package/lib/utils/declarative-url-ports.js +3 -0
  56. package/lib/utils/env-environment-file-paths.js +105 -0
  57. package/lib/utils/environment-scoped-resources.js +48 -14
  58. package/lib/utils/paths-system-builder-keys.js +7 -1
  59. package/lib/utils/paths.js +7 -2
  60. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  61. package/lib/utils/redis-env-scope.js +21 -1
  62. package/lib/utils/registry-auth-sources.js +154 -0
  63. package/lib/utils/registry-credentials.js +131 -14
  64. package/lib/utils/secrets-generator.js +30 -21
  65. package/lib/utils/secrets-helpers.js +5 -7
  66. package/lib/utils/secrets-kv-scope.js +127 -28
  67. package/lib/utils/secrets-materialize-local.js +9 -0
  68. package/lib/utils/secrets-missing-error.js +35 -1
  69. package/lib/utils/token-manager.js +14 -8
  70. package/lib/utils/url-public-path-prefix.js +6 -10
  71. package/package.json +3 -2
  72. package/templates/applications/builder-api/application.yaml +1 -1
  73. package/templates/applications/dataplane/application.yaml +2 -2
  74. package/templates/applications/dataplane/env.template +1 -1
  75. package/templates/applications/integration-server/application.yaml +91 -0
  76. package/templates/applications/integration-server/env.template +215 -0
  77. package/templates/applications/mirrored-templates.json +52 -0
  78. package/templates/applications/miso-controller/application.yaml +43 -1
  79. package/templates/applications/miso-controller/env.template +45 -39
  80. package/templates/marketplace/createUiDefinition.json +52 -4
  81. 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
  }
@@ -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,
@@ -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 };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Where a registry credential comes from when none is stored.
3
+ *
4
+ * Builder is registry-agnostic: Docker Hub, GHCR and ACR are all a host, a username and a
5
+ * password, and an individual identity, a registry token and an admin key all fit that
6
+ * shape. The only special case is that an `*.azurecr.io` host can also be authenticated by
7
+ * the Azure CLI, which is worth trying before asking a person to paste a secret.
8
+ *
9
+ * Order: the stored pair (handled by the caller), then `az acr login` for ACR, then a
10
+ * prompt. A non-interactive run never prompts — it fails naming the host and how to
11
+ * configure it, because a hung prompt in CI is worse than a clear failure.
12
+ *
13
+ * @fileoverview Credential source strategy for registries without a stored pair
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ /** Azure Container Registry login server suffix. */
19
+ const ACR_HOST_SUFFIX = '.azurecr.io';
20
+
21
+ /**
22
+ * @param {string} host - Registry host
23
+ * @returns {boolean} True for an Azure Container Registry login server
24
+ */
25
+ function isAzureContainerRegistryHost(host) {
26
+ return String(host || '').trim().toLowerCase().endsWith(ACR_HOST_SUFFIX);
27
+ }
28
+
29
+ /**
30
+ * Message for a run that cannot ask for credentials.
31
+ *
32
+ * @param {string} host - Registry host
33
+ * @returns {string}
34
+ */
35
+ function nonInteractiveCredentialMessage(host) {
36
+ const acrHint = isAzureContainerRegistryHost(host)
37
+ ? ` For Azure Container Registry you can instead run "az login" so "az acr login --name ${String(host).split('.')[0]}" succeeds.`
38
+ : '';
39
+ return (
40
+ `No credentials for ${host} and nothing to ask: this run is not interactive. ` +
41
+ `Store a pair with "aifabrix dev set-registry ${host} --username <username> --password-stdin".${acrHint}`
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Build the fallback `ensureRegistryLogin` uses when no pair is stored.
47
+ *
48
+ * Dependencies are injected so the chain can be tested without a terminal, an Azure CLI or
49
+ * a Docker daemon.
50
+ *
51
+ * @param {Object} [deps] - Injectable dependencies
52
+ * @param {boolean} [deps.interactive] - Whether a person can be asked; defaults to TTY
53
+ * @param {Function} [deps.acrLogin] - async (host) => void, defaults to `az acr login`
54
+ * @param {Function} [deps.promptCredentials] - async (host) => { username, password }
55
+ * @param {Function} [deps.saveCredentials] - async (host, username, password) => void
56
+ * @param {Function} [deps.dockerLogin] - async (host, username, password) => void
57
+ * @returns {(host: string) => Promise<'acr'|'prompt'>} Fallback for ensureRegistryLogin
58
+ */
59
+ /**
60
+ * An error that names the host it is about.
61
+ *
62
+ * Callers read `registryHost` off it to report which registry refused, so the property is
63
+ * attached in one place rather than at each throw.
64
+ *
65
+ * @param {string} message - Message
66
+ * @param {string} host - Registry host
67
+ * @returns {Error}
68
+ */
69
+ function hostError(message, host) {
70
+ const err = new Error(message);
71
+ err.registryHost = host;
72
+ return err;
73
+ }
74
+
75
+ function buildRegistryAuthFallback(deps = {}) {
76
+ const interactive = deps.interactive === undefined
77
+ ? Boolean(process.stdin.isTTY && process.stdout.isTTY)
78
+ : Boolean(deps.interactive);
79
+
80
+ /**
81
+ * Try `az acr login` for an ACR host.
82
+ *
83
+ * Extracted so the fallback itself stays readable. A failure here is deliberately not
84
+ * fatal when a prompt is still possible: an expired or absent `az login` is an ordinary
85
+ * state, and a stored pair or an answered prompt can still authenticate the host.
86
+ *
87
+ * @param {string} host - Registry host
88
+ * @returns {Promise<boolean>} True when ACR login succeeded
89
+ */
90
+ async function tryAcrLogin(host) {
91
+ if (!isAzureContainerRegistryHost(host)) {
92
+ return false;
93
+ }
94
+ const acrLogin = deps.acrLogin
95
+ || ((target) => require('../deployment/push').authenticateACR(target));
96
+ try {
97
+ await acrLogin(host);
98
+ return true;
99
+ } catch (error) {
100
+ if (!interactive) {
101
+ throw hostError(`${nonInteractiveCredentialMessage(host)} (az acr login failed: ${error.message})`, host);
102
+ }
103
+ return false;
104
+ }
105
+ }
106
+
107
+ return async function registryAuthFallback(host) {
108
+ if (await tryAcrLogin(host)) {
109
+ return 'acr';
110
+ }
111
+ if (!interactive) {
112
+ throw hostError(nonInteractiveCredentialMessage(host), host);
113
+ }
114
+
115
+ const promptCredentials = deps.promptCredentials || promptForRegistryCredentials;
116
+ const { username, password } = await promptCredentials(host);
117
+ if (!username || !password) {
118
+ throw new Error(`Registry credentials for ${host} are required: both a username and a password.`);
119
+ }
120
+
121
+ const saveCredentials = deps.saveCredentials
122
+ || ((target, user, pass) => require('./registry-credentials').saveRegistryCredentials(target, user, pass));
123
+ // Store before logging in, so the answer is not asked for again on the next command
124
+ // even if the login itself fails for an unrelated reason.
125
+ await saveCredentials(host, username, password);
126
+
127
+ const dockerLogin = deps.dockerLogin
128
+ || ((target, user, pass) => require('../deployment/push').authenticateExternalRegistry(target, user, pass));
129
+ await dockerLogin(host, username, password);
130
+ return 'prompt';
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Ask for a registry username and password. The password is never echoed.
136
+ *
137
+ * @param {string} host - Registry host
138
+ * @returns {Promise<{ username: string, password: string }>}
139
+ */
140
+ async function promptForRegistryCredentials(host) {
141
+ const inquirer = require('inquirer');
142
+ return inquirer.prompt([
143
+ { type: 'input', name: 'username', message: `Username for ${host}:` },
144
+ { type: 'password', name: 'password', mask: '*', message: `Password or token for ${host}:` }
145
+ ]);
146
+ }
147
+
148
+ module.exports = {
149
+ ACR_HOST_SUFFIX,
150
+ isAzureContainerRegistryHost,
151
+ nonInteractiveCredentialMessage,
152
+ buildRegistryAuthFallback,
153
+ promptForRegistryCredentials
154
+ };