@aifabrix/builder 2.42.0 → 2.43.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/README.md +1 -1
- package/bin/aifabrix.js +1 -1
- package/integration/hubspot-test/README.md +126 -0
- package/integration/{hubspot → hubspot-test}/application.json +6 -6
- package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
- package/integration/hubspot-test/env.template +4 -0
- package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
- package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
- package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
- package/integration/hubspot-test/rbac.json +166 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test.js +102 -59
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
- package/lib/api/external-test.api.js +1 -1
- package/lib/api/service-users.api.js +111 -2
- package/lib/api/types/service-users.types.js +41 -0
- package/lib/api/wizard.api.js +2 -1
- package/lib/app/index.js +2 -2
- package/lib/app/prompts.js +2 -2
- package/lib/app/readme.js +3 -1
- package/lib/app/register.js +3 -1
- package/lib/app/rotate-secret.js +3 -0
- package/lib/cli/setup-app.js +5 -5
- package/lib/cli/setup-auth.js +19 -11
- package/lib/cli/setup-dev.js +62 -32
- package/lib/cli/setup-environment.js +6 -21
- package/lib/cli/setup-infra.js +13 -0
- package/lib/cli/setup-secrets.js +45 -6
- package/lib/cli/setup-service-user.js +146 -20
- package/lib/cli/setup-utility.js +12 -0
- package/lib/commands/auth-config.js +25 -19
- package/lib/commands/datasource.js +46 -1
- package/lib/commands/dev-init.js +1 -1
- package/lib/commands/repair-env-template.js +14 -8
- package/lib/commands/repair-rbac.js +25 -19
- package/lib/commands/repair.js +108 -31
- package/lib/commands/secrets-remove.js +1 -1
- package/lib/commands/secrets-set.js +6 -0
- package/lib/commands/secrets-validate.js +17 -4
- package/lib/commands/service-user.js +231 -2
- package/lib/commands/up-common.js +25 -0
- package/lib/commands/up-dataplane.js +91 -7
- package/lib/commands/wizard-core-helpers.js +5 -2
- package/lib/commands/wizard-core.js +2 -1
- package/lib/commands/wizard-headless.js +6 -1
- package/lib/commands/wizard.js +13 -6
- package/lib/core/admin-secrets.js +2 -0
- package/lib/core/config.js +7 -5
- package/lib/core/ensure-encryption-key.js +1 -3
- package/lib/core/secrets.js +32 -9
- package/lib/core/templates.js +1 -1
- package/lib/datasource/abac-validator.js +157 -0
- package/lib/datasource/field-reference-validator.js +74 -36
- package/lib/datasource/log-viewer.js +221 -0
- package/lib/datasource/resolve-app.js +109 -0
- package/lib/datasource/test-e2e.js +11 -20
- package/lib/datasource/test-integration.js +42 -22
- package/lib/datasource/validate.js +5 -2
- package/lib/external-system/download-helpers.js +3 -1
- package/lib/external-system/generator.js +12 -8
- package/lib/external-system/test-system-level.js +1 -1
- package/lib/generator/external-controller-manifest.js +3 -3
- package/lib/generator/external-schema-utils.js +3 -1
- package/lib/generator/external.js +7 -7
- package/lib/generator/helpers.js +13 -9
- package/lib/generator/index.js +4 -4
- package/lib/generator/split.js +45 -10
- package/lib/generator/wizard-prompts-secondary.js +39 -7
- package/lib/generator/wizard-readme.js +4 -1
- package/lib/generator/wizard.js +68 -53
- package/lib/infrastructure/helpers.js +50 -35
- package/lib/infrastructure/index.js +39 -23
- package/lib/schema/env-config.yaml +19 -2
- package/lib/schema/external-datasource.schema.json +11 -1
- package/lib/schema/wizard-config.schema.json +7 -1
- package/lib/utils/app-config-resolver.js +23 -1
- package/lib/utils/config-paths.js +48 -4
- package/lib/utils/credential-secrets-env.js +16 -1
- package/lib/utils/env-map.js +7 -3
- package/lib/utils/error-formatter.js +37 -0
- package/lib/utils/external-env-template.js +180 -0
- package/lib/utils/external-readme.js +33 -1
- package/lib/utils/external-system-display.js +43 -0
- package/lib/utils/external-system-validators.js +2 -2
- package/lib/utils/help-builder.js +3 -5
- package/lib/utils/local-secrets.js +26 -3
- package/lib/utils/paths.js +2 -1
- package/lib/utils/secrets-generator.js +2 -2
- package/lib/utils/secrets-utils.js +4 -0
- package/lib/utils/secure-file-permissions.js +91 -0
- package/lib/utils/token-manager.js +36 -3
- package/lib/utils/yaml-preserve.js +59 -1
- package/lib/validation/env-template-auth.js +50 -2
- package/lib/validation/external-manifest-validator.js +8 -0
- package/lib/validation/validate.js +8 -0
- package/lib/validation/validator.js +10 -13
- package/package.json +6 -2
- package/templates/applications/dataplane/env.template +5 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +13 -2
- package/templates/external-system/README.md.hbs +18 -5
- package/templates/external-system/env.template.hbs +22 -0
- package/integration/hubspot/README.md +0 -100
- package/integration/hubspot/env.template +0 -4
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
- /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
|
@@ -12,6 +12,7 @@ const chalk = require('chalk');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const logger = require('../utils/logger');
|
|
14
14
|
const { validateSecretsFile } = require('../utils/secrets-validation');
|
|
15
|
+
const { validateDataplaneSecrets } = require('../utils/token-manager');
|
|
15
16
|
const pathsUtil = require('../utils/paths');
|
|
16
17
|
const secretsEnsure = require('../core/secrets-ensure');
|
|
17
18
|
|
|
@@ -38,13 +39,25 @@ async function handleSecretsValidate(pathArg, options = {}) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const result = validateSecretsFile(filePath, { checkNaming: Boolean(options.naming) });
|
|
42
|
+
const dataplaneResult = validateDataplaneSecrets(filePath);
|
|
43
|
+
const allValid = result.valid && dataplaneResult.valid;
|
|
41
44
|
if (result.valid) {
|
|
42
45
|
logger.log(chalk.green(`✓ Secrets file is valid: ${result.path}`));
|
|
43
|
-
|
|
46
|
+
} else {
|
|
47
|
+
logger.log(chalk.red(`✗ Validation failed: ${result.path}`));
|
|
48
|
+
result.errors.forEach((err) => logger.log(chalk.yellow(` • ${err}`)));
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
if (!dataplaneResult.valid) {
|
|
51
|
+
logger.log(chalk.yellow(`⚠ ${dataplaneResult.hint}`));
|
|
52
|
+
if (result.valid) {
|
|
53
|
+
logger.log(chalk.yellow(' Wizard/dataplane calls may fail until dataplane credentials are present.'));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
valid: allValid,
|
|
58
|
+
errors: allValid ? [] : [...result.errors, ...(dataplaneResult.valid ? [] : [dataplaneResult.hint])],
|
|
59
|
+
dataplaneValid: dataplaneResult.valid
|
|
60
|
+
};
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
module.exports = { handleSecretsValidate };
|
|
@@ -12,7 +12,14 @@ const logger = require('../utils/logger');
|
|
|
12
12
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
13
13
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
14
14
|
const { normalizeControllerUrl } = require('../core/config');
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
createServiceUser,
|
|
17
|
+
listServiceUsers,
|
|
18
|
+
regenerateSecretServiceUser,
|
|
19
|
+
deleteServiceUser,
|
|
20
|
+
updateGroupsServiceUser,
|
|
21
|
+
updateRedirectUrisServiceUser
|
|
22
|
+
} = require('../api/service-users.api');
|
|
16
23
|
|
|
17
24
|
const ONE_TIME_WARNING =
|
|
18
25
|
'Save this secret now; it will not be shown again.';
|
|
@@ -54,6 +61,12 @@ function extractCreateResponse(response) {
|
|
|
54
61
|
return { clientId, clientSecret };
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
const ID_WIDTH = 38;
|
|
65
|
+
const USERNAME_WIDTH = 22;
|
|
66
|
+
const EMAIL_WIDTH = 28;
|
|
67
|
+
const CLIENT_ID_WIDTH = 24;
|
|
68
|
+
const TABLE_SEPARATOR_LENGTH = 130;
|
|
69
|
+
|
|
57
70
|
/**
|
|
58
71
|
* Log error for failed create response and exit
|
|
59
72
|
* @param {Object} response - API response with success: false
|
|
@@ -74,6 +87,86 @@ function handleCreateError(response) {
|
|
|
74
87
|
process.exit(1);
|
|
75
88
|
}
|
|
76
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Log error for service-user API response and exit
|
|
92
|
+
* @param {Object} response - API response with success: false
|
|
93
|
+
* @param {string} permissionScope - Permission hint: 'read' | 'update' | 'delete'
|
|
94
|
+
*/
|
|
95
|
+
function handleServiceUserApiError(response, permissionScope) {
|
|
96
|
+
const status = response.status;
|
|
97
|
+
const msg = response.formattedError || response.error || 'Request failed';
|
|
98
|
+
if (status === 400) {
|
|
99
|
+
logger.error(chalk.red(`❌ Validation error: ${msg}`));
|
|
100
|
+
} else if (status === 401) {
|
|
101
|
+
logger.error(chalk.red('❌ Unauthorized. Run "aifabrix login" and try again.'));
|
|
102
|
+
} else if (status === 403) {
|
|
103
|
+
logger.error(chalk.red(`❌ Missing permission: service-user:${permissionScope}`));
|
|
104
|
+
logger.error(chalk.gray(`Your account needs the service-user:${permissionScope} permission on the controller.`));
|
|
105
|
+
} else if (status === 404) {
|
|
106
|
+
logger.error(chalk.red('❌ Service user not found.'));
|
|
107
|
+
const detail = response.error || '';
|
|
108
|
+
if (detail) {
|
|
109
|
+
logger.error(chalk.gray(detail));
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
logger.error(chalk.red(`❌ Request failed: ${msg}`));
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve controller URL and auth for list/rotate/delete/update (no create-specific validation)
|
|
119
|
+
* @async
|
|
120
|
+
* @param {Object} options - CLI options (controller optional)
|
|
121
|
+
* @returns {Promise<{ controllerUrl: string, authConfig: Object }>}
|
|
122
|
+
*/
|
|
123
|
+
async function resolveControllerAndAuth(options) {
|
|
124
|
+
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
125
|
+
if (!controllerUrl) {
|
|
126
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const authResult = await getServiceUserAuth(controllerUrl);
|
|
130
|
+
if (!authResult || !authResult.token) {
|
|
131
|
+
logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
|
|
132
|
+
logger.error(chalk.gray('Run: aifabrix login'));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
controllerUrl: authResult.controllerUrl,
|
|
137
|
+
authConfig: { type: 'bearer', token: authResult.token }
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Display service user list as a table (id, username, email, clientId, active).
|
|
143
|
+
* API shape: items have id, username, email, status, federatedIdentity.keycloakClientId.
|
|
144
|
+
* @param {Array<{ id?: string, username?: string, email?: string, status?: string, active?: boolean, clientId?: string, federatedIdentity?: { keycloakClientId?: string } }>} items - Service users
|
|
145
|
+
*/
|
|
146
|
+
function displayServiceUserList(items) {
|
|
147
|
+
logger.log(chalk.bold('\n📋 Service users:\n'));
|
|
148
|
+
if (!items || items.length === 0) {
|
|
149
|
+
logger.log(chalk.gray(' No service users found.\n'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const idCol = 'Id'.padEnd(ID_WIDTH);
|
|
153
|
+
const usernameCol = 'Username'.padEnd(USERNAME_WIDTH);
|
|
154
|
+
const emailCol = 'Email'.padEnd(EMAIL_WIDTH);
|
|
155
|
+
const clientIdCol = 'ClientId'.padEnd(CLIENT_ID_WIDTH);
|
|
156
|
+
const activeCol = 'Active';
|
|
157
|
+
logger.log(chalk.gray(`${idCol}${usernameCol}${emailCol}${clientIdCol}${activeCol}`));
|
|
158
|
+
logger.log(chalk.gray('-'.repeat(TABLE_SEPARATOR_LENGTH)));
|
|
159
|
+
items.forEach((row) => {
|
|
160
|
+
const id = (row.id ?? '').toString().padEnd(ID_WIDTH);
|
|
161
|
+
const username = (row.username ?? '—').padEnd(USERNAME_WIDTH);
|
|
162
|
+
const email = (row.email ?? '—').padEnd(EMAIL_WIDTH);
|
|
163
|
+
const clientId = (row.federatedIdentity?.keycloakClientId ?? row.clientId ?? '—').padEnd(CLIENT_ID_WIDTH);
|
|
164
|
+
const active = row.status === 'active' ? 'yes' : (row.status ?? (row.active === true ? 'yes' : row.active === false ? 'no' : '—'));
|
|
165
|
+
logger.log(`${id}${username}${email}${clientId}${active}`);
|
|
166
|
+
});
|
|
167
|
+
logger.log('');
|
|
168
|
+
}
|
|
169
|
+
|
|
77
170
|
/**
|
|
78
171
|
* Display success output with clientId, clientSecret and one-time warning
|
|
79
172
|
* @param {string} clientId - Service user client ID
|
|
@@ -194,6 +287,142 @@ async function runServiceUserCreate(options = {}) {
|
|
|
194
287
|
displayCreateSuccess(clientId, clientSecret);
|
|
195
288
|
}
|
|
196
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Require service user id option; exit with message if missing
|
|
292
|
+
* @param {string} [id] - Service user ID from options
|
|
293
|
+
* @returns {string} Trimmed id
|
|
294
|
+
*/
|
|
295
|
+
function requireServiceUserId(id) {
|
|
296
|
+
const trimmed = (id && typeof id === 'string' ? id.trim() : '') || '';
|
|
297
|
+
if (!trimmed) {
|
|
298
|
+
logger.error(chalk.red('❌ Service user ID is required. Use --id <uuid>.'));
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
return trimmed;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Run service-user list: call GET /api/v1/service-users and display table
|
|
306
|
+
* @async
|
|
307
|
+
* @param {Object} options - CLI options (controller, page, pageSize, sort, filter, search)
|
|
308
|
+
* @returns {Promise<void>}
|
|
309
|
+
*/
|
|
310
|
+
async function runServiceUserList(options = {}) {
|
|
311
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
312
|
+
const listOptions = {
|
|
313
|
+
page: options.page,
|
|
314
|
+
pageSize: options.pageSize,
|
|
315
|
+
sort: options.sort,
|
|
316
|
+
filter: options.filter,
|
|
317
|
+
search: options.search
|
|
318
|
+
};
|
|
319
|
+
const response = await listServiceUsers(controllerUrl, authConfig, listOptions);
|
|
320
|
+
if (response && response.success === false) {
|
|
321
|
+
handleServiceUserApiError(response, 'read');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const body = response?.data?.data ?? response?.data ?? response ?? {};
|
|
325
|
+
const items = Array.isArray(body) ? body : (body.data ?? []);
|
|
326
|
+
displayServiceUserList(items);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Run service-user rotate-secret: call POST .../regenerate-secret and print new secret once with warning
|
|
331
|
+
* @async
|
|
332
|
+
* @param {Object} options - CLI options (controller, id required)
|
|
333
|
+
* @returns {Promise<void>}
|
|
334
|
+
*/
|
|
335
|
+
async function runServiceUserRotateSecret(options = {}) {
|
|
336
|
+
const id = requireServiceUserId(options.id);
|
|
337
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
338
|
+
const response = await regenerateSecretServiceUser(controllerUrl, authConfig, id);
|
|
339
|
+
if (response && response.success === false) {
|
|
340
|
+
handleServiceUserApiError(response, 'update');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const payload = response?.data?.data ?? response?.data ?? response ?? {};
|
|
344
|
+
const clientSecret = payload?.clientSecret ?? '';
|
|
345
|
+
if (response && response.success === true) {
|
|
346
|
+
logger.log(chalk.bold('\n✓ Secret rotated\n'));
|
|
347
|
+
logger.log(chalk.cyan(' clientSecret: ') + clientSecret);
|
|
348
|
+
logger.log('');
|
|
349
|
+
logger.log(chalk.yellow('⚠ ' + ONE_TIME_WARNING));
|
|
350
|
+
logger.log('');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Run service-user delete: call DELETE .../service-users/{id} (deactivates the user)
|
|
356
|
+
* @async
|
|
357
|
+
* @param {Object} options - CLI options (controller, id required)
|
|
358
|
+
* @returns {Promise<void>}
|
|
359
|
+
*/
|
|
360
|
+
async function runServiceUserDelete(options = {}) {
|
|
361
|
+
const id = requireServiceUserId(options.id);
|
|
362
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
363
|
+
const response = await deleteServiceUser(controllerUrl, authConfig, id);
|
|
364
|
+
if (response && response.success === false) {
|
|
365
|
+
handleServiceUserApiError(response, 'delete');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (response && response.success === true) {
|
|
369
|
+
logger.log(chalk.green('✓ Service user deactivated.\n'));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Run service-user update-groups: call PUT .../groups with groupNames
|
|
375
|
+
* @async
|
|
376
|
+
* @param {Object} options - CLI options (controller, id, groupNames required)
|
|
377
|
+
* @returns {Promise<void>}
|
|
378
|
+
*/
|
|
379
|
+
async function runServiceUserUpdateGroups(options = {}) {
|
|
380
|
+
const id = requireServiceUserId(options.id);
|
|
381
|
+
const groupNames = parseList(options.groupNames);
|
|
382
|
+
if (groupNames.length === 0) {
|
|
383
|
+
logger.error(chalk.red('❌ At least one group name is required. Use --group-names <name1,name2,...>.'));
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
387
|
+
const response = await updateGroupsServiceUser(controllerUrl, authConfig, id, { groupNames });
|
|
388
|
+
if (response && response.success === false) {
|
|
389
|
+
handleServiceUserApiError(response, 'update');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (response && response.success === true) {
|
|
393
|
+
logger.log(chalk.green('✓ Service user groups updated.\n'));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Run service-user update-redirect-uris: call PUT .../redirect-uris (min 1 URI)
|
|
399
|
+
* @async
|
|
400
|
+
* @param {Object} options - CLI options (controller, id, redirectUris required, min 1)
|
|
401
|
+
* @returns {Promise<void>}
|
|
402
|
+
*/
|
|
403
|
+
async function runServiceUserUpdateRedirectUris(options = {}) {
|
|
404
|
+
const id = requireServiceUserId(options.id);
|
|
405
|
+
const redirectUris = parseList(options.redirectUris);
|
|
406
|
+
if (redirectUris.length === 0) {
|
|
407
|
+
logger.error(chalk.red('❌ At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
411
|
+
const response = await updateRedirectUrisServiceUser(controllerUrl, authConfig, id, { redirectUris });
|
|
412
|
+
if (response && response.success === false) {
|
|
413
|
+
handleServiceUserApiError(response, 'update');
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (response && response.success === true) {
|
|
417
|
+
logger.log(chalk.green('✓ Service user redirect URIs updated.\n'));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
197
421
|
module.exports = {
|
|
198
|
-
runServiceUserCreate
|
|
422
|
+
runServiceUserCreate,
|
|
423
|
+
runServiceUserList,
|
|
424
|
+
runServiceUserRotateSecret,
|
|
425
|
+
runServiceUserDelete,
|
|
426
|
+
runServiceUserUpdateGroups,
|
|
427
|
+
runServiceUserUpdateRedirectUris
|
|
199
428
|
};
|
|
@@ -165,6 +165,30 @@ function patchEnvOutputPathForDeployOnly(appName) {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Removes builder app directories for the given app names. Only removes paths under the builder root
|
|
170
|
+
* to prevent path traversal. Uses getBuilderPath for each app and validates before removal.
|
|
171
|
+
*
|
|
172
|
+
* @param {string[]} appNames - Application names (e.g. ['keycloak', 'miso-controller', 'dataplane'])
|
|
173
|
+
* @returns {Promise<void>}
|
|
174
|
+
* @throws {Error} If any path is outside builder root (path traversal attempt)
|
|
175
|
+
*/
|
|
176
|
+
async function cleanBuilderAppDirs(appNames) {
|
|
177
|
+
if (!Array.isArray(appNames) || appNames.length === 0) return;
|
|
178
|
+
const builderRoot = path.resolve(pathsUtil.getBuilderRoot());
|
|
179
|
+
for (const appName of appNames) {
|
|
180
|
+
if (!appName || typeof appName !== 'string') continue;
|
|
181
|
+
const appPath = path.resolve(pathsUtil.getBuilderPath(appName));
|
|
182
|
+
if (!appPath.startsWith(builderRoot + path.sep) && appPath !== builderRoot) {
|
|
183
|
+
throw new Error(`Path ${appPath} is outside builder root ${builderRoot}; refusing to clean`);
|
|
184
|
+
}
|
|
185
|
+
if (fs.existsSync(appPath)) {
|
|
186
|
+
fs.rmSync(appPath, { recursive: true });
|
|
187
|
+
logger.log(chalk.blue(`Cleaned builder/${appName}`));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
168
192
|
/**
|
|
169
193
|
* Ensures builder app directory exists from template if application config is missing.
|
|
170
194
|
* If builder/<appName>/application config does not exist, copies from templates/applications/<appName>.
|
|
@@ -206,6 +230,7 @@ async function ensureAppFromTemplate(appName) {
|
|
|
206
230
|
}
|
|
207
231
|
|
|
208
232
|
module.exports = {
|
|
233
|
+
cleanBuilderAppDirs,
|
|
209
234
|
ensureAppFromTemplate,
|
|
210
235
|
patchEnvOutputPathForDeployOnly,
|
|
211
236
|
validateEnvOutputPathFolderOrNull,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @version 2.0.0
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
const readline = require('readline');
|
|
14
15
|
const chalk = require('chalk');
|
|
15
16
|
const pathsUtil = require('../utils/paths');
|
|
16
17
|
const { loadConfigFile } = require('../utils/config-format');
|
|
@@ -18,13 +19,87 @@ const logger = require('../utils/logger');
|
|
|
18
19
|
const config = require('../core/config');
|
|
19
20
|
const { checkAuthentication } = require('../utils/app-register-auth');
|
|
20
21
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
21
|
-
const { resolveEnvironment } = require('../core/config');
|
|
22
|
+
const { resolveEnvironment, setControllerUrl } = require('../core/config');
|
|
22
23
|
const { registerApplication } = require('../app/register');
|
|
23
24
|
const { rotateSecret } = require('../app/rotate-secret');
|
|
24
25
|
const { checkApplicationExists } = require('../utils/app-existence');
|
|
26
|
+
const { checkHealthEndpoint } = require('../utils/health-check');
|
|
27
|
+
const { validateControllerUrl } = require('../utils/auth-config-validator');
|
|
25
28
|
const app = require('../app');
|
|
26
29
|
const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull } = require('./up-common');
|
|
27
30
|
|
|
31
|
+
const CONTROLLER_HEALTH_PATH = '/health';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if controller is reachable (health endpoint).
|
|
35
|
+
* @param {string} baseUrl - Controller base URL (no trailing slash)
|
|
36
|
+
* @returns {Promise<boolean>} True if healthy
|
|
37
|
+
*/
|
|
38
|
+
async function isControllerHealthy(baseUrl) {
|
|
39
|
+
const healthUrl = `${baseUrl.replace(/\/+$/, '')}${CONTROLLER_HEALTH_PATH}`;
|
|
40
|
+
try {
|
|
41
|
+
return await checkHealthEndpoint(healthUrl, false);
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Prompt user for controller URL when current controller is not available.
|
|
49
|
+
* @param {string} currentUrl - Current controller URL that failed health check
|
|
50
|
+
* @returns {Promise<string|null>} New URL or null if user aborted (empty input)
|
|
51
|
+
*/
|
|
52
|
+
function promptForControllerUrl(currentUrl) {
|
|
53
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
rl.question(
|
|
56
|
+
chalk.yellow(`Controller at ${currentUrl} is not available. Enter controller URL (or press Enter to abort): `),
|
|
57
|
+
(answer) => {
|
|
58
|
+
rl.close();
|
|
59
|
+
const trimmed = (answer || '').trim();
|
|
60
|
+
resolve(trimmed === '' ? null : trimmed);
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve controller URL and ensure it is healthy; if not, prompt once for new URL.
|
|
68
|
+
* @returns {Promise<string>} Controller URL to use
|
|
69
|
+
* @throws {Error} If controller unavailable and user aborts or new URL still unhealthy
|
|
70
|
+
*/
|
|
71
|
+
async function resolveControllerUrlWithHealthCheck() {
|
|
72
|
+
let controllerUrl = await resolveControllerUrl();
|
|
73
|
+
controllerUrl = controllerUrl.replace(/\/+$/, '');
|
|
74
|
+
|
|
75
|
+
let healthy = await isControllerHealthy(controllerUrl);
|
|
76
|
+
if (healthy) {
|
|
77
|
+
return controllerUrl;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.log(chalk.yellow(`\nController at ${controllerUrl} is not responding (health check failed).\n`));
|
|
81
|
+
const newUrl = await promptForControllerUrl(controllerUrl);
|
|
82
|
+
if (!newUrl) {
|
|
83
|
+
throw new Error('Controller URL is required. Run "aifabrix up-dataplane" again and enter a valid controller URL, or set it with: aifabrix auth --set-controller <url>');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
validateControllerUrl(newUrl);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
throw new Error(`Invalid controller URL: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await setControllerUrl(newUrl);
|
|
93
|
+
const normalizedNew = newUrl.trim().replace(/\/+$/, '');
|
|
94
|
+
healthy = await isControllerHealthy(normalizedNew);
|
|
95
|
+
if (!healthy) {
|
|
96
|
+
throw new Error(`Controller at ${normalizedNew} is not responding. Ensure the controller is running and reachable, then run "aifabrix up-dataplane" again.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
logger.log(chalk.green(`✓ Using controller: ${normalizedNew}`));
|
|
100
|
+
return normalizedNew;
|
|
101
|
+
}
|
|
102
|
+
|
|
28
103
|
/**
|
|
29
104
|
* Register or rotate dataplane: if app exists in controller, rotate secret; otherwise register.
|
|
30
105
|
* @async
|
|
@@ -45,6 +120,17 @@ async function registerOrRotateDataplane(options, controllerUrl, environmentKey,
|
|
|
45
120
|
}
|
|
46
121
|
}
|
|
47
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Deploy dataplane app (send manifest to controller).
|
|
125
|
+
* @param {Object} options - Commander options (registry, registryMode, image)
|
|
126
|
+
* @returns {Promise<void>}
|
|
127
|
+
*/
|
|
128
|
+
async function deployDataplaneToController(options) {
|
|
129
|
+
const imageOverride = options.image || (options.registry ? buildDataplaneImageRef(options.registry) : undefined);
|
|
130
|
+
const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
|
|
131
|
+
await app.deployApp('dataplane', deployOpts);
|
|
132
|
+
}
|
|
133
|
+
|
|
48
134
|
/**
|
|
49
135
|
* Build full image ref from registry and dataplane config (registry/name:tag)
|
|
50
136
|
* @param {string} registry - Registry URL
|
|
@@ -85,14 +171,15 @@ async function handleUpDataplane(options = {}) {
|
|
|
85
171
|
}
|
|
86
172
|
logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
|
|
87
173
|
|
|
88
|
-
const
|
|
174
|
+
const controllerUrl = await resolveControllerUrlWithHealthCheck();
|
|
175
|
+
const environmentKey = await resolveEnvironment();
|
|
89
176
|
const authConfig = await checkAuthentication(controllerUrl, environmentKey, { throwOnFailure: true });
|
|
90
177
|
|
|
91
178
|
const cfg = await config.getConfig();
|
|
92
179
|
const environment = (cfg && cfg.environment) ? cfg.environment : 'dev';
|
|
93
180
|
if (environment !== 'dev') {
|
|
94
181
|
throw new Error(
|
|
95
|
-
'Dataplane is only supported in dev environment. Set with: aifabrix auth
|
|
182
|
+
'Dataplane is only supported in dev environment. Set with: aifabrix auth --set-environment dev.'
|
|
96
183
|
);
|
|
97
184
|
}
|
|
98
185
|
logger.log(chalk.green('✓ Logged in and environment is dev'));
|
|
@@ -103,10 +190,7 @@ async function handleUpDataplane(options = {}) {
|
|
|
103
190
|
|
|
104
191
|
await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
|
|
105
192
|
|
|
106
|
-
|
|
107
|
-
const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
|
|
108
|
-
|
|
109
|
-
await app.deployApp('dataplane', deployOpts);
|
|
193
|
+
await deployDataplaneToController(options);
|
|
110
194
|
logger.log('');
|
|
111
195
|
await app.runApp('dataplane', { skipEnvOutputPath: true });
|
|
112
196
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @author AI Fabrix Team
|
|
4
4
|
* @version 2.0.0
|
|
5
5
|
*/
|
|
6
|
+
/* eslint-disable max-lines -- 502 lines; wizard payload and helpers */
|
|
6
7
|
|
|
7
8
|
const chalk = require('chalk');
|
|
8
9
|
const ora = require('ora');
|
|
@@ -181,11 +182,12 @@ function buildConfigPreferences(configPrefs) {
|
|
|
181
182
|
* @param {string} params.mode - Selected mode
|
|
182
183
|
* @param {Object} params.prefs - Configuration preferences
|
|
183
184
|
* @param {string} [params.credentialIdOrKey] - Credential ID or key
|
|
184
|
-
* @param {string} [params.systemIdOrKey] - System ID or key
|
|
185
|
+
* @param {string} [params.systemIdOrKey] - System ID or key (application/system key, not datasource/entity key)
|
|
185
186
|
* @param {string} [params.entityName] - Entity name for multi-entity OpenAPI (from discover-entities)
|
|
187
|
+
* @param {string|null} [params.systemDisplayName] - System-level display name for credential (e.g. 'Hubspot Demo')
|
|
186
188
|
* @returns {Object} Configuration payload
|
|
187
189
|
*/
|
|
188
|
-
function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey, entityName }) {
|
|
190
|
+
function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey, entityName, systemDisplayName }) {
|
|
189
191
|
const detectedTypeValue = detectedType?.recommendedType || detectedType?.apiType || detectedType?.selectedType || 'record-based';
|
|
190
192
|
const payload = {
|
|
191
193
|
openapiSpec,
|
|
@@ -199,6 +201,7 @@ function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credential
|
|
|
199
201
|
if (credentialIdOrKey) payload.credentialIdOrKey = credentialIdOrKey;
|
|
200
202
|
if (systemIdOrKey) payload.systemIdOrKey = systemIdOrKey;
|
|
201
203
|
if (entityName) payload.entityName = entityName;
|
|
204
|
+
if (systemDisplayName) payload.systemDisplayName = systemDisplayName;
|
|
202
205
|
if (prefs.debug) payload.debug = true;
|
|
203
206
|
return payload;
|
|
204
207
|
}
|
|
@@ -298,7 +298,8 @@ async function callGenerateApi(dataplaneUrl, authConfig, options, prefs) {
|
|
|
298
298
|
prefs,
|
|
299
299
|
credentialIdOrKey: options.credentialIdOrKey,
|
|
300
300
|
systemIdOrKey: options.systemIdOrKey,
|
|
301
|
-
entityName: options.entityName
|
|
301
|
+
entityName: options.entityName,
|
|
302
|
+
systemDisplayName: options.systemDisplayName
|
|
302
303
|
});
|
|
303
304
|
return await generateConfig(dataplaneUrl, authConfig, configPayload);
|
|
304
305
|
}
|
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
} = require('./wizard-core');
|
|
25
25
|
const { discoverEntities } = require('../api/wizard.api');
|
|
26
26
|
const { validateEntityNameForOpenApi } = require('../validation/wizard-datasource-validation');
|
|
27
|
+
const { humanizeAppKey } = require('../generator/wizard-prompts-secondary');
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Validate entityName for headless config (throws if invalid)
|
|
@@ -80,6 +81,9 @@ async function executeWizardFromConfig(wizardConfig, dataplaneUrl, authConfig, o
|
|
|
80
81
|
|
|
81
82
|
await validateHeadlessEntityName(source?.entityName, openapiSpec, sourceType, dataplaneUrl, authConfig);
|
|
82
83
|
|
|
84
|
+
const systemDisplayName = wizardConfig.systemDisplayName ||
|
|
85
|
+
(mode === 'create-system' ? humanizeAppKey(appName) : undefined);
|
|
86
|
+
|
|
83
87
|
// Step 5: Generate Configuration
|
|
84
88
|
const { systemConfig, datasourceConfigs, systemKey } = await handleConfigurationGeneration(dataplaneUrl, authConfig, {
|
|
85
89
|
mode,
|
|
@@ -93,7 +97,8 @@ async function executeWizardFromConfig(wizardConfig, dataplaneUrl, authConfig, o
|
|
|
93
97
|
datasourceKeys: source?.datasourceKeys,
|
|
94
98
|
configurationValues: source?.configurationValues,
|
|
95
99
|
entityName: source?.entityName,
|
|
96
|
-
appName
|
|
100
|
+
appName,
|
|
101
|
+
systemDisplayName
|
|
97
102
|
});
|
|
98
103
|
|
|
99
104
|
// Step 6: Validate Configuration
|
package/lib/commands/wizard.js
CHANGED
|
@@ -48,6 +48,7 @@ const {
|
|
|
48
48
|
showWizardConfigSummary,
|
|
49
49
|
ensureIntegrationDir
|
|
50
50
|
} = require('./wizard-helpers');
|
|
51
|
+
const { humanizeAppKey } = require('../generator/wizard-prompts-secondary');
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Create wizard session with given mode and optional systemIdOrKey (no prompts)
|
|
@@ -150,7 +151,8 @@ async function handleInteractiveConfigGeneration(options) {
|
|
|
150
151
|
sourceType: options.sourceType,
|
|
151
152
|
platformKey: options.sourceType === 'known-platform' ? options.sourceData : undefined,
|
|
152
153
|
entityName: options.entityName,
|
|
153
|
-
appName: options.appName
|
|
154
|
+
appName: options.appName,
|
|
155
|
+
systemDisplayName: options.systemDisplayName
|
|
154
156
|
});
|
|
155
157
|
|
|
156
158
|
return {
|
|
@@ -190,7 +192,7 @@ async function handleConfigurationReview(dataplaneUrl, authConfig, sessionId, sy
|
|
|
190
192
|
logger.warn('Preview unavailable, showing full configuration.');
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
const reviewResult = await promptForConfigReview({ preview, systemConfig, datasourceConfigs });
|
|
195
|
+
const reviewResult = await promptForConfigReview({ preview, systemConfig, datasourceConfigs, appKey: opts.appKey });
|
|
194
196
|
|
|
195
197
|
if (reviewResult.action === 'cancel') {
|
|
196
198
|
logger.log(chalk.yellow('Wizard cancelled.'));
|
|
@@ -251,7 +253,8 @@ async function doWizardSteps(appKey, dataplaneUrl, authConfig, sessionId, flowOp
|
|
|
251
253
|
sourceData,
|
|
252
254
|
entityName: entityName || undefined,
|
|
253
255
|
appName: appKey,
|
|
254
|
-
debug
|
|
256
|
+
debug,
|
|
257
|
+
systemDisplayName: flowOpts.systemDisplayName
|
|
255
258
|
});
|
|
256
259
|
const { systemConfig, datasourceConfigs, systemKey, preferences: savedPrefs } = genResult;
|
|
257
260
|
state.preferences = savedPrefs || {};
|
|
@@ -314,7 +317,7 @@ async function runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sess
|
|
|
314
317
|
* @returns {Promise<void>} Resolves when wizard flow completes
|
|
315
318
|
*/
|
|
316
319
|
async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}) {
|
|
317
|
-
const { mode, systemIdOrKey, configPath, debug } = flowOpts;
|
|
320
|
+
const { mode, systemIdOrKey, configPath, debug, systemDisplayName } = flowOpts;
|
|
318
321
|
|
|
319
322
|
if (debug) {
|
|
320
323
|
logger.log(chalk.gray(`[DEBUG] Wizard debug mode enabled for app: ${appKey}`));
|
|
@@ -329,7 +332,8 @@ async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}
|
|
|
329
332
|
systemIdOrKey,
|
|
330
333
|
platforms,
|
|
331
334
|
configPath,
|
|
332
|
-
debug: flowOpts.debug
|
|
335
|
+
debug: flowOpts.debug,
|
|
336
|
+
systemDisplayName
|
|
333
337
|
});
|
|
334
338
|
if (!state) return;
|
|
335
339
|
|
|
@@ -474,12 +478,15 @@ async function handleWizardInteractive(options) {
|
|
|
474
478
|
if (!resolved) return;
|
|
475
479
|
const { appKey, configPath, dataplaneUrl, authConfig } = resolved;
|
|
476
480
|
const systemIdOrKey = mode === 'add-datasource' ? resolved.systemIdOrKey : undefined;
|
|
481
|
+
const systemDisplayName = options.systemDisplayName || options.displayName ||
|
|
482
|
+
(mode === 'create-system' ? humanizeAppKey(appKey) : undefined);
|
|
477
483
|
try {
|
|
478
484
|
await executeWizardFlow(appKey, dataplaneUrl, authConfig, {
|
|
479
485
|
mode,
|
|
480
486
|
systemIdOrKey,
|
|
481
487
|
configPath,
|
|
482
|
-
debug: options.debug
|
|
488
|
+
debug: options.debug,
|
|
489
|
+
systemDisplayName
|
|
483
490
|
});
|
|
484
491
|
logger.log(chalk.gray(`To change settings, edit integration/${appKey}/wizard.yaml and run: aifabrix wizard ${appKey}`));
|
|
485
492
|
} catch (error) {
|
|
@@ -14,6 +14,7 @@ const path = require('path');
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const config = require('./config');
|
|
16
16
|
const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
|
|
17
|
+
const { ensureSecureFilePermissions } = require('../utils/secure-file-permissions');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Parse .env-style content into key-value map (excludes comments and empty lines).
|
|
@@ -57,6 +58,7 @@ async function readAndDecryptAdminSecrets(adminSecretsPath) {
|
|
|
57
58
|
if (!fs.existsSync(resolvedPath)) {
|
|
58
59
|
throw new Error(`Admin secrets file not found: ${resolvedPath}. Run 'aifabrix up-infra' or ensure admin-secrets.env exists.`);
|
|
59
60
|
}
|
|
61
|
+
ensureSecureFilePermissions(resolvedPath);
|
|
60
62
|
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
61
63
|
const raw = parseAdminEnvContent(content);
|
|
62
64
|
const encryptionKey = await config.getSecretsEncryptionKey();
|