@aifabrix/builder 2.62.0 → 2.63.1

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 (34) hide show
  1. package/lib/api/onboarding-status.api.js +117 -0
  2. package/lib/app/register.js +24 -2
  3. package/lib/app/run-env-compose.js +39 -2
  4. package/lib/app/run-env-recovery.js +1 -0
  5. package/lib/app/run-helpers.js +1 -1
  6. package/lib/cli/infra-guided.js +30 -2
  7. package/lib/cli/setup-platform.js +18 -3
  8. package/lib/commands/setup-modes.js +2 -1
  9. package/lib/commands/setup-onboarding-readiness.js +83 -0
  10. package/lib/commands/setup.js +32 -10
  11. package/lib/commands/up-dataplane-bootstrap.js +115 -38
  12. package/lib/commands/up-dataplane.js +4 -31
  13. package/lib/core/local-env-overrides.js +23 -2
  14. package/lib/deployment/installation/index.js +4 -1
  15. package/lib/deployment/installation/infra-catalog.js +0 -2
  16. package/lib/deployment/installation/local-environment-authorize.js +221 -0
  17. package/lib/deployment/installation/local-environment-snapshot.js +124 -0
  18. package/lib/deployment/installation/local-environment-stage.js +43 -66
  19. package/lib/generator/index.js +32 -9
  20. package/lib/parameters/rsa-secret-values.js +14 -31
  21. package/lib/schema/infra.parameter.yaml +2 -45
  22. package/lib/schema/infrastructure-schema.json +0 -58
  23. package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
  24. package/lib/utils/log-redaction.js +4 -1
  25. package/package.json +2 -1
  26. package/templates/README.md +6 -0
  27. package/templates/applications/builder-api/application.yaml +1 -1
  28. package/templates/applications/dataplane/application.yaml +1 -1
  29. package/templates/applications/dataplane/env.template +1 -1
  30. package/templates/applications/mirrored-templates.json +2 -2
  31. package/templates/applications/miso-controller/application.yaml +18 -1
  32. package/templates/applications/miso-controller/env.template +3 -21
  33. package/templates/applications/miso-controller/rbac.yaml +10 -6
  34. package/templates/marketplace/main.json +5 -69
@@ -19,6 +19,10 @@ const logger = require('../../utils/logger');
19
19
  const { resolveDatabaseNamesWithAdoption } = require('../../parameters/database-adoption');
20
20
  const { parseDatabaseSecretKey } = require('../../parameters/database-secret-values');
21
21
  const { resolveLocalInstallationName } = require('./local-installation');
22
+ const {
23
+ authorizeEnvironmentWithController
24
+ } = require('./local-environment-authorize');
25
+ const { loadEnvironmentBootstrapSnapshot } = require('./local-environment-snapshot');
22
26
 
23
27
  /**
24
28
  * Applications that exist **once per environment**.
@@ -127,65 +131,6 @@ async function ensureEnvironmentDatabases(envKey, deps) {
127
131
  return ensured;
128
132
  }
129
133
 
130
- /**
131
- * Ask Miso whether this environment may exist, before anything is created.
132
- *
133
- * **Editions are Miso's, not Builder's.** Whether a licence permits `tst` or `pro` is not a
134
- * question this repository answers or caches: it asks, and Miso accepts or declines. Builder
135
- * holding its own copy of the entitlement rules would be a second answer to drift from the
136
- * licence, and a local one at that — the easiest possible thing to edit.
137
- *
138
- * This runs **first**. A decline after the databases exist would leave an environment's
139
- * objects behind for an environment that is not licensed, and nothing would later remove
140
- * them.
141
- *
142
- * @async
143
- * @param {string} envKey - Environment key
144
- * @param {Object} deps - Injected collaborators
145
- * @returns {Promise<void>}
146
- * @throws {Error} When Miso declines, or cannot be reached to ask
147
- */
148
- async function authorizeEnvironmentWithController(envKey, deps) {
149
- if (typeof deps.authorizeEnvironment === 'function') {
150
- await deps.authorizeEnvironment(envKey);
151
- return;
152
- }
153
- const { resolveControllerUrl } = require('../../utils/controller-url');
154
- const { getOrRefreshDeviceToken } = require('../../utils/token-manager');
155
- const { createEnvironment } = require('../../api/environments.api');
156
-
157
- const controllerUrl = await resolveControllerUrl();
158
- if (!controllerUrl) {
159
- throw new Error(
160
- `Cannot create ${envKey}: no controller is configured, so whether your licence ` +
161
- 'permits this environment cannot be checked. Run "aifabrix login" first.'
162
- );
163
- }
164
- const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
165
- if (!deviceToken || !deviceToken.token) {
166
- throw new Error(
167
- `Cannot create ${envKey}: not signed in, so whether your licence permits this ` +
168
- 'environment cannot be checked. Run "aifabrix login" first.'
169
- );
170
- }
171
- const authConfig = {
172
- type: 'device',
173
- token: deviceToken.token,
174
- controller: deviceToken.controller || controllerUrl
175
- };
176
- try {
177
- await createEnvironment(controllerUrl, authConfig, { key: envKey, environment: envKey });
178
- } catch (error) {
179
- // Miso owns the edition rules, so its refusal is reported as given rather than
180
- // reinterpreted here. Guessing at the reason would put a second, stale explanation of
181
- // the licence in front of the operator.
182
- throw new Error(
183
- `Miso declined environment ${envKey}: ${error.message}\n` +
184
- 'Editions are licensed in Miso. Nothing was created locally.'
185
- );
186
- }
187
- }
188
-
189
134
  /**
190
135
  * Workloads `env deploy` promotes into a promotion-only environment.
191
136
  *
@@ -241,24 +186,32 @@ async function promoteWorkloadsForEnvironment(envKey, deps) {
241
186
  * @param {string} installationKey - Positional installation key (`local` or `local-…`)
242
187
  * @param {string} envKey - Validated stage, one of dev|tst|pro
243
188
  * @param {Object} [deps] - Injected collaborators
244
- * @returns {Promise<{ stage: string, vendor: string, installation: string, databases: string[], promoted: string[] }>}
189
+ * @returns {Promise<{ stage: string, vendor: string, installation: string, databases: string[], promoted: string[], snapshot: { environmentId?: string, applicationId?: string }|null }>}
245
190
  */
246
191
  async function runLocalEnvironmentStage(installationKey, envKey, deps = {}) {
247
192
  const config = deps.config || require('../../core/config');
248
193
  const policy = deps.policy || require('../../core/environment-mode-policy');
194
+ const authorize = deps.authorizeEnvironmentWithController || authorizeEnvironmentWithController;
195
+ const loadSnapshot = deps.loadEnvironmentBootstrapSnapshot || loadEnvironmentBootstrapSnapshot;
196
+
197
+ const developerId = await config.getDeveloperId();
198
+ const installation = resolveLocalInstallationName(installationKey, developerId);
249
199
 
250
200
  // Miso first: it owns editions, and it accepts or declines this environment. Everything
251
- // below creates something, so the licence answer has to come before any of it.
252
- await authorizeEnvironmentWithController(envKey, deps);
201
+ // below creates something, so the licence answer has to come before any of it. Local
202
+ // create uses the same configuration fields as Azure (preset, serviceName, location).
203
+ await authorize(envKey, {
204
+ ...deps,
205
+ installationKey,
206
+ serviceName: installation,
207
+ preset: deps.preset
208
+ });
253
209
 
254
210
  // Then the local precondition. tst and pro are unreachable outside the front-door mode,
255
211
  // so creating their databases would leave resources behind for an environment that cannot
256
212
  // serve a request.
257
213
  await policy.assertEnvironmentAllowedForCurrentMode(envKey);
258
214
 
259
- const developerId = await config.getDeveloperId();
260
- const installation = resolveLocalInstallationName(installationKey, developerId);
261
-
262
215
  logger.log(`Deploying environment ${envKey} into local installation ${installation}`);
263
216
 
264
217
  // Databases before containers: a workload that starts without its database fails on its
@@ -271,7 +224,30 @@ async function runLocalEnvironmentStage(installationKey, envKey, deps = {}) {
271
224
  logger.log(`Promoted into ${envKey}: ${promoted.join(', ')}`);
272
225
  }
273
226
 
274
- return { stage: envKey, vendor: 'DOCKER', installation, databases, promoted };
227
+ const snapshot = await summarizeEnvironmentSnapshot(envKey, loadSnapshot, deps);
228
+ return { stage: envKey, vendor: 'DOCKER', installation, databases, promoted, snapshot };
229
+ }
230
+
231
+ /**
232
+ * Load this environment's own bootstrap snapshot after the dataplane can register.
233
+ *
234
+ * @async
235
+ * @param {string} envKey
236
+ * @param {Function} loadSnapshot
237
+ * @param {Object} deps
238
+ * @returns {Promise<{ environmentId?: string, applicationId?: string }>}
239
+ */
240
+ async function summarizeEnvironmentSnapshot(envKey, loadSnapshot, deps) {
241
+ const snapshotPayload = await loadSnapshot(envKey, deps);
242
+ const context = (snapshotPayload && snapshotPayload.context) || {};
243
+ logger.log(
244
+ `Loaded configuration snapshot for ${envKey}` +
245
+ (context.environmentId ? ` (environment ${context.environmentId})` : '')
246
+ );
247
+ return {
248
+ environmentId: context.environmentId,
249
+ applicationId: context.applicationId
250
+ };
275
251
  }
276
252
 
277
253
  module.exports = {
@@ -281,5 +257,6 @@ module.exports = {
281
257
  resolveEnvironmentDatabasePlan,
282
258
  ensureEnvironmentDatabases,
283
259
  promoteWorkloadsForEnvironment,
260
+ loadEnvironmentBootstrapSnapshot,
284
261
  runLocalEnvironmentStage
285
262
  };
@@ -22,6 +22,22 @@ const { generateControllerManifest } = require('./external-controller-manifest')
22
22
  const { resolveVersionForApp } = require('../utils/image-version');
23
23
  const { getContainerPort } = require('../utils/port-resolver');
24
24
  const { buildEnvVarMap } = require('../utils/env-map');
25
+ const config = require('../core/config');
26
+ const {
27
+ resolveDimensionAbacSetting,
28
+ applyDimensionAbacToConfiguration
29
+ } = require('../utils/dataplane-dimension-abac-setting');
30
+
31
+ async function resolveDimensionAbacValue(appName) {
32
+ if (appName !== 'dataplane') return undefined;
33
+ const userConfig = await config.getConfig();
34
+ return resolveDimensionAbacSetting(userConfig).value;
35
+ }
36
+
37
+ async function applyResolvedDimensionAbacToConfiguration(appName, configuration) {
38
+ const value = await resolveDimensionAbacValue(appName);
39
+ if (value !== undefined) applyDimensionAbacToConfiguration(appName, configuration, value);
40
+ }
25
41
  const { mergeAdditionalPublishedPorts } = require('../utils/docker-manifest-public-port');
26
42
 
27
43
  /**
@@ -115,6 +131,13 @@ function substitutePortInDeployment(obj, portNumber) {
115
131
  }
116
132
  }
117
133
 
134
+ function normalizeDeploymentPort(deployment, effectivePort) {
135
+ if (deployment.port === undefined) return;
136
+ deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
137
+ ? parseInt(deployment.port, 10)
138
+ : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
139
+ }
140
+
118
141
  /** Regex to find ${VAR} placeholders for env substitution */
119
142
  const ENV_VAR_PLACEHOLDER_REGEX = /\$\{([^}]+)\}/g;
120
143
 
@@ -179,6 +202,9 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac, optio
179
202
  // Parse environment variables from template and merge portalInput from application config.
180
203
  // A known deployment target resolves manifest {NAME} values; otherwise they stay for the Controller.
181
204
  const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
205
+ if (options && options.dimensionAbacValue !== undefined) {
206
+ applyDimensionAbacToConfiguration(appName, configuration, options.dimensionAbacValue);
207
+ }
182
208
 
183
209
  // Build deployment manifest (Controller computes deploymentKey from schema)
184
210
  const deployment = builders.buildManifestStructure(appName, variables, configuration, rbac);
@@ -193,10 +219,7 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac, optio
193
219
  // Replace ${PORT} with port from application.yaml so manifest deploys correctly
194
220
  const effectivePort = getEffectivePortForSubstitution(variables, 3000);
195
221
  substitutePortInDeployment(deployment, effectivePort);
196
- if (deployment.port !== undefined) {
197
- deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
198
- ? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
199
- }
222
+ normalizeDeploymentPort(deployment, effectivePort);
200
223
 
201
224
  // Resolve ${REDIS_HOST}, ${DB_HOST}, etc. from env-config.yaml so manifest has no unresolved vars
202
225
  const envVarMap = options && options.envVarMap;
@@ -239,14 +262,12 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
239
262
  app: { ...variables.app, version: resolved.version }
240
263
  };
241
264
  const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
265
+ await applyResolvedDimensionAbacToConfiguration(appName, configuration);
242
266
  const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
243
267
 
244
268
  const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
245
269
  substitutePortInDeployment(deployment, effectivePort);
246
- if (deployment.port !== undefined) {
247
- deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
248
- ? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
249
- }
270
+ normalizeDeploymentPort(deployment, effectivePort);
250
271
  const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
251
272
  await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
252
273
  substituteEnvVarsInDeployment(deployment, envVarMap);
@@ -302,10 +323,12 @@ async function writeRegularDeployJson(appName, appPath, appType, options = {}) {
302
323
  };
303
324
  const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
304
325
  const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
326
+ const dimensionAbacValue = await resolveDimensionAbacValue(appName);
305
327
  await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
306
328
  const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, {
307
329
  envVarMap,
308
- target: options.target
330
+ target: options.target,
331
+ dimensionAbacValue
309
332
  });
310
333
  const jsonContent = JSON.stringify(deployment, null, 2);
311
334
  fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
@@ -30,30 +30,14 @@ function readPrivateKey(value, encryptionKey, pairId) {
30
30
  /**
31
31
  * Pairs whose private half belongs to Mori, not to an installation.
32
32
  *
33
- * Builder must never mint these. Licence trust is a two-signature chain, and both links
34
- * exist so that substituting a key fails rather than silently succeeding:
33
+ * Builder must never mint these. Generating either half would hand an installation the
34
+ * private key of material it only verifies against, so it could sign a licence it then
35
+ * accepts. 227.1 pinned the verifying key "precisely because a key travelling inside the
36
+ * token it verifies proves nothing"; a self-generated key proves exactly as little.
35
37
  *
36
- * 1. Mori signs the licence. The installation verifies it against the public key held on
37
- * disk, so a licence from anywhere else does not verify.
38
- * 2. That on-disk material is itself validated against a key in Key Vault. The Key Vault is
39
- * provisioned by the Marketplace deployment and readable only by Miso's managed
40
- * identity — nobody can change it, including us — so swapping the on-disk public key for
41
- * one whose private half an attacker holds fails at the second check.
42
- *
43
- * The second link is an **Azure** guarantee. A Docker installation has no Key Vault and no
44
- * managed identity, so locally the chain is the pinned public key alone. Closing that is
45
- * online validation against Mori, which is a known gap deferred on purpose: the delivery
46
- * target is Azure, and Docker exists to validate the shape of that stack.
47
- *
48
- * It does not soften this rule. The one environment lacking the second check is the last
49
- * one that should mint its own keys, and a locally generated pair would make local
50
- * behaviour diverge from the Azure stack it exists to mirror.
51
- *
52
- * Generating the pair locally breaks both links at once: the installation would hold the
53
- * private half of the key it verifies against, so it could sign a licence it then accepts,
54
- * and the material would no longer correspond to what Key Vault validates. 227.1 pinned
55
- * this key "precisely because a key travelling inside the token it verifies proves
56
- * nothing"; a self-generated key proves exactly as little.
38
+ * How Miso pins, stores, or cross-checks that public key (Key Vault, Docker secrets file,
39
+ * online Mori check) is Miso's contract — Builder has no code path that inspects a vault
40
+ * or deployment mode. Builder's job stops at: refuse to mint, resolve what was supplied.
57
41
  *
58
42
  * The public half reaches an installation through the `moriLicensePublicKey` deployment
59
43
  * parameter, which the installation contract already classifies as registration-supplied.
@@ -120,8 +104,8 @@ function generateRsaSecretValues(keys, existing = {}, encryptionKey) {
120
104
  if (populated(existing[key]) || populated(values[key])) continue;
121
105
  const entry = catalog.findEntryForKey(key);
122
106
  if (entry?.generator?.type !== 'rsaKeyPair') continue;
123
- // Supplied by Mori at deployment and validated against Key Vault. Generating it here
124
- // would hand the installation the private half of the key it verifies licences against.
107
+ // Supplied by Mori; never generated here. Minting it would hand the installation the
108
+ // private half of the key it verifies licences against.
125
109
  if (isIssuerSuppliedPair(entry)) continue;
126
110
  const entries = catalog.data.parameters.filter(e => e.key &&
127
111
  e.generator?.type === 'rsaKeyPair' && e.generator.pairId === entry.generator.pairId);
@@ -146,12 +130,11 @@ function generateRsaSecretValues(keys, existing = {}, encryptionKey) {
146
130
  function generateRsaValueFromCatalog(key, catalogEntry) {
147
131
  if (isIssuerSuppliedPair(catalogEntry)) {
148
132
  throw new Error(
149
- `Secret "${key}" is supplied by Mori and is never generated here. Licence trust is a ` +
150
- 'two-signature chain: Mori signs the licence, which is verified against the on-disk ' +
151
- 'public key, and that material is validated against a Key Vault key readable only by ' +
152
- 'Miso\'s managed identity. The public half reaches an installation through the ' +
153
- 'moriLicensePublicKey deployment parameter. An installation with neither stays ' +
154
- 'unpinned and refuses every licence, which is the intended failure.'
133
+ `Secret "${key}" is supplied by Mori and is never generated here. An installation that ` +
134
+ 'minted it would hold the private half of the key it verifies licences against. The ' +
135
+ 'public half reaches an installation through the moriLicensePublicKey deployment ' +
136
+ 'parameter. An installation with neither stays unpinned and refuses every licence, ' +
137
+ 'which is the intended failure.'
155
138
  );
156
139
  }
157
140
  return generateRsaSecretValues([key])[key];
@@ -28,10 +28,6 @@ standardUpInfraEnsureKeys:
28
28
  - databases-dataplane-2-passwordKeyVault
29
29
  - databases-dataplane-3-urlKeyVault
30
30
  - databases-dataplane-3-passwordKeyVault
31
- # Miso-controller Entra onboarding (vendor app); required for aifabrix resolve miso-controller
32
- - entra-callback-uri
33
- - entra-client-idKeyVault
34
- - entra-client-secretKeyVault
35
31
  parameters:
36
32
  - key: postgres-passwordKeyVault
37
33
  scope: infra
@@ -351,47 +347,8 @@ parameters:
351
347
 
352
348
  # Miso-controller Entra ID onboarding (vendor multi-tenant app). Empty client/callback values
353
349
  # allow local resolve and start-dev; set real values before Entra admin-consent flows.
354
- - key: entra-callback-uri
355
- category: Microsoft Entra ID
356
- sensitive: false
357
- valueSource: optionalInput
358
- requiredOnSystemOnboarding: false
359
- scope: system
360
- generator:
361
- type: emptyString
362
- ensureOn: [upInfra, resolveApp]
363
- azure:
364
- notes: >-
365
- ENTRA_ID_CENTRAL_CALLBACK_URI — Mori web redirect (e.g. …/entra-id-concent/callback);
366
- empty locally until Mori URL is configured.
367
-
368
- - key: entra-client-idKeyVault
369
- category: Microsoft Entra ID
370
- sensitive: false
371
- valueSource: optionalInput
372
- requiredOnSystemOnboarding: false
373
- scope: system
374
- generator:
375
- type: emptyString
376
- ensureOn: [upInfra, resolveApp]
377
- azure:
378
- notes: >-
379
- Vendor Entra app client id (ENTRA_ID_CLIENT_ID); empty for local dev without Entra flows.
380
-
381
- - key: entra-client-secretKeyVault
382
- category: Microsoft Entra ID
383
- sensitive: true
384
- valueSource: optionalInput
385
- requiredOnSystemOnboarding: false
386
- scope: system
387
- generator:
388
- type: emptyString
389
- ensureOn: [upInfra, resolveApp]
390
- azure:
391
- legacyVaultSecretNames:
392
- - entra-vendor-client-secret
393
- notes: >-
394
- Vendor Entra app client secret (ENTRA_ID_CLIENT_SECRET); empty for local dev without Entra flows.
350
+ # No entra-callback-uri, entra-client-idKeyVault or entra-client-secretKeyVault: Miso holds no
351
+ # Entra client credentials; Mori owns the registration and every sign-in (plan 248.0).
395
352
 
396
353
  # Keycloak events: webhook signature secret used by miso-controller (KEYCLOAK_EVENTS_SECRET).
397
354
  # This must exist on first boot because up-platform runs containers from templates (no prior resolve).
@@ -509,8 +509,6 @@
509
509
  "acrResourceId",
510
510
  "deployRoleAssignments",
511
511
  "moriBaseUrl",
512
- "entraIdClientId",
513
- "entraIdClientSecret",
514
512
  "entraIdTenantId",
515
513
  "licenseEdition",
516
514
  "capacityProfile",
@@ -970,62 +968,6 @@
970
968
  "encryptionKey": {
971
969
  "$ref": "#/definitions/envReference"
972
970
  },
973
- "entraIdClientId": {
974
- "type": "object",
975
- "required": [
976
- "value",
977
- "source",
978
- "description"
979
- ],
980
- "additionalProperties": false,
981
- "properties": {
982
- "value": {
983
- "type": "null"
984
- },
985
- "source": {
986
- "type": "string",
987
- "minLength": 1
988
- },
989
- "description": {
990
- "type": "string",
991
- "minLength": 1
992
- },
993
- "required": {
994
- "type": "boolean"
995
- },
996
- "requiredAt": {
997
- "const": "tenantActivation"
998
- }
999
- }
1000
- },
1001
- "entraIdClientSecret": {
1002
- "type": "object",
1003
- "required": [
1004
- "value",
1005
- "source",
1006
- "description"
1007
- ],
1008
- "additionalProperties": false,
1009
- "properties": {
1010
- "value": {
1011
- "type": "null"
1012
- },
1013
- "source": {
1014
- "type": "string",
1015
- "minLength": 1
1016
- },
1017
- "description": {
1018
- "type": "string",
1019
- "minLength": 1
1020
- },
1021
- "required": {
1022
- "type": "boolean"
1023
- },
1024
- "requiredAt": {
1025
- "const": "tenantActivation"
1026
- }
1027
- }
1028
- },
1029
971
  "entraIdTenantId": {
1030
972
  "type": "object",
1031
973
  "required": [
@@ -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
+ };
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.62.0",
3
+ "version": "2.63.1",
4
4
  "description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -42,6 +42,7 @@
42
42
  "check:cli-help": "node scripts/check-cli-help-examples.js",
43
43
  "check:mirrored-templates": "node scripts/check-mirrored-templates.js",
44
44
  "test:manual": "jest --config jest.config.manual.js --runInBand",
45
+ "test:manual:public": "jest --config jest.config.manual-public.js --runInBand",
45
46
  "test:integration": "jest --config jest.config.integration.js --runInBand",
46
47
  "test:integration:fixtures": "jest --config jest.config.integration.fixtures.js --runInBand",
47
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.62.0
8
+ version: 2.63.1
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.82
8
+ version: 2.0.84
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).
@@ -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.82
47
+ VERSION=2.0.84
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