@aifabrix/builder 2.22.1 → 2.31.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/jest.config.coverage.js +37 -0
- package/lib/api/pipeline.api.js +10 -9
- package/lib/app-deploy.js +36 -14
- package/lib/app-list.js +191 -71
- package/lib/app-prompts.js +77 -26
- package/lib/app-readme.js +123 -5
- package/lib/app-rotate-secret.js +101 -57
- package/lib/app-run-helpers.js +200 -172
- package/lib/app-run.js +137 -68
- package/lib/audit-logger.js +8 -7
- package/lib/build.js +161 -250
- package/lib/cli.js +73 -65
- package/lib/commands/login.js +45 -31
- package/lib/commands/logout.js +181 -0
- package/lib/commands/secrets-set.js +2 -2
- package/lib/commands/secure.js +61 -26
- package/lib/config.js +79 -45
- package/lib/datasource-deploy.js +89 -29
- package/lib/deployer.js +164 -129
- package/lib/diff.js +63 -21
- package/lib/environment-deploy.js +36 -19
- package/lib/external-system-deploy.js +134 -66
- package/lib/external-system-download.js +244 -171
- package/lib/external-system-test.js +199 -164
- package/lib/generator-external.js +145 -72
- package/lib/generator-helpers.js +49 -17
- package/lib/generator-split.js +105 -58
- package/lib/infra.js +101 -131
- package/lib/schema/application-schema.json +895 -896
- package/lib/schema/env-config.yaml +11 -4
- package/lib/template-validator.js +13 -4
- package/lib/utils/api.js +8 -8
- package/lib/utils/app-register-auth.js +36 -18
- package/lib/utils/app-run-containers.js +140 -0
- package/lib/utils/auth-headers.js +6 -6
- package/lib/utils/build-copy.js +60 -2
- package/lib/utils/build-helpers.js +94 -0
- package/lib/utils/cli-utils.js +177 -76
- package/lib/utils/compose-generator.js +12 -2
- package/lib/utils/config-tokens.js +151 -9
- package/lib/utils/deployment-errors.js +137 -69
- package/lib/utils/deployment-validation-helpers.js +103 -0
- package/lib/utils/docker-build.js +57 -0
- package/lib/utils/dockerfile-utils.js +13 -3
- package/lib/utils/env-copy.js +163 -94
- package/lib/utils/env-map.js +226 -86
- package/lib/utils/environment-checker.js +2 -2
- package/lib/utils/error-formatters/network-errors.js +0 -1
- package/lib/utils/external-system-display.js +14 -19
- package/lib/utils/external-system-env-helpers.js +107 -0
- package/lib/utils/external-system-test-helpers.js +144 -0
- package/lib/utils/health-check.js +10 -8
- package/lib/utils/infra-status.js +123 -0
- package/lib/utils/local-secrets.js +3 -2
- package/lib/utils/paths.js +228 -49
- package/lib/utils/schema-loader.js +125 -57
- package/lib/utils/token-manager.js +10 -7
- package/lib/utils/yaml-preserve.js +55 -16
- package/lib/validate.js +87 -89
- package/package.json +4 -4
- package/scripts/ci-fix.sh +19 -0
- package/scripts/ci-simulate.sh +19 -0
- package/templates/applications/miso-controller/test.yaml +1 -0
- package/templates/python/Dockerfile.hbs +8 -45
- package/templates/typescript/Dockerfile.hbs +8 -42
package/lib/utils/cli-utils.js
CHANGED
|
@@ -22,6 +22,157 @@ function validateCommand(_command, _options) {
|
|
|
22
22
|
return true;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Format already formatted error message
|
|
27
|
+
* @param {string} formatted - Formatted error message
|
|
28
|
+
* @returns {string[]} Array of error message lines
|
|
29
|
+
*/
|
|
30
|
+
function formatFormattedError(formatted) {
|
|
31
|
+
const messages = [];
|
|
32
|
+
const lines = formatted.split('\n');
|
|
33
|
+
lines.forEach(line => {
|
|
34
|
+
if (line.trim()) {
|
|
35
|
+
messages.push(` ${line}`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return messages;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format Docker-related errors
|
|
43
|
+
* @param {string} errorMsg - Error message
|
|
44
|
+
* @returns {string[]|null} Array of error message lines or null if not a Docker error
|
|
45
|
+
*/
|
|
46
|
+
function formatDockerError(errorMsg) {
|
|
47
|
+
if (errorMsg.includes('not found locally') || (errorMsg.includes('Docker image') && errorMsg.includes('not found'))) {
|
|
48
|
+
return [
|
|
49
|
+
' Docker image not found.',
|
|
50
|
+
' Run: aifabrix build <app> first'
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
if (errorMsg.includes('Docker') && (errorMsg.includes('not running') || errorMsg.includes('not installed') || errorMsg.includes('Cannot connect'))) {
|
|
54
|
+
return [
|
|
55
|
+
' Docker is not running or not installed.',
|
|
56
|
+
' Please start Docker Desktop and try again.'
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
if (errorMsg.toLowerCase().includes('port') && (errorMsg.includes('already in use') || errorMsg.includes('in use') || errorMsg.includes('conflict'))) {
|
|
60
|
+
return [
|
|
61
|
+
' Port conflict detected.',
|
|
62
|
+
' Run "aifabrix doctor" to check which ports are in use.'
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
if ((errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) && !errorMsg.includes('permissions/') && !errorMsg.includes('Field "permissions')) {
|
|
66
|
+
return [
|
|
67
|
+
' Permission denied.',
|
|
68
|
+
' Make sure you have the necessary permissions to run Docker commands.'
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Format Azure-related errors
|
|
76
|
+
* @param {string} errorMsg - Error message
|
|
77
|
+
* @returns {string[]|null} Array of error message lines or null if not an Azure error
|
|
78
|
+
*/
|
|
79
|
+
function formatAzureError(errorMsg) {
|
|
80
|
+
if (errorMsg.includes('Azure CLI is not installed') || errorMsg.includes('az --version failed') || (errorMsg.includes('az') && errorMsg.includes('failed'))) {
|
|
81
|
+
return [
|
|
82
|
+
' Azure CLI is not installed or not working properly.',
|
|
83
|
+
' Install from: https://docs.microsoft.com/cli/azure/install-azure-cli',
|
|
84
|
+
' Run: az login'
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
if (errorMsg.includes('Invalid ACR URL') || errorMsg.includes('Invalid registry URL') || errorMsg.includes('Expected format')) {
|
|
88
|
+
return [
|
|
89
|
+
' Invalid registry URL format.',
|
|
90
|
+
' Use format: *.azurecr.io (e.g., myacr.azurecr.io)'
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
if (errorMsg.includes('authenticate') || errorMsg.includes('ACR') || errorMsg.includes('Authentication required')) {
|
|
94
|
+
return [
|
|
95
|
+
' Azure Container Registry authentication failed.',
|
|
96
|
+
' Run: az acr login --name <registry-name>',
|
|
97
|
+
' Or login to Azure: az login'
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
if (errorMsg.includes('Registry URL is required')) {
|
|
101
|
+
return [
|
|
102
|
+
' Registry URL is required.',
|
|
103
|
+
' Provide via --registry flag or configure in variables.yaml under image.registry'
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format secrets-related errors
|
|
111
|
+
* @param {string} errorMsg - Error message
|
|
112
|
+
* @returns {string[]|null} Array of error message lines or null if not a secrets error
|
|
113
|
+
*/
|
|
114
|
+
function formatSecretsError(errorMsg) {
|
|
115
|
+
if (!errorMsg.includes('Missing secrets')) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const messages = [];
|
|
120
|
+
const missingSecretsMatch = errorMsg.match(/Missing secrets: ([^\n]+)/);
|
|
121
|
+
const fileInfoMatch = errorMsg.match(/Secrets file location: ([^\n]+)/);
|
|
122
|
+
const resolveMatch = errorMsg.match(/Run "aifabrix resolve ([^"]+)"/);
|
|
123
|
+
|
|
124
|
+
if (missingSecretsMatch) {
|
|
125
|
+
messages.push(` Missing secrets: ${missingSecretsMatch[1]}`);
|
|
126
|
+
} else {
|
|
127
|
+
messages.push(' Missing secrets in secrets file.');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (fileInfoMatch) {
|
|
131
|
+
messages.push(` Secrets file location: ${fileInfoMatch[1]}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (resolveMatch) {
|
|
135
|
+
messages.push(` Run: aifabrix resolve ${resolveMatch[1]} to generate missing secrets.`);
|
|
136
|
+
} else {
|
|
137
|
+
messages.push(' Run: aifabrix resolve <app-name> to generate missing secrets.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return messages;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Format deployment-related errors
|
|
145
|
+
* @param {string} errorMsg - Error message
|
|
146
|
+
* @returns {string[]|null} Array of error message lines or null if not a deployment error
|
|
147
|
+
*/
|
|
148
|
+
function formatDeploymentError(errorMsg) {
|
|
149
|
+
if (!errorMsg.includes('Deployment failed after')) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const match = errorMsg.match(/Deployment failed after \d+ attempts: (.+)/);
|
|
154
|
+
if (match) {
|
|
155
|
+
return [` ${match[1]}`];
|
|
156
|
+
}
|
|
157
|
+
return [` ${errorMsg}`];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Format validation errors
|
|
162
|
+
* @param {string} errorMsg - Error message
|
|
163
|
+
* @returns {string[]|null} Array of error message lines or null if not a validation error
|
|
164
|
+
*/
|
|
165
|
+
function formatValidationError(errorMsg) {
|
|
166
|
+
if (errorMsg.includes('Configuration not found') ||
|
|
167
|
+
errorMsg.includes('does not match schema') ||
|
|
168
|
+
errorMsg.includes('Validation failed') ||
|
|
169
|
+
errorMsg.includes('Field "') ||
|
|
170
|
+
errorMsg.includes('Invalid format')) {
|
|
171
|
+
return [` ${errorMsg}`];
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
25
176
|
/**
|
|
26
177
|
* Formats error message based on error type
|
|
27
178
|
* @function formatError
|
|
@@ -29,92 +180,42 @@ function validateCommand(_command, _options) {
|
|
|
29
180
|
* @returns {string[]} Array of error message lines
|
|
30
181
|
*/
|
|
31
182
|
function formatError(error) {
|
|
32
|
-
const messages = [];
|
|
33
|
-
|
|
34
183
|
// If error has formatted message (from API error handler), use it directly
|
|
35
184
|
if (error.formatted) {
|
|
36
|
-
|
|
37
|
-
const lines = error.formatted.split('\n');
|
|
38
|
-
lines.forEach(line => {
|
|
39
|
-
if (line.trim()) {
|
|
40
|
-
messages.push(` ${line}`);
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
return messages;
|
|
185
|
+
return formatFormattedError(error.formatted);
|
|
44
186
|
}
|
|
45
187
|
|
|
46
188
|
const errorMsg = error.message || '';
|
|
189
|
+
const messages = [];
|
|
47
190
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
messages.push(` ${errorMsg}`);
|
|
54
|
-
} else if (errorMsg.includes('not found locally') || (errorMsg.includes('Docker image') && errorMsg.includes('not found'))) {
|
|
55
|
-
messages.push(' Docker image not found.');
|
|
56
|
-
messages.push(' Run: aifabrix build <app> first');
|
|
57
|
-
} else if (errorMsg.includes('Docker') && (errorMsg.includes('not running') || errorMsg.includes('not installed') || errorMsg.includes('Cannot connect'))) {
|
|
58
|
-
messages.push(' Docker is not running or not installed.');
|
|
59
|
-
messages.push(' Please start Docker Desktop and try again.');
|
|
60
|
-
} else if (errorMsg.toLowerCase().includes('port') && (errorMsg.includes('already in use') || errorMsg.includes('in use') || errorMsg.includes('conflict'))) {
|
|
61
|
-
messages.push(' Port conflict detected.');
|
|
62
|
-
messages.push(' Run "aifabrix doctor" to check which ports are in use.');
|
|
63
|
-
} else if ((errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) && !errorMsg.includes('permissions/') && !errorMsg.includes('Field "permissions')) {
|
|
64
|
-
// Only match actual permission denied errors, not validation errors about permissions fields
|
|
65
|
-
messages.push(' Permission denied.');
|
|
66
|
-
messages.push(' Make sure you have the necessary permissions to run Docker commands.');
|
|
67
|
-
} else if (errorMsg.includes('Azure CLI is not installed') || errorMsg.includes('az --version failed') || (errorMsg.includes('az') && errorMsg.includes('failed'))) {
|
|
68
|
-
// Specific error for missing Azure CLI installation or Azure CLI command failures
|
|
69
|
-
messages.push(' Azure CLI is not installed or not working properly.');
|
|
70
|
-
messages.push(' Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
|
|
71
|
-
messages.push(' Run: az login');
|
|
72
|
-
} else if (errorMsg.includes('Invalid ACR URL') || errorMsg.includes('Invalid registry URL') || errorMsg.includes('Expected format')) {
|
|
73
|
-
messages.push(' Invalid registry URL format.');
|
|
74
|
-
messages.push(' Use format: *.azurecr.io (e.g., myacr.azurecr.io)');
|
|
75
|
-
} else if (errorMsg.includes('authenticate') || errorMsg.includes('ACR') || errorMsg.includes('Authentication required')) {
|
|
76
|
-
messages.push(' Azure Container Registry authentication failed.');
|
|
77
|
-
messages.push(' Run: az acr login --name <registry-name>');
|
|
78
|
-
messages.push(' Or login to Azure: az login');
|
|
79
|
-
} else if (errorMsg.includes('Registry URL is required')) {
|
|
80
|
-
messages.push(' Registry URL is required.');
|
|
81
|
-
messages.push(' Provide via --registry flag or configure in variables.yaml under image.registry');
|
|
82
|
-
} else if (errorMsg.includes('Missing secrets')) {
|
|
83
|
-
// Extract the missing secrets list and file info from the error message
|
|
84
|
-
const missingSecretsMatch = errorMsg.match(/Missing secrets: ([^\n]+)/);
|
|
85
|
-
const fileInfoMatch = errorMsg.match(/Secrets file location: ([^\n]+)/);
|
|
86
|
-
const resolveMatch = errorMsg.match(/Run "aifabrix resolve ([^"]+)"/);
|
|
87
|
-
|
|
88
|
-
if (missingSecretsMatch) {
|
|
89
|
-
messages.push(` Missing secrets: ${missingSecretsMatch[1]}`);
|
|
90
|
-
} else {
|
|
91
|
-
messages.push(' Missing secrets in secrets file.');
|
|
92
|
-
}
|
|
191
|
+
// Try different error formatters in order of specificity
|
|
192
|
+
const dockerError = formatDockerError(errorMsg);
|
|
193
|
+
if (dockerError) {
|
|
194
|
+
return dockerError;
|
|
195
|
+
}
|
|
93
196
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
197
|
+
const azureError = formatAzureError(errorMsg);
|
|
198
|
+
if (azureError) {
|
|
199
|
+
return azureError;
|
|
200
|
+
}
|
|
97
201
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} else {
|
|
112
|
-
messages.push(` ${errorMsg}`);
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
messages.push(` ${errorMsg}`);
|
|
202
|
+
const secretsError = formatSecretsError(errorMsg);
|
|
203
|
+
if (secretsError) {
|
|
204
|
+
return secretsError;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const deploymentError = formatDeploymentError(errorMsg);
|
|
208
|
+
if (deploymentError) {
|
|
209
|
+
return deploymentError;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const validationError = formatValidationError(errorMsg);
|
|
213
|
+
if (validationError) {
|
|
214
|
+
return validationError;
|
|
116
215
|
}
|
|
117
216
|
|
|
217
|
+
// Default: return generic error message
|
|
218
|
+
messages.push(` ${errorMsg}`);
|
|
118
219
|
return messages;
|
|
119
220
|
}
|
|
120
221
|
|
|
@@ -74,9 +74,19 @@ handlebars.registerHelper('pgUserName', (dbName) => {
|
|
|
74
74
|
* @throws {Error} If template not found
|
|
75
75
|
*/
|
|
76
76
|
function loadDockerComposeTemplate(language) {
|
|
77
|
-
|
|
77
|
+
// Use getProjectRoot to reliably find templates in all environments
|
|
78
|
+
const { getProjectRoot } = require('./paths');
|
|
79
|
+
const projectRoot = getProjectRoot();
|
|
80
|
+
const templatePath = path.join(projectRoot, 'templates', language, 'docker-compose.hbs');
|
|
81
|
+
|
|
78
82
|
if (!fsSync.existsSync(templatePath)) {
|
|
79
|
-
|
|
83
|
+
// Provide helpful error message with actual paths checked
|
|
84
|
+
const errorMessage = `Docker Compose template not found for language: ${language}\n` +
|
|
85
|
+
` Expected path: ${templatePath}\n` +
|
|
86
|
+
` Project root: ${projectRoot}\n` +
|
|
87
|
+
` Templates directory: ${path.join(projectRoot, 'templates', language)}\n` +
|
|
88
|
+
` Global PROJECT_ROOT: ${typeof global !== 'undefined' && global.PROJECT_ROOT ? global.PROJECT_ROOT : 'not set'}`;
|
|
89
|
+
throw new Error(errorMessage);
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
@@ -30,22 +30,23 @@ function normalizeControllerUrl(url) {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Create token management functions with config access
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {Function}
|
|
35
|
-
* @param {Function}
|
|
36
|
-
* @param {Function}
|
|
37
|
-
* @param {Function}
|
|
38
|
-
* @param {Function}
|
|
33
|
+
* @param {Object} params - Function parameters
|
|
34
|
+
* @param {Function} params.getConfigFn - Function to get config
|
|
35
|
+
* @param {Function} params.saveConfigFn - Function to save config
|
|
36
|
+
* @param {Function} params.getSecretsEncryptionKeyFn - Function to get encryption key
|
|
37
|
+
* @param {Function} params.encryptTokenValueFn - Function to encrypt token
|
|
38
|
+
* @param {Function} params.decryptTokenValueFn - Function to decrypt token
|
|
39
|
+
* @param {Function} params.isTokenEncryptedFn - Function to check if token is encrypted
|
|
39
40
|
* @returns {Object} Token management functions
|
|
40
41
|
*/
|
|
41
|
-
function createTokenManagementFunctions(
|
|
42
|
+
function createTokenManagementFunctions({
|
|
42
43
|
getConfigFn,
|
|
43
44
|
saveConfigFn,
|
|
44
45
|
getSecretsEncryptionKeyFn,
|
|
45
46
|
encryptTokenValueFn,
|
|
46
47
|
decryptTokenValueFn,
|
|
47
48
|
isTokenEncryptedFn
|
|
48
|
-
) {
|
|
49
|
+
}) {
|
|
49
50
|
/**
|
|
50
51
|
* Extract device token information with encryption/decryption handling
|
|
51
52
|
* @param {Object} deviceToken - Device token object from config
|
|
@@ -219,11 +220,152 @@ function createTokenManagementFunctions(
|
|
|
219
220
|
await saveConfigFn(config);
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Clear device token for specific controller
|
|
225
|
+
* @param {string} controllerUrl - Controller URL
|
|
226
|
+
* @returns {Promise<boolean>} True if token was cleared, false if it didn't exist
|
|
227
|
+
*/
|
|
228
|
+
async function clearDeviceToken(controllerUrl) {
|
|
229
|
+
const config = await getConfigFn();
|
|
230
|
+
if (!config.device || !controllerUrl) return false;
|
|
231
|
+
|
|
232
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
233
|
+
let cleared = false;
|
|
234
|
+
|
|
235
|
+
// Try exact match first
|
|
236
|
+
if (config.device[normalizedUrl]) {
|
|
237
|
+
delete config.device[normalizedUrl];
|
|
238
|
+
cleared = true;
|
|
239
|
+
} else {
|
|
240
|
+
// Try to find matching URL by normalizing all stored URLs
|
|
241
|
+
for (const storedUrl of Object.keys(config.device)) {
|
|
242
|
+
if (normalizeControllerUrl(storedUrl) === normalizedUrl) {
|
|
243
|
+
delete config.device[storedUrl];
|
|
244
|
+
cleared = true;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (cleared) {
|
|
251
|
+
await saveConfigFn(config);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return cleared;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Clear all device tokens
|
|
259
|
+
* @returns {Promise<number>} Number of tokens cleared
|
|
260
|
+
*/
|
|
261
|
+
async function clearAllDeviceTokens() {
|
|
262
|
+
const config = await getConfigFn();
|
|
263
|
+
if (!config.device) return 0;
|
|
264
|
+
|
|
265
|
+
const count = Object.keys(config.device).length;
|
|
266
|
+
if (count > 0) {
|
|
267
|
+
config.device = {};
|
|
268
|
+
await saveConfigFn(config);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return count;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Clear client token for specific environment and app
|
|
276
|
+
* @param {string} environment - Environment key
|
|
277
|
+
* @param {string} appName - Application name
|
|
278
|
+
* @returns {Promise<boolean>} True if token was cleared, false if it didn't exist
|
|
279
|
+
*/
|
|
280
|
+
async function clearClientToken(environment, appName) {
|
|
281
|
+
const config = await getConfigFn();
|
|
282
|
+
if (!config.environments || !config.environments[environment]) return false;
|
|
283
|
+
if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) return false;
|
|
284
|
+
|
|
285
|
+
delete config.environments[environment].clients[appName];
|
|
286
|
+
|
|
287
|
+
// Clean up empty clients object if no clients remain
|
|
288
|
+
if (Object.keys(config.environments[environment].clients).length === 0) {
|
|
289
|
+
delete config.environments[environment].clients;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Clean up empty environment object if no clients remain
|
|
293
|
+
if (Object.keys(config.environments[environment]).length === 0) {
|
|
294
|
+
delete config.environments[environment];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await saveConfigFn(config);
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Clear all client tokens for a specific environment
|
|
303
|
+
* @param {string} environment - Environment key
|
|
304
|
+
* @returns {Promise<number>} Number of tokens cleared
|
|
305
|
+
*/
|
|
306
|
+
async function clearClientTokensForEnvironment(environment) {
|
|
307
|
+
const config = await getConfigFn();
|
|
308
|
+
if (!config.environments || !config.environments[environment]) return 0;
|
|
309
|
+
if (!config.environments[environment].clients) return 0;
|
|
310
|
+
|
|
311
|
+
const count = Object.keys(config.environments[environment].clients).length;
|
|
312
|
+
if (count > 0) {
|
|
313
|
+
delete config.environments[environment].clients;
|
|
314
|
+
// Clean up empty environment object if no other properties remain
|
|
315
|
+
if (Object.keys(config.environments[environment]).length === 0) {
|
|
316
|
+
delete config.environments[environment];
|
|
317
|
+
}
|
|
318
|
+
await saveConfigFn(config);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return count;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Clear all client tokens across all environments
|
|
326
|
+
* @returns {Promise<number>} Number of tokens cleared
|
|
327
|
+
*/
|
|
328
|
+
async function clearAllClientTokens() {
|
|
329
|
+
const config = await getConfigFn();
|
|
330
|
+
if (!config.environments) return 0;
|
|
331
|
+
|
|
332
|
+
let totalCount = 0;
|
|
333
|
+
const environmentsToRemove = [];
|
|
334
|
+
|
|
335
|
+
for (const env of Object.keys(config.environments)) {
|
|
336
|
+
if (config.environments[env].clients) {
|
|
337
|
+
const count = Object.keys(config.environments[env].clients).length;
|
|
338
|
+
totalCount += count;
|
|
339
|
+
delete config.environments[env].clients;
|
|
340
|
+
// Mark environment for removal if no other properties remain
|
|
341
|
+
if (Object.keys(config.environments[env]).length === 0) {
|
|
342
|
+
environmentsToRemove.push(env);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Remove empty environments
|
|
348
|
+
environmentsToRemove.forEach(env => {
|
|
349
|
+
delete config.environments[env];
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (totalCount > 0) {
|
|
353
|
+
await saveConfigFn(config);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return totalCount;
|
|
357
|
+
}
|
|
358
|
+
|
|
222
359
|
return {
|
|
223
360
|
getDeviceToken,
|
|
224
361
|
getClientToken,
|
|
225
362
|
saveDeviceToken,
|
|
226
|
-
saveClientToken
|
|
363
|
+
saveClientToken,
|
|
364
|
+
clearDeviceToken,
|
|
365
|
+
clearAllDeviceTokens,
|
|
366
|
+
clearClientToken,
|
|
367
|
+
clearClientTokensForEnvironment,
|
|
368
|
+
clearAllClientTokens
|
|
227
369
|
};
|
|
228
370
|
}
|
|
229
371
|
|