@aifabrix/builder 2.41.0 → 2.42.1
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 +2 -2
- package/integration/hubspot/README.md +11 -5
- 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/jest.config.manual.js +2 -1
- 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/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +36 -2
- package/lib/app/config.js +23 -11
- package/lib/app/index.js +5 -3
- package/lib/app/prompts.js +46 -31
- package/lib/app/readme.js +11 -4
- package/lib/app/run-env-compose.js +64 -1
- package/lib/app/run-helpers.js +1 -1
- package/lib/app/show-display.js +1 -1
- package/lib/cli/setup-app.js +45 -14
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +27 -0
- package/lib/cli/setup-environment.js +12 -4
- package/lib/cli/setup-external-system.js +19 -4
- package/lib/cli/setup-infra.js +54 -14
- package/lib/cli/setup-utility.js +117 -21
- package/lib/commands/auth-config.js +22 -12
- 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-init.js +39 -1
- 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 +518 -0
- package/lib/commands/secrets-set.js +6 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +90 -6
- package/lib/commands/upload.js +71 -40
- package/lib/commands/wizard-core-helpers.js +230 -5
- package/lib/commands/wizard-core.js +68 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +49 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +93 -64
- package/lib/core/config.js +7 -1
- package/lib/core/secrets.js +33 -12
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/deployment/deployer.js +7 -5
- package/lib/external-system/download-helpers.js +3 -1
- package/lib/external-system/download.js +182 -204
- package/lib/external-system/generator.js +204 -56
- 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 +51 -18
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +4 -2
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +4 -1
- 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 +326 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +91 -0
- package/lib/generator/wizard.js +180 -179
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/index.js +11 -3
- package/lib/infrastructure/services.js +22 -11
- package/lib/schema/application-schema.json +8 -5
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +82 -6
- package/lib/schema/wizard-config.schema.json +23 -1
- package/lib/utils/api.js +38 -10
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/compose-generator.js +1 -1
- package/lib/utils/compose-handlebars-helpers.js +11 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +115 -25
- 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/env-copy.js +23 -3
- package/lib/utils/error-formatters/http-status-errors.js +0 -1
- 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 +89 -30
- 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 +1 -0
- package/lib/utils/infra-status.js +50 -44
- package/lib/utils/local-secrets.js +5 -5
- package/lib/utils/paths.js +85 -4
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +20 -0
- package/lib/utils/secrets-helpers.js +75 -89
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager.js +24 -32
- 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 +7 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +7 -2
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +5 -5
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/README.md.hbs +75 -22
- 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 +4 -4
- package/templates/typescript/docker-compose.hbs +4 -4
- package/integration/hubspot/application.yaml +0 -37
|
@@ -123,7 +123,7 @@ function cleanupRunFiles(runEnvPath, pgpassRunPath) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* Starts Docker services and configures pgAdmin.
|
|
126
|
+
* Starts Docker services and configures pgAdmin (when enabled).
|
|
127
127
|
* Writes decrypted admin secrets to a temporary .env in infra dir, runs compose, then deletes the file (ISO 27K).
|
|
128
128
|
*
|
|
129
129
|
* @async
|
|
@@ -132,11 +132,13 @@ function cleanupRunFiles(runEnvPath, pgpassRunPath) {
|
|
|
132
132
|
* @param {string} devId - Developer ID
|
|
133
133
|
* @param {number} idNum - Developer ID number
|
|
134
134
|
* @param {string} infraDir - Infrastructure directory
|
|
135
|
+
* @param {Object} [opts] - Options (pgadmin, redisCommander, traefik)
|
|
135
136
|
*/
|
|
136
|
-
async function startDockerServicesAndConfigure(composePath, devId, idNum, infraDir) {
|
|
137
|
+
async function startDockerServicesAndConfigure(composePath, devId, idNum, infraDir, opts = {}) {
|
|
137
138
|
let runEnvPath;
|
|
138
139
|
let pgpassRunPath;
|
|
139
140
|
let adminObj;
|
|
141
|
+
const { pgadmin = true, redisCommander = true, traefik = false } = opts;
|
|
140
142
|
try {
|
|
141
143
|
({ adminObj, runEnvPath } = await prepareRunEnv(infraDir));
|
|
142
144
|
} catch (err) {
|
|
@@ -146,8 +148,10 @@ async function startDockerServicesAndConfigure(composePath, devId, idNum, infraD
|
|
|
146
148
|
try {
|
|
147
149
|
const projectName = getInfraProjectName(devId);
|
|
148
150
|
await startDockerServices(composePath, projectName, runEnvPath, infraDir);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
if (pgadmin) {
|
|
152
|
+
pgpassRunPath = await writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum);
|
|
153
|
+
}
|
|
154
|
+
await waitForServices(devId, { pgadmin, redisCommander, traefik });
|
|
151
155
|
logger.log('All services are healthy and ready');
|
|
152
156
|
} finally {
|
|
153
157
|
cleanupRunFiles(runEnvPath, pgpassRunPath);
|
|
@@ -157,14 +161,16 @@ async function startDockerServicesAndConfigure(composePath, devId, idNum, infraD
|
|
|
157
161
|
/**
|
|
158
162
|
* Waits for services to be healthy
|
|
159
163
|
* @private
|
|
160
|
-
* @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
|
|
161
166
|
*/
|
|
162
|
-
async function waitForServices(devId = null) {
|
|
167
|
+
async function waitForServices(devId = null, opts = {}) {
|
|
163
168
|
const maxAttempts = 30;
|
|
164
169
|
const delay = 2000; // 2 seconds
|
|
170
|
+
const { pgadmin = true, redisCommander = true, traefik = false } = opts;
|
|
165
171
|
|
|
166
172
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
167
|
-
const health = await checkInfraHealth(devId);
|
|
173
|
+
const health = await checkInfraHealth(devId, { pgadmin, redisCommander, traefik });
|
|
168
174
|
const allHealthy = Object.values(health).every(status => status === 'healthy');
|
|
169
175
|
|
|
170
176
|
if (allHealthy) {
|
|
@@ -182,15 +188,17 @@ async function waitForServices(devId = null) {
|
|
|
182
188
|
|
|
183
189
|
/**
|
|
184
190
|
* Checks if infrastructure services are running
|
|
185
|
-
* Validates that all
|
|
191
|
+
* Validates that all expected services are healthy and accessible
|
|
186
192
|
*
|
|
187
193
|
* @async
|
|
188
194
|
* @function checkInfraHealth
|
|
189
195
|
* @param {number|string|null} [devId] - Developer ID (null = use current)
|
|
190
196
|
* @param {Object} [options] - Options
|
|
191
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
|
|
192
201
|
* @returns {Promise<Object>} Health status of each service
|
|
193
|
-
* @throws {Error} If health check fails
|
|
194
202
|
*
|
|
195
203
|
* @example
|
|
196
204
|
* const health = await checkInfraHealth();
|
|
@@ -199,7 +207,10 @@ async function waitForServices(devId = null) {
|
|
|
199
207
|
async function checkInfraHealth(devId = null, options = {}) {
|
|
200
208
|
const developerId = devId || await config.getDeveloperId();
|
|
201
209
|
const servicesWithHealthCheck = ['postgres', 'redis'];
|
|
202
|
-
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');
|
|
203
214
|
const health = {};
|
|
204
215
|
const lookupOptions = options.strict ? { strict: true } : {};
|
|
205
216
|
|
|
@@ -208,7 +219,7 @@ async function checkInfraHealth(devId = null, options = {}) {
|
|
|
208
219
|
health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId, lookupOptions);
|
|
209
220
|
}
|
|
210
221
|
|
|
211
|
-
// Check if services without health checks are running
|
|
222
|
+
// Check if optional services without health checks are running
|
|
212
223
|
for (const service of servicesWithoutHealthCheck) {
|
|
213
224
|
health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId, lookupOptions);
|
|
214
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
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
"$id":"https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-datasource.schema.json",
|
|
4
4
|
"title":"External Data Source",
|
|
5
5
|
"description":"Configuration for AI Fabrix ExternalDataSource entities. Includes metadata schema, data dimensions, transformation mappings, OpenAPI/MCP exposure, execution logic, and sync behavior.",
|
|
6
|
-
|
|
6
|
+
"metadata":{
|
|
7
7
|
"key":"external-datasource-schema",
|
|
8
8
|
"name":"External Data Source Configuration Schema",
|
|
9
9
|
"description":"JSON schema for validating ExternalDataSource configuration files",
|
|
10
|
-
"version":"2.
|
|
10
|
+
"version":"2.3.0",
|
|
11
11
|
"type":"schema",
|
|
12
12
|
"category":"integration",
|
|
13
13
|
"author":"AI Fabrix Team",
|
|
@@ -91,6 +91,22 @@
|
|
|
91
91
|
"Added contract versioning configuration to datasource root for CI/CD safety and agent stability"
|
|
92
92
|
],
|
|
93
93
|
"breaking":false
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"version":"2.2.0",
|
|
97
|
+
"date":"2026-02-26T00:00:00Z",
|
|
98
|
+
"changes":[
|
|
99
|
+
"capabilities: preferred format is array [\"list\",\"get\",...]. Schema oneOf accepts both array and legacy object; runtime accepts both for backward compatibility."
|
|
100
|
+
],
|
|
101
|
+
"breaking":false
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"version":"2.3.0",
|
|
105
|
+
"date":"2026-03-08T00:00:00Z",
|
|
106
|
+
"changes":[
|
|
107
|
+
"BREAKING: Added required primaryKey (array of normalized attribute names). Used for get/update/delete and table indexing. Migration: add primaryKey array (e.g. [\"id\"] or [\"externalId\"]) to each datasource config."
|
|
108
|
+
],
|
|
109
|
+
"breaking":true
|
|
94
110
|
}
|
|
95
111
|
]
|
|
96
112
|
},
|
|
@@ -101,7 +117,8 @@
|
|
|
101
117
|
"systemKey",
|
|
102
118
|
"entityType",
|
|
103
119
|
"resourceType",
|
|
104
|
-
"fieldMappings"
|
|
120
|
+
"fieldMappings",
|
|
121
|
+
"primaryKey"
|
|
105
122
|
],
|
|
106
123
|
"properties":{
|
|
107
124
|
"key":{
|
|
@@ -147,6 +164,16 @@
|
|
|
147
164
|
"description":"Subset of JSON Schema used to validate raw input metadata.",
|
|
148
165
|
"additionalProperties":true
|
|
149
166
|
},
|
|
167
|
+
"primaryKey":{
|
|
168
|
+
"type":"array",
|
|
169
|
+
"description":"Normalized field names that uniquely identify a record (used for get/update/delete and table indexing). Each element must exist in fieldMappings.dimensions or fieldMappings.attributes.",
|
|
170
|
+
"minItems":1,
|
|
171
|
+
"items":{
|
|
172
|
+
"type":"string",
|
|
173
|
+
"pattern":"^[a-zA-Z0-9_]+$"
|
|
174
|
+
},
|
|
175
|
+
"uniqueItems":true
|
|
176
|
+
},
|
|
150
177
|
"fieldMappings":{
|
|
151
178
|
"type":"object",
|
|
152
179
|
"description":"Transformation rules and data dimensions. Maps canonical dimensions to system attributes.",
|
|
@@ -764,31 +791,27 @@
|
|
|
764
791
|
"additionalProperties":false
|
|
765
792
|
},
|
|
766
793
|
"capabilities":{
|
|
767
|
-
"
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
"type":"
|
|
772
|
-
"
|
|
773
|
-
},
|
|
774
|
-
"get":{
|
|
775
|
-
"type":"boolean",
|
|
776
|
-
"default":false
|
|
777
|
-
},
|
|
778
|
-
"create":{
|
|
779
|
-
"type":"boolean",
|
|
780
|
-
"default":false
|
|
781
|
-
},
|
|
782
|
-
"update":{
|
|
783
|
-
"type":"boolean",
|
|
784
|
-
"default":false
|
|
794
|
+
"oneOf":[
|
|
795
|
+
{
|
|
796
|
+
"type":"array",
|
|
797
|
+
"description":"Preferred: list of supported operation names. Values: list, get, create, update, delete.",
|
|
798
|
+
"items":{"type":"string","enum":["list","get","create","update","delete"]},
|
|
799
|
+
"uniqueItems":true
|
|
785
800
|
},
|
|
786
|
-
|
|
787
|
-
"type":"
|
|
788
|
-
"
|
|
801
|
+
{
|
|
802
|
+
"type":"object",
|
|
803
|
+
"description":"Legacy: object with boolean flags per operation. Accepted for backward compatibility.",
|
|
804
|
+
"properties":{
|
|
805
|
+
"list":{"type":"boolean"},
|
|
806
|
+
"get":{"type":"boolean"},
|
|
807
|
+
"create":{"type":"boolean"},
|
|
808
|
+
"update":{"type":"boolean"},
|
|
809
|
+
"delete":{"type":"boolean"}
|
|
810
|
+
},
|
|
811
|
+
"additionalProperties":false
|
|
789
812
|
}
|
|
790
|
-
|
|
791
|
-
"
|
|
813
|
+
],
|
|
814
|
+
"description":"Supported operations. When omitted, derived from execution.engine (CIP operations or list-only for Python)."
|
|
792
815
|
},
|
|
793
816
|
"execution":{
|
|
794
817
|
"type":"object",
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
"$id":"https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/external-system.schema.json",
|
|
4
4
|
"title":"AI Fabrix External System Configuration Schema",
|
|
5
5
|
"description":"Schema for configuring an external system connected to the AI Fabrix Dataplane. This defines authentication, OpenAPI/MCP bindings, field mappings defaults, metadata handling and portal inputs.",
|
|
6
|
-
|
|
6
|
+
"metadata":{
|
|
7
7
|
"key":"external-system-schema",
|
|
8
8
|
"name":"External System Configuration Schema",
|
|
9
9
|
"description":"JSON schema for validating ExternalSystem configuration files",
|
|
10
|
-
"version":"1.
|
|
10
|
+
"version":"1.5.0",
|
|
11
11
|
"type":"schema",
|
|
12
12
|
"category":"integration",
|
|
13
13
|
"author":"AI Fabrix Team",
|
|
14
14
|
"createdAt":"2024-01-01T00:00:00Z",
|
|
15
|
-
"updatedAt":"2026-
|
|
15
|
+
"updatedAt":"2026-03-07T00:00:00Z",
|
|
16
16
|
"compatibility":{
|
|
17
|
-
"minVersion":"1.
|
|
17
|
+
"minVersion":"1.4.0",
|
|
18
18
|
"maxVersion":"2.0.0",
|
|
19
19
|
"deprecated":false
|
|
20
20
|
},
|
|
@@ -29,6 +29,20 @@
|
|
|
29
29
|
|
|
30
30
|
],
|
|
31
31
|
"changelog":[
|
|
32
|
+
{
|
|
33
|
+
"version":"1.5.0",
|
|
34
|
+
"date":"2026-03-07T00:00:00Z",
|
|
35
|
+
"changes":[
|
|
36
|
+
"Optional rateLimit: outbound per-system rate limit (requestsPerWindow/windowSeconds or requestsPerSecond/burstSize); dataplane enforces and handles 429"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"version":"1.4.0",
|
|
41
|
+
"date":"2026-03-07T00:00:00Z",
|
|
42
|
+
"changes":[
|
|
43
|
+
"Optional testEndpoint in authentication.variables for apikey: full URL or path (path resolved against baseUrl); credential test URL for E2E/credential step"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
32
46
|
{
|
|
33
47
|
"version":"1.3.0",
|
|
34
48
|
"date":"2026-02-18T00:00:00Z",
|
|
@@ -71,6 +85,18 @@
|
|
|
71
85
|
}
|
|
72
86
|
]
|
|
73
87
|
},
|
|
88
|
+
"$defs":{
|
|
89
|
+
"authenticationVariablesByMethod":{
|
|
90
|
+
"oauth2":{"variables":[{"key":"baseUrl","required":true,"description":"API base URL"},{"key":"tokenUrl","required":true,"description":"OAuth token endpoint. Full URL (https://...) or path (e.g. /oauth/v2/token); path is resolved against baseUrl."},{"key":"authorizationUrl","required":false,"description":"OAuth authorization endpoint. Full URL or path; path is resolved against baseUrl. Required when grantType is authorization_code or omitted."},{"key":"grantType","required":false,"description":"OAuth 2.0 grant type. One of: client_credentials, authorization_code. Default: authorization_code."},{"key":"scope","required":false},{"key":"tenantId","required":false},{"key":"testEndpoint","required":false,"description":"Optional URL used when testing the credential (GET). Full URL or path (path resolved against baseUrl). If omitted, baseUrl + /health is used."}],"security":[{"key":"clientId","required":true},{"key":"clientSecret","required":true}]},
|
|
91
|
+
"aad":{"variables":[{"key":"baseUrl","required":true},{"key":"tokenUrl","required":true,"description":"Token endpoint. Full URL or path (path resolved against baseUrl)."},{"key":"authorizationUrl","required":false,"description":"Authorization endpoint. Full URL or path (path resolved against baseUrl). Required when grantType is authorization_code or omitted."},{"key":"grantType","required":false,"description":"OAuth 2.0 grant type. One of: client_credentials, authorization_code. Default: authorization_code."},{"key":"tenantId","required":false},{"key":"testEndpoint","required":false,"description":"Optional URL used when testing the credential (GET). Full URL or path (path resolved against baseUrl). If omitted, baseUrl + /health is used."}],"security":[{"key":"clientId","required":true},{"key":"clientSecret","required":true}]},
|
|
92
|
+
"apikey":{"variables":[{"key":"baseUrl","required":true},{"key":"headerName","required":false},{"key":"prefix","required":false},{"key":"testEndpoint","required":false,"description":"Optional URL used when testing the credential (GET). Full URL (https://...) or path (e.g. /crm/v3/objects/contacts?limit=1); path is resolved against baseUrl. If omitted, baseUrl + /health is used."}],"security":[{"key":"apiKey","required":true}]},
|
|
93
|
+
"basic":{"variables":[{"key":"baseUrl","required":true},{"key":"testEndpoint","required":false,"description":"Optional URL used when testing the credential (GET). Full URL or path (path resolved against baseUrl). If omitted, baseUrl + /health is used."}],"security":[{"key":"username","required":true},{"key":"password","required":true}]},
|
|
94
|
+
"queryParam":{"variables":[{"key":"baseUrl","required":true},{"key":"paramName","required":true},{"key":"testEndpoint","required":false,"description":"Optional URL used when testing the credential (GET). Full URL or path (path resolved against baseUrl). If omitted, baseUrl + /health is used."}],"security":[{"key":"paramValue","required":true}]},
|
|
95
|
+
"oidc":{"variables":[{"key":"openIdConfigUrl","required":true},{"key":"clientId","required":true},{"key":"expectedIssuer","required":false},{"key":"algorithms","required":false},{"key":"validateSignature","required":false},{"key":"clockSkewSeconds","required":false},{"key":"testEndpoint","required":false,"description":"Optional URL used when testing the credential (GET). Full URL or path. For OIDC, discovery URL is typically used if testEndpoint omitted."}],"security":[]},
|
|
96
|
+
"hmac":{"variables":[{"key":"baseUrl","required":false},{"key":"algorithm","required":false},{"key":"signatureHeader","required":false},{"key":"timestampHeader","required":false},{"key":"signaturePrefix","required":false},{"key":"testEndpoint","required":false,"description":"Optional URL used when testing the credential (GET). Full URL or path (path resolved against baseUrl when baseUrl present)."}],"security":[{"key":"signingSecret","required":true}]},
|
|
97
|
+
"none":{"variables":[],"security":[]}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
74
100
|
"type":"object",
|
|
75
101
|
"required":[
|
|
76
102
|
"key",
|
|
@@ -153,14 +179,14 @@
|
|
|
153
179
|
},
|
|
154
180
|
"variables":{
|
|
155
181
|
"type":"object",
|
|
156
|
-
"description":"Non-secret config.
|
|
182
|
+
"description":"Non-secret config. See $defs.authenticationVariablesByMethod for per-method keys. oauth2/aad: baseUrl, tokenUrl, authorizationUrl (optional; required when grantType is authorization_code or omitted), grantType (optional, default authorization_code); apikey: baseUrl, headerName (optional), prefix (optional), testEndpoint (optional); basic: baseUrl; queryParam: baseUrl, paramName; oidc: openIdConfigUrl, clientId; hmac: optional; none: empty. URL-valued variables (tokenUrl, authorizationUrl, testEndpoint) accept full URL or path; paths are resolved against baseUrl by the backend.",
|
|
157
183
|
"additionalProperties":{
|
|
158
184
|
"type":"string"
|
|
159
185
|
}
|
|
160
186
|
},
|
|
161
187
|
"security":{
|
|
162
188
|
"type":"object",
|
|
163
|
-
"description":"Secret-bearing keys only.
|
|
189
|
+
"description":"Secret-bearing keys only. Values must be kv:// references. See $defs.authenticationVariablesByMethod. oauth2/aad: clientId, clientSecret; apikey: apiKey; basic: username, password; queryParam: paramValue; oidc: none; hmac: signingSecret.",
|
|
164
190
|
"additionalProperties":{
|
|
165
191
|
"type":"string",
|
|
166
192
|
"pattern":"^kv://.+$"
|
|
@@ -172,6 +198,17 @@
|
|
|
172
198
|
"default":true
|
|
173
199
|
}
|
|
174
200
|
},
|
|
201
|
+
"examples":[
|
|
202
|
+
{"method":"oauth2","variables":{"baseUrl":"https://api.example.com","tokenUrl":"https://api.example.com/oauth/token","authorizationUrl":"https://api.example.com/oauth/authorize"},"security":{"clientId":"kv://example/clientId","clientSecret":"kv://example/clientSecret"}},
|
|
203
|
+
{"method":"oauth2","variables":{"baseUrl":"https://api.example.com","tokenUrl":"https://api.example.com/oauth/token","grantType":"client_credentials"},"security":{"clientId":"kv://example/clientId","clientSecret":"kv://example/clientSecret"}},
|
|
204
|
+
{"method":"apikey","variables":{"baseUrl":"https://api.example.com","headerName":"X-API-Key","testEndpoint":"https://api.example.com/health"},"security":{"apiKey":"kv://example/apiKey"}},
|
|
205
|
+
{"method":"apikey","variables":{"baseUrl":"https://api.example.com","headerName":"Authorization","prefix":"Bearer","testEndpoint":"/crm/v3/objects/contacts?limit=1"},"security":{"apiKey":"kv://example/apiKey"}},
|
|
206
|
+
{"method":"basic","variables":{"baseUrl":"https://api.example.com"},"security":{"username":"kv://example/username","password":"kv://example/password"}},
|
|
207
|
+
{"method":"queryParam","variables":{"baseUrl":"https://api.example.com","paramName":"api_key"},"security":{"paramValue":"kv://example/apiKey"}},
|
|
208
|
+
{"method":"oidc","variables":{"openIdConfigUrl":"https://example.com/.well-known/openid-configuration","clientId":"app-id"}},
|
|
209
|
+
{"method":"hmac","variables":{"signatureHeader":"X-Signature"},"security":{"signingSecret":"kv://example/signingSecret"}},
|
|
210
|
+
{"method":"none","variables":{}}
|
|
211
|
+
],
|
|
175
212
|
"additionalProperties":false
|
|
176
213
|
},
|
|
177
214
|
"openapi":{
|
|
@@ -446,6 +483,45 @@
|
|
|
446
483
|
"type":"string",
|
|
447
484
|
"description":"SHA256 hash of triggerPaths payload (64-char hex). Used to detect structural changes. Optional; Dataplane computes when absent.",
|
|
448
485
|
"pattern":"^[a-f0-9]{64}$"
|
|
486
|
+
},
|
|
487
|
+
"rateLimit":{
|
|
488
|
+
"type":"object",
|
|
489
|
+
"description":"Outbound rate limit for requests from the dataplane to this external system. When set, the dataplane enforces the limit per base URL and handles HTTP 429 (wait and retry). When absent, global env defaults apply (CIP_EXECUTION_RATE_LIMIT_REQUESTS_PER_SECOND, CIP_EXECUTION_RATE_LIMIT_BURST_SIZE). Supports window-based (e.g. HubSpot 100/10s) or token-bucket style (requestsPerSecond + burstSize).",
|
|
490
|
+
"properties":{
|
|
491
|
+
"requestsPerWindow":{
|
|
492
|
+
"type":"integer",
|
|
493
|
+
"minimum":1,
|
|
494
|
+
"description":"Maximum requests allowed in the time window (window-based limit). Example: 100 for HubSpot's 100 requests per 10 seconds."
|
|
495
|
+
},
|
|
496
|
+
"windowSeconds":{
|
|
497
|
+
"type":"integer",
|
|
498
|
+
"minimum":1,
|
|
499
|
+
"description":"Time window in seconds. Used with requestsPerWindow. Example: 10 for HubSpot (100 requests per 10 seconds)."
|
|
500
|
+
},
|
|
501
|
+
"requestsPerSecond":{
|
|
502
|
+
"type":"number",
|
|
503
|
+
"minimum":0.1,
|
|
504
|
+
"description":"Sustained request rate (token-bucket style). When used with burstSize, allows short bursts up to burstSize while refilling at this rate."
|
|
505
|
+
},
|
|
506
|
+
"burstSize":{
|
|
507
|
+
"type":"integer",
|
|
508
|
+
"minimum":1,
|
|
509
|
+
"description":"Maximum burst size in tokens (token-bucket style). Used with requestsPerSecond."
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
"additionalProperties":false,
|
|
513
|
+
"oneOf":[
|
|
514
|
+
{
|
|
515
|
+
"required":["requestsPerWindow","windowSeconds"]
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
"required":["requestsPerSecond","burstSize"]
|
|
519
|
+
}
|
|
520
|
+
],
|
|
521
|
+
"examples":[
|
|
522
|
+
{"requestsPerWindow":100,"windowSeconds":10},
|
|
523
|
+
{"requestsPerSecond":10,"burstSize":100}
|
|
524
|
+
]
|
|
449
525
|
}
|
|
450
526
|
},
|
|
451
527
|
"additionalProperties":false
|
|
@@ -20,11 +20,17 @@
|
|
|
20
20
|
},
|
|
21
21
|
"systemIdOrKey": {
|
|
22
22
|
"type": "string",
|
|
23
|
-
"description": "
|
|
23
|
+
"description": "Application/system key (e.g. hubspot-demo), not a datasource or entity key. Required when mode='add-datasource'. Must be the system key from the dataplane, not an entity key (e.g. 'companies').",
|
|
24
24
|
"pattern": "^[a-z0-9-]+$",
|
|
25
25
|
"minLength": 1,
|
|
26
26
|
"maxLength": 50
|
|
27
27
|
},
|
|
28
|
+
"systemDisplayName": {
|
|
29
|
+
"type": ["string", "null"],
|
|
30
|
+
"title": "Systemdisplayname",
|
|
31
|
+
"description": "System-level display name for the credential (e.g. 'Hubspot Demo'). When the OpenAPI title is entity-specific (e.g. 'Companies'), pass the system name here so authentication.displayName is system-level.",
|
|
32
|
+
"maxLength": 200
|
|
33
|
+
},
|
|
28
34
|
"source": {
|
|
29
35
|
"type": "object",
|
|
30
36
|
"description": "Source configuration for the wizard",
|
|
@@ -59,6 +65,17 @@
|
|
|
59
65
|
"type": "string",
|
|
60
66
|
"description": "Known platform identifier (for known-platform type)",
|
|
61
67
|
"enum": ["hubspot", "salesforce", "zendesk", "slack", "microsoft365"]
|
|
68
|
+
},
|
|
69
|
+
"datasourceKeys": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"description": "Datasource keys to include (validated against platform; omit for all)",
|
|
72
|
+
"items": { "type": "string", "minLength": 1 },
|
|
73
|
+
"minItems": 1
|
|
74
|
+
},
|
|
75
|
+
"entityName": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Entity for multi-entity OpenAPI (validated against discover-entities; for openapi-file and openapi-url)",
|
|
78
|
+
"minLength": 1
|
|
62
79
|
}
|
|
63
80
|
},
|
|
64
81
|
"allOf": [
|
|
@@ -194,6 +211,11 @@
|
|
|
194
211
|
"type": "boolean",
|
|
195
212
|
"description": "Enable Role-Based Access Control",
|
|
196
213
|
"default": false
|
|
214
|
+
},
|
|
215
|
+
"debug": {
|
|
216
|
+
"type": "boolean",
|
|
217
|
+
"description": "When true, capture detailed generation steps and save to debug.log (dataplane returns debugLog)",
|
|
218
|
+
"default": false
|
|
197
219
|
}
|
|
198
220
|
}
|
|
199
221
|
},
|
package/lib/utils/api.js
CHANGED
|
@@ -121,10 +121,22 @@ async function handleSuccessResponse(response, url, options, duration) {
|
|
|
121
121
|
success: true
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
// 204 No Content or empty body: nothing to parse (avoids "Unexpected end of JSON input")
|
|
125
|
+
if (response.status === 204) {
|
|
126
|
+
return { success: true, data: null, status: response.status };
|
|
127
|
+
}
|
|
128
|
+
|
|
124
129
|
const contentType = response.headers.get('content-type');
|
|
125
130
|
if (contentType && contentType.includes('application/json')) {
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
try {
|
|
132
|
+
const data = await response.json();
|
|
133
|
+
return { success: true, data, status: response.status };
|
|
134
|
+
} catch (e) {
|
|
135
|
+
if (e instanceof SyntaxError && e.message && e.message.includes('JSON')) {
|
|
136
|
+
return { success: true, data: null, status: response.status };
|
|
137
|
+
}
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
const text = await response.text();
|
|
@@ -286,12 +298,28 @@ function extractControllerUrl(url) {
|
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
301
|
+
* Set auth header on headers object: Bearer for user token, x-client-token for application token.
|
|
302
|
+
* @param {Object} headers - Headers object to mutate
|
|
303
|
+
* @param {string} token - Token value
|
|
304
|
+
* @param {string} authType - 'bearer' or 'client-token'
|
|
305
|
+
*/
|
|
306
|
+
function setAuthHeader(headers, token, authType) {
|
|
307
|
+
if (!token) return;
|
|
308
|
+
if (authType === 'client-token') {
|
|
309
|
+
headers['x-client-token'] = token;
|
|
310
|
+
} else {
|
|
311
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Make an authenticated API call with user token (Bearer) or application token (x-client-token).
|
|
317
|
+
* Automatically refreshes device token on 401 when user Bearer was used.
|
|
291
318
|
* @param {string} url - API endpoint URL
|
|
292
319
|
* @param {Object} options - Fetch options
|
|
293
|
-
* @param {string|Object} tokenOrAuthConfig -
|
|
294
|
-
* @param {string} [tokenOrAuthConfig.
|
|
320
|
+
* @param {string|Object} tokenOrAuthConfig - User token string (Bearer), or authConfig object with type 'bearer'|'client-token'
|
|
321
|
+
* @param {string} [tokenOrAuthConfig.type] - 'bearer' (user token) or 'client-token' (application token)
|
|
322
|
+
* @param {string} [tokenOrAuthConfig.token] - Token (if object)
|
|
295
323
|
* @param {string} [tokenOrAuthConfig.controller] - Controller URL for token refresh (if object)
|
|
296
324
|
* @returns {Promise<Object>} Response object
|
|
297
325
|
*/
|
|
@@ -299,22 +327,22 @@ function extractControllerUrl(url) {
|
|
|
299
327
|
async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
|
|
300
328
|
const isStringToken = typeof tokenOrAuthConfig === 'string';
|
|
301
329
|
const token = isStringToken ? tokenOrAuthConfig : tokenOrAuthConfig?.token;
|
|
330
|
+
const authType = isStringToken ? 'bearer' : tokenOrAuthConfig?.type;
|
|
302
331
|
const authControllerUrl = isStringToken ? null : tokenOrAuthConfig?.controller;
|
|
303
332
|
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData;
|
|
304
333
|
const headers = { ...options.headers };
|
|
305
334
|
if (!isFormData && !headers['Content-Type']) {
|
|
306
335
|
headers['Content-Type'] = 'application/json';
|
|
307
336
|
}
|
|
308
|
-
|
|
309
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
310
|
-
}
|
|
337
|
+
setAuthHeader(headers, token, authType);
|
|
311
338
|
|
|
312
339
|
const response = await makeApiCall(url, {
|
|
313
340
|
...options,
|
|
314
341
|
headers
|
|
315
342
|
});
|
|
316
343
|
|
|
317
|
-
|
|
344
|
+
// Only attempt device token refresh on 401 when user Bearer token was used (not for client-token)
|
|
345
|
+
if (!response.success && response.status === 401 && authType !== 'client-token') {
|
|
318
346
|
try {
|
|
319
347
|
const { forceRefreshDeviceToken } = require('./token-manager');
|
|
320
348
|
const refreshedToken = await forceRefreshDeviceToken(authControllerUrl || extractControllerUrl(url));
|
|
@@ -25,11 +25,12 @@ function createBearerTokenHeaders(token) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Creates authentication headers for
|
|
28
|
+
* Creates authentication headers for the token-issuing endpoint only (e.g. POST /api/v1/auth/token).
|
|
29
|
+
* Do not use for Controller or Dataplane app endpoints—those require Bearer token (use createBearerTokenHeaders).
|
|
29
30
|
*
|
|
30
31
|
* @param {string} clientId - Application client ID
|
|
31
32
|
* @param {string} clientSecret - Application client secret
|
|
32
|
-
* @returns {Object} Headers object with
|
|
33
|
+
* @returns {Object} Headers object with x-client-id and x-client-secret
|
|
33
34
|
* @throws {Error} If credentials are missing
|
|
34
35
|
*/
|
|
35
36
|
function createClientCredentialsHeaders(clientId, clientSecret) {
|
|
@@ -43,14 +44,14 @@ function createClientCredentialsHeaders(clientId, clientSecret) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
* Creates authentication headers based on auth configuration
|
|
47
|
-
*
|
|
47
|
+
* Creates authentication headers based on auth configuration.
|
|
48
|
+
* For app endpoints use type 'bearer' only. Use 'client-credentials' only when calling the token-issuing endpoint (e.g. /api/v1/auth/token).
|
|
48
49
|
*
|
|
49
50
|
* @param {Object} authConfig - Authentication configuration
|
|
50
|
-
* @param {string} authConfig.type - Auth type: 'bearer' or 'client-credentials'
|
|
51
|
+
* @param {string} authConfig.type - Auth type: 'bearer' (for app endpoints) or 'client-credentials' (token endpoint only)
|
|
51
52
|
* @param {string} [authConfig.token] - Bearer token (for type 'bearer')
|
|
52
|
-
* @param {string} [authConfig.clientId] - Client ID (for type 'client-credentials')
|
|
53
|
-
* @param {string} [authConfig.clientSecret] - Client secret (for type 'client-credentials')
|
|
53
|
+
* @param {string} [authConfig.clientId] - Client ID (for type 'client-credentials', token endpoint only)
|
|
54
|
+
* @param {string} [authConfig.clientSecret] - Client secret (for type 'client-credentials', token endpoint only)
|
|
54
55
|
* @returns {Object} Headers object with authentication
|
|
55
56
|
* @throws {Error} If auth config is invalid
|
|
56
57
|
*/
|
|
@@ -224,7 +224,7 @@ function buildVolumesConfig(appName) {
|
|
|
224
224
|
/**
|
|
225
225
|
* Builds networks configuration for template data
|
|
226
226
|
* @param {Object} config - Application configuration
|
|
227
|
-
* @returns {Object} Networks configuration
|
|
227
|
+
* @returns {Object} Networks configuration with databases array
|
|
228
228
|
*/
|
|
229
229
|
function buildNetworksConfig(config) {
|
|
230
230
|
return { databases: config.requires?.databases || config.databases || [] };
|
|
@@ -38,6 +38,17 @@ function registerComposeHelpers() {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
handlebars.registerHelper('isVectorDatabase', (name) => isVectorDatabaseName(name));
|
|
41
|
+
|
|
42
|
+
/** Returns list of extension names for this database (config extensions + vector if name ends with "vector"). */
|
|
43
|
+
handlebars.registerHelper('extensionsForDb', (db) => {
|
|
44
|
+
if (!db) return [];
|
|
45
|
+
const explicit = Array.isArray(db.extensions) ? db.extensions : [];
|
|
46
|
+
const list = [...explicit];
|
|
47
|
+
if (isVectorDatabaseName(db.name) && !list.includes('vector')) {
|
|
48
|
+
list.push('vector');
|
|
49
|
+
}
|
|
50
|
+
return list;
|
|
51
|
+
});
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
module.exports = { registerComposeHelpers };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config format preference utilities (json/yaml)
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Format preference get/set for config
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate and normalize format (json or yaml)
|
|
11
|
+
* @param {*} format - Format value
|
|
12
|
+
* @returns {string} Normalized format ('json' or 'yaml')
|
|
13
|
+
* @throws {Error} If format is invalid
|
|
14
|
+
*/
|
|
15
|
+
function validateAndNormalizeFormat(format) {
|
|
16
|
+
if (!format || typeof format !== 'string') {
|
|
17
|
+
throw new Error('Option --format must be \'json\' or \'yaml\'');
|
|
18
|
+
}
|
|
19
|
+
const normalized = format.trim().toLowerCase();
|
|
20
|
+
if (normalized !== 'json' && normalized !== 'yaml') {
|
|
21
|
+
throw new Error('Option --format must be \'json\' or \'yaml\'');
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create format preference functions
|
|
28
|
+
* @param {Function} getConfigFn - Async function to get config
|
|
29
|
+
* @param {Function} saveConfigFn - Async function to save config
|
|
30
|
+
* @returns {{ getFormat: Function, setFormat: Function, validateAndNormalizeFormat: Function }}
|
|
31
|
+
*/
|
|
32
|
+
function createFormatFunctions(getConfigFn, saveConfigFn) {
|
|
33
|
+
return {
|
|
34
|
+
async getFormat() {
|
|
35
|
+
const config = await getConfigFn();
|
|
36
|
+
const raw = config.format;
|
|
37
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
38
|
+
const normalized = raw.trim().toLowerCase();
|
|
39
|
+
return normalized === 'json' || normalized === 'yaml' ? normalized : null;
|
|
40
|
+
},
|
|
41
|
+
async setFormat(format) {
|
|
42
|
+
const normalized = validateAndNormalizeFormat(format);
|
|
43
|
+
const config = await getConfigFn();
|
|
44
|
+
config.format = normalized;
|
|
45
|
+
await saveConfigFn(config);
|
|
46
|
+
},
|
|
47
|
+
validateAndNormalizeFormat
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { createFormatFunctions, validateAndNormalizeFormat };
|