@aifabrix/builder 2.40.2 → 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 (198) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +7 -5
  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/integration/hubspot/test.js +1 -1
  16. package/jest.config.manual.js +2 -1
  17. package/lib/api/credential.api.js +40 -0
  18. package/lib/api/dev.api.js +423 -0
  19. package/lib/api/external-test.api.js +111 -0
  20. package/lib/api/index.js +42 -19
  21. package/lib/api/pipeline.api.js +66 -120
  22. package/lib/api/types/credential.types.js +23 -0
  23. package/lib/api/types/dev.types.js +140 -0
  24. package/lib/api/types/pipeline.types.js +37 -0
  25. package/lib/api/wizard-platform.api.js +61 -0
  26. package/lib/api/wizard.api.js +34 -1
  27. package/lib/app/config.js +44 -11
  28. package/lib/app/down.js +2 -1
  29. package/lib/app/index.js +12 -1
  30. package/lib/app/prompts.js +44 -29
  31. package/lib/app/push.js +36 -12
  32. package/lib/app/readme.js +9 -6
  33. package/lib/app/run-env-compose.js +264 -0
  34. package/lib/app/run-helpers.js +121 -118
  35. package/lib/app/run.js +148 -28
  36. package/lib/app/show-display.js +1 -1
  37. package/lib/app/show.js +5 -2
  38. package/lib/build/index.js +11 -3
  39. package/lib/cli/setup-app.js +172 -15
  40. package/lib/cli/setup-credential-deployment.js +31 -6
  41. package/lib/cli/setup-dev.js +206 -16
  42. package/lib/cli/setup-environment.js +16 -6
  43. package/lib/cli/setup-external-system.js +89 -24
  44. package/lib/cli/setup-infra.js +82 -15
  45. package/lib/cli/setup-secrets.js +52 -5
  46. package/lib/cli/setup-utility.js +129 -24
  47. package/lib/commands/app-install.js +172 -0
  48. package/lib/commands/app-shell.js +75 -0
  49. package/lib/commands/app-test.js +282 -0
  50. package/lib/commands/app.js +1 -1
  51. package/lib/commands/credential-env.js +162 -0
  52. package/lib/commands/credential-list.js +17 -22
  53. package/lib/commands/credential-push.js +96 -0
  54. package/lib/commands/datasource.js +77 -6
  55. package/lib/commands/dev-cli-handlers.js +141 -0
  56. package/lib/commands/dev-down.js +114 -0
  57. package/lib/commands/dev-init.js +347 -0
  58. package/lib/commands/repair-auth-config.js +99 -0
  59. package/lib/commands/repair-datasource-keys.js +208 -0
  60. package/lib/commands/repair-datasource.js +235 -0
  61. package/lib/commands/repair-env-template.js +348 -0
  62. package/lib/commands/repair-internal.js +85 -0
  63. package/lib/commands/repair-rbac.js +158 -0
  64. package/lib/commands/repair.js +507 -0
  65. package/lib/commands/secrets-list.js +118 -0
  66. package/lib/commands/secrets-remove.js +97 -0
  67. package/lib/commands/secrets-set.js +30 -17
  68. package/lib/commands/secrets-validate.js +50 -0
  69. package/lib/commands/test-e2e-external.js +165 -0
  70. package/lib/commands/up-dataplane.js +2 -2
  71. package/lib/commands/up-miso.js +0 -25
  72. package/lib/commands/upload.js +96 -40
  73. package/lib/commands/wizard-core-helpers.js +226 -4
  74. package/lib/commands/wizard-core.js +67 -29
  75. package/lib/commands/wizard-dataplane.js +1 -1
  76. package/lib/commands/wizard-entity-selection.js +43 -0
  77. package/lib/commands/wizard-headless.js +44 -5
  78. package/lib/commands/wizard-helpers.js +7 -3
  79. package/lib/commands/wizard.js +86 -64
  80. package/lib/core/admin-secrets.js +96 -0
  81. package/lib/core/config.js +7 -1
  82. package/lib/core/secrets-ensure.js +378 -0
  83. package/lib/core/secrets-env-write.js +157 -0
  84. package/lib/core/secrets.js +176 -89
  85. package/lib/datasource/deploy.js +12 -3
  86. package/lib/datasource/field-reference-validator.js +91 -0
  87. package/lib/datasource/test-e2e.js +219 -0
  88. package/lib/datasource/test-integration.js +154 -0
  89. package/lib/datasource/validate.js +21 -3
  90. package/lib/deployment/deployer.js +7 -5
  91. package/lib/deployment/environment-config.js +137 -0
  92. package/lib/deployment/environment.js +21 -98
  93. package/lib/deployment/push.js +32 -2
  94. package/lib/external-system/download.js +188 -203
  95. package/lib/external-system/generator.js +204 -56
  96. package/lib/external-system/test-auth.js +7 -3
  97. package/lib/external-system/test-execution.js +2 -1
  98. package/lib/external-system/test-system-level.js +73 -0
  99. package/lib/external-system/test.js +56 -19
  100. package/lib/generator/external-controller-manifest.js +29 -2
  101. package/lib/generator/external-schema-utils.js +1 -1
  102. package/lib/generator/external.js +10 -3
  103. package/lib/generator/index.js +177 -25
  104. package/lib/generator/split-readme.js +1 -0
  105. package/lib/generator/split-variables.js +7 -1
  106. package/lib/generator/split.js +194 -54
  107. package/lib/generator/wizard-prompts-secondary.js +294 -0
  108. package/lib/generator/wizard-prompts.js +105 -106
  109. package/lib/generator/wizard-readme.js +88 -0
  110. package/lib/generator/wizard.js +155 -158
  111. package/lib/infrastructure/compose.js +11 -1
  112. package/lib/infrastructure/helpers.js +103 -20
  113. package/lib/infrastructure/index.js +98 -12
  114. package/lib/infrastructure/services.js +88 -22
  115. package/lib/schema/application-schema.json +32 -8
  116. package/lib/schema/external-datasource.schema.json +49 -26
  117. package/lib/schema/external-system.schema.json +509 -411
  118. package/lib/schema/wizard-config.schema.json +16 -0
  119. package/lib/utils/api.js +41 -13
  120. package/lib/utils/app-register-auth.js +25 -3
  121. package/lib/utils/auth-headers.js +8 -7
  122. package/lib/utils/cli-utils.js +20 -0
  123. package/lib/utils/compose-generator.js +77 -76
  124. package/lib/utils/compose-handlebars-helpers.js +54 -0
  125. package/lib/utils/compose-vector-helper.js +18 -0
  126. package/lib/utils/config-format-preference.js +51 -0
  127. package/lib/utils/config-format.js +36 -0
  128. package/lib/utils/config-paths.js +127 -2
  129. package/lib/utils/configuration-env-resolver.js +179 -0
  130. package/lib/utils/credential-display.js +83 -0
  131. package/lib/utils/credential-secrets-env.js +357 -0
  132. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  133. package/lib/utils/deployment-validation-helpers.js +4 -4
  134. package/lib/utils/dev-ca-install.js +139 -0
  135. package/lib/utils/dev-cert-helper.js +122 -0
  136. package/lib/utils/device-code-helpers.js +224 -0
  137. package/lib/utils/device-code.js +37 -336
  138. package/lib/utils/docker-build.js +40 -8
  139. package/lib/utils/env-copy.js +103 -13
  140. package/lib/utils/env-map.js +35 -5
  141. package/lib/utils/env-template.js +6 -5
  142. package/lib/utils/error-formatters/http-status-errors.js +20 -2
  143. package/lib/utils/error-formatters/permission-errors.js +0 -1
  144. package/lib/utils/error-formatters/validation-errors.js +0 -1
  145. package/lib/utils/external-readme.js +56 -29
  146. package/lib/utils/external-system-display.js +59 -1
  147. package/lib/utils/external-system-test-helpers.js +21 -8
  148. package/lib/utils/external-system-validators.js +3 -0
  149. package/lib/utils/file-upload.js +20 -50
  150. package/lib/utils/help-builder.js +16 -2
  151. package/lib/utils/infra-status.js +80 -45
  152. package/lib/utils/local-secrets.js +7 -52
  153. package/lib/utils/mutagen-install.js +195 -0
  154. package/lib/utils/mutagen.js +146 -0
  155. package/lib/utils/paths.js +128 -37
  156. package/lib/utils/port-resolver.js +28 -16
  157. package/lib/utils/remote-dev-auth.js +38 -0
  158. package/lib/utils/remote-docker-env.js +43 -0
  159. package/lib/utils/remote-secrets-loader.js +60 -0
  160. package/lib/utils/secrets-canonical.js +93 -0
  161. package/lib/utils/secrets-generator.js +114 -6
  162. package/lib/utils/secrets-helpers.js +108 -114
  163. package/lib/utils/secrets-path.js +2 -2
  164. package/lib/utils/secrets-utils.js +52 -1
  165. package/lib/utils/secrets-validation.js +84 -0
  166. package/lib/utils/ssh-key-helper.js +116 -0
  167. package/lib/utils/test-log-writer.js +56 -0
  168. package/lib/utils/token-manager-messages.js +90 -0
  169. package/lib/utils/token-manager.js +29 -36
  170. package/lib/utils/variable-transformer.js +3 -3
  171. package/lib/validation/env-template-auth.js +157 -0
  172. package/lib/validation/env-template-kv.js +41 -0
  173. package/lib/validation/external-manifest-validator.js +25 -0
  174. package/lib/validation/external-system-auth-rules.js +86 -0
  175. package/lib/validation/validate-batch.js +149 -0
  176. package/lib/validation/validate-datasource-keys-api.js +33 -0
  177. package/lib/validation/validate-display.js +94 -16
  178. package/lib/validation/validate.js +25 -12
  179. package/lib/validation/validator.js +72 -9
  180. package/lib/validation/wizard-datasource-validation.js +50 -0
  181. package/package.json +8 -3
  182. package/scripts/install-local.js +34 -15
  183. package/templates/README.md +0 -1
  184. package/templates/applications/README.md.hbs +4 -4
  185. package/templates/applications/dataplane/application.yaml +6 -5
  186. package/templates/applications/dataplane/env.template +15 -10
  187. package/templates/applications/dataplane/rbac.yaml +2 -2
  188. package/templates/applications/keycloak/env.template +2 -0
  189. package/templates/applications/miso-controller/application.yaml +1 -0
  190. package/templates/applications/miso-controller/env.template +12 -10
  191. package/templates/external-system/README.md.hbs +65 -25
  192. package/templates/external-system/deploy.js.hbs +4 -2
  193. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  194. package/templates/external-system/external-system.json.hbs +1 -18
  195. package/templates/infra/compose.yaml.hbs +6 -0
  196. package/templates/python/docker-compose.hbs +49 -23
  197. package/templates/typescript/docker-compose.hbs +48 -22
  198. 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
  ];
@@ -281,7 +281,7 @@ async function loadEnvFile(envPath, options) {
281
281
  /**
282
282
  * Load test config (controller, environment, dataplane, openapi file).
283
283
  * Reads integration/hubspot/.env; missing CONTROLLER_URL/ENVIRONMENT fall back to
284
- * the same resolution as the CLI (aifx auth status) so tests use the same controller.
284
+ * the same resolution as the CLI (af auth status) so tests use the same controller.
285
285
  * @async
286
286
  * @function loadTestConfigFromEnv
287
287
  * @returns {Promise<Object>} Context with controllerUrl, environment, dataplaneUrl, openapiFile
@@ -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,40 @@
1
+ /**
2
+ * @fileoverview Credential API functions (Dataplane secret store)
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const { ApiClient } = require('./index');
8
+
9
+ const CREDENTIAL_SECRET_ENDPOINT = '/api/v1/credential/secret';
10
+
11
+ /**
12
+ * Store credential secrets in the dataplane secret store.
13
+ * Values are encrypted at rest by the dataplane; send plain values only (no kv:// as value).
14
+ *
15
+ * POST /api/v1/credential/secret
16
+ * @requiresPermission {Dataplane} credential:create
17
+ * @async
18
+ * @function storeCredentialSecrets
19
+ * @param {string} dataplaneUrl - Dataplane base URL
20
+ * @param {Object} authConfig - Authentication configuration (Bearer token required)
21
+ * @param {Array<{ key: string, value: string }>} items - Secret items (key = kv path, value = plain)
22
+ * @returns {Promise<{ stored?: number, success?: boolean, error?: string }>} Secret store response
23
+ * @throws {Error} If request fails (non-2xx) and caller may handle 403/401 as warning
24
+ */
25
+ async function storeCredentialSecrets(dataplaneUrl, authConfig, items) {
26
+ if (!dataplaneUrl || typeof dataplaneUrl !== 'string') {
27
+ throw new Error('dataplaneUrl is required and must be a string');
28
+ }
29
+ if (!items || !Array.isArray(items) || items.length === 0) {
30
+ return { stored: 0 };
31
+ }
32
+ const client = new ApiClient(dataplaneUrl, authConfig);
33
+ return await client.post(CREDENTIAL_SECRET_ENDPOINT, {
34
+ body: items
35
+ });
36
+ }
37
+
38
+ module.exports = {
39
+ storeCredentialSecrets
40
+ };
@@ -0,0 +1,423 @@
1
+ /**
2
+ * @fileoverview Builder Server (dev) API – issue-cert, settings, users, SSH keys, secrets.
3
+ * First call: issue-cert is public (no client cert). All other routes require client certificate:
4
+ * when clientKeyPem is provided, requests use mTLS (TLS client cert); otherwise X-Client-Cert header only.
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const https = require('https');
10
+ const { makeApiCall } = require('../utils/api');
11
+
12
+ const DEFAULT_TIMEOUT_MS = 15000;
13
+
14
+ /**
15
+ * Encode PEM for use in X-Client-Cert header. HTTP header values must not contain newlines;
16
+ * we send certPem as base64: Buffer.from(pem, 'utf8').toString('base64').
17
+ * Server should decode with Buffer.from(headerVal, 'base64').toString('utf8').
18
+ * @param {string} clientCertPem - PEM-encoded client certificate
19
+ * @returns {string} Base64-encoded PEM for header
20
+ */
21
+ function encodeCertForHeader(clientCertPem) {
22
+ if (!clientCertPem || typeof clientCertPem !== 'string') return '';
23
+ return Buffer.from(clientCertPem, 'utf8').toString('base64');
24
+ }
25
+
26
+ /**
27
+ * Normalize base URL (no trailing slash)
28
+ * @param {string} serverUrl - Base URL of Builder Server
29
+ * @returns {string} Normalized URL
30
+ */
31
+ function normalizeBaseUrl(serverUrl) {
32
+ if (!serverUrl || typeof serverUrl !== 'string') {
33
+ throw new Error('remote-server URL is required and must be a string');
34
+ }
35
+ return serverUrl.trim().replace(/\/+$/, '');
36
+ }
37
+
38
+ /**
39
+ * Build full URL for an endpoint path
40
+ * @param {string} baseUrl - Normalized base URL
41
+ * @param {string} path - Path (e.g. /api/dev/issue-cert)
42
+ * @returns {string} Full URL
43
+ */
44
+ function buildUrl(baseUrl, path) {
45
+ const p = path.startsWith('/') ? path : `/${path}`;
46
+ return `${baseUrl}${p}`;
47
+ }
48
+
49
+ /**
50
+ * Make request to Builder Server. Throws on !success with message from response or error.
51
+ * @param {string} url - Full URL
52
+ * @param {Object} options - Fetch options (method, headers, body)
53
+ * @returns {Promise<Object>} result.data when success
54
+ */
55
+ async function request(url, options = {}) {
56
+ const fetchOptions = {
57
+ method: options.method || 'GET',
58
+ headers: { 'Content-Type': 'application/json', ...options.headers },
59
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
60
+ };
61
+ if (options.body !== undefined) {
62
+ fetchOptions.body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body);
63
+ }
64
+ const result = await makeApiCall(url, fetchOptions);
65
+ if (!result.success) {
66
+ const msg = result.formattedError || result.error || result.message || `Request failed (${result.status})`;
67
+ const err = new Error(msg);
68
+ err.status = result.status;
69
+ err.errorData = result.errorData;
70
+ throw err;
71
+ }
72
+ return result.data;
73
+ }
74
+
75
+ /**
76
+ * Build request options and TLS agent for mTLS request.
77
+ * @param {string} url - Full URL
78
+ * @param {Object} options - { method, headers, body }
79
+ * @param {string} certPem - PEM client certificate
80
+ * @param {string} keyPem - PEM client key
81
+ * @returns {{ urlObj: URL, method: string, headers: Object, body: string|undefined, agent: https.Agent }}
82
+ */
83
+ function buildMtlsRequestOptions(url, options, certPem, keyPem) {
84
+ const urlObj = new URL(url);
85
+ const method = (options.method || 'GET').toUpperCase();
86
+ const headers = { 'Content-Type': 'application/json', ...options.headers };
87
+ let body = options.body;
88
+ if (body !== undefined && typeof body !== 'string') {
89
+ body = JSON.stringify(body);
90
+ }
91
+ if (body) {
92
+ headers['Content-Length'] = Buffer.byteLength(body, 'utf8');
93
+ }
94
+ const tlsOptions = { cert: certPem, key: keyPem, rejectUnauthorized: true };
95
+ const agent = new https.Agent(tlsOptions);
96
+ return { urlObj, method, headers, body, agent, tlsOptions };
97
+ }
98
+
99
+ /**
100
+ * Handle mTLS response: collect body, parse JSON, resolve or reject.
101
+ * @param {import('http').IncomingMessage} res - HTTP response
102
+ * @param {Function} resolve - Promise resolve
103
+ * @param {Function} reject - Promise reject
104
+ */
105
+ function handleMtlsResponse(res, resolve, reject) {
106
+ const chunks = [];
107
+ res.on('data', (c) => chunks.push(c));
108
+ res.on('end', () => {
109
+ const raw = Buffer.concat(chunks).toString('utf8');
110
+ let data;
111
+ try {
112
+ data = raw ? JSON.parse(raw) : {};
113
+ } catch {
114
+ data = raw;
115
+ }
116
+ if (res.statusCode < 200 || res.statusCode >= 300) {
117
+ const msg = (data && (data.message || data.error)) || res.statusMessage || `Request failed (${res.statusCode})`;
118
+ const err = new Error(msg);
119
+ err.status = res.statusCode;
120
+ err.errorData = data;
121
+ reject(err);
122
+ } else {
123
+ resolve(data);
124
+ }
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Make request with mTLS (TLS client certificate). Uses Node https module so the client cert
130
+ * is presented in the TLS handshake. Also sends X-Client-Cert header for backends that read it.
131
+ * @param {string} url - Full URL (https only)
132
+ * @param {Object} options - { method, headers, body }
133
+ * @param {string} certPem - PEM-encoded client certificate
134
+ * @param {string} keyPem - PEM-encoded client private key
135
+ * @returns {Promise<Object>} response data when success
136
+ */
137
+ function requestWithCertImpl(url, options, certPem, keyPem) {
138
+ return new Promise((resolve, reject) => {
139
+ const urlObj = new URL(url);
140
+ if (urlObj.protocol !== 'https:') {
141
+ reject(new Error('mTLS request requires https URL'));
142
+ return;
143
+ }
144
+ const { method, headers, body, agent, tlsOptions } = buildMtlsRequestOptions(url, options, certPem, keyPem);
145
+ const req = https.request(
146
+ {
147
+ hostname: urlObj.hostname,
148
+ port: urlObj.port || 443,
149
+ path: urlObj.pathname + urlObj.search,
150
+ method,
151
+ headers,
152
+ agent,
153
+ ...tlsOptions
154
+ },
155
+ (res) => handleMtlsResponse(res, resolve, reject)
156
+ );
157
+ req.on('error', reject);
158
+ req.setTimeout(DEFAULT_TIMEOUT_MS, () => {
159
+ req.destroy();
160
+ reject(new Error(`Request timed out after ${DEFAULT_TIMEOUT_MS}ms`));
161
+ });
162
+ if (body) {
163
+ req.write(body, 'utf8');
164
+ }
165
+ req.end();
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Issue developer certificate (public; no client cert). POST /api/dev/issue-cert
171
+ * @requiresPermission {BuilderServer} Public (no auth required)
172
+ * @param {string} serverUrl - Builder Server base URL
173
+ * @param {Object} body - IssueCertDto: developerId, pin, csr
174
+ * @returns {Promise<Object>} IssueCertResponseDto: certificate, validDays, validNotAfter
175
+ */
176
+ async function issueCert(serverUrl, body) {
177
+ const base = normalizeBaseUrl(serverUrl);
178
+ return request(buildUrl(base, '/api/dev/issue-cert'), { method: 'POST', body });
179
+ }
180
+
181
+ /**
182
+ * Get health. GET /health (public)
183
+ * @requiresPermission {BuilderServer} Public (no auth required)
184
+ * @param {string} serverUrl - Builder Server base URL
185
+ * @returns {Promise<Object>} HealthResponseDto: status, checks (dataDir, encryptionKey, ca, users, tokens)
186
+ */
187
+ async function getHealth(serverUrl) {
188
+ const base = normalizeBaseUrl(serverUrl);
189
+ return request(buildUrl(base, '/health'));
190
+ }
191
+
192
+ /**
193
+ * Get developer settings (cert-authenticated). GET /api/dev/settings
194
+ * When clientKeyPem is provided, uses mTLS (TLS client cert); otherwise X-Client-Cert header only.
195
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert or mTLS)
196
+ * @param {string} serverUrl - Builder Server base URL
197
+ * @param {string} clientCertPem - PEM-encoded client certificate
198
+ * @param {string} [clientKeyPem] - PEM-encoded client private key (enables mTLS when provided)
199
+ * @returns {Promise<Object>} SettingsResponseDto
200
+ */
201
+ async function getSettings(serverUrl, clientCertPem, clientKeyPem) {
202
+ if (!clientCertPem || typeof clientCertPem !== 'string') {
203
+ throw new Error('Client certificate PEM is required for getSettings');
204
+ }
205
+ const base = normalizeBaseUrl(serverUrl);
206
+ const url = buildUrl(base, '/api/dev/settings');
207
+ const reqOptions = { method: 'GET', headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) } };
208
+ if (clientKeyPem && typeof clientKeyPem === 'string') {
209
+ return requestWithCertImpl(url, reqOptions, clientCertPem, clientKeyPem);
210
+ }
211
+ return request(url, reqOptions);
212
+ }
213
+
214
+ /**
215
+ * List developers. GET /api/dev/users
216
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
217
+ * @param {string} serverUrl - Builder Server base URL
218
+ * @param {string} clientCertPem - PEM client certificate
219
+ * @returns {Promise<Object[]>} Array of UserResponseDto (empty when none)
220
+ */
221
+ async function listUsers(serverUrl, clientCertPem) {
222
+ const base = normalizeBaseUrl(serverUrl);
223
+ const data = await request(buildUrl(base, '/api/dev/users'), {
224
+ method: 'GET',
225
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
226
+ });
227
+ return Array.isArray(data) ? data : [];
228
+ }
229
+
230
+ /**
231
+ * Create developer. POST /api/dev/users
232
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
233
+ * @param {string} serverUrl - Builder Server base URL
234
+ * @param {string} clientCertPem - PEM client certificate
235
+ * @param {Object} body - CreateUserDto: developerId, name, email, optional groups
236
+ * @returns {Promise<Object>} UserResponseDto
237
+ */
238
+ async function createUser(serverUrl, clientCertPem, body) {
239
+ const base = normalizeBaseUrl(serverUrl);
240
+ return request(buildUrl(base, '/api/dev/users'), {
241
+ method: 'POST',
242
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) },
243
+ body
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Update developer. PATCH /api/dev/users/:id
249
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
250
+ * @param {string} serverUrl - Builder Server base URL
251
+ * @param {string} clientCertPem - PEM client certificate
252
+ * @param {string} id - Developer ID
253
+ * @param {Object} body - UpdateUserDto: at least one of name, email, groups
254
+ * @returns {Promise<Object>} UserResponseDto
255
+ */
256
+ async function updateUser(serverUrl, clientCertPem, id, body) {
257
+ const base = normalizeBaseUrl(serverUrl);
258
+ return request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}`), {
259
+ method: 'PATCH',
260
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) },
261
+ body
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Delete developer. DELETE /api/dev/users/:id
267
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
268
+ * @param {string} serverUrl - Builder Server base URL
269
+ * @param {string} clientCertPem - PEM client certificate
270
+ * @param {string} id - Developer ID
271
+ * @returns {Promise<Object>} DeletedResponseDto
272
+ */
273
+ async function deleteUser(serverUrl, clientCertPem, id) {
274
+ const base = normalizeBaseUrl(serverUrl);
275
+ return request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}`), {
276
+ method: 'DELETE',
277
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Create or regenerate one-time PIN. POST /api/dev/users/:id/pin
283
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
284
+ * @param {string} serverUrl - Builder Server base URL
285
+ * @param {string} clientCertPem - PEM client certificate
286
+ * @param {string} id - Developer ID
287
+ * @returns {Promise<Object>} CreatePinResponseDto: pin, expiresAt
288
+ */
289
+ async function createPin(serverUrl, clientCertPem, id) {
290
+ const base = normalizeBaseUrl(serverUrl);
291
+ return request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}/pin`), {
292
+ method: 'POST',
293
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
294
+ });
295
+ }
296
+
297
+ /**
298
+ * List SSH keys for developer. GET /api/dev/users/:id/ssh-keys
299
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
300
+ * @param {string} serverUrl - Builder Server base URL
301
+ * @param {string} clientCertPem - PEM client certificate
302
+ * @param {string} id - Developer ID
303
+ * @returns {Promise<Object[]>} Array of SshKeyItemDto
304
+ */
305
+ async function listSshKeys(serverUrl, clientCertPem, id) {
306
+ const base = normalizeBaseUrl(serverUrl);
307
+ const data = await request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}/ssh-keys`), {
308
+ method: 'GET',
309
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
310
+ });
311
+ return Array.isArray(data) ? data : [];
312
+ }
313
+
314
+ /**
315
+ * Add SSH public key for developer. POST /api/dev/users/:id/ssh-keys
316
+ * When clientKeyPem is provided, uses mTLS (TLS client cert); otherwise X-Client-Cert header only.
317
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert or mTLS)
318
+ * @param {string} serverUrl - Builder Server base URL
319
+ * @param {string} clientCertPem - PEM client certificate
320
+ * @param {string} id - Developer ID
321
+ * @param {Object} body - AddSshKeyDto: publicKey, optional label
322
+ * @param {string} [clientKeyPem] - PEM-encoded client private key (enables mTLS when provided)
323
+ * @returns {Promise<Object>} SshKeyItemDto
324
+ */
325
+ async function addSshKey(serverUrl, clientCertPem, id, body, clientKeyPem) {
326
+ const base = normalizeBaseUrl(serverUrl);
327
+ const url = buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}/ssh-keys`);
328
+ const reqOptions = {
329
+ method: 'POST',
330
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) },
331
+ body
332
+ };
333
+ if (clientKeyPem && typeof clientKeyPem === 'string') {
334
+ return requestWithCertImpl(url, reqOptions, clientCertPem, clientKeyPem);
335
+ }
336
+ return request(url, reqOptions);
337
+ }
338
+
339
+ /**
340
+ * Remove SSH key by fingerprint. DELETE /api/dev/users/:id/ssh-keys/:fingerprint
341
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
342
+ * @param {string} serverUrl - Builder Server base URL
343
+ * @param {string} clientCertPem - PEM client certificate
344
+ * @param {string} id - Developer ID
345
+ * @param {string} fingerprint - Key fingerprint
346
+ * @returns {Promise<Object>} DeletedResponseDto
347
+ */
348
+ async function removeSshKey(serverUrl, clientCertPem, id, fingerprint) {
349
+ const base = normalizeBaseUrl(serverUrl);
350
+ return request(buildUrl(base, `/api/dev/users/${encodeURIComponent(id)}/ssh-keys/${encodeURIComponent(fingerprint)}`), {
351
+ method: 'DELETE',
352
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
353
+ });
354
+ }
355
+
356
+ /**
357
+ * List secrets. GET /api/dev/secrets
358
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
359
+ * @param {string} serverUrl - Builder Server base URL
360
+ * @param {string} clientCertPem - PEM client certificate
361
+ * @returns {Promise<Object[]>} Array of SecretItemDto: name, value
362
+ */
363
+ async function listSecrets(serverUrl, clientCertPem) {
364
+ const base = normalizeBaseUrl(serverUrl);
365
+ const data = await request(buildUrl(base, '/api/dev/secrets'), {
366
+ method: 'GET',
367
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
368
+ });
369
+ return Array.isArray(data) ? data : [];
370
+ }
371
+
372
+ /**
373
+ * Add or update secret. POST /api/dev/secrets
374
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
375
+ * @param {string} serverUrl - Builder Server base URL
376
+ * @param {string} clientCertPem - PEM client certificate
377
+ * @param {Object} body - AddSecretDto: key, value
378
+ * @returns {Promise<Object>} AddSecretResponseDto
379
+ */
380
+ async function addSecret(serverUrl, clientCertPem, body) {
381
+ const base = normalizeBaseUrl(serverUrl);
382
+ return request(buildUrl(base, '/api/dev/secrets'), {
383
+ method: 'POST',
384
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) },
385
+ body
386
+ });
387
+ }
388
+
389
+ /**
390
+ * Delete secret by key. DELETE /api/dev/secrets/:key
391
+ * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
392
+ * @param {string} serverUrl - Builder Server base URL
393
+ * @param {string} clientCertPem - PEM client certificate
394
+ * @param {string} key - Secret key
395
+ * @returns {Promise<Object>} DeleteSecretResponseDto
396
+ */
397
+ async function deleteSecret(serverUrl, clientCertPem, key) {
398
+ const base = normalizeBaseUrl(serverUrl);
399
+ return request(buildUrl(base, `/api/dev/secrets/${encodeURIComponent(key)}`), {
400
+ method: 'DELETE',
401
+ headers: { 'X-Client-Cert': encodeCertForHeader(clientCertPem) }
402
+ });
403
+ }
404
+
405
+ module.exports = {
406
+ issueCert,
407
+ getHealth,
408
+ getSettings,
409
+ listUsers,
410
+ createUser,
411
+ updateUser,
412
+ deleteUser,
413
+ createPin,
414
+ listSshKeys,
415
+ addSshKey,
416
+ removeSshKey,
417
+ listSecrets,
418
+ addSecret,
419
+ deleteSecret,
420
+ normalizeBaseUrl,
421
+ buildUrl,
422
+ encodeCertForHeader
423
+ };