@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
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const fs = require('fs');
|
|
14
|
+
const chalk = require('chalk');
|
|
14
15
|
const handlebars = require('handlebars');
|
|
15
16
|
const secrets = require('../core/secrets');
|
|
16
17
|
const logger = require('../utils/logger');
|
|
17
18
|
const dockerUtils = require('../utils/docker');
|
|
18
19
|
const paths = require('../utils/paths');
|
|
20
|
+
const secretsEnsure = require('../core/secrets-ensure');
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Gets infrastructure directory name based on developer ID
|
|
@@ -53,24 +55,92 @@ async function checkDockerAvailability() {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
/** Default admin password for new local installations when admin-secrets.env is empty */
|
|
59
|
+
const DEFAULT_ADMIN_PASSWORD = 'admin123';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Log hint to reset Postgres volume when admin password was changed after first init.
|
|
63
|
+
* @param {string} infraDir - Path to infra directory
|
|
64
|
+
*/
|
|
65
|
+
function logVolumeResetHint(infraDir) {
|
|
66
|
+
logger.log(chalk.yellow(
|
|
67
|
+
'If Postgres was already started with a different password, login will fail until you reset the volume. ' +
|
|
68
|
+
`Run: cd ${infraDir} && docker compose -f compose.yaml -p aifabrix down -v , then run 'aifabrix up-infra --adminPwd <password>' again.`
|
|
69
|
+
));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Apply password to admin-secrets file content (all three password keys).
|
|
74
|
+
* @param {string} content - Current file content
|
|
75
|
+
* @param {string} password - Password to set
|
|
76
|
+
* @returns {string} Updated content
|
|
77
|
+
*/
|
|
78
|
+
function applyPasswordToAdminSecretsContent(content, password) {
|
|
79
|
+
return content
|
|
80
|
+
.replace(/^POSTGRES_PASSWORD=.*$/m, `POSTGRES_PASSWORD=${password}`)
|
|
81
|
+
.replace(/^PGADMIN_DEFAULT_PASSWORD=.*$/m, `PGADMIN_DEFAULT_PASSWORD=${password}`)
|
|
82
|
+
.replace(/^REDIS_COMMANDER_PASSWORD=.*$/m, `REDIS_COMMANDER_PASSWORD=${password}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
56
85
|
/**
|
|
57
|
-
*
|
|
86
|
+
* Sync postgres-passwordKeyVault to the main secrets store (file or remote).
|
|
87
|
+
* @param {string} password - Password to store
|
|
88
|
+
*/
|
|
89
|
+
async function syncPostgresPasswordToStore(password) {
|
|
90
|
+
try {
|
|
91
|
+
await secretsEnsure.setSecretInStore('postgres-passwordKeyVault', password);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
logger.warn(`Could not sync postgres-passwordKeyVault to secrets store: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Ensure admin secrets file exists and set admin password.
|
|
99
|
+
* When adminPwd is provided, update POSTGRES_PASSWORD, PGADMIN_DEFAULT_PASSWORD, REDIS_COMMANDER_PASSWORD
|
|
100
|
+
* in admin-secrets.env (overwrites existing values). Otherwise only backfill empty fields.
|
|
101
|
+
*
|
|
58
102
|
* @async
|
|
103
|
+
* @param {Object} [options] - Options
|
|
104
|
+
* @param {string} [options.adminPwd] - Override admin password for Postgres, pgAdmin, Redis Commander (updates file when provided)
|
|
59
105
|
* @returns {Promise<string>} Path to admin secrets file
|
|
60
106
|
*/
|
|
61
|
-
async function ensureAdminSecrets() {
|
|
107
|
+
async function ensureAdminSecrets(options = {}) {
|
|
108
|
+
const adminPwdOverride = options.adminPwd && typeof options.adminPwd === 'string' && options.adminPwd.trim() !== ''
|
|
109
|
+
? options.adminPwd.trim()
|
|
110
|
+
: null;
|
|
111
|
+
const passwordToUse = adminPwdOverride || DEFAULT_ADMIN_PASSWORD;
|
|
112
|
+
|
|
62
113
|
const adminSecretsPath = path.join(paths.getAifabrixHome(), 'admin-secrets.env');
|
|
63
114
|
if (!fs.existsSync(adminSecretsPath)) {
|
|
64
115
|
logger.log('Generating admin-secrets.env...');
|
|
65
|
-
await secrets.generateAdminSecretsEnv();
|
|
116
|
+
await secrets.generateAdminSecretsEnv(undefined);
|
|
117
|
+
}
|
|
118
|
+
let content = fs.readFileSync(adminSecretsPath, 'utf8');
|
|
119
|
+
const needsBackfill = /^POSTGRES_PASSWORD=\s*$/m.test(content) ||
|
|
120
|
+
/^PGADMIN_DEFAULT_PASSWORD=\s*$/m.test(content) ||
|
|
121
|
+
/^REDIS_COMMANDER_PASSWORD=\s*$/m.test(content);
|
|
122
|
+
const shouldOverwriteWithAdminPwd = adminPwdOverride !== null;
|
|
123
|
+
|
|
124
|
+
if (shouldOverwriteWithAdminPwd) {
|
|
125
|
+
content = applyPasswordToAdminSecretsContent(content, passwordToUse);
|
|
126
|
+
fs.writeFileSync(adminSecretsPath, content, { mode: 0o600 });
|
|
127
|
+
logger.log('Updated admin password in admin-secrets.env.');
|
|
128
|
+
await syncPostgresPasswordToStore(passwordToUse);
|
|
129
|
+
logVolumeResetHint(path.join(paths.getAifabrixHome(), getInfraDirName(0)));
|
|
130
|
+
} else if (needsBackfill) {
|
|
131
|
+
content = applyPasswordToAdminSecretsContent(content, passwordToUse);
|
|
132
|
+
fs.writeFileSync(adminSecretsPath, content, { mode: 0o600 });
|
|
133
|
+
logger.log('Set default admin password in admin-secrets.env for local use.');
|
|
66
134
|
}
|
|
67
135
|
return adminSecretsPath;
|
|
68
136
|
}
|
|
69
137
|
|
|
70
138
|
/**
|
|
71
|
-
* Generates pgAdmin4
|
|
139
|
+
* Generates pgAdmin4 servers.json only. pgpass is not written to disk (ISO 27K);
|
|
140
|
+
* it is created temporarily in startDockerServicesAndConfigure and deleted after copy to container.
|
|
141
|
+
*
|
|
72
142
|
* @param {string} infraDir - Infrastructure directory path
|
|
73
|
-
* @param {string} postgresPassword - PostgreSQL password
|
|
143
|
+
* @param {string} postgresPassword - PostgreSQL password (for servers.json PassFile reference only; password not stored in file)
|
|
74
144
|
*/
|
|
75
145
|
function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
76
146
|
const serversJsonTemplatePath = path.join(__dirname, '..', '..', 'templates', 'infra', 'servers.json.hbs');
|
|
@@ -83,10 +153,6 @@ function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
|
83
153
|
const serversJsonContent = serversJsonTemplate({ postgresPassword });
|
|
84
154
|
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
85
155
|
fs.writeFileSync(serversJsonPath, serversJsonContent, { mode: 0o644 });
|
|
86
|
-
|
|
87
|
-
const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
|
|
88
|
-
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
89
|
-
fs.writeFileSync(pgpassPath, pgpassContent, { mode: 0o600 });
|
|
90
156
|
}
|
|
91
157
|
|
|
92
158
|
/**
|
|
@@ -117,10 +183,8 @@ function extractPasswordFromUrlOrValue(urlOrPassword) {
|
|
|
117
183
|
|
|
118
184
|
/**
|
|
119
185
|
* Ensures Postgres init script exists for miso-controller app (database miso, user miso_user).
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* started before this script existed, run `aifabrix down-infra -v` then `aifabrix up-infra` to re-init, or
|
|
123
|
-
* create the database and user manually (e.g. via pgAdmin or psql).
|
|
186
|
+
* Reads password from configured store (file or remote). Fails with clear message if secret is missing.
|
|
187
|
+
* Run ensureInfraSecrets before startInfra so databases-miso-controller-0-passwordKeyVault exists.
|
|
124
188
|
*
|
|
125
189
|
* @async
|
|
126
190
|
* @param {string} infraDir - Infrastructure directory path
|
|
@@ -132,15 +196,24 @@ async function ensureMisoInitScript(infraDir) {
|
|
|
132
196
|
fs.mkdirSync(initScriptsDir, { recursive: true });
|
|
133
197
|
}
|
|
134
198
|
|
|
135
|
-
|
|
199
|
+
const secretKey = 'databases-miso-controller-0-passwordKeyVault';
|
|
200
|
+
let password;
|
|
136
201
|
try {
|
|
137
202
|
const loaded = await secrets.loadSecrets(undefined);
|
|
138
|
-
const urlOrPassword = loaded['databases-miso-controller-0-
|
|
139
|
-
loaded['databases-miso-controller-0-urlKeyVault'];
|
|
203
|
+
const urlOrPassword = loaded[secretKey] || loaded['databases-miso-controller-0-urlKeyVault'];
|
|
140
204
|
const extracted = extractPasswordFromUrlOrValue(urlOrPassword);
|
|
141
|
-
if (extracted !== null)
|
|
142
|
-
|
|
143
|
-
|
|
205
|
+
if (extracted !== null && extracted.trim() !== '') {
|
|
206
|
+
password = extracted;
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Secret ${secretKey} not found or could not load secrets. Run "aifabrix up-infra" to ensure infra secrets, or add it to your secrets file. ${err.message}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (!password || password.trim() === '') {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Secret ${secretKey} is missing or empty. Run "aifabrix up-infra" to ensure infra secrets, or add it to your secrets file.`
|
|
216
|
+
);
|
|
144
217
|
}
|
|
145
218
|
|
|
146
219
|
const passwordEscaped = escapePgString(password);
|
|
@@ -183,9 +256,19 @@ function prepareInfraDirectory(devId, adminSecretsPath) {
|
|
|
183
256
|
fs.mkdirSync(infraDir, { recursive: true });
|
|
184
257
|
}
|
|
185
258
|
|
|
259
|
+
const oldPgpassPath = path.join(infraDir, 'pgpass');
|
|
260
|
+
if (fs.existsSync(oldPgpassPath)) {
|
|
261
|
+
try {
|
|
262
|
+
fs.unlinkSync(oldPgpassPath);
|
|
263
|
+
} catch {
|
|
264
|
+
// Ignore
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
186
268
|
const adminSecretsContent = fs.readFileSync(adminSecretsPath, 'utf8');
|
|
187
269
|
const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
|
|
188
|
-
const
|
|
270
|
+
const raw = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
|
|
271
|
+
const postgresPassword = (raw && raw.trim()) || DEFAULT_ADMIN_PASSWORD;
|
|
189
272
|
generatePgAdminConfig(infraDir, postgresPassword);
|
|
190
273
|
|
|
191
274
|
return { infraDir, postgresPassword };
|
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
ensureMisoInitScript,
|
|
27
27
|
registerHandlebarsHelper
|
|
28
28
|
} = require('./helpers');
|
|
29
|
+
const secretsEnsure = require('../core/secrets-ensure');
|
|
29
30
|
const {
|
|
30
31
|
buildTraefikConfig,
|
|
31
32
|
validateTraefikConfig,
|
|
@@ -36,17 +37,22 @@ const {
|
|
|
36
37
|
startDockerServicesAndConfigure,
|
|
37
38
|
checkInfraHealth
|
|
38
39
|
} = require('./services');
|
|
40
|
+
// Lazy require to avoid circular dependency: infra -> app/down -> run-helpers -> infra
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Prepares infrastructure environment
|
|
44
|
+
* Ensures infra secrets exist, then admin-secrets.env, then miso init script.
|
|
45
|
+
*
|
|
42
46
|
* @async
|
|
43
47
|
* @function prepareInfrastructureEnvironment
|
|
44
48
|
* @param {string|number|null} developerId - Developer ID
|
|
49
|
+
* @param {Object} [options] - Options (traefik, adminPwd)
|
|
45
50
|
* @returns {Promise<Object>} Prepared environment configuration
|
|
46
51
|
*/
|
|
47
|
-
async function prepareInfrastructureEnvironment(developerId) {
|
|
52
|
+
async function prepareInfrastructureEnvironment(developerId, options = {}) {
|
|
48
53
|
await checkDockerAvailability();
|
|
49
|
-
|
|
54
|
+
await secretsEnsure.ensureInfraSecrets({ adminPwd: options.adminPwd });
|
|
55
|
+
const adminSecretsPath = await ensureAdminSecrets({ adminPwd: options.adminPwd });
|
|
50
56
|
|
|
51
57
|
const devId = developerId || await config.getDeveloperId();
|
|
52
58
|
const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
@@ -82,8 +88,8 @@ async function prepareInfrastructureEnvironment(developerId) {
|
|
|
82
88
|
* // Infrastructure services are now running
|
|
83
89
|
*/
|
|
84
90
|
async function startInfra(developerId = null, options = {}) {
|
|
85
|
-
const { devId, idNum, ports, templatePath, infraDir
|
|
86
|
-
const { traefik = false } = options;
|
|
91
|
+
const { devId, idNum, ports, templatePath, infraDir } = await prepareInfrastructureEnvironment(developerId, options);
|
|
92
|
+
const { traefik = false, pgadmin = true, redisCommander = true } = options;
|
|
87
93
|
const traefikConfig = buildTraefikConfig(traefik);
|
|
88
94
|
const validation = validateTraefikConfig(traefikConfig);
|
|
89
95
|
if (!validation.valid) {
|
|
@@ -94,18 +100,67 @@ async function startInfra(developerId = null, options = {}) {
|
|
|
94
100
|
registerHandlebarsHelper();
|
|
95
101
|
|
|
96
102
|
// Generate compose file
|
|
97
|
-
const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir, {
|
|
103
|
+
const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir, {
|
|
104
|
+
traefik: traefikConfig,
|
|
105
|
+
pgadmin: { enabled: !!pgadmin },
|
|
106
|
+
redisCommander: { enabled: !!redisCommander }
|
|
107
|
+
});
|
|
98
108
|
|
|
99
109
|
try {
|
|
100
|
-
await startDockerServicesAndConfigure(composePath, devId, idNum,
|
|
110
|
+
await startDockerServicesAndConfigure(composePath, devId, idNum, infraDir, {
|
|
111
|
+
pgadmin: !!pgadmin,
|
|
112
|
+
redisCommander: !!redisCommander,
|
|
113
|
+
traefik: !!traefik
|
|
114
|
+
});
|
|
101
115
|
} finally {
|
|
102
116
|
// Keep the compose file for stop commands
|
|
103
117
|
}
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
/**
|
|
107
|
-
* Stops and removes
|
|
108
|
-
*
|
|
121
|
+
* Stops and removes all app containers for the current developer (same network).
|
|
122
|
+
* @param {string} devId - Developer ID
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
async function stopAllAppContainers(devId) {
|
|
126
|
+
const containerNames = await statusHelpers.listAppContainerNamesForDeveloper(devId, { includeExited: true });
|
|
127
|
+
for (const name of containerNames) {
|
|
128
|
+
try {
|
|
129
|
+
await execAsyncWithCwd(`docker rm -f ${name}`);
|
|
130
|
+
logger.log(`Stopped and removed container: ${name}`);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logger.log(`Container ${name} not running or already removed`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Removes Docker volumes for the given app names (current developer).
|
|
139
|
+
* @param {string[]} appNames - Application names
|
|
140
|
+
* @param {string} devId - Developer ID
|
|
141
|
+
* @returns {Promise<void>}
|
|
142
|
+
*/
|
|
143
|
+
async function removeAppVolumes(appNames, devId) {
|
|
144
|
+
const { getAppVolumeName } = require('../app/down');
|
|
145
|
+
const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
146
|
+
for (const appName of appNames) {
|
|
147
|
+
const primaryName = getAppVolumeName(appName, devId);
|
|
148
|
+
const legacyDev0Name = idNum === 0 ? `aifabrix_dev0_${appName}_data` : null;
|
|
149
|
+
const candidates = Array.from(new Set([primaryName, legacyDev0Name].filter(Boolean)));
|
|
150
|
+
for (const volumeName of candidates) {
|
|
151
|
+
try {
|
|
152
|
+
await execAsyncWithCwd(`docker volume rm -f ${volumeName}`);
|
|
153
|
+
logger.log(`Removed volume: ${volumeName}`);
|
|
154
|
+
} catch {
|
|
155
|
+
logger.log(`Volume ${volumeName} not found or already removed`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Stops and removes local infrastructure services and all application containers
|
|
163
|
+
* on the same network. Cleanly shuts down infra and app containers.
|
|
109
164
|
*
|
|
110
165
|
* @async
|
|
111
166
|
* @function stopInfra
|
|
@@ -114,7 +169,7 @@ async function startInfra(developerId = null, options = {}) {
|
|
|
114
169
|
*
|
|
115
170
|
* @example
|
|
116
171
|
* await stopInfra();
|
|
117
|
-
* // All infrastructure containers are stopped and removed
|
|
172
|
+
* // All infrastructure and app containers on the same network are stopped and removed
|
|
118
173
|
*/
|
|
119
174
|
async function stopInfra() {
|
|
120
175
|
const devId = await config.getDeveloperId();
|
|
@@ -130,6 +185,8 @@ async function stopInfra() {
|
|
|
130
185
|
}
|
|
131
186
|
|
|
132
187
|
try {
|
|
188
|
+
logger.log('Stopping application containers on the same network...');
|
|
189
|
+
await stopAllAppContainers(devId);
|
|
133
190
|
logger.log('Stopping infrastructure services...');
|
|
134
191
|
const projectName = getInfraProjectName(devId);
|
|
135
192
|
const composeCmd = await dockerUtils.getComposeCommand();
|
|
@@ -141,8 +198,35 @@ async function stopInfra() {
|
|
|
141
198
|
}
|
|
142
199
|
|
|
143
200
|
/**
|
|
144
|
-
* Stops
|
|
145
|
-
*
|
|
201
|
+
* Stops all app containers on the network and removes their volumes.
|
|
202
|
+
* @param {string} devId - Developer ID
|
|
203
|
+
* @returns {Promise<void>}
|
|
204
|
+
*/
|
|
205
|
+
async function stopAllAppContainersAndVolumes(devId) {
|
|
206
|
+
const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
207
|
+
const containerNames = await statusHelpers.listAppContainerNamesForDeveloper(devId, { includeExited: true });
|
|
208
|
+
for (const name of containerNames) {
|
|
209
|
+
try {
|
|
210
|
+
await execAsyncWithCwd(`docker rm -f ${name}`);
|
|
211
|
+
logger.log(`Stopped and removed container: ${name}`);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
logger.log(`Container ${name} not running or already removed`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const appNames = [...new Set(
|
|
217
|
+
containerNames
|
|
218
|
+
.map(n => statusHelpers.extractAppName(n, devIdNum, devId))
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
)];
|
|
221
|
+
if (appNames.length > 0) {
|
|
222
|
+
logger.log('Removing application volumes...');
|
|
223
|
+
await removeAppVolumes(appNames, devId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Stops and removes local infrastructure services and all application containers
|
|
229
|
+
* on the same network, and removes all volumes (infra and app data).
|
|
146
230
|
*
|
|
147
231
|
* @async
|
|
148
232
|
* @function stopInfraWithVolumes
|
|
@@ -151,7 +235,7 @@ async function stopInfra() {
|
|
|
151
235
|
*
|
|
152
236
|
* @example
|
|
153
237
|
* await stopInfraWithVolumes();
|
|
154
|
-
* // All infrastructure containers and
|
|
238
|
+
* // All infrastructure and app containers and volumes are removed
|
|
155
239
|
*/
|
|
156
240
|
async function stopInfraWithVolumes() {
|
|
157
241
|
const devId = await config.getDeveloperId();
|
|
@@ -167,6 +251,8 @@ async function stopInfraWithVolumes() {
|
|
|
167
251
|
}
|
|
168
252
|
|
|
169
253
|
try {
|
|
254
|
+
logger.log('Stopping application containers on the same network...');
|
|
255
|
+
await stopAllAppContainersAndVolumes(devId);
|
|
170
256
|
logger.log('Stopping infrastructure services and removing all data...');
|
|
171
257
|
const projectName = getInfraProjectName(devId);
|
|
172
258
|
const composeCmd = await dockerUtils.getComposeCommand();
|
|
@@ -17,6 +17,7 @@ const containerUtils = require('../utils/infra-containers');
|
|
|
17
17
|
const dockerUtils = require('../utils/docker');
|
|
18
18
|
const config = require('../core/config');
|
|
19
19
|
const { getInfraProjectName } = require('./helpers');
|
|
20
|
+
const adminSecrets = require('../core/admin-secrets');
|
|
20
21
|
|
|
21
22
|
const execAsync = promisify(exec);
|
|
22
23
|
|
|
@@ -74,42 +75,102 @@ async function copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPa
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
|
-
*
|
|
78
|
+
* Prepare run env file from decrypted admin secrets.
|
|
79
|
+
* @async
|
|
80
|
+
* @param {string} infraDir - Infrastructure directory
|
|
81
|
+
* @returns {Promise<{ adminObj: Object, runEnvPath: string }>}
|
|
82
|
+
*/
|
|
83
|
+
async function prepareRunEnv(infraDir) {
|
|
84
|
+
const runEnvPath = path.join(infraDir, '.env.run');
|
|
85
|
+
const adminObj = await adminSecrets.readAndDecryptAdminSecrets();
|
|
86
|
+
const content = adminSecrets.envObjectToContent(adminObj);
|
|
87
|
+
fs.writeFileSync(runEnvPath, content, { mode: 0o600 });
|
|
88
|
+
return { adminObj, runEnvPath };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Write pgpass file and copy pgAdmin config into container.
|
|
93
|
+
* @async
|
|
94
|
+
* @param {string} infraDir - Infrastructure directory
|
|
95
|
+
* @param {Object} adminObj - Decrypted admin secrets object
|
|
96
|
+
* @param {string} devId - Developer ID
|
|
97
|
+
* @param {number} idNum - Developer ID number
|
|
98
|
+
* @returns {Promise<string>} Path to pgpass run file
|
|
99
|
+
*/
|
|
100
|
+
async function writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum) {
|
|
101
|
+
const pgpassRunPath = path.join(infraDir, '.pgpass.run');
|
|
102
|
+
const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
|
|
103
|
+
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
104
|
+
const postgresPassword = adminObj.POSTGRES_PASSWORD || '';
|
|
105
|
+
const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
|
|
106
|
+
fs.writeFileSync(pgpassRunPath, pgpassContent, { mode: 0o600 });
|
|
107
|
+
await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassRunPath);
|
|
108
|
+
return pgpassRunPath;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove temporary run files (env and pgpass) if they exist.
|
|
113
|
+
* @param {string} runEnvPath - Path to .env.run
|
|
114
|
+
* @param {string} [pgpassRunPath] - Path to .pgpass.run
|
|
115
|
+
*/
|
|
116
|
+
function cleanupRunFiles(runEnvPath, pgpassRunPath) {
|
|
117
|
+
try {
|
|
118
|
+
if (fs.existsSync(runEnvPath)) fs.unlinkSync(runEnvPath);
|
|
119
|
+
if (pgpassRunPath && fs.existsSync(pgpassRunPath)) fs.unlinkSync(pgpassRunPath);
|
|
120
|
+
} catch {
|
|
121
|
+
// Ignore unlink errors
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Starts Docker services and configures pgAdmin (when enabled).
|
|
127
|
+
* Writes decrypted admin secrets to a temporary .env in infra dir, runs compose, then deletes the file (ISO 27K).
|
|
128
|
+
*
|
|
78
129
|
* @async
|
|
79
130
|
* @function startDockerServicesAndConfigure
|
|
80
131
|
* @param {string} composePath - Compose file path
|
|
81
132
|
* @param {string} devId - Developer ID
|
|
82
133
|
* @param {number} idNum - Developer ID number
|
|
83
|
-
* @param {string} adminSecretsPath - Admin secrets path
|
|
84
134
|
* @param {string} infraDir - Infrastructure directory
|
|
135
|
+
* @param {Object} [opts] - Options (pgadmin, redisCommander, traefik)
|
|
85
136
|
*/
|
|
86
|
-
async function startDockerServicesAndConfigure(composePath, devId, idNum,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
async function startDockerServicesAndConfigure(composePath, devId, idNum, infraDir, opts = {}) {
|
|
138
|
+
let runEnvPath;
|
|
139
|
+
let pgpassRunPath;
|
|
140
|
+
let adminObj;
|
|
141
|
+
const { pgadmin = true, redisCommander = true, traefik = false } = opts;
|
|
142
|
+
try {
|
|
143
|
+
({ adminObj, runEnvPath } = await prepareRunEnv(infraDir));
|
|
144
|
+
} catch (err) {
|
|
145
|
+
throw new Error(`Failed to prepare infra env: ${err.message}`);
|
|
146
|
+
}
|
|
96
147
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
148
|
+
try {
|
|
149
|
+
const projectName = getInfraProjectName(devId);
|
|
150
|
+
await startDockerServices(composePath, projectName, runEnvPath, infraDir);
|
|
151
|
+
if (pgadmin) {
|
|
152
|
+
pgpassRunPath = await writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum);
|
|
153
|
+
}
|
|
154
|
+
await waitForServices(devId, { pgadmin, redisCommander, traefik });
|
|
155
|
+
logger.log('All services are healthy and ready');
|
|
156
|
+
} finally {
|
|
157
|
+
cleanupRunFiles(runEnvPath, pgpassRunPath);
|
|
158
|
+
}
|
|
100
159
|
}
|
|
101
160
|
|
|
102
161
|
/**
|
|
103
162
|
* Waits for services to be healthy
|
|
104
163
|
* @private
|
|
105
|
-
* @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
164
|
+
* @param {number|string|null} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
165
|
+
* @param {Object} [opts] - Options (pgadmin, redisCommander, traefik) - which optional services to expect
|
|
106
166
|
*/
|
|
107
|
-
async function waitForServices(devId = null) {
|
|
167
|
+
async function waitForServices(devId = null, opts = {}) {
|
|
108
168
|
const maxAttempts = 30;
|
|
109
169
|
const delay = 2000; // 2 seconds
|
|
170
|
+
const { pgadmin = true, redisCommander = true, traefik = false } = opts;
|
|
110
171
|
|
|
111
172
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
112
|
-
const health = await checkInfraHealth(devId);
|
|
173
|
+
const health = await checkInfraHealth(devId, { pgadmin, redisCommander, traefik });
|
|
113
174
|
const allHealthy = Object.values(health).every(status => status === 'healthy');
|
|
114
175
|
|
|
115
176
|
if (allHealthy) {
|
|
@@ -127,15 +188,17 @@ async function waitForServices(devId = null) {
|
|
|
127
188
|
|
|
128
189
|
/**
|
|
129
190
|
* Checks if infrastructure services are running
|
|
130
|
-
* Validates that all
|
|
191
|
+
* Validates that all expected services are healthy and accessible
|
|
131
192
|
*
|
|
132
193
|
* @async
|
|
133
194
|
* @function checkInfraHealth
|
|
134
195
|
* @param {number|string|null} [devId] - Developer ID (null = use current)
|
|
135
196
|
* @param {Object} [options] - Options
|
|
136
197
|
* @param {boolean} [options.strict=false] - When true, only consider current dev's containers (no fallback to dev 0); use for up-miso and status consistency
|
|
198
|
+
* @param {boolean} [options.pgadmin=true] - Include pgAdmin in health check
|
|
199
|
+
* @param {boolean} [options.redisCommander=true] - Include Redis Commander in health check
|
|
200
|
+
* @param {boolean} [options.traefik=false] - Include Traefik in health check
|
|
137
201
|
* @returns {Promise<Object>} Health status of each service
|
|
138
|
-
* @throws {Error} If health check fails
|
|
139
202
|
*
|
|
140
203
|
* @example
|
|
141
204
|
* const health = await checkInfraHealth();
|
|
@@ -144,7 +207,10 @@ async function waitForServices(devId = null) {
|
|
|
144
207
|
async function checkInfraHealth(devId = null, options = {}) {
|
|
145
208
|
const developerId = devId || await config.getDeveloperId();
|
|
146
209
|
const servicesWithHealthCheck = ['postgres', 'redis'];
|
|
147
|
-
const servicesWithoutHealthCheck = [
|
|
210
|
+
const servicesWithoutHealthCheck = [];
|
|
211
|
+
if (options.pgadmin !== false) servicesWithoutHealthCheck.push('pgadmin');
|
|
212
|
+
if (options.redisCommander !== false) servicesWithoutHealthCheck.push('redis-commander');
|
|
213
|
+
if (options.traefik === true) servicesWithoutHealthCheck.push('traefik');
|
|
148
214
|
const health = {};
|
|
149
215
|
const lookupOptions = options.strict ? { strict: true } : {};
|
|
150
216
|
|
|
@@ -153,7 +219,7 @@ async function checkInfraHealth(devId = null, options = {}) {
|
|
|
153
219
|
health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId, lookupOptions);
|
|
154
220
|
}
|
|
155
221
|
|
|
156
|
-
// Check if services without health checks are running
|
|
222
|
+
// Check if optional services without health checks are running
|
|
157
223
|
for (const service of servicesWithoutHealthCheck) {
|
|
158
224
|
health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId, lookupOptions);
|
|
159
225
|
}
|
|
@@ -115,11 +115,6 @@
|
|
|
115
115
|
"minimum": 1,
|
|
116
116
|
"maximum": 65535
|
|
117
117
|
},
|
|
118
|
-
"deploymentKey": {
|
|
119
|
-
"type": "string",
|
|
120
|
-
"description": "SHA256 hash of deployment manifest (excluding deploymentKey field)",
|
|
121
|
-
"pattern": "^[a-f0-9]{64}$"
|
|
122
|
-
},
|
|
123
118
|
"requiresDatabase": {
|
|
124
119
|
"type": "boolean",
|
|
125
120
|
"description": "Whether application requires database"
|
|
@@ -137,6 +132,14 @@
|
|
|
137
132
|
"type": "string",
|
|
138
133
|
"description": "Database name",
|
|
139
134
|
"pattern": "^[a-z0-9_-]+$"
|
|
135
|
+
},
|
|
136
|
+
"extensions": {
|
|
137
|
+
"type": "array",
|
|
138
|
+
"description": "PostgreSQL extension names to create in this database during db-init (e.g. pgcrypto, uuid-ossp, vector, btree_gin, btree_gist). If the database name ends with 'vector', the vector extension is still added automatically if not listed.",
|
|
139
|
+
"items": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"pattern": "^[a-z0-9_-]+$"
|
|
142
|
+
}
|
|
140
143
|
}
|
|
141
144
|
},
|
|
142
145
|
"additionalProperties": false
|
|
@@ -736,13 +739,13 @@
|
|
|
736
739
|
"properties": {
|
|
737
740
|
"envOutputPath": {
|
|
738
741
|
"type": "string",
|
|
739
|
-
"description": "Path where .env file is
|
|
742
|
+
"description": "Path where .env file is written for local development (relative to config dir); single .env for run",
|
|
740
743
|
"pattern": "^[^/].*"
|
|
741
744
|
},
|
|
742
745
|
"localPort": {
|
|
743
746
|
"type": "integer",
|
|
744
|
-
"description": "Port for local development (
|
|
745
|
-
"minimum":
|
|
747
|
+
"description": "Port for local development (host and .env PORT); when set and > 0, overrides port for run and local .env. Container/deployment still use port/containerPort.",
|
|
748
|
+
"minimum": 1,
|
|
746
749
|
"maximum": 65535
|
|
747
750
|
},
|
|
748
751
|
"containerPort": {
|
|
@@ -768,6 +771,27 @@
|
|
|
768
771
|
"type": "string",
|
|
769
772
|
"description": "Dockerfile name (empty or missing = use auto-generated template)",
|
|
770
773
|
"pattern": "^[^/].*"
|
|
774
|
+
},
|
|
775
|
+
"remoteSyncPath": {
|
|
776
|
+
"type": "string",
|
|
777
|
+
"description": "Relative path under user-mutagen-folder for remote sync and Docker -v; when unset, defaults to dev/<appKey>. No leading slash.",
|
|
778
|
+
"pattern": "^[^/].*"
|
|
779
|
+
},
|
|
780
|
+
"reloadStart": {
|
|
781
|
+
"type": "string",
|
|
782
|
+
"description": "When running with --reload, override the container command with this command (run from the mounted app root /app). Examples: TypeScript pnpm run reloadStart, Python make reloadStart."
|
|
783
|
+
},
|
|
784
|
+
"scripts": {
|
|
785
|
+
"type": "object",
|
|
786
|
+
"description": "Override CLI script commands (aifabrix test, install, lint, test-e2e, test-integration). When a key is missing, language default is used (TypeScript: pnpm test/install/lint/test:e2e/test:integration, Python: make test/install/lint/test:e2e/test-integration).",
|
|
787
|
+
"properties": {
|
|
788
|
+
"test": { "type": "string", "description": "Command for aifabrix test <app>" },
|
|
789
|
+
"install": { "type": "string", "description": "Command for aifabrix install <app>" },
|
|
790
|
+
"lint": { "type": "string", "description": "Command for aifabrix lint <app>" },
|
|
791
|
+
"testE2e": { "type": "string", "description": "Command for aifabrix test-e2e <app> (YAML may use test:e2e)" },
|
|
792
|
+
"testIntegration": { "type": "string", "description": "Command for aifabrix test-integration <app> (YAML may use test:integration). Defaults to testE2e when unset." }
|
|
793
|
+
},
|
|
794
|
+
"additionalProperties": true
|
|
771
795
|
}
|
|
772
796
|
},
|
|
773
797
|
"additionalProperties": false
|