@aifabrix/builder 2.39.0 → 2.39.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.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Deploy status display helpers: build app URL from controller/port and show after deploy.
3
+ * @fileoverview Status URL display for application deployment
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+
8
+ const chalk = require('chalk');
9
+ const logger = require('../utils/logger');
10
+ const { getApplicationStatus } = require('../api/applications.api');
11
+
12
+ /**
13
+ * Builds app URL from controller base URL and app port (e.g. http://localhost:3600 + 3601 -> http://localhost:3601).
14
+ * @param {string} controllerUrl - Controller base URL
15
+ * @param {number} port - Application port
16
+ * @returns {string|null} App URL or null if parsing fails
17
+ */
18
+ function buildAppUrlFromControllerAndPort(controllerUrl, port) {
19
+ if (!controllerUrl || (port === null || port === undefined) || typeof port !== 'number') return null;
20
+ try {
21
+ const u = new URL(controllerUrl);
22
+ u.port = String(port);
23
+ return u.toString();
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Parses URL and port from application status response body.
31
+ * @param {Object} body - Response body (may have data wrapper or top-level url/port)
32
+ * @returns {{ url: string|null, port: number|null }}
33
+ */
34
+ function parseUrlAndPortFromStatusBody(body) {
35
+ const data = body?.data ?? body;
36
+ const url = (data && typeof data.url === 'string' && data.url.trim() !== '')
37
+ ? data.url
38
+ : (body?.url && typeof body.url === 'string' && body.url.trim() !== '')
39
+ ? body.url
40
+ : null;
41
+ const port = (data && typeof data.port === 'number')
42
+ ? data.port
43
+ : (body && typeof body.port === 'number')
44
+ ? body.port
45
+ : null;
46
+ return { url, port };
47
+ }
48
+
49
+ /**
50
+ * Fetches app URL from controller application status and displays it.
51
+ * Uses status API url when present; otherwise derives from status port and controller host.
52
+ * @param {string} controllerUrl - Controller base URL (used only to derive host when status returns port)
53
+ * @param {string} envKey - Environment key
54
+ * @param {string} appKey - Application key (manifest.key)
55
+ * @param {Object} authConfig - Auth used for deployment (same as for status)
56
+ */
57
+ async function displayAppUrlFromController(controllerUrl, envKey, appKey, authConfig) {
58
+ let url = null;
59
+ let port = null;
60
+ try {
61
+ const res = await getApplicationStatus(controllerUrl, envKey, appKey, authConfig);
62
+ const parsed = parseUrlAndPortFromStatusBody(res?.data);
63
+ url = parsed.url;
64
+ port = parsed.port;
65
+ } catch (_) {
66
+ // Show fallback message below
67
+ }
68
+ if (!url && (port !== null && port !== undefined)) {
69
+ url = buildAppUrlFromControllerAndPort(controllerUrl, port);
70
+ }
71
+ if (url) {
72
+ logger.log(chalk.green(` ✓ App running at ${url}`));
73
+ } else {
74
+ logger.log(chalk.blue(' ✓ App deployed. Get URL from controller dashboard.'));
75
+ }
76
+ }
77
+
78
+ module.exports = { displayAppUrlFromController, buildAppUrlFromControllerAndPort, parseUrlAndPortFromStatusBody };
package/lib/app/deploy.js CHANGED
@@ -17,8 +17,8 @@ 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');
21
20
  const { loadDeploymentConfig } = require('./deploy-config');
21
+ const { displayAppUrlFromController } = require('./deploy-status-display');
22
22
 
23
23
  /**
24
24
  * Validate application name format
@@ -219,26 +219,6 @@ function displayDeploymentResults(result) {
219
219
  }
220
220
  }
221
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
-
242
222
  /**
243
223
  * Check if app is external and handle external deployment.
244
224
  * When options.type === 'external', forces deployment from integration/<app> (no app register needed).
@@ -19,17 +19,26 @@ const { runServiceUserCreate } = require('../commands/service-user');
19
19
  function setupServiceUserCommands(program) {
20
20
  const serviceUser = program
21
21
  .command('service-user')
22
- .description('Create service users for integrations (one-time secret on create)');
22
+ .description('Create and manage service users (API clients) for integrations and CI')
23
+ .addHelpText('after', `
24
+ Service users are dedicated accounts for integrations, CI pipelines, or API clients.
25
+ The controller returns a one-time clientSecret on create—save it immediately; it cannot be retrieved again.
26
+
27
+ Example:
28
+ $ aifabrix service-user create -u api-client-001 -e api@example.com \\
29
+ --redirect-uris "https://app.example.com/callback" --group-names "AI-Fabrix-Developers"
30
+
31
+ Required: permission service-user:create on the controller. Run "aifabrix login" first.`);
23
32
 
24
33
  serviceUser
25
34
  .command('create')
26
- .description('Create a service user (username, email, redirectUris, groupIds); receive one-time clientSecret (save it now)')
27
- .option('--controller <url>', 'Controller URL (default: from config)')
35
+ .description('Create a service user and receive a one-time clientSecret (save it now; it will not be shown again)')
36
+ .option('--controller <url>', 'Controller base URL (default: from config)')
28
37
  .option('-u, --username <username>', 'Service user username (required)')
29
38
  .option('-e, --email <email>', 'Email address (required)')
30
- .option('--redirect-uris <uris>', 'Comma-separated redirect URIs for OAuth2 (required, e.g. https://app.example.com/callback)')
31
- .option('--group-names <names>', 'Comma-separated group names (required, e.g. AI-Fabrix-Developers)')
32
- .option('-d, --description <description>', 'Optional description')
39
+ .option('--redirect-uris <uris>', 'Comma-separated OAuth2 redirect URIs (required)')
40
+ .option('--group-names <names>', 'Comma-separated group names to assign (required)')
41
+ .option('-d, --description <description>', 'Description for the service user')
33
42
  .action(async(options) => {
34
43
  try {
35
44
  const opts = {
@@ -36,16 +36,22 @@ async function getServiceUserAuth(controllerUrl) {
36
36
  }
37
37
 
38
38
  /**
39
- * Extract clientId and clientSecret from API response (response may be wrapped in data)
40
- * @param {Object} response - API response
39
+ * Extract clientId and clientSecret from API response.
40
+ * Controller returns { data: { user, clientSecret } }; API client puts body in response.data.
41
+ * So payload is at response.data.data. clientId may be on user.clientId or user.federatedIdentity.keycloakClientId.
42
+ * @param {Object} response - API response (success: true, data: body)
41
43
  * @returns {{ clientId: string, clientSecret: string }}
42
44
  */
43
45
  function extractCreateResponse(response) {
44
- const data = response?.data ?? response;
45
- return {
46
- clientId: data?.clientId ?? '',
47
- clientSecret: data?.clientSecret ?? ''
48
- };
46
+ const payload = response?.data?.data ?? response?.data ?? response;
47
+ const user = payload?.user;
48
+ const clientId =
49
+ user?.clientId ??
50
+ user?.federatedIdentity?.keycloakClientId ??
51
+ payload?.clientId ??
52
+ '';
53
+ const clientSecret = payload?.clientSecret ?? '';
54
+ return { clientId, clientSecret };
49
55
  }
50
56
 
51
57
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.39.0",
3
+ "version": "2.39.1",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {