@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.
Files changed (142) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +2 -2
  3. package/integration/hubspot/README.md +11 -5
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +36 -2
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +5 -3
  24. package/lib/app/prompts.js +46 -31
  25. package/lib/app/readme.js +11 -4
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +45 -14
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/auth-config.js +22 -12
  37. package/lib/commands/credential-env.js +162 -0
  38. package/lib/commands/credential-list.js +17 -22
  39. package/lib/commands/credential-push.js +96 -0
  40. package/lib/commands/datasource.js +77 -6
  41. package/lib/commands/dev-init.js +39 -1
  42. package/lib/commands/repair-auth-config.js +99 -0
  43. package/lib/commands/repair-datasource-keys.js +208 -0
  44. package/lib/commands/repair-datasource.js +235 -0
  45. package/lib/commands/repair-env-template.js +348 -0
  46. package/lib/commands/repair-internal.js +85 -0
  47. package/lib/commands/repair-rbac.js +158 -0
  48. package/lib/commands/repair.js +518 -0
  49. package/lib/commands/secrets-set.js +6 -0
  50. package/lib/commands/test-e2e-external.js +165 -0
  51. package/lib/commands/up-dataplane.js +90 -6
  52. package/lib/commands/upload.js +71 -40
  53. package/lib/commands/wizard-core-helpers.js +230 -5
  54. package/lib/commands/wizard-core.js +68 -29
  55. package/lib/commands/wizard-dataplane.js +1 -1
  56. package/lib/commands/wizard-entity-selection.js +43 -0
  57. package/lib/commands/wizard-headless.js +49 -5
  58. package/lib/commands/wizard-helpers.js +7 -3
  59. package/lib/commands/wizard.js +93 -64
  60. package/lib/core/config.js +7 -1
  61. package/lib/core/secrets.js +33 -12
  62. package/lib/datasource/deploy.js +12 -3
  63. package/lib/datasource/test-e2e.js +219 -0
  64. package/lib/datasource/test-integration.js +154 -0
  65. package/lib/deployment/deployer.js +7 -5
  66. package/lib/external-system/download-helpers.js +3 -1
  67. package/lib/external-system/download.js +182 -204
  68. package/lib/external-system/generator.js +204 -56
  69. package/lib/external-system/test-execution.js +2 -1
  70. package/lib/external-system/test-system-level.js +73 -0
  71. package/lib/external-system/test.js +51 -18
  72. package/lib/generator/external-controller-manifest.js +29 -2
  73. package/lib/generator/external-schema-utils.js +4 -2
  74. package/lib/generator/external.js +10 -3
  75. package/lib/generator/index.js +4 -1
  76. package/lib/generator/split-readme.js +1 -0
  77. package/lib/generator/split-variables.js +7 -1
  78. package/lib/generator/split.js +194 -54
  79. package/lib/generator/wizard-prompts-secondary.js +326 -0
  80. package/lib/generator/wizard-prompts.js +105 -106
  81. package/lib/generator/wizard-readme.js +91 -0
  82. package/lib/generator/wizard.js +180 -179
  83. package/lib/infrastructure/compose.js +11 -1
  84. package/lib/infrastructure/index.js +11 -3
  85. package/lib/infrastructure/services.js +22 -11
  86. package/lib/schema/application-schema.json +8 -5
  87. package/lib/schema/external-datasource.schema.json +49 -26
  88. package/lib/schema/external-system.schema.json +82 -6
  89. package/lib/schema/wizard-config.schema.json +23 -1
  90. package/lib/utils/api.js +38 -10
  91. package/lib/utils/auth-headers.js +8 -7
  92. package/lib/utils/compose-generator.js +1 -1
  93. package/lib/utils/compose-handlebars-helpers.js +11 -0
  94. package/lib/utils/config-format-preference.js +51 -0
  95. package/lib/utils/config-format.js +36 -0
  96. package/lib/utils/configuration-env-resolver.js +179 -0
  97. package/lib/utils/credential-display.js +83 -0
  98. package/lib/utils/credential-secrets-env.js +115 -25
  99. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  100. package/lib/utils/deployment-validation-helpers.js +4 -4
  101. package/lib/utils/dev-ca-install.js +139 -0
  102. package/lib/utils/env-copy.js +23 -3
  103. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  104. package/lib/utils/error-formatters/permission-errors.js +0 -1
  105. package/lib/utils/error-formatters/validation-errors.js +0 -1
  106. package/lib/utils/external-readme.js +89 -30
  107. package/lib/utils/external-system-display.js +59 -1
  108. package/lib/utils/external-system-test-helpers.js +21 -8
  109. package/lib/utils/external-system-validators.js +3 -0
  110. package/lib/utils/file-upload.js +20 -50
  111. package/lib/utils/help-builder.js +1 -0
  112. package/lib/utils/infra-status.js +50 -44
  113. package/lib/utils/local-secrets.js +5 -5
  114. package/lib/utils/paths.js +85 -4
  115. package/lib/utils/secrets-canonical.js +93 -0
  116. package/lib/utils/secrets-generator.js +20 -0
  117. package/lib/utils/secrets-helpers.js +75 -89
  118. package/lib/utils/test-log-writer.js +56 -0
  119. package/lib/utils/token-manager.js +24 -32
  120. package/lib/validation/env-template-auth.js +157 -0
  121. package/lib/validation/env-template-kv.js +41 -0
  122. package/lib/validation/external-manifest-validator.js +25 -0
  123. package/lib/validation/external-system-auth-rules.js +86 -0
  124. package/lib/validation/validate-batch.js +149 -0
  125. package/lib/validation/validate-datasource-keys-api.js +33 -0
  126. package/lib/validation/validate-display.js +94 -16
  127. package/lib/validation/validate.js +25 -12
  128. package/lib/validation/validator.js +7 -9
  129. package/lib/validation/wizard-datasource-validation.js +50 -0
  130. package/package.json +7 -2
  131. package/templates/applications/dataplane/application.yaml +1 -1
  132. package/templates/applications/dataplane/env.template +5 -5
  133. package/templates/applications/dataplane/rbac.yaml +2 -2
  134. package/templates/applications/miso-controller/env.template +1 -1
  135. package/templates/external-system/README.md.hbs +75 -22
  136. package/templates/external-system/deploy.js.hbs +4 -2
  137. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  138. package/templates/external-system/external-system.json.hbs +1 -18
  139. package/templates/infra/compose.yaml.hbs +6 -0
  140. package/templates/python/docker-compose.hbs +4 -4
  141. package/templates/typescript/docker-compose.hbs +4 -4
  142. 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
- pgpassRunPath = await writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum);
150
- await waitForServices(devId);
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 required services are healthy and accessible
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 = ['pgadmin', 'redis-commander'];
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
- "metadata":{
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.1.0",
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
- "type":"object",
768
- "description":"Declares which logical operations are supported by this datasource.",
769
- "properties":{
770
- "list":{
771
- "type":"boolean",
772
- "default":true
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
- "delete":{
787
- "type":"boolean",
788
- "default":false
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
- "additionalProperties":false
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
- "metadata":{
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.3.0",
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-02-18T00:00:00Z",
15
+ "updatedAt":"2026-03-07T00:00:00Z",
16
16
  "compatibility":{
17
- "minVersion":"1.3.0",
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. Must include baseUrl for all methods except none. Common keys: baseUrl, tokenUrl, authorizationUrl, tenantId, audience, scope, headerName; queryParam: paramName; oidc: openIdConfigUrl, clientId, expectedIssuer; hmac: algorithm, signatureHeader, timestampHeader, signaturePrefix, timestampMaxAge.",
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. Every value must be a Key Vault reference matching ^kv://.+$",
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": "Existing system ID or key (required when mode='add-datasource')",
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
- const data = await response.json();
127
- return { success: true, data, status: response.status };
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
- * Make an authenticated API call with bearer token
290
- * Automatically refreshes device token on 401 errors if refresh token is available
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 - Bearer token string or authConfig object
294
- * @param {string} [tokenOrAuthConfig.token] - Bearer token (if object)
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
- if (token) {
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
- if (!response.success && response.status === 401) {
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 Client Credentials flow (legacy support)
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 authentication
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
- * Supports both Bearer token and client credentials authentication
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 };