@aifabrix/builder 2.44.0 → 2.44.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 (66) hide show
  1. package/.cursor/rules/cli-layout.mdc +75 -0
  2. package/.cursor/rules/project-rules.mdc +8 -0
  3. package/.npmrc.token +1 -0
  4. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  5. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
  6. package/.nyc_output/processinfo/index.json +1 -0
  7. package/jest.projects.js +15 -2
  8. package/lib/api/certificates.api.js +62 -0
  9. package/lib/api/index.js +11 -2
  10. package/lib/api/types/certificates.types.js +48 -0
  11. package/lib/api/validation-run.api.js +16 -4
  12. package/lib/api/validation-runner.js +13 -3
  13. package/lib/app/certification-show-enrich.js +129 -0
  14. package/lib/app/certification-verify-rows.js +60 -0
  15. package/lib/app/show-display.js +43 -0
  16. package/lib/app/show.js +92 -8
  17. package/lib/certification/cli-cert-sync-skip.js +21 -0
  18. package/lib/certification/merge-certification-from-artifact.js +185 -0
  19. package/lib/certification/post-unified-cert-sync.js +33 -0
  20. package/lib/certification/sync-after-external-command.js +52 -0
  21. package/lib/certification/sync-system-certification.js +197 -0
  22. package/lib/cli/setup-app.js +4 -0
  23. package/lib/cli/setup-app.test-commands.js +24 -8
  24. package/lib/cli/setup-external-system.js +22 -1
  25. package/lib/cli/setup-secrets.js +34 -13
  26. package/lib/cli/setup-utility.js +18 -2
  27. package/lib/commands/app.js +10 -1
  28. package/lib/commands/datasource-unified-test-cli.js +50 -117
  29. package/lib/commands/datasource-unified-test-cli.options.js +44 -2
  30. package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +106 -0
  31. package/lib/commands/datasource-validation-cli.js +15 -1
  32. package/lib/commands/datasource.js +25 -2
  33. package/lib/commands/upload.js +17 -6
  34. package/lib/datasource/log-viewer.js +105 -14
  35. package/lib/datasource/test-e2e.js +35 -17
  36. package/lib/datasource/unified-validation-run-body.js +3 -0
  37. package/lib/datasource/unified-validation-run.js +2 -1
  38. package/lib/external-system/deploy.js +53 -18
  39. package/lib/infrastructure/compose.js +12 -3
  40. package/lib/infrastructure/helpers-docker-check.js +67 -0
  41. package/lib/infrastructure/helpers.js +47 -58
  42. package/lib/infrastructure/index.js +3 -1
  43. package/lib/infrastructure/services.js +4 -56
  44. package/lib/schema/external-system.schema.json +25 -3
  45. package/lib/schema/type/document-storage.json +15 -2
  46. package/lib/utils/api.js +28 -3
  47. package/lib/utils/configuration-env-resolver.js +11 -8
  48. package/lib/utils/credential-secrets-env.js +5 -5
  49. package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
  50. package/lib/utils/datasource-test-run-display.js +19 -2
  51. package/lib/utils/datasource-test-run-exit.js +25 -0
  52. package/lib/utils/external-system-display.js +8 -0
  53. package/lib/utils/external-system-system-test-tty-overview.js +120 -0
  54. package/lib/utils/external-system-system-test-tty.js +417 -0
  55. package/lib/utils/paths.js +14 -0
  56. package/lib/utils/validation-run-poll.js +28 -5
  57. package/lib/utils/validation-run-post-retry.js +20 -8
  58. package/lib/utils/validation-run-request.js +18 -0
  59. package/lib/validation/validate-external-cert-sync.js +23 -0
  60. package/lib/validation/validate.js +4 -1
  61. package/package.json +4 -3
  62. package/scripts/install-local.js +4 -1
  63. package/scripts/pnpm-global-remove.js +48 -0
  64. package/templates/applications/dataplane/env.template +4 -0
  65. package/templates/infra/compose.yaml.hbs +15 -14
  66. package/templates/infra/servers.json.hbs +3 -1
package/lib/app/show.js CHANGED
@@ -32,6 +32,11 @@ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
32
32
  const { formatApiError } = require('../utils/api-error-handler');
33
33
  const { formatAuthenticationError } = require('../utils/error-formatters/http-status-errors');
34
34
  const { display: displayShow } = require('./show-display');
35
+ const {
36
+ attachLocalCertification,
37
+ attachCertificationVerifyFromDataplane,
38
+ sanitizeCertificationForJson
39
+ } = require('./certification-show-enrich');
35
40
 
36
41
  /** Truncate deployment key for display */
37
42
  const DEPLOYMENT_KEY_TRUNCATE_LEN = 12;
@@ -273,6 +278,69 @@ async function getShowAuthToken(controllerUrl, config) {
273
278
  throw new Error('Authentication required for --online. Run aifabrix login.');
274
279
  }
275
280
 
281
+ /**
282
+ * Optional dataplane certificate verify rows on the show summary (external only).
283
+ * @param {Object} summary
284
+ * @param {string} appKey
285
+ * @param {boolean} verifyCert
286
+ * @param {{ token: string, controllerUrl: string }|null} [authBundleOptional]
287
+ */
288
+ async function maybeAttachCertificationVerify(summary, appKey, verifyCert, authBundleOptional) {
289
+ if (!verifyCert || !summary.isExternal) return;
290
+ if (authBundleOptional && authBundleOptional.token && authBundleOptional.controllerUrl) {
291
+ await attachCertificationVerifyFromDataplane(summary, appKey, {
292
+ token: authBundleOptional.token,
293
+ controllerUrl: authBundleOptional.controllerUrl
294
+ });
295
+ return;
296
+ }
297
+ try {
298
+ const controllerUrl = await resolveControllerUrl();
299
+ if (!controllerUrl) {
300
+ summary.certificationVerifySkipped = true;
301
+ return;
302
+ }
303
+ const config = await getConfig();
304
+ const authResult = await getShowAuthToken(controllerUrl, config);
305
+ await attachCertificationVerifyFromDataplane(summary, appKey, {
306
+ token: authResult.token,
307
+ controllerUrl: authResult.actualControllerUrl
308
+ });
309
+ } catch {
310
+ summary.certificationVerifySkipped = true;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * @param {Object} out - JSON payload (mutated)
316
+ * @param {Object} summary
317
+ */
318
+ function appendCertificationJsonFields(out, summary) {
319
+ if (!summary.isExternal) return;
320
+ out.localCertification = sanitizeCertificationForJson(summary.localCertification);
321
+ if (summary.certificationVerifyRows) {
322
+ out.certificationVerify = summary.certificationVerifyRows;
323
+ }
324
+ if (summary.certificationVerifySkipped) {
325
+ out.certificationVerifySkipped = true;
326
+ }
327
+ if (summary.certificationVerifyError) {
328
+ out.certificationVerifyError = summary.certificationVerifyError;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * @param {Object} summary
334
+ * @param {string} appKey
335
+ * @param {boolean} verifyCert
336
+ * @param {{ token: string, controllerUrl: string }|null} [authBundleOptional]
337
+ */
338
+ async function enrichExternalShowSummary(summary, appKey, verifyCert, authBundleOptional) {
339
+ if (!summary.isExternal) return;
340
+ attachLocalCertification(summary, appKey);
341
+ await maybeAttachCertificationVerify(summary, appKey, verifyCert, authBundleOptional);
342
+ }
343
+
276
344
  async function fetchOpenApiLists(dataplaneUrl, appKey, authConfig) {
277
345
  let openapiFiles = [];
278
346
  let openapiEndpoints = [];
@@ -566,22 +634,25 @@ function buildOnlineSummary(apiApp, controllerUrl, externalSystem) {
566
634
  * @param {string} appKey - Application key
567
635
  * @param {boolean} json - Output as JSON
568
636
  * @param {boolean} [permissionsOnly] - When true, output only permissions
637
+ * @param {boolean} [verifyCert] - When true, attempt dataplane verify for external apps
569
638
  * @throws {Error} If application config not found or invalid
570
639
  */
571
- async function runOffline(appKey, json, permissionsOnly) {
572
- let summary;
573
-
640
+ async function loadOfflineShowSummary(appKey) {
574
641
  try {
575
642
  const { deployment, appPath } = await generator.buildDeploymentManifestInMemory(appKey);
576
643
  const sourcePath = path.relative(process.cwd(), appPath) || appPath;
577
- summary = buildOfflineSummaryFromDeployJson(deployment, sourcePath);
644
+ return buildOfflineSummaryFromDeployJson(deployment, sourcePath);
578
645
  } catch (_err) {
579
646
  const { appPath } = await detectAppType(appKey);
580
647
  const configPath = resolveApplicationConfigPath(appPath);
581
648
  const variables = loadVariablesFromPath(appPath);
582
649
  const sourcePath = path.relative(process.cwd(), configPath) || configPath;
583
- summary = buildOfflineSummary(variables, sourcePath);
650
+ return buildOfflineSummary(variables, sourcePath);
584
651
  }
652
+ }
653
+
654
+ async function runOffline(appKey, json, permissionsOnly, verifyCert = false) {
655
+ const summary = await loadOfflineShowSummary(appKey);
585
656
 
586
657
  if (json) {
587
658
  if (permissionsOnly) {
@@ -607,9 +678,14 @@ async function runOffline(appKey, json, permissionsOnly) {
607
678
  databases: summary.databases
608
679
  }
609
680
  };
681
+ if (summary.isExternal) {
682
+ await enrichExternalShowSummary(summary, appKey, verifyCert, null);
683
+ appendCertificationJsonFields(out, summary);
684
+ }
610
685
  logger.log(JSON.stringify(out, null, 2));
611
686
  return;
612
687
  }
688
+ await enrichExternalShowSummary(summary, appKey, verifyCert, null);
613
689
  displayShow(summary, { permissionsOnly: !!permissionsOnly });
614
690
  }
615
691
 
@@ -680,6 +756,7 @@ function outputOnlineJson(summary, permissionsOnly) {
680
756
  ? { error: summary.externalSystem.error }
681
757
  : summary.externalSystem;
682
758
  }
759
+ appendCertificationJsonFields(out, summary);
683
760
  logger.log(JSON.stringify(out, null, 2));
684
761
  }
685
762
 
@@ -688,9 +765,10 @@ function outputOnlineJson(summary, permissionsOnly) {
688
765
  * @param {string} appKey - Application key
689
766
  * @param {boolean} json - Output as JSON
690
767
  * @param {boolean} [permissionsOnly] - When true, output only permissions
768
+ * @param {boolean} [verifyCert] - When true, attempt dataplane verify for external apps
691
769
  * @throws {Error} On auth failure, 404, or API error
692
770
  */
693
- async function runOnline(appKey, json, permissionsOnly) {
771
+ async function runOnline(appKey, json, permissionsOnly, verifyCert = false) {
694
772
  const controllerUrl = await resolveControllerUrl();
695
773
  if (!controllerUrl) {
696
774
  throw new Error('Controller URL is required for --online. Run aifabrix login to set the controller URL in config.yaml.');
@@ -705,6 +783,10 @@ async function runOnline(appKey, json, permissionsOnly) {
705
783
  ? await fetchExternalSystemForOnline(controllerUrl, appKey, authConfig)
706
784
  : null;
707
785
  const summary = buildOnlineSummary(apiApp, authResult.actualControllerUrl, externalSystem);
786
+ await enrichExternalShowSummary(summary, appKey, verifyCert, {
787
+ token: authConfig.token,
788
+ controllerUrl: authResult.actualControllerUrl
789
+ });
708
790
  if (json) {
709
791
  outputOnlineJson(summary, permissionsOnly);
710
792
  return;
@@ -720,6 +802,7 @@ async function runOnline(appKey, json, permissionsOnly) {
720
802
  * @param {boolean} [options.online] - Fetch from controller
721
803
  * @param {boolean} [options.json] - Output as JSON
722
804
  * @param {boolean} [options.permissions] - When true, output only permissions (app show --permissions)
805
+ * @param {boolean} [options.verifyCert] - When true, attach dataplane certificate verify rows (external)
723
806
  * @throws {Error} If file missing/invalid (offline) or API/auth error (online)
724
807
  */
725
808
  async function showApp(appKey, options = {}) {
@@ -730,11 +813,12 @@ async function showApp(appKey, options = {}) {
730
813
  const online = Boolean(options.online);
731
814
  const json = Boolean(options.json);
732
815
  const permissions = Boolean(options.permissions);
816
+ const verifyCert = Boolean(options.verifyCert);
733
817
 
734
818
  if (online) {
735
- await runOnline(appKey, json, permissions);
819
+ await runOnline(appKey, json, permissions, verifyCert);
736
820
  } else {
737
- await runOffline(appKey, json, permissions);
821
+ await runOffline(appKey, json, permissions, verifyCert);
738
822
  }
739
823
  }
740
824
 
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @fileoverview Detect skip-cert-sync from Commander + legacy option shapes.
3
+ * Commander registers `--no-cert-sync` as `certSync` defaulting to true; `--no-cert-sync` sets `certSync: false`.
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ * @param {Object|null|undefined} options
12
+ * @returns {boolean}
13
+ */
14
+ function cliOptsSkipCertSync(options) {
15
+ if (!options || typeof options !== 'object') return false;
16
+ if (options.noCertSync === true) return true;
17
+ if (options.certSync === false) return true;
18
+ return false;
19
+ }
20
+
21
+ module.exports = { cliOptsSkipCertSync };
@@ -0,0 +1,185 @@
1
+ /**
2
+ * @fileoverview Map dataplane certificate artifacts into **certification** (external-system.schema.json).
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * @param {unknown} id - certificateId field (string or FK-shaped object)
11
+ * @returns {string}
12
+ */
13
+ function certificateIdToString(id) {
14
+ if (id === undefined || id === null) return '';
15
+ if (typeof id === 'string') return id.trim();
16
+ if (typeof id === 'object' && id !== null && typeof id.id === 'string') return String(id.id).trim();
17
+ return String(id).trim();
18
+ }
19
+
20
+ /**
21
+ * @param {string|undefined|null} s
22
+ * @returns {string}
23
+ */
24
+ function trimOrEmpty(s) {
25
+ return s !== undefined && s !== null ? String(s).trim() : '';
26
+ }
27
+
28
+ /**
29
+ * Prefer an artifact that includes PEM/JWK **publicKey** material for verify-publish.
30
+ * @param {import('../api/types/certificates.types').CertificateArtifactResponse[]} artifacts
31
+ * @returns {import('../api/types/certificates.types').CertificateArtifactResponse|null}
32
+ */
33
+ function pickArtifactForCertificationMerge(artifacts) {
34
+ const list = Array.isArray(artifacts) ? artifacts.filter((a) => a && typeof a === 'object') : [];
35
+ if (list.length === 0) return null;
36
+ const withKey = list.find((a) => a.publicKey && String(a.publicKey).trim());
37
+ return withKey || list[0];
38
+ }
39
+
40
+ /**
41
+ * @param {string} algorithmUpper
42
+ * @param {import('../api/types/certificates.types').CertificateArtifactResponse} art
43
+ * @returns {string}
44
+ */
45
+ function hs256DevPublicKeyPlaceholder(algorithmUpper, art) {
46
+ if (algorithmUpper !== 'HS256') return '';
47
+ const cid = certificateIdToString(art.certificateId);
48
+ return `HS256-DEV-NO-PEM:${cid || 'integration-certificate'}`;
49
+ }
50
+
51
+ /**
52
+ * @param {import('../api/types/certificates.types').CertificateArtifactResponse} art
53
+ * @param {Object} ex
54
+ * @returns {string}
55
+ */
56
+ function resolvePublicKey(art, ex) {
57
+ const fromArt = trimOrEmpty(art.publicKey);
58
+ if (fromArt) return fromArt;
59
+ const fromExisting = trimOrEmpty(ex.publicKey);
60
+ if (fromExisting) return fromExisting;
61
+ const algorithmUpper = trimOrEmpty(art.algorithm).toUpperCase();
62
+ return hs256DevPublicKeyPlaceholder(algorithmUpper, art);
63
+ }
64
+
65
+ /**
66
+ * @param {import('../api/types/certificates.types').CertificateArtifactResponse} art
67
+ * @param {Object} ex
68
+ * @returns {string}
69
+ */
70
+ function resolveIssuer(art, ex) {
71
+ return (
72
+ trimOrEmpty(art.licenseLevelIssuer) ||
73
+ trimOrEmpty(art.issuedBy) ||
74
+ trimOrEmpty(ex.issuer) ||
75
+ 'dataplane'
76
+ );
77
+ }
78
+
79
+ /**
80
+ * @param {import('../api/types/certificates.types').CertificateArtifactResponse} art
81
+ * @param {Object} ex
82
+ * @returns {string}
83
+ */
84
+ function resolveVersion(art, ex) {
85
+ return (
86
+ trimOrEmpty(art.version) ||
87
+ trimOrEmpty(art.certificateVersion) ||
88
+ trimOrEmpty(ex.version) ||
89
+ certificateIdToString(art.certificateId)
90
+ );
91
+ }
92
+
93
+ const CERTIFICATION_LEVELS = new Set(['BRONZE', 'SILVER', 'GOLD', 'PLATINUM']);
94
+ const CERTIFICATION_STATUSES = new Set(['passed', 'not_passed', 'pending']);
95
+
96
+ /**
97
+ * @param {string} raw
98
+ * @returns {string}
99
+ */
100
+ function normalizeCertificationLevel(raw) {
101
+ const s = trimOrEmpty(raw).toUpperCase();
102
+ return CERTIFICATION_LEVELS.has(s) ? s : '';
103
+ }
104
+
105
+ /**
106
+ * @param {string} raw
107
+ * @returns {string}
108
+ */
109
+ function normalizeCertificationStatus(raw) {
110
+ const s = trimOrEmpty(raw).toLowerCase();
111
+ return CERTIFICATION_STATUSES.has(s) ? s : '';
112
+ }
113
+
114
+ /**
115
+ * @param {import('../api/types/certificates.types').CertificateArtifactResponse} art
116
+ * @param {Object} ex
117
+ * @returns {string}
118
+ */
119
+ function resolveLevel(art, ex) {
120
+ return (
121
+ normalizeCertificationLevel(ex.level) ||
122
+ normalizeCertificationLevel(art.certificationLevel) ||
123
+ ''
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Prefer existing file status when valid; otherwise **passed** for an active dataplane artifact.
129
+ *
130
+ * @param {import('../api/types/certificates.types').CertificateArtifactResponse} art
131
+ * @param {Object} ex
132
+ * @returns {string}
133
+ */
134
+ function resolveStatus(_art, ex) {
135
+ const fromEx = normalizeCertificationStatus(ex.status);
136
+ if (fromEx) return fromEx;
137
+ return 'passed';
138
+ }
139
+
140
+ /**
141
+ * Build `certification` object matching **external-system.schema.json** (required: enabled, publicKey, algorithm, issuer, version; optional status, level).
142
+ * Fills gaps from `existingCertification` when the artifact omits publishable fields (common when dataplane redacts `publicKey`).
143
+ * For **HS256** dev certificates with no PEM, uses a non-secret placeholder `publicKey` so the system file stays schema-valid.
144
+ *
145
+ * @param {import('../api/types/certificates.types').CertificateArtifactResponse|null} artifact
146
+ * @param {Object|null|undefined} existingCertification - Current `system.certification`
147
+ * @returns {Object|null} Full certification object, or null if **publicKey** or **version** cannot be satisfied
148
+ */
149
+ function buildCertificationFromArtifact(artifact, existingCertification) {
150
+ const ex = existingCertification && typeof existingCertification === 'object' ? existingCertification : {};
151
+ const art = artifact && typeof artifact === 'object' ? artifact : null;
152
+ if (!art) return null;
153
+
154
+ const publicKey = resolvePublicKey(art, ex);
155
+ if (!publicKey) return null;
156
+
157
+ const issuer = resolveIssuer(art, ex);
158
+ if (!issuer) return null;
159
+
160
+ const versionStr = resolveVersion(art, ex);
161
+ if (!versionStr) return null;
162
+
163
+ const algorithmUpper = trimOrEmpty(art.algorithm).toUpperCase();
164
+ const algorithm = algorithmUpper === 'HS256' ? 'HS256' : 'RS256';
165
+
166
+ const out = {
167
+ enabled: true,
168
+ publicKey,
169
+ algorithm,
170
+ issuer,
171
+ version: versionStr,
172
+ status: resolveStatus(art, ex)
173
+ };
174
+ const level = resolveLevel(art, ex);
175
+ if (level) {
176
+ out.level = level;
177
+ }
178
+ return out;
179
+ }
180
+
181
+ module.exports = {
182
+ buildCertificationFromArtifact,
183
+ pickArtifactForCertificationMerge,
184
+ certificateIdToString
185
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview After successful unified datasource validation, optionally sync system certification.
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('../utils/logger');
11
+ const { trySyncCertificationFromDataplaneForExternalApp } = require('./sync-after-external-command');
12
+ const { cliOptsSkipCertSync } = require('./cli-cert-sync-skip');
13
+
14
+ /**
15
+ * @async
16
+ * @param {number} exitCode
17
+ * @param {string} datasourceKey
18
+ * @param {Object} options - CLI flags (app, noCertSync)
19
+ * @param {string} label - Log label
20
+ * @returns {Promise<void>}
21
+ */
22
+ async function afterUnifiedValidationCertSync(exitCode, datasourceKey, options, label) {
23
+ if (exitCode !== 0 || cliOptsSkipCertSync(options)) return;
24
+ try {
25
+ const { resolveAppKeyForDatasource } = require('../datasource/resolve-app');
26
+ const { appKey } = await resolveAppKeyForDatasource(datasourceKey, options.app);
27
+ await trySyncCertificationFromDataplaneForExternalApp(appKey, label);
28
+ } catch (e) {
29
+ logger.log(chalk.yellow(`⚠ Certification sync (${label}) skipped: ${e.message}`));
30
+ }
31
+ }
32
+
33
+ module.exports = { afterUnifiedValidationCertSync };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @fileoverview Optional certification sync after external flows (validate, tests).
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('../utils/logger');
11
+
12
+ /**
13
+ * After a successful external integration flow, refresh `certification` on the primary system file from dataplane.
14
+ * Best-effort: skips non-external apps, missing Bearer token, or resolver errors (warn only).
15
+ *
16
+ * @async
17
+ * @param {string} appKey - Integration / system key
18
+ * @param {string} label - Short label for logs (e.g. "validate", "datasource test")
19
+ * @returns {Promise<void>}
20
+ */
21
+ async function trySyncCertificationFromDataplaneForExternalApp(appKey, label) {
22
+ try {
23
+ const { detectAppType } = require('../utils/paths');
24
+ const t = await detectAppType(appKey).catch(() => null);
25
+ if (!t || !t.isExternal) return;
26
+
27
+ const { resolveDataplaneAndAuth, validateSystemKeyFormat } = require('../commands/upload');
28
+ const { generateControllerManifest } = require('../generator/external-controller-manifest');
29
+ const { maybeSyncSystemCertificationFromDataplane } = require('./sync-system-certification');
30
+
31
+ validateSystemKeyFormat(appKey);
32
+ const { dataplaneUrl, authConfig } = await resolveDataplaneAndAuth(appKey);
33
+ if (!authConfig.token) {
34
+ logger.log(chalk.gray(`Certification sync (${label}) skipped: no Bearer token (run aifabrix login).`));
35
+ return;
36
+ }
37
+ const manifest = await generateControllerManifest(appKey, { type: 'external' });
38
+ const dsKeys = (manifest.dataSources || []).map((ds) => ds && ds.key).filter(Boolean);
39
+ await maybeSyncSystemCertificationFromDataplane({
40
+ label,
41
+ noCertSync: false,
42
+ systemKey: manifest.key,
43
+ dataplaneUrl,
44
+ authConfig,
45
+ datasourceKeys: dsKeys
46
+ });
47
+ } catch (e) {
48
+ logger.log(chalk.yellow(`⚠ Certification sync (${label}) skipped: ${e.message}`));
49
+ }
50
+ }
51
+
52
+ module.exports = { trySyncCertificationFromDataplaneForExternalApp };
@@ -0,0 +1,197 @@
1
+ /**
2
+ * @fileoverview Sync `certification` on *-system.json|yaml from dataplane active certificate(s).
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const path = require('path');
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const { getIntegrationPath } = require('../utils/paths');
13
+ const { discoverIntegrationFiles } = require('../commands/repair-internal');
14
+ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
15
+ const { unwrapApiData } = require('../utils/external-system-readiness-core');
16
+ const { getActiveIntegrationCertificate } = require('../api/certificates.api');
17
+ const {
18
+ buildCertificationFromArtifact,
19
+ pickArtifactForCertificationMerge
20
+ } = require('./merge-certification-from-artifact');
21
+
22
+ /**
23
+ * @param {string} systemKey
24
+ * @returns {{ systemFilePath: string }|null}
25
+ */
26
+ function resolvePrimarySystemFilePath(systemKey) {
27
+ const appPath = getIntegrationPath(systemKey);
28
+ const { systemFiles } = discoverIntegrationFiles(appPath);
29
+ if (!systemFiles || systemFiles.length === 0) return null;
30
+ return { systemFilePath: path.join(appPath, systemFiles[0]) };
31
+ }
32
+
33
+ /**
34
+ * @async
35
+ * @param {Object} ctx
36
+ * @returns {Promise<Array<import('../api/types/certificates.types').CertificateArtifactResponse>>}
37
+ */
38
+ async function collectActiveArtifacts(ctx) {
39
+ const { dataplaneUrl, authConfig, systemKey, datasourceKeys } = ctx;
40
+ const out = [];
41
+ for (const dk of datasourceKeys || []) {
42
+ if (!dk || typeof dk !== 'string') continue;
43
+ try {
44
+ const res = await getActiveIntegrationCertificate(dataplaneUrl, authConfig, systemKey, dk);
45
+ if (res && res.success === false) continue;
46
+ const art = unwrapApiData(res);
47
+ if (art && typeof art === 'object') out.push(art);
48
+ } catch {
49
+ /* skip datasource */
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+
55
+ /**
56
+ * @param {string} systemFilePath
57
+ * @returns {Object|null}
58
+ */
59
+ function readSystemObject(systemFilePath) {
60
+ try {
61
+ return loadConfigFile(systemFilePath);
62
+ } catch (e) {
63
+ logger.log(chalk.yellow(`⚠ Certification sync: could not read system file: ${e.message}`));
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * @param {string} systemFilePath
70
+ * @param {Object} systemObj
71
+ * @param {Object} nextCert
72
+ * @returns {{ ok: true } | { ok: false }}
73
+ */
74
+ function tryWriteSystemCertification(systemFilePath, systemObj, nextCert) {
75
+ try {
76
+ writeConfigFile(systemFilePath, { ...systemObj, certification: nextCert });
77
+ return { ok: true };
78
+ } catch (e) {
79
+ logger.log(chalk.yellow(`⚠ Certification sync: could not write system file: ${e.message}`));
80
+ return { ok: false };
81
+ }
82
+ }
83
+
84
+ /**
85
+ * @param {Object|null} chosen
86
+ * @param {'no_active'|'no_public_key'} detail
87
+ */
88
+ function logSkippedCertification(chosen, detail) {
89
+ if (detail === 'no_active' || !chosen) {
90
+ logger.log(
91
+ chalk.yellow(
92
+ '⚠ Certification not written: dataplane has no active trusted certificate for this system/datasource scope yet. ' +
93
+ 'Run a successful validation or E2E that issues a certificate, then upload or run cert sync again.'
94
+ )
95
+ );
96
+ return;
97
+ }
98
+ logger.log(
99
+ chalk.yellow(
100
+ '⚠ Certification not written: active certificate has no publicKey and your system file has none to merge. ' +
101
+ 'The dataplane must return publicKey (or add certification.publicKey locally once) to satisfy the schema.'
102
+ )
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Read system file, merge **certification** from dataplane, write back. Does not modify other top-level keys.
108
+ * @async
109
+ * @param {Object} params
110
+ * @param {string} params.systemKey
111
+ * @param {string} params.dataplaneUrl
112
+ * @param {Object} params.authConfig
113
+ * @param {string[]} params.datasourceKeys
114
+ * @returns {Promise<{ written: boolean, reason?: string }>}
115
+ */
116
+ async function syncSystemCertificationFromDataplane(params) {
117
+ const { systemKey, dataplaneUrl, authConfig, datasourceKeys } = params;
118
+ const resolved = resolvePrimarySystemFilePath(systemKey);
119
+ if (!resolved) return { written: false, reason: 'no_system_file' };
120
+ if (!dataplaneUrl || !authConfig || !authConfig.token) {
121
+ return { written: false, reason: 'no_auth' };
122
+ }
123
+
124
+ const systemObj = readSystemObject(resolved.systemFilePath);
125
+ if (!systemObj || typeof systemObj !== 'object') {
126
+ return { written: false, reason: 'invalid_system' };
127
+ }
128
+
129
+ const existing = systemObj.certification;
130
+ const artifacts = await collectActiveArtifacts({
131
+ dataplaneUrl,
132
+ authConfig,
133
+ systemKey,
134
+ datasourceKeys
135
+ });
136
+ const chosen = pickArtifactForCertificationMerge(artifacts);
137
+ const nextCert = buildCertificationFromArtifact(chosen, existing);
138
+ if (!nextCert) {
139
+ const detail = chosen ? 'no_public_key' : 'no_active';
140
+ logSkippedCertification(chosen, detail);
141
+ return { written: false, reason: 'incomplete_certification', detail };
142
+ }
143
+
144
+ const writeResult = tryWriteSystemCertification(resolved.systemFilePath, systemObj, nextCert);
145
+ if (!writeResult.ok) return { written: false, reason: 'write_error' };
146
+ return { written: true };
147
+ }
148
+
149
+ /**
150
+ * Non-throwing entry for upload/deploy: failures are warnings only.
151
+ * @async
152
+ * @param {Object} params
153
+ * @param {string} [params.label] - e.g. "upload"
154
+ * @param {boolean} [params.noCertSync]
155
+ * @returns {Promise<void>}
156
+ */
157
+ function logCertificationSyncNotWritten(r, label) {
158
+ const prefix = label ? ` (${label})` : '';
159
+ const map = {
160
+ no_system_file: `No *-system* file found under integration folder for this key${prefix}.`,
161
+ no_auth: `No Bearer token for dataplane${prefix}; run aifabrix login.`,
162
+ invalid_system: `Could not read or parse the primary system file${prefix}.`,
163
+ incomplete_certification: 'Could not build certification (see message above).',
164
+ write_error: 'Write to system file failed (see message above).'
165
+ };
166
+ const msg = (r && r.reason && map[r.reason]) || `Certification block not updated${prefix}.`;
167
+ logger.log(chalk.yellow(`⚠ ${msg}`));
168
+ }
169
+
170
+ async function maybeSyncSystemCertificationFromDataplane(params) {
171
+ const { label, noCertSync, systemKey, dataplaneUrl, authConfig, datasourceKeys } = params;
172
+ if (noCertSync === true) return;
173
+ try {
174
+ const r = await syncSystemCertificationFromDataplane({
175
+ systemKey,
176
+ dataplaneUrl,
177
+ authConfig,
178
+ datasourceKeys
179
+ });
180
+ if (r.written) {
181
+ logger.log(
182
+ chalk.gray(`Updated certification block from dataplane${label ? ` (${label})` : ''} in system file.`)
183
+ );
184
+ } else if (r.reason) {
185
+ logCertificationSyncNotWritten(r, label);
186
+ }
187
+ } catch (e) {
188
+ logger.log(chalk.yellow(`⚠ Certification sync skipped: ${e.message}`));
189
+ }
190
+ }
191
+
192
+ module.exports = {
193
+ syncSystemCertificationFromDataplane,
194
+ maybeSyncSystemCertificationFromDataplane,
195
+ resolvePrimarySystemFilePath,
196
+ collectActiveArtifacts
197
+ };
@@ -332,6 +332,10 @@ function setupPushDeployDockerfileCommands(program) {
332
332
  .option('--no-poll', 'Do not poll for status')
333
333
  .option('--probe', 'After external deploy, run dataplane runtime checks (validation/run); slower')
334
334
  .option('--probe-timeout <ms>', 'Timeout for --probe on external deploy (default: 120000)', '120000')
335
+ .option(
336
+ '--no-cert-sync',
337
+ 'Skip updating integration certification in the system file from the dataplane after external deploy'
338
+ )
335
339
  .action(async(appName, options) => {
336
340
  try {
337
341
  const probeTimeout =