@aifabrix/builder 2.31.1 → 2.32.1

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 (118) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +145 -133
  67. package/lib/schema/external-system.schema.json +42 -0
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +31 -2
  111. package/templates/external-system/deploy.ps1.hbs +34 -0
  112. package/templates/external-system/deploy.sh.hbs +34 -0
  113. package/templates/external-system/external-datasource.json.hbs +31 -12
  114. package/lib/app.js +0 -467
  115. package/lib/datasource-list.js +0 -141
  116. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  117. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  118. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -11,7 +11,7 @@
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
13
  const yaml = require('js-yaml');
14
- const config = require('../config');
14
+ const config = require('../core/config');
15
15
  const { buildHostnameToServiceMap, resolveUrlPort } = require('./secrets-utils');
16
16
  const { rewriteInfraEndpoints, getEnvHosts, getServicePort, getServiceHost, getLocalhostOverride } = require('./env-endpoints');
17
17
  const { loadEnvConfig } = require('./env-config-loader');
@@ -130,6 +130,65 @@ function loadEnvTemplate(templatePath) {
130
130
  return fs.readFileSync(templatePath, 'utf8');
131
131
  }
132
132
 
133
+ /**
134
+ * Gets port from local environment config
135
+ * @function getPortFromLocalEnv
136
+ * @param {Object} localEnv - Local environment config
137
+ * @returns {number|null} Port value or null
138
+ */
139
+ function getPortFromLocalEnv(localEnv) {
140
+ if (localEnv.PORT === undefined || localEnv.PORT === null) {
141
+ return null;
142
+ }
143
+ const portVal = typeof localEnv.PORT === 'number' ? localEnv.PORT : parseInt(localEnv.PORT, 10);
144
+ return Number.isNaN(portVal) ? null : portVal;
145
+ }
146
+
147
+ /**
148
+ * Gets port from variables.yaml file
149
+ * @function getPortFromVariablesFile
150
+ * @param {string} variablesPath - Path to variables.yaml
151
+ * @returns {number|null} Port value or null
152
+ */
153
+ function getPortFromVariablesFile(variablesPath) {
154
+ if (!variablesPath || !fs.existsSync(variablesPath)) {
155
+ return null;
156
+ }
157
+ try {
158
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
159
+ const variables = yaml.load(variablesContent);
160
+ const localPort = variables?.build?.localPort;
161
+ if (typeof localPort === 'number' && localPort > 0) {
162
+ return localPort;
163
+ }
164
+ return variables?.port || null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Gets port from environment content fallback
172
+ * @function getPortFromEnvContent
173
+ * @param {string} envContent - Environment content
174
+ * @returns {number} Port value (defaults to 3000)
175
+ */
176
+ function getPortFromEnvContent(envContent) {
177
+ const portMatch = envContent.match(/^PORT\s*=\s*(\d+)/m);
178
+ return portMatch ? parseInt(portMatch[1], 10) : 3000;
179
+ }
180
+
181
+ /**
182
+ * Applies developer-id adjustment to port
183
+ * @function applyDeveloperIdAdjustment
184
+ * @param {number} baseAppPort - Base application port
185
+ * @param {number} devIdNum - Developer ID number
186
+ * @returns {number} Adjusted port
187
+ */
188
+ function applyDeveloperIdAdjustment(baseAppPort, devIdNum) {
189
+ return devIdNum === 0 ? baseAppPort : (baseAppPort + (devIdNum * 100));
190
+ }
191
+
133
192
  /**
134
193
  * Calculate application port following override chain and developer-id adjustment
135
194
  * Override chain: env-config.yaml → config.yaml → variables.yaml build.localPort → variables.yaml port
@@ -143,43 +202,21 @@ function loadEnvTemplate(templatePath) {
143
202
  */
144
203
  async function calculateAppPort(variablesPath, localEnv, envContent, devIdNum) {
145
204
  // Start with env-config value
146
- let baseAppPort = null;
147
- if (localEnv.PORT !== undefined && localEnv.PORT !== null) {
148
- const portVal = typeof localEnv.PORT === 'number' ? localEnv.PORT : parseInt(localEnv.PORT, 10);
149
- if (!Number.isNaN(portVal)) {
150
- baseAppPort = portVal;
151
- }
152
- }
205
+ let baseAppPort = getPortFromLocalEnv(localEnv);
153
206
 
154
207
  // Override with variables.yaml → build.localPort (strongest)
155
- if (variablesPath && fs.existsSync(variablesPath)) {
156
- try {
157
- const variablesContent = fs.readFileSync(variablesPath, 'utf8');
158
- const variables = yaml.load(variablesContent);
159
- const localPort = variables?.build?.localPort;
160
- if (typeof localPort === 'number' && localPort > 0) {
161
- baseAppPort = localPort;
162
- } else if (baseAppPort === null || baseAppPort === undefined) {
163
- // Fallback to variables.yaml → port
164
- baseAppPort = variables?.port || 3000;
165
- }
166
- } catch {
167
- // Fallback to reading from env content if variables.yaml read fails
168
- if (baseAppPort === null || baseAppPort === undefined) {
169
- const portMatch = envContent.match(/^PORT\s*=\s*(\d+)/m);
170
- baseAppPort = portMatch ? parseInt(portMatch[1], 10) : 3000;
171
- }
172
- }
173
- } else {
174
- // Fallback if variablesPath not provided
175
- if (baseAppPort === null || baseAppPort === undefined) {
176
- const portMatch = envContent.match(/^PORT\s*=\s*(\d+)/m);
177
- baseAppPort = portMatch ? parseInt(portMatch[1], 10) : 3000;
178
- }
208
+ const variablesPort = getPortFromVariablesFile(variablesPath);
209
+ if (variablesPort !== null) {
210
+ baseAppPort = variablesPort;
211
+ }
212
+
213
+ // Fallback to env content if still no port
214
+ if (baseAppPort === null || baseAppPort === undefined) {
215
+ baseAppPort = getPortFromEnvContent(envContent);
179
216
  }
180
217
 
181
218
  // Apply developer-id adjustment
182
- return devIdNum === 0 ? baseAppPort : (baseAppPort + (devIdNum * 100));
219
+ return applyDeveloperIdAdjustment(baseAppPort, devIdNum);
183
220
  }
184
221
 
185
222
  /**
@@ -211,21 +248,32 @@ function updateLocalhostUrls(content, baseAppPort, appPort) {
211
248
  * @param {string} [variablesPath] - Path to variables.yaml (to read build.localPort)
212
249
  * @returns {Promise<string>} Updated content with local ports
213
250
  */
214
- async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
215
- // Get developer-id for port adjustment
251
+ /**
252
+ * Gets developer ID number
253
+ * @async
254
+ * @function getDeveloperIdNumber
255
+ * @returns {Promise<number>} Developer ID number
256
+ */
257
+ async function getDeveloperIdNumber() {
216
258
  const devId = await config.getDeveloperId();
217
- let devIdNum = 0;
218
259
  if (devId !== null && devId !== undefined) {
219
260
  const parsed = parseInt(devId, 10);
220
261
  if (!Number.isNaN(parsed)) {
221
- devIdNum = parsed;
262
+ return parsed;
222
263
  }
223
264
  }
265
+ return 0;
266
+ }
224
267
 
225
- // Get base config from env-config.yaml (includes user env-config file if configured)
268
+ /**
269
+ * Gets local environment configuration with overrides
270
+ * @async
271
+ * @function getLocalEnvWithOverrides
272
+ * @returns {Promise<Object>} Local environment configuration
273
+ */
274
+ async function getLocalEnvWithOverrides() {
226
275
  let localEnv = await getEnvHosts('local');
227
276
 
228
- // Apply config.yaml → environments.local override (if exists)
229
277
  try {
230
278
  const os = require('os');
231
279
  const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
@@ -237,33 +285,34 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
237
285
  }
238
286
  }
239
287
  } catch {
240
- // Ignore config.yaml read errors, continue with env-config values
288
+ // Ignore config.yaml read errors
241
289
  }
242
290
 
243
- // Calculate base port (without developer-id adjustment) for URL matching
244
- const baseAppPort = await calculateAppPort(variablesPath, localEnv, envContent, 0);
245
- // Calculate final port with developer-id adjustment
246
- const appPort = await calculateAppPort(variablesPath, localEnv, envContent, devIdNum);
247
-
248
- // Update .env content - only handle PORT variable
249
- // Other port variables (DB_PORT, REDIS_PORT, etc.) are handled by interpolation
250
- let updated = envContent;
291
+ return localEnv;
292
+ }
251
293
 
252
- // Update PORT
253
- if (/^PORT\s*=.*$/m.test(updated)) {
254
- updated = updated.replace(/^PORT\s*=\s*.*$/m, `PORT=${appPort}`);
255
- } else {
256
- updated = `${updated}\nPORT=${appPort}\n`;
294
+ /**
295
+ * Updates PORT variable in content
296
+ * @function updatePortVariable
297
+ * @param {string} envContent - Environment content
298
+ * @param {number} appPort - Application port
299
+ * @returns {string} Updated content
300
+ */
301
+ function updatePortVariable(envContent, appPort) {
302
+ if (/^PORT\s*=.*$/m.test(envContent)) {
303
+ return envContent.replace(/^PORT\s*=\s*.*$/m, `PORT=${appPort}`);
257
304
  }
305
+ return `${envContent}\nPORT=${appPort}\n`;
306
+ }
258
307
 
259
- // Update localhost URLs
260
- updated = updateLocalhostUrls(updated, baseAppPort, appPort);
261
-
262
- // Update infra endpoints with developer-id adjusted ports for local context
263
- updated = await rewriteInfraEndpoints(updated, 'local');
264
-
265
- // Interpolate ${VAR} references created by rewriteInfraEndpoints
266
- // Get the ports that were just set by rewriteInfraEndpoints for interpolation
308
+ /**
309
+ * Builds environment variables for interpolation
310
+ * @async
311
+ * @function buildEnvVarsForInterpolation
312
+ * @param {number} devIdNum - Developer ID number
313
+ * @returns {Promise<Object>} Environment variables object
314
+ */
315
+ async function buildEnvVarsForInterpolation(devIdNum) {
267
316
  const hostsForPorts = await getEnvHosts('local');
268
317
  const redisPort = await getServicePort('REDIS_PORT', 'redis', hostsForPorts, 'local');
269
318
  const dbPort = await getServicePort('DB_PORT', 'postgres', hostsForPorts, 'local');
@@ -271,13 +320,27 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
271
320
  const redisHost = getServiceHost(hostsForPorts.REDIS_HOST, 'local', 'localhost', localhostOverride);
272
321
  const dbHost = getServiceHost(hostsForPorts.DB_HOST, 'local', 'localhost', localhostOverride);
273
322
 
274
- // Build envVars map and ensure it has the correct values
275
323
  const envVars = await buildEnvVarMap('local', null, devIdNum);
276
- // Override with the actual values that were just set by rewriteInfraEndpoints
277
324
  envVars.REDIS_HOST = redisHost;
278
325
  envVars.REDIS_PORT = String(redisPort);
279
326
  envVars.DB_HOST = dbHost;
280
327
  envVars.DB_PORT = String(dbPort);
328
+
329
+ return envVars;
330
+ }
331
+
332
+ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
333
+ const devIdNum = await getDeveloperIdNumber();
334
+ const localEnv = await getLocalEnvWithOverrides();
335
+
336
+ const baseAppPort = await calculateAppPort(variablesPath, localEnv, envContent, 0);
337
+ const appPort = await calculateAppPort(variablesPath, localEnv, envContent, devIdNum);
338
+
339
+ let updated = updatePortVariable(envContent, appPort);
340
+ updated = updateLocalhostUrls(updated, baseAppPort, appPort);
341
+ updated = await rewriteInfraEndpoints(updated, 'local');
342
+
343
+ const envVars = await buildEnvVarsForInterpolation(devIdNum);
281
344
  updated = interpolateEnvVars(updated, envVars);
282
345
 
283
346
  return updated;
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  const path = require('path');
13
- const config = require('../config');
13
+ const config = require('../core/config');
14
14
  const paths = require('./paths');
15
15
 
16
16
  /**
@@ -26,7 +26,7 @@ async function resolveServicePortsInEnvContent(envContent, environment) {
26
26
  const envConfig = await loadEnvConfig();
27
27
  const dockerHosts = envConfig.environments.docker || {};
28
28
  const hostnameToService = buildHostnameToServiceMap(dockerHosts);
29
- const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
29
+ const urlPattern = /(https?:\/\/)([a-zA-Z0-9.-]+):(\d+)([^\s\n]*)?/g;
30
30
  return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
31
31
  return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
32
32
  });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Token Manager Refresh Utilities
3
+ *
4
+ * Token refresh functions for device and client tokens
5
+ *
6
+ * @fileoverview Token refresh utilities for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const config = require('../core/config');
12
+ const { refreshDeviceToken: apiRefreshDeviceToken } = require('./api');
13
+
14
+ /**
15
+ * Validates refresh token parameters
16
+ * @function validateRefreshTokenParams
17
+ * @param {string} environment - Environment key
18
+ * @param {string} appName - Application name
19
+ * @param {string} controllerUrl - Controller URL
20
+ * @throws {Error} If validation fails
21
+ */
22
+ function validateRefreshTokenParams(environment, appName, controllerUrl) {
23
+ if (!environment || typeof environment !== 'string') {
24
+ throw new Error('Environment is required and must be a string');
25
+ }
26
+ if (!appName || typeof appName !== 'string') {
27
+ throw new Error('App name is required and must be a string');
28
+ }
29
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
30
+ throw new Error('Controller URL is required and must be a string');
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Loads client credentials from parameters or secrets file
36
+ * @async
37
+ * @function loadClientCredentialsForRefresh
38
+ * @param {string} appName - Application name
39
+ * @param {string} [clientId] - Optional client ID
40
+ * @param {string} [clientSecret] - Optional client secret
41
+ * @returns {Promise<Object>} Credentials object with clientId and clientSecret
42
+ * @throws {Error} If credentials cannot be loaded
43
+ */
44
+ async function loadClientCredentialsForRefresh(appName, clientId, clientSecret) {
45
+ const { loadClientCredentials } = require('./token-manager');
46
+ if (clientId && clientSecret) {
47
+ return { clientId, clientSecret };
48
+ }
49
+ const credentials = await loadClientCredentials(appName);
50
+ if (!credentials) {
51
+ throw new Error(`Client credentials not found for app '${appName}'. Add them to ~/.aifabrix/secrets.local.yaml as '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`);
52
+ }
53
+ return credentials;
54
+ }
55
+
56
+ /**
57
+ * Calls token API to get new token
58
+ * @async
59
+ * @function callTokenApi
60
+ * @param {string} controllerUrl - Controller URL
61
+ * @param {Object} credentials - Credentials object
62
+ * @returns {Promise<Object>} API response
63
+ * @throws {Error} If API call fails
64
+ */
65
+ async function callTokenApi(controllerUrl, credentials) {
66
+ const { makeApiCall: _makeApiCall } = require('./api');
67
+ const response = await _makeApiCall(`${controllerUrl}/api/v1/auth/token`, {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ 'x-client-id': credentials.clientId,
72
+ 'x-client-secret': credentials.clientSecret
73
+ }
74
+ });
75
+
76
+ if (!response.success) {
77
+ throw new Error(`Failed to refresh token: ${response.error || 'Unknown error'}`);
78
+ }
79
+
80
+ const responseData = response.data;
81
+ if (!responseData || !responseData.token) {
82
+ throw new Error('Invalid response: missing token');
83
+ }
84
+
85
+ return responseData;
86
+ }
87
+
88
+ /**
89
+ * Calculates token expiration timestamp
90
+ * @function calculateTokenExpiration
91
+ * @param {Object} responseData - API response data
92
+ * @returns {string} ISO timestamp of expiration
93
+ */
94
+ function calculateTokenExpiration(responseData) {
95
+ const expiresIn = responseData.expiresIn || 86400;
96
+ return responseData.expiresAt || new Date(Date.now() + expiresIn * 1000).toISOString();
97
+ }
98
+
99
+ /**
100
+ * Refresh client token using credentials
101
+ * @async
102
+ * @function refreshClientToken
103
+ * @param {string} environment - Environment key
104
+ * @param {string} appName - Application name
105
+ * @param {string} controllerUrl - Controller URL
106
+ * @param {string} [clientId] - Optional client ID override
107
+ * @param {string} [clientSecret] - Optional client secret override
108
+ * @returns {Promise<{token: string, expiresAt: string}>} New token info
109
+ * @throws {Error} If credentials are missing or token refresh fails
110
+ */
111
+ async function refreshClientToken(environment, appName, controllerUrl, clientId, clientSecret) {
112
+ validateRefreshTokenParams(environment, appName, controllerUrl);
113
+
114
+ const credentials = await loadClientCredentialsForRefresh(appName, clientId, clientSecret);
115
+ const responseData = await callTokenApi(controllerUrl, credentials);
116
+
117
+ const token = responseData.token;
118
+ const expiresAt = calculateTokenExpiration(responseData);
119
+
120
+ // Save token to config.yaml (NEVER save credentials)
121
+ await config.saveClientToken(environment, appName, controllerUrl, token, expiresAt);
122
+
123
+ return { token, expiresAt };
124
+ }
125
+
126
+ /**
127
+ * Refresh device token using refresh token
128
+ * Calls API refresh endpoint and saves new token to config
129
+ * @param {string} controllerUrl - Controller URL
130
+ * @param {string} refreshToken - Refresh token
131
+ * @returns {Promise<{token: string, refreshToken: string, expiresAt: string}>} New token info
132
+ * @throws {Error} If refresh fails or refresh token is expired/invalid
133
+ */
134
+ async function refreshDeviceToken(controllerUrl, refreshToken) {
135
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
136
+ throw new Error('Controller URL is required');
137
+ }
138
+ if (!refreshToken || typeof refreshToken !== 'string') {
139
+ throw new Error('Refresh token is required');
140
+ }
141
+
142
+ try {
143
+ // Call API refresh endpoint
144
+ const tokenResponse = await apiRefreshDeviceToken(controllerUrl, refreshToken);
145
+
146
+ const token = tokenResponse.access_token;
147
+ const newRefreshToken = tokenResponse.refresh_token || refreshToken; // Use new refresh token if provided, otherwise keep old one
148
+ const expiresIn = tokenResponse.expires_in || 3600;
149
+ const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
150
+
151
+ // Save new token and refresh token to config
152
+ await config.saveDeviceToken(controllerUrl, token, newRefreshToken, expiresAt);
153
+
154
+ return {
155
+ token,
156
+ refreshToken: newRefreshToken,
157
+ expiresAt
158
+ };
159
+ } catch (error) {
160
+ // Check if error indicates refresh token expiry (case-insensitive)
161
+ const errorMessage = (error.message || String(error)).toLowerCase();
162
+ if (errorMessage.includes('expired') ||
163
+ errorMessage.includes('invalid') ||
164
+ errorMessage.includes('401') ||
165
+ errorMessage.includes('unauthorized')) {
166
+ throw new Error('Refresh token has expired. Please login again using: aifabrix login');
167
+ }
168
+ // Re-throw other errors as-is
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ module.exports = {
174
+ refreshClientToken,
175
+ refreshDeviceToken,
176
+ validateRefreshTokenParams,
177
+ loadClientCredentialsForRefresh,
178
+ callTokenApi,
179
+ calculateTokenExpiration
180
+ };
181
+