@aifabrix/builder 2.21.0 → 2.22.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/lib/app-list.js +60 -22
- package/lib/app-register.js +3 -2
- package/lib/app-rotate-secret.js +86 -48
- package/lib/cli.js +28 -28
- package/lib/commands/app.js +3 -0
- package/lib/config.js +40 -173
- package/lib/external-system-generator.js +3 -1
- package/lib/generator-external.js +229 -0
- package/lib/generator-helpers.js +205 -0
- package/lib/generator.js +2 -367
- package/lib/schema/external-system.schema.json +92 -1
- package/lib/utils/api-error-handler.js +9 -2
- package/lib/utils/app-register-api.js +39 -29
- package/lib/utils/app-register-auth.js +103 -39
- package/lib/utils/config-paths.js +112 -0
- package/lib/utils/config-tokens.js +233 -0
- package/lib/utils/device-code.js +28 -6
- package/lib/utils/error-formatters/http-status-errors.js +78 -5
- package/lib/utils/error-formatters/network-errors.js +24 -4
- package/lib/validate.js +67 -7
- package/lib/validator.js +3 -1
- package/package.json +1 -1
- package/templates/external-system/external-system.json.hbs +20 -1
|
@@ -10,62 +10,126 @@
|
|
|
10
10
|
|
|
11
11
|
const chalk = require('chalk');
|
|
12
12
|
const logger = require('./logger');
|
|
13
|
-
const { getConfig } = require('../config');
|
|
13
|
+
const { getConfig, normalizeControllerUrl } = require('../config');
|
|
14
14
|
const { getOrRefreshDeviceToken } = require('./token-manager');
|
|
15
|
+
const { formatAuthenticationError } = require('./error-formatters/http-status-errors');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Display authentication error and exit
|
|
19
|
+
* Uses the centralized error formatter for consistent error messages
|
|
20
|
+
* @param {Error|null} [error] - Optional error object
|
|
21
|
+
* @param {Object|string} [controllerUrlOrData] - Optional controller URL string or error data object
|
|
22
|
+
*/
|
|
23
|
+
function displayAuthenticationError(error = null, controllerUrlOrData = null) {
|
|
24
|
+
// Build error data object for the formatter
|
|
25
|
+
let errorData;
|
|
26
|
+
if (typeof controllerUrlOrData === 'object' && controllerUrlOrData !== null) {
|
|
27
|
+
// If it's an object, use it directly (may contain attemptedUrls, etc.)
|
|
28
|
+
errorData = {
|
|
29
|
+
message: error ? error.message : controllerUrlOrData.message,
|
|
30
|
+
controllerUrl: controllerUrlOrData.controllerUrl || undefined,
|
|
31
|
+
attemptedUrls: controllerUrlOrData.attemptedUrls || undefined,
|
|
32
|
+
correlationId: controllerUrlOrData.correlationId || undefined
|
|
33
|
+
};
|
|
34
|
+
} else {
|
|
35
|
+
// If it's a string or null, treat as controllerUrl
|
|
36
|
+
errorData = {
|
|
37
|
+
message: error ? error.message : undefined,
|
|
38
|
+
controllerUrl: controllerUrlOrData || undefined,
|
|
39
|
+
correlationId: undefined
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Use centralized formatter (it will include controller URL in the command)
|
|
44
|
+
const formattedError = formatAuthenticationError(errorData);
|
|
45
|
+
logger.error(formattedError);
|
|
46
|
+
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
15
49
|
|
|
16
50
|
/**
|
|
17
51
|
* Check if user is authenticated and get token
|
|
18
52
|
* @async
|
|
19
|
-
* @param {string} [controllerUrl] - Optional controller URL from variables.yaml
|
|
53
|
+
* @param {string} [controllerUrl] - Optional controller URL from variables.yaml or --controller flag
|
|
20
54
|
* @param {string} [environment] - Optional environment key
|
|
21
|
-
* @returns {Promise<{apiUrl: string, token: string}>} Configuration with API URL and
|
|
55
|
+
* @returns {Promise<{apiUrl: string, token: string, controllerUrl: string}>} Configuration with API URL, token, and controller URL
|
|
22
56
|
*/
|
|
23
57
|
async function checkAuthentication(controllerUrl, environment) {
|
|
24
|
-
|
|
58
|
+
try {
|
|
59
|
+
const config = await getConfig();
|
|
25
60
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
61
|
+
// Try to get controller URL from parameter, config, or device tokens
|
|
62
|
+
// Handle empty string as falsy (treat same as undefined/null)
|
|
63
|
+
const normalizedControllerUrl = (controllerUrl && controllerUrl.trim()) ? normalizeControllerUrl(controllerUrl) : null;
|
|
64
|
+
let finalControllerUrl = normalizedControllerUrl;
|
|
65
|
+
let token = null;
|
|
66
|
+
let lastError = null;
|
|
67
|
+
const attemptedUrls = []; // Track all attempted URLs
|
|
29
68
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
69
|
+
// If controller URL provided, try to get device token
|
|
70
|
+
if (finalControllerUrl) {
|
|
71
|
+
attemptedUrls.push(finalControllerUrl);
|
|
72
|
+
try {
|
|
73
|
+
const deviceToken = await getOrRefreshDeviceToken(finalControllerUrl);
|
|
74
|
+
if (deviceToken && deviceToken.token) {
|
|
75
|
+
token = deviceToken.token;
|
|
76
|
+
finalControllerUrl = deviceToken.controller || finalControllerUrl;
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
lastError = error;
|
|
80
|
+
logger.warn(chalk.yellow(`⚠️ Failed to get token for controller ${finalControllerUrl}: ${error.message}`));
|
|
81
|
+
}
|
|
36
82
|
}
|
|
37
|
-
}
|
|
38
83
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
84
|
+
// If no token yet, try to find any device token in config
|
|
85
|
+
if (!token && config.device) {
|
|
86
|
+
const deviceUrls = Object.keys(config.device);
|
|
87
|
+
if (deviceUrls.length > 0) {
|
|
88
|
+
// Try each device token until we find a valid one
|
|
89
|
+
for (const storedUrl of deviceUrls) {
|
|
90
|
+
attemptedUrls.push(storedUrl);
|
|
91
|
+
try {
|
|
92
|
+
const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
|
|
93
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
|
|
94
|
+
if (deviceToken && deviceToken.token) {
|
|
95
|
+
token = deviceToken.token;
|
|
96
|
+
finalControllerUrl = deviceToken.controller || normalizedStoredUrl;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
lastError = error;
|
|
101
|
+
// Continue to next URL
|
|
102
|
+
}
|
|
103
|
+
}
|
|
49
104
|
}
|
|
50
105
|
}
|
|
51
|
-
}
|
|
52
106
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
107
|
+
// If still no token, check for client token (requires environment and app)
|
|
108
|
+
if (!token && environment) {
|
|
109
|
+
// For app register, we don't have an app yet, so client tokens won't work
|
|
110
|
+
// This is expected - device tokens should be used for registration
|
|
111
|
+
}
|
|
58
112
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
113
|
+
// If no token found, display error with attempted URLs
|
|
114
|
+
if (!token || !finalControllerUrl) {
|
|
115
|
+
const errorData = {
|
|
116
|
+
message: lastError ? lastError.message : 'No valid authentication found',
|
|
117
|
+
controllerUrl: controllerUrl || (attemptedUrls.length > 0 ? attemptedUrls[0] : undefined),
|
|
118
|
+
attemptedUrls: attemptedUrls.length > 1 ? attemptedUrls : undefined,
|
|
119
|
+
correlationId: undefined
|
|
120
|
+
};
|
|
121
|
+
displayAuthenticationError(lastError, errorData);
|
|
122
|
+
}
|
|
64
123
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
124
|
+
return {
|
|
125
|
+
apiUrl: finalControllerUrl,
|
|
126
|
+
token: token,
|
|
127
|
+
controllerUrl: finalControllerUrl // Return the actual URL used
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Handle any unexpected errors during authentication check
|
|
131
|
+
displayAuthenticationError(error, { controllerUrl: controllerUrl });
|
|
132
|
+
}
|
|
69
133
|
}
|
|
70
134
|
|
|
71
135
|
module.exports = { checkAuthentication };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder - Configuration Path Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for managing path configuration in config.yaml
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Path configuration utilities for config management
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get path configuration value
|
|
13
|
+
* @async
|
|
14
|
+
* @param {Function} getConfigFn - Function to get config
|
|
15
|
+
* @param {string} key - Configuration key
|
|
16
|
+
* @returns {Promise<string|null>} Path value or null
|
|
17
|
+
*/
|
|
18
|
+
async function getPathConfig(getConfigFn, key) {
|
|
19
|
+
const config = await getConfigFn();
|
|
20
|
+
return config[key] || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set path configuration value
|
|
25
|
+
* @async
|
|
26
|
+
* @param {Function} getConfigFn - Function to get config
|
|
27
|
+
* @param {Function} saveConfigFn - Function to save config
|
|
28
|
+
* @param {string} key - Configuration key
|
|
29
|
+
* @param {string} value - Path value
|
|
30
|
+
* @param {string} errorMsg - Error message if validation fails
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
async function setPathConfig(getConfigFn, saveConfigFn, key, value, errorMsg) {
|
|
34
|
+
if (!value || typeof value !== 'string') {
|
|
35
|
+
throw new Error(errorMsg);
|
|
36
|
+
}
|
|
37
|
+
const config = await getConfigFn();
|
|
38
|
+
config[key] = value;
|
|
39
|
+
await saveConfigFn(config);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create path configuration functions with config access
|
|
44
|
+
* @param {Function} getConfigFn - Function to get config
|
|
45
|
+
* @param {Function} saveConfigFn - Function to save config
|
|
46
|
+
* @returns {Object} Path configuration functions
|
|
47
|
+
*/
|
|
48
|
+
function createPathConfigFunctions(getConfigFn, saveConfigFn) {
|
|
49
|
+
return {
|
|
50
|
+
/**
|
|
51
|
+
* Get aifabrix-home override path
|
|
52
|
+
* @async
|
|
53
|
+
* @returns {Promise<string|null>} Home path or null
|
|
54
|
+
*/
|
|
55
|
+
async getAifabrixHomeOverride() {
|
|
56
|
+
return getPathConfig(getConfigFn, 'aifabrix-home');
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set aifabrix-home override path
|
|
61
|
+
* @async
|
|
62
|
+
* @param {string} homePath - Home path
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
async setAifabrixHomeOverride(homePath) {
|
|
66
|
+
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-home', homePath, 'Home path is required and must be a string');
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get aifabrix-secrets path
|
|
71
|
+
* @async
|
|
72
|
+
* @returns {Promise<string|null>} Secrets path or null
|
|
73
|
+
*/
|
|
74
|
+
async getAifabrixSecretsPath() {
|
|
75
|
+
return getPathConfig(getConfigFn, 'aifabrix-secrets');
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Set aifabrix-secrets path
|
|
80
|
+
* @async
|
|
81
|
+
* @param {string} secretsPath - Secrets path
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
async setAifabrixSecretsPath(secretsPath) {
|
|
85
|
+
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-secrets', secretsPath, 'Secrets path is required and must be a string');
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get aifabrix-env-config path
|
|
90
|
+
* @async
|
|
91
|
+
* @returns {Promise<string|null>} Env config path or null
|
|
92
|
+
*/
|
|
93
|
+
async getAifabrixEnvConfigPath() {
|
|
94
|
+
return getPathConfig(getConfigFn, 'aifabrix-env-config');
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Set aifabrix-env-config path
|
|
99
|
+
* @async
|
|
100
|
+
* @param {string} envConfigPath - Env config path
|
|
101
|
+
* @returns {Promise<void>}
|
|
102
|
+
*/
|
|
103
|
+
async setAifabrixEnvConfigPath(envConfigPath) {
|
|
104
|
+
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-env-config', envConfigPath, 'Env config path is required and must be a string');
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
createPathConfigFunctions
|
|
111
|
+
};
|
|
112
|
+
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder - Configuration Token Management
|
|
3
|
+
*
|
|
4
|
+
* Token management functions for device and client tokens in config.yaml
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Token management utilities for config
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalize controller URL for consistent storage and lookup
|
|
13
|
+
* Removes trailing slashes and normalizes the URL format
|
|
14
|
+
* @param {string} url - Controller URL to normalize
|
|
15
|
+
* @returns {string} Normalized controller URL
|
|
16
|
+
*/
|
|
17
|
+
function normalizeControllerUrl(url) {
|
|
18
|
+
if (!url || typeof url !== 'string') {
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
// Remove trailing slashes
|
|
22
|
+
let normalized = url.trim().replace(/\/+$/, '');
|
|
23
|
+
// Ensure it starts with http:// or https://
|
|
24
|
+
if (!normalized.match(/^https?:\/\//)) {
|
|
25
|
+
// If it doesn't start with protocol, assume http://
|
|
26
|
+
normalized = `http://${normalized}`;
|
|
27
|
+
}
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create token management functions with config access
|
|
33
|
+
* @param {Function} getConfigFn - Function to get config
|
|
34
|
+
* @param {Function} saveConfigFn - Function to save config
|
|
35
|
+
* @param {Function} getSecretsEncryptionKeyFn - Function to get encryption key
|
|
36
|
+
* @param {Function} encryptTokenValueFn - Function to encrypt token
|
|
37
|
+
* @param {Function} decryptTokenValueFn - Function to decrypt token
|
|
38
|
+
* @param {Function} isTokenEncryptedFn - Function to check if token is encrypted
|
|
39
|
+
* @returns {Object} Token management functions
|
|
40
|
+
*/
|
|
41
|
+
function createTokenManagementFunctions(
|
|
42
|
+
getConfigFn,
|
|
43
|
+
saveConfigFn,
|
|
44
|
+
getSecretsEncryptionKeyFn,
|
|
45
|
+
encryptTokenValueFn,
|
|
46
|
+
decryptTokenValueFn,
|
|
47
|
+
isTokenEncryptedFn
|
|
48
|
+
) {
|
|
49
|
+
/**
|
|
50
|
+
* Extract device token information with encryption/decryption handling
|
|
51
|
+
* @param {Object} deviceToken - Device token object from config
|
|
52
|
+
* @param {string} controllerUrl - Controller URL
|
|
53
|
+
* @returns {Promise<{controller: string, token: string, refreshToken: string, expiresAt: string}>} Device token info
|
|
54
|
+
*/
|
|
55
|
+
async function extractDeviceTokenInfo(deviceToken, controllerUrl) {
|
|
56
|
+
// Migration: If tokens are plain text and encryption key exists, encrypt them first
|
|
57
|
+
const encryptionKey = await getSecretsEncryptionKeyFn();
|
|
58
|
+
if (encryptionKey) {
|
|
59
|
+
let needsSave = false;
|
|
60
|
+
|
|
61
|
+
if (deviceToken.token && !isTokenEncryptedFn(deviceToken.token)) {
|
|
62
|
+
// Token is plain text, encrypt it
|
|
63
|
+
deviceToken.token = await encryptTokenValueFn(deviceToken.token);
|
|
64
|
+
needsSave = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (deviceToken.refreshToken && !isTokenEncryptedFn(deviceToken.refreshToken)) {
|
|
68
|
+
// Refresh token is plain text, encrypt it
|
|
69
|
+
deviceToken.refreshToken = await encryptTokenValueFn(deviceToken.refreshToken);
|
|
70
|
+
needsSave = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (needsSave) {
|
|
74
|
+
// Save encrypted tokens back to config
|
|
75
|
+
const config = await getConfigFn();
|
|
76
|
+
await saveConfigFn(config);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Decrypt tokens if encrypted (for return value)
|
|
80
|
+
const token = deviceToken.token ? await decryptTokenValueFn(deviceToken.token) : undefined;
|
|
81
|
+
const refreshToken = deviceToken.refreshToken ? await decryptTokenValueFn(deviceToken.refreshToken) : null;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
controller: controllerUrl,
|
|
85
|
+
token: token,
|
|
86
|
+
refreshToken: refreshToken,
|
|
87
|
+
expiresAt: deviceToken.expiresAt
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get device token for controller
|
|
93
|
+
* @param {string} controllerUrl - Controller URL
|
|
94
|
+
* @returns {Promise<{controller: string, token: string, refreshToken: string, expiresAt: string}|null>} Device token info or null
|
|
95
|
+
*/
|
|
96
|
+
async function getDeviceToken(controllerUrl) {
|
|
97
|
+
const config = await getConfigFn();
|
|
98
|
+
if (!controllerUrl) return null;
|
|
99
|
+
|
|
100
|
+
// Normalize URL for consistent lookup
|
|
101
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
102
|
+
|
|
103
|
+
// Try exact match first
|
|
104
|
+
if (config.device && config.device[normalizedUrl]) {
|
|
105
|
+
const deviceToken = config.device[normalizedUrl];
|
|
106
|
+
return extractDeviceTokenInfo(deviceToken, normalizedUrl);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Try to find matching URL by normalizing all stored URLs
|
|
110
|
+
if (config.device) {
|
|
111
|
+
for (const storedUrl of Object.keys(config.device)) {
|
|
112
|
+
if (normalizeControllerUrl(storedUrl) === normalizedUrl) {
|
|
113
|
+
const deviceToken = config.device[storedUrl];
|
|
114
|
+
// Migrate to normalized URL if different
|
|
115
|
+
if (storedUrl !== normalizedUrl) {
|
|
116
|
+
config.device[normalizedUrl] = deviceToken;
|
|
117
|
+
delete config.device[storedUrl];
|
|
118
|
+
await saveConfigFn(config);
|
|
119
|
+
}
|
|
120
|
+
return extractDeviceTokenInfo(deviceToken, normalizedUrl);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Save device token for controller (root level)
|
|
130
|
+
* @param {string} controllerUrl - Controller URL (used as key)
|
|
131
|
+
* @param {string} token - Device access token
|
|
132
|
+
* @param {string} refreshToken - Refresh token for token renewal
|
|
133
|
+
* @param {string} expiresAt - ISO timestamp string
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
|
|
137
|
+
const config = await getConfigFn();
|
|
138
|
+
if (!config.device) config.device = {};
|
|
139
|
+
|
|
140
|
+
// Normalize URL for consistent storage
|
|
141
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
142
|
+
|
|
143
|
+
// If there's an existing entry with a different URL format, remove it
|
|
144
|
+
if (config.device) {
|
|
145
|
+
for (const storedUrl of Object.keys(config.device)) {
|
|
146
|
+
if (normalizeControllerUrl(storedUrl) === normalizedUrl && storedUrl !== normalizedUrl) {
|
|
147
|
+
delete config.device[storedUrl];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Encrypt tokens before saving
|
|
153
|
+
const encryptedToken = await encryptTokenValueFn(token);
|
|
154
|
+
const encryptedRefreshToken = refreshToken ? await encryptTokenValueFn(refreshToken) : null;
|
|
155
|
+
|
|
156
|
+
config.device[normalizedUrl] = {
|
|
157
|
+
token: encryptedToken,
|
|
158
|
+
refreshToken: encryptedRefreshToken,
|
|
159
|
+
expiresAt
|
|
160
|
+
};
|
|
161
|
+
await saveConfigFn(config);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get client token for environment and app
|
|
166
|
+
* @param {string} environment - Environment key
|
|
167
|
+
* @param {string} appName - Application name
|
|
168
|
+
* @returns {Promise<{controller: string, token: string, expiresAt: string}|null>} Client token info or null
|
|
169
|
+
*/
|
|
170
|
+
async function getClientToken(environment, appName) {
|
|
171
|
+
const config = await getConfigFn();
|
|
172
|
+
if (!config.environments || !config.environments[environment]) return null;
|
|
173
|
+
if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) return null;
|
|
174
|
+
|
|
175
|
+
const clientToken = config.environments[environment].clients[appName];
|
|
176
|
+
|
|
177
|
+
// Migration: If token is plain text and encryption key exists, encrypt it first
|
|
178
|
+
const encryptionKey = await getSecretsEncryptionKeyFn();
|
|
179
|
+
if (encryptionKey && clientToken.token && !isTokenEncryptedFn(clientToken.token)) {
|
|
180
|
+
// Token is plain text, encrypt it
|
|
181
|
+
clientToken.token = await encryptTokenValueFn(clientToken.token);
|
|
182
|
+
// Save encrypted token back to config
|
|
183
|
+
await saveConfigFn(config);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Decrypt token if encrypted (for return value)
|
|
187
|
+
const token = await decryptTokenValueFn(clientToken.token);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
controller: clientToken.controller,
|
|
191
|
+
token: token,
|
|
192
|
+
expiresAt: clientToken.expiresAt
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Save client token for environment and app
|
|
198
|
+
* @param {string} environment - Environment key
|
|
199
|
+
* @param {string} appName - Application name
|
|
200
|
+
* @param {string} controllerUrl - Controller URL
|
|
201
|
+
* @param {string} token - Client token
|
|
202
|
+
* @param {string} expiresAt - ISO timestamp string
|
|
203
|
+
* @returns {Promise<void>}
|
|
204
|
+
*/
|
|
205
|
+
async function saveClientToken(environment, appName, controllerUrl, token, expiresAt) {
|
|
206
|
+
const config = await getConfigFn();
|
|
207
|
+
if (!config.environments) config.environments = {};
|
|
208
|
+
if (!config.environments[environment]) config.environments[environment] = { clients: {} };
|
|
209
|
+
if (!config.environments[environment].clients) config.environments[environment].clients = {};
|
|
210
|
+
|
|
211
|
+
// Encrypt token before saving
|
|
212
|
+
const encryptedToken = await encryptTokenValueFn(token);
|
|
213
|
+
|
|
214
|
+
config.environments[environment].clients[appName] = {
|
|
215
|
+
controller: controllerUrl,
|
|
216
|
+
token: encryptedToken,
|
|
217
|
+
expiresAt
|
|
218
|
+
};
|
|
219
|
+
await saveConfigFn(config);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
getDeviceToken,
|
|
224
|
+
getClientToken,
|
|
225
|
+
saveDeviceToken,
|
|
226
|
+
saveClientToken
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
createTokenManagementFunctions
|
|
232
|
+
};
|
|
233
|
+
|
package/lib/utils/device-code.js
CHANGED
|
@@ -213,7 +213,9 @@ function handlePollingErrors(error, status, response) {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
// Handle validation errors with detailed message
|
|
216
|
-
|
|
216
|
+
// Check for validation_error, status 400, or specific validation error codes
|
|
217
|
+
if (error === 'validation_error' || status === 400 ||
|
|
218
|
+
error === 'INVALID_TOKEN' || error === 'INVALID_ACCESS_TOKEN') {
|
|
217
219
|
throw createValidationError(response);
|
|
218
220
|
}
|
|
219
221
|
|
|
@@ -245,14 +247,26 @@ function extractPollingError(response) {
|
|
|
245
247
|
if (response.errorType === 'validation') {
|
|
246
248
|
return 'validation_error';
|
|
247
249
|
}
|
|
250
|
+
// Check if error code indicates validation error (e.g., INVALID_TOKEN)
|
|
251
|
+
const errorCode = errorData.error || errorData.code || response.error;
|
|
252
|
+
if (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN') {
|
|
253
|
+
return 'validation_error';
|
|
254
|
+
}
|
|
248
255
|
// Return the error message from structured error
|
|
249
|
-
return errorData.detail || errorData.title || errorData.message ||
|
|
256
|
+
return errorData.detail || errorData.title || errorData.message || errorCode || response.error || 'Unknown error';
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
// Fallback to original extraction logic
|
|
253
260
|
const apiResponse = response.data || {};
|
|
254
261
|
const errorData = typeof apiResponse === 'object' ? apiResponse : {};
|
|
255
|
-
|
|
262
|
+
const errorCode = errorData.error || response.error || 'Unknown error';
|
|
263
|
+
|
|
264
|
+
// Check if error code indicates validation error (e.g., INVALID_TOKEN)
|
|
265
|
+
if (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN') {
|
|
266
|
+
return 'validation_error';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return errorCode;
|
|
256
270
|
}
|
|
257
271
|
|
|
258
272
|
/**
|
|
@@ -278,14 +292,22 @@ function handleSuccessfulPoll(response) {
|
|
|
278
292
|
*/
|
|
279
293
|
async function processPollingResponse(response, interval) {
|
|
280
294
|
if (response.success) {
|
|
295
|
+
// Check if response contains an error code even though success is true
|
|
296
|
+
const apiResponse = response.data || {};
|
|
297
|
+
const responseData = apiResponse.data || apiResponse;
|
|
298
|
+
const errorCode = responseData.error || apiResponse.error || response.error;
|
|
299
|
+
|
|
300
|
+
// If there's an error code like INVALID_TOKEN, treat it as a validation error
|
|
301
|
+
if (errorCode && (errorCode === 'INVALID_TOKEN' || errorCode === 'INVALID_ACCESS_TOKEN')) {
|
|
302
|
+
throw createValidationError(response);
|
|
303
|
+
}
|
|
304
|
+
|
|
281
305
|
const tokenResponse = handleSuccessfulPoll(response);
|
|
282
306
|
if (tokenResponse) {
|
|
283
307
|
return tokenResponse;
|
|
284
308
|
}
|
|
285
309
|
|
|
286
|
-
const
|
|
287
|
-
const responseData = apiResponse.data || apiResponse;
|
|
288
|
-
const error = responseData.error || apiResponse.error;
|
|
310
|
+
const error = errorCode;
|
|
289
311
|
const slowDown = error === 'slow_down';
|
|
290
312
|
await waitForNextPoll(interval, slowDown);
|
|
291
313
|
return null;
|