@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
@@ -0,0 +1,347 @@
1
+ /**
2
+ * @fileoverview aifabrix dev init – onboard with Builder Server (issue-cert, save cert, get settings, add SSH key).
3
+ * Auth: first call (issue-cert) uses no client cert; all other calls (getSettings, addSshKey, and every other dev API) send the client certificate.
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ const fs = require('fs').promises;
9
+ const path = require('path');
10
+ const chalk = require('chalk');
11
+ const config = require('../core/config');
12
+ const { getConfigDirForPaths } = require('../utils/paths');
13
+ const { generateCSR, getCertDir, readClientCertPem, readClientKeyPem, getCertValidNotAfter } = require('../utils/dev-cert-helper');
14
+ const { getOrCreatePublicKeyContent } = require('../utils/ssh-key-helper');
15
+ const devApi = require('../api/dev.api');
16
+ const logger = require('../utils/logger');
17
+ const {
18
+ isSslUntrustedError,
19
+ fetchInstallCa,
20
+ installCaPlatform,
21
+ promptInstallCa
22
+ } = require('../utils/dev-ca-install');
23
+
24
+ /**
25
+ * Ensure the Builder Server is trusted: run health check; on SSL untrusted error,
26
+ * optionally fetch and install CA, then retry.
27
+ * @param {string} baseUrl - Builder Server base URL
28
+ * @param {Object} options - Commander options (yes, y, no-install-ca)
29
+ * @returns {Promise<void>}
30
+ */
31
+ async function ensureServerTrusted(baseUrl, options) {
32
+ const skipInstall = options['no-install-ca'];
33
+ const autoInstall = options.yes || options.y;
34
+ try {
35
+ await devApi.getHealth(baseUrl);
36
+ } catch (err) {
37
+ if (!isSslUntrustedError(err)) throw err;
38
+ const manualUrl = `${baseUrl.replace(/\/+$/, '')}/install-ca`;
39
+ if (skipInstall) {
40
+ throw new Error(`Server certificate not trusted. Install CA manually: ${manualUrl}`);
41
+ }
42
+ if (!autoInstall) {
43
+ const install = await promptInstallCa();
44
+ if (!install) {
45
+ throw new Error(`Server certificate not trusted. Install CA manually: ${manualUrl}`);
46
+ }
47
+ }
48
+ logger.log(chalk.gray(' Downloading and installing CA...'));
49
+ const caPem = await fetchInstallCa(baseUrl);
50
+ await installCaPlatform(caPem, baseUrl);
51
+ logger.log(chalk.gray(' CA installed. Retrying...'));
52
+ await devApi.getHealth(baseUrl);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Validate init options and return normalized baseUrl and devId.
58
+ * @param {Object} options - Commander options
59
+ * @returns {{ baseUrl: string, devId: string }}
60
+ */
61
+ function validateInitOptions(options) {
62
+ const devId = options.developerId || options['developer-id'];
63
+ const server = options.server;
64
+ const pin = options.pin;
65
+
66
+ if (!devId || typeof devId !== 'string' || !/^[0-9]+$/.test(devId)) {
67
+ throw new Error('--developer-id is required and must be a non-empty digit string (e.g. 01)');
68
+ }
69
+ if (!server || typeof server !== 'string' || !server.trim()) {
70
+ throw new Error('--server is required and must be the Builder Server base URL (e.g. https://dev.aifabrix.dev)');
71
+ }
72
+ if (!pin || typeof pin !== 'string' || !pin.trim()) {
73
+ throw new Error('--pin is required (one-time PIN from your admin)');
74
+ }
75
+ return { baseUrl: server.trim().replace(/\/+$/, ''), devId };
76
+ }
77
+
78
+ /**
79
+ * Request certificate from Builder Server; map API errors to user messages.
80
+ * @param {string} baseUrl - Builder Server base URL
81
+ * @param {string} devId - Developer ID
82
+ * @param {string} pin - One-time PIN
83
+ * @param {string} csrPem - PEM CSR
84
+ * @returns {Promise<Object>} IssueCertResponseDto
85
+ */
86
+ async function requestCertificate(baseUrl, devId, pin, csrPem) {
87
+ try {
88
+ return await devApi.issueCert(baseUrl, {
89
+ developerId: devId,
90
+ pin: pin.trim(),
91
+ csr: csrPem
92
+ });
93
+ } catch (err) {
94
+ if (err.status === 401) {
95
+ throw new Error('Invalid or expired PIN. Ask your admin for a new PIN (aifabrix dev pin <developerId>).');
96
+ }
97
+ if (err.status === 404) {
98
+ throw new Error(`Developer ${devId} not found on the server.`);
99
+ }
100
+ if (err.status === 503) {
101
+ throw new Error('Certificate signing is temporarily unavailable. Try again later.');
102
+ }
103
+ throw err;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Normalize PEM string: turn literal \n (backslash-n) into real newlines so Docker/OpenSSL accept it.
109
+ * Some servers return JSON with escaped newlines in the PEM string.
110
+ * @param {string} pem - PEM string (certificate or CA)
111
+ * @returns {string} PEM with real newlines
112
+ */
113
+ function normalizePemNewlines(pem) {
114
+ if (typeof pem !== 'string') return pem;
115
+ return pem.replace(/\\n/g, '\n');
116
+ }
117
+
118
+ /**
119
+ * Save certificate, key, and optional CA to cert dir; set developer-id in config.
120
+ * Remote Docker requires ca.pem in the cert dir; if the server provides it (e.g. issue-cert
121
+ * response caCertificate or ca), it is saved so DOCKER_CERT_PATH works.
122
+ * @param {string} configDir - Config directory
123
+ * @param {string} devId - Developer ID
124
+ * @param {string} certificatePem - Issued certificate PEM
125
+ * @param {string} keyPem - Private key PEM
126
+ * @param {string} [caPem] - Optional CA certificate PEM (for remote Docker TLS)
127
+ */
128
+ async function saveCertAndConfig(configDir, devId, certificatePem, keyPem, caPem) {
129
+ const certDir = getCertDir(configDir, devId);
130
+ await fs.mkdir(certDir, { recursive: true });
131
+ const certNormalized = normalizePemNewlines(certificatePem);
132
+ const keyNormalized = normalizePemNewlines(keyPem);
133
+ await fs.writeFile(path.join(certDir, 'cert.pem'), certNormalized, { mode: 0o600 });
134
+ await fs.writeFile(path.join(certDir, 'key.pem'), keyNormalized, { mode: 0o600 });
135
+ if (caPem && typeof caPem === 'string' && caPem.trim()) {
136
+ const caNormalized = normalizePemNewlines(caPem.trim());
137
+ await fs.writeFile(path.join(certDir, 'ca.pem'), caNormalized, { mode: 0o600 });
138
+ logger.log(chalk.green(' ✓ Certificate and CA saved to ') + chalk.cyan(path.join(certDir, 'cert.pem')));
139
+ } else {
140
+ logger.log(chalk.green(' ✓ Certificate saved to ') + chalk.cyan(path.join(certDir, 'cert.pem')));
141
+ }
142
+ await config.setDeveloperId(devId);
143
+ logger.log(chalk.green(' ✓ Developer ID set to ') + chalk.cyan(devId));
144
+ }
145
+
146
+ /**
147
+ * Message for 400 Bad Request: nginx often forwards X-Client-Cert with literal newlines.
148
+ * @returns {string} Hint for server-side nginx fix
149
+ */
150
+ function getBadRequestHint() {
151
+ return 'Bad Request (400) often means the server\'s nginx is forwarding the client certificate with literal newlines in X-Client-Cert. On the server, use nginx njs to escape newlines (see .cursor/plans/builder-cli.md §5).';
152
+ }
153
+
154
+ /**
155
+ * Log a one-line hint for cert troubleshooting (curl test and docs).
156
+ * @param {string} configDir - Config directory
157
+ * @param {string} devId - Developer ID
158
+ * @param {string} baseUrl - Builder Server base URL
159
+ */
160
+ function logCertTroubleshootingHint(configDir, devId, baseUrl) {
161
+ const certDir = getCertDir(configDir, devId);
162
+ const certPath = path.join(certDir, 'cert.pem');
163
+ const keyPath = path.join(certDir, 'key.pem');
164
+ logger.log(chalk.gray(` Test with: curl -v --cert ${certPath} --key ${keyPath} ${baseUrl}/api/dev/settings`));
165
+ logger.log(chalk.gray(' See .cursor/plans/builder-cli.md §5 for 200 vs 401 vs 400 and nginx/server fix.'));
166
+ }
167
+
168
+ /**
169
+ * Register SSH public key with Builder Server for Mutagen sync.
170
+ * @param {string} baseUrl - Builder Server base URL
171
+ * @param {string} clientCertPem - Client certificate PEM
172
+ * @param {string} clientKeyPem - Client private key PEM (for mTLS)
173
+ * @param {string} devId - Developer ID
174
+ */
175
+ async function registerSshKey(baseUrl, clientCertPem, clientKeyPem, devId) {
176
+ const publicKey = getOrCreatePublicKeyContent();
177
+ try {
178
+ await devApi.addSshKey(baseUrl, clientCertPem, devId, {
179
+ publicKey,
180
+ label: 'aifabrix-init'
181
+ }, clientKeyPem);
182
+ logger.log(chalk.green(' ✓ SSH key registered'));
183
+ } catch (err) {
184
+ if (err.status === 409) {
185
+ logger.log(chalk.yellow(' ⚠ SSH key already registered'));
186
+ } else {
187
+ throw err;
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Run SSH key registration step (log, register, handle 400 hint).
194
+ * @param {string} baseUrl - Builder Server base URL
195
+ * @param {Object} issueResponse - IssueCert response (certificate)
196
+ * @param {string} keyPem - Client key PEM
197
+ * @param {string} configDir - Config directory
198
+ * @param {string} devId - Developer ID
199
+ * @private
200
+ */
201
+ async function _runSshKeyRegistrationStep(baseUrl, issueResponse, keyPem, configDir, devId) {
202
+ logger.log(chalk.gray(' Registering SSH key for Mutagen sync...'));
203
+ try {
204
+ if (keyPem && typeof keyPem === 'string') {
205
+ logger.log(chalk.gray(' Using client certificate for TLS'));
206
+ }
207
+ await registerSshKey(baseUrl, issueResponse.certificate, keyPem, devId);
208
+ } catch (err) {
209
+ const msg = err.status === 400 ? getBadRequestHint() : (err.message || String(err));
210
+ logger.log(chalk.yellow(' ⚠ Could not register SSH key: ' + msg));
211
+ logCertTroubleshootingHint(configDir, devId, baseUrl);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Apply settings from issue-cert response or fetch via getSettings; merge into config.
217
+ * @param {string} baseUrl - Builder Server base URL
218
+ * @param {string} devId - Developer ID
219
+ * @param {Object} issueResponse - IssueCert response (certificate, settings)
220
+ * @param {string} keyPem - Client key PEM
221
+ */
222
+ async function applySettingsFromServer(baseUrl, devId, issueResponse, keyPem) {
223
+ const configDir = getConfigDirForPaths();
224
+ if (issueResponse.settings && typeof issueResponse.settings === 'object') {
225
+ await config.mergeRemoteSettings(issueResponse.settings);
226
+ logger.log(chalk.green(' ✓ Config updated from server (issue-cert response)'));
227
+ return;
228
+ }
229
+ logger.log(chalk.gray(' Fetching settings...'));
230
+ try {
231
+ if (keyPem && typeof keyPem === 'string') {
232
+ logger.log(chalk.gray(' Using client certificate for TLS'));
233
+ }
234
+ const settings = await devApi.getSettings(baseUrl, issueResponse.certificate, keyPem);
235
+ await config.mergeRemoteSettings(settings);
236
+ logger.log(chalk.green(' ✓ Config updated from server'));
237
+ } catch (err) {
238
+ const msg = err.status === 400 ? getBadRequestHint() : (err.message || String(err));
239
+ logger.log(chalk.yellow(' ⚠ Could not fetch settings (server may not support cert yet): ' + msg));
240
+ logCertTroubleshootingHint(configDir, devId, baseUrl);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Run dev init: validate PIN via issue-cert, save certificate, fetch settings, add SSH key.
246
+ * @param {Object} options - Commander options (devId, server, pin)
247
+ * @returns {Promise<void>}
248
+ */
249
+ async function runDevInit(options) {
250
+ const { baseUrl, devId } = validateInitOptions(options);
251
+ logger.log(chalk.blue('\n🔐 Onboarding with Builder Server...\n'));
252
+
253
+ try {
254
+ await ensureServerTrusted(baseUrl, options);
255
+ } catch (err) {
256
+ throw new Error(`Cannot reach Builder Server at ${baseUrl}. Check URL and network. ${err.message}`);
257
+ }
258
+
259
+ logger.log(chalk.gray(' Generating certificate request...'));
260
+ const { csrPem, keyPem } = generateCSR(devId);
261
+
262
+ logger.log(chalk.gray(' Requesting certificate (issue-cert)...'));
263
+ const issueResponse = await requestCertificate(baseUrl, devId, options.pin, csrPem);
264
+
265
+ const configDir = getConfigDirForPaths();
266
+ const caPem = issueResponse.caCertificate || issueResponse.ca;
267
+ await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
268
+
269
+ await config.setRemoteServer(baseUrl);
270
+
271
+ await applySettingsFromServer(baseUrl, devId, issueResponse, keyPem);
272
+ await _runSshKeyRegistrationStep(baseUrl, issueResponse, keyPem, configDir, devId);
273
+ logger.log(chalk.green('\n✓ Onboarding complete. You can use remote Docker and Mutagen sync.\n'));
274
+ }
275
+
276
+ /** Days before cert expiry at which we auto-refresh on dev refresh. */
277
+ const CERT_REFRESH_DAYS = 14;
278
+
279
+ /**
280
+ * True if the cert in certDir expires within CERT_REFRESH_DAYS (or we cannot read expiry).
281
+ * @param {string} certDir - Certificate directory
282
+ * @returns {boolean}
283
+ */
284
+ function shouldRefreshDevCert(certDir) {
285
+ const validNotAfter = getCertValidNotAfter(certDir);
286
+ if (!validNotAfter) return true;
287
+ const now = Date.now();
288
+ const threshold = now + CERT_REFRESH_DAYS * 24 * 60 * 60 * 1000;
289
+ return validNotAfter.getTime() < threshold;
290
+ }
291
+
292
+ /**
293
+ * Refresh developer certificate: create PIN (with current cert), issue new cert, save and apply settings.
294
+ * @param {{ serverUrl: string, clientCertPem: string }} auth - Current auth from getRemoteDevAuth
295
+ * @returns {Promise<void>}
296
+ */
297
+ async function runCertificateRefresh(auth) {
298
+ const devId = await config.getDeveloperId();
299
+ if (!devId) throw new Error('developer-id not set in config.');
300
+ const configDir = getConfigDirForPaths();
301
+ logger.log(chalk.blue('\n🔄 Refreshing certificate (create PIN + issue-cert)...\n'));
302
+ const pinRes = await devApi.createPin(auth.serverUrl, auth.clientCertPem, devId);
303
+ const pin = pinRes.pin;
304
+ if (!pin || typeof pin !== 'string') throw new Error('Server did not return a PIN.');
305
+ logger.log(chalk.gray(' Generating new certificate request...'));
306
+ const { csrPem, keyPem } = generateCSR(devId);
307
+ logger.log(chalk.gray(' Requesting new certificate (issue-cert)...'));
308
+ const issueResponse = await requestCertificate(auth.serverUrl, devId, pin, csrPem);
309
+ const caPem = issueResponse.caCertificate || issueResponse.ca;
310
+ await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
311
+ await applySettingsFromServer(auth.serverUrl, devId, issueResponse, keyPem);
312
+ logger.log(chalk.green('✓ Certificate refreshed and config updated from server.\n'));
313
+ }
314
+
315
+ /**
316
+ * Fetch settings from Builder Server and merge into config (GET /api/dev/settings).
317
+ * If certificate expires within CERT_REFRESH_DAYS (or --cert), refresh cert first (create PIN + issue-cert).
318
+ * @param {Object} [options] - Commander options; options.cert = true forces cert refresh
319
+ * @returns {Promise<void>}
320
+ * @throws {Error} If remote server or certificate not configured, or getSettings fails
321
+ */
322
+ async function runDevRefresh(options = {}) {
323
+ const { getRemoteDevAuth } = require('../utils/remote-dev-auth');
324
+ const auth = await getRemoteDevAuth();
325
+ if (!auth) {
326
+ throw new Error('Remote server is not configured. Set remote-server and run "aifabrix dev init" first.');
327
+ }
328
+ const devId = await config.getDeveloperId();
329
+ const configDir = getConfigDirForPaths();
330
+ const certDir = getCertDir(configDir, devId);
331
+ const clientCertPem = readClientCertPem(certDir);
332
+ const clientKeyPem = readClientKeyPem(certDir);
333
+ if (!clientCertPem) {
334
+ throw new Error('Client certificate not found. Run "aifabrix dev init" first.');
335
+ }
336
+ const forceCertRefresh = Boolean(options.cert);
337
+ if (forceCertRefresh || shouldRefreshDevCert(certDir)) {
338
+ await runCertificateRefresh(auth);
339
+ return;
340
+ }
341
+ logger.log(chalk.blue('\n🔄 Fetching settings from Builder Server...\n'));
342
+ const settings = await devApi.getSettings(auth.serverUrl, clientCertPem, clientKeyPem || undefined);
343
+ await config.mergeRemoteSettings(settings);
344
+ logger.log(chalk.green('✓ Config updated from server. Run "aifabrix dev config" to verify.\n'));
345
+ }
346
+
347
+ module.exports = { runDevInit, runDevRefresh };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Normalizes system file authentication.security and configuration keyvault entries.
3
+ * @fileoverview Repair auth/config KV_* names and path-style kv:// values
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { systemKeyToKvPrefix, securityKeyToVar, kvEnvKeyToPath } = require('../utils/credential-secrets-env');
11
+
12
+ /**
13
+ * Returns true if a kv value looks like legacy format (KeyVault suffix or no path segments).
14
+ * @param {string} val - Value from authentication.security or configuration
15
+ * @returns {boolean}
16
+ */
17
+ function isLegacyKvValue(val) {
18
+ if (typeof val !== 'string' || !val.trim().toLowerCase().startsWith('kv://')) return false;
19
+ const after = val.trim().slice(5); // after 'kv://'
20
+ return after.includes('KeyVault') || !after.includes('/');
21
+ }
22
+
23
+ /**
24
+ * Normalizes authentication.security keyvault entries to path-style kv:// values (kv://systemKey/variable).
25
+ * @param {Object} security - authentication.security object (mutated)
26
+ * @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
27
+ * @param {string} systemKey - System key for path namespace
28
+ * @param {string[]} changes - Array to append change descriptions to
29
+ * @returns {boolean} True if any change was made
30
+ */
31
+ function normalizeSecuritySection(security, prefix, systemKey, changes) {
32
+ let updated = false;
33
+ for (const key of Object.keys(security)) {
34
+ const val = security[key];
35
+ if (typeof val !== 'string' || !isLegacyKvValue(val)) continue;
36
+ const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
37
+ const pathVal = kvEnvKeyToPath(envName, systemKey);
38
+ if (pathVal) {
39
+ security[key] = pathVal;
40
+ changes.push(`authentication.security.${key}: normalized to path-style ${pathVal}`);
41
+ updated = true;
42
+ }
43
+ }
44
+ return updated;
45
+ }
46
+
47
+ /**
48
+ * Normalizes configuration array keyvault entries to canonical KV_* names and path-style values.
49
+ * @param {Object[]} config - configuration array (mutated)
50
+ * @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
51
+ * @param {string} systemKey - System key for path namespace
52
+ * @param {string[]} changes - Array to append change descriptions to
53
+ * @returns {boolean} True if any change was made
54
+ */
55
+ function normalizeConfigurationSection(config, prefix, systemKey, changes) {
56
+ let updated = false;
57
+ for (let i = 0; i < config.length; i++) {
58
+ const entry = config[i];
59
+ if (!entry || !entry.name || (entry.location !== 'keyvault' && !String(entry.name).startsWith('KV_'))) continue;
60
+ const afterPrefix = entry.name.startsWith(`KV_${prefix}_`)
61
+ ? entry.name.slice(`KV_${prefix}_`.length)
62
+ : entry.name.replace(/^KV_[A-Z0-9]+_/, '');
63
+ const normalizedVar = afterPrefix.replace(/_/g, '').toUpperCase();
64
+ const canonicalName = `KV_${prefix}_${normalizedVar}`;
65
+ const pathVal = kvEnvKeyToPath(canonicalName, systemKey);
66
+ if (!pathVal) continue;
67
+ const pathValWithoutPrefix = pathVal.replace(/^kv:\/\//, '');
68
+ const valueLegacy = typeof entry.value === 'string' && (entry.value.includes('KeyVault') || !entry.value.includes('/'));
69
+ if (entry.name !== canonicalName || (valueLegacy && entry.value !== pathValWithoutPrefix)) {
70
+ config[i] = { ...entry, name: canonicalName, value: pathValWithoutPrefix, location: 'keyvault' };
71
+ changes.push(`configuration: normalized ${entry.name} → ${canonicalName}, value → path-style`);
72
+ updated = true;
73
+ }
74
+ }
75
+ return updated;
76
+ }
77
+
78
+ /**
79
+ * Normalizes system file authentication.security and configuration keyvault entries to canonical
80
+ * KV_* names and path-style kv:// values so upload validation and env.template align.
81
+ *
82
+ * @param {Object} systemParsed - Parsed system config (mutated)
83
+ * @param {string} systemKey - System key (e.g. 'hubspot')
84
+ * @param {string[]} changes - Array to append change descriptions to
85
+ * @returns {boolean} True if any change was made
86
+ */
87
+ function normalizeSystemFileAuthAndConfig(systemParsed, systemKey, changes) {
88
+ const prefix = systemKeyToKvPrefix(systemKey);
89
+ if (!prefix) return false;
90
+ const security = systemParsed.authentication?.security;
91
+ let updated = (security && typeof security === 'object' && normalizeSecuritySection(security, prefix, systemKey, changes));
92
+ const config = systemParsed.configuration;
93
+ if (Array.isArray(config)) {
94
+ updated = normalizeConfigurationSection(config, prefix, systemKey, changes) || updated;
95
+ }
96
+ return updated;
97
+ }
98
+
99
+ module.exports = { normalizeSystemFileAuthAndConfig, isLegacyKvValue };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Normalize datasource keys and filenames to canonical form during repair.
3
+ *
4
+ * Key: <systemKey>-<resourceType> or <systemKey>-<resourceType>-2, -3 for duplicates.
5
+ * Filename: <systemKey>-datasource-<suffix>.<ext> where suffix = key without leading systemKey-.
6
+ * Skips keys/filenames that already match the valid pattern (e.g. customer-extra, customer-1).
7
+ *
8
+ * @fileoverview Datasource key and filename normalization for repair
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+ const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
18
+
19
+ /**
20
+ * Returns suffix from a canonical-format filename: <systemKey>-datasource-<suffix>.<ext>.
21
+ *
22
+ * @param {string} fileName - Filename
23
+ * @param {string} systemKey - System key
24
+ * @returns {string|null} Suffix or null if not in canonical format
25
+ */
26
+ function suffixFromCanonicalFilename(fileName, systemKey) {
27
+ const base = path.basename(fileName);
28
+ const ext = path.extname(fileName);
29
+ const withoutExt = base.slice(0, -ext.length);
30
+ const prefix = `${systemKey}-datasource-`;
31
+ if (!withoutExt.startsWith(prefix)) return null;
32
+ return withoutExt.slice(prefix.length) || null;
33
+ }
34
+
35
+ /**
36
+ * Returns true if the key already matches canonical form and should not be changed.
37
+ * Valid: <systemKey>-<resourceType> or <systemKey>-<resourceType>-<extra> (e.g. customer-extra, customer-1).
38
+ * When fileName is provided and is canonical, key may be just the suffix (e.g. record-storage).
39
+ * Invalid (will normalize): key ending with redundant -datasource (e.g. hubspot-demo-companies-datasource).
40
+ *
41
+ * @param {string} key - Datasource key
42
+ * @param {string} systemKey - System key
43
+ * @param {string} [fileName] - Optional filename; if canonical, key can be suffix-only
44
+ * @returns {boolean}
45
+ */
46
+ function isKeyAlreadyCanonical(key, systemKey, fileName) {
47
+ if (!key || !systemKey) return false;
48
+ if (fileName && isFilenameAlreadyCanonical(fileName, systemKey)) {
49
+ const suffixFromFile = suffixFromCanonicalFilename(fileName, systemKey);
50
+ if (suffixFromFile && (key === suffixFromFile || key === `${systemKey}-${suffixFromFile}`)) {
51
+ return true;
52
+ }
53
+ }
54
+ if (!key.startsWith(systemKey + '-')) return false;
55
+ const suffix = key.slice(systemKey.length + 1);
56
+ if (!suffix) return false;
57
+ if (suffix.endsWith('-datasource')) return false;
58
+ return true;
59
+ }
60
+
61
+ /**
62
+ * Derives resourceType slug from key: strip systemKey prefix, then strip trailing -datasource if present.
63
+ *
64
+ * @param {string} key - Current datasource key
65
+ * @param {string} systemKey - System key
66
+ * @returns {string}
67
+ */
68
+ function slugFromKey(key, systemKey) {
69
+ if (!key || !systemKey || !key.startsWith(systemKey + '-')) return key || '';
70
+ let suffix = key.slice(systemKey.length + 1);
71
+ if (suffix.endsWith('-datasource')) suffix = suffix.slice(0, -'-datasource'.length);
72
+ return suffix || key;
73
+ }
74
+
75
+ /**
76
+ * Returns canonical filename for a datasource: <systemKey>-datasource-<suffix>.<ext>.
77
+ *
78
+ * @param {string} canonicalKey - Canonical datasource key
79
+ * @param {string} systemKey - System key
80
+ * @param {string} ext - File extension including dot (e.g. .json)
81
+ * @returns {string}
82
+ */
83
+ function canonicalDatasourceFilename(canonicalKey, systemKey, ext) {
84
+ const suffix = canonicalKey.startsWith(systemKey + '-')
85
+ ? canonicalKey.slice(systemKey.length + 1)
86
+ : canonicalKey;
87
+ return `${systemKey}-datasource-${suffix}${ext}`;
88
+ }
89
+
90
+ /**
91
+ * Returns true if filename already matches canonical pattern <systemKey>-datasource-<suffix>.<ext>.
92
+ *
93
+ * @param {string} fileName - Current filename
94
+ * @param {string} systemKey - System key
95
+ * @returns {boolean}
96
+ */
97
+ function isFilenameAlreadyCanonical(fileName, systemKey) {
98
+ const base = path.basename(fileName);
99
+ const ext = path.extname(fileName);
100
+ const withoutExt = base.slice(0, -ext.length);
101
+ const prefix = `${systemKey}-datasource-`;
102
+ if (!withoutExt.startsWith(prefix)) return false;
103
+ const suffix = withoutExt.slice(prefix.length);
104
+ if (!suffix || suffix.endsWith('-datasource')) return false;
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * Normalizes datasource keys and filenames to canonical form. Runs early in repair.
110
+ * Updates file contents (key property), renames files when needed, and updates variables.externalIntegration.dataSources.
111
+ *
112
+ * @param {string} appPath - Application directory path
113
+ * @param {string[]} datasourceFiles - Current list of datasource filenames
114
+ * @param {string} systemKey - System key
115
+ * @param {Object} variables - Application variables (mutated: externalIntegration.dataSources updated)
116
+ * @param {boolean} dryRun - If true, do not write or rename
117
+ * @param {string[]} changes - Array to append change descriptions to
118
+ * @returns {{ updated: boolean, datasourceFiles: string[] }} Updated flag and new list of datasource filenames
119
+ */
120
+ /* eslint-disable max-lines-per-function, max-statements, complexity -- Normalization loops and branching per file */
121
+ function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey, variables, dryRun, changes) {
122
+ if (!datasourceFiles || datasourceFiles.length === 0) {
123
+ return { updated: false, datasourceFiles: datasourceFiles || [] };
124
+ }
125
+
126
+ const slugCounts = new Map();
127
+ const fileInfos = [];
128
+
129
+ for (const fileName of datasourceFiles) {
130
+ const filePath = path.join(appPath, fileName);
131
+ if (!fs.existsSync(filePath)) continue;
132
+ let parsed;
133
+ try {
134
+ parsed = loadConfigFile(filePath);
135
+ } catch (err) {
136
+ fileInfos.push({ fileName, key: null, skip: true });
137
+ continue;
138
+ }
139
+ const key = (parsed && typeof parsed.key === 'string' && parsed.key.trim()) ? parsed.key.trim() : null;
140
+ if (isKeyAlreadyCanonical(key, systemKey, fileName) && isFilenameAlreadyCanonical(fileName, systemKey)) {
141
+ fileInfos.push({ fileName, key, skip: true });
142
+ continue;
143
+ }
144
+ const slug = slugFromKey(key || fileName, systemKey);
145
+ fileInfos.push({
146
+ fileName,
147
+ parsed,
148
+ key,
149
+ slug,
150
+ canonicalKey: null,
151
+ skip: false
152
+ });
153
+ }
154
+
155
+ for (const info of fileInfos) {
156
+ if (info.skip) continue;
157
+ const slug = info.slug;
158
+ const n = (slugCounts.get(slug) || 0) + 1;
159
+ slugCounts.set(slug, n);
160
+ info.canonicalKey = n === 1 ? `${systemKey}-${slug}` : `${systemKey}-${slug}-${n}`;
161
+ }
162
+
163
+ let updated = false;
164
+ const newDatasourceFiles = [];
165
+
166
+ for (const info of fileInfos) {
167
+ if (info.skip) {
168
+ newDatasourceFiles.push(info.fileName);
169
+ continue;
170
+ }
171
+ const { fileName, parsed, canonicalKey } = info;
172
+ const ext = path.extname(fileName);
173
+ const canonicalFileName = canonicalDatasourceFilename(canonicalKey, systemKey, ext);
174
+
175
+ if (parsed.key !== canonicalKey) {
176
+ parsed.key = canonicalKey;
177
+ if (!dryRun) writeConfigFile(path.join(appPath, fileName), parsed);
178
+ changes.push(`${fileName}: key → ${canonicalKey}`);
179
+ updated = true;
180
+ }
181
+ if (fileName !== canonicalFileName) {
182
+ const oldPath = path.join(appPath, fileName);
183
+ const newPath = path.join(appPath, canonicalFileName);
184
+ if (!dryRun && fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
185
+ fs.renameSync(oldPath, newPath);
186
+ }
187
+ changes.push(`Renamed ${fileName} → ${canonicalFileName}`);
188
+ updated = true;
189
+ newDatasourceFiles.push(canonicalFileName);
190
+ } else {
191
+ newDatasourceFiles.push(fileName);
192
+ }
193
+ }
194
+
195
+ if (updated && variables.externalIntegration && Array.isArray(variables.externalIntegration.dataSources)) {
196
+ variables.externalIntegration.dataSources = [...newDatasourceFiles].sort();
197
+ }
198
+
199
+ return { updated, datasourceFiles: newDatasourceFiles };
200
+ }
201
+
202
+ module.exports = {
203
+ normalizeDatasourceKeysAndFilenames,
204
+ isKeyAlreadyCanonical,
205
+ slugFromKey,
206
+ canonicalDatasourceFilename,
207
+ isFilenameAlreadyCanonical
208
+ };