@aifabrix/builder 2.44.2 → 2.44.4

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 (62) hide show
  1. package/.npmrc.token +1 -1
  2. package/integration/roundtrip-test-local/README.md +1 -2
  3. package/integration/roundtrip-test-local2/README.md +1 -2
  4. package/jest.projects.js +12 -1
  5. package/lib/api/certificates.api.js +21 -3
  6. package/lib/api/types/certificates.types.js +1 -1
  7. package/lib/app/show-display.js +60 -16
  8. package/lib/certification/merge-certification-from-artifact.js +46 -16
  9. package/lib/certification/post-unified-cert-sync.js +13 -2
  10. package/lib/certification/sync-after-external-command.js +6 -3
  11. package/lib/certification/sync-system-certification.js +60 -14
  12. package/lib/cli/setup-app.test-commands.js +67 -35
  13. package/lib/cli/setup-utility.js +1 -1
  14. package/lib/commands/datasource-unified-test-cli.js +81 -46
  15. package/lib/commands/datasource-unified-test-cli.options.js +4 -2
  16. package/lib/commands/datasource.js +3 -31
  17. package/lib/commands/repair-datasource-keys.js +1 -1
  18. package/lib/commands/repair-datasource-openapi.js +57 -0
  19. package/lib/commands/repair-datasource.js +5 -0
  20. package/lib/commands/repair-internal.js +2 -4
  21. package/lib/commands/repair.js +1 -2
  22. package/lib/commands/test-e2e-external.js +5 -6
  23. package/lib/commands/upload.js +18 -4
  24. package/lib/commands/wizard-dataplane.js +14 -6
  25. package/lib/datasource/datasource-validate-display.js +162 -0
  26. package/lib/datasource/datasource-validate-summary.js +194 -0
  27. package/lib/datasource/test-e2e.js +65 -37
  28. package/lib/datasource/unified-validation-run-body.js +1 -2
  29. package/lib/datasource/validate.js +14 -6
  30. package/lib/external-system/test.js +12 -8
  31. package/lib/generator/external-controller-manifest.js +12 -2
  32. package/lib/schema/cip-capacity-display.fallback.json +7 -0
  33. package/lib/schema/datasource-test-run.schema.json +79 -1
  34. package/lib/schema/external-datasource.schema.json +94 -2
  35. package/lib/schema/external-system.schema.json +29 -6
  36. package/lib/schema/flag-map-validation-run.json +1 -2
  37. package/lib/schema/type/document-storage.json +83 -3
  38. package/lib/utils/configuration-env-resolver.js +38 -0
  39. package/lib/utils/dataplane-resolver.js +3 -2
  40. package/lib/utils/datasource-test-run-capacity-operations.js +149 -0
  41. package/lib/utils/datasource-test-run-debug-display.js +143 -1
  42. package/lib/utils/datasource-test-run-display.js +46 -33
  43. package/lib/utils/datasource-test-run-tty-log.js +6 -2
  44. package/lib/utils/datasource-test-run-tty-meta-lines.js +123 -0
  45. package/lib/utils/error-formatter.js +32 -2
  46. package/lib/utils/external-system-readiness-core.js +39 -0
  47. package/lib/utils/external-system-readiness-deploy-display.js +2 -3
  48. package/lib/utils/external-system-readiness-display-internals.js +3 -2
  49. package/lib/utils/external-system-system-test-tty.js +33 -9
  50. package/lib/utils/external-system-validators.js +62 -5
  51. package/lib/utils/load-cip-capacity-display-config.js +130 -0
  52. package/lib/utils/paths.js +10 -3
  53. package/lib/utils/schema-resolver.js +98 -2
  54. package/lib/utils/validation-run-poll.js +15 -4
  55. package/lib/utils/validation-run-request.js +4 -6
  56. package/lib/validation/dimension-display-helpers.js +60 -0
  57. package/lib/validation/external-manifest-validator.js +22 -15
  58. package/lib/validation/validate-display-log-helpers.js +39 -0
  59. package/lib/validation/validate-display.js +89 -83
  60. package/package.json +1 -1
  61. package/templates/applications/dataplane/env.template +2 -1
  62. package/templates/external-system/README.md.hbs +1 -2
package/.npmrc.token CHANGED
@@ -1 +1 @@
1
- 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
1
+ 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
@@ -123,8 +123,7 @@ Options:
123
123
  -e, --env <env> Environment: dev, tst, or pro
124
124
  -v, --verbose Show detailed step output and poll progress
125
125
  --debug Include debug output and write log to integration/roundtrip-test-local/logs/
126
- --test-crud Enable CRUD lifecycle test (body testCrud: true)
127
- --record-id <id> Record ID for test (body recordId)
126
+ --no-run-scenarios Skip expanding testPayload.scenarios in capacity step
128
127
  --no-cleanup Disable cleanup after test (body cleanup: false)
129
128
  --primary-key-value <value|@path> Primary key value or path to JSON file (e.g. @pk.json) for body primaryKeyValue
130
129
  --no-async Use sync mode (no polling); single POST, no asyncRun
@@ -123,8 +123,7 @@ Options:
123
123
  -e, --env <env> Environment: dev, tst, or pro
124
124
  -v, --verbose Show detailed step output and poll progress
125
125
  --debug Include debug output and write log to integration/roundtrip-test-local2/logs/
126
- --test-crud Enable CRUD lifecycle test (body testCrud: true)
127
- --record-id <id> Record ID for test (body recordId)
126
+ --no-run-scenarios Skip expanding testPayload.scenarios in capacity step
128
127
  --no-cleanup Disable cleanup after test (body cleanup: false)
129
128
  --primary-key-value <value|@path> Primary key value or path to JSON file (e.g. @pk.json) for body primaryKeyValue
130
129
  --no-async Use sync mode (no polling); single POST, no asyncRun
package/jest.projects.js CHANGED
@@ -72,6 +72,9 @@ const defaultProject = {
72
72
  '/tests/lib/datasource/log-viewer.test.js',
73
73
  '\\\\tests\\\\lib\\\\datasource\\\\log-viewer.test.js',
74
74
  'lib/datasource/log-viewer.test.js',
75
+ '/tests/lib/datasource/log-viewer-structural.test.js',
76
+ '\\\\tests\\\\lib\\\\datasource\\\\log-viewer-structural.test.js',
77
+ 'lib/datasource/log-viewer-structural.test.js',
75
78
  '/tests/lib/commands/parameters-validate.test.js',
76
79
  '\\\\tests\\\\lib\\\\commands\\\\parameters-validate.test.js',
77
80
  'lib/commands/parameters-validate.test.js',
@@ -190,6 +193,10 @@ const defaultProject = {
190
193
  '\\\\tests\\\\lib\\\\validation\\\\schema-241-alignment.test.js',
191
194
  'lib/validation/schema-241-alignment.test.js',
192
195
  'schema-241-alignment\\.test\\.js',
196
+ '/tests/lib/utils/schema-resolver-order.test.js',
197
+ '\\\\tests\\\\lib\\\\utils\\\\schema-resolver-order.test.js',
198
+ 'lib/utils/schema-resolver-order.test.js',
199
+ 'schema-resolver-order\\.test\\.js',
193
200
  '/tests/lib/app/app.test.js',
194
201
  '\\\\tests\\\\lib\\\\app\\\\app.test.js',
195
202
  'lib/app/app.test.js',
@@ -222,7 +229,10 @@ const isolatedProjects = [
222
229
  makeIsolatedProject('datasource-validation-watch', [
223
230
  '**/tests/lib/utils/datasource-validation-watch.test.js'
224
231
  ]),
225
- makeIsolatedProject('log-viewer', ['**/tests/lib/datasource/log-viewer.test.js']),
232
+ makeIsolatedProject('log-viewer', [
233
+ '**/tests/lib/datasource/log-viewer.test.js',
234
+ '**/tests/lib/datasource/log-viewer-structural.test.js'
235
+ ]),
226
236
  makeIsolatedProject('datasource-test-run-schema-sync', [
227
237
  '**/tests/lib/utils/datasource-test-run-schema-sync.test.js'
228
238
  ]),
@@ -285,6 +295,7 @@ const isolatedProjects = [
285
295
  makeIsolatedProject('generator-validation', ['**/tests/lib/generator/generator-validation.test.js']),
286
296
  makeIsolatedProject('secrets-databaselog', ['**/tests/lib/core/secrets-databaselog.test.js']),
287
297
  makeIsolatedProject('schema-241-alignment', ['**/tests/lib/validation/schema-241-alignment.test.js']),
298
+ makeIsolatedProject('schema-resolver-order', ['**/tests/lib/utils/schema-resolver-order.test.js']),
288
299
  makeIsolatedProject('app-module', ['**/tests/lib/app/app.test.js']),
289
300
  makeIsolatedProject('admin-secrets', ['**/tests/lib/core/admin-secrets.test.js'])
290
301
  ];
@@ -5,6 +5,24 @@
5
5
  */
6
6
 
7
7
  const { ApiClient } = require('./index');
8
+ const { normalizeDataplaneAuth } = require('./validation-run.api');
9
+
10
+ /**
11
+ * @param {Object} authConfig
12
+ * @returns {Object}
13
+ */
14
+ function dataplaneAuthForBearerGet(authConfig) {
15
+ if (!authConfig || typeof authConfig !== 'object') {
16
+ return authConfig;
17
+ }
18
+ if (authConfig.token || authConfig.clientId) {
19
+ return authConfig;
20
+ }
21
+ if (authConfig.apiKey) {
22
+ return { ...authConfig, token: authConfig.apiKey, type: authConfig.type || 'bearer' };
23
+ }
24
+ return normalizeDataplaneAuth(authConfig);
25
+ }
8
26
 
9
27
  /**
10
28
  * Get active trusted integration certificate for a datasource.
@@ -18,7 +36,7 @@ const { ApiClient } = require('./index');
18
36
  * @returns {Promise<Object>} API envelope `{ success, data?, status, ... }`
19
37
  */
20
38
  async function getActiveIntegrationCertificate(dataplaneUrl, authConfig, systemKey, datasourceKey) {
21
- const client = new ApiClient(dataplaneUrl, authConfig);
39
+ const client = new ApiClient(dataplaneUrl, dataplaneAuthForBearerGet(authConfig));
22
40
  const path = `/api/v1/systems/${encodeURIComponent(systemKey)}/datasources/${encodeURIComponent(
23
41
  datasourceKey
24
42
  )}/certificates/active`;
@@ -36,7 +54,7 @@ async function getActiveIntegrationCertificate(dataplaneUrl, authConfig, systemK
36
54
  * @returns {Promise<Object>}
37
55
  */
38
56
  async function listIntegrationCertificates(dataplaneUrl, authConfig, params = {}) {
39
- const client = new ApiClient(dataplaneUrl, authConfig);
57
+ const client = new ApiClient(dataplaneUrl, dataplaneAuthForBearerGet(authConfig));
40
58
  return await client.get('/api/v1/certificates', { params });
41
59
  }
42
60
 
@@ -51,7 +69,7 @@ async function listIntegrationCertificates(dataplaneUrl, authConfig, params = {}
51
69
  * @returns {Promise<Object>}
52
70
  */
53
71
  async function verifyIntegrationCertificate(dataplaneUrl, authConfig, body) {
54
- const client = new ApiClient(dataplaneUrl, authConfig);
72
+ const client = new ApiClient(dataplaneUrl, dataplaneAuthForBearerGet(authConfig));
55
73
  return await client.post('/api/v1/certificates/verify', { body: body || {} });
56
74
  }
57
75
 
@@ -22,7 +22,7 @@
22
22
  * @property {string} [issuedBy]
23
23
  * @property {string} [licenseLevelIssuer]
24
24
  * @property {string} [dataplaneVersion]
25
- * @property {('RS256'|'HS256')} [algorithm] RS256 production; HS256 local dev HMAC signer only
25
+ * @property {'RS256'} [algorithm] Integration certificate signing algorithm (RS256 only)
26
26
  * @property {string|null} [publicKey]
27
27
  * @property {string|null} [publicKeyFingerprint]
28
28
  * @property {Object} [metadata]
@@ -11,7 +11,6 @@
11
11
 
12
12
  const chalk = require('chalk');
13
13
  const logger = require('../utils/logger');
14
- const { truncatePublicKeyPreview } = require('./certification-show-enrich');
15
14
 
16
15
  function logSourceAndHeader(summary) {
17
16
  const isOffline = summary.source === 'offline';
@@ -263,23 +262,51 @@ function displayExternalAppBlock(summary) {
263
262
  }
264
263
 
265
264
  /**
266
- * Local certification + optional verify rows (external integrations).
267
- * @param {Object} summary
265
+ * Colored certification outcome from `status` string.
266
+ * @param {string} raw - lowercased status
267
+ * @returns {string}
268
268
  */
269
- function logCertificationSection(summary) {
270
- if (!summary.isExternal) return;
271
- logger.log('');
272
- logger.log('🪪 Certification (local system file)');
273
- const c = summary.localCertification;
274
- if (!c || typeof c !== 'object') {
275
- logger.log(' (none or unreadable)');
276
- } else {
277
- logger.log(` enabled: ${c.enabled}`);
278
- logger.log(` algorithm: ${c.algorithm ?? '—'}`);
279
- logger.log(` issuer: ${c.issuer ?? '—'}`);
280
- logger.log(` version: ${c.version ?? '—'}`);
281
- logger.log(` publicKey: ${truncatePublicKeyPreview(c.publicKey, 64)}`);
269
+ function formatCertificationOutcome(raw) {
270
+ if (raw === 'passed') return chalk.green('passed');
271
+ if (raw === 'not_passed') return chalk.red('not_passed');
272
+ if (raw === 'pending') return chalk.yellow('pending');
273
+ return raw || '—';
274
+ }
275
+
276
+ /**
277
+ * Certification status line: enabled ✔/✖ plus outcome (passed / not_passed / pending).
278
+ * @param {Object} c - certification block from system file
279
+ * @returns {string}
280
+ */
281
+ function formatCertificationStatusLine(c) {
282
+ const enabledTrue = c.enabled === true;
283
+ const enabledSym = enabledTrue ? chalk.green('✔') : chalk.red('✖');
284
+ const raw = (c.status && String(c.status).toLowerCase()) || '';
285
+ const outcome = formatCertificationOutcome(raw);
286
+ return ` Status: ${enabledSym} ${outcome}`;
287
+ }
288
+
289
+ /**
290
+ * Local certification fields from system file (TTY block).
291
+ * @param {Object} c
292
+ */
293
+ function logCertificationLocalFileDetails(c) {
294
+ logger.log(formatCertificationStatusLine(c));
295
+ const levelStr =
296
+ c.level === undefined || c.level === null ? '' : String(c.level).trim();
297
+ if (levelStr) {
298
+ logger.log(` Level: ${levelStr}`);
282
299
  }
300
+ logger.log(` algorithm: ${c.algorithm ?? '—'}`);
301
+ logger.log(` issuer: ${c.issuer ?? '—'}`);
302
+ logger.log(` version: ${c.version ?? '—'}`);
303
+ }
304
+
305
+ /**
306
+ * Dataplane certification verify rows (when `--verify-cert` populated summary).
307
+ * @param {Object} summary
308
+ */
309
+ function logCertificationVerifySection(summary) {
283
310
  if (summary.certificationVerifyRows && summary.certificationVerifyRows.length > 0) {
284
311
  logger.log('');
285
312
  logger.log('🪪 Certification verify (dataplane)');
@@ -303,6 +330,23 @@ function logCertificationSection(summary) {
303
330
  }
304
331
  }
305
332
 
333
+ /**
334
+ * Local certification + optional verify rows (external integrations).
335
+ * @param {Object} summary
336
+ */
337
+ function logCertificationSection(summary) {
338
+ if (!summary.isExternal) return;
339
+ logger.log('');
340
+ logger.log('🪪 Certification (local system file)');
341
+ const c = summary.localCertification;
342
+ if (!c || typeof c !== 'object') {
343
+ logger.log(' (none or unreadable)');
344
+ } else {
345
+ logCertificationLocalFileDetails(c);
346
+ }
347
+ logCertificationVerifySection(summary);
348
+ }
349
+
306
350
  /**
307
351
  * Format and print human-readable show output (offline or online).
308
352
  * @param {Object} summary - Unified summary (buildOfflineSummaryFromDeployJson or buildOnlineSummary)
@@ -38,14 +38,14 @@ function pickArtifactForCertificationMerge(artifacts) {
38
38
  }
39
39
 
40
40
  /**
41
- * @param {string} algorithmUpper
42
41
  * @param {import('../api/types/certificates.types').CertificateArtifactResponse} art
42
+ * @param {Object} ex
43
43
  * @returns {string}
44
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'}`;
45
+ function resolvePublicKey(art, ex) {
46
+ const fromArt = trimOrEmpty(art.publicKey);
47
+ if (fromArt) return fromArt;
48
+ return trimOrEmpty(ex.publicKey);
49
49
  }
50
50
 
51
51
  /**
@@ -53,13 +53,34 @@ function hs256DevPublicKeyPlaceholder(algorithmUpper, art) {
53
53
  * @param {Object} ex
54
54
  * @returns {string}
55
55
  */
56
- function resolvePublicKey(art, ex) {
57
- const fromArt = trimOrEmpty(art.publicKey);
56
+ function resolvePublicKeyFingerprint(art, ex) {
57
+ const fromArt = trimOrEmpty(art.publicKeyFingerprint);
58
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);
59
+ return trimOrEmpty(ex.publicKeyFingerprint);
60
+ }
61
+
62
+ /**
63
+ * @param {string} raw
64
+ * @returns {string} Normalized digest or empty when invalid
65
+ */
66
+ function normalizeContractHash(raw) {
67
+ const s = trimOrEmpty(raw);
68
+ return CONTRACT_HASH_PATTERN.test(s) ? s : '';
69
+ }
70
+
71
+ /**
72
+ * @param {import('../api/types/certificates.types').CertificateArtifactResponse} art
73
+ * @param {Object} ex
74
+ * @returns {string}
75
+ */
76
+ function resolveContractHash(art, ex) {
77
+ return (
78
+ normalizeContractHash(art.contractHash) ||
79
+ normalizeContractHash(art.integrationHash) ||
80
+ normalizeContractHash(ex.contractHash) ||
81
+ normalizeContractHash(ex.integrationHash) ||
82
+ ''
83
+ );
63
84
  }
64
85
 
65
86
  /**
@@ -92,6 +113,8 @@ function resolveVersion(art, ex) {
92
113
 
93
114
  const CERTIFICATION_LEVELS = new Set(['BRONZE', 'SILVER', 'GOLD', 'PLATINUM']);
94
115
  const CERTIFICATION_STATUSES = new Set(['passed', 'not_passed', 'pending']);
116
+ /** Matches external-system.schema.json `certification.contractHash` pattern. */
117
+ const CONTRACT_HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
95
118
 
96
119
  /**
97
120
  * @param {string} raw
@@ -138,9 +161,10 @@ function resolveStatus(_art, ex) {
138
161
  }
139
162
 
140
163
  /**
141
- * Build `certification` object matching **external-system.schema.json** (required: enabled, publicKey, algorithm, issuer, version; optional status, level).
164
+ * Build `certification` object matching **external-system.schema.json** (required: enabled, publicKey, algorithm, issuer, version; optional status, level, publicKeyFingerprint, contractHash).
142
165
  * 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.
166
+ * Dataplane issues **RS256** certificates with PEM `publicKey` and optional `publicKeyFingerprint` (sha256:… of SPKI); merge output uses **RS256** only.
167
+ * Optional **contractHash** is copied from the certificate `contractHash` or legacy `integrationHash` when it matches `sha256:` + 64 hex.
144
168
  *
145
169
  * @param {import('../api/types/certificates.types').CertificateArtifactResponse|null} artifact
146
170
  * @param {Object|null|undefined} existingCertification - Current `system.certification`
@@ -160,13 +184,13 @@ function buildCertificationFromArtifact(artifact, existingCertification) {
160
184
  const versionStr = resolveVersion(art, ex);
161
185
  if (!versionStr) return null;
162
186
 
163
- const algorithmUpper = trimOrEmpty(art.algorithm).toUpperCase();
164
- const algorithm = algorithmUpper === 'HS256' ? 'HS256' : 'RS256';
187
+ const publicKeyFingerprint = resolvePublicKeyFingerprint(art, ex);
188
+ const contractHash = resolveContractHash(art, ex);
165
189
 
166
190
  const out = {
167
191
  enabled: true,
168
192
  publicKey,
169
- algorithm,
193
+ algorithm: 'RS256',
170
194
  issuer,
171
195
  version: versionStr,
172
196
  status: resolveStatus(art, ex)
@@ -175,6 +199,12 @@ function buildCertificationFromArtifact(artifact, existingCertification) {
175
199
  if (level) {
176
200
  out.level = level;
177
201
  }
202
+ if (publicKeyFingerprint) {
203
+ out.publicKeyFingerprint = publicKeyFingerprint;
204
+ }
205
+ if (contractHash) {
206
+ out.contractHash = contractHash;
207
+ }
178
208
  return out;
179
209
  }
180
210
 
@@ -10,6 +10,7 @@ const chalk = require('chalk');
10
10
  const logger = require('../utils/logger');
11
11
  const { trySyncCertificationFromDataplaneForExternalApp } = require('./sync-after-external-command');
12
12
  const { cliOptsSkipCertSync } = require('./cli-cert-sync-skip');
13
+ const { resolveDebugDisplayMode } = require('../utils/datasource-test-run-debug-display');
13
14
 
14
15
  /**
15
16
  * @async
@@ -17,14 +18,24 @@ const { cliOptsSkipCertSync } = require('./cli-cert-sync-skip');
17
18
  * @param {string} datasourceKey
18
19
  * @param {Object} options - CLI flags (app, noCertSync)
19
20
  * @param {string} label - Log label
21
+ * @param {Object|null} [envelope] - Last DatasourceTestRun (optional); used for certificateIssuance failed hint
20
22
  * @returns {Promise<void>}
21
23
  */
22
- async function afterUnifiedValidationCertSync(exitCode, datasourceKey, options, label) {
24
+ async function afterUnifiedValidationCertSync(exitCode, datasourceKey, options, label, envelope = null) {
23
25
  if (exitCode !== 0 || cliOptsSkipCertSync(options)) return;
24
26
  try {
25
27
  const { resolveAppKeyForDatasource } = require('../datasource/resolve-app');
26
28
  const { appKey } = await resolveAppKeyForDatasource(datasourceKey, options.app);
27
- await trySyncCertificationFromDataplaneForExternalApp(appKey, label);
29
+ let issuanceFailureHint = null;
30
+ if (envelope && envelope.certificateIssuance && envelope.certificateIssuance.status === 'failed') {
31
+ const ci = envelope.certificateIssuance;
32
+ issuanceFailureHint = [ci.reasonCode, ci.message].filter(Boolean).join(': ');
33
+ }
34
+ const verboseCertHints = resolveDebugDisplayMode(options.debug) !== null;
35
+ await trySyncCertificationFromDataplaneForExternalApp(appKey, label, {
36
+ issuanceFailureHint,
37
+ verboseCertHints
38
+ });
28
39
  } catch (e) {
29
40
  logger.log(chalk.yellow(`⚠ Certification sync (${label}) skipped: ${e.message}`));
30
41
  }
@@ -16,9 +16,10 @@ const logger = require('../utils/logger');
16
16
  * @async
17
17
  * @param {string} appKey - Integration / system key
18
18
  * @param {string} label - Short label for logs (e.g. "validate", "datasource test")
19
+ * @param {Object} [extra] - Optional `{ issuanceFailureHint }` from last DatasourceTestRun.certificateIssuance (failed)
19
20
  * @returns {Promise<void>}
20
21
  */
21
- async function trySyncCertificationFromDataplaneForExternalApp(appKey, label) {
22
+ async function trySyncCertificationFromDataplaneForExternalApp(appKey, label, extra = {}) {
22
23
  try {
23
24
  const { detectAppType } = require('../utils/paths');
24
25
  const t = await detectAppType(appKey).catch(() => null);
@@ -29,7 +30,7 @@ async function trySyncCertificationFromDataplaneForExternalApp(appKey, label) {
29
30
  const { maybeSyncSystemCertificationFromDataplane } = require('./sync-system-certification');
30
31
 
31
32
  validateSystemKeyFormat(appKey);
32
- const { dataplaneUrl, authConfig } = await resolveDataplaneAndAuth(appKey);
33
+ const { dataplaneUrl, authConfig } = await resolveDataplaneAndAuth(appKey, { silent: true });
33
34
  if (!authConfig.token) {
34
35
  logger.log(chalk.gray(`Certification sync (${label}) skipped: no Bearer token (run aifabrix login).`));
35
36
  return;
@@ -42,7 +43,9 @@ async function trySyncCertificationFromDataplaneForExternalApp(appKey, label) {
42
43
  systemKey: manifest.key,
43
44
  dataplaneUrl,
44
45
  authConfig,
45
- datasourceKeys: dsKeys
46
+ datasourceKeys: dsKeys,
47
+ issuanceFailureHint: extra && extra.issuanceFailureHint ? String(extra.issuanceFailureHint).trim() : null,
48
+ verboseCertHints: extra && extra.verboseCertHints === true
46
49
  });
47
50
  } catch (e) {
48
51
  logger.log(chalk.yellow(`⚠ Certification sync (${label}) skipped: ${e.message}`));
@@ -84,15 +84,43 @@ function tryWriteSystemCertification(systemFilePath, systemObj, nextCert) {
84
84
  /**
85
85
  * @param {Object|null} chosen
86
86
  * @param {'no_active'|'no_public_key'} detail
87
+ * @param {string|null} [issuanceFailureHint] - From last DatasourceTestRun.certificateIssuance when status failed
88
+ * @param {boolean} [verboseCertHints] - Extra gray lines and auto-issue detail (CLI `--debug`)
87
89
  */
88
- function logSkippedCertification(chosen, detail) {
90
+ function logSkippedCertification(chosen, detail, issuanceFailureHint, verboseCertHints = false) {
89
91
  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
- );
92
+ const hint = issuanceFailureHint && String(issuanceFailureHint).trim();
93
+ const certNotPassed =
94
+ hint &&
95
+ (hint.includes('CERTIFICATION_NOT_PASSED') ||
96
+ hint.toLowerCase().includes('certification did not pass'));
97
+ if (certNotPassed) {
98
+ logger.log(
99
+ chalk.yellow(
100
+ '⚠ Certification not written: no active integration certificate because certification did not pass on the dataplane issuance validation pass.'
101
+ )
102
+ );
103
+ if (verboseCertHints) {
104
+ logger.log(
105
+ chalk.gray(
106
+ ' E2E can be green for every datasource while auto-issue still fails: issuance re-validates the whole system before signing.'
107
+ )
108
+ );
109
+ if (hint) {
110
+ logger.log(chalk.gray(` Auto-issue detail: ${hint}`));
111
+ }
112
+ }
113
+ } else {
114
+ logger.log(
115
+ chalk.yellow(
116
+ '⚠ Certification not written: dataplane has no active trusted certificate for this system/datasource scope yet. ' +
117
+ 'Fix auto-issue or signing (see hint), or run unified validation then upload or run cert sync again.'
118
+ )
119
+ );
120
+ if (hint && verboseCertHints) {
121
+ logger.log(chalk.gray(` Auto-issue detail: ${hint}`));
122
+ }
123
+ }
96
124
  return;
97
125
  }
98
126
  logger.log(
@@ -111,13 +139,19 @@ function logSkippedCertification(chosen, detail) {
111
139
  * @param {string} params.dataplaneUrl
112
140
  * @param {Object} params.authConfig
113
141
  * @param {string[]} params.datasourceKeys
142
+ * @param {boolean} [params.verboseCertHints]
114
143
  * @returns {Promise<{ written: boolean, reason?: string }>}
115
144
  */
116
145
  async function syncSystemCertificationFromDataplane(params) {
117
- const { systemKey, dataplaneUrl, authConfig, datasourceKeys } = params;
146
+ const { systemKey, dataplaneUrl, authConfig, datasourceKeys, issuanceFailureHint, verboseCertHints } = params;
118
147
  const resolved = resolvePrimarySystemFilePath(systemKey);
119
148
  if (!resolved) return { written: false, reason: 'no_system_file' };
120
- if (!dataplaneUrl || !authConfig || !authConfig.token) {
149
+ const hasBearerLike =
150
+ authConfig &&
151
+ typeof authConfig === 'object' &&
152
+ ((typeof authConfig.token === 'string' && authConfig.token.trim()) ||
153
+ (typeof authConfig.apiKey === 'string' && authConfig.apiKey.trim()));
154
+ if (!dataplaneUrl || !authConfig || !hasBearerLike) {
121
155
  return { written: false, reason: 'no_auth' };
122
156
  }
123
157
 
@@ -137,7 +171,7 @@ async function syncSystemCertificationFromDataplane(params) {
137
171
  const nextCert = buildCertificationFromArtifact(chosen, existing);
138
172
  if (!nextCert) {
139
173
  const detail = chosen ? 'no_public_key' : 'no_active';
140
- logSkippedCertification(chosen, detail);
174
+ logSkippedCertification(chosen, detail, issuanceFailureHint, verboseCertHints === true);
141
175
  return { written: false, reason: 'incomplete_certification', detail };
142
176
  }
143
177
 
@@ -154,7 +188,8 @@ async function syncSystemCertificationFromDataplane(params) {
154
188
  * @param {boolean} [params.noCertSync]
155
189
  * @returns {Promise<void>}
156
190
  */
157
- function logCertificationSyncNotWritten(r, label) {
191
+ function logCertificationSyncNotWritten(r, label, verboseCertHints = false) {
192
+ if (r && r.reason === 'incomplete_certification' && !verboseCertHints) return;
158
193
  const prefix = label ? ` (${label})` : '';
159
194
  const map = {
160
195
  no_system_file: `No *-system* file found under integration folder for this key${prefix}.`,
@@ -168,21 +203,32 @@ function logCertificationSyncNotWritten(r, label) {
168
203
  }
169
204
 
170
205
  async function maybeSyncSystemCertificationFromDataplane(params) {
171
- const { label, noCertSync, systemKey, dataplaneUrl, authConfig, datasourceKeys } = params;
206
+ const {
207
+ label,
208
+ noCertSync,
209
+ systemKey,
210
+ dataplaneUrl,
211
+ authConfig,
212
+ datasourceKeys,
213
+ issuanceFailureHint,
214
+ verboseCertHints
215
+ } = params;
172
216
  if (noCertSync === true) return;
173
217
  try {
174
218
  const r = await syncSystemCertificationFromDataplane({
175
219
  systemKey,
176
220
  dataplaneUrl,
177
221
  authConfig,
178
- datasourceKeys
222
+ datasourceKeys,
223
+ issuanceFailureHint,
224
+ verboseCertHints
179
225
  });
180
226
  if (r.written) {
181
227
  logger.log(
182
228
  chalk.gray(`Updated certification block from dataplane${label ? ` (${label})` : ''} in system file.`)
183
229
  );
184
230
  } else if (r.reason) {
185
- logCertificationSyncNotWritten(r, label);
231
+ logCertificationSyncNotWritten(r, label, verboseCertHints === true);
186
232
  }
187
233
  } catch (e) {
188
234
  logger.log(chalk.yellow(`⚠ Certification sync skipped: ${e.message}`));
@@ -54,6 +54,70 @@ function setupTestCommand(program) {
54
54
  });
55
55
  }
56
56
 
57
+ /**
58
+ * First failed `certificateIssuance` across E2E datasource rows (for cert-sync hint).
59
+ * @param {Array<{ key?: string, datasourceTestRun?: { certificateIssuance?: { status?: string, reasonCode?: string, message?: string } } }>} results
60
+ * @returns {string|null}
61
+ */
62
+ function firstIssuanceFailureHintFromE2eResults(results) {
63
+ const failed = [];
64
+ for (const row of results || []) {
65
+ const ci = row.datasourceTestRun && row.datasourceTestRun.certificateIssuance;
66
+ if (ci && ci.status === 'failed') {
67
+ failed.push({ key: row.key, ci });
68
+ }
69
+ }
70
+ if (failed.length === 0) return null;
71
+ const firstRc = failed[0].ci.reasonCode;
72
+ const sameAll = failed.every(f => f.ci.reasonCode === firstRc);
73
+ if (sameAll && failed.length > 1) {
74
+ const parts = [firstRc, failed[0].ci.message].filter(Boolean);
75
+ return `${parts.join(': ')} (${failed.length} datasource scopes; first: ${failed[0].key})`;
76
+ }
77
+ const parts = [failed[0].ci.reasonCode, failed[0].ci.message].filter(Boolean);
78
+ return `[${failed[0].key}] ${parts.join(': ')}`.trim();
79
+ }
80
+
81
+ /**
82
+ * External integration E2E: run, display, exit on aggregate, optional cert sync.
83
+ * @param {string} appName
84
+ * @param {Object} options
85
+ * @returns {Promise<void>}
86
+ */
87
+ async function runExternalIntegrationE2EAndCertSync(appName, options) {
88
+ const { runTestE2EForExternalSystem } = require('../commands/test-e2e-external');
89
+ const { success, results } = await runTestE2EForExternalSystem(appName, {
90
+ env: options.env,
91
+ debug: options.debug,
92
+ verbose: options.verbose,
93
+ async: options.async !== false,
94
+ sync: options.sync === true
95
+ });
96
+ const { displayIntegrationTestResults } = require('../utils/external-system-display');
97
+ const datasourceResults = results.map(r => ({
98
+ key: r.key,
99
+ success: r.success,
100
+ error: r.error,
101
+ skipped: false,
102
+ datasourceTestRun: r.datasourceTestRun
103
+ }));
104
+ displayIntegrationTestResults(
105
+ { systemKey: appName, success, datasourceResults },
106
+ options.verbose,
107
+ { debug: options.debug, runType: 'e2e' }
108
+ );
109
+ const { computeSystemExitCodeFromDatasourceRows } = require('../utils/datasource-test-run-exit');
110
+ const exitCode = computeSystemExitCodeFromDatasourceRows(datasourceResults, {
111
+ warningsAsErrors: options.warningsAsErrors === true,
112
+ requireCert: options.requireCert === true
113
+ });
114
+ if (exitCode !== 0) process.exit(exitCode);
115
+ if (cliOptsSkipCertSync(options)) return;
116
+ const { trySyncCertificationFromDataplaneForExternalApp } = require('../certification/sync-after-external-command');
117
+ const issuanceFailureHint = firstIssuanceFailureHintFromE2eResults(results);
118
+ await trySyncCertificationFromDataplaneForExternalApp(appName, 'test-e2e', { issuanceFailureHint });
119
+ }
120
+
57
121
  async function runTestE2ECommand(appName, options) {
58
122
  const pathsUtil = require('../utils/paths');
59
123
  const appType = await pathsUtil.detectAppType(appName).catch(() => null);
@@ -64,40 +128,7 @@ async function runTestE2ECommand(appName, options) {
64
128
  );
65
129
  }
66
130
  if (appType && appType.baseDir === 'integration') {
67
- const { runTestE2EForExternalSystem } = require('../commands/test-e2e-external');
68
- const { success, results } = await runTestE2EForExternalSystem(appName, {
69
- env: options.env,
70
- debug: options.debug,
71
- verbose: options.verbose,
72
- async: options.async !== false,
73
- sync: options.sync === true
74
- });
75
- const { displayIntegrationTestResults } = require('../utils/external-system-display');
76
- const datasourceResults = results.map(r => ({
77
- key: r.key,
78
- success: r.success,
79
- error: r.error,
80
- skipped: false,
81
- datasourceTestRun: r.datasourceTestRun
82
- }));
83
- displayIntegrationTestResults(
84
- {
85
- systemKey: appName,
86
- success,
87
- datasourceResults
88
- },
89
- options.verbose,
90
- { debug: options.debug, runType: 'e2e' }
91
- );
92
- const { computeSystemExitCodeFromDatasourceRows } = require('../utils/datasource-test-run-exit');
93
- const exitCode = computeSystemExitCodeFromDatasourceRows(datasourceResults, {
94
- warningsAsErrors: options.warningsAsErrors === true,
95
- requireCert: options.requireCert === true
96
- });
97
- if (exitCode !== 0) process.exit(exitCode);
98
- if (cliOptsSkipCertSync(options)) return;
99
- const { trySyncCertificationFromDataplaneForExternalApp } = require('../certification/sync-after-external-command');
100
- await trySyncCertificationFromDataplaneForExternalApp(appName, 'test-e2e');
131
+ await runExternalIntegrationE2EAndCertSync(appName, options);
101
132
  return;
102
133
  }
103
134
  const { runAppTestE2e } = require('../commands/app-test');
@@ -190,6 +221,7 @@ function setupInstallTestE2eLintCommands(program) {
190
221
  }
191
222
 
192
223
  module.exports = {
193
- setupInstallTestE2eLintCommands
224
+ setupInstallTestE2eLintCommands,
225
+ firstIssuanceFailureHintFromE2eResults
194
226
  };
195
227