@aifabrix/builder 2.41.0 → 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.
Files changed (138) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +1 -1
  3. package/integration/hubspot/README.md +8 -4
  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 +34 -1
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +3 -1
  24. package/lib/app/prompts.js +44 -29
  25. package/lib/app/readme.js +8 -3
  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 +42 -11
  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/credential-env.js +162 -0
  37. package/lib/commands/credential-list.js +17 -22
  38. package/lib/commands/credential-push.js +96 -0
  39. package/lib/commands/datasource.js +77 -6
  40. package/lib/commands/dev-init.js +39 -1
  41. package/lib/commands/repair-auth-config.js +99 -0
  42. package/lib/commands/repair-datasource-keys.js +208 -0
  43. package/lib/commands/repair-datasource.js +235 -0
  44. package/lib/commands/repair-env-template.js +348 -0
  45. package/lib/commands/repair-internal.js +85 -0
  46. package/lib/commands/repair-rbac.js +158 -0
  47. package/lib/commands/repair.js +507 -0
  48. package/lib/commands/test-e2e-external.js +165 -0
  49. package/lib/commands/upload.js +71 -40
  50. package/lib/commands/wizard-core-helpers.js +226 -4
  51. package/lib/commands/wizard-core.js +67 -29
  52. package/lib/commands/wizard-dataplane.js +1 -1
  53. package/lib/commands/wizard-entity-selection.js +43 -0
  54. package/lib/commands/wizard-headless.js +44 -5
  55. package/lib/commands/wizard-helpers.js +7 -3
  56. package/lib/commands/wizard.js +86 -64
  57. package/lib/core/config.js +7 -1
  58. package/lib/core/secrets.js +33 -12
  59. package/lib/datasource/deploy.js +12 -3
  60. package/lib/datasource/test-e2e.js +219 -0
  61. package/lib/datasource/test-integration.js +154 -0
  62. package/lib/deployment/deployer.js +7 -5
  63. package/lib/external-system/download.js +182 -204
  64. package/lib/external-system/generator.js +204 -56
  65. package/lib/external-system/test-execution.js +2 -1
  66. package/lib/external-system/test-system-level.js +73 -0
  67. package/lib/external-system/test.js +51 -18
  68. package/lib/generator/external-controller-manifest.js +29 -2
  69. package/lib/generator/external-schema-utils.js +1 -1
  70. package/lib/generator/external.js +10 -3
  71. package/lib/generator/index.js +4 -1
  72. package/lib/generator/split-readme.js +1 -0
  73. package/lib/generator/split-variables.js +7 -1
  74. package/lib/generator/split.js +194 -54
  75. package/lib/generator/wizard-prompts-secondary.js +294 -0
  76. package/lib/generator/wizard-prompts.js +105 -106
  77. package/lib/generator/wizard-readme.js +88 -0
  78. package/lib/generator/wizard.js +147 -158
  79. package/lib/infrastructure/compose.js +11 -1
  80. package/lib/infrastructure/index.js +11 -3
  81. package/lib/infrastructure/services.js +22 -11
  82. package/lib/schema/application-schema.json +8 -5
  83. package/lib/schema/external-datasource.schema.json +49 -26
  84. package/lib/schema/external-system.schema.json +82 -6
  85. package/lib/schema/wizard-config.schema.json +16 -0
  86. package/lib/utils/api.js +38 -10
  87. package/lib/utils/auth-headers.js +8 -7
  88. package/lib/utils/compose-generator.js +1 -1
  89. package/lib/utils/compose-handlebars-helpers.js +11 -0
  90. package/lib/utils/config-format-preference.js +51 -0
  91. package/lib/utils/config-format.js +36 -0
  92. package/lib/utils/configuration-env-resolver.js +179 -0
  93. package/lib/utils/credential-display.js +83 -0
  94. package/lib/utils/credential-secrets-env.js +115 -25
  95. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  96. package/lib/utils/deployment-validation-helpers.js +4 -4
  97. package/lib/utils/dev-ca-install.js +139 -0
  98. package/lib/utils/env-copy.js +23 -3
  99. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  100. package/lib/utils/error-formatters/permission-errors.js +0 -1
  101. package/lib/utils/error-formatters/validation-errors.js +0 -1
  102. package/lib/utils/external-readme.js +56 -29
  103. package/lib/utils/external-system-display.js +59 -1
  104. package/lib/utils/external-system-test-helpers.js +21 -8
  105. package/lib/utils/external-system-validators.js +3 -0
  106. package/lib/utils/file-upload.js +20 -50
  107. package/lib/utils/help-builder.js +1 -0
  108. package/lib/utils/infra-status.js +50 -44
  109. package/lib/utils/local-secrets.js +5 -5
  110. package/lib/utils/paths.js +85 -4
  111. package/lib/utils/secrets-canonical.js +93 -0
  112. package/lib/utils/secrets-generator.js +20 -0
  113. package/lib/utils/secrets-helpers.js +75 -89
  114. package/lib/utils/test-log-writer.js +56 -0
  115. package/lib/utils/token-manager.js +24 -32
  116. package/lib/validation/env-template-auth.js +157 -0
  117. package/lib/validation/env-template-kv.js +41 -0
  118. package/lib/validation/external-manifest-validator.js +25 -0
  119. package/lib/validation/external-system-auth-rules.js +86 -0
  120. package/lib/validation/validate-batch.js +149 -0
  121. package/lib/validation/validate-datasource-keys-api.js +33 -0
  122. package/lib/validation/validate-display.js +94 -16
  123. package/lib/validation/validate.js +25 -12
  124. package/lib/validation/validator.js +7 -9
  125. package/lib/validation/wizard-datasource-validation.js +50 -0
  126. package/package.json +7 -2
  127. package/templates/applications/dataplane/application.yaml +1 -1
  128. package/templates/applications/dataplane/env.template +5 -5
  129. package/templates/applications/dataplane/rbac.yaml +2 -2
  130. package/templates/applications/miso-controller/env.template +1 -1
  131. package/templates/external-system/README.md.hbs +65 -25
  132. package/templates/external-system/deploy.js.hbs +4 -2
  133. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  134. package/templates/external-system/external-system.json.hbs +1 -18
  135. package/templates/infra/compose.yaml.hbs +6 -0
  136. package/templates/python/docker-compose.hbs +4 -4
  137. package/templates/typescript/docker-compose.hbs +4 -4
  138. package/integration/hubspot/application.yaml +0 -37
@@ -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
@@ -59,6 +59,17 @@
59
59
  "type": "string",
60
60
  "description": "Known platform identifier (for known-platform type)",
61
61
  "enum": ["hubspot", "salesforce", "zendesk", "slack", "microsoft365"]
62
+ },
63
+ "datasourceKeys": {
64
+ "type": "array",
65
+ "description": "Datasource keys to include (validated against platform; omit for all)",
66
+ "items": { "type": "string", "minLength": 1 },
67
+ "minItems": 1
68
+ },
69
+ "entityName": {
70
+ "type": "string",
71
+ "description": "Entity for multi-entity OpenAPI (validated against discover-entities; for openapi-file and openapi-url)",
72
+ "minLength": 1
62
73
  }
63
74
  },
64
75
  "allOf": [
@@ -194,6 +205,11 @@
194
205
  "type": "boolean",
195
206
  "description": "Enable Role-Based Access Control",
196
207
  "default": false
208
+ },
209
+ "debug": {
210
+ "type": "boolean",
211
+ "description": "When true, capture detailed generation steps and save to debug.log (dataplane returns debugLog)",
212
+ "default": false
197
213
  }
198
214
  }
199
215
  },
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 };
@@ -14,6 +14,7 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
  const yaml = require('js-yaml');
17
+ const YAML = require('yaml');
17
18
 
18
19
  const YAML_EXTENSIONS = ['.yaml', '.yml'];
19
20
  const JSON_EXTENSIONS = ['.json'];
@@ -144,11 +145,46 @@ function writeConfigFile(filePath, object, format) {
144
145
  fs.writeFileSync(filePath, content, 'utf8');
145
146
  }
146
147
 
148
+ /**
149
+ * Writes application config YAML by updating only repaired keys in the original content,
150
+ * so comments and formatting on other keys are preserved. Use for repair flows that
151
+ * only change externalIntegration and/or app.key.
152
+ *
153
+ * @param {string} filePath - Absolute path to the YAML file
154
+ * @param {string} originalContent - Original file content (with comments)
155
+ * @param {Object} repairedVariables - Repaired config object; only externalIntegration and app are written
156
+ * @throws {Error} If parsing or write fails
157
+ */
158
+ function writeYamlPreservingComments(filePath, originalContent, repairedVariables) {
159
+ if (!filePath || typeof filePath !== 'string') {
160
+ throw new Error('writeYamlPreservingComments requires a non-empty file path');
161
+ }
162
+ if (typeof originalContent !== 'string') {
163
+ throw new Error('writeYamlPreservingComments requires original content string');
164
+ }
165
+ const doc = YAML.parseDocument(originalContent);
166
+ if (doc.errors && doc.errors.length > 0) {
167
+ const first = doc.errors[0];
168
+ throw new Error(`Invalid YAML: ${first.message}`);
169
+ }
170
+ if (!doc.contents) {
171
+ doc.contents = doc.createNode({});
172
+ }
173
+ if (repairedVariables.externalIntegration !== undefined) {
174
+ doc.set('externalIntegration', doc.createNode(repairedVariables.externalIntegration));
175
+ }
176
+ if (repairedVariables.app !== undefined) {
177
+ doc.set('app', doc.createNode(repairedVariables.app));
178
+ }
179
+ fs.writeFileSync(filePath, String(doc), 'utf8');
180
+ }
181
+
147
182
  module.exports = {
148
183
  yamlToJson,
149
184
  jsonToYaml,
150
185
  loadConfigFile,
151
186
  writeConfigFile,
187
+ writeYamlPreservingComments,
152
188
  isYamlPath,
153
189
  isJsonPath
154
190
  };