@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
@@ -1,6 +1,21 @@
1
1
  /**
2
2
  * Env-scoped kv:// resolution helpers (prefixed keys + in-memory aliases).
3
3
  *
4
+ * Two regimes share this module:
5
+ *
6
+ * - `dev` and `miso` are **lenient** (plan 117). A `<env>-<key>` secret wins when present,
7
+ * and an absent one falls back to the unprefixed key. Existing installations are written
8
+ * that way, so removing the fallback would break every one of them.
9
+ * - `tst` and `pro` are **strict** (plan 210.0). Only `<env>-<key>` is read. An absent key
10
+ * is absent — it never falls back to the unprefixed value and it is never generated.
11
+ *
12
+ * The strict rule is the whole point: these environments share one Postgres, one Redis and
13
+ * one secrets file with `dev`. A fallback there would hand production whatever `dev` happens
14
+ * to hold, which is worse than failing, because it looks like it worked. Strictness is
15
+ * therefore deliberately independent of `useEnvironmentScopedResources` and of the
16
+ * per-application `environmentScopedResources` flag: an installation that forgot to set
17
+ * either must not silently resolve `pro` against `dev`'s secrets.
18
+ *
4
19
  * @fileoverview Split from secrets-helpers for file size limits
5
20
  * @author AI Fabrix Team
6
21
  * @version 1.0.0
@@ -8,6 +23,25 @@
8
23
 
9
24
  'use strict';
10
25
 
26
+ /**
27
+ * Environments where the `<env>-` prefix is mandatory rather than preferred.
28
+ *
29
+ * Not the same list as `physical-database-naming.PHYSICAL_NAME_ENVIRONMENTS`: that one is the
30
+ * Postgres naming vocabulary and includes `miso` and `dev`, which resolve leniently here.
31
+ */
32
+ const STRICT_SCOPED_ENVIRONMENTS = Object.freeze(['tst', 'pro']);
33
+
34
+ /**
35
+ * @param {string} envKey - Run environment key
36
+ * @returns {boolean} True when the prefix is mandatory for this environment
37
+ */
38
+ function isStrictScopedEnvironment(envKey) {
39
+ if (!envKey || typeof envKey !== 'string') {
40
+ return false;
41
+ }
42
+ return STRICT_SCOPED_ENVIRONMENTS.includes(envKey.trim().toLowerCase());
43
+ }
44
+
11
45
  /**
12
46
  * Treat undefined/null and whitespace-only strings as no value so prefixed slots
13
47
  * cannot shadow unprefixed secrets with empty placeholders (plan 117 scoped KV).
@@ -25,9 +59,80 @@ function isKvSlotEmpty(v) {
25
59
  return false;
26
60
  }
27
61
 
62
+ /**
63
+ * The secret key a strict environment actually requires.
64
+ *
65
+ * **Not simply `<env>-<key>`.** Miso's naming rule has three cases, and only the middle one
66
+ * is a bare environment prefix:
67
+ *
68
+ * ```text
69
+ * system <key> unprefixed, deliberately
70
+ * environment-scoped pro-<key>
71
+ * application-scoped pro-<app>-<key>
72
+ * ```
73
+ *
74
+ * They are not distinguishable from the key alone, so the name comes from the catalog entry's
75
+ * own vault-name pattern rather than being built here. A database password is therefore
76
+ * `pro-dataplane-databases-0-passwordKeyVault` — the application segment before the key — and
77
+ * `keycloak-*` and `acr-*` keep the names the Marketplace template writes.
78
+ *
79
+ * @param {string} envKey - Run environment key
80
+ * @param {string} pathStr - Unprefixed kv path
81
+ * @returns {string}
82
+ */
83
+ function strictScopedKeyName(envKey, pathStr) {
84
+ const { resolveStrictSecretName } = require('../parameters/physical-secret-name');
85
+ return resolveStrictSecretName(pathStr, String(envKey).toLowerCase());
86
+ }
87
+
88
+ /**
89
+ * Alias every unprefixed key to its `<env>-` name so a lenient lookup finds it.
90
+ *
91
+ * Never called for a strict environment: aliasing there would reintroduce the fallback
92
+ * this module exists to remove.
93
+ *
94
+ * @param {Object} secrets
95
+ * @param {string} envKey
96
+ * @returns {Object}
97
+ */
98
+ function mergeSecretsWithPrefixedCopies(secrets, envKey) {
99
+ if (!secrets || typeof secrets !== 'object' || !envKey) {
100
+ return secrets;
101
+ }
102
+ const prefix = `${String(envKey).toLowerCase()}-`;
103
+ const merged = { ...secrets };
104
+ for (const [k, v] of Object.entries(secrets)) {
105
+ if (typeof k !== 'string' || !k || k.startsWith(prefix)) continue;
106
+ const pk = prefix + k;
107
+ if (isKvSlotEmpty(merged[pk]) && !isKvSlotEmpty(v)) {
108
+ merged[pk] = v;
109
+ }
110
+ }
111
+ return merged;
112
+ }
113
+
114
+ /**
115
+ * Resolve the scoped-kv regime and the secrets map to read through.
116
+ *
117
+ * One helper so every caller applies the same rule; a caller that recomputed `effective`
118
+ * itself could leave a strict environment resolving leniently.
119
+ *
120
+ * @param {Object} secrets - Merged secrets map
121
+ * @param {{ effective?: boolean, envKey?: string }|null} scopedKv
122
+ * @returns {{ secretsMap: Object, envKey: string|undefined, effective: boolean, strict: boolean }}
123
+ */
124
+ function prepareScopedKv(secrets, scopedKv) {
125
+ const envKey = scopedKv && scopedKv.envKey ? scopedKv.envKey : undefined;
126
+ const strict = isStrictScopedEnvironment(envKey);
127
+ const effective = strict || Boolean(scopedKv && scopedKv.effective && envKey);
128
+ const secretsMap =
129
+ effective && !strict ? mergeSecretsWithPrefixedCopies(secrets, envKey) : secrets;
130
+ return { secretsMap, envKey, effective, strict };
131
+ }
132
+
28
133
  /**
29
134
  * @param {Function} getValueByPath - From secrets-helpers
30
- * @returns {{ getValueByPathWithEnvScope: Function, mergeSecretsWithPrefixedCopies: Function }}
135
+ * @returns {Object} Scoped kv helpers bound to that lookup
31
136
  */
32
137
  function createScopedKvHelpers(getValueByPath) {
33
138
  /**
@@ -38,40 +143,34 @@ function createScopedKvHelpers(getValueByPath) {
38
143
  * @returns {*}
39
144
  */
40
145
  function getValueByPathWithEnvScope(secrets, pathStr, envKey, effective) {
41
- if (!effective || !envKey) {
146
+ const strict = isStrictScopedEnvironment(envKey);
147
+ if (!strict && (!effective || !envKey)) {
42
148
  return getValueByPath(secrets, pathStr);
43
149
  }
44
- const prefix = `${String(envKey).toLowerCase()}-`;
45
- const prefixedPath = prefix + pathStr;
46
- const fromPrefixed = getValueByPath(secrets, prefixedPath);
150
+ const fromPrefixed = getValueByPath(secrets, strictScopedKeyName(envKey, pathStr));
151
+ if (strict) {
152
+ // No fallback. Undefined rather than the empty value so the caller's
153
+ // allowed-empty rule still decides whether absence is an error.
154
+ return isKvSlotEmpty(fromPrefixed) ? undefined : fromPrefixed;
155
+ }
47
156
  if (!isKvSlotEmpty(fromPrefixed)) {
48
157
  return fromPrefixed;
49
158
  }
50
159
  return getValueByPath(secrets, pathStr);
51
160
  }
52
161
 
53
- /**
54
- * @param {Object} secrets
55
- * @param {string} envKey
56
- * @returns {Object}
57
- */
58
- function mergeSecretsWithPrefixedCopies(secrets, envKey) {
59
- if (!secrets || typeof secrets !== 'object' || !envKey) {
60
- return secrets;
61
- }
62
- const prefix = `${String(envKey).toLowerCase()}-`;
63
- const merged = { ...secrets };
64
- for (const [k, v] of Object.entries(secrets)) {
65
- if (typeof k !== 'string' || !k || k.startsWith(prefix)) continue;
66
- const pk = prefix + k;
67
- if (isKvSlotEmpty(merged[pk]) && !isKvSlotEmpty(v)) {
68
- merged[pk] = v;
69
- }
70
- }
71
- return merged;
72
- }
73
-
74
- return { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies };
162
+ return {
163
+ getValueByPathWithEnvScope,
164
+ mergeSecretsWithPrefixedCopies,
165
+ prepareScopedKv
166
+ };
75
167
  }
76
168
 
77
- module.exports = { createScopedKvHelpers };
169
+ module.exports = {
170
+ STRICT_SCOPED_ENVIRONMENTS,
171
+ isStrictScopedEnvironment,
172
+ strictScopedKeyName,
173
+ mergeSecretsWithPrefixedCopies,
174
+ prepareScopedKv,
175
+ createScopedKvHelpers
176
+ };
@@ -17,6 +17,7 @@ const secretsUtils = require('./secrets-utils');
17
17
  const { loadConfiguredSharedSecretsStore } = require('./remote-secrets-loader');
18
18
  const { decryptSecretsObject } = require('../core/secrets-load');
19
19
  const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
20
+ const { isStrictScopedEnvironment } = require('./secrets-kv-scope');
20
21
  const {
21
22
  resolveKvRefValue,
22
23
  mergeSecretsWithPrefixedCopies,
@@ -98,6 +99,14 @@ async function materializeResolvedKvSecretsToUserLocal(templateContent, mergedSe
98
99
  return [];
99
100
  }
100
101
 
102
+ // This writes back under the UNPREFIXED key. In a strict environment that would mint an
103
+ // unprefixed copy of a tst or pro secret which dev would then resolve, quietly undoing the
104
+ // isolation. Nothing is lost by skipping: a strict environment resolves only `<env>-<key>`,
105
+ // so every value it resolved was already stored under the name it will be read by.
106
+ if (isStrictScopedEnvironment(scopedKv && scopedKv.envKey)) {
107
+ return [];
108
+ }
109
+
101
110
  try {
102
111
  const { effective, secretsMap, localMap } = buildScopedMaps(mergedSecrets, scopedKv);
103
112
  const sharedScoped = await resolveSharedScopedMapForMaterialize(scopedKv, effective);
@@ -10,6 +10,7 @@
10
10
 
11
11
  const { formatMissingSecretsFileInfo } = require('./secrets-helpers');
12
12
  const { isCommentOrEmptyLine } = require('./secrets-kv-refs');
13
+ const { isStrictScopedEnvironment, strictScopedKeyName } = require('./secrets-kv-scope');
13
14
 
14
15
  /**
15
16
  * @param {string} kvRef - e.g. kv://hubspot-demo/clientSecret
@@ -117,6 +118,35 @@ function buildKvNamespaceAlternateHints(missing, secrets, appName) {
117
118
  return lines;
118
119
  }
119
120
 
121
+ /**
122
+ * Name the key a strict environment actually needs.
123
+ *
124
+ * The template references `kv://<key>`, but `tst` and `pro` only read `<env>-<key>`, so an
125
+ * error quoting the template reference alone would send someone to create the one name that
126
+ * will not be used. Both are printed: the reference so the template line is findable, and
127
+ * the required key so `aifabrix secret set` gets the right argument.
128
+ *
129
+ * @param {string[]} missing - kv:// refs
130
+ * @param {{ envKey?: string }|null} [scopedKv]
131
+ * @returns {string[]}
132
+ */
133
+ function buildStrictScopedKeyHints(missing, scopedKv) {
134
+ const envKey = scopedKv && scopedKv.envKey ? scopedKv.envKey : null;
135
+ if (!envKey || !isStrictScopedEnvironment(envKey)) {
136
+ return [];
137
+ }
138
+ const env = String(envKey).toLowerCase();
139
+ const lines = missing.map(
140
+ (ref) => `Required-key: ${strictScopedKeyName(env, kvRefToSecretSetKey(ref))}`
141
+ );
142
+ lines.push(
143
+ `Scoped-env-hint: ${env} reads only ${env}-prefixed secrets. The unprefixed key is ` +
144
+ `not a fallback and is never generated for ${env}, so another environment's value ` +
145
+ 'cannot be used by accident.'
146
+ );
147
+ return lines;
148
+ }
149
+
120
150
  /**
121
151
  * @param {Object} opts
122
152
  * @param {string[]} opts.missing
@@ -125,10 +155,11 @@ function buildKvNamespaceAlternateHints(missing, secrets, appName) {
125
155
  * @param {string} [opts.envTemplatePath]
126
156
  * @param {string} [opts.envTemplate]
127
157
  * @param {Object} [opts.secrets]
158
+ * @param {{ envKey?: string }|null} [opts.scopedKv]
128
159
  * @returns {string}
129
160
  */
130
161
  function buildMissingSecretsErrorMessage(opts) {
131
- const { missing, secretsFilePaths, appName, envTemplatePath, envTemplate, secrets } = opts;
162
+ const { missing, secretsFilePaths, appName, envTemplatePath, envTemplate, secrets, scopedKv } = opts;
132
163
  const fileInfo = formatMissingSecretsFileInfo(secretsFilePaths);
133
164
  const lines = [`Missing secrets: ${missing.join(', ')}`];
134
165
  if (fileInfo) lines.push(fileInfo.trim());
@@ -138,6 +169,9 @@ function buildMissingSecretsErrorMessage(opts) {
138
169
  if (templateLine) lines.push(`Env-template-line: ${ref}|${templateLine}`);
139
170
  }
140
171
  if (appName) lines.push(`App-name: ${appName}`);
172
+ for (const hint of buildStrictScopedKeyHints(missing, scopedKv)) {
173
+ lines.push(hint);
174
+ }
141
175
  if (hasStaleOAuthTemplatePaths(missing) && appName) {
142
176
  lines.push(`Stale-template-hint: oauth-leftover | repair ${appName}`);
143
177
  }
@@ -10,7 +10,6 @@
10
10
  */
11
11
 
12
12
  const config = require('../core/config');
13
- const logger = require('./logger');
14
13
  const { maskSensitiveData } = require('./log-redaction');
15
14
  const {
16
15
  refreshClientToken,
@@ -205,12 +204,17 @@ async function tryClientTokenAuth(environment, appName, controllerUrl) {
205
204
  return null;
206
205
  }
207
206
 
208
- function throwNoDeploymentAuth(credentialApp) {
209
- const err = new Error(
210
- `No authentication method available. For CI: set AIFABRIX_DEPLOYMENT_AUTH=client-credentials and MISO_CLIENTID/MISO_CLIENTSECRET in .env or '${credentialApp}-client-idKeyVault' in secrets.local.yaml. For interactive use: aifabrix login`
211
- );
207
+ // Two causes reach here: no credentials at all, or credentials the controller rejected.
208
+ // Saying "put it in secrets.local.yaml" for the second sends the reader after a key that is
209
+ // already present and simply not valid, so name the rejection instead.
210
+ function throwNoDeploymentAuth(credentialApp, exchangeError) {
211
+ const err = new Error(exchangeError
212
+ ? `Client credentials for '${credentialApp}' were rejected by the controller: ${exchangeError.message}. ` +
213
+ `Re-issue them with: aifabrix login && aifabrix app rotate-secret ${credentialApp}`
214
+ : `No authentication method available. For CI: set AIFABRIX_DEPLOYMENT_AUTH=client-credentials and MISO_CLIENTID/MISO_CLIENTSECRET in .env or '${credentialApp}-client-idKeyVault' in secrets.local.yaml. For interactive use: aifabrix login`);
212
215
  err.statusCode = 401;
213
216
  err.authFailure = true;
217
+ if (exchangeError) err.cause = exchangeError;
214
218
  throw err;
215
219
  }
216
220
 
@@ -262,8 +266,10 @@ async function getDeploymentAuth(controllerUrl, environment, appName, options =
262
266
  controller: controllerUrl
263
267
  };
264
268
  }
265
- } catch {
266
- // Refresh failed; fall through to throw below
269
+ } catch (err) {
270
+ // Keep it: credentials existed and the controller turned them down, which is a
271
+ // different problem from having none, and the only place that fact is observable.
272
+ throwNoDeploymentAuth(credentialApp, err);
267
273
  }
268
274
  }
269
275
 
@@ -341,7 +347,7 @@ async function forceRefreshDeviceToken(controllerUrl) {
341
347
 
342
348
  // Must have refresh token to force refresh
343
349
  if (!tokenInfo.refreshToken) {
344
- logger.warn('Cannot refresh: no refresh token available. Please login again using: aifabrix login');
350
+ warnRefreshFailureOnce(controllerUrl, 'Cannot refresh: no refresh token available. Please login again using: aifabrix login');
345
351
  return null;
346
352
  }
347
353
 
@@ -1,18 +1,20 @@
1
1
  /**
2
- * Plan 117 URL path prefix for url://public (dev/tst only when gated).
2
+ * Plan 117 URL path prefix for url://public (scoped environments only when gated).
3
3
  *
4
- * @fileoverview baseEffective ∧ derived envKey ∈ {dev,tst}. Caller must pass prefix only when `traefik` is on (see url-declarative-resolve).
4
+ * @fileoverview baseEffective ∧ derived envKey ∈ SCOPED_RUN_ENVIRONMENTS. Caller must pass prefix only when `traefik` is on (see url-declarative-resolve).
5
5
  * @author AI Fabrix Team
6
6
  * @version 1.0.0
7
7
  */
8
8
 
9
9
  'use strict';
10
10
 
11
+ const { isScopedRunEnvironmentKey } = require('./environment-scoped-resources');
12
+
11
13
  /**
12
14
  * @param {boolean} useEnvironmentScopedResources - config gate
13
15
  * @param {boolean} appEnvironmentScopedResources - application.yaml
14
16
  * @param {string} derivedEnvKey - from client id (dev|tst|pro|miso)
15
- * @returns {string} '' | '/dev' | '/tst'
17
+ * @returns {string} '' | '/dev' | '/tst' | '/pro'
16
18
  */
17
19
  function computePublicUrlPathPrefix(useEnvironmentScopedResources, appEnvironmentScopedResources, derivedEnvKey) {
18
20
  const baseEffective = Boolean(useEnvironmentScopedResources) && Boolean(appEnvironmentScopedResources);
@@ -20,13 +22,7 @@ function computePublicUrlPathPrefix(useEnvironmentScopedResources, appEnvironmen
20
22
  return '';
21
23
  }
22
24
  const k = String(derivedEnvKey || '').toLowerCase();
23
- if (k === 'dev') {
24
- return '/dev';
25
- }
26
- if (k === 'tst') {
27
- return '/tst';
28
- }
29
- return '';
25
+ return isScopedRunEnvironmentKey(k) ? `/${k}` : '';
30
26
  }
31
27
 
32
28
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.61.2",
3
+ "version": "2.63.0",
4
4
  "description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -35,12 +35,14 @@
35
35
  "lint:fix": "eslint . --ext .js --fix",
36
36
  "test": "node tests/scripts/test-wrapper.js",
37
37
  "build": "npm run lint && npm run test",
38
- "build:ci": "npm run lint && npm run check:schema-sync && npm run check:flags && npm run check:cli-help && npm run test:ci",
38
+ "build:ci": "npm run lint && npm run check:schema-sync && npm run check:flags && npm run check:cli-help && npm run check:mirrored-templates && npm run test:ci",
39
39
  "test:ci": "bash tests/scripts/ci-simulate.sh",
40
40
  "check:schema-sync": "node scripts/check-datasource-test-run-schema-sync.js",
41
41
  "check:flags": "jest tests/lib/schema/flag-map-validation-run.test.js --runInBand --config jest.config.default.js",
42
42
  "check:cli-help": "node scripts/check-cli-help-examples.js",
43
+ "check:mirrored-templates": "node scripts/check-mirrored-templates.js",
43
44
  "test:manual": "jest --config jest.config.manual.js --runInBand",
45
+ "test:manual:public": "jest --config jest.config.manual-public.js --runInBand",
44
46
  "test:integration": "jest --config jest.config.integration.js --runInBand",
45
47
  "test:integration:fixtures": "jest --config jest.config.integration.fixtures.js --runInBand",
46
48
  "test:hubspot-wizard": "node integration/hubspot-test/test.js",
@@ -26,6 +26,12 @@ Application templates are folder-based and located under `templates/applications
26
26
  | `aifabrix-miso/packages/miso-controller/configs/infra.parameter.yaml` | `lib/schema/infra.parameter.yaml` |
27
27
  | `aifabrix-dataplane/builder/dataplane/` (`env.template`, `rbac.yaml`, `application.yaml`) | `templates/applications/dataplane/` |
28
28
 
29
+ The Dataplane `env.template` is the source of truth for
30
+ `DIMENSION_ABAC_ENFORCEMENT_ENABLED` and its default `false`. Do not hand-edit
31
+ the bundled mirror. `aifabrix setup --dimension-abac on|off` stores an operator
32
+ selection outside the template and overlays the resolved lowercase value during
33
+ local runtime and deployment-manifest materialization.
34
+
29
35
  **Schema master (dataplane):** Also copies dataplane `app/schemas/json/` core integration schemas + `type/*` and `configs/deployment-rules.yaml` into `lib/schema/` (same contract as dataplane `make sync-json`). Do not hand-edit those mirrors — edit dataplane SSOT, then sync. `lib/schema/infra.parameter.yaml` is the Miso catalog mirror above; other Builder-owned schemas in `lib/schema/` (wizard and the rest) are left alone.
30
36
 
31
37
  Overrides: `AIFABRIX_MISO_SRC`, `AIFABRIX_DATAPLANE_SRC` / `AIFABRIX_DATAPLANE_ROOT` (repo roots). Install with `aifabrix up-platform` / `up-builder-api` — do not hand-edit the template copies as primary.
@@ -5,7 +5,7 @@ app:
5
5
  description: 'Business Transformation services — HTTP orchestration for connected-system workspace lifecycle via Dataplane Enterprise MCP and governance APIs.'
6
6
  type: webapp
7
7
  language: typescript
8
- version: 2.61.2
8
+ version: 2.63.0
9
9
 
10
10
  # Image Configuration
11
11
  image:
@@ -5,7 +5,7 @@ app:
5
5
  description: "AI Fabrix Dataplane is a secure, in-tenant integration and automation layer that supplies governed, normalized, and explainable enterprise data to AI agents. Using CIP as a declarative standard, it enforces RBAC and ABAC, executes integrations, and exposes trusted data via MCP and OpenAPI."
6
6
  type: webapp
7
7
  language: python # Explicitly specify Python language
8
- version: 2.0.79
8
+ version: 2.0.82
9
9
  # Image Configuration
10
10
  # Set tag to match your build (example: aifabrix build dataplane -t 1.0.0).
11
11
  # Registry is required so the controller can pull the image (avoids docker-not-found on the controller host).
@@ -26,7 +26,7 @@ frontDoorRouting:
26
26
  pattern: /data/*
27
27
  enabled: true
28
28
  # Docker url://internal full URLs: http://dataplane:PORT only (ingress /data is not on in-container routes).
29
- internalDockerUseOriginOnly: false
29
+ internalDockerUseOriginOnly: true
30
30
  host: ${DEV_USERNAME}.${REMOTE_HOST}
31
31
  tls: ${TLS_ENABLED}
32
32
  # Origin RFC 9728 PRM (outside /data PathPrefix) — Builder Traefik pass-through (plan 171.0).
@@ -44,7 +44,7 @@ API_KEY_BYPASS_ROLES=full-access
44
44
 
45
45
  # API Configuration
46
46
  API_V1_STR=/api/v1
47
- VERSION=2.0.79
47
+ VERSION=2.0.82
48
48
  # Minimum supported aifabrix-builder CLI semver (advertised on /health and /api/v1/health; empty = no gate)
49
49
  # MIN_BUILDER_CLI_VERSION=2.45.0
50
50
  MIN_BUILDER_CLI_VERSION=2.45.5
@@ -0,0 +1,91 @@
1
+ # AI Fabrix Builder app config — INTEGRATOR server type.
2
+ #
3
+ # Same code as builder/builder-server/, different installation. One source tree, two
4
+ # manifests: the Dockerfile's `integrator` target and this folder's env.template are the
5
+ # only things that differ, so there is no forked codebase to keep in step.
6
+ #
7
+ # Enterprise is the existing full development box with Docker. Integrator is a single
8
+ # container giving developers SSH-only access, with no Docker execution inside it.
9
+
10
+ app:
11
+ key: integration-server
12
+ displayName: AI Fabrix Integration Server
13
+ description: AI Fabrix Integration Server onboards developers over SSH into one container. It creates their Linux accounts and authorized keys, and gives them SSH-only access without Docker inside that container.
14
+ type: webapp
15
+ version: 2.0.10
16
+ image:
17
+ name: aifabrix/integration-server
18
+ tag: latest
19
+ registry: aifabrixacr.azurecr.io
20
+ registryMode: external
21
+ # Base 3006 keeps it clear of builder-server (3002), dataplane (3001), mori-controller
22
+ # (3004) and builder-api (3005), so both server types can run on one dev host.
23
+ # Builder derives the host port as `port` + developerId * 100 (dev01 -> 3106).
24
+ port: 3006
25
+ webSocketsEnabled: true
26
+ # This container administers its own operating system: it creates the developers'
27
+ # Linux accounts, writes /etc/ssh/authorized_keys.d and runs sshd. Builder otherwise
28
+ # runs containers as the invoking host user, which cannot do any of that.
29
+ runAsRoot: true
30
+ # Developers reach this installation over SSH, so that port has to be published as well
31
+ # as the HTTP one. The base keeps SSH's well-known number and the published port follows
32
+ # the same formula as the main port — 22 + developerId * 100 — so dev01 gets 122 and two
33
+ # developers on one host cannot collide. Bootstrap advertises it via SSH_PUBLIC_PORT.
34
+ additionalPorts:
35
+ - name: ssh
36
+ containerPort: 22
37
+ hostPort: 22
38
+ requires:
39
+ database: false
40
+ redis: false
41
+ storage: true
42
+ build:
43
+ language: typescript
44
+ context: ../..
45
+ dockerfile: builder/builder-server/Dockerfile
46
+ # Selects the Dockerfile's integrator stage. Without it Docker builds the last stage,
47
+ # which is enterprise — the same image under a different name. This is the one build
48
+ # difference between the two installations.
49
+ target: integrator
50
+ # No envOutputPath: this installation only runs in Docker, and Builder treats the
51
+ # value as a directory — pointing it at a file name creates a folder of that name.
52
+ #
53
+ # Reload runs the real entrypoint, not a bare watcher. The generated compose bind-mounts
54
+ # the repository over /app and clears the image ENTRYPOINT, which for this installation
55
+ # meant no sshd, no account-ops worker, and the API running as root — so nothing about
56
+ # SSH, provisioning or privilege separation worked in the mode developers actually use,
57
+ # and a queued account was never created. The entrypoint finds the build itself —
58
+ # /app/dist in the image, /app/builder-server/dist under the bind mount — because the
59
+ # compose template HTML-escapes `=`, so a VAR=value prefix never survives as an
60
+ # assignment.
61
+ #
62
+ # The API therefore starts from the host's build, so a code change is
63
+ # `npm run build --prefix builder-server` and a restart, not an in-container watch. That
64
+ # is deliberate: the watcher ran as root and left root-owned dist/ behind, which then
65
+ # broke the next `pnpm check` on the host.
66
+ reloadStart: exec /docker-entrypoint-integrator.sh
67
+ # Ingress. A distinct prefix from builder-server's /server/*, which is not cosmetic: the
68
+ # two installation types are alternatives in production, but a development host runs both,
69
+ # and two applications declaring the same host and prefix collide. Traefik then routes the
70
+ # prefix to whichever container was created last, so integrator-only routes answer 404
71
+ # from the enterprise container — a failure that looks like a bug in the route rather than
72
+ # a collision in the manifests.
73
+ #
74
+ # The pattern is a PathPrefix, not an allowlist: it forwards every path beneath it, so the
75
+ # application refuses anything else that arrives through the front door. Filtering at the
76
+ # ingress is still required and is tracked as open in 029 V4.
77
+ frontDoorRouting:
78
+ pattern: /integrator/*
79
+ enabled: true
80
+ host: ${DEV_USERNAME}.${REMOTE_HOST}
81
+ tls: ${TLS_ENABLED}
82
+ internalDockerUseOriginOnly: true
83
+ repository:
84
+ enabled: false
85
+ repositoryUrl: ''
86
+ deployment:
87
+ controllerUrl: ''
88
+ environment: dev
89
+ healthCheck:
90
+ path: /health
91
+ interval: 30