@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,224 @@
1
+ /**
2
+ * Device code flow parsing, error handling, and polling helpers.
3
+ * Used by device-code.js; not part of the public API.
4
+ *
5
+ * @fileoverview Helpers for device code flow (RFC 8628)
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ /**
11
+ * Parses device code response from API
12
+ * @param {Object} response - API response object
13
+ * @returns {Object} Parsed device code response
14
+ */
15
+ function parseDeviceCodeResponse(response) {
16
+ const apiResponse = response.data;
17
+ const responseData = apiResponse.data || apiResponse;
18
+ const deviceCode = responseData.deviceCode;
19
+ const userCode = responseData.userCode;
20
+ const verificationUri = responseData.verificationUri;
21
+ const expiresIn = responseData.expiresIn || 600;
22
+ const interval = responseData.interval || 5;
23
+
24
+ if (!deviceCode || !userCode || !verificationUri) {
25
+ throw new Error('Invalid device code response: missing required fields');
26
+ }
27
+
28
+ return {
29
+ device_code: deviceCode,
30
+ user_code: userCode,
31
+ verification_uri: verificationUri,
32
+ expires_in: expiresIn,
33
+ interval: interval
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Parses token response from API
39
+ * @param {Object} response - API response object
40
+ * @returns {Object|null} Parsed token response or null if pending
41
+ */
42
+ function parseTokenResponse(response) {
43
+ const apiResponse = response.data;
44
+ const responseData = apiResponse.data || apiResponse;
45
+ const error = responseData.error || apiResponse.error;
46
+ if (error === 'authorization_pending' || error === 'slow_down') {
47
+ return null;
48
+ }
49
+
50
+ const accessToken = responseData.accessToken;
51
+ const refreshToken = responseData.refreshToken;
52
+ const expiresIn = responseData.expiresIn || 3600;
53
+
54
+ if (!accessToken) {
55
+ throw new Error('Invalid token response: missing accessToken');
56
+ }
57
+
58
+ return {
59
+ access_token: accessToken,
60
+ refresh_token: refreshToken,
61
+ expires_in: expiresIn
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Checks if token has expired based on elapsed time
67
+ * @param {number} startTime - Start time in milliseconds
68
+ * @param {number} expiresIn - Expiration time in seconds
69
+ */
70
+ function checkTokenExpiration(startTime, expiresIn) {
71
+ const maxWaitTime = (expiresIn + 30) * 1000;
72
+ if (Date.now() - startTime > maxWaitTime) {
73
+ throw new Error('Device code expired: Maximum polling time exceeded');
74
+ }
75
+ }
76
+
77
+ function attachFormattedError(validationError, response) {
78
+ if (response && response.formattedError) {
79
+ validationError.formattedError = response.formattedError;
80
+ validationError.message = `Token polling failed:\n${response.formattedError}`;
81
+ }
82
+ }
83
+
84
+ function buildDetailedErrorMessage(errorData) {
85
+ const detail = errorData.detail || errorData.title || errorData.message || 'Validation error';
86
+ let errorMsg = `Token polling failed: ${detail}`;
87
+ if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) {
88
+ errorMsg += '\n\nValidation errors:';
89
+ errorData.errors.forEach(err => {
90
+ const field = err.field || err.path || 'validation';
91
+ const message = err.message || 'Invalid value';
92
+ errorMsg += (field === 'validation' || field === 'unknown')
93
+ ? `\n • ${message}` : `\n • ${field}: ${message}`;
94
+ });
95
+ }
96
+ return errorMsg;
97
+ }
98
+
99
+ function attachErrorData(validationError, response) {
100
+ if (response && response.errorData) {
101
+ validationError.errorData = response.errorData;
102
+ validationError.errorType = response.errorType || 'validation';
103
+ if (!validationError.formattedError) {
104
+ validationError.message = buildDetailedErrorMessage(response.errorData);
105
+ }
106
+ }
107
+ }
108
+
109
+ function createValidationError(response) {
110
+ const validationError = new Error('Token polling failed: Validation error');
111
+ attachFormattedError(validationError, response);
112
+ attachErrorData(validationError, response);
113
+ return validationError;
114
+ }
115
+
116
+ /**
117
+ * Handles polling errors; throws for fatal errors, returns true to continue polling.
118
+ * @param {string} error - Error code
119
+ * @param {number} status - HTTP status code
120
+ * @param {Object} response - Full API response object
121
+ * @returns {boolean} True if should continue polling
122
+ */
123
+ function handlePollingErrors(error, status, response) {
124
+ if (error === 'authorization_pending' || status === 202) {
125
+ return true;
126
+ }
127
+ if (error === 'authorization_declined') {
128
+ throw new Error('Authorization declined: User denied the request');
129
+ }
130
+ if (error === 'expired_token' || status === 410) {
131
+ throw new Error('Device code expired: Please restart the authentication process');
132
+ }
133
+ if (error === 'slow_down') {
134
+ return true;
135
+ }
136
+ if (error === 'validation_error' || status === 400 ||
137
+ error === 'INVALID_TOKEN' || error === 'INVALID_ACCESS_TOKEN') {
138
+ throw createValidationError(response);
139
+ }
140
+ throw new Error(`Token polling failed: ${error}`);
141
+ }
142
+
143
+ async function waitForNextPoll(interval, slowDown) {
144
+ const waitInterval = slowDown ? interval * 2 : interval;
145
+ await new Promise(resolve => setTimeout(resolve, waitInterval * 1000));
146
+ }
147
+
148
+ function isValidationErrorCode(errorCode) {
149
+ return errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN';
150
+ }
151
+
152
+ function extractStructuredError(response) {
153
+ if (response.errorType === 'validation') {
154
+ return 'validation_error';
155
+ }
156
+ const errorData = response.errorData;
157
+ const errorCode = errorData.error || errorData.code || response.error;
158
+ if (isValidationErrorCode(errorCode)) {
159
+ return 'validation_error';
160
+ }
161
+ return errorData.detail || errorData.title || errorData.message || errorCode || response.error || 'Unknown error';
162
+ }
163
+
164
+ function extractFallbackError(response) {
165
+ const apiResponse = response.data || {};
166
+ const errorData = typeof apiResponse === 'object' ? apiResponse : {};
167
+ const errorCode = errorData.error || response.error || 'Unknown error';
168
+ if (isValidationErrorCode(errorCode)) {
169
+ return 'validation_error';
170
+ }
171
+ return errorCode;
172
+ }
173
+
174
+ function extractPollingError(response) {
175
+ if (response.errorData) {
176
+ return extractStructuredError(response);
177
+ }
178
+ return extractFallbackError(response);
179
+ }
180
+
181
+ function handleSuccessfulPoll(response) {
182
+ const tokenResponse = parseTokenResponse(response);
183
+ return tokenResponse || null;
184
+ }
185
+
186
+ /**
187
+ * Processes polling response and determines next action.
188
+ * @param {Object} response - API response object
189
+ * @param {number} interval - Polling interval in seconds
190
+ * @returns {Promise<Object|null>} Token response if complete, null if should continue
191
+ */
192
+ async function processPollingResponse(response, interval) {
193
+ if (response.success) {
194
+ const apiResponse = response.data || {};
195
+ const responseData = apiResponse.data || apiResponse;
196
+ const errorCode = responseData.error || apiResponse.error || response.error;
197
+ if (errorCode && (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN')) {
198
+ throw createValidationError(response);
199
+ }
200
+ const tokenResponse = handleSuccessfulPoll(response);
201
+ if (tokenResponse) {
202
+ return tokenResponse;
203
+ }
204
+ const error = errorCode;
205
+ const slowDown = error === 'slow_down';
206
+ await waitForNextPoll(interval, slowDown);
207
+ return null;
208
+ }
209
+
210
+ const error = extractPollingError(response);
211
+ const shouldContinue = handlePollingErrors(error, response.status, response);
212
+ if (shouldContinue) {
213
+ await waitForNextPoll(interval, error === 'slow_down');
214
+ return null;
215
+ }
216
+ return null;
217
+ }
218
+
219
+ module.exports = {
220
+ parseDeviceCodeResponse,
221
+ parseTokenResponse,
222
+ checkTokenExpiration,
223
+ processPollingResponse
224
+ };
@@ -9,6 +9,13 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
+ const {
13
+ parseDeviceCodeResponse,
14
+ parseTokenResponse,
15
+ checkTokenExpiration,
16
+ processPollingResponse
17
+ } = require('./device-code-helpers');
18
+
12
19
  // Lazy require to avoid circular dependency
13
20
  let makeApiCall;
14
21
  function getMakeApiCall() {
@@ -19,40 +26,6 @@ function getMakeApiCall() {
19
26
  return makeApiCall;
20
27
  }
21
28
 
22
- /**
23
- * Parses device code response from API
24
- * Matches OpenAPI DeviceCodeResponse schema (camelCase)
25
- * @function parseDeviceCodeResponse
26
- * @param {Object} response - API response object
27
- * @returns {Object} Parsed device code response
28
- * @throws {Error} If response is invalid
29
- */
30
- function parseDeviceCodeResponse(response) {
31
- // OpenAPI spec: { success: boolean, data: DeviceCodeResponse, timestamp: string }
32
- const apiResponse = response.data;
33
- const responseData = apiResponse.data || apiResponse;
34
-
35
- // OpenAPI spec uses camelCase: deviceCode, userCode, verificationUri, expiresIn, interval
36
- const deviceCode = responseData.deviceCode;
37
- const userCode = responseData.userCode;
38
- const verificationUri = responseData.verificationUri;
39
- const expiresIn = responseData.expiresIn || 600;
40
- const interval = responseData.interval || 5;
41
-
42
- if (!deviceCode || !userCode || !verificationUri) {
43
- throw new Error('Invalid device code response: missing required fields');
44
- }
45
-
46
- // Return in snake_case for internal consistency (used by existing code)
47
- return {
48
- device_code: deviceCode,
49
- user_code: userCode,
50
- verification_uri: verificationUri,
51
- expires_in: expiresIn,
52
- interval: interval
53
- };
54
- }
55
-
56
29
  /**
57
30
  * Initiates OAuth2 Device Code Flow
58
31
  * Calls the device code endpoint to get device_code and user_code
@@ -70,7 +43,6 @@ async function initiateDeviceCodeFlow(controllerUrl, environment, scope) {
70
43
  throw new Error('Environment key is required');
71
44
  }
72
45
 
73
- // Default scope for backward compatibility
74
46
  const defaultScope = 'openid profile email';
75
47
  const requestScope = scope || defaultScope;
76
48
 
@@ -92,298 +64,6 @@ async function initiateDeviceCodeFlow(controllerUrl, environment, scope) {
92
64
  return parseDeviceCodeResponse(response);
93
65
  }
94
66
 
95
- /**
96
- * Checks if token has expired based on elapsed time
97
- * @function checkTokenExpiration
98
- * @param {number} startTime - Start time in milliseconds
99
- * @param {number} expiresIn - Expiration time in seconds
100
- * @throws {Error} If token has expired
101
- */
102
- function checkTokenExpiration(startTime, expiresIn) {
103
- const maxWaitTime = (expiresIn + 30) * 1000;
104
- if (Date.now() - startTime > maxWaitTime) {
105
- throw new Error('Device code expired: Maximum polling time exceeded');
106
- }
107
- }
108
-
109
- /**
110
- * Parses token response from API
111
- * Matches OpenAPI DeviceCodeTokenResponse schema (camelCase)
112
- * @function parseTokenResponse
113
- * @param {Object} response - API response object
114
- * @returns {Object|null} Parsed token response or null if pending
115
- */
116
- function parseTokenResponse(response) {
117
- // OpenAPI spec: { success: boolean, data: DeviceCodeTokenResponse, timestamp: string }
118
- const apiResponse = response.data;
119
- const responseData = apiResponse.data || apiResponse;
120
-
121
- const error = responseData.error || apiResponse.error;
122
- if (error === 'authorization_pending' || error === 'slow_down') {
123
- return null;
124
- }
125
-
126
- // OpenAPI spec uses camelCase: accessToken, refreshToken, expiresIn
127
- const accessToken = responseData.accessToken;
128
- const refreshToken = responseData.refreshToken;
129
- const expiresIn = responseData.expiresIn || 3600;
130
-
131
- if (!accessToken) {
132
- throw new Error('Invalid token response: missing accessToken');
133
- }
134
-
135
- // Return in snake_case for internal consistency (used by existing code)
136
- return {
137
- access_token: accessToken,
138
- refresh_token: refreshToken,
139
- expires_in: expiresIn
140
- };
141
- }
142
-
143
- /**
144
- * Creates a validation error with detailed information
145
- * @function createValidationError
146
- * @param {Object} response - Full API response object
147
- * @returns {Error} Validation error with formattedError and errorData attached
148
- */
149
- /**
150
- * Attaches formatted error to validation error
151
- * @function attachFormattedError
152
- * @param {Error} validationError - Validation error object
153
- * @param {Object} response - API response
154
- */
155
- function attachFormattedError(validationError, response) {
156
- if (response && response.formattedError) {
157
- validationError.formattedError = response.formattedError;
158
- validationError.message = `Token polling failed:\n${response.formattedError}`;
159
- }
160
- }
161
-
162
- /**
163
- * Builds detailed error message from error data
164
- * @function buildDetailedErrorMessage
165
- * @param {Object} errorData - Error data object
166
- * @returns {string} Detailed error message
167
- */
168
- function buildDetailedErrorMessage(errorData) {
169
- const detail = errorData.detail || errorData.title || errorData.message || 'Validation error';
170
- let errorMsg = `Token polling failed: ${detail}`;
171
-
172
- if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) {
173
- errorMsg += '\n\nValidation errors:';
174
- errorData.errors.forEach(err => {
175
- const field = err.field || err.path || 'validation';
176
- const message = err.message || 'Invalid value';
177
- if (field === 'validation' || field === 'unknown') {
178
- errorMsg += `\n • ${message}`;
179
- } else {
180
- errorMsg += `\n • ${field}: ${message}`;
181
- }
182
- });
183
- }
184
-
185
- return errorMsg;
186
- }
187
-
188
- /**
189
- * Attaches error data to validation error
190
- * @function attachErrorData
191
- * @param {Error} validationError - Validation error object
192
- * @param {Object} response - API response
193
- */
194
- function attachErrorData(validationError, response) {
195
- if (response && response.errorData) {
196
- validationError.errorData = response.errorData;
197
- validationError.errorType = response.errorType || 'validation';
198
-
199
- if (!validationError.formattedError) {
200
- validationError.message = buildDetailedErrorMessage(response.errorData);
201
- }
202
- }
203
- }
204
-
205
- function createValidationError(response) {
206
- const validationError = new Error('Token polling failed: Validation error');
207
-
208
- attachFormattedError(validationError, response);
209
- attachErrorData(validationError, response);
210
-
211
- return validationError;
212
- }
213
-
214
- /**
215
- * Handles polling errors
216
- * @function handlePollingErrors
217
- * @param {string} error - Error code
218
- * @param {number} status - HTTP status code
219
- * @param {Object} response - Full API response object (for accessing formattedError and errorData)
220
- * @throws {Error} For fatal errors
221
- * @returns {boolean} True if should continue polling
222
- */
223
- function handlePollingErrors(error, status, response) {
224
- if (error === 'authorization_pending' || status === 202) {
225
- return true;
226
- }
227
-
228
- // Check error field first, then status code
229
- if (error === 'authorization_declined') {
230
- throw new Error('Authorization declined: User denied the request');
231
- }
232
-
233
- if (error === 'expired_token' || status === 410) {
234
- throw new Error('Device code expired: Please restart the authentication process');
235
- }
236
-
237
- if (error === 'slow_down') {
238
- return true;
239
- }
240
-
241
- // Handle validation errors with detailed message
242
- // Check for validation_error, status 400, or specific validation error codes
243
- if (error === 'validation_error' || status === 400 ||
244
- error === 'INVALID_TOKEN' || error === 'INVALID_ACCESS_TOKEN') {
245
- throw createValidationError(response);
246
- }
247
-
248
- throw new Error(`Token polling failed: ${error}`);
249
- }
250
-
251
- /**
252
- * Waits for next polling interval
253
- * @async
254
- * @function waitForNextPoll
255
- * @param {number} interval - Polling interval in seconds
256
- * @param {boolean} slowDown - Whether to slow down
257
- */
258
- async function waitForNextPoll(interval, slowDown) {
259
- const waitInterval = slowDown ? interval * 2 : interval;
260
- await new Promise(resolve => setTimeout(resolve, waitInterval * 1000));
261
- }
262
-
263
- /**
264
- * Extracts error from API response
265
- * @param {Object} response - API response object
266
- * @returns {string} Error code or 'Unknown error'
267
- */
268
- /**
269
- * Checks if error code indicates validation error
270
- * @function isValidationErrorCode
271
- * @param {string} errorCode - Error code to check
272
- * @returns {boolean} True if validation error
273
- */
274
- function isValidationErrorCode(errorCode) {
275
- return errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN';
276
- }
277
-
278
- /**
279
- * Extracts error from structured error data
280
- * @function extractStructuredError
281
- * @param {Object} response - API response with errorData
282
- * @returns {string} Error message or code
283
- */
284
- function extractStructuredError(response) {
285
- const errorData = response.errorData;
286
-
287
- // For validation errors, return the error type so we can handle it specially
288
- if (response.errorType === 'validation') {
289
- return 'validation_error';
290
- }
291
-
292
- // Check if error code indicates validation error (e.g., INVALID_TOKEN)
293
- const errorCode = errorData.error || errorData.code || response.error;
294
- if (isValidationErrorCode(errorCode)) {
295
- return 'validation_error';
296
- }
297
-
298
- // Return the error message from structured error
299
- return errorData.detail || errorData.title || errorData.message || errorCode || response.error || 'Unknown error';
300
- }
301
-
302
- /**
303
- * Extracts error from fallback response structure
304
- * @function extractFallbackError
305
- * @param {Object} response - API response
306
- * @returns {string} Error code
307
- */
308
- function extractFallbackError(response) {
309
- const apiResponse = response.data || {};
310
- const errorData = typeof apiResponse === 'object' ? apiResponse : {};
311
- const errorCode = errorData.error || response.error || 'Unknown error';
312
-
313
- // Check if error code indicates validation error (e.g., INVALID_TOKEN)
314
- if (isValidationErrorCode(errorCode)) {
315
- return 'validation_error';
316
- }
317
-
318
- return errorCode;
319
- }
320
-
321
- function extractPollingError(response) {
322
- // Check for structured error data first (from api-error-handler)
323
- if (response.errorData) {
324
- return extractStructuredError(response);
325
- }
326
-
327
- // Fallback to original extraction logic
328
- return extractFallbackError(response);
329
- }
330
-
331
- /**
332
- * Handles successful polling response
333
- * @param {Object} response - API response object
334
- * @returns {Object|null} Token response or null if pending
335
- */
336
- function handleSuccessfulPoll(response) {
337
- const tokenResponse = parseTokenResponse(response);
338
- if (tokenResponse) {
339
- return tokenResponse;
340
- }
341
- return null;
342
- }
343
-
344
- /**
345
- * Processes polling response and determines next action
346
- * @async
347
- * @function processPollingResponse
348
- * @param {Object} response - API response object
349
- * @param {number} interval - Polling interval in seconds
350
- * @returns {Promise<Object|null>} Token response if complete, null if should continue
351
- */
352
- async function processPollingResponse(response, interval) {
353
- if (response.success) {
354
- // Check if response contains an error code even though success is true
355
- const apiResponse = response.data || {};
356
- const responseData = apiResponse.data || apiResponse;
357
- const errorCode = responseData.error || apiResponse.error || response.error;
358
-
359
- // If there's an error code like INVALID_TOKEN, treat it as a validation error
360
- if (errorCode && (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN')) {
361
- throw createValidationError(response);
362
- }
363
-
364
- const tokenResponse = handleSuccessfulPoll(response);
365
- if (tokenResponse) {
366
- return tokenResponse;
367
- }
368
-
369
- const error = errorCode;
370
- const slowDown = error === 'slow_down';
371
- await waitForNextPoll(interval, slowDown);
372
- return null;
373
- }
374
-
375
- const error = extractPollingError(response);
376
- const shouldContinue = handlePollingErrors(error, response.status, response);
377
-
378
- if (shouldContinue) {
379
- const slowDown = error === 'slow_down';
380
- await waitForNextPoll(interval, slowDown);
381
- return null;
382
- }
383
-
384
- return null;
385
- }
386
-
387
67
  /**
388
68
  * Polls for token during Device Code Flow
389
69
  * Continuously polls the token endpoint until user approves or flow expires
@@ -431,27 +111,47 @@ async function pollDeviceCodeToken(controllerUrl, deviceCode, interval, expiresI
431
111
  }
432
112
  }
433
113
 
114
+ /**
115
+ * Builds verification URL with user_code query parameter so the device page can pre-fill the code.
116
+ *
117
+ * @function buildVerificationUrlWithUserCode
118
+ * @param {string} verificationUri - Base verification URL (e.g. http://localhost:8182/realms/aifabrix/device)
119
+ * @param {string} userCode - User code to append as user_code query param
120
+ * @returns {string} Full URL with ?user_code=<code> or &user_code=<code>
121
+ */
122
+ function buildVerificationUrlWithUserCode(verificationUri, userCode) {
123
+ if (!verificationUri || !userCode) {
124
+ return verificationUri || '';
125
+ }
126
+ const separator = verificationUri.includes('?') ? '&' : '?';
127
+ return `${verificationUri}${separator}user_code=${encodeURIComponent(userCode)}`;
128
+ }
129
+
434
130
  /**
435
131
  * Displays device code information to the user
436
- * Formats user code and verification URL for easy reading
132
+ * Formats user code and verification URL for easy reading. Uses a URL with user_code in the query
133
+ * so the device page can pre-fill the code and the user does not need to type it.
437
134
  *
438
135
  * @function displayDeviceCodeInfo
439
136
  * @param {string} userCode - User code to display
440
- * @param {string} verificationUri - Verification URL
137
+ * @param {string} verificationUri - Verification URL (base, without user_code)
441
138
  * @param {Object} logger - Logger instance with log method
442
139
  * @param {Object} chalk - Chalk instance for colored output
443
140
  */
444
141
  function displayDeviceCodeInfo(userCode, verificationUri, logger, chalk) {
142
+ const visitUrl = buildVerificationUrlWithUserCode(verificationUri, userCode);
445
143
  logger.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
446
144
  logger.log(chalk.cyan(' Device Code Flow Authentication'));
447
145
  logger.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
448
146
  logger.log(chalk.yellow('To complete authentication:'));
449
- logger.log(chalk.gray(' 1. Visit: ') + chalk.blue.underline(verificationUri));
450
- logger.log(chalk.gray(' 2. Enter code: ') + chalk.bold.cyan(userCode));
451
- logger.log(chalk.gray(' 3. Approve the request\n'));
147
+ logger.log(chalk.gray(' 1. Visit (code is in the URL): ') + chalk.blue.underline(visitUrl));
148
+ logger.log(chalk.gray(' 2. Approve the request\n'));
452
149
  logger.log(chalk.gray('Waiting for approval...'));
453
150
  }
454
151
 
152
+ /** Timeout for token refresh request (ms). Longer than default to allow for slow controller/Keycloak. */
153
+ const REFRESH_TOKEN_TIMEOUT_MS = 60000;
154
+
455
155
  /**
456
156
  * Refresh device code access token using refresh token
457
157
  * Uses OpenAPI /api/v1/auth/login/device/refresh endpoint
@@ -469,13 +169,13 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
469
169
  }
470
170
 
471
171
  const url = `${controllerUrl}/api/v1/auth/login/device/refresh`;
472
- // Send both refresh_token (OAuth2 RFC 6749 / Keycloak) and refreshToken (camelCase) so controller accepts either
473
172
  const response = await getMakeApiCall()(url, {
474
173
  method: 'POST',
475
174
  headers: {
476
175
  'Content-Type': 'application/json'
477
176
  },
478
- body: JSON.stringify({ refresh_token: refreshToken, refreshToken })
177
+ body: JSON.stringify({ refresh_token: refreshToken, refreshToken }),
178
+ signal: AbortSignal.timeout(REFRESH_TOKEN_TIMEOUT_MS)
479
179
  });
480
180
 
481
181
  if (!response.success) {
@@ -483,7 +183,6 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
483
183
  throw new Error(`Failed to refresh token: ${errorMsg}`);
484
184
  }
485
185
 
486
- // Parse response using existing parseTokenResponse function
487
186
  const tokenResponse = parseTokenResponse(response);
488
187
  if (!tokenResponse) {
489
188
  throw new Error('Invalid refresh token response');
@@ -491,10 +190,12 @@ async function refreshDeviceToken(controllerUrl, refreshToken) {
491
190
 
492
191
  return tokenResponse;
493
192
  }
193
+
494
194
  module.exports = {
495
195
  initiateDeviceCodeFlow,
496
196
  pollDeviceCodeToken,
497
197
  displayDeviceCodeInfo,
498
198
  refreshDeviceToken,
499
- parseTokenResponse
199
+ parseTokenResponse,
200
+ buildVerificationUrlWithUserCode
500
201
  };