@aifabrix/builder 2.37.9 → 2.39.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 (74) hide show
  1. package/.cursor/rules/project-rules.mdc +3 -0
  2. package/README.md +19 -0
  3. package/integration/hubspot/hubspot-deploy.json +1 -5
  4. package/integration/hubspot/hubspot-system.json +0 -3
  5. package/lib/api/applications.api.js +29 -1
  6. package/lib/api/auth.api.js +14 -0
  7. package/lib/api/credentials.api.js +34 -0
  8. package/lib/api/datasources-core.api.js +16 -1
  9. package/lib/api/datasources-extended.api.js +18 -1
  10. package/lib/api/deployments.api.js +32 -0
  11. package/lib/api/environments.api.js +11 -0
  12. package/lib/api/external-systems.api.js +16 -1
  13. package/lib/api/pipeline.api.js +12 -4
  14. package/lib/api/service-users.api.js +41 -0
  15. package/lib/api/types/applications.types.js +1 -1
  16. package/lib/api/types/deployments.types.js +1 -1
  17. package/lib/api/types/pipeline.types.js +1 -1
  18. package/lib/api/types/service-users.types.js +24 -0
  19. package/lib/api/wizard.api.js +40 -1
  20. package/lib/app/deploy.js +86 -21
  21. package/lib/app/rotate-secret.js +3 -1
  22. package/lib/app/run-helpers.js +35 -2
  23. package/lib/app/show-display.js +30 -11
  24. package/lib/app/show.js +34 -8
  25. package/lib/cli/index.js +4 -0
  26. package/lib/cli/setup-app.js +40 -0
  27. package/lib/cli/setup-credential-deployment.js +72 -0
  28. package/lib/cli/setup-infra.js +3 -3
  29. package/lib/cli/setup-service-user.js +52 -0
  30. package/lib/cli/setup-utility.js +1 -25
  31. package/lib/commands/app-down.js +80 -0
  32. package/lib/commands/app-logs.js +146 -0
  33. package/lib/commands/app.js +24 -1
  34. package/lib/commands/credential-list.js +104 -0
  35. package/lib/commands/deployment-list.js +184 -0
  36. package/lib/commands/service-user.js +193 -0
  37. package/lib/commands/up-common.js +74 -5
  38. package/lib/commands/up-dataplane.js +13 -7
  39. package/lib/commands/up-miso.js +17 -24
  40. package/lib/core/templates.js +2 -2
  41. package/lib/external-system/deploy.js +79 -15
  42. package/lib/generator/builders.js +8 -27
  43. package/lib/generator/external-controller-manifest.js +5 -4
  44. package/lib/generator/index.js +16 -14
  45. package/lib/generator/split.js +1 -0
  46. package/lib/generator/wizard.js +4 -1
  47. package/lib/schema/application-schema.json +6 -14
  48. package/lib/schema/deployment-rules.yaml +121 -0
  49. package/lib/schema/external-system.schema.json +0 -16
  50. package/lib/utils/app-register-config.js +10 -12
  51. package/lib/utils/app-run-containers.js +2 -1
  52. package/lib/utils/compose-generator.js +2 -1
  53. package/lib/utils/deployment-errors.js +10 -0
  54. package/lib/utils/environment-checker.js +25 -6
  55. package/lib/utils/help-builder.js +0 -1
  56. package/lib/utils/image-version.js +209 -0
  57. package/lib/utils/schema-loader.js +1 -1
  58. package/lib/utils/variable-transformer.js +7 -33
  59. package/lib/validation/external-manifest-validator.js +1 -1
  60. package/package.json +1 -1
  61. package/templates/applications/README.md.hbs +1 -3
  62. package/templates/applications/dataplane/Dockerfile +2 -2
  63. package/templates/applications/dataplane/README.md +20 -6
  64. package/templates/applications/dataplane/env.template +31 -2
  65. package/templates/applications/dataplane/rbac.yaml +1 -1
  66. package/templates/applications/dataplane/variables.yaml +7 -4
  67. package/templates/applications/keycloak/Dockerfile +3 -3
  68. package/templates/applications/keycloak/README.md +14 -4
  69. package/templates/applications/keycloak/env.template +17 -2
  70. package/templates/applications/keycloak/variables.yaml +2 -1
  71. package/templates/applications/miso-controller/README.md +1 -3
  72. package/templates/applications/miso-controller/env.template +85 -25
  73. package/templates/applications/miso-controller/rbac.yaml +15 -0
  74. package/templates/applications/miso-controller/variables.yaml +24 -23
@@ -9,6 +9,7 @@ const { ApiClient } = require('./index');
9
9
  /**
10
10
  * List external systems
11
11
  * GET /api/v1/external/systems
12
+ * @requiresPermission {Dataplane} external-system:read
12
13
  * @async
13
14
  * @function listExternalSystems
14
15
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -32,6 +33,7 @@ async function listExternalSystems(dataplaneUrl, authConfig, options = {}) {
32
33
  /**
33
34
  * Create external system
34
35
  * POST /api/v1/external/systems
36
+ * @requiresPermission {Dataplane} external-system:create
35
37
  * @async
36
38
  * @function createExternalSystem
37
39
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -50,12 +52,16 @@ async function createExternalSystem(dataplaneUrl, authConfig, systemData) {
50
52
  /**
51
53
  * Get external system details
52
54
  * GET /api/v1/external/systems/{systemIdOrKey}
55
+ * @requiresPermission {Dataplane} external-system:read
53
56
  * @async
54
57
  * @function getExternalSystem
55
58
  * @param {string} dataplaneUrl - Dataplane base URL
56
59
  * @param {string} systemIdOrKey - System ID or key
57
60
  * @param {Object} authConfig - Authentication configuration
58
- * @returns {Promise<Object>} External system details response
61
+ * @returns {Promise<Object>} External system details response. May include optional fields:
62
+ * - {string} [mcpServerUrl] - Full URL of external system MCP server when configured
63
+ * - {string} [apiDocumentUrl] - Full URL of API document (OpenAPI spec) when configured
64
+ * - {string} [openApiDocsPageUrl] - Full URL of dataplane API docs page when showOpenApiDocs is true
59
65
  * @throws {Error} If request fails
60
66
  */
61
67
  async function getExternalSystem(dataplaneUrl, systemIdOrKey, authConfig) {
@@ -66,6 +72,7 @@ async function getExternalSystem(dataplaneUrl, systemIdOrKey, authConfig) {
66
72
  /**
67
73
  * Update external system
68
74
  * PUT /api/v1/external/systems/{systemIdOrKey}
75
+ * @requiresPermission {Dataplane} external-system:update
69
76
  * @async
70
77
  * @function updateExternalSystem
71
78
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -85,6 +92,7 @@ async function updateExternalSystem(dataplaneUrl, systemIdOrKey, authConfig, upd
85
92
  /**
86
93
  * Delete external system (soft delete)
87
94
  * DELETE /api/v1/external/systems/{systemIdOrKey}
95
+ * @requiresPermission {Dataplane} external-system:delete
88
96
  * @async
89
97
  * @function deleteExternalSystem
90
98
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -101,6 +109,7 @@ async function deleteExternalSystem(dataplaneUrl, systemIdOrKey, authConfig) {
101
109
  /**
102
110
  * Get full config with application schema and dataSources
103
111
  * GET /api/v1/external/systems/{systemIdOrKey}/config
112
+ * @requiresPermission {Dataplane} external-system:read
104
113
  * @async
105
114
  * @function getExternalSystemConfig
106
115
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -117,6 +126,7 @@ async function getExternalSystemConfig(dataplaneUrl, systemIdOrKey, authConfig)
117
126
  /**
118
127
  * Create external system from integration template
119
128
  * POST /api/v1/external/systems/from-template
129
+ * @requiresPermission {Dataplane} external-system:create
120
130
  * @async
121
131
  * @function createFromTemplate
122
132
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -138,6 +148,7 @@ async function createFromTemplate(dataplaneUrl, authConfig, templateData) {
138
148
  /**
139
149
  * List OpenAPI files for system
140
150
  * GET /api/v1/external/systems/{systemIdOrKey}/openapi-files
151
+ * @requiresPermission {Dataplane} external-system:read
141
152
  * @async
142
153
  * @function listOpenAPIFiles
143
154
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -157,6 +168,7 @@ async function listOpenAPIFiles(dataplaneUrl, systemIdOrKey, authConfig, options
157
168
  /**
158
169
  * List OpenAPI endpoints for system
159
170
  * GET /api/v1/external/systems/{systemIdOrKey}/openapi-endpoints
171
+ * @requiresPermission {Dataplane} external-system:read
160
172
  * @async
161
173
  * @function listOpenAPIEndpoints
162
174
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -176,6 +188,7 @@ async function listOpenAPIEndpoints(dataplaneUrl, systemIdOrKey, authConfig, opt
176
188
  /**
177
189
  * Publish external system
178
190
  * POST /api/v1/external/systems/{systemIdOrKey}/publish
191
+ * @requiresPermission {Dataplane} external-system:update (or publish scope per dataplane spec)
179
192
  * @async
180
193
  * @function publishExternalSystem
181
194
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -196,6 +209,7 @@ async function publishExternalSystem(dataplaneUrl, systemIdOrKey, authConfig, pu
196
209
  /**
197
210
  * Rollback external system to version
198
211
  * POST /api/v1/external/systems/{systemIdOrKey}/rollback
212
+ * @requiresPermission {Dataplane} external-system:update
199
213
  * @async
200
214
  * @function rollbackExternalSystem
201
215
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -216,6 +230,7 @@ async function rollbackExternalSystem(dataplaneUrl, systemIdOrKey, authConfig, r
216
230
  /**
217
231
  * Save external system as integration template
218
232
  * POST /api/v1/external/systems/{systemIdOrKey}/save-template
233
+ * @requiresPermission {Dataplane} external-system:update
219
234
  * @async
220
235
  * @function saveAsTemplate
221
236
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -9,6 +9,7 @@ const { ApiClient } = require('./index');
9
9
  /**
10
10
  * Validate deployment configuration
11
11
  * POST /api/v1/pipeline/{envKey}/validate
12
+ * @requiresPermission {Controller} applications:deploy
12
13
  * @async
13
14
  * @function validatePipeline
14
15
  * @param {string} controllerUrl - Controller base URL
@@ -31,6 +32,7 @@ async function validatePipeline(controllerUrl, envKey, authConfig, validationDat
31
32
  /**
32
33
  * Deploy application using validateToken
33
34
  * POST /api/v1/pipeline/{envKey}/deploy
35
+ * @requiresPermission {Controller} applications:deploy
34
36
  * @async
35
37
  * @function deployPipeline
36
38
  * @param {string} controllerUrl - Controller base URL
@@ -52,6 +54,7 @@ async function deployPipeline(controllerUrl, envKey, authConfig, deployData) {
52
54
  /**
53
55
  * Get deployment status for CI/CD
54
56
  * GET /api/v1/pipeline/{envKey}/deployments/{deploymentId}
57
+ * @requiresPermission {Controller} applications:deploy
55
58
  * @async
56
59
  * @function getPipelineDeployment
57
60
  * @param {string} controllerUrl - Controller base URL
@@ -69,6 +72,7 @@ async function getPipelineDeployment(controllerUrl, envKey, deploymentId, authCo
69
72
  /**
70
73
  * Pipeline health check
71
74
  * GET /api/v1/pipeline/{envKey}/health
75
+ * @requiresPermission {Controller} Public (no auth required)
72
76
  * @async
73
77
  * @function getPipelineHealth
74
78
  * @param {string} controllerUrl - Controller base URL
@@ -87,7 +91,7 @@ async function getPipelineHealth(controllerUrl, envKey) {
87
91
  * Request body: external system JSON (external-system.schema.json). Optional field in body:
88
92
  * generateMcpContract (boolean, default true). Optional: generateOpenApiContract (boolean).
89
93
  * Do not use query parameters for MCP; use the field in the body only.
90
- *
94
+ * @requiresPermission {Dataplane} Authenticated (oauth2: [])
91
95
  * @async
92
96
  * @function publishSystemViaPipeline
93
97
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -107,7 +111,7 @@ async function publishSystemViaPipeline(dataplaneUrl, authConfig, systemConfig)
107
111
  * Publish datasource via dataplane pipeline endpoint
108
112
  * POST /api/v1/pipeline/{systemKey}/publish
109
113
  * No generateMcpContract for this endpoint; dataplane always uses default (MCP generated).
110
- *
114
+ * @requiresPermission {Dataplane} Authenticated (oauth2: [])
111
115
  * @async
112
116
  * @function publishDatasourceViaPipeline
113
117
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -127,6 +131,7 @@ async function publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig,
127
131
  /**
128
132
  * Test datasource via dataplane pipeline endpoint
129
133
  * POST /api/v1/pipeline/{systemKey}/{datasourceKey}/test
134
+ * @requiresPermission {Dataplane} Authenticated (oauth2: [])
130
135
  * @async
131
136
  * @function testDatasourceViaPipeline
132
137
  * @param {Object} params - Function parameters
@@ -156,6 +161,7 @@ async function testDatasourceViaPipeline({ dataplaneUrl, systemKey, datasourceKe
156
161
  /**
157
162
  * Deploy external system via dataplane pipeline endpoint
158
163
  * POST /api/v1/pipeline/deploy
164
+ * @requiresPermission {Dataplane} Authenticated (oauth2: [])
159
165
  * @async
160
166
  * @function deployExternalSystemViaPipeline
161
167
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -174,6 +180,7 @@ async function deployExternalSystemViaPipeline(dataplaneUrl, authConfig, systemC
174
180
  /**
175
181
  * Deploy datasource via dataplane pipeline endpoint
176
182
  * POST /api/v1/pipeline/{systemKey}/deploy
183
+ * @requiresPermission {Dataplane} Authenticated (oauth2: [])
177
184
  * @async
178
185
  * @function deployDatasourceViaPipeline
179
186
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -196,7 +203,7 @@ async function deployDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig,
196
203
  * Body: { version, application, dataSources }. Include application.generateMcpContract
197
204
  * and/or application.generateOpenApiContract to control contract generation when
198
205
  * publishing this upload (publish reads from stored config; no query param on publish).
199
- *
206
+ * @requiresPermission {Dataplane} Authenticated (oauth2: [])
200
207
  * @async
201
208
  * @function uploadApplicationViaPipeline
202
209
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -215,6 +222,7 @@ async function uploadApplicationViaPipeline(dataplaneUrl, authConfig, applicatio
215
222
  /**
216
223
  * Validate upload via dataplane pipeline endpoint
217
224
  * POST /api/v1/pipeline/upload/{uploadId}/validate
225
+ * @requiresPermission {Dataplane} Authenticated (oauth2: [])
218
226
  * @async
219
227
  * @function validateUploadViaPipeline
220
228
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -235,7 +243,7 @@ async function validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig) {
235
243
  * that was uploaded (application.generateMcpContract, application.generateOpenApiContract).
236
244
  * To control MCP/OpenAPI, include those fields in the application object when calling
237
245
  * uploadApplicationViaPipeline.
238
- *
246
+ * @requiresPermission {Dataplane} Authenticated (oauth2: [])
239
247
  * @async
240
248
  * @function publishUploadViaPipeline
241
249
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @fileoverview Service users API functions (create service user with one-time secret)
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const { ApiClient } = require('./index');
8
+
9
+ /**
10
+ * Create a service user (username, email, redirectUris, groupIds); return one-time secret
11
+ * POST /api/v1/service-users
12
+ * @requiresPermission {Controller} service-user:create
13
+ * @async
14
+ * @function createServiceUser
15
+ * @param {string} controllerUrl - Controller base URL
16
+ * @param {Object} authConfig - Authentication configuration (bearer or client-credentials)
17
+ * @param {Object} body - Request body
18
+ * @param {string} body.username - Username (required)
19
+ * @param {string} body.email - Email (required)
20
+ * @param {string[]} body.redirectUris - Redirect URIs for OAuth2 (required, min 1)
21
+ * @param {string[]} body.groupNames - Group names (required, e.g. AI-Fabrix-Developers)
22
+ * @param {string} [body.description] - Optional description
23
+ * @returns {Promise<Object>} Response with clientId and one-time clientSecret
24
+ * @throws {Error} If request fails (400/401/403 or network)
25
+ */
26
+ async function createServiceUser(controllerUrl, authConfig, body) {
27
+ const client = new ApiClient(controllerUrl, authConfig);
28
+ return await client.post('/api/v1/service-users', {
29
+ body: {
30
+ username: body.username,
31
+ email: body.email,
32
+ redirectUris: body.redirectUris,
33
+ groupNames: body.groupNames,
34
+ description: body.description
35
+ }
36
+ });
37
+ }
38
+
39
+ module.exports = {
40
+ createServiceUser
41
+ };
@@ -30,7 +30,7 @@
30
30
  * @property {string} displayName - Human-readable application name
31
31
  * @property {string} description - Application description
32
32
  * @property {string} type - Azure application type ('webapp' | 'functionapp' | 'api' | 'service' | 'external')
33
- * @property {string} deploymentKey - SHA256 hash of deployment manifest
33
+ * @property {string} [deploymentKey] - SHA256 hash of deployment manifest (Controller adds internally)
34
34
  * @property {string} [image] - Container image reference
35
35
  * @property {string} [registryMode] - Registry mode ('acr' | 'external' | 'public')
36
36
  * @property {number} [port] - Application port number
@@ -30,7 +30,7 @@
30
30
  * @property {string} displayName - Human-readable application name
31
31
  * @property {string} description - Application description
32
32
  * @property {string} type - Azure application type
33
- * @property {string} deploymentKey - SHA256 hash of deployment manifest
33
+ * @property {string} [deploymentKey] - SHA256 hash of deployment manifest (Controller adds internally)
34
34
  * @property {*} [additionalProperties] - Additional configuration properties
35
35
  */
36
36
 
@@ -11,7 +11,7 @@
11
11
  * @property {string} displayName - Human-readable application name
12
12
  * @property {string} description - Application description
13
13
  * @property {string} type - Azure application type
14
- * @property {string} deploymentKey - SHA256 hash of deployment manifest
14
+ * @property {string} [deploymentKey] - SHA256 hash of deployment manifest (Controller adds internally)
15
15
  * @property {*} [additionalProperties] - Additional configuration properties
16
16
  */
17
17
 
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @fileoverview Service users API type definitions
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ /**
8
+ * Service user create request body (builder sends this to controller)
9
+ * @typedef {Object} ServiceUserCreateRequest
10
+ * @property {string} username - Username (1–100 chars, e.g. api-client-001)
11
+ * @property {string} email - Email address
12
+ * @property {string[]} redirectUris - Allowed redirect URIs for OAuth2 (min 1)
13
+ * @property {string[]} groupNames - Group names to assign (e.g. AI-Fabrix-Developers)
14
+ * @property {string} [description] - Optional description (max 500 chars)
15
+ */
16
+
17
+ /**
18
+ * Service user create response (clientSecret is one-time-only; store at creation time)
19
+ * @typedef {Object} ServiceUserCreateResponse
20
+ * @property {string} clientId - Stable identifier for the service user
21
+ * @property {string} clientSecret - One-time-only secret; not returned by any other endpoint
22
+ * @property {boolean} [success] - Optional wrapper flag
23
+ * @property {string} [createdAt] - Optional creation timestamp (ISO 8601)
24
+ */
@@ -10,6 +10,7 @@ const { uploadFile } = require('../utils/file-upload');
10
10
  /**
11
11
  * Create wizard session
12
12
  * POST /api/v1/wizard/sessions
13
+ * @requiresPermission {Dataplane} external-system:create
13
14
  * @async
14
15
  * @function createWizardSession
15
16
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -33,6 +34,7 @@ async function createWizardSession(dataplaneUrl, authConfig, mode, systemIdOrKey
33
34
  /**
34
35
  * Get wizard session
35
36
  * GET /api/v1/wizard/sessions/{sessionId}
37
+ * @requiresPermission {Dataplane} external-system:read or external-system:create
36
38
  * @async
37
39
  * @function getWizardSession
38
40
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -49,6 +51,7 @@ async function getWizardSession(dataplaneUrl, sessionId, authConfig) {
49
51
  /**
50
52
  * Update wizard session
51
53
  * PUT /api/v1/wizard/sessions/{sessionId}
54
+ * @requiresPermission {Dataplane} external-system:create
52
55
  * @async
53
56
  * @function updateWizardSession
54
57
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -80,6 +83,7 @@ async function updateWizardSession(dataplaneUrl, sessionId, authConfig, updateDa
80
83
  /**
81
84
  * Delete wizard session
82
85
  * DELETE /api/v1/wizard/sessions/{sessionId}
86
+ * @requiresPermission {Dataplane} external-system:create
83
87
  * @async
84
88
  * @function deleteWizardSession
85
89
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -96,6 +100,7 @@ async function deleteWizardSession(dataplaneUrl, sessionId, authConfig) {
96
100
  /**
97
101
  * Get wizard session progress
98
102
  * GET /api/v1/wizard/sessions/{sessionId}/progress
103
+ * @requiresPermission {Dataplane} external-system:create
99
104
  * @async
100
105
  * @function getWizardProgress
101
106
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -112,6 +117,7 @@ async function getWizardProgress(dataplaneUrl, sessionId, authConfig) {
112
117
  /**
113
118
  * Parse OpenAPI file or URL
114
119
  * POST /api/v1/wizard/parse-openapi
120
+ * @requiresPermission {Dataplane} external-system:create
115
121
  * @async
116
122
  * @function parseOpenApi
117
123
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -133,6 +139,7 @@ async function parseOpenApi(dataplaneUrl, authConfig, openApiFilePathOrUrl, isUr
133
139
  /**
134
140
  * Credential selection for wizard
135
141
  * POST /api/v1/wizard/credential-selection
142
+ * @requiresPermission {Dataplane} external-system:create
136
143
  * @async
137
144
  * @function credentialSelection
138
145
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -154,6 +161,7 @@ async function credentialSelection(dataplaneUrl, authConfig, selectionData) {
154
161
  /**
155
162
  * Detect API type from OpenAPI spec
156
163
  * POST /api/v1/wizard/detect-type
164
+ * @requiresPermission {Dataplane} external-system:create
157
165
  * @async
158
166
  * @function detectType
159
167
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -172,6 +180,7 @@ async function detectType(dataplaneUrl, authConfig, openapiSpec) {
172
180
  /**
173
181
  * Generate configuration via AI
174
182
  * POST /api/v1/wizard/generate-config
183
+ * @requiresPermission {Dataplane} external-system:create
175
184
  * @async
176
185
  * @function generateConfig
177
186
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -202,6 +211,7 @@ async function generateConfig(dataplaneUrl, authConfig, config) {
202
211
  /**
203
212
  * Generate configuration via AI (streaming)
204
213
  * POST /api/v1/wizard/generate-config-stream
214
+ * @requiresPermission {Dataplane} external-system:create
205
215
  * @async
206
216
  * @function generateConfigStream
207
217
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -220,6 +230,7 @@ async function generateConfigStream(dataplaneUrl, authConfig, config) {
220
230
  /**
221
231
  * Validate wizard configuration
222
232
  * POST /api/v1/wizard/validate
233
+ * @requiresPermission {Dataplane} external-system:create
223
234
  * @async
224
235
  * @function validateWizardConfig
225
236
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -242,6 +253,7 @@ async function validateWizardConfig(dataplaneUrl, authConfig, systemConfig, data
242
253
  /**
243
254
  * Validate all completed wizard steps
244
255
  * GET /api/v1/wizard/sessions/{sessionId}/validate
256
+ * @requiresPermission {Dataplane} external-system:create
245
257
  * @async
246
258
  * @function validateAllSteps
247
259
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -258,6 +270,7 @@ async function validateAllSteps(dataplaneUrl, sessionId, authConfig) {
258
270
  /**
259
271
  * Validate specific wizard step
260
272
  * POST /api/v1/wizard/sessions/{sessionId}/validate-step
273
+ * @requiresPermission {Dataplane} external-system:create
261
274
  * @async
262
275
  * @function validateStep
263
276
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -275,6 +288,7 @@ async function validateStep(dataplaneUrl, sessionId, authConfig, stepNumber) {
275
288
  /**
276
289
  * Get configuration preview with summaries
277
290
  * GET /api/v1/wizard/preview/{sessionId}
291
+ * @requiresPermission {Dataplane} external-system:create
278
292
  * @async
279
293
  * @function getPreview
280
294
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -291,6 +305,7 @@ async function getPreview(dataplaneUrl, sessionId, authConfig) {
291
305
  /**
292
306
  * Test MCP server connection
293
307
  * POST /api/v1/wizard/test-mcp-connection
308
+ * @requiresPermission {Dataplane} external-system:create
294
309
  * @async
295
310
  * @function testMcpConnection
296
311
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -313,6 +328,7 @@ async function testMcpConnection(dataplaneUrl, authConfig, serverUrl, token) {
313
328
  /**
314
329
  * Get deployment documentation for a system (from dataplane DB only)
315
330
  * GET /api/v1/wizard/deployment-docs/{systemKey}
331
+ * @requiresPermission {Dataplane} external-system:read
316
332
  * @async
317
333
  * @function getDeploymentDocs
318
334
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -330,6 +346,7 @@ async function getDeploymentDocs(dataplaneUrl, authConfig, systemKey) {
330
346
  * Generate deployment documentation with variables.yaml and deploy JSON for better quality
331
347
  * POST /api/v1/wizard/deployment-docs/{systemKey}
332
348
  * Sends deployJson and variablesYaml in the request body so the dataplane can align README with the integration folder.
349
+ * @requiresPermission {Dataplane} external-system:update
333
350
  * @async
334
351
  * @function postDeploymentDocs
335
352
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -353,6 +370,7 @@ async function postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body = nu
353
370
  * GET /api/v1/wizard/platforms
354
371
  * Expected response: { platforms: [ { key: string, displayName?: string } ] } or equivalent.
355
372
  * On 404 or error, returns empty array (caller should hide "Known platform" choice).
373
+ * @requiresPermission {Dataplane} external-system:read or unauthenticated
356
374
  * @async
357
375
  * @function getWizardPlatforms
358
376
  * @param {string} dataplaneUrl - Dataplane base URL
@@ -370,6 +388,26 @@ async function getWizardPlatforms(dataplaneUrl, authConfig) {
370
388
  }
371
389
  }
372
390
 
391
+ /**
392
+ * List credentials for wizard selection (Step 3)
393
+ * GET /api/v1/wizard/credentials
394
+ * @requiresPermission {Dataplane} credential:read or external-system:create
395
+ * @async
396
+ * @function listWizardCredentials
397
+ * @param {string} dataplaneUrl - Dataplane base URL
398
+ * @param {Object} authConfig - Authentication configuration
399
+ * @param {Object} [options] - Query options
400
+ * @param {boolean} [options.activeOnly] - If true, return only active credentials
401
+ * @returns {Promise<Object>} Response with credentials list (e.g. data.credentials or data.items)
402
+ * @throws {Error} If request fails
403
+ */
404
+ async function listWizardCredentials(dataplaneUrl, authConfig, options = {}) {
405
+ const client = new ApiClient(dataplaneUrl, authConfig);
406
+ return await client.get('/api/v1/wizard/credentials', {
407
+ params: options
408
+ });
409
+ }
410
+
373
411
  module.exports = {
374
412
  createWizardSession,
375
413
  getWizardSession,
@@ -388,5 +426,6 @@ module.exports = {
388
426
  testMcpConnection,
389
427
  getDeploymentDocs,
390
428
  postDeploymentDocs,
391
- getWizardPlatforms
429
+ getWizardPlatforms,
430
+ listWizardCredentials
392
431
  };
package/lib/app/deploy.js CHANGED
@@ -17,6 +17,7 @@ const pushUtils = require('../deployment/push');
17
17
  const logger = require('../utils/logger');
18
18
  const { detectAppType, getBuilderPath, getIntegrationPath } = require('../utils/paths');
19
19
  const { checkApplicationExists } = require('../utils/app-existence');
20
+ const { getApplicationStatus } = require('../api/applications.api');
20
21
  const { loadDeploymentConfig } = require('./deploy-config');
21
22
 
22
23
  /**
@@ -218,6 +219,26 @@ function displayDeploymentResults(result) {
218
219
  }
219
220
  }
220
221
 
222
+ /**
223
+ * Fetches app URL from controller application status and displays it.
224
+ * On API failure or missing URL, shows controllerUrl so the user always sees where the app is.
225
+ * @param {string} controllerUrl - Controller base URL (used as fallback)
226
+ * @param {string} envKey - Environment key
227
+ * @param {string} appKey - Application key (manifest.key)
228
+ * @param {Object} authConfig - Auth used for deployment (same as for status)
229
+ */
230
+ async function displayAppUrlFromController(controllerUrl, envKey, appKey, authConfig) {
231
+ let url = null;
232
+ try {
233
+ const res = await getApplicationStatus(controllerUrl, envKey, appKey, authConfig);
234
+ const body = res?.data;
235
+ url = (body && (body.url || body.data?.url)) || res?.url || null;
236
+ } catch (_) {
237
+ // Use controllerUrl fallback below
238
+ }
239
+ logger.log(chalk.green(` āœ“ App running at ${url || controllerUrl}`));
240
+ }
241
+
221
242
  /**
222
243
  * Check if app is external and handle external deployment.
223
244
  * When options.type === 'external', forces deployment from integration/<app> (no app register needed).
@@ -272,6 +293,59 @@ function validateImageIsPullable(imageRef, appName) {
272
293
  }
273
294
  }
274
295
 
296
+ /**
297
+ * Throws an error when deployment status is failed or cancelled
298
+ * @param {string} status - Status value
299
+ * @param {Object} statusObj - Full status object from result
300
+ * @throws {Error}
301
+ */
302
+ function throwIfDeploymentFailed(status, statusObj) {
303
+ if (status !== 'failed' && status !== 'cancelled') return;
304
+ const msg =
305
+ statusObj.message ||
306
+ statusObj.error ||
307
+ (status === 'cancelled' ? 'Deployment cancelled' : 'Deployment failed');
308
+ const err = new Error(`Deployment ${status}: ${msg}`);
309
+ err.formatted = `Deployment ${status}.\n\n${msg}`;
310
+ err.deploymentStatus = statusObj;
311
+ throw err;
312
+ }
313
+
314
+ /**
315
+ * Enhances 401 error with rotate-secret hint when app exists
316
+ * @param {Error} error - Caught error
317
+ * @param {boolean} appExists - Whether app exists in controller
318
+ * @param {string} appName - Application name
319
+ * @param {string} envKey - Environment key
320
+ * @throws {Error} Enhanced or original error
321
+ */
322
+ function enhanceAuthErrorIfNeeded(error, appExists, appName, envKey) {
323
+ if (!appExists || error.status !== 401 || error.message.includes('rotate-secret')) {
324
+ throw error;
325
+ }
326
+ const enhancedError = new Error(
327
+ `${error.message}\n\nšŸ’” The application '${appName}' exists in environment '${envKey}'. ` +
328
+ `To fix invalid credentials, rotate the application secret:\n aifabrix app rotate-secret ${appName}`
329
+ );
330
+ enhancedError.status = 401;
331
+ enhancedError.formatted = error.formatted || enhancedError.message;
332
+ throw enhancedError;
333
+ }
334
+
335
+ /**
336
+ * Apply image/registry overrides from options to manifest
337
+ * @param {Object} manifest - Deployment manifest
338
+ * @param {Object} options - Deployment options
339
+ */
340
+ function applyManifestOverrides(manifest, options) {
341
+ if (options.imageOverride || options.image) {
342
+ manifest.image = options.imageOverride || options.image;
343
+ }
344
+ if (options.registryMode) {
345
+ manifest.registryMode = options.registryMode;
346
+ }
347
+ }
348
+
275
349
  /**
276
350
  * Execute standard application deployment flow
277
351
  * @async
@@ -283,38 +357,29 @@ function validateImageIsPullable(imageRef, appName) {
283
357
  async function executeStandardDeployment(appName, options) {
284
358
  const config = await loadDeploymentConfig(appName, options);
285
359
  const controllerUrl = config.controllerUrl || 'unknown';
286
-
287
- // Check if application exists before deployment
288
360
  const appExists = await checkApplicationExists(appName, controllerUrl, config.envKey, config.auth);
289
361
 
290
362
  const { manifest, manifestPath } = await generateAndValidateManifest(appName);
291
- if (options.imageOverride || options.image) {
292
- manifest.image = options.imageOverride || options.image;
293
- }
294
- if (options.registryMode) {
295
- manifest.registryMode = options.registryMode;
296
- }
297
-
363
+ applyManifestOverrides(manifest, options);
298
364
  validateImageIsPullable(manifest.image, appName);
299
-
300
365
  displayDeploymentInfo(manifest, manifestPath);
301
366
 
302
367
  try {
303
368
  const result = await executeDeployment(manifest, config);
304
369
  displayDeploymentResults(result);
305
- return { result, controllerUrl, appExists };
306
- } catch (error) {
307
- // Enhance error if app exists and credentials are invalid
308
- if (appExists && error.status === 401 && !error.message.includes('rotate-secret')) {
309
- const enhancedError = new Error(
310
- `${error.message}\n\nšŸ’” The application '${appName}' exists in environment '${config.envKey}'. ` +
311
- `To fix invalid credentials, rotate the application secret:\n aifabrix app rotate-secret ${appName}`
370
+ const status = result.status?.status;
371
+ throwIfDeploymentFailed(status, result.status || {});
372
+ if (status === 'completed') {
373
+ await displayAppUrlFromController(
374
+ config.controllerUrl,
375
+ config.envKey,
376
+ manifest.key,
377
+ config.auth
312
378
  );
313
- enhancedError.status = 401;
314
- enhancedError.formatted = error.formatted || enhancedError.message;
315
- throw enhancedError;
316
379
  }
317
- throw error;
380
+ return { result, controllerUrl, appExists };
381
+ } catch (error) {
382
+ enhanceAuthErrorIfNeeded(error, appExists, appName, config.envKey);
318
383
  }
319
384
  }
320
385
 
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  const chalk = require('chalk');
12
- const { getConfig, normalizeControllerUrl, resolveEnvironment } = require('../core/config');
12
+ const { getConfig, normalizeControllerUrl, resolveEnvironment, clearClientToken } = require('../core/config');
13
13
  const { resolveControllerUrl } = require('../utils/controller-url');
14
14
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
15
15
  const { rotateApplicationSecret } = require('../api/applications.api');
@@ -324,6 +324,8 @@ async function executeRotation(appKey, actualControllerUrl, environment, token)
324
324
  const { credentials, message } = validateResponse(response);
325
325
  await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
326
326
  displayRotationResults(appKey, environment, credentials, actualControllerUrl, message);
327
+ // Clear cached client token so next deploy uses new credentials (avoids "Invalid or expired credentials" when running deploy/up-dataplane immediately after rotate)
328
+ await clearClientToken(environment, appKey);
327
329
  }
328
330
 
329
331
  /**