@aifabrix/builder 2.41.0 → 2.42.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 (138) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +1 -1
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +34 -1
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +3 -1
  24. package/lib/app/prompts.js +44 -29
  25. package/lib/app/readme.js +8 -3
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +42 -11
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/credential-env.js +162 -0
  37. package/lib/commands/credential-list.js +17 -22
  38. package/lib/commands/credential-push.js +96 -0
  39. package/lib/commands/datasource.js +77 -6
  40. package/lib/commands/dev-init.js +39 -1
  41. package/lib/commands/repair-auth-config.js +99 -0
  42. package/lib/commands/repair-datasource-keys.js +208 -0
  43. package/lib/commands/repair-datasource.js +235 -0
  44. package/lib/commands/repair-env-template.js +348 -0
  45. package/lib/commands/repair-internal.js +85 -0
  46. package/lib/commands/repair-rbac.js +158 -0
  47. package/lib/commands/repair.js +507 -0
  48. package/lib/commands/test-e2e-external.js +165 -0
  49. package/lib/commands/upload.js +71 -40
  50. package/lib/commands/wizard-core-helpers.js +226 -4
  51. package/lib/commands/wizard-core.js +67 -29
  52. package/lib/commands/wizard-dataplane.js +1 -1
  53. package/lib/commands/wizard-entity-selection.js +43 -0
  54. package/lib/commands/wizard-headless.js +44 -5
  55. package/lib/commands/wizard-helpers.js +7 -3
  56. package/lib/commands/wizard.js +86 -64
  57. package/lib/core/config.js +7 -1
  58. package/lib/core/secrets.js +33 -12
  59. package/lib/datasource/deploy.js +12 -3
  60. package/lib/datasource/test-e2e.js +219 -0
  61. package/lib/datasource/test-integration.js +154 -0
  62. package/lib/deployment/deployer.js +7 -5
  63. package/lib/external-system/download.js +182 -204
  64. package/lib/external-system/generator.js +204 -56
  65. package/lib/external-system/test-execution.js +2 -1
  66. package/lib/external-system/test-system-level.js +73 -0
  67. package/lib/external-system/test.js +51 -18
  68. package/lib/generator/external-controller-manifest.js +29 -2
  69. package/lib/generator/external-schema-utils.js +1 -1
  70. package/lib/generator/external.js +10 -3
  71. package/lib/generator/index.js +4 -1
  72. package/lib/generator/split-readme.js +1 -0
  73. package/lib/generator/split-variables.js +7 -1
  74. package/lib/generator/split.js +194 -54
  75. package/lib/generator/wizard-prompts-secondary.js +294 -0
  76. package/lib/generator/wizard-prompts.js +105 -106
  77. package/lib/generator/wizard-readme.js +88 -0
  78. package/lib/generator/wizard.js +147 -158
  79. package/lib/infrastructure/compose.js +11 -1
  80. package/lib/infrastructure/index.js +11 -3
  81. package/lib/infrastructure/services.js +22 -11
  82. package/lib/schema/application-schema.json +8 -5
  83. package/lib/schema/external-datasource.schema.json +49 -26
  84. package/lib/schema/external-system.schema.json +82 -6
  85. package/lib/schema/wizard-config.schema.json +16 -0
  86. package/lib/utils/api.js +38 -10
  87. package/lib/utils/auth-headers.js +8 -7
  88. package/lib/utils/compose-generator.js +1 -1
  89. package/lib/utils/compose-handlebars-helpers.js +11 -0
  90. package/lib/utils/config-format-preference.js +51 -0
  91. package/lib/utils/config-format.js +36 -0
  92. package/lib/utils/configuration-env-resolver.js +179 -0
  93. package/lib/utils/credential-display.js +83 -0
  94. package/lib/utils/credential-secrets-env.js +115 -25
  95. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  96. package/lib/utils/deployment-validation-helpers.js +4 -4
  97. package/lib/utils/dev-ca-install.js +139 -0
  98. package/lib/utils/env-copy.js +23 -3
  99. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  100. package/lib/utils/error-formatters/permission-errors.js +0 -1
  101. package/lib/utils/error-formatters/validation-errors.js +0 -1
  102. package/lib/utils/external-readme.js +56 -29
  103. package/lib/utils/external-system-display.js +59 -1
  104. package/lib/utils/external-system-test-helpers.js +21 -8
  105. package/lib/utils/external-system-validators.js +3 -0
  106. package/lib/utils/file-upload.js +20 -50
  107. package/lib/utils/help-builder.js +1 -0
  108. package/lib/utils/infra-status.js +50 -44
  109. package/lib/utils/local-secrets.js +5 -5
  110. package/lib/utils/paths.js +85 -4
  111. package/lib/utils/secrets-canonical.js +93 -0
  112. package/lib/utils/secrets-generator.js +20 -0
  113. package/lib/utils/secrets-helpers.js +75 -89
  114. package/lib/utils/test-log-writer.js +56 -0
  115. package/lib/utils/token-manager.js +24 -32
  116. package/lib/validation/env-template-auth.js +157 -0
  117. package/lib/validation/env-template-kv.js +41 -0
  118. package/lib/validation/external-manifest-validator.js +25 -0
  119. package/lib/validation/external-system-auth-rules.js +86 -0
  120. package/lib/validation/validate-batch.js +149 -0
  121. package/lib/validation/validate-datasource-keys-api.js +33 -0
  122. package/lib/validation/validate-display.js +94 -16
  123. package/lib/validation/validate.js +25 -12
  124. package/lib/validation/validator.js +7 -9
  125. package/lib/validation/wizard-datasource-validation.js +50 -0
  126. package/package.json +7 -2
  127. package/templates/applications/dataplane/application.yaml +1 -1
  128. package/templates/applications/dataplane/env.template +5 -5
  129. package/templates/applications/dataplane/rbac.yaml +2 -2
  130. package/templates/applications/miso-controller/env.template +1 -1
  131. package/templates/external-system/README.md.hbs +65 -25
  132. package/templates/external-system/deploy.js.hbs +4 -2
  133. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  134. package/templates/external-system/external-system.json.hbs +1 -18
  135. package/templates/infra/compose.yaml.hbs +6 -0
  136. package/templates/python/docker-compose.hbs +4 -4
  137. package/templates/typescript/docker-compose.hbs +4 -4
  138. package/integration/hubspot/application.yaml +0 -37
@@ -5,41 +5,18 @@
5
5
  "type": "openapi",
6
6
  "enabled": true,
7
7
  "authentication": {
8
- "type": "oauth2",
9
- "mode": "oauth2",
10
- "oauth2": {
11
- "tokenUrl": "{{TOKENURL}}",
12
- "clientId": "{{CLIENTID}}",
13
- "clientSecret": "{{CLIENTSECRET}}",
14
- "scopes": [
15
- "crm.objects.companies.read",
16
- "crm.objects.companies.write",
17
- "crm.objects.contacts.read",
18
- "crm.objects.contacts.write",
19
- "crm.objects.deals.read",
20
- "crm.objects.deals.write"
21
- ]
8
+ "method": "oauth2",
9
+ "variables": {
10
+ "baseUrl": "https://api.hubapi.com",
11
+ "tokenUrl": "https://api.hubapi.com/oauth/v1/token",
12
+ "scope": "crm.objects.companies.read crm.objects.companies.write crm.objects.contacts.read crm.objects.contacts.write crm.objects.deals.read crm.objects.deals.write"
13
+ },
14
+ "security": {
15
+ "clientId": "kv://hubspot/clientid",
16
+ "clientSecret": "kv://hubspot/clientsecret"
22
17
  }
23
18
  },
24
19
  "configuration": [
25
- {
26
- "name": "CLIENTID",
27
- "value": "hubspot-clientidKeyVault",
28
- "location": "keyvault",
29
- "required": true
30
- },
31
- {
32
- "name": "CLIENTSECRET",
33
- "value": "hubspot-clientsecretKeyVault",
34
- "location": "keyvault",
35
- "required": true
36
- },
37
- {
38
- "name": "TOKENURL",
39
- "value": "https://api.hubapi.com/oauth/v1/token",
40
- "location": "variable",
41
- "required": true
42
- },
43
20
  {
44
21
  "name": "HUBSPOT_API_VERSION",
45
22
  "value": "v3",
@@ -86,5 +63,11 @@
86
63
  "sales",
87
64
  "marketing",
88
65
  "hubspot"
66
+ ],
67
+ "dataSources": [
68
+ "hubspot-company",
69
+ "hubspot-contact",
70
+ "hubspot-deal",
71
+ "hubspot-users-datasource"
89
72
  ]
90
73
  }
@@ -194,27 +194,27 @@ async function createTestDatasource(datasourcePath) {
194
194
  }
195
195
 
196
196
  /**
197
- * Builds datasource deploy command arguments
198
- * @function buildDatasourceDeployArgs
197
+ * Builds datasource upload command arguments
198
+ * @function buildDatasourceUploadArgs
199
199
  * @param {string} datasourcePath - Path to datasource file
200
200
  * @returns {string[]} Command arguments
201
201
  */
202
- function buildDatasourceDeployArgs(datasourcePath) {
202
+ function buildDatasourceUploadArgs(datasourcePath) {
203
203
  return [
204
204
  'bin/aifabrix.js',
205
205
  'datasource',
206
- 'deploy',
206
+ 'upload',
207
207
  'test-app',
208
208
  datasourcePath
209
209
  ];
210
210
  }
211
211
 
212
212
  /**
213
- * Gets expected error patterns for datasource deploy
214
- * @function getDatasourceDeployErrorPatterns
213
+ * Gets expected error patterns for datasource upload
214
+ * @function getDatasourceUploadErrorPatterns
215
215
  * @returns {string[]} Expected error patterns
216
216
  */
217
- function getDatasourceDeployErrorPatterns() {
217
+ function getDatasourceUploadErrorPatterns() {
218
218
  return [
219
219
  'failed to connect',
220
220
  'connection refused',
@@ -224,24 +224,25 @@ function getDatasourceDeployErrorPatterns() {
224
224
  'timeout',
225
225
  'unreachable',
226
226
  'failed to publish',
227
+ 'upload failed',
227
228
  'deployment failed'
228
229
  ];
229
230
  }
230
231
 
231
232
  /**
232
- * Test datasource deploy command with invalid dataplane
233
+ * Test datasource upload command with invalid dataplane
233
234
  * @async
234
- * @function testDatasourceDeploy
235
+ * @function testDatasourceUpload
235
236
  * @returns {Promise<Object>} Test result
236
237
  */
237
- async function testDatasourceDeploy() {
238
- logInfo('\n🚀 Testing: datasource deploy command');
238
+ async function testDatasourceUpload() {
239
+ logInfo('\n🚀 Testing: datasource upload command');
239
240
 
240
241
  const datasourcePath = path.join(process.cwd(), 'integration', 'test-datasource.json');
241
242
 
242
243
  try {
243
244
  await createTestDatasource(datasourcePath);
244
- const args = buildDatasourceDeployArgs(datasourcePath);
245
+ const args = buildDatasourceUploadArgs(datasourcePath);
245
246
  const result = await runCommand('node', args);
246
247
  const output = `${result.stdout}\n${result.stderr}`;
247
248
 
@@ -252,18 +253,18 @@ async function testDatasourceDeploy() {
252
253
  // Ignore cleanup errors
253
254
  }
254
255
 
255
- const expectedPatterns = getDatasourceDeployErrorPatterns();
256
+ const expectedPatterns = getDatasourceUploadErrorPatterns();
256
257
  const isValid = !result.success && validateError(output, expectedPatterns);
257
258
 
258
259
  return {
259
- name: 'datasource deploy',
260
+ name: 'datasource upload',
260
261
  success: isValid,
261
262
  output,
262
263
  expectedPatterns
263
264
  };
264
265
  } catch (error) {
265
266
  return {
266
- name: 'datasource deploy',
267
+ name: 'datasource upload',
267
268
  success: false,
268
269
  output: error.message,
269
270
  error: error.message
@@ -385,7 +386,7 @@ module.exports = {
385
386
  testWizard,
386
387
  testDownload,
387
388
  testDelete,
388
- testDatasourceDeploy,
389
+ testDatasourceUpload,
389
390
  testIntegration,
390
391
  testDataplaneDiscovery
391
392
  };
@@ -24,7 +24,7 @@ const {
24
24
  testWizard,
25
25
  testDownload,
26
26
  testDelete,
27
- testDatasourceDeploy,
27
+ testDatasourceUpload,
28
28
  testIntegration,
29
29
  testDataplaneDiscovery
30
30
  } = require('./test-dataplane-down-tests');
@@ -139,7 +139,7 @@ async function runTests() {
139
139
  testWizard,
140
140
  testDownload,
141
141
  testDelete,
142
- testDatasourceDeploy,
142
+ testDatasourceUpload,
143
143
  testIntegration,
144
144
  testDataplaneDiscovery
145
145
  ];
@@ -10,6 +10,8 @@
10
10
  const baseProject = require('./jest.config').projects[0];
11
11
 
12
12
  module.exports = {
13
+ // Top-level so Jest actually applies it (project-level testTimeout is ignored in some Jest versions)
14
+ testTimeout: 60000,
13
15
  projects: [
14
16
  {
15
17
  ...baseProject,
@@ -22,7 +24,6 @@ module.exports = {
22
24
  '\\\\node_modules\\\\'
23
25
  ],
24
26
  setupFilesAfterEnv: ['<rootDir>/tests/manual/setup.js'],
25
- testTimeout: 60000,
26
27
  maxWorkers: 1
27
28
  }
28
29
  ]
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @fileoverview External test API - dataplane external endpoints (test, test-e2e)
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const { ApiClient } = require('./index');
8
+
9
+ /**
10
+ * Run E2E test for one datasource (config, credential, sync, data, CIP) via dataplane external API.
11
+ * Requires Bearer token or API key; client credentials are not accepted.
12
+ * When asyncRun is true, POST returns 202 with { testRunId, status, startedAt }; caller must poll
13
+ * getE2ETestRun until status is 'completed' or 'failed'. When asyncRun is false, POST returns 200
14
+ * with sync body { steps, success, error?, ... }.
15
+ *
16
+ * @requiresPermission {Dataplane} external-data-source:read
17
+ * @async
18
+ * @function testDatasourceE2E
19
+ * @param {string} dataplaneUrl - Dataplane base URL
20
+ * @param {string} sourceIdOrKey - Source ID or datasource key (e.g. hubspot-test-v4-contacts)
21
+ * @param {Object} authConfig - Authentication configuration (must have token or apiKey; client creds rejected)
22
+ * @param {Object} [body] - Optional request body (e.g. includeDebug, testCrud, recordId, cleanup, primaryKeyValue)
23
+ * @param {Object} [options] - Optional options
24
+ * @param {boolean} [options.asyncRun] - If true, request async run (query param asyncRun=true); response may be 202 with testRunId
25
+ * @returns {Promise<Object>} Response with success, data (sync: steps/success/error; async start: testRunId/status/startedAt), status
26
+ * @throws {Error} If auth lacks Bearer/API_KEY or if test fails
27
+ */
28
+ async function testDatasourceE2E(dataplaneUrl, sourceIdOrKey, authConfig, body = {}, options = {}) {
29
+ if (!authConfig.token && !authConfig.apiKey) {
30
+ throw new Error(
31
+ 'E2E tests require Bearer token or API key. Run \'aifabrix login\' or configure API key. ' +
32
+ 'Client credentials are not supported for external test endpoints.'
33
+ );
34
+ }
35
+ const client = new ApiClient(dataplaneUrl, authConfig);
36
+ const postOptions = { body };
37
+ if (options.asyncRun === true) {
38
+ postOptions.params = { asyncRun: 'true' };
39
+ }
40
+ return await client.post(`/api/v1/external/${encodeURIComponent(sourceIdOrKey)}/test-e2e`, postOptions);
41
+ }
42
+
43
+ /**
44
+ * Poll E2E test run status. Call after testDatasourceE2E with asyncRun true when response has testRunId.
45
+ * Same auth as E2E (Bearer or API key).
46
+ *
47
+ * @requiresPermission {Dataplane} external-data-source:read
48
+ * @async
49
+ * @function getE2ETestRun
50
+ * @param {string} dataplaneUrl - Dataplane base URL
51
+ * @param {string} sourceIdOrKey - Source ID or datasource key
52
+ * @param {string} testRunId - Test run ID from async start response
53
+ * @param {Object} authConfig - Authentication configuration (must have token or apiKey)
54
+ * @returns {Promise<Object>} Poll response: { status, completedActions?, steps?, success?, error?, durationSeconds?, debug? }
55
+ * @throws {Error} If auth lacks Bearer/API_KEY, or if run not found/expired (404)
56
+ */
57
+ async function getE2ETestRun(dataplaneUrl, sourceIdOrKey, testRunId, authConfig) {
58
+ if (!authConfig.token && !authConfig.apiKey) {
59
+ throw new Error(
60
+ 'E2E poll requires Bearer token or API key. Run \'aifabrix login\' or configure API key.'
61
+ );
62
+ }
63
+ if (!testRunId || typeof testRunId !== 'string') {
64
+ throw new Error('testRunId is required for E2E poll');
65
+ }
66
+ const client = new ApiClient(dataplaneUrl, authConfig);
67
+ const response = await client.get(
68
+ `/api/v1/external/${encodeURIComponent(sourceIdOrKey)}/test-e2e/${encodeURIComponent(testRunId)}`
69
+ );
70
+ if (!response.success) {
71
+ if (response.status === 404) {
72
+ throw new Error(
73
+ `E2E test run not found or expired (run ID: ${testRunId}). The run may have been purged or the ID is invalid.`
74
+ );
75
+ }
76
+ throw new Error(response.formattedError || response.error || 'E2E poll failed');
77
+ }
78
+ return response.data || response;
79
+ }
80
+
81
+ /**
82
+ * Run config test for one datasource via dataplane external API.
83
+ * Requires Bearer token or API key; client credentials are not accepted.
84
+ *
85
+ * @requiresPermission {Dataplane} external-data-source:read
86
+ * @async
87
+ * @function testDatasourceConfig
88
+ * @param {string} dataplaneUrl - Dataplane base URL
89
+ * @param {string} sourceIdOrKey - Source ID or datasource key
90
+ * @param {Object} authConfig - Authentication configuration
91
+ * @param {Object} [body] - Optional request body
92
+ * @returns {Promise<Object>} Config test response
93
+ * @throws {Error} If auth lacks Bearer/API_KEY or if test fails
94
+ */
95
+ async function testDatasourceConfig(dataplaneUrl, sourceIdOrKey, authConfig, body = {}) {
96
+ if (!authConfig.token && !authConfig.apiKey) {
97
+ throw new Error(
98
+ 'External config tests require Bearer token or API key. Run \'aifabrix login\' or configure API key.'
99
+ );
100
+ }
101
+ const client = new ApiClient(dataplaneUrl, authConfig);
102
+ return await client.post(`/api/v1/external/${encodeURIComponent(sourceIdOrKey)}/test`, {
103
+ body
104
+ });
105
+ }
106
+
107
+ module.exports = {
108
+ testDatasourceE2E,
109
+ getE2ETestRun,
110
+ testDatasourceConfig
111
+ };
package/lib/api/index.js CHANGED
@@ -13,13 +13,12 @@ const { makeApiCall, authenticatedApiCall } = require('../utils/api');
13
13
  */
14
14
  class ApiClient {
15
15
  /**
16
- * Create an API client instance
16
+ * Create an API client instance.
17
+ * App endpoints receive token-only auth (Bearer). x-client-id/x-client-secret are not sent to app endpoints.
17
18
  * @param {string} baseUrl - Base URL for the API (controller URL)
18
19
  * @param {Object} [authConfig] - Authentication configuration
19
- * @param {string} [authConfig.type] - Auth type ('bearer' | 'client-credentials' | 'client-token')
20
+ * @param {string} [authConfig.type] - Auth type ('bearer' | 'client-token')
20
21
  * @param {string} [authConfig.token] - Bearer token
21
- * @param {string} [authConfig.clientId] - Client ID
22
- * @param {string} [authConfig.clientSecret] - Client secret
23
22
  */
24
23
  constructor(baseUrl, authConfig = {}) {
25
24
  if (baseUrl === null || baseUrl === undefined || typeof baseUrl !== 'string') {
@@ -48,26 +47,23 @@ class ApiClient {
48
47
  * Build request headers with authentication
49
48
  * @private
50
49
  * @param {Object} [additionalHeaders] - Additional headers to include
50
+ * @param {Object} [opts] - Options
51
+ * @param {boolean} [opts.skipContentType] - If true, do not set Content-Type (e.g. for FormData when boundary is set by fetch)
51
52
  * @returns {Object} Request headers
52
53
  */
53
- _buildHeaders(additionalHeaders = {}) {
54
- const headers = {
55
- 'Content-Type': 'application/json',
56
- ...additionalHeaders
57
- };
54
+ _buildHeaders(additionalHeaders = {}, opts = {}) {
55
+ const headers = { ...additionalHeaders };
56
+ if (!opts.skipContentType) {
57
+ headers['Content-Type'] = 'application/json';
58
+ }
58
59
 
59
- // Add authentication headers based on authConfig
60
- if (this.authConfig.type === 'bearer' || this.authConfig.type === 'client-token') {
61
- if (this.authConfig.token) {
60
+ // User token (bearer) Authorization: Bearer; application token (client-token) → x-client-token
61
+ if (this.authConfig.token) {
62
+ if (this.authConfig.type === 'client-token') {
63
+ headers['x-client-token'] = this.authConfig.token;
64
+ } else {
62
65
  headers['Authorization'] = `Bearer ${this.authConfig.token}`;
63
66
  }
64
- } else if (this.authConfig.type === 'client-credentials') {
65
- if (this.authConfig.clientId) {
66
- headers['x-client-id'] = this.authConfig.clientId;
67
- }
68
- if (this.authConfig.clientSecret) {
69
- headers['x-client-secret'] = this.authConfig.clientSecret;
70
- }
71
67
  }
72
68
 
73
69
  return headers;
@@ -153,6 +149,33 @@ class ApiClient {
153
149
  return await makeApiCall(url, requestOptions);
154
150
  }
155
151
 
152
+ /**
153
+ * POST multipart/form-data (e.g. file upload). Uses same auth as other methods; does not set Content-Type so fetch sets boundary.
154
+ * @async
155
+ * @param {string} endpoint - API endpoint path
156
+ * @param {FormData} formData - FormData body
157
+ * @param {Object} [options] - Request options
158
+ * @param {Object} [options.headers] - Additional headers
159
+ * @returns {Promise<Object>} API response
160
+ */
161
+ async postFormData(endpoint, formData, options = {}) {
162
+ const url = this._buildUrl(endpoint);
163
+ const headers = this._buildHeaders(options.headers || {}, { skipContentType: true });
164
+
165
+ const requestOptions = {
166
+ method: 'POST',
167
+ headers,
168
+ body: formData
169
+ };
170
+
171
+ const hasToken = this.authConfig.type === 'bearer' || this.authConfig.type === 'client-token';
172
+ if (hasToken && this.authConfig.token) {
173
+ return await authenticatedApiCall(url, requestOptions, this.authConfig);
174
+ }
175
+
176
+ return await makeApiCall(url, requestOptions);
177
+ }
178
+
156
179
  /**
157
180
  * Make a PATCH request
158
181
  * @async
@@ -14,7 +14,7 @@ const { ApiClient } = require('./index');
14
14
  * @function validatePipeline
15
15
  * @param {string} controllerUrl - Controller base URL
16
16
  * @param {string} envKey - Environment key
17
- * @param {Object} authConfig - Authentication configuration (supports client credentials)
17
+ * @param {Object} authConfig - Authentication configuration (Bearer token only for app endpoints)
18
18
  * @param {Object} validationData - Validation data
19
19
  * @param {string} validationData.clientId - Client ID for application authentication
20
20
  * @param {string} validationData.repositoryUrl - Repository URL for validation
@@ -37,7 +37,7 @@ async function validatePipeline(controllerUrl, envKey, authConfig, validationDat
37
37
  * @function deployPipeline
38
38
  * @param {string} controllerUrl - Controller base URL
39
39
  * @param {string} envKey - Environment key
40
- * @param {Object} authConfig - Authentication configuration (supports client credentials)
40
+ * @param {Object} authConfig - Authentication configuration (Bearer token only for app endpoints)
41
41
  * @param {Object} deployData - Deployment data
42
42
  * @param {string} deployData.validateToken - One-time deployment token from /validate endpoint
43
43
  * @param {string} deployData.imageTag - Container image tag to deploy
@@ -60,7 +60,7 @@ async function deployPipeline(controllerUrl, envKey, authConfig, deployData) {
60
60
  * @param {string} controllerUrl - Controller base URL
61
61
  * @param {string} envKey - Environment key
62
62
  * @param {string} deploymentId - Deployment ID
63
- * @param {Object} authConfig - Authentication configuration (supports client credentials)
63
+ * @param {Object} authConfig - Authentication configuration (Bearer token only for app endpoints)
64
64
  * @returns {Promise<Object>} Minimal deployment status response
65
65
  * @throws {Error} If request fails
66
66
  */
@@ -85,31 +85,9 @@ async function getPipelineHealth(controllerUrl, envKey) {
85
85
  return await client.get(`/api/v1/pipeline/${envKey}/health`);
86
86
  }
87
87
 
88
- /**
89
- * Publish one external system via dataplane pipeline endpoint
90
- * POST /api/v1/pipeline/publish (Dataplane OpenAPI operationId: publishExternalSystemViaPipeline)
91
- * Request body: external system JSON (external-system.schema.json). Optional field in body:
92
- * generateMcpContract (boolean, default true). Optional: generateOpenApiContract (boolean).
93
- * Do not use query parameters for MCP; use the field in the body only.
94
- * @requiresPermission {Dataplane} external-system:publish. Auth: OAuth2 (Bearer) or API_KEY only; client id/secret are not accepted.
95
- * @async
96
- * @function publishSystemViaPipeline
97
- * @param {string} dataplaneUrl - Dataplane base URL
98
- * @param {Object} authConfig - Authentication configuration (must include token for Bearer; client id/secret rejected)
99
- * @param {Object} systemConfig - External system configuration (conforms to external-system.schema.json)
100
- * @returns {Promise<Object>} Published external system response
101
- * @throws {Error} If publish fails
102
- */
103
- async function publishSystemViaPipeline(dataplaneUrl, authConfig, systemConfig) {
104
- const client = new ApiClient(dataplaneUrl, authConfig);
105
- return await client.post('/api/v1/pipeline/publish', {
106
- body: systemConfig
107
- });
108
- }
109
-
110
88
  /**
111
89
  * Publish datasource via dataplane pipeline endpoint
112
- * POST /api/v1/pipeline/{systemKey}/publish (Dataplane OpenAPI operationId: publishExternalDataSourceViaPipeline)
90
+ * POST /api/v1/pipeline/{systemKey}/upload (Dataplane: renamed from /publish)
113
91
  * No generateMcpContract for this endpoint; dataplane always uses default (MCP generated).
114
92
  * @requiresPermission {Dataplane} external-system:publish. Auth: OAuth2 (Bearer) or API_KEY only; client id/secret are not accepted.
115
93
  * @async
@@ -123,15 +101,62 @@ async function publishSystemViaPipeline(dataplaneUrl, authConfig, systemConfig)
123
101
  */
124
102
  async function publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasourceConfig) {
125
103
  const client = new ApiClient(dataplaneUrl, authConfig);
126
- return await client.post(`/api/v1/pipeline/${systemKey}/publish`, {
104
+ return await client.post(`/api/v1/pipeline/${systemKey}/upload`, {
127
105
  body: datasourceConfig
128
106
  });
129
107
  }
130
108
 
109
+ /**
110
+ * Validate pipeline config against Dataplane (dry-run; no publish).
111
+ * POST /api/v1/pipeline/validate
112
+ * @requiresPermission {Dataplane} external-system:read or external-system:publish
113
+ * @async
114
+ * @function validatePipelineConfig
115
+ * @param {string} dataplaneUrl - Dataplane base URL
116
+ * @param {Object} authConfig - Authentication configuration
117
+ * @param {Object} params - Request params
118
+ * @param {Object} params.config - Full config: { version, application, dataSources }
119
+ * @returns {Promise<Object>} { isValid, errors, warnings }
120
+ * @throws {Error} If request fails
121
+ */
122
+ async function validatePipelineConfig(dataplaneUrl, authConfig, { config }) {
123
+ const client = new ApiClient(dataplaneUrl, authConfig);
124
+ return await client.post('/api/v1/pipeline/validate', {
125
+ body: { config }
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Test external system (all datasources) via dataplane pipeline endpoint
131
+ * POST /api/v1/pipeline/{systemKey}/test
132
+ * @requiresPermission {Dataplane} external-system:publish. Auth: Bearer or x-client-token only.
133
+ * @async
134
+ * @function testSystemViaPipeline
135
+ * @param {string} dataplaneUrl - Dataplane base URL
136
+ * @param {string} systemKey - System key
137
+ * @param {Object} authConfig - Authentication configuration (Bearer token)
138
+ * @param {Object} testData - Test data
139
+ * @param {Object} [testData.payloadTemplate] - Optional payload template
140
+ * @param {boolean} [testData.includeDebug] - Include debug output in response
141
+ * @param {Object} [options] - Request options
142
+ * @param {number} [options.timeout] - Request timeout in milliseconds
143
+ * @returns {Promise<Object>} Test response
144
+ * @throws {Error} If test fails
145
+ */
146
+ async function testSystemViaPipeline(dataplaneUrl, systemKey, authConfig, testData = {}, options = {}) {
147
+ const client = new ApiClient(dataplaneUrl, authConfig);
148
+ const requestOptions = { body: testData };
149
+ if (options.timeout) {
150
+ requestOptions.timeout = options.timeout;
151
+ }
152
+ return await client.post(`/api/v1/pipeline/${systemKey}/test`, requestOptions);
153
+ }
154
+
131
155
  /**
132
156
  * Test datasource via dataplane pipeline endpoint
133
157
  * POST /api/v1/pipeline/{systemKey}/{datasourceKey}/test (Dataplane OpenAPI operationId: testExternalDataSourceViaPipeline)
134
- * @requiresPermission {Dataplane} external-system:publish or external-data-source:read. Auth: OAuth2 (Bearer) or API_KEY only; client id/secret are not accepted.
158
+ * Supports client credentials for CI/CD.
159
+ * @requiresPermission {Dataplane} external-system:publish or external-data-source:read. Auth: Bearer or x-client-token only.
135
160
  * @async
136
161
  * @function testDatasourceViaPipeline
137
162
  * @param {Object} params - Function parameters
@@ -159,116 +184,37 @@ async function testDatasourceViaPipeline({ dataplaneUrl, systemKey, datasourceKe
159
184
  }
160
185
 
161
186
  /**
162
- * Deploy external system via dataplane pipeline endpoint
163
- * POST /api/v1/pipeline/deploy
164
- * @requiresPermission {Dataplane} external-system:publish. Auth: OAuth2 (Bearer) or API_KEY only; client id/secret are not accepted.
165
- * @async
166
- * @function deployExternalSystemViaPipeline
167
- * @param {string} dataplaneUrl - Dataplane base URL
168
- * @param {Object} authConfig - Authentication configuration
169
- * @param {Object} systemConfig - External system configuration to deploy
170
- * @returns {Promise<Object>} Deployment response
171
- * @throws {Error} If deployment fails
172
- */
173
- async function deployExternalSystemViaPipeline(dataplaneUrl, authConfig, systemConfig) {
174
- const client = new ApiClient(dataplaneUrl, authConfig);
175
- return await client.post('/api/v1/pipeline/deploy', {
176
- body: systemConfig
177
- });
178
- }
179
-
180
- /**
181
- * Deploy datasource via dataplane pipeline endpoint
182
- * POST /api/v1/pipeline/{systemKey}/deploy
183
- * @requiresPermission {Dataplane} external-system:publish. Auth: OAuth2 (Bearer) or API_KEY only; client id/secret are not accepted.
184
- * @async
185
- * @function deployDatasourceViaPipeline
186
- * @param {string} dataplaneUrl - Dataplane base URL
187
- * @param {string} systemKey - System key
188
- * @param {Object} authConfig - Authentication configuration
189
- * @param {Object} datasourceConfig - Datasource configuration to deploy
190
- * @returns {Promise<Object>} Deployment response
191
- * @throws {Error} If deployment fails
192
- */
193
- async function deployDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasourceConfig) {
194
- const client = new ApiClient(dataplaneUrl, authConfig);
195
- return await client.post(`/api/v1/pipeline/${systemKey}/deploy`, {
196
- body: datasourceConfig
197
- });
198
- }
199
-
200
- /**
201
- * Upload application configuration via dataplane pipeline endpoint
202
- * POST /api/v1/pipeline/upload (Dataplane OpenAPI operationId: uploadApplication)
203
- * Body: { version, application, dataSources }. Include application.generateMcpContract
204
- * and/or application.generateOpenApiContract to control contract generation when
205
- * publishing this upload (publish reads from stored config; no query param on publish).
187
+ * Upload application configuration via dataplane pipeline endpoint (single call: upload → validate → publish → controller register).
188
+ * POST /api/v1/pipeline/upload
189
+ * Body: { version, application, dataSources, status }. status "draft" (default) or "published".
190
+ * Include application.generateMcpContract and/or application.generateOpenApiContract to control contract generation.
206
191
  * @requiresPermission {Dataplane} external-system:publish. Auth: OAuth2 (Bearer) or API_KEY only; client id/secret are not accepted.
207
192
  * @async
208
193
  * @function uploadApplicationViaPipeline
209
194
  * @param {string} dataplaneUrl - Dataplane base URL
210
195
  * @param {Object} authConfig - Authentication configuration (must include token for Bearer; client id/secret rejected)
211
- * @param {Object} applicationSchema - { version, application, dataSources }; application may include generateMcpContract, generateOpenApiContract
212
- * @returns {Promise<Object>} Upload response with uploadId
196
+ * @param {Object} payload - { version, application, dataSources }; optional status (default "draft")
197
+ * @param {string} [payload.status="draft"] - "draft" or "published"; Builder uses "draft"
198
+ * @returns {Promise<Object>} Publication result (system, datasources, warnings); no uploadId
213
199
  * @throws {Error} If upload fails
214
200
  */
215
- async function uploadApplicationViaPipeline(dataplaneUrl, authConfig, applicationSchema) {
201
+ async function uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload) {
202
+ const body = { ...payload, status: payload.status ?? 'draft' };
216
203
  const client = new ApiClient(dataplaneUrl, authConfig);
217
204
  return await client.post('/api/v1/pipeline/upload', {
218
- body: applicationSchema
205
+ body
219
206
  });
220
207
  }
221
208
 
222
- /**
223
- * Validate upload via dataplane pipeline endpoint
224
- * POST /api/v1/pipeline/upload/{uploadId}/validate (Dataplane OpenAPI operationId: validateApplication)
225
- * @requiresPermission {Dataplane} external-system:publish. Auth: OAuth2 (Bearer) or API_KEY only; client id/secret are not accepted.
226
- * @async
227
- * @function validateUploadViaPipeline
228
- * @param {string} dataplaneUrl - Dataplane base URL
229
- * @param {string} uploadId - Upload ID
230
- * @param {Object} authConfig - Authentication configuration
231
- * @returns {Promise<Object>} Validation response with changes and summary
232
- * @throws {Error} If validation fails
233
- */
234
- async function validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig) {
235
- const client = new ApiClient(dataplaneUrl, authConfig);
236
- return await client.post(`/api/v1/pipeline/upload/${uploadId}/validate`);
237
- }
238
-
239
- /**
240
- * Publish upload via dataplane pipeline endpoint
241
- * POST /api/v1/pipeline/upload/{uploadId}/publish (Dataplane OpenAPI operationId: publishApplication)
242
- * No body or query parameters. MCP/OpenAPI generation is taken from the application config
243
- * that was uploaded (application.generateMcpContract, application.generateOpenApiContract).
244
- * To control MCP/OpenAPI, include those fields in the application object when calling
245
- * uploadApplicationViaPipeline.
246
- * @requiresPermission {Dataplane} external-system:publish. Auth: OAuth2 (Bearer) or API_KEY only; client id/secret are not accepted.
247
- * @async
248
- * @function publishUploadViaPipeline
249
- * @param {string} dataplaneUrl - Dataplane base URL
250
- * @param {string} uploadId - Upload ID
251
- * @param {Object} authConfig - Authentication configuration (must include token for Bearer; client id/secret rejected)
252
- * @returns {Promise<Object>} Publish response
253
- * @throws {Error} If publish fails
254
- */
255
- async function publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig) {
256
- const client = new ApiClient(dataplaneUrl, authConfig);
257
- return await client.post(`/api/v1/pipeline/upload/${uploadId}/publish`);
258
- }
259
-
260
209
  module.exports = {
261
210
  validatePipeline,
262
211
  deployPipeline,
263
212
  getPipelineDeployment,
264
213
  getPipelineHealth,
265
- publishSystemViaPipeline,
266
214
  publishDatasourceViaPipeline,
215
+ validatePipelineConfig,
216
+ testSystemViaPipeline,
267
217
  testDatasourceViaPipeline,
268
- deployExternalSystemViaPipeline,
269
- deployDatasourceViaPipeline,
270
- uploadApplicationViaPipeline,
271
- validateUploadViaPipeline,
272
- publishUploadViaPipeline
218
+ uploadApplicationViaPipeline
273
219
  };
274
220