@aifabrix/builder 2.50.0 → 2.51.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 (103) hide show
  1. package/.cursor/plans/157.1-security-quality-violations-by-type.csv +374 -0
  2. package/.cursor/rules/assistant-driven-integration-build.mdc +40 -0
  3. package/integration/hubspot-test/create-hubspot.js +1 -16
  4. package/lib/api/evidence-kinds.api.js +128 -0
  5. package/lib/api/pipeline.api.js +5 -1
  6. package/lib/api/resource-types.api.js +4 -4
  7. package/lib/api/roles.api.js +121 -0
  8. package/lib/api/types/evidence-kinds.types.js +42 -0
  9. package/lib/api/types/roles.types.js +36 -0
  10. package/lib/app/certification-verify-rows.js +1 -1
  11. package/lib/app/show.js +2 -2
  12. package/lib/certification/merge-certification-from-artifact.js +1 -1
  13. package/lib/channels/channel-artifact-generator.js +2 -1
  14. package/lib/cli/index.js +4 -0
  15. package/lib/cli/setup-app.js +22 -5
  16. package/lib/cli/setup-dev.js +23 -7
  17. package/lib/cli/setup-utility-help-after.js +12 -0
  18. package/lib/cli/setup-utility-validate.js +41 -2
  19. package/lib/commands/deploy-gate-failure-details.js +21 -0
  20. package/lib/commands/deploy-gate-orchestrator.js +64 -18
  21. package/lib/commands/dev-cli-handlers.js +6 -3
  22. package/lib/commands/dev-init.js +7 -109
  23. package/lib/commands/dev-refresh.js +197 -0
  24. package/lib/commands/evidence-kind.js +428 -0
  25. package/lib/commands/governance-command-action.js +4 -2
  26. package/lib/commands/repair-openapi-export.js +1 -1
  27. package/lib/commands/repair-openapi-sync.js +14 -6
  28. package/lib/commands/resource-type.js +101 -18
  29. package/lib/commands/role.js +452 -0
  30. package/lib/commands/setup.js +1 -1
  31. package/lib/commands/validate-json.js +230 -0
  32. package/lib/commands/verify-operations-step-plan.js +78 -40
  33. package/lib/core/audit-logger.js +1 -1
  34. package/lib/core/secrets-load.js +1 -3
  35. package/lib/datasource/capability/relate-operations.js +1 -1
  36. package/lib/generator/helpers.js +13 -5
  37. package/lib/generator/split-variables.js +9 -3
  38. package/lib/infrastructure/helpers-docker-check.js +6 -0
  39. package/lib/infrastructure/helpers.js +1 -1
  40. package/lib/programmatic/auth-config.js +3 -2
  41. package/lib/programmatic/bearer-auth.js +1 -1
  42. package/lib/programmatic/enterprise-mcp-client.js +18 -2
  43. package/lib/programmatic/manifest-section-registry.js +18 -1
  44. package/lib/programmatic/openapi-result-schemas.js +28 -11
  45. package/lib/programmatic/openapi-schema-descriptions-core.js +5 -2
  46. package/lib/programmatic/openapi-schema-helpers.js +16 -1
  47. package/lib/programmatic/openapi-spec.js +1 -1
  48. package/lib/programmatic/run-repository-emcp.js +11 -3
  49. package/lib/programmatic/run-template-workspace.js +14 -1
  50. package/lib/resolvers/evidence-kind-file.js +230 -0
  51. package/lib/resolvers/resource-type-file.js +36 -1
  52. package/lib/resolvers/role-file.js +227 -0
  53. package/lib/schema/application-schema.json +22 -2
  54. package/lib/schema/external-datasource.schema.json +85 -2
  55. package/lib/schema/external-system.schema.json +16 -2
  56. package/lib/schema/infra.parameter.yaml +25 -1
  57. package/lib/schema/type/credential-field-registry.json +19 -3
  58. package/lib/utils/api.js +8 -2
  59. package/lib/utils/app-register-config.js +4 -3
  60. package/lib/utils/cli-sync-options.js +59 -3
  61. package/lib/utils/cli-utils.js +11 -3
  62. package/lib/utils/dev-ca-install.js +5 -6
  63. package/lib/utils/dev-config.js +2 -2
  64. package/lib/utils/dev-list-sort.js +145 -0
  65. package/lib/utils/dev-platform-tls.js +0 -63
  66. package/lib/utils/docker-build.js +46 -27
  67. package/lib/utils/docker-daemon-tls-ca.js +1 -1
  68. package/lib/utils/docker-exec.js +91 -6
  69. package/lib/utils/env-copy.js +1 -3
  70. package/lib/utils/error-formatters/error-parser.js +0 -3
  71. package/lib/utils/external-env-template.js +1 -1
  72. package/lib/utils/external-system-readiness-compact-blocks.js +28 -1
  73. package/lib/utils/health-check-request.js +2 -3
  74. package/lib/utils/help-builder.js +3 -0
  75. package/lib/utils/image-name.js +22 -4
  76. package/lib/utils/logger.js +37 -4
  77. package/lib/utils/manifest-source-emit.js +1 -1
  78. package/lib/utils/paths.js +32 -4
  79. package/lib/utils/pipeline-upload-auth-retry.js +4 -0
  80. package/lib/utils/postgres-platform-bootstrap.js +24 -17
  81. package/lib/utils/postgres-wipe.js +5 -2
  82. package/lib/utils/remote-dev-auth.js +39 -4
  83. package/lib/utils/resolve-docker-image-ref.js +36 -6
  84. package/lib/utils/token-manager.js +16 -0
  85. package/lib/utils/upload-sync-options.js +17 -3
  86. package/lib/validation/openapi-contract-surface-validator.js +4 -0
  87. package/lib/validation/validator.js +6 -5
  88. package/package.json +34 -12
  89. package/scripts/export-builder-api-evidence.js +2 -2
  90. package/scripts/lib/plan-actor.js +2 -2
  91. package/scripts/lib/plan-meta.js +1 -1
  92. package/scripts/lib/plan-parse.js +1 -1
  93. package/scripts/lib/plan-prevalidate-gates.js +12 -1
  94. package/scripts/lib/plan-run-report.js +13 -2
  95. package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +10 -10
  96. package/templates/applications/dataplane/application.yaml +26 -4
  97. package/templates/applications/dataplane/env.template +28 -9
  98. package/templates/applications/dataplane/rbac.yaml +64 -2
  99. package/templates/applications/miso-controller/application.yaml +10 -1
  100. package/templates/applications/miso-controller/env.template +10 -7
  101. package/templates/applications/miso-controller/rbac.yaml +72 -0
  102. package/templates/channels/slack/integration/slack-system.json +60 -2
  103. package/templates/python/requirements.txt +1 -1
@@ -103,9 +103,16 @@ async function runRequiredDeployStep(label, runner, isOk, stepOptions = {}) {
103
103
  const outcome = await runner();
104
104
  if (!isOk(outcome)) {
105
105
  const detail = formatDeployGateFailure(label, outcome);
106
- logger.log(chalk.red(`āœ– ${detail}`));
106
+ if (stepOptions.quietDeployUi !== true) {
107
+ logger.log(chalk.red(`āœ– ${detail}`));
108
+ }
107
109
  const err = new Error(`${label} failed: ${detail}`);
108
110
  err.gateFailure = true;
111
+ err.gateDetail = detail;
112
+ err.gateLabel = label;
113
+ if (stepOptions.gatePillar) {
114
+ err.gatePillar = stepOptions.gatePillar;
115
+ }
109
116
  throw err;
110
117
  }
111
118
  return outcome;
@@ -134,7 +141,9 @@ async function runDeployBaselineSteps(systemKey, options) {
134
141
  ...options,
135
142
  uploadStatus: 'published',
136
143
  minimal: true,
137
- suppressSummary: true
144
+ suppressSummary: true,
145
+ suppressPipelineSpinner: true,
146
+ uploadContext: 'deploy-preflight'
138
147
  });
139
148
  stepOptions = mergeUploadAuthOptions({ ...options, uploadStatus: 'published' }, uploadCtx);
140
149
  }
@@ -191,7 +200,7 @@ async function runOperationsGate(systemKey, options) {
191
200
  syncMode: e2eSyncMode || stepOptions.syncMode
192
201
  }),
193
202
  (result) => result.success === true,
194
- options
203
+ { ...options, gatePillar: 'operations' }
195
204
  );
196
205
 
197
206
  return { pillar: 'operations', verdict: VERDICT.VERIFIED };
@@ -208,7 +217,7 @@ async function runTrustGate(systemKey, options) {
208
217
  'Running trust gate',
209
218
  () => runVerifyTrustForExternalSystem(systemKey, { ...options, noPublish: true }),
210
219
  (outcome) => outcome.verdict === VERDICT.VERIFIED,
211
- options
220
+ { ...options, gatePillar: 'trust' }
212
221
  );
213
222
  return {
214
223
  pillar: 'trust',
@@ -254,7 +263,7 @@ async function runGovernanceGate(systemKey, options) {
254
263
  'Running governance gate',
255
264
  runner,
256
265
  (outcome) => outcome.verdict === VERDICT.VERIFIED,
257
- options
266
+ { ...options, gatePillar: 'governance' }
258
267
  );
259
268
  const scenarios = result.enforcementScenarios || {};
260
269
  return {
@@ -274,18 +283,37 @@ async function runGovernanceGate(systemKey, options) {
274
283
  */
275
284
  async function runDeployOptionalGates(systemKey, gates, options) {
276
285
  const outcomes = [];
277
- if (gates.has('operations')) {
278
- outcomes.push(await runOperationsGate(systemKey, options));
279
- }
280
- if (gates.has('trust')) {
281
- outcomes.push(await runTrustGate(systemKey, options));
282
- }
283
- if (gates.has('governance')) {
284
- outcomes.push(await runGovernanceGate(systemKey, options));
286
+ try {
287
+ if (gates.has('operations')) {
288
+ outcomes.push(await runOperationsGate(systemKey, options));
289
+ }
290
+ if (gates.has('trust')) {
291
+ outcomes.push(await runTrustGate(systemKey, options));
292
+ }
293
+ if (gates.has('governance')) {
294
+ outcomes.push(await runGovernanceGate(systemKey, options));
295
+ }
296
+ } catch (err) {
297
+ if (err && err.gateFailure === true) {
298
+ err.gatesSoFar = outcomes;
299
+ }
300
+ throw err;
285
301
  }
286
302
  return outcomes;
287
303
  }
288
304
 
305
+ /**
306
+ * @param {string|undefined} label
307
+ * @returns {string}
308
+ */
309
+ function inferGatePillarFromLabel(label) {
310
+ const text = String(label || '').toLowerCase();
311
+ if (text.includes('operations')) return 'operations';
312
+ if (text.includes('trust')) return 'trust';
313
+ if (text.includes('governance')) return 'governance';
314
+ return 'operations';
315
+ }
316
+
289
317
  /**
290
318
  * Baseline validate + test + test-integration, then optional --gate steps before controller deploy.
291
319
  * @async
@@ -304,11 +332,29 @@ async function runDeployGateOrchestrator(systemKey, options = {}) {
304
332
  if (gates.size === 0) {
305
333
  return { baseline, gates: [] };
306
334
  }
307
- const gateOutcomes = await runDeployOptionalGates(systemKey, gates, {
308
- ...options,
309
- quietDeployUi: quiet
310
- });
311
- return { baseline, gates: gateOutcomes };
335
+ try {
336
+ const gateOutcomes = await runDeployOptionalGates(systemKey, gates, {
337
+ ...options,
338
+ quietDeployUi: quiet
339
+ });
340
+ return { baseline, gates: gateOutcomes };
341
+ } catch (err) {
342
+ if (err && err.gateFailure === true && quiet) {
343
+ const { logDeployGateAbortCompact } = require('../utils/external-system-readiness-compact-blocks');
344
+ const failedPillar = err.gatePillar || inferGatePillarFromLabel(err.gateLabel);
345
+ logDeployGateAbortCompact({
346
+ systemKey,
347
+ environment: options.environment || options.env || 'dev',
348
+ dataplaneUrl: options.dataplaneUrl || options.knownDataplaneUrl,
349
+ baseline,
350
+ gatesSoFar: err.gatesSoFar || [],
351
+ failedPillar,
352
+ failedDetail: err.gateDetail || err.message
353
+ });
354
+ err.compactGateAbortLogged = true;
355
+ }
356
+ throw err;
357
+ }
312
358
  }
313
359
 
314
360
  module.exports = {
@@ -18,6 +18,7 @@ const {
18
18
  } = require('../utils/dev-user-groups');
19
19
  const { handleProvisionDevUser, handleRemoveDevUser } = require('./dev-host-ops');
20
20
  const { logDevPinShareInstructions } = require('../utils/dev-pin-share-instructions');
21
+ const { sortDevUsers } = require('../utils/dev-list-sort');
21
22
 
22
23
  const REMOTE_NOT_CONFIGURED_MSG = 'Remote server is not configured. Set remote-server and run "aifabrix dev init" first.';
23
24
 
@@ -52,10 +53,12 @@ function formatCertUntil(iso) {
52
53
  }
53
54
 
54
55
  /**
55
- * Handle dev list – list developer users (remote only). Table format, sorted by name.
56
+ * Handle dev list – list developer users (remote only). Table format; default sort by name.
57
+ * @param {Object} [options] - Commander options
58
+ * @param {string} [options.sort] - Sort field/direction (id|name|email|until; prefix - for descending)
56
59
  * @returns {Promise<void>}
57
60
  */
58
- async function handleDevList() {
61
+ async function handleDevList(options = {}) {
59
62
  const auth = await getRemoteDevAuth();
60
63
  if (!auth) {
61
64
  logger.log(chalk.yellow(REMOTE_NOT_CONFIGURED_MSG));
@@ -69,7 +72,7 @@ async function handleDevList() {
69
72
  logger.log(chalk.bold('\nšŸ“‹ Developers:\n'));
70
73
  logger.log(chalk.gray('ID'.padEnd(ID_WIDTH) + 'Name'.padEnd(NAME_WIDTH) + 'Email'.padEnd(EMAIL_WIDTH) + 'Cert'.padEnd(CERT_WIDTH) + 'Until'.padEnd(UNTIL_WIDTH) + 'Groups'));
71
74
  logger.log(chalk.gray('-'.repeat(TABLE_SEPARATOR_LENGTH)));
72
- const sorted = [...users].sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
75
+ const sorted = sortDevUsers(users, options.sort);
73
76
  sorted.forEach(u => {
74
77
  const certLabel = u.certificateIssued ? 'yes' : 'no cert';
75
78
  const untilRaw = u.certificateIssued && u.certificateValidNotAfter ? u.certificateValidNotAfter : null;
@@ -14,10 +14,6 @@ const { getConfigDirForPaths } = require('../utils/paths');
14
14
  const {
15
15
  generateCSR,
16
16
  getCertDir,
17
- readClientCertPem,
18
- readClientKeyPem,
19
- readServerCaPem,
20
- getCertValidNotAfter,
21
17
  normalizePemNewlines,
22
18
  mergeCaPemBlocks
23
19
  } = require('../utils/dev-cert-helper');
@@ -33,9 +29,8 @@ const {
33
29
  isLinuxCaSudoRequiredError
34
30
  } = require('../utils/dev-ca-install');
35
31
  const { runOptionalHostsSetup } = require('../utils/dev-hosts-helper');
36
- const { mergeDevSshConfigAfterInit, logDevSshConnectHint } = require('../utils/dev-init-ssh-merge');
32
+ const { mergeDevSshConfigAfterInit } = require('../utils/dev-init-ssh-merge');
37
33
  const { resolveInitOptions } = require('../utils/dev-init-resolve');
38
- const { displayDevConfig } = require('./dev-show-display');
39
34
  const {
40
35
  isHealthHttpServerError,
41
36
  formatEnsureServerTrustedFailure
@@ -383,106 +378,9 @@ async function runDevInit(options) {
383
378
  logOnboardingFinished(sshInfo);
384
379
  }
385
380
 
386
- /** Days before cert expiry at which we auto-refresh on dev refresh. */
387
- const CERT_REFRESH_DAYS = 14;
388
-
389
- /**
390
- * True if the cert in certDir expires within CERT_REFRESH_DAYS (or we cannot read expiry).
391
- * @param {string} certDir - Certificate directory
392
- * @returns {boolean}
393
- */
394
- function shouldRefreshDevCert(certDir) {
395
- const validNotAfter = getCertValidNotAfter(certDir);
396
- if (!validNotAfter) return true;
397
- const now = Date.now();
398
- const threshold = now + CERT_REFRESH_DAYS * 24 * 60 * 60 * 1000;
399
- return validNotAfter.getTime() < threshold;
400
- }
401
-
402
- /**
403
- * Refresh developer certificate: create PIN (with current cert), issue new cert, save and apply settings.
404
- * @param {{ serverUrl: string, clientCertPem: string, serverCaPem?: string|null }} auth - Current auth from getRemoteDevAuth
405
- * @returns {Promise<void>}
406
- */
407
- async function runCertificateRefresh(auth) {
408
- const devId = await config.getDeveloperId();
409
- if (!devId) throw new Error('developer-id not set in config.');
410
- const configDir = getConfigDirForPaths();
411
- const serverCaPem = auth.serverCaPem || undefined;
412
- logger.log(chalk.blue('\nšŸ”„ Refreshing certificate (create PIN + issue-cert)...\n'));
413
- const pinRes = await devApi.createPin(auth.serverUrl, auth.clientCertPem, devId, serverCaPem);
414
- const pin = pinRes.pin;
415
- if (!pin || typeof pin !== 'string') throw new Error('Server did not return a PIN.');
416
- logger.log(chalk.gray(' Generating new certificate request...'));
417
- const { csrPem, keyPem } = generateCSR(devId);
418
- logger.log(chalk.gray(' Requesting new certificate (issue-cert)...'));
419
- const issueResponse = await requestCertificate(auth.serverUrl, devId, pin, csrPem, serverCaPem);
420
- const caPem = mergeCaPemBlocks(serverCaPem, issueResponse.caCertificate, issueResponse.ca);
421
- await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
422
- await applySettingsFromServer(auth.serverUrl, devId, issueResponse, keyPem, serverCaPem);
423
- await augmentAndSaveCaPemForRemoteDocker(configDir, devId, caPem, {
424
- settings: issueResponse.settings,
425
- remoteServerUrl: auth.serverUrl,
426
- warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
427
- });
428
- logger.log(formatSuccessLine('Certificate refreshed and config updated from server.\n'));
429
- logDevSshConnectHint(await mergeDevSshConfigAfterInit(auth.serverUrl, devId));
430
- }
431
-
432
- /**
433
- * GET /api/dev/settings and merge into local config (used by dev refresh when cert is still valid).
434
- * @param {{ serverUrl: string, serverCaPem?: string|null }} auth
435
- * @param {string} clientCertPem
436
- * @param {string|null|undefined} clientKeyPem
437
- * @param {string} devId
438
- * @returns {Promise<void>}
439
- */
440
- async function fetchAndMergeRemoteSettings(auth, clientCertPem, clientKeyPem, devId) {
441
- logger.log(chalk.blue('\nšŸ”„ Fetching settings from Builder Server...\n'));
442
- const settings = await devApi.getSettings(
443
- auth.serverUrl,
444
- clientCertPem,
445
- clientKeyPem || undefined,
446
- auth.serverCaPem || undefined
447
- );
448
- await config.mergeRemoteSettings(settings);
449
- const existingCa = readServerCaPem(getCertDir(getConfigDirForPaths(), devId));
450
- await augmentAndSaveCaPemForRemoteDocker(getConfigDirForPaths(), devId, existingCa, {
451
- settings,
452
- remoteServerUrl: auth.serverUrl,
453
- warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
454
- });
455
- logger.log(formatSuccessLine('Config updated from server.\n'));
456
- await displayDevConfig(devId);
457
- }
458
-
459
- /**
460
- * Fetch settings from Builder Server and merge into config (GET /api/dev/settings).
461
- * If certificate expires within CERT_REFRESH_DAYS (or --cert), refresh cert first (create PIN + issue-cert).
462
- * @param {Object} [options] - Commander options; options.cert = true forces cert refresh
463
- * @returns {Promise<void>}
464
- * @throws {Error} If remote server or certificate not configured, or getSettings fails
465
- */
466
- async function runDevRefresh(options = {}) {
467
- const { getRemoteDevAuth } = require('../utils/remote-dev-auth');
468
- const auth = await getRemoteDevAuth();
469
- if (!auth) {
470
- throw new Error('Remote server is not configured. Set remote-server and run "aifabrix dev init" first.');
471
- }
472
- const devId = await config.getDeveloperId();
473
- const certDir = getCertDir(getConfigDirForPaths(), devId);
474
- const clientCertPem = readClientCertPem(certDir);
475
- const clientKeyPem = readClientKeyPem(certDir);
476
- if (!clientCertPem) {
477
- throw new Error('Client certificate not found. Run "aifabrix dev init" first.');
478
- }
479
- if (Boolean(options.cert) || shouldRefreshDevCert(certDir)) {
480
- await runCertificateRefresh(auth);
481
- await displayDevConfig(devId);
482
- return;
483
- }
484
- await fetchAndMergeRemoteSettings(auth, clientCertPem, clientKeyPem, devId);
485
- logDevSshConnectHint(await mergeDevSshConfigAfterInit(auth.serverUrl, devId));
486
- }
487
-
488
- module.exports = { runDevInit, runDevRefresh };
381
+ module.exports = {
382
+ runDevInit,
383
+ requestCertificate,
384
+ saveCertAndConfig,
385
+ applySettingsFromServer
386
+ };
@@ -0,0 +1,197 @@
1
+ /**
2
+ * @fileoverview aifabrix dev refresh – sync remote settings and renew client cert when due
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const config = require('../core/config');
11
+ const { getConfigDirForPaths } = require('../utils/paths');
12
+ const {
13
+ generateCSR,
14
+ getCertDir,
15
+ readClientCertPem,
16
+ readClientKeyPem,
17
+ readServerCaPem,
18
+ getCertValidNotAfter,
19
+ mergeCaPemBlocks
20
+ } = require('../utils/dev-cert-helper');
21
+ const devApi = require('../api/dev.api');
22
+ const logger = require('../utils/logger');
23
+ const { mergeDevSshConfigAfterInit, logDevSshConnectHint } = require('../utils/dev-init-ssh-merge');
24
+ const { displayDevConfig } = require('./dev-show-display');
25
+ const { augmentAndSaveCaPemForRemoteDocker } = require('../utils/dev-ca-docker-augment');
26
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
27
+ const {
28
+ requestCertificate,
29
+ saveCertAndConfig,
30
+ applySettingsFromServer
31
+ } = require('./dev-init');
32
+
33
+ /** Days before cert expiry at which we auto-refresh on dev refresh / auto-renew. */
34
+ const CERT_REFRESH_DAYS = 14;
35
+
36
+ /**
37
+ * True if the cert expires within CERT_REFRESH_DAYS (or expiry cannot be read).
38
+ * Used by explicit `dev refresh` (also attempts when expiry is unknown).
39
+ * @param {string} certDir
40
+ * @returns {boolean}
41
+ */
42
+ function shouldRefreshDevCert(certDir) {
43
+ const validNotAfter = getCertValidNotAfter(certDir);
44
+ if (!validNotAfter) return true;
45
+ const now = Date.now();
46
+ const threshold = now + CERT_REFRESH_DAYS * 24 * 60 * 60 * 1000;
47
+ return validNotAfter.getTime() < threshold;
48
+ }
49
+
50
+ /**
51
+ * True when cert is still valid but expires within CERT_REFRESH_DAYS.
52
+ * Used for automatic renew (never tries when already expired).
53
+ * @param {string} certDir
54
+ * @returns {boolean}
55
+ */
56
+ function isDevCertAutoRenewalDue(certDir) {
57
+ const validNotAfter = getCertValidNotAfter(certDir);
58
+ if (!validNotAfter) return false;
59
+ const now = Date.now();
60
+ if (validNotAfter.getTime() <= now) return false;
61
+ const threshold = now + CERT_REFRESH_DAYS * 24 * 60 * 60 * 1000;
62
+ return validNotAfter.getTime() < threshold;
63
+ }
64
+
65
+ /**
66
+ * Refresh developer certificate: create PIN (with current cert), issue new cert, save and apply settings.
67
+ * @param {{ serverUrl: string, clientCertPem: string, serverCaPem?: string|null }} auth
68
+ * @returns {Promise<void>}
69
+ */
70
+ async function runCertificateRefresh(auth) {
71
+ const devId = await config.getDeveloperId();
72
+ if (!devId) throw new Error('developer-id not set in config.');
73
+ const configDir = getConfigDirForPaths();
74
+ const serverCaPem = auth.serverCaPem || undefined;
75
+ logger.log(chalk.blue('\nšŸ”„ Refreshing certificate (create PIN + issue-cert)...\n'));
76
+ const pinRes = await devApi.createPin(auth.serverUrl, auth.clientCertPem, devId, serverCaPem);
77
+ const pin = pinRes.pin;
78
+ if (!pin || typeof pin !== 'string') throw new Error('Server did not return a PIN.');
79
+ logger.log(chalk.gray(' Generating new certificate request...'));
80
+ const { csrPem, keyPem } = generateCSR(devId);
81
+ logger.log(chalk.gray(' Requesting new certificate (issue-cert)...'));
82
+ const issueResponse = await requestCertificate(auth.serverUrl, devId, pin, csrPem, serverCaPem);
83
+ const caPem = mergeCaPemBlocks(serverCaPem, issueResponse.caCertificate, issueResponse.ca);
84
+ await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
85
+ await applySettingsFromServer(auth.serverUrl, devId, issueResponse, keyPem, serverCaPem);
86
+ await augmentAndSaveCaPemForRemoteDocker(configDir, devId, caPem, {
87
+ settings: issueResponse.settings,
88
+ remoteServerUrl: auth.serverUrl,
89
+ warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
90
+ });
91
+ logger.log(formatSuccessLine('Certificate refreshed and config updated from server.\n'));
92
+ logDevSshConnectHint(await mergeDevSshConfigAfterInit(auth.serverUrl, devId));
93
+ }
94
+
95
+ /**
96
+ * GET /api/dev/settings and merge into local config (when cert is still valid).
97
+ * @param {{ serverUrl: string, serverCaPem?: string|null }} auth
98
+ * @param {string} clientCertPem
99
+ * @param {string|null|undefined} clientKeyPem
100
+ * @param {string} devId
101
+ * @returns {Promise<void>}
102
+ */
103
+ async function fetchAndMergeRemoteSettings(auth, clientCertPem, clientKeyPem, devId) {
104
+ logger.log(chalk.blue('\nšŸ”„ Fetching settings from Builder Server...\n'));
105
+ const settings = await devApi.getSettings(
106
+ auth.serverUrl,
107
+ clientCertPem,
108
+ clientKeyPem || undefined,
109
+ auth.serverCaPem || undefined
110
+ );
111
+ await config.mergeRemoteSettings(settings);
112
+ const existingCa = readServerCaPem(getCertDir(getConfigDirForPaths(), devId));
113
+ await augmentAndSaveCaPemForRemoteDocker(getConfigDirForPaths(), devId, existingCa, {
114
+ settings,
115
+ remoteServerUrl: auth.serverUrl,
116
+ warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
117
+ });
118
+ logger.log(formatSuccessLine('Config updated from server.\n'));
119
+ await displayDevConfig(devId);
120
+ }
121
+
122
+ /**
123
+ * Fetch settings from Builder Server and merge into config.
124
+ * If certificate expires within CERT_REFRESH_DAYS (or --cert), refresh cert first.
125
+ * @param {Object} [options] - Commander options; options.cert = true forces cert refresh
126
+ * @returns {Promise<void>}
127
+ */
128
+ async function runDevRefresh(options = {}) {
129
+ const { readRemoteDevAuth } = require('../utils/remote-dev-auth');
130
+ const auth = await readRemoteDevAuth();
131
+ if (!auth) {
132
+ throw new Error('Remote server is not configured. Set remote-server and run "aifabrix dev init" first.');
133
+ }
134
+ const devId = await config.getDeveloperId();
135
+ const certDir = getCertDir(getConfigDirForPaths(), devId);
136
+ const clientCertPem = readClientCertPem(certDir);
137
+ const clientKeyPem = readClientKeyPem(certDir);
138
+ if (!clientCertPem) {
139
+ throw new Error('Client certificate not found. Run "aifabrix dev init" first.');
140
+ }
141
+ if (Boolean(options.cert) || shouldRefreshDevCert(certDir)) {
142
+ await runCertificateRefresh(auth);
143
+ await displayDevConfig(devId);
144
+ return;
145
+ }
146
+ await fetchAndMergeRemoteSettings(auth, clientCertPem, clientKeyPem, devId);
147
+ logDevSshConnectHint(await mergeDevSshConfigAfterInit(auth.serverUrl, devId));
148
+ }
149
+
150
+ /**
151
+ * Automatically renew the developer client certificate when still valid but due within
152
+ * CERT_REFRESH_DAYS. Safe for any cert-using command: no-op when not due / offline.
153
+ * Uses readRemoteDevAuth (not getRemoteDevAuth) to avoid recursion with auto-renew.
154
+ * Does not display config (caller shows status afterward).
155
+ *
156
+ * @param {{ quiet?: boolean }} [opts] - quiet=true suppresses failure warnings
157
+ * @returns {Promise<{ refreshed: boolean, reason?: string, error?: Error }>}
158
+ */
159
+ async function ensureDevCertFreshIfDue(opts = {}) {
160
+ const quiet = opts.quiet === true;
161
+ const { readRemoteDevAuth } = require('../utils/remote-dev-auth');
162
+ const auth = await readRemoteDevAuth();
163
+ if (!auth) {
164
+ return { refreshed: false, reason: 'no-remote-auth' };
165
+ }
166
+ const devId = await config.getDeveloperId();
167
+ if (!devId) {
168
+ return { refreshed: false, reason: 'no-developer-id' };
169
+ }
170
+ const certDir = getCertDir(getConfigDirForPaths(), devId);
171
+ if (!readClientCertPem(certDir)) {
172
+ return { refreshed: false, reason: 'no-cert' };
173
+ }
174
+ if (!isDevCertAutoRenewalDue(certDir)) {
175
+ return { refreshed: false, reason: 'not-due' };
176
+ }
177
+ try {
178
+ await runCertificateRefresh(auth);
179
+ return { refreshed: true };
180
+ } catch (error) {
181
+ if (!quiet) {
182
+ logger.warn(
183
+ chalk.yellow(`Could not auto-refresh developer certificate: ${error.message}`)
184
+ );
185
+ logger.warn(chalk.gray(' Run: aifabrix dev refresh --cert'));
186
+ }
187
+ return { refreshed: false, reason: 'refresh-failed', error };
188
+ }
189
+ }
190
+
191
+ module.exports = {
192
+ runDevRefresh,
193
+ ensureDevCertFreshIfDue,
194
+ isDevCertAutoRenewalDue,
195
+ shouldRefreshDevCert,
196
+ CERT_REFRESH_DAYS
197
+ };