@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.
- package/.cursor/rules/docs-rules.mdc +30 -0
- package/README.md +7 -5
- package/integration/hubspot/README.md +8 -4
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +2 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +34 -1
- package/lib/app/config.js +44 -11
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +12 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +9 -6
- package/lib/app/run-env-compose.js +264 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show-display.js +1 -1
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +172 -15
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +206 -16
- package/lib/cli/setup-environment.js +16 -6
- package/lib/cli/setup-external-system.js +89 -24
- package/lib/cli/setup-infra.js +82 -15
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +129 -24
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +347 -0
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +507 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +96 -40
- package/lib/commands/wizard-core-helpers.js +226 -4
- package/lib/commands/wizard-core.js +67 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +44 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +86 -64
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/config.js +7 -1
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +176 -89
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/deployer.js +7 -5
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +188 -203
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +56 -19
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +1 -1
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +177 -25
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +294 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +88 -0
- package/lib/generator/wizard.js +155 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +98 -12
- package/lib/infrastructure/services.js +88 -22
- package/lib/schema/application-schema.json +32 -8
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +509 -411
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +41 -13
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +77 -76
- package/lib/utils/compose-handlebars-helpers.js +54 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +357 -0
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +103 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -2
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +56 -29
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +16 -2
- package/lib/utils/infra-status.js +80 -45
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +128 -37
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +114 -6
- package/lib/utils/secrets-helpers.js +108 -114
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +72 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +8 -3
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +6 -5
- package/templates/applications/dataplane/env.template +15 -10
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +12 -10
- package/templates/external-system/README.md.hbs +65 -25
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
- 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
|
+
};
|
package/lib/utils/device-code.js
CHANGED
|
@@ -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(
|
|
450
|
-
logger.log(chalk.gray(' 2.
|
|
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
|
};
|