@aifabrix/builder 2.44.6 → 2.45.5

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 (257) hide show
  1. package/.cursor/rules/cli-layout.mdc +7 -3
  2. package/.cursor/rules/docs-rules.mdc +3 -1
  3. package/integration/hubspot-test/test-artifacts/wizard-hubspot-credential-real.yaml +1 -1
  4. package/integration/hubspot-test/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
  5. package/integration/hubspot-test/wizard-hubspot-e2e.yaml +1 -1
  6. package/jest.projects.js +100 -1
  7. package/lib/api/agent-metadata-validation.api.js +93 -0
  8. package/lib/api/audit-api-helpers.js +68 -0
  9. package/lib/api/audit.api.js +119 -0
  10. package/lib/api/auth-cache.api.js +44 -0
  11. package/lib/api/certificates.api.js +4 -4
  12. package/lib/api/credential.api.js +2 -2
  13. package/lib/api/dataplane-health.api.js +137 -0
  14. package/lib/api/dataplane-sync.api.js +27 -0
  15. package/lib/api/datasources-core.api.js +21 -18
  16. package/lib/api/datasources-extended.api.js +22 -22
  17. package/lib/api/dimensions.api.js +3 -0
  18. package/lib/api/external-systems.api.js +13 -13
  19. package/lib/api/governance-scenarios.api.js +39 -0
  20. package/lib/api/groups.api.js +79 -0
  21. package/lib/api/index.js +45 -2
  22. package/lib/api/pipeline.api.js +4 -4
  23. package/lib/api/protection.api.js +157 -0
  24. package/lib/api/records-bulk.api.js +30 -0
  25. package/lib/api/records-search-parse.js +49 -0
  26. package/lib/api/records-search.api.js +29 -0
  27. package/lib/api/types/agent-metadata-validation.types.js +67 -0
  28. package/lib/api/types/audit.types.js +72 -0
  29. package/lib/api/types/dataplane-health.types.js +27 -0
  30. package/lib/api/types/dimensions.types.js +2 -0
  31. package/lib/api/types/governance-scenarios.types.js +39 -0
  32. package/lib/api/types/protection.types.js +69 -0
  33. package/lib/api/types/records-bulk.types.js +35 -0
  34. package/lib/api/types/records-search.types.js +28 -0
  35. package/lib/api/user-groups.api.js +50 -0
  36. package/lib/api/users.api.js +95 -0
  37. package/lib/api/validation-run.api.js +3 -3
  38. package/lib/api/validation-runner.js +3 -3
  39. package/lib/api/wizard-platform.api.js +3 -3
  40. package/lib/api/wizard.api.js +21 -21
  41. package/lib/app/deploy.js +34 -19
  42. package/lib/app/helpers.js +3 -3
  43. package/lib/app/index.js +3 -3
  44. package/lib/app/register.js +7 -6
  45. package/lib/app/restart-display.js +52 -21
  46. package/lib/app/rotate-secret.js +7 -6
  47. package/lib/app/run-helpers.js +15 -8
  48. package/lib/app/run.js +57 -9
  49. package/lib/app/show-display.js +7 -0
  50. package/lib/app/show.js +87 -5
  51. package/lib/build/index.js +9 -5
  52. package/lib/cli/index.js +4 -2
  53. package/lib/cli/infra-guided.js +62 -28
  54. package/lib/cli/installation-log-command.js +73 -0
  55. package/lib/cli/setup-app.help.js +50 -2
  56. package/lib/cli/setup-app.js +44 -25
  57. package/lib/cli/setup-app.test-commands.js +80 -21
  58. package/lib/cli/setup-auth.js +124 -52
  59. package/lib/cli/setup-external-system.js +37 -3
  60. package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
  61. package/lib/cli/setup-infra-up-platform-action.js +131 -0
  62. package/lib/cli/setup-infra.js +60 -119
  63. package/lib/cli/setup-platform.js +1 -1
  64. package/lib/cli/setup-utility-help-after.js +45 -0
  65. package/lib/cli/setup-utility-resolve.js +132 -0
  66. package/lib/cli/setup-utility.js +148 -116
  67. package/lib/commands/app-logs.js +81 -33
  68. package/lib/commands/auth-config.js +116 -18
  69. package/lib/commands/auth-status-dataplane-version.js +231 -0
  70. package/lib/commands/auth-status-display.js +94 -0
  71. package/lib/commands/auth-status.js +70 -6
  72. package/lib/commands/credential-push.js +10 -5
  73. package/lib/commands/datasource-load-export-cli.js +178 -0
  74. package/lib/commands/datasource-test-trust-cli.js +114 -0
  75. package/lib/commands/datasource-unified-test-cli.js +25 -17
  76. package/lib/commands/datasource-unified-test-cli.options.js +18 -3
  77. package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +5 -5
  78. package/lib/commands/datasource-verify-audit-cli.js +96 -0
  79. package/lib/commands/datasource.js +68 -1
  80. package/lib/commands/dimension-value.js +43 -13
  81. package/lib/commands/dimension.js +38 -7
  82. package/lib/commands/identity-apply-sync.js +131 -0
  83. package/lib/commands/identity-group-cli.js +146 -0
  84. package/lib/commands/identity-membership-role-cache.js +175 -0
  85. package/lib/commands/identity-shared.js +94 -0
  86. package/lib/commands/identity-user-cli.js +158 -0
  87. package/lib/commands/identity.js +43 -0
  88. package/lib/commands/login.js +46 -16
  89. package/lib/commands/protection-cli-leaves.js +286 -0
  90. package/lib/commands/protection.js +50 -0
  91. package/lib/commands/repair.js +13 -3
  92. package/lib/commands/setup-modes.js +19 -6
  93. package/lib/commands/setup-prompts.js +41 -8
  94. package/lib/commands/setup.js +114 -9
  95. package/lib/commands/teardown.js +54 -5
  96. package/lib/commands/test-e2e-external.js +40 -9
  97. package/lib/commands/test-governance-cli-options.js +47 -0
  98. package/lib/commands/test-governance-command-action.js +55 -0
  99. package/lib/commands/test-governance-external.js +88 -0
  100. package/lib/commands/test-trust-cli-options.js +32 -0
  101. package/lib/commands/test-trust-command-action.js +118 -0
  102. package/lib/commands/test-trust-external.js +127 -0
  103. package/lib/commands/up-common.js +48 -14
  104. package/lib/commands/up-dataplane.js +21 -18
  105. package/lib/commands/up-miso.js +12 -8
  106. package/lib/commands/upload.js +38 -15
  107. package/lib/commands/wizard-core.js +3 -0
  108. package/lib/core/audit-logger.js +1 -34
  109. package/lib/core/config-admin-email.js +56 -0
  110. package/lib/core/config-device-dataplane.js +185 -0
  111. package/lib/core/config-normalize.js +60 -0
  112. package/lib/core/config-registered-controller-urls.js +54 -0
  113. package/lib/core/config.js +33 -50
  114. package/lib/core/secrets-ensure-infra.js +1 -1
  115. package/lib/core/secrets-env-content.js +86 -90
  116. package/lib/core/secrets-env-declarative-expand.js +170 -0
  117. package/lib/core/secrets-env-write.js +2 -0
  118. package/lib/core/secrets-load.js +106 -102
  119. package/lib/datasource/agent-trust-debug-log.js +45 -0
  120. package/lib/datasource/agent-trust-fetch.js +115 -0
  121. package/lib/datasource/agent-trust-map.js +101 -0
  122. package/lib/datasource/agent-trust-mock.js +44 -0
  123. package/lib/datasource/agent-trust-run.js +93 -0
  124. package/lib/datasource/agent-trust-timeout.js +25 -0
  125. package/lib/datasource/audit-evidence-display.js +80 -0
  126. package/lib/datasource/audit-evidence-extract-debug.js +103 -0
  127. package/lib/datasource/audit-evidence-extract.js +177 -0
  128. package/lib/datasource/audit-evidence-matrix-rows.js +158 -0
  129. package/lib/datasource/audit-evidence-operation-aliases.js +63 -0
  130. package/lib/datasource/audit-evidence-poll.js +109 -0
  131. package/lib/datasource/audit-evidence-run.js +177 -0
  132. package/lib/datasource/audit-evidence-verify-helpers.js +167 -0
  133. package/lib/datasource/audit-evidence-verify-matrix.js +177 -0
  134. package/lib/datasource/audit-evidence-verify.js +108 -0
  135. package/lib/datasource/bulk-loader-run-batches.js +88 -0
  136. package/lib/datasource/bulk-loader-service-retry.js +52 -0
  137. package/lib/datasource/bulk-loader-service.js +127 -0
  138. package/lib/datasource/capability/run-capability-dimension.js +2 -1
  139. package/lib/datasource/datasource-exporter-resolve.js +82 -0
  140. package/lib/datasource/datasource-exporter-service.js +144 -0
  141. package/lib/datasource/datasource-identifier.js +148 -0
  142. package/lib/datasource/datasource-load-export-context.js +92 -0
  143. package/lib/datasource/integration-context.js +6 -4
  144. package/lib/datasource/local-data-paths.js +145 -0
  145. package/lib/datasource/log-cleaner.js +241 -0
  146. package/lib/datasource/log-viewer.js +111 -6
  147. package/lib/datasource/record-file-parser.js +85 -0
  148. package/lib/datasource/record-mapper.js +129 -0
  149. package/lib/datasource/resolve-app.js +74 -55
  150. package/lib/datasource/test-e2e.js +26 -2
  151. package/lib/datasource/unified-validation-run-resolve.js +45 -12
  152. package/lib/datasource/unified-validation-run.js +43 -17
  153. package/lib/datasource/validate.js +37 -20
  154. package/lib/external-system/deploy.js +5 -1
  155. package/lib/external-system/test-system-level.js +25 -4
  156. package/lib/external-system/test.js +3 -1
  157. package/lib/governance/governance-pack-loader.js +70 -0
  158. package/lib/governance/governance-run-display.js +119 -0
  159. package/lib/identity/identity-apply-core.js +108 -0
  160. package/lib/identity/identity-apply-phases.js +244 -0
  161. package/lib/identity/identity-apply-service.js +77 -0
  162. package/lib/identity/identity-csv-parser.js +114 -0
  163. package/lib/internal/fs-real-sync.js +29 -1
  164. package/lib/internal/node-fs.js +2 -0
  165. package/lib/protection/auth-context.js +45 -0
  166. package/lib/protection/convert-batch.js +90 -0
  167. package/lib/protection/load.js +41 -0
  168. package/lib/protection/paths.js +158 -0
  169. package/lib/protection/preflight-datasource-ready.js +46 -0
  170. package/lib/protection/protection-create-probe-lines.js +142 -0
  171. package/lib/protection/protection-create-probes.js +95 -0
  172. package/lib/protection/protection-create-scaffold.js +281 -0
  173. package/lib/protection/protection-display-helpers.js +445 -0
  174. package/lib/protection/protection-display.js +215 -0
  175. package/lib/protection/protection-preset-registry.js +97 -0
  176. package/lib/protection/report-exit.js +62 -0
  177. package/lib/protection/resolve.js +202 -0
  178. package/lib/protection/run-commands-validate.js +130 -0
  179. package/lib/protection/run-commands.js +216 -0
  180. package/lib/protection/run-protection-create-helpers.js +134 -0
  181. package/lib/protection/run-protection-create-probes-flow.js +43 -0
  182. package/lib/protection/run-protection-create.js +191 -0
  183. package/lib/protection/scope.js +22 -0
  184. package/lib/protection/sync-after-upload.js +64 -0
  185. package/lib/protection/upload-batch.js +127 -0
  186. package/lib/protection/validate-batch.js +113 -0
  187. package/lib/protection/validate-local.js +65 -0
  188. package/lib/resolvers/dimension-file.js +29 -4
  189. package/lib/schema/application-schema.json +4 -0
  190. package/lib/schema/external-datasource.schema.json +47 -13
  191. package/lib/schema/flag-map-validation-run.json +2 -2
  192. package/lib/schema/infra.parameter.yaml +10 -0
  193. package/lib/schema/protection.schema.json +374 -0
  194. package/lib/schema/type/credential-field-registry.json +636 -0
  195. package/lib/schema/wizard-config.schema.json +1 -1
  196. package/lib/utils/agent-trust-run-display.js +231 -0
  197. package/lib/utils/agent-trust-run-exit.js +59 -0
  198. package/lib/utils/agent-trust-wait-ui.js +94 -0
  199. package/lib/utils/app-config-resolver.js +24 -1
  200. package/lib/utils/applications-config-defaults.js +206 -0
  201. package/lib/utils/auth-config-validator.js +2 -12
  202. package/lib/utils/bash-secret-env.js +1 -1
  203. package/lib/utils/cli-exit-codes.js +30 -0
  204. package/lib/utils/cli-sync-options.js +33 -0
  205. package/lib/utils/compose-generate-docker-compose.js +111 -6
  206. package/lib/utils/compose-generator.js +17 -8
  207. package/lib/utils/controller-url.js +50 -7
  208. package/lib/utils/credential-push-ui.js +90 -0
  209. package/lib/utils/dataplane-cli-version-gate.js +202 -0
  210. package/lib/utils/dataplane-cli-version-help.js +74 -0
  211. package/lib/utils/datasource-export-display.js +137 -0
  212. package/lib/utils/datasource-load-display.js +174 -0
  213. package/lib/utils/datasource-load-export-ui.js +120 -0
  214. package/lib/utils/datasource-test-run-capability-scope.js +3 -3
  215. package/lib/utils/datasource-test-run-display.js +3 -3
  216. package/lib/utils/datasource-test-run-tty-log.js +5 -1
  217. package/lib/utils/env-copy.js +99 -14
  218. package/lib/utils/env-template.js +5 -1
  219. package/lib/utils/health-check-url.js +18 -15
  220. package/lib/utils/health-check.js +7 -5
  221. package/lib/utils/help-builder.js +30 -24
  222. package/lib/utils/infra-optional-service-flags.js +69 -0
  223. package/lib/utils/installation-log-core.js +282 -0
  224. package/lib/utils/installation-log-record.js +237 -0
  225. package/lib/utils/installation-log.js +123 -0
  226. package/lib/utils/log-redaction.js +105 -0
  227. package/lib/utils/manifest-location.js +164 -0
  228. package/lib/utils/manifest-source-emit.js +184 -0
  229. package/lib/utils/paths.js +238 -89
  230. package/lib/utils/remote-secrets-loader.js +7 -1
  231. package/lib/utils/run-cli-flags.js +29 -0
  232. package/lib/utils/secrets-canonical.js +10 -3
  233. package/lib/utils/secrets-path.js +3 -4
  234. package/lib/utils/secrets-utils.js +20 -10
  235. package/lib/utils/semver-compare.js +127 -0
  236. package/lib/utils/system-builder-root.js +10 -2
  237. package/lib/utils/upload-pipeline-ui.js +91 -0
  238. package/lib/utils/url-declarative-public-base.js +80 -12
  239. package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
  240. package/lib/utils/url-declarative-resolve-build.js +24 -393
  241. package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
  242. package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
  243. package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
  244. package/lib/utils/url-declarative-resolve.js +47 -7
  245. package/lib/utils/url-declarative-runtime-base-path.js +21 -1
  246. package/lib/utils/urls-local-registry-scan.js +103 -0
  247. package/lib/utils/urls-local-registry.js +161 -90
  248. package/lib/utils/validation-poll-ui.js +14 -7
  249. package/lib/validation/wizard-config-validator.js +58 -19
  250. package/package.json +3 -1
  251. package/templates/applications/dataplane/application.yaml +4 -0
  252. package/templates/applications/dataplane/env.template +13 -1
  253. package/templates/applications/dataplane/rbac.yaml +4 -0
  254. package/templates/applications/miso-controller/application.yaml +2 -0
  255. package/templates/applications/miso-controller/env.template +27 -29
  256. package/templates/external-system/README.md.hbs +56 -8
  257. package/.npmrc.token +0 -1
@@ -22,6 +22,23 @@ const { copyTemplateFiles } = require('../validation/template');
22
22
  const { ensureReadmeForAppPath, ensureReadmeForApp } = require('../app/readme');
23
23
  const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-registry');
24
24
 
25
+ /**
26
+ * @param {string[]} entries
27
+ * @returns {string[]}
28
+ */
29
+ function uniqueResolvedRoots(entries) {
30
+ const seen = new Set();
31
+ const out = [];
32
+ for (const e of entries) {
33
+ const r = path.resolve(e);
34
+ if (!seen.has(r)) {
35
+ seen.add(r);
36
+ out.push(r);
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
25
42
  /**
26
43
  * Copy template to a target path if application config is missing there.
27
44
  * After copy, generates README.md from templates/applications/README.md.hbs.
@@ -226,7 +243,11 @@ function isUnderAllowedBuilderRoot(resolvedAppPath, allowedRoots) {
226
243
  async function cleanBuilderAppDirs(appNames, opts = {}) {
227
244
  const silent = Boolean(opts.silent);
228
245
  if (!Array.isArray(appNames) || appNames.length === 0) return;
229
- const allowedRoots = [path.resolve(pathsUtil.getBuilderRoot()), path.resolve(pathsUtil.getSystemBuilderRoot())];
246
+ const allowedRoots = uniqueResolvedRoots([
247
+ path.join(process.cwd(), 'builder'),
248
+ pathsUtil.getBuilderRoot(),
249
+ pathsUtil.getSystemBuilderRoot()
250
+ ]);
230
251
  const cleaned = [];
231
252
  for (const appName of appNames) {
232
253
  if (!appName || typeof appName !== 'string') continue;
@@ -253,8 +274,9 @@ async function cleanBuilderAppDirs(appNames, opts = {}) {
253
274
  * Uses AIFABRIX_BUILDER_DIR when set (e.g. by up-miso/up-dataplane from config aifabrix-env-config).
254
275
  * When `process.env.AIFABRIX_BUILDER_DIR` is set and the primary app path differs from
255
276
  * `cwd/builder/<appName>`, also copies into `cwd/builder/<appName>` so the repo tree is not empty
256
- * while using a custom builder root. Skips that extra copy when no custom dir is in use (e.g.
257
- * platform apps materialized only under the system builder root).
277
+ * while using a custom builder root. Skips that extra copy for platform apps (`keycloak`,
278
+ * `miso-controller`, `dataplane`) so they materialize only under the system builder root, and when
279
+ * no custom dir is in use.
258
280
  *
259
281
  * @async
260
282
  * @function ensureAppFromTemplate
@@ -281,7 +303,8 @@ async function ensureAppFromTemplate(appName) {
281
303
  const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
282
304
  if (
283
305
  envBuilderRoot &&
284
- path.resolve(cwdBuilderPath) !== path.resolve(appPath)
306
+ path.resolve(cwdBuilderPath) !== path.resolve(appPath) &&
307
+ !pathsUtil.isSystemBuilderAppName(appName)
285
308
  ) {
286
309
  const cwdCopied = await ensureTemplateAtPath(appName, cwdBuilderPath);
287
310
  if (cwdCopied) {
@@ -295,22 +318,32 @@ async function ensureAppFromTemplate(appName) {
295
318
  }
296
319
 
297
320
  /**
298
- * For `aifabrix up-platform` only: align builder dir env with up-miso/up-dataplane, materialize all three
299
- * platform apps from templates if missing, then refresh `~/.aifabrix/urls.local.yaml` so declarative
300
- * `url://` expansion (e.g. cross-references between miso-controller, dataplane, keycloak) sees every
301
- * app's port and pattern before Keycloak or Miso resolve their `.env` files.
321
+ * Merge builder/packages `application.yaml` into `~/.aifabrix/urls.local.yaml`.
322
+ * Same scan as the end of {@link prepareUrlsLocalRegistryForUpPlatform}; call after platform app
323
+ * templates exist (e.g. `up-miso`, `up-dataplane`) so declarative `url://` and tooling see ports/patterns.
324
+ */
325
+ function refreshUrlsLocalRegistryForCurrentProject() {
326
+ try {
327
+ refreshUrlsLocalRegistryFromBuilder(pathsUtil.getProjectRoot());
328
+ } catch (error) {
329
+ logger.warn(chalk.yellow(`⚠ Could not refresh URLs registry: ${error.message}`));
330
+ }
331
+ }
332
+
333
+ /**
334
+ * For `aifabrix up-platform` only: materialize all three platform apps from templates if missing
335
+ * (paths follow cwd / material `builder/` only — no `AIFABRIX_BUILDER_DIR` override), then refresh
336
+ * `~/.aifabrix/urls.local.yaml` so declarative `url://` expansion (e.g. cross-references between
337
+ * miso-controller, dataplane, keycloak) sees every app's port and pattern before Keycloak or Miso
338
+ * resolve their `.env` files.
302
339
  *
303
340
  * @returns {Promise<void>}
304
341
  */
305
342
  async function prepareUrlsLocalRegistryForUpPlatform() {
306
- const builderDir = await config.getAifabrixBuilderDir();
307
- if (builderDir) {
308
- process.env.AIFABRIX_BUILDER_DIR = path.resolve(builderDir);
309
- }
310
343
  await ensureAppFromTemplate('keycloak');
311
344
  await ensureAppFromTemplate('miso-controller');
312
345
  await ensureAppFromTemplate('dataplane');
313
- refreshUrlsLocalRegistryFromBuilder(pathsUtil.getProjectRoot());
346
+ refreshUrlsLocalRegistryForCurrentProject();
314
347
  }
315
348
 
316
349
  module.exports = {
@@ -320,5 +353,6 @@ module.exports = {
320
353
  patchEnvOutputPathForDeployOnly,
321
354
  validateEnvOutputPathFolderOrNull,
322
355
  getEnvOutputPathFolder,
323
- prepareUrlsLocalRegistryForUpPlatform
356
+ prepareUrlsLocalRegistryForUpPlatform,
357
+ refreshUrlsLocalRegistryForCurrentProject
324
358
  };
@@ -12,7 +12,6 @@ const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-test
12
12
  * @version 2.0.0
13
13
  */
14
14
 
15
- const path = require('path');
16
15
  const readline = require('readline');
17
16
  const chalk = require('chalk');
18
17
  const pathsUtil = require('../utils/paths');
@@ -30,7 +29,7 @@ const { checkHealthEndpoint } = require('../utils/health-check');
30
29
  const { validateControllerUrl } = require('../utils/auth-config-validator');
31
30
  const app = require('../app');
32
31
  const { assertDevInfraUp } = require('./dev-infra-gate');
33
- const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull } = require('./up-common');
32
+ const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull, refreshUrlsLocalRegistryForCurrentProject } = require('./up-common');
34
33
 
35
34
  const CONTROLLER_HEALTH_PATH = '/health';
36
35
 
@@ -168,14 +167,17 @@ function buildDataplaneImageRef(options = {}) {
168
167
  }
169
168
  }
170
169
 
171
- /**
172
- * Sets `AIFABRIX_BUILDER_DIR` when configured (shared by deploy/run paths).
173
- * @returns {Promise<void>}
174
- */
175
- async function applyAifabrixBuilderDirEnv() {
176
- const builderDir = await config.getAifabrixBuilderDir();
177
- if (builderDir) {
178
- process.env.AIFABRIX_BUILDER_DIR = path.resolve(builderDir);
170
+ function emitDataplaneManifestLineFromBuilder() {
171
+ const { emitSystemBuilderAppManifestLineIfTTY } = require('../utils/manifest-source-emit');
172
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'dataplane');
173
+ }
174
+
175
+ function assertDevEnvironmentForDataplane(cfg) {
176
+ const environment = (cfg && cfg.environment) ? cfg.environment : 'dev';
177
+ if (environment !== 'dev') {
178
+ throw new Error(
179
+ 'Dataplane is only supported in dev environment. Set with: aifabrix auth --set-environment dev.'
180
+ );
179
181
  }
180
182
  }
181
183
 
@@ -195,7 +197,6 @@ async function applyAifabrixBuilderDirEnv() {
195
197
  * @throws {Error} If infra not up, controller unavailable, not logged in, environment not dev, or any step fails
196
198
  */
197
199
  async function handleUpDataplane(options = {}) {
198
- await applyAifabrixBuilderDirEnv();
199
200
  logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
200
201
 
201
202
  if (options.skipInfraCheck !== true) {
@@ -207,18 +208,19 @@ async function handleUpDataplane(options = {}) {
207
208
  const authConfig = await checkAuthentication(controllerUrl, environmentKey, { throwOnFailure: true });
208
209
 
209
210
  const cfg = await config.getConfig();
210
- const environment = (cfg && cfg.environment) ? cfg.environment : 'dev';
211
- if (environment !== 'dev') {
212
- throw new Error(
213
- 'Dataplane is only supported in dev environment. Set with: aifabrix auth --set-environment dev.'
214
- );
215
- }
211
+ assertDevEnvironmentForDataplane(cfg);
216
212
  logger.log(formatSuccessLine('Logged in and environment is dev'));
217
213
 
218
214
  await ensureAppFromTemplate('dataplane');
219
215
  // If envOutputPath target folder does not exist, set envOutputPath to null
220
216
  validateEnvOutputPathFolderOrNull('dataplane');
221
217
 
218
+ refreshUrlsLocalRegistryForCurrentProject();
219
+
220
+ if (options.platformInstall !== true) {
221
+ emitDataplaneManifestLineFromBuilder();
222
+ }
223
+
222
224
  await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
223
225
 
224
226
  await deployDataplaneToController(options);
@@ -226,7 +228,8 @@ async function handleUpDataplane(options = {}) {
226
228
  await app.runApp('dataplane', {
227
229
  skipEnvOutputPath: true,
228
230
  registry: options.registry || undefined,
229
- base: options.base !== false
231
+ base: options.base !== false,
232
+ skipManifestMetadataLine: true
230
233
  });
231
234
 
232
235
  logger.log(formatSuccessParagraph('up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
@@ -10,13 +10,16 @@ const { formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
10
10
  * @version 2.0.0
11
11
  */
12
12
 
13
- const path = require('path');
14
13
  const chalk = require('chalk');
15
14
  const logger = require('../utils/logger');
16
- const config = require('../core/config');
17
15
  const app = require('../app');
18
16
  const { assertDevInfraUp } = require('./dev-infra-gate');
19
- const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly, validateEnvOutputPathFolderOrNull } = require('./up-common');
17
+ const {
18
+ ensureAppFromTemplate,
19
+ patchEnvOutputPathForDeployOnly,
20
+ validateEnvOutputPathFolderOrNull,
21
+ refreshUrlsLocalRegistryForCurrentProject
22
+ } = require('./up-common');
20
23
 
21
24
  /**
22
25
  * Parse --image options array into map { keycloak?: string, 'miso-controller'?: string }
@@ -51,7 +54,8 @@ async function runMisoApps(options) {
51
54
  registryMode: options.registryMode,
52
55
  skipEnvOutputPath: true,
53
56
  skipInfraCheck: true,
54
- base: useBaseImage
57
+ base: useBaseImage,
58
+ skipManifestMetadataLine: true
55
59
  };
56
60
  const keycloakRunOpts = { ...common };
57
61
  if (imageMap.keycloak) {
@@ -61,9 +65,12 @@ async function runMisoApps(options) {
61
65
  if (imageMap['miso-controller']) {
62
66
  misoRunOpts.image = imageMap['miso-controller'];
63
67
  }
68
+ const { emitSystemBuilderAppManifestLineIfTTY } = require('../utils/manifest-source-emit');
64
69
  logger.log(chalk.blue('Starting keycloak...'));
70
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'keycloak');
65
71
  await app.runApp('keycloak', keycloakRunOpts);
66
72
  logger.log(chalk.blue('Starting miso-controller...'));
73
+ emitSystemBuilderAppManifestLineIfTTY(logger, 'miso-controller');
67
74
  await app.runApp('miso-controller', misoRunOpts);
68
75
  }
69
76
 
@@ -80,10 +87,6 @@ async function runMisoApps(options) {
80
87
  * @throws {Error} If infra not up or any step fails
81
88
  */
82
89
  async function handleUpMiso(options = {}) {
83
- const builderDir = await config.getAifabrixBuilderDir();
84
- if (builderDir) {
85
- process.env.AIFABRIX_BUILDER_DIR = path.resolve(builderDir);
86
- }
87
90
  logger.log(chalk.blue('Starting up-miso (keycloak + miso-controller from images)...\n'));
88
91
  await assertDevInfraUp();
89
92
  await ensureAppFromTemplate('keycloak');
@@ -94,6 +97,7 @@ async function handleUpMiso(options = {}) {
94
97
  // Deploy-only: do not copy .env to repo paths; patch variables so envOutputPath is null
95
98
  patchEnvOutputPathForDeployOnly('keycloak');
96
99
  patchEnvOutputPathForDeployOnly('miso-controller');
100
+ refreshUrlsLocalRegistryForCurrentProject();
97
101
  await runMisoApps(options);
98
102
  logger.log(formatSuccessParagraph('up-miso complete. Keycloak and miso-controller are running.') +
99
103
  chalk.gray('\n Run onboarding and register Keycloak from the miso-controller repo if needed. Use \'aifabrix up-dataplane\' for dataplane.'));
@@ -25,6 +25,8 @@ const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../uti
25
25
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
26
26
  const { getIntegrationPath } = require('../utils/paths');
27
27
  const { pushCredentialSecrets } = require('../utils/credential-secrets-env');
28
+ const { runWithCredentialPushSpinner } = require('../utils/credential-push-ui');
29
+ const { runWithUploadPipelineSpinner } = require('../utils/upload-pipeline-ui');
28
30
  const {
29
31
  buildResolvedEnvMapForIntegration,
30
32
  resolveConfigurationValues,
@@ -49,6 +51,7 @@ const {
49
51
  const { maybeSyncSystemCertificationFromDataplane } = require('../certification/sync-system-certification');
50
52
  const { cliOptsSkipCertSync } = require('../certification/cli-cert-sync-skip');
51
53
  const { maybeSyncOpenApiFilesForMcp } = require('./repair-openapi-sync');
54
+ const { assertDataplaneCliVersionCompatible } = require('../utils/dataplane-cli-version-gate');
52
55
 
53
56
  /**
54
57
  * Validates system-key format (same as download).
@@ -170,13 +173,17 @@ async function resolvePayloadConfiguration(systemKey, payload) {
170
173
  * @param {string} systemKey - System key
171
174
  * @param {Object} payload - Upload payload
172
175
  */
173
- async function pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload) {
176
+ async function pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload, uiOpts = {}) {
174
177
  const envFilePath = path.join(getIntegrationPath(systemKey), '.env');
175
- const pushResult = await pushCredentialSecrets(dataplaneUrl, authConfig, {
176
- envFilePath,
177
- appName: systemKey,
178
- payload
179
- });
178
+ const pushResult = await runWithCredentialPushSpinner(
179
+ () =>
180
+ pushCredentialSecrets(dataplaneUrl, authConfig, {
181
+ envFilePath,
182
+ appName: systemKey,
183
+ payload
184
+ }),
185
+ { systemKey, json: uiOpts.json === true }
186
+ );
180
187
  if (pushResult.pushed > 0) {
181
188
  const keyList = pushResult.keys?.length ? ` (${pushResult.keys.join(', ')})` : '';
182
189
  logger.log(
@@ -279,8 +286,10 @@ async function buildValidatedUploadManifestPayload(systemKey, _opts = {}) {
279
286
  }
280
287
 
281
288
  /**
282
- * Upload local `integration/<systemKey>/openapi/*.json` specs when present so pipeline can resolve
283
- * each datasource `openapi.documentKey` (same behavior as repair --api OpenAPI sync).
289
+ * Upload local `integration/<systemKey>/openapi/*.json` specs when present so dataplane can store
290
+ * vendor OpenAPI keyed by each datasource `openapi.documentKey` (same behavior as repair --api OpenAPI sync).
291
+ * Call this **after** a successful pipeline publish: list/upload endpoints require the external system row.
292
+ * First publish still succeeds because pipeline materializes internal specs from `openapi.operations` when needed.
284
293
  *
285
294
  * @param {string} systemKey
286
295
  * @param {Object} manifest
@@ -362,8 +371,13 @@ async function handlePublicationAndFollowups(ctx) {
362
371
  * @param {Object} payload
363
372
  */
364
373
  async function runUploadPublishAndSummary(systemKey, options, manifest, payload) {
365
- const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey);
374
+ const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey, {
375
+ silent: options.silentResolve === true
376
+ });
366
377
  requireBearerForDataplanePipeline(authConfig);
378
+ const controllerUrl = await resolveControllerUrl();
379
+ // Plan 142.0: gate dataplane API entrypoints by the dataplane-advertised minimum Builder CLI version.
380
+ await assertDataplaneCliVersionCompatible(dataplaneUrl, { controllerUrl });
367
381
  logger.log('');
368
382
  logger.log(sectionTitle('Target'));
369
383
  logger.log(SEP);
@@ -373,10 +387,18 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
373
387
  if (options.verbose) {
374
388
  await maybeRunVerboseServerValidation(dataplaneUrl, authConfig, payload);
375
389
  }
376
- await pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload);
377
- await logAndSyncLocalOpenApiForUpload(systemKey, manifest);
390
+ await pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload, {
391
+ json: options.json === true
392
+ });
378
393
 
379
- const rawRes = await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
394
+ const rawRes = await runWithUploadPipelineSpinner(
395
+ async() => {
396
+ const res = await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
397
+ await logAndSyncLocalOpenApiForUpload(systemKey, manifest);
398
+ return res;
399
+ },
400
+ { systemKey, json: options.json === true }
401
+ );
380
402
  await handlePublicationAndFollowups({
381
403
  systemKey,
382
404
  options,
@@ -387,6 +409,7 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
387
409
  authConfig,
388
410
  rawRes
389
411
  });
412
+ return { dataplaneUrl, authConfig, environment };
390
413
  }
391
414
 
392
415
  /**
@@ -398,7 +421,7 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
398
421
  * @param {boolean} [options.probe] - POST validation/run after publish
399
422
  * @param {number} [options.probeTimeout] - Probe timeout ms (default 120000)
400
423
  * @param {boolean} [options.minimal] - Short summary only
401
- * @returns {Promise<void>}
424
+ * @returns {Promise<{ dataplaneUrl: string, authConfig: Object, environment: string }|void>}
402
425
  * @throws {Error} If validation or API calls fail
403
426
  */
404
427
  async function uploadExternalSystem(systemKey, options = {}) {
@@ -415,9 +438,9 @@ async function uploadExternalSystem(systemKey, options = {}) {
415
438
  logger.log(
416
439
  chalk.gray(` System: ${manifest.key}, version: ${payload.version}, datasources: ${payload.dataSources.length}`)
417
440
  );
418
- return;
441
+ return undefined;
419
442
  }
420
- await runUploadPublishAndSummary(systemKey, options, manifest, payload);
443
+ return runUploadPublishAndSummary(systemKey, options, manifest, payload);
421
444
  }
422
445
 
423
446
  module.exports = {
@@ -525,6 +525,9 @@ async function setupDataplaneAndAuth(options, appName) {
525
525
  ' aifabrix login'
526
526
  );
527
527
  }
528
+ // Plan 142.0: gate wizard dataplane calls by the dataplane-advertised minimum Builder CLI version.
529
+ const { assertDataplaneCliVersionCompatible } = require('../utils/dataplane-cli-version-gate');
530
+ await assertDataplaneCliVersionCompatible(dataplaneUrl, { controllerUrl });
528
531
  return { dataplaneUrl, authConfig };
529
532
  }
530
533
 
@@ -15,6 +15,7 @@ const fs = require('fs').promises;
15
15
  const path = require('path');
16
16
  const os = require('os');
17
17
  const paths = require('../utils/paths');
18
+ const { maskSensitiveData } = require('../utils/log-redaction');
18
19
 
19
20
  // Audit log file path (beside config.yaml / CLI system dir for compliance)
20
21
  let auditLogPath = null;
@@ -51,40 +52,6 @@ async function getAuditLogPath() {
51
52
  return auditLogPath;
52
53
  }
53
54
 
54
- /**
55
- * Masks sensitive data in strings
56
- * Prevents secrets, keys, and passwords from appearing in logs
57
- *
58
- * @param {string} value - Value to mask
59
- * @returns {string} Masked value
60
- */
61
- function maskSensitiveData(value) {
62
- if (!value || typeof value !== 'string') {
63
- return value;
64
- }
65
-
66
- // Mask patterns: passwords, secrets, keys, tokens
67
- const sensitivePatterns = [
68
- { pattern: /password[=:]\s*([^\s]+)/gi, replacement: 'password=***' },
69
- { pattern: /secret[=:]\s*([^\s]+)/gi, replacement: 'secret=***' },
70
- { pattern: /key[=:]\s*([^\s]+)/gi, replacement: 'key=***' },
71
- { pattern: /token[=:]\s*([^\s]+)/gi, replacement: 'token=***' },
72
- { pattern: /api[_-]?key[=:]\s*([^\s]+)/gi, replacement: 'api_key=***' }
73
- ];
74
-
75
- let masked = value;
76
- for (const { pattern, replacement } of sensitivePatterns) {
77
- masked = masked.replace(pattern, replacement);
78
- }
79
-
80
- // If value looks like a hash/key (long hex string), mask it
81
- if (/^[a-f0-9]{32,}$/i.test(masked.trim())) {
82
- return '***';
83
- }
84
-
85
- return masked;
86
- }
87
-
88
55
  /**
89
56
  * Creates an audit log entry with ISO 27001 compliance
90
57
  *
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Admin email field in `config.yaml` (Keycloak / pgAdmin), written by `aifabrix setup`.
3
+ *
4
+ * @fileoverview config.yaml adminEmail helpers (keeps config.js under max-lines)
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * @param {unknown} email
13
+ * @returns {string} Trimmed valid email
14
+ * @throws {Error} When empty or not a plausible email
15
+ */
16
+ function validateAdminEmailForConfig(email) {
17
+ const value = String(email ?? '').trim();
18
+ if (!value) {
19
+ throw new Error('Admin email must be a non-empty string');
20
+ }
21
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
22
+ throw new Error('Admin email must be a valid email address');
23
+ }
24
+ return value;
25
+ }
26
+
27
+ /**
28
+ * @param {() => Promise<Object>} getConfig
29
+ * @returns {Promise<string>}
30
+ */
31
+ async function getAdminEmailFromConfig(getConfig) {
32
+ const cfg = await getConfig();
33
+ if (cfg && typeof cfg.adminEmail === 'string' && cfg.adminEmail.trim()) {
34
+ return cfg.adminEmail.trim();
35
+ }
36
+ return '';
37
+ }
38
+
39
+ /**
40
+ * @param {() => Promise<Object>} getConfig
41
+ * @param {(data: Object) => Promise<void>} saveConfig
42
+ * @param {string} email
43
+ * @returns {Promise<void>}
44
+ */
45
+ async function setAdminEmailInConfig(getConfig, saveConfig, email) {
46
+ const trimmed = validateAdminEmailForConfig(email);
47
+ const cfg = await getConfig();
48
+ cfg.adminEmail = trimmed;
49
+ await saveConfig(cfg);
50
+ }
51
+
52
+ module.exports = {
53
+ validateAdminEmailForConfig,
54
+ getAdminEmailFromConfig,
55
+ setAdminEmailInConfig
56
+ };
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Persist dataplane version metadata under `device.<controllerUrl>` in
3
+ * `~/.aifabrix/config.yaml` for the Builder CLI compatibility gate (plan 142.0).
4
+ *
5
+ * Only public dataplane fields are written here. Token / refresh-token entries
6
+ * on the same device entry are preserved untouched.
7
+ *
8
+ * Schema additions (kebab-case, matching `developer-id` style):
9
+ * - `dataplane-version` ← health `version`
10
+ * - `dataplane-min-cli-version` ← health `minBuilderCliVersion` (omitted when not enforced)
11
+ * - `dataplane-checked-at` ← ISO timestamp for cache TTL (default 5 min)
12
+ *
13
+ * @fileoverview Cache dataplane health under existing `device.<controllerUrl>` entry
14
+ * @author AI Fabrix Team
15
+ * @version 2.0.0
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const { getConfig, saveConfig, normalizeControllerUrl } = require('./config');
21
+
22
+ const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
23
+
24
+ /**
25
+ * Resolve the device entry key matching `controllerUrl`, taking URL
26
+ * normalization into account so callers do not need to worry about trailing
27
+ * slashes or scheme differences.
28
+ * @private
29
+ * @param {Object} deviceMap - `config.device` map
30
+ * @param {string} normalizedUrl
31
+ * @returns {string|null} The key under `config.device` or null when missing
32
+ */
33
+ function findDeviceKey(deviceMap, normalizedUrl) {
34
+ if (!deviceMap || typeof deviceMap !== 'object') return null;
35
+ if (deviceMap[normalizedUrl]) return normalizedUrl;
36
+ for (const storedUrl of Object.keys(deviceMap)) {
37
+ if (normalizeControllerUrl(storedUrl) === normalizedUrl) {
38
+ return storedUrl;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Read the dataplane cache currently persisted under `device.<controllerUrl>`.
46
+ *
47
+ * @async
48
+ * @param {string} controllerUrl
49
+ * @returns {Promise<{
50
+ * version?: string,
51
+ * minBuilderCliVersion?: string,
52
+ * checkedAt?: string
53
+ * }|null>} The cached snapshot, or null when no entry exists
54
+ * @throws {Error} If controllerUrl is missing
55
+ */
56
+ async function getDeviceDataplaneVersions(controllerUrl) {
57
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
58
+ throw new Error('controllerUrl is required and must be a string');
59
+ }
60
+ const config = await getConfig();
61
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
62
+ const key = findDeviceKey(config.device, normalizedUrl);
63
+ if (!key) return null;
64
+ const entry = config.device[key];
65
+ if (!entry || typeof entry !== 'object') return null;
66
+ const snapshot = {};
67
+ if (typeof entry['dataplane-version'] === 'string') {
68
+ snapshot.version = entry['dataplane-version'];
69
+ }
70
+ if (typeof entry['dataplane-min-cli-version'] === 'string') {
71
+ snapshot.minBuilderCliVersion = entry['dataplane-min-cli-version'];
72
+ }
73
+ if (typeof entry['dataplane-checked-at'] === 'string') {
74
+ snapshot.checkedAt = entry['dataplane-checked-at'];
75
+ }
76
+ if (Object.keys(snapshot).length === 0) return null;
77
+ return snapshot;
78
+ }
79
+
80
+ /**
81
+ * Apply the `dataplane-version` field to a device entry in-place.
82
+ * @private
83
+ * @param {Object} entry
84
+ * @param {string|null|undefined} version
85
+ */
86
+ function applyDataplaneVersionField(entry, version) {
87
+ if (typeof version === 'string' && version.trim()) {
88
+ entry['dataplane-version'] = version.trim();
89
+ } else if (version === null || version === '') {
90
+ delete entry['dataplane-version'];
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Apply the `dataplane-min-cli-version` field to a device entry in-place.
96
+ * Treat absent / empty as "no enforcement" → clear the cached floor so an
97
+ * older value never lingers after the operator removes the env variable.
98
+ * @private
99
+ * @param {Object} entry
100
+ * @param {string|null|undefined} minCli
101
+ */
102
+ function applyMinCliField(entry, minCli) {
103
+ if (typeof minCli === 'string' && minCli.trim()) {
104
+ entry['dataplane-min-cli-version'] = minCli.trim();
105
+ return;
106
+ }
107
+ if (minCli === null || minCli === '' || minCli === undefined) {
108
+ delete entry['dataplane-min-cli-version'];
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Compute the timestamp persisted as `dataplane-checked-at`.
114
+ * @private
115
+ * @param {string|undefined} checkedAt
116
+ * @returns {string} ISO timestamp
117
+ */
118
+ function resolveCheckedAt(checkedAt) {
119
+ if (typeof checkedAt === 'string' && checkedAt.trim()) {
120
+ return checkedAt.trim();
121
+ }
122
+ return new Date().toISOString();
123
+ }
124
+
125
+ /**
126
+ * Persist dataplane version metadata under the device entry for `controllerUrl`.
127
+ *
128
+ * Token / refresh-token entries are preserved; only the `dataplane-*` keys are
129
+ * written. When `versions.minBuilderCliVersion` is empty/undefined the cache
130
+ * key is removed (dataplane does not enforce a minimum).
131
+ *
132
+ * @async
133
+ * @param {string} controllerUrl - Controller URL (will be normalized)
134
+ * @param {{
135
+ * version?: string,
136
+ * minBuilderCliVersion?: string|null,
137
+ * checkedAt?: string
138
+ * }} versions
139
+ * @returns {Promise<void>}
140
+ * @throws {Error} If controllerUrl is missing
141
+ */
142
+ async function updateDeviceDataplaneVersions(controllerUrl, versions = {}) {
143
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
144
+ throw new Error('controllerUrl is required and must be a string');
145
+ }
146
+ const config = await getConfig();
147
+ if (!config.device || typeof config.device !== 'object') {
148
+ config.device = {};
149
+ }
150
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
151
+ const key = findDeviceKey(config.device, normalizedUrl) || normalizedUrl;
152
+ if (!config.device[key] || typeof config.device[key] !== 'object') {
153
+ config.device[key] = {};
154
+ }
155
+ const entry = config.device[key];
156
+
157
+ applyDataplaneVersionField(entry, versions.version);
158
+ applyMinCliField(entry, versions.minBuilderCliVersion);
159
+ entry['dataplane-checked-at'] = resolveCheckedAt(versions.checkedAt);
160
+
161
+ await saveConfig(config);
162
+ }
163
+
164
+ /**
165
+ * True when the cached snapshot is older than the configured TTL (or missing).
166
+ *
167
+ * @param {{checkedAt?: string}|null} snapshot
168
+ * @param {number} [ttlMs] - TTL in milliseconds (defaults to 5 minutes)
169
+ * @param {Date|number} [now] - Override "now" for tests
170
+ * @returns {boolean}
171
+ */
172
+ function isCacheStale(snapshot, ttlMs = DEFAULT_CACHE_TTL_MS, now = Date.now()) {
173
+ if (!snapshot || !snapshot.checkedAt) return true;
174
+ const checkedAtMs = Date.parse(snapshot.checkedAt);
175
+ if (Number.isNaN(checkedAtMs)) return true;
176
+ const reference = typeof now === 'number' ? now : now.getTime();
177
+ return reference - checkedAtMs >= ttlMs;
178
+ }
179
+
180
+ module.exports = {
181
+ getDeviceDataplaneVersions,
182
+ updateDeviceDataplaneVersions,
183
+ isCacheStale,
184
+ DEFAULT_CACHE_TTL_MS
185
+ };