@aifabrix/builder 2.55.2 → 2.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/lib/api/pipeline.api.js +32 -3
  2. package/lib/app/deploy.js +6 -5
  3. package/lib/cli/setup-app.js +7 -6
  4. package/lib/cli/setup-external-system-help.js +15 -3
  5. package/lib/cli/setup-external-system.js +21 -3
  6. package/lib/commands/repair-datasource-auto-rbac.js +4 -9
  7. package/lib/commands/upload-openapi-sync-followup.js +59 -0
  8. package/lib/commands/upload-qualification-client.js +131 -0
  9. package/lib/commands/upload-qualification-followups.js +104 -0
  10. package/lib/commands/upload-qualification-poll.js +143 -0
  11. package/lib/commands/upload.js +37 -156
  12. package/lib/deployment/deploy-poll-ui.js +14 -10
  13. package/lib/deployment/deployer-status.js +4 -4
  14. package/lib/external-system/deploy.js +46 -51
  15. package/lib/generator/helpers.js +4 -47
  16. package/lib/generator/index.js +0 -4
  17. package/lib/programmatic/builder-api-request.js +8 -4
  18. package/lib/programmatic/capability-registry.js +2 -0
  19. package/lib/programmatic/get-deploy-status.js +45 -0
  20. package/lib/programmatic/get-upload-status.js +45 -0
  21. package/lib/programmatic/http-accepted-response.js +31 -0
  22. package/lib/programmatic/openapi-descriptions.js +14 -4
  23. package/lib/programmatic/request-timeout.js +21 -15
  24. package/lib/programmatic/route-handler-map.js +6 -0
  25. package/lib/programmatic/run-deploy.js +7 -3
  26. package/lib/programmatic/run-upload.js +11 -7
  27. package/lib/schema/infra.parameter.yaml +6 -7
  28. package/lib/utils/cli-utils.js +3 -2
  29. package/lib/utils/qualification-poll-ui.js +136 -0
  30. package/package.json +3 -2
  31. package/templates/applications/builder-api/env.template +4 -0
  32. package/templates/applications/dataplane/application.yaml +1 -1
  33. package/templates/applications/dataplane/env.template +1 -1
  34. package/templates/applications/keycloak/Dockerfile +12 -5
  35. package/templates/applications/keycloak/application.yaml +2 -2
  36. package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +3 -3
  37. package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +6 -6
  38. package/templates/applications/keycloak/env.template +13 -1
  39. package/templates/applications/miso-controller/application.yaml +1 -1
  40. package/templates/applications/miso-controller/env.template +13 -16
  41. package/lib/generator/deploy-manifest-azure-kv.js +0 -81
@@ -278,7 +278,10 @@ function resolvePipelineUploadTimeoutMs(raw) {
278
278
  * @param {Object} authConfig
279
279
  * @param {Object} payload
280
280
  * @param {Object} [options]
281
- * @param {number|string} [options.timeoutMs] - HTTP + dataplane publish budget (default 300000)
281
+ * @param {number|string} [options.timeoutMs] - HTTP + dataplane publish budget (default 300000); ignored when options.async is true.
282
+ * @param {boolean} [options.async=false] - When true, durably accept the upload for qualification (202 jobId/candidateFingerprint) instead of blocking for the ladder (plan 191.0 task 1).
283
+ * @param {string} [options.gate] - Optional comma-separated checks: operations, trust, governance, full.
284
+ * @param {string} [options.subjectEmail] - Optional comma-separated governance subject emails.
282
285
  */
283
286
  async function uploadApplicationViaPipeline(
284
287
  dataplaneUrl,
@@ -287,8 +290,17 @@ async function uploadApplicationViaPipeline(
287
290
  options = {}
288
291
  ) {
289
292
  const body = { ...payload, status: payload.status ?? 'draft' };
290
- const timeoutMs = resolvePipelineUploadTimeoutMs(options.timeoutMs);
291
293
  const client = createDataplaneApiClient(dataplaneUrl, authConfig);
294
+ if (options.async === true) {
295
+ const params = { async: 'true' };
296
+ if (options.gate) params.gate = options.gate;
297
+ if (options.subjectEmail) params.subjectEmail = options.subjectEmail;
298
+ return await client.post('/api/v1/pipeline/upload', {
299
+ body,
300
+ params
301
+ });
302
+ }
303
+ const timeoutMs = resolvePipelineUploadTimeoutMs(options.timeoutMs);
292
304
  return await client.post('/api/v1/pipeline/upload', {
293
305
  body,
294
306
  timeoutMs,
@@ -297,6 +309,23 @@ async function uploadApplicationViaPipeline(
297
309
  });
298
310
  }
299
311
 
312
+ /**
313
+ * Get durable publish/qualification job status.
314
+ * GET /api/v1/publish-jobs/{jobId}
315
+ * @requiresPermission {Dataplane} external-system:publish or external-system:read
316
+ * @async
317
+ * @function getPublishJobStatus
318
+ * @param {string} dataplaneUrl - Dataplane base URL
319
+ * @param {Object} authConfig - Authentication configuration
320
+ * @param {string} jobId - Publish/qualification job id returned by uploadApplicationViaPipeline({ async: true })
321
+ * @returns {Promise<Object>} API envelope `{ success, data }` where `data` is `{ jobId, state, targetType, targetKey, steps, error, result }`
322
+ * @throws {Error} If request fails
323
+ */
324
+ async function getPublishJobStatus(dataplaneUrl, authConfig, jobId) {
325
+ const client = createDataplaneApiClient(dataplaneUrl, authConfig);
326
+ return await client.get(`/api/v1/publish-jobs/${encodeURIComponent(jobId)}`);
327
+ }
328
+
300
329
  module.exports = {
301
330
  validatePipeline,
302
331
  deployPipeline,
@@ -307,7 +336,7 @@ module.exports = {
307
336
  testSystemViaPipeline,
308
337
  testDatasourceViaPipeline,
309
338
  uploadApplicationViaPipeline,
339
+ getPublishJobStatus,
310
340
  resolvePipelineUploadTimeoutMs,
311
341
  PIPELINE_UPLOAD_TIMEOUT_MS
312
342
  };
313
-
package/lib/app/deploy.js CHANGED
@@ -308,10 +308,12 @@ function validateImageIsPullable(imageRef, appName) {
308
308
  */
309
309
  function throwIfDeploymentFailed(status, statusObj) {
310
310
  if (status !== 'failed' && status !== 'cancelled') return;
311
- const msg =
312
- statusObj.message ||
313
- statusObj.error ||
314
- (status === 'cancelled' ? 'Deployment cancelled' : 'Deployment failed');
311
+ const reportedErrors = Array.isArray(statusObj.errors) ? statusObj.errors.filter(Boolean).join('; ') : '';
312
+ // A failed deployment can retain its last progress message (for example
313
+ // "Database-only mode"). Prefer the terminal error so the CLI reports the cause.
314
+ const msg = status === 'failed'
315
+ ? statusObj.error || reportedErrors || statusObj.message || 'Deployment failed'
316
+ : statusObj.message || statusObj.error || reportedErrors || 'Deployment cancelled';
315
317
  const err = new Error(`Deployment ${status}: ${msg}`);
316
318
  err.formatted = `Deployment ${status}.\n\n${msg}`;
317
319
  err.deploymentStatus = statusObj;
@@ -495,4 +497,3 @@ module.exports = {
495
497
  validateAppName,
496
498
  validateImageIsPullable
497
499
  };
498
-
@@ -56,20 +56,21 @@ Examples:
56
56
  --client-id "$DEV_MISO_CLIENTID" \\
57
57
  --client-secret "$DEV_MISO_CLIENTSECRET"
58
58
 
59
- # CI/CD — external integration (after upload/tests; promote to controller)
59
+ # CI/CD — external integration (qualify once via upload, then fast promote via deploy)
60
60
  $ aifabrix validate hubspot-demo
61
61
  $ aifabrix upload hubspot-demo --probe
62
62
  $ aifabrix deploy hubspot-demo --probe -v
63
- # Prove pillars in tst/CI, then promote (plain deploy seals cert on published Deploy)
64
- $ aifabrix deploy ek-sales --gate full --subject-email ops@example.com
65
63
  $ aifabrix deploy ek-sales
66
64
 
67
65
  Notes:
68
66
  - Login or set MISO_CONTROLLER_URL + environment (aifabrix login / aifabrix auth).
69
67
  - CI: register once (aifabrix app register <appKey>), store DEV_MISO_CLIENTID and DEV_MISO_CLIENTSECRET in pipeline secrets.
70
- - upload = dataplane publish for dev iteration; deploy = controller validate + deploy (Azure or local Docker).
71
- - Prove (tst/CI): use --gate full (optional --subject-email) before promote; plain deploy promotes without live E2E gates.
72
- - After a successful published Deploy, the platform seals the integration certificate (no separate certificates issue step).
68
+ - For Connected Systems, upload validates, publishes, and finalizes the system; optional
69
+ checks run only with upload --gate. Deploy promotes the completed upload — the
70
+ server decides this automatically, no flag needed. A system that has never been uploaded
71
+ falls back to the full legacy ladder at deploy time instead.
72
+ - --gate is a no-op on deploy (kept for compatibility; prints a notice) — gates now run once
73
+ during "aifabrix upload", not at deploy time.
73
74
  - Default output is a compact status card; use -v for expanded preflight and readiness detail.
74
75
  - Controller deploy always polls to a terminal status (no --poll / --no-poll on this command).
75
76
  - --repository-url overrides repo URL for pipeline validate (default: application.yaml repository.repositoryUrl).
@@ -6,11 +6,18 @@
6
6
 
7
7
  const UPLOAD_HELP_AFTER = `
8
8
  Examples:
9
- # Publish integration/<systemKey>/ to the dataplane (typical lab flow after validate + resolve)
9
+ # Upload and wait for completion
10
10
  $ aifabrix upload hubspot-demo
11
11
  $ aifabrix upload hubspot-demo -v
12
12
  $ aifabrix upload hubspot-demo --probe
13
- $ aifabrix upload hubspot-demo --timeout 600000
13
+ $ aifabrix upload hubspot-demo --gate operations,trust
14
+ $ aifabrix upload hubspot-demo --gate full --subject-email user@example.com
15
+
16
+ # Queue the upload and return immediately (script/CI-friendly)
17
+ $ aifabrix upload hubspot-demo --no-wait
18
+
19
+ # Resume watching an existing upload job (e.g. after Ctrl-C or a closed terminal)
20
+ $ aifabrix upload hubspot-demo --resume <jobId>
14
21
 
15
22
  # Build and validate payload only (no API calls)
16
23
  $ aifabrix upload hubspot-demo --dry-run
@@ -25,9 +32,14 @@ Examples:
25
32
  Notes:
26
33
  - Requires login (aifabrix auth status).
27
34
  - Reads files under integration/<systemKey>/ or role-assistant/<roleKey>/ for digital workers.
35
+ - Upload validates, publishes, and finalizes the system. Operations, trust, and governance
36
+ checks are optional and run only when selected with --gate. The upload keeps running on
37
+ Dataplane even if this CLI process is interrupted, closed, or times out — --resume re-attaches
38
+ to it later, it is never re-submitted or lost.
28
39
  - Default output is a compact readiness summary; use -v for expanded detail and server-side validate warnings.
29
40
  - --probe runs dataplane runtime checks after publish (slower).
30
- - --timeout sets publish budget in ms (HTTP client and dataplane); default 300000.
41
+ - --timeout bounds only how long this CLI waits before printing a resume hint
42
+ (default 1800000ms/30min); it does not stop the upload job.
31
43
  - --env selects role-assistant.yaml section (dev|tst|pro) for digital workers; default dev.
32
44
  - Role Assistant packages under role-assistant/ do not use application.yaml; upload applies settings and Evidence only.
33
45
  - test-e2e and test-integration publish local files first by default; use --no-publish on those commands to skip.
@@ -50,7 +50,11 @@ async function handleUploadCommandAction(systemKey, options) {
50
50
  options.probeTimeout === undefined || options.probeTimeout === null
51
51
  ? 120000
52
52
  : Number(options.probeTimeout);
53
- const timeoutMs = resolvePipelineUploadTimeoutMs(options.timeout);
53
+ // Only resolve --timeout when the caller explicitly set it; leaving it undefined otherwise
54
+ // lets the qualification poll fall back to its own default (30min) instead of the legacy
55
+ // 300000ms HTTP/dataplane-publish budget default, which no longer applies to the async path.
56
+ const timeoutMs =
57
+ options.timeout !== undefined ? resolvePipelineUploadTimeoutMs(options.timeout) : undefined;
54
58
  await upload.uploadExternalSystem(systemKey, {
55
59
  ...options,
56
60
  probeTimeout: Number.isFinite(probeTimeout) ? probeTimeout : 120000,
@@ -101,9 +105,17 @@ function setupUploadCommand(program) {
101
105
  .option('-v, --verbose', 'Expanded upload summary and server-side pipeline validate warnings before publish')
102
106
  .option('--probe', 'After publish, run dataplane runtime checks (validation/run); slower')
103
107
  .option('--probe-timeout <ms>', 'Timeout for --probe (default: 120000)', '120000')
108
+ .option(
109
+ '--gate <gates>',
110
+ 'Optional upload checks (comma-separated): operations, trust, governance, full'
111
+ )
112
+ .option(
113
+ '--subject-email <emails>',
114
+ 'Subject email(s) for governance checks (comma-separated; used with --gate governance|full)'
115
+ )
104
116
  .option(
105
117
  '--timeout <ms>',
106
- 'Publish timeout in ms for HTTP and dataplane budget (default: 300000; min 5000, max 2700000)'
118
+ 'How long this CLI waits for the upload job before printing a resume hint (default: 1800000/30min; min 5000, max 2700000). The job keeps running regardless.'
107
119
  )
108
120
  .option(
109
121
  '--force',
@@ -114,6 +126,12 @@ function setupUploadCommand(program) {
114
126
  'Role Assistant settings environment section: dev, tst, or pro (default: dev)',
115
127
  'dev'
116
128
  )
129
+ .option('--wait', 'Wait for the upload job to complete (default)', true)
130
+ .option('--no-wait', 'Return the upload job id immediately without waiting')
131
+ .option(
132
+ '--resume <jobId>',
133
+ 'Wait for an existing upload job instead of submitting again'
134
+ )
117
135
  .action(async(systemKey, options) => {
118
136
  try {
119
137
  await handleUploadCommandAction(systemKey, options);
@@ -341,4 +359,4 @@ function setupExternalSystemCommands(program) {
341
359
  setupExternalSystemTestCommands(program);
342
360
  }
343
361
 
344
- module.exports = { setupExternalSystemCommands };
362
+ module.exports = { setupExternalSystemCommands, handleUploadCommandAction };
@@ -34,18 +34,14 @@ function buildRenameMap(operationMap) {
34
34
  if (!operationMap || typeof operationMap !== 'object' || Array.isArray(operationMap)) return {};
35
35
  const renameMap = {};
36
36
  for (const k of Object.keys(operationMap)) {
37
- // If it's already schema-valid but contains capitals, normalize to lowercase to align
38
- // with RBAC permission name restrictions (external-system schema forbids A-Z).
37
+ // Operation keys use the capability naming contract, which allows camelCase.
38
+ // RBAC permission names are normalized separately when permissions are generated.
39
39
  if (/^[a-z][a-zA-Z0-9]*$/.test(k)) {
40
- if (/[A-Z]/.test(k)) {
41
- renameMap[k] = k.toLowerCase();
42
- }
43
40
  continue;
44
41
  }
45
42
  const camel = toCamelCaseKey(k);
46
43
  if (!camel || camel === k) continue;
47
- // Use lowercase key to keep RBAC permission names schema-valid.
48
- renameMap[k] = camel.toLowerCase();
44
+ renameMap[k] = camel;
49
45
  }
50
46
  return renameMap;
51
47
  }
@@ -212,7 +208,7 @@ function normalizeAutoRbacOperationKeys(parsed, changes) {
212
208
  if (updated && Array.isArray(changes)) {
213
209
  const pairs = keysToRename.map((k) => `${k}→${renameMap[k]}`).join(', ');
214
210
  changes.push(
215
- `Normalized autoRbac operation keys (permission names are lowercase per schema; kebab aliases fold into canonical keys): ${pairs}`
211
+ `Normalized schema-invalid autoRbac operation aliases to camelCase keys (RBAC permission names remain lowercase): ${pairs}`
216
212
  );
217
213
  }
218
214
 
@@ -224,4 +220,3 @@ module.exports = {
224
220
  toCamelCaseKey,
225
221
  toKebabAliasKey
226
222
  };
227
-
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @fileoverview Post-publish local OpenAPI sync follow-up, shared by the synchronous and
3
+ * qualification-async upload paths (extracted from upload.js per plan 191.0 file-size gate).
4
+ * @author AI Fabrix Team
5
+ * @version 1.0.0
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const logger = require('../utils/logger');
11
+ const { formatSuccessLine, formatWarningLine } = require('../utils/cli-test-layout-chalk');
12
+ const { resolveExternalSystemPath } = require('../utils/app-path-resolve');
13
+ const { maybeSyncOpenApiFilesForMcp } = require('./repair-openapi-sync');
14
+ const { exportProgrammaticOpenApiForRepair } = require('./repair-openapi-export');
15
+
16
+ /**
17
+ * Upload local `integration/<systemKey>/openapi/*.json` specs when present so dataplane can store
18
+ * vendor OpenAPI keyed by each datasource `openapi.documentKey` (same behavior as repair --api OpenAPI sync).
19
+ * Call this **after** the external system row exists (successful publish or completed qualification):
20
+ * list/upload endpoints require it. First publish still succeeds because pipeline materializes
21
+ * internal specs from `openapi.operations` when needed.
22
+ *
23
+ * @param {string} systemKey
24
+ * @param {Object} manifest
25
+ * @param {Object} [uploadContext]
26
+ * @returns {Promise<void>}
27
+ */
28
+ async function logAndSyncLocalOpenApiForUpload(systemKey, manifest, uploadContext = {}) {
29
+ const pathOptions = uploadContext.pathOptions || {};
30
+ const appPath = resolveExternalSystemPath(systemKey, pathOptions);
31
+ const ei = manifest && manifest.externalIntegration;
32
+ const datasourceFiles = ei && Array.isArray(ei.dataSources) ? ei.dataSources : [];
33
+ await exportProgrammaticOpenApiForRepair({
34
+ appPath,
35
+ systemKey,
36
+ datasourceFiles,
37
+ dryRun: false,
38
+ backupCtx: { dryRun: false, changedFiles: [] }
39
+ });
40
+ const openapiSyncLines = await maybeSyncOpenApiFilesForMcp({
41
+ enabled: true,
42
+ dryRun: false,
43
+ appPath,
44
+ systemKey,
45
+ datasourceFiles,
46
+ dataplaneUrl: uploadContext.dataplaneUrl,
47
+ authConfig: uploadContext.authConfig,
48
+ // Always replace SpecFile rows keyed by openapi.documentKey so vendor/full
49
+ // specs are not left missing while only thin internal:* slices remain.
50
+ force: true
51
+ });
52
+ if (!uploadContext.quiet) {
53
+ for (const line of openapiSyncLines) {
54
+ logger.log(line.startsWith('Skipped') ? formatWarningLine(line) : formatSuccessLine(line));
55
+ }
56
+ }
57
+ }
58
+
59
+ module.exports = { logAndSyncLocalOpenApiForUpload };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @fileoverview Resumable Dataplane qualification upload: accept, --wait/--no-wait, poll, resume.
3
+ * Mirrors dev-host-ops.js's job id + wait/no-wait contract (plan 191.0 task 1). Never invents
4
+ * qualification truth — accept/poll surface exactly what Dataplane returns.
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const logger = require('../utils/logger');
12
+ const { formatSuccessLine, formatWarningLine } = require('../utils/cli-test-layout-chalk');
13
+ const { formatApiError } = require('../utils/api-error-handler');
14
+ const { uploadApplicationViaPipelineWithAuthRetry } = require('../utils/pipeline-upload-auth-retry');
15
+ const { pollQualificationJobUntilTerminal } = require('./upload-qualification-poll');
16
+ const { createQualificationPollHandlers } = require('../utils/qualification-poll-ui');
17
+
18
+ /**
19
+ * Ctrl-C / disconnect during polling must detach without corrupting truth (plan 191.0 task 4):
20
+ * the qualification job keeps running on Dataplane regardless of whether this CLI process exits.
21
+ * This only makes that safe fact visible, printing the resume command before exiting 130.
22
+ * @param {string} jobId
23
+ * @param {string} systemKey
24
+ * @returns {() => void} Call to remove the listeners once polling finishes normally.
25
+ */
26
+ function registerUploadInterruptHint(jobId, systemKey) {
27
+ const onSignal = () => {
28
+ logger.log(
29
+ formatWarningLine(
30
+ `Interrupted — upload job ${jobId} is still running, unaffected. Resume with: aifabrix upload ${systemKey} --resume ${jobId}`
31
+ )
32
+ );
33
+ process.exit(130);
34
+ };
35
+ process.once('SIGINT', onSignal);
36
+ process.once('SIGTERM', onSignal);
37
+ return () => {
38
+ process.removeListener('SIGINT', onSignal);
39
+ process.removeListener('SIGTERM', onSignal);
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Submit the candidate for durable Dataplane qualification (async). Always returns quickly —
45
+ * a 202 accept, or the current state when an equivalent job is already in flight — never blocks
46
+ * for the ladder to finish.
47
+ * @param {string} dataplaneUrl
48
+ * @param {Object} authConfig
49
+ * @param {Object} payload
50
+ * @param {string} systemKey
51
+ * @param {Object} [options] - Forwarded to uploadApplicationViaPipeline (e.g. timeoutMs, ignored when async)
52
+ * @returns {Promise<{jobId: string, state: string, resumed: boolean, candidateFingerprint: string}>}
53
+ */
54
+ async function acceptQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options = {}) {
55
+ const res = await uploadApplicationViaPipelineWithAuthRetry(dataplaneUrl, authConfig, payload, systemKey, {
56
+ ...options,
57
+ async: true
58
+ });
59
+ if (res?.success === false) {
60
+ const err = new Error(formatApiError(res, dataplaneUrl));
61
+ const status = Number(res.status || res.statusCode);
62
+ err.statusCode = status >= 400 && status < 600 ? status : 400;
63
+ throw err;
64
+ }
65
+ return res.data;
66
+ }
67
+
68
+ /**
69
+ * Poll an existing qualification job id to terminal state (resume without re-uploading).
70
+ * @param {string} dataplaneUrl
71
+ * @param {Object} authConfig
72
+ * @param {string} jobId
73
+ * @param {string} systemKey - For the printed resume hint if interrupted mid-poll.
74
+ * @param {{ json?: boolean, silent?: boolean, timeoutMs?: number|string }} [options] - `timeoutMs`
75
+ * bounds only how long this CLI process waits (CLI `--timeout`); Dataplane keeps running the
76
+ * job regardless, so timing out returns a resumable jobId, never a fabricated failure.
77
+ * @returns {Promise<Object>} Terminal job status
78
+ */
79
+ async function resumeQualificationUpload(dataplaneUrl, authConfig, jobId, systemKey, options = {}) {
80
+ const pollUi = options.json || options.silent
81
+ ? createQualificationPollHandlers(jobId, { silent: true })
82
+ : createQualificationPollHandlers(jobId, { verbose: options.verbose === true });
83
+ const unregisterInterruptHint = registerUploadInterruptHint(jobId, systemKey);
84
+ try {
85
+ return await pollQualificationJobUntilTerminal(
86
+ dataplaneUrl,
87
+ authConfig,
88
+ jobId,
89
+ pollUi,
90
+ options.timeoutMs !== undefined ? Number(options.timeoutMs) : undefined
91
+ );
92
+ } finally {
93
+ pollUi.finish();
94
+ unregisterInterruptHint();
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Accept then, unless `--no-wait`, poll to terminal. On `--no-wait`, prints/returns the
100
+ * resumable job id immediately and never blocks.
101
+ * @param {string} dataplaneUrl
102
+ * @param {Object} authConfig
103
+ * @param {Object} payload
104
+ * @param {string} systemKey
105
+ * @param {{ wait?: boolean, json?: boolean, timeoutMs?: number|string }} [options]
106
+ * @returns {Promise<{jobId: string, candidateFingerprint: string, terminal: Object|null}>}
107
+ */
108
+ async function runQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options = {}) {
109
+ const accepted = await acceptQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options);
110
+ const wait = options.wait !== false;
111
+ if (!wait) {
112
+ if (options.json) {
113
+ logger.log(JSON.stringify({ ...accepted, terminal: null }, null, 2));
114
+ } else {
115
+ logger.log(
116
+ formatSuccessLine(
117
+ `Upload queued (job ${accepted.jobId}). Resume with: aifabrix upload ${systemKey} --resume ${accepted.jobId}`
118
+ )
119
+ );
120
+ }
121
+ return { ...accepted, terminal: null };
122
+ }
123
+ const terminal = await resumeQualificationUpload(dataplaneUrl, authConfig, accepted.jobId, systemKey, options);
124
+ return { ...accepted, terminal };
125
+ }
126
+
127
+ module.exports = {
128
+ acceptQualificationUpload,
129
+ resumeQualificationUpload,
130
+ runQualificationUpload
131
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @fileoverview Post-qualification follow-ups for the async upload path: credential push,
3
+ * local OpenAPI sync, sidecar governance/evidence sync, probe and a qualification-specific
4
+ * summary (plan 191.0 task 1).
5
+ *
6
+ * Note: this intentionally does not reconstruct the legacy tiered datasource-readiness summary
7
+ * (`logUploadReadinessSummary`) that the synchronous path shows — that summary classifies real
8
+ * server-assigned datasource state (status/isActive/mcpContract) which requires a fresh
9
+ * Dataplane fetch+remap not yet ported to this path. The qualification summary below reports
10
+ * what Dataplane actually returned instead of approximating server state from local data.
11
+ * @author AI Fabrix Team
12
+ * @version 1.0.0
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const logger = require('../utils/logger');
18
+ const { formatSuccessLine, metadata, sectionTitle } = require('../utils/cli-test-layout-chalk');
19
+ const { pushAndLogCredentialSecrets } = require('./upload-credential-push-log');
20
+ const { finalizeUploadPublishResult } = require('./upload-data-sync-finalize');
21
+ const { runUploadSidecarFollowups } = require('./upload-sidecar-followups');
22
+ const { logAndSyncLocalOpenApiForUpload } = require('./upload-openapi-sync-followup');
23
+ const { maybeRunUploadProbe } = require('./upload-probe');
24
+
25
+ /**
26
+ * @param {string} systemKey
27
+ * @param {Object} _accepted - Machine-readable qualification identity (not shown in CLI output)
28
+ * @param {Object} job - Terminal PublishJobStatusResponse data
29
+ * @param {Object} options
30
+ */
31
+ function logQualifiedUploadSummary(systemKey, _accepted, job, options) {
32
+ if (options.suppressSummary === true || options.json === true) {
33
+ return;
34
+ }
35
+ logger.log('');
36
+ logger.log(sectionTitle('Upload complete'));
37
+ logger.log(metadata(`System: ${systemKey}`));
38
+ if (options.verbose === true) {
39
+ logger.log(metadata(`Job: ${job.jobId}`));
40
+ const certificateIds = job.result && Array.isArray(job.result.certificateIds) ? job.result.certificateIds : [];
41
+ if (certificateIds.length > 0) {
42
+ logger.log(metadata(`Certificates: ${certificateIds.join(', ')}`));
43
+ }
44
+ }
45
+ logger.log(formatSuccessLine(`${systemKey} uploaded successfully.`));
46
+ }
47
+
48
+ /**
49
+ * Runs the same downstream follow-ups as the synchronous publish path (credential push, local
50
+ * OpenAPI sync, sidecar governance/evidence sync, optional probe) once a qualification job has
51
+ * reached `completed`, then finalizes with the same result-envelope shape as the sync path.
52
+ * @param {string} systemKey
53
+ * @param {Object} options
54
+ * @param {Object} manifest
55
+ * @param {Object} payload
56
+ * @param {{ dataplaneUrl: string, authConfig: Object, environment: string, accepted: Object, job: Object }} ctx
57
+ * @returns {Promise<Object>}
58
+ */
59
+ async function completeQualifiedUpload(systemKey, options, manifest, payload, ctx) {
60
+ const quietCompact = options.minimal === true && options.verbose !== true;
61
+ const pushMeta = await pushAndLogCredentialSecrets(ctx.dataplaneUrl, ctx.authConfig, systemKey, payload, {
62
+ json: options.json === true,
63
+ pathOptions: options.pathOptions,
64
+ suppressCredentialLog: quietCompact,
65
+ excludeSecretKeys: options.excludeSecretKeys
66
+ });
67
+ await logAndSyncLocalOpenApiForUpload(systemKey, manifest, {
68
+ dataplaneUrl: ctx.dataplaneUrl,
69
+ authConfig: ctx.authConfig,
70
+ pathOptions: options.pathOptions,
71
+ quiet: quietCompact
72
+ });
73
+ const sidecar = await runUploadSidecarFollowups({
74
+ systemKey,
75
+ options,
76
+ payload,
77
+ dataplaneUrl: ctx.dataplaneUrl,
78
+ authConfig: ctx.authConfig,
79
+ environment: ctx.environment
80
+ });
81
+ logQualifiedUploadSummary(systemKey, ctx.accepted, ctx.job, options);
82
+ if (options.probe === true) {
83
+ await maybeRunUploadProbe(ctx.dataplaneUrl, systemKey, ctx.authConfig, options.probeTimeout, {
84
+ compact: quietCompact
85
+ });
86
+ }
87
+ return finalizeUploadPublishResult(
88
+ {
89
+ dataplaneUrl: ctx.dataplaneUrl,
90
+ authConfig: ctx.authConfig,
91
+ environment: ctx.environment,
92
+ uploadSkipped: false,
93
+ sidecar,
94
+ credentialPush: pushMeta.credentialPush,
95
+ configPush: pushMeta.configPush,
96
+ jobId: ctx.job.jobId,
97
+ candidateFingerprint: ctx.accepted.candidateFingerprint
98
+ },
99
+ payload,
100
+ options
101
+ );
102
+ }
103
+
104
+ module.exports = { completeQualifiedUpload, logQualifiedUploadSummary };