@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.
- package/.cursor/rules/cli-layout.mdc +75 -0
- package/.cursor/rules/project-rules.mdc +8 -0
- package/.npmrc.token +1 -0
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/jest.projects.js +15 -2
- package/lib/api/certificates.api.js +62 -0
- package/lib/api/index.js +11 -2
- package/lib/api/types/certificates.types.js +48 -0
- package/lib/api/validation-run.api.js +16 -4
- package/lib/api/validation-runner.js +13 -3
- package/lib/app/certification-show-enrich.js +129 -0
- package/lib/app/certification-verify-rows.js +60 -0
- package/lib/app/show-display.js +43 -0
- package/lib/app/show.js +92 -8
- package/lib/certification/cli-cert-sync-skip.js +21 -0
- package/lib/certification/merge-certification-from-artifact.js +185 -0
- package/lib/certification/post-unified-cert-sync.js +33 -0
- package/lib/certification/sync-after-external-command.js +52 -0
- package/lib/certification/sync-system-certification.js +197 -0
- package/lib/cli/setup-app.js +4 -0
- package/lib/cli/setup-app.test-commands.js +24 -8
- package/lib/cli/setup-external-system.js +22 -1
- package/lib/cli/setup-secrets.js +34 -13
- package/lib/cli/setup-utility.js +18 -2
- package/lib/commands/app.js +10 -1
- package/lib/commands/datasource-unified-test-cli.js +50 -117
- package/lib/commands/datasource-unified-test-cli.options.js +44 -2
- package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +106 -0
- package/lib/commands/datasource-validation-cli.js +15 -1
- package/lib/commands/datasource.js +25 -2
- package/lib/commands/upload.js +17 -6
- package/lib/datasource/log-viewer.js +105 -14
- package/lib/datasource/test-e2e.js +35 -17
- package/lib/datasource/unified-validation-run-body.js +3 -0
- package/lib/datasource/unified-validation-run.js +2 -1
- package/lib/external-system/deploy.js +53 -18
- package/lib/infrastructure/compose.js +12 -3
- package/lib/infrastructure/helpers-docker-check.js +67 -0
- package/lib/infrastructure/helpers.js +47 -58
- package/lib/infrastructure/index.js +3 -1
- package/lib/infrastructure/services.js +4 -56
- package/lib/schema/external-system.schema.json +25 -3
- package/lib/schema/type/document-storage.json +15 -2
- package/lib/utils/api.js +28 -3
- package/lib/utils/configuration-env-resolver.js +11 -8
- package/lib/utils/credential-secrets-env.js +5 -5
- package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
- package/lib/utils/datasource-test-run-display.js +19 -2
- package/lib/utils/datasource-test-run-exit.js +25 -0
- package/lib/utils/external-system-display.js +8 -0
- package/lib/utils/external-system-system-test-tty-overview.js +120 -0
- package/lib/utils/external-system-system-test-tty.js +417 -0
- package/lib/utils/paths.js +14 -0
- package/lib/utils/validation-run-poll.js +28 -5
- package/lib/utils/validation-run-post-retry.js +20 -8
- package/lib/utils/validation-run-request.js +18 -0
- package/lib/validation/validate-external-cert-sync.js +23 -0
- package/lib/validation/validate.js +4 -1
- package/package.json +4 -3
- package/scripts/install-local.js +4 -1
- package/scripts/pnpm-global-remove.js +48 -0
- package/templates/applications/dataplane/env.template +4 -0
- package/templates/infra/compose.yaml.hbs +15 -14
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/lib/cli/setup-app.js
CHANGED
|
@@ -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 =
|