@aifabrix/builder 2.46.0 → 2.47.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 (191) hide show
  1. package/.npmrc.token +1 -0
  2. package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +1 -0
  3. package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +17 -0
  4. package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +1 -0
  5. package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +1 -0
  6. package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +1 -0
  7. package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +17 -0
  8. package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +17 -0
  9. package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +17 -0
  10. package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +17 -0
  11. package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +17 -0
  12. package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +17 -0
  13. package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +17 -0
  14. package/.temp/validation/summary.txt +5 -0
  15. package/jest.isolated-projects.js +18 -2
  16. package/jest.projects.js +24 -0
  17. package/lib/api/dataplane-health.api.js +19 -7
  18. package/lib/api/lifecycle.api.js +22 -1
  19. package/lib/api/resource-types.api.js +107 -0
  20. package/lib/api/runtime-configuration.api.js +72 -0
  21. package/lib/api/types/resource-types.types.js +45 -0
  22. package/lib/api/user-groups.api.js +9 -9
  23. package/lib/api/users.api.js +8 -8
  24. package/lib/api/wizard.api.js +10 -2
  25. package/lib/app/down.js +58 -26
  26. package/lib/app/index.js +2 -2
  27. package/lib/app/run-resolve-image.js +2 -1
  28. package/lib/app/run-stop-containers.js +113 -0
  29. package/lib/app/run.js +1 -31
  30. package/lib/app/show-display.js +35 -0
  31. package/lib/app/show.js +32 -1
  32. package/lib/build/index.js +17 -5
  33. package/lib/build/miso-controller-npm-sdk-guard.js +25 -0
  34. package/lib/channels/add-channel.js +348 -0
  35. package/lib/channels/channel-artifact-generator.js +141 -0
  36. package/lib/channels/channel-audit.js +101 -0
  37. package/lib/channels/channel-auth-context.js +37 -0
  38. package/lib/channels/channel-credential.js +200 -0
  39. package/lib/channels/channel-display-name.js +64 -0
  40. package/lib/channels/channel-external-system.js +106 -0
  41. package/lib/channels/channel-integration-generator.js +274 -0
  42. package/lib/channels/channel-probe.js +89 -0
  43. package/lib/channels/channel-runtime-resolver.js +152 -0
  44. package/lib/channels/channel-status-display.js +141 -0
  45. package/lib/channels/channel-targets.js +115 -0
  46. package/lib/cli/index.js +18 -4
  47. package/lib/cli/infra-guided-auth.js +36 -12
  48. package/lib/cli/setup-app.js +54 -3
  49. package/lib/cli/setup-infra-up-dataplane-action.js +2 -2
  50. package/lib/cli/setup-infra-up-platform-action.js +2 -1
  51. package/lib/cli/setup-npm.js +59 -0
  52. package/lib/cli/setup-platform.js +1 -1
  53. package/lib/cli/setup-secrets.js +1 -1
  54. package/lib/cli/setup-utility-help-after.js +4 -1
  55. package/lib/cli/setup-utility-resolve.js +8 -5
  56. package/lib/commands/app-logs.js +54 -26
  57. package/lib/commands/app.js +41 -0
  58. package/lib/commands/auth-status-dataplane-version.js +97 -61
  59. package/lib/commands/auth-status.js +2 -7
  60. package/lib/commands/channel.js +58 -0
  61. package/lib/commands/datasource.js +1 -1
  62. package/lib/commands/identity-apply-sync.js +49 -8
  63. package/lib/commands/identity-group-cli.js +66 -26
  64. package/lib/commands/identity-membership-role-cache.js +58 -37
  65. package/lib/commands/identity-shared.js +22 -1
  66. package/lib/commands/identity-user-cli.js +80 -34
  67. package/lib/commands/login-device.js +11 -3
  68. package/lib/commands/login.js +8 -3
  69. package/lib/commands/npm-config.js +103 -0
  70. package/lib/commands/repair-auth-apply.js +30 -4
  71. package/lib/commands/repair-auth-config.js +8 -13
  72. package/lib/commands/repair-datasource-com-v2.js +15 -0
  73. package/lib/commands/repair-datasource-service-contract.js +206 -0
  74. package/lib/commands/repair-datasource.js +53 -18
  75. package/lib/commands/repair-env-template.js +12 -12
  76. package/lib/commands/repair-openapi-sync.js +26 -4
  77. package/lib/commands/repair-steps.js +28 -13
  78. package/lib/commands/repair-system-alignment.js +7 -4
  79. package/lib/commands/resource-type.js +371 -0
  80. package/lib/commands/setup-modes.js +1 -1
  81. package/lib/commands/setup-prompts-platform-mode.js +24 -21
  82. package/lib/commands/test-trust-external.js +39 -90
  83. package/lib/commands/up-common.js +33 -1
  84. package/lib/commands/up-dataplane.js +8 -4
  85. package/lib/commands/upload.js +24 -3
  86. package/lib/commands/verify-trust-command-action.js +4 -1
  87. package/lib/commands/verify-trust-external.js +12 -20
  88. package/lib/core/config-setup-platform-mode.js +3 -3
  89. package/lib/core/setup-platform-mode.js +41 -6
  90. package/lib/datasource/capability/capability-diff-slice.js +2 -2
  91. package/lib/datasource/capability/capability-manifest.js +254 -0
  92. package/lib/datasource/capability/capability-resolve.js +3 -4
  93. package/lib/datasource/capability/copy-operations.js +5 -3
  94. package/lib/datasource/capability/remove-operations.js +2 -1
  95. package/lib/datasource/capability/run-capability-copy.js +32 -4
  96. package/lib/datasource/capability/run-capability-diff.js +3 -3
  97. package/lib/datasource/capability/run-capability-dimension.js +2 -4
  98. package/lib/datasource/capability/run-capability-edit.js +4 -7
  99. package/lib/datasource/capability/run-capability-relate.js +2 -4
  100. package/lib/datasource/capability/run-capability-remove.js +2 -4
  101. package/lib/datasource/capability/validate-capability-slice.js +2 -1
  102. package/lib/datasource/datasource-identifier.js +2 -1
  103. package/lib/datasource/datasource-validate-display.js +1 -1
  104. package/lib/datasource/field-reference-validator.js +24 -3
  105. package/lib/datasource/orphan-profiles-validator.js +98 -0
  106. package/lib/datasource/validate.js +41 -23
  107. package/lib/external-system/download-helpers.js +87 -0
  108. package/lib/external-system/download.js +61 -122
  109. package/lib/external-system/generator.js +1 -59
  110. package/lib/external-system/test.js +10 -15
  111. package/lib/generator/builders.js +11 -1
  112. package/lib/generator/external-controller-manifest.js +5 -2
  113. package/lib/generator/split.js +3 -1
  114. package/lib/generator/wizard.js +1 -28
  115. package/lib/identity/fk-reference.js +65 -0
  116. package/lib/identity/identity-apply-core.js +13 -58
  117. package/lib/identity/identity-apply-phases.js +5 -1
  118. package/lib/identity/identity-display.js +243 -0
  119. package/lib/identity/identity-response-helpers.js +52 -0
  120. package/lib/identity/identity-sync-errors.js +62 -0
  121. package/lib/identity/resolve-user-id.js +129 -0
  122. package/lib/identity/username-default.js +31 -0
  123. package/lib/lifecycle/certification-progress.js +2 -2
  124. package/lib/lifecycle/report-display-trust.js +70 -0
  125. package/lib/lifecycle/report-display.js +14 -13
  126. package/lib/resolvers/datasource-resolver.js +2 -3
  127. package/lib/resolvers/resource-type-file.js +159 -0
  128. package/lib/schema/application-schema.json +16 -0
  129. package/lib/schema/deployment-rules.yaml +2 -1
  130. package/lib/schema/external-datasource.schema.json +372 -58
  131. package/lib/schema/external-system.schema.json +331 -2
  132. package/lib/schema/type/document-storage.json +9 -5
  133. package/lib/utils/api.js +2 -1
  134. package/lib/utils/app-run-containers.js +25 -2
  135. package/lib/utils/authentication-shape.js +224 -0
  136. package/lib/utils/build-copy.js +36 -5
  137. package/lib/utils/build-resolve-image.js +66 -12
  138. package/lib/utils/celery-companion-apps.js +53 -0
  139. package/lib/utils/cli-utils.js +4 -0
  140. package/lib/utils/compose-celery-worker.js +23 -0
  141. package/lib/utils/compose-generate-docker-compose.js +9 -1
  142. package/lib/utils/configuration-env-resolver.js +36 -0
  143. package/lib/utils/controller-url.js +4 -1
  144. package/lib/utils/credential-secrets-env-kv.js +216 -0
  145. package/lib/utils/credential-secrets-env.js +52 -262
  146. package/lib/utils/dataplane-resolver.js +4 -115
  147. package/lib/utils/declarative-url-ports.js +24 -1
  148. package/lib/utils/dev-platform-tls.js +148 -0
  149. package/lib/utils/docker-build.js +1 -4
  150. package/lib/utils/external-env-template.js +10 -3
  151. package/lib/utils/external-system-validators.js +8 -2
  152. package/lib/utils/health-check-request.js +114 -0
  153. package/lib/utils/health-check-url.js +3 -1
  154. package/lib/utils/health-check.js +1 -113
  155. package/lib/utils/help-builder.js +3 -0
  156. package/lib/utils/keycloak-realm-ready.js +5 -1
  157. package/lib/utils/localhost-public-host.js +31 -0
  158. package/lib/utils/npm-user-config.js +145 -0
  159. package/lib/utils/paths.js +25 -0
  160. package/lib/utils/platform-controller-url.js +58 -4
  161. package/lib/utils/platform-kv-ref.js +57 -0
  162. package/lib/utils/runtime-configuration-download.js +189 -0
  163. package/lib/utils/runtime-configuration-values.js +119 -0
  164. package/lib/utils/schema-loader.js +19 -4
  165. package/lib/utils/url-declarative-public-base.js +33 -25
  166. package/lib/utils/worker-sidecars.js +142 -0
  167. package/lib/validation/external-manifest-validator.js +2 -0
  168. package/lib/validation/runtime-configuration-rules.js +98 -0
  169. package/lib/validation/validate-display-dimensions.js +154 -0
  170. package/lib/validation/validate-display-sections.js +276 -0
  171. package/lib/validation/validate-display-steps.js +67 -0
  172. package/lib/validation/validate-display.js +24 -402
  173. package/package.json +1 -1
  174. package/templates/applications/dataplane/application.yaml +5 -0
  175. package/templates/applications/dataplane/env.template +26 -13
  176. package/templates/applications/dataplane/rbac.yaml +108 -1
  177. package/templates/applications/miso-controller/application.yaml +2 -2
  178. package/templates/applications/miso-controller/env.template +37 -122
  179. package/templates/applications/miso-controller/rbac.yaml +28 -13
  180. package/templates/channels/chatgpt/install.template.md.hbs +49 -0
  181. package/templates/channels/chatgpt/manifest.template.json.hbs +9 -0
  182. package/templates/channels/microsoft-copilot/install.template.md.hbs +49 -0
  183. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +8 -0
  184. package/templates/channels/slack/install.template.md.hbs +49 -0
  185. package/templates/channels/slack/manifest.template.json.hbs +66 -0
  186. package/templates/channels/teams-bot/install.template.md.hbs +49 -0
  187. package/templates/channels/teams-bot/manifest.template.json.hbs +13 -0
  188. package/templates/channels/web-embed/install.template.md.hbs +49 -0
  189. package/templates/channels/web-embed/manifest.template.json.hbs +8 -0
  190. package/templates/python/docker-compose.hbs +56 -0
  191. package/templates/typescript/docker-compose.hbs +52 -0
@@ -7,7 +7,6 @@
7
7
  */
8
8
  /* eslint-disable max-lines,max-lines-per-function,max-statements,complexity,max-depth -- Integration test flow with system-level and per-datasource paths */
9
9
 
10
- const fs = require('fs').promises;
11
10
  const fsSync = require('fs');
12
11
  const path = require('path');
13
12
  const chalk = require('chalk');
@@ -69,15 +68,15 @@ async function loadVariablesYamlFile(variablesPath) {
69
68
  }
70
69
 
71
70
  /**
72
- * Loads a single JSON file
71
+ * Loads a single integration config file (JSON, YAML, or YML).
73
72
  * @async
74
- * @function loadJsonFile
75
- * @param {string} filePath - Path to JSON file
73
+ * @function loadIntegrationConfigFile
74
+ * @param {string} filePath - Path to config file
76
75
  * @param {string} fileName - File name for error messages
77
- * @returns {Promise<Object>} Parsed JSON data
78
- * @throws {Error} If file not found or invalid JSON
76
+ * @returns {Promise<Object>} Parsed config object
77
+ * @throws {Error} If file not found or invalid format
79
78
  */
80
- async function loadJsonFile(filePath, fileName) {
79
+ async function loadIntegrationConfigFile(filePath, fileName) {
81
80
  if (!fsSync.existsSync(filePath)) {
82
81
  // Use "System file not found" for system files to match test expectations
83
82
  if (fileName.includes('system') || fileName.includes('deploy')) {
@@ -86,12 +85,8 @@ async function loadJsonFile(filePath, fileName) {
86
85
  throw new Error(`${fileName} not found: ${filePath}`);
87
86
  }
88
87
 
89
- const content = await fs.readFile(filePath, 'utf8');
90
- try {
91
- return JSON.parse(content);
92
- } catch (error) {
93
- throw new Error(`Invalid JSON syntax in ${fileName}: ${error.message}`);
94
- }
88
+ const { loadConfigFile } = require('../utils/config-format');
89
+ return loadConfigFile(filePath);
95
90
  }
96
91
 
97
92
  /**
@@ -121,7 +116,7 @@ async function loadSystemFiles(systemFiles, schemaBasePath, appPath) {
121
116
  const systemJsonFiles = [];
122
117
  for (const systemFile of systemFiles) {
123
118
  const systemPath = resolveFilePath(schemaBasePath, appPath, systemFile);
124
- const systemJson = await loadJsonFile(systemPath, systemFile);
119
+ const systemJson = await loadIntegrationConfigFile(systemPath, systemFile);
125
120
  systemJsonFiles.push({ path: systemPath, data: systemJson });
126
121
  }
127
122
  return systemJsonFiles;
@@ -140,7 +135,7 @@ async function loadDatasourceFiles(datasourceFiles, schemaBasePath, appPath) {
140
135
  const datasourceJsonFiles = [];
141
136
  for (const datasourceFile of datasourceFiles) {
142
137
  const datasourcePath = resolveFilePath(schemaBasePath, appPath, datasourceFile);
143
- const datasourceJson = await loadJsonFile(datasourcePath, datasourceFile);
138
+ const datasourceJson = await loadIntegrationConfigFile(datasourcePath, datasourceFile);
144
139
  datasourceJsonFiles.push({ path: datasourcePath, data: datasourceJson });
145
140
  }
146
141
  return datasourceJsonFiles;
@@ -267,6 +267,15 @@ function validateBuildFields(build) {
267
267
  if (build.dockerfile && build.dockerfile.trim()) {
268
268
  buildConfig.dockerfile = build.dockerfile;
269
269
  }
270
+ if (build.worker === true) {
271
+ buildConfig.worker = true;
272
+ }
273
+ if (typeof build.imageRunWorker === 'string' && build.imageRunWorker.trim()) {
274
+ buildConfig.imageRunWorker = build.imageRunWorker.trim();
275
+ }
276
+ if (typeof build.imageRunBeat === 'string' && build.imageRunBeat.trim()) {
277
+ buildConfig.imageRunBeat = build.imageRunBeat.trim();
278
+ }
270
279
 
271
280
  return Object.keys(buildConfig).length > 0 ? buildConfig : null;
272
281
  }
@@ -390,6 +399,7 @@ module.exports = {
390
399
  buildOptionalFields,
391
400
  buildManifestStructure,
392
401
  filterConfigurationByRegistryMode,
393
- sanitizeAuthType
402
+ sanitizeAuthType,
403
+ validateBuildFields
394
404
  };
395
405
 
@@ -27,11 +27,14 @@ const { loadVariables, loadRbac } = require('./helpers');
27
27
  * @param {Object} systemJson - System JSON object (mutated in place)
28
28
  */
29
29
  function normalizeLegacyAuthMethod(systemJson) {
30
+ const { getAuthCredentialTemplate } = require('../utils/authentication-shape');
30
31
  const auth = systemJson && systemJson.authentication;
31
32
  if (!auth || typeof auth !== 'object') return;
32
- const method = String(auth.method || '').trim().toLowerCase();
33
+ const credential = getAuthCredentialTemplate(auth);
34
+ if (!credential) return;
35
+ const method = String(credential.method || '').trim().toLowerCase();
33
36
  if (method === 'bearerkey') {
34
- auth.method = 'bearerToken';
37
+ credential.method = 'bearerToken';
35
38
  }
36
39
  }
37
40
 
@@ -453,6 +453,8 @@ module.exports = {
453
453
  extractVariablesYaml,
454
454
  extractRbacYaml,
455
455
  parseImageReference,
456
- generateReadmeFromDeployJson
456
+ generateReadmeFromDeployJson,
457
+ mergeEnvTemplateWithExisting,
458
+ buildEnvTemplateExpectedByKey
457
459
  };
458
460
 
@@ -32,34 +32,7 @@ function toKeySegment(str) {
32
32
  return sanitized || 'default';
33
33
  }
34
34
 
35
- /**
36
- * Align authentication.security kv:// namespaces and credentialKey with the final system key.
37
- * Dataplane normalizes this before respond; when appName overrides a spec-derived key (e.g.
38
- * OpenAPI title "Companies"), the builder still must rewrite nested auth so it matches env.template.
39
- *
40
- * @param {Object|null|undefined} authentication - authentication block from dataplane
41
- * @param {string} systemKey - Final external system key (integration app name)
42
- * @param {string} [authDisplayName] - Credential display name (typically title-cased app name)
43
- */
44
- function normalizeAuthenticationToSystemKey(authentication, systemKey, authDisplayName) {
45
- if (!authentication || typeof authentication !== 'object') return;
46
- authentication.credentialKey = `${systemKey}-cred`;
47
- const security = authentication.security;
48
- if (security && typeof security === 'object') {
49
- for (const k of Object.keys(security)) {
50
- const v = security[k];
51
- if (typeof v === 'string' && v.startsWith('kv://')) {
52
- const rest = v.slice(5);
53
- const idx = rest.indexOf('/');
54
- const suffix = idx >= 0 ? rest.slice(idx + 1) : '';
55
- security[k] = suffix ? `kv://${systemKey}/${suffix}` : `kv://${systemKey}`;
56
- }
57
- }
58
- }
59
- if (authDisplayName) {
60
- authentication.displayName = authDisplayName;
61
- }
62
- }
35
+ const { normalizeAuthenticationToSystemKey } = require('../utils/authentication-shape');
63
36
 
64
37
  /**
65
38
  * Generate files from dataplane-generated wizard configurations
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @fileoverview Parse ForeignKeyReference objects from controller identity APIs (plan 194)
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * @param {string|Object|null|undefined} ref
11
+ * @returns {string}
12
+ */
13
+ function readFkId(ref) {
14
+ if (typeof ref === 'string') {
15
+ return ref;
16
+ }
17
+ return String(ref?.id || '').trim();
18
+ }
19
+
20
+ /**
21
+ * @param {string|Object|null|undefined} ref
22
+ * @returns {string}
23
+ */
24
+ function readFkKey(ref) {
25
+ if (typeof ref === 'string') {
26
+ return ref;
27
+ }
28
+ return String(ref?.key || '').trim();
29
+ }
30
+
31
+ /**
32
+ * @param {string|Object|null|undefined} ref
33
+ * @returns {string}
34
+ */
35
+ function readFkName(ref) {
36
+ if (typeof ref === 'string') {
37
+ return '';
38
+ }
39
+ return String(ref?.name || '').trim();
40
+ }
41
+
42
+ /**
43
+ * @param {string|Object|null|undefined} ref
44
+ * @param {string} [fallback]
45
+ * @returns {string}
46
+ */
47
+ function formatFkLabel(ref, fallback = '—') {
48
+ const name = readFkName(ref);
49
+ if (name) {
50
+ return name;
51
+ }
52
+ const key = readFkKey(ref);
53
+ if (key) {
54
+ return key;
55
+ }
56
+ const id = readFkId(ref);
57
+ return id || fallback;
58
+ }
59
+
60
+ module.exports = {
61
+ readFkId,
62
+ readFkKey,
63
+ readFkName,
64
+ formatFkLabel
65
+ };
@@ -6,46 +6,13 @@
6
6
 
7
7
  'use strict';
8
8
 
9
- const { getUser, listUsers } = require('../api/users.api');
10
-
11
- /**
12
- * @param {Object} response
13
- * @returns {any}
14
- */
15
- function unwrapControllerData(response) {
16
- if (!response || response.success === false) {
17
- return null;
18
- }
19
- return response?.data?.data ?? response?.data ?? response;
20
- }
21
-
22
- /**
23
- * @param {Object} response
24
- * @returns {string}
25
- */
26
- function apiErrorMessage(response) {
27
- if (!response) {
28
- return 'Request failed';
29
- }
30
- const err = response.error;
31
- if (typeof err === 'string') {
32
- return err;
33
- }
34
- if (err && typeof err === 'object') {
35
- return err.formattedError || err.message || err.error || 'Request failed';
36
- }
37
- return response.formattedError || response.message || 'Request failed';
38
- }
39
-
40
- /**
41
- * @param {Object} response
42
- * @throws {Error}
43
- */
44
- function throwIfApiFailed(response) {
45
- if (!response || response.success === false) {
46
- throw new Error(apiErrorMessage(response));
47
- }
48
- }
9
+ const { lookupUserByIdOrEmail } = require('./resolve-user-id');
10
+ const { buildSyncStatsFailureMessage } = require('./identity-sync-errors');
11
+ const {
12
+ unwrapControllerData,
13
+ apiErrorMessage,
14
+ throwIfApiFailed
15
+ } = require('./identity-response-helpers');
49
16
 
50
17
  /**
51
18
  * @param {Object|null|undefined} stats
@@ -55,11 +22,7 @@ function throwIfApiFailed(response) {
55
22
  function assertSyncStatsOk(stats, opts = {}) {
56
23
  const syncErrors = Number(stats?.errors ?? 0);
57
24
  if (syncErrors > 0) {
58
- throw new Error(
59
- `Dataplane full sync reported ${syncErrors} error(s). ` +
60
- `usersProcessed=${stats?.usersProcessed ?? 0} groupsProcessed=${stats?.groupsProcessed ?? 0}. ` +
61
- 'Check miso-controller logs for the sync request.'
62
- );
25
+ throw new Error(buildSyncStatsFailureMessage(stats));
63
26
  }
64
27
  const processed = Number(stats?.usersProcessed ?? 0);
65
28
  if (processed === 0 && !opts.allowEmptySync) {
@@ -79,24 +42,16 @@ function assertSyncStatsOk(stats, opts = {}) {
79
42
  async function findExistingUser(controllerUrl, authConfig, userSpec) {
80
43
  const csvId = String(userSpec.csvId || '').trim();
81
44
  if (csvId) {
82
- const byId = await getUser(controllerUrl, authConfig, csvId);
83
- const row = unwrapControllerData(byId);
84
- if (row && row.id) {
85
- return row;
45
+ const byId = await lookupUserByIdOrEmail(controllerUrl, authConfig, csvId);
46
+ if (byId?.id) {
47
+ return byId;
86
48
  }
87
49
  }
88
50
  const email = String(userSpec.email || '').trim();
89
- const listed = await listUsers(controllerUrl, authConfig, {
90
- search: email,
91
- pageSize: 10,
92
- page: 1
93
- });
94
- const items = unwrapControllerData(listed);
95
- const list = Array.isArray(items) ? items : items?.data;
96
- if (!Array.isArray(list)) {
51
+ if (!email) {
97
52
  return null;
98
53
  }
99
- return list.find((u) => String(u.email || '').toLowerCase() === email.toLowerCase()) || null;
54
+ return lookupUserByIdOrEmail(controllerUrl, authConfig, email);
100
55
  }
101
56
 
102
57
  module.exports = {
@@ -18,6 +18,7 @@ const {
18
18
  apiErrorMessage,
19
19
  findExistingUser
20
20
  } = require('./identity-apply-core');
21
+ const { resolveIdentityUsername } = require('./username-default');
21
22
 
22
23
  /**
23
24
  * @param {string} msg
@@ -75,7 +76,10 @@ async function ensureUser(controllerUrl, authConfig, userSpec, dryRun) {
75
76
  firstName: userSpec.firstName,
76
77
  lastName: userSpec.lastName,
77
78
  displayName: userSpec.displayName,
78
- username: userSpec.username,
79
+ username: resolveIdentityUsername({
80
+ username: userSpec.username,
81
+ email: userSpec.email
82
+ }),
79
83
  status: 'active'
80
84
  };
81
85
  Object.keys(body).forEach((k) => {
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @fileoverview TTY layout helpers for aifabrix identity commands (plan 151)
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('../utils/logger');
11
+ const {
12
+ formatBlockingError,
13
+ formatNextActions,
14
+ headerKeyValue,
15
+ sectionTitle
16
+ } = require('../utils/cli-layout-chalk');
17
+ const { readFkId, readFkKey, readFkName, formatFkLabel } = require('./fk-reference');
18
+ const { unwrapControllerData } = require('./identity-response-helpers');
19
+
20
+ /**
21
+ * @param {Object|null|undefined} response
22
+ * @returns {{ items: Object[], meta: Object|null }}
23
+ */
24
+ function extractPaginatedList(response) {
25
+ if (!response || response.success === false) {
26
+ return { items: [], meta: null };
27
+ }
28
+ const body = response.data;
29
+ if (body && Array.isArray(body.data)) {
30
+ return { items: body.data, meta: body.meta ?? null };
31
+ }
32
+ const unwrapped = unwrapControllerData(response);
33
+ if (Array.isArray(unwrapped)) {
34
+ return { items: unwrapped, meta: null };
35
+ }
36
+ if (unwrapped && Array.isArray(unwrapped.data)) {
37
+ return { items: unwrapped.data, meta: unwrapped.meta ?? null };
38
+ }
39
+ return { items: [], meta: null };
40
+ }
41
+
42
+ /**
43
+ * @param {Object} user
44
+ * @returns {string}
45
+ */
46
+ function formatUserDisplayName(user) {
47
+ const display = String(user?.displayName || '').trim();
48
+ if (display) {
49
+ return display;
50
+ }
51
+ const parts = [user?.firstName, user?.lastName].filter(Boolean).map(String);
52
+ const joined = parts.join(' ').trim();
53
+ return joined || '—';
54
+ }
55
+
56
+ /**
57
+ * @param {string} value
58
+ * @param {number} width
59
+ * @returns {string}
60
+ */
61
+ function padCell(value, width) {
62
+ const text = String(value ?? '—');
63
+ if (text.length <= width) {
64
+ return text.padEnd(width);
65
+ }
66
+ if (width <= 1) {
67
+ return text.slice(0, width);
68
+ }
69
+ return `${text.slice(0, width - 1)}…`;
70
+ }
71
+
72
+ /**
73
+ * @param {{ emoji?: string, title: string, context?: string, shown?: number, total?: number|null }} opts
74
+ */
75
+ function printIdentityListHeader(opts) {
76
+ const emoji = opts.emoji ? `${opts.emoji} ` : '';
77
+ const context = opts.context ? ` (${opts.context})` : '';
78
+ const shown = Number.isFinite(opts.shown) ? opts.shown : 0;
79
+ const total = Number.isFinite(opts.total) ? opts.total : null;
80
+ const count =
81
+ total !== null && total >= shown ? `${shown} shown of ${total}` : `${shown} shown`;
82
+ logger.log(chalk.bold(`\n${emoji}${opts.title}${context} — ${count}:\n`));
83
+ }
84
+
85
+ /**
86
+ * @param {Array<{ label: string, width: number, value: (row: Object) => string }>} columns
87
+ * @param {Object[]} rows
88
+ */
89
+ function printIdentityTable(columns, rows) {
90
+ const header = columns.map((col) => padCell(col.label, col.width)).join('');
91
+ logger.log(chalk.gray(header));
92
+ logger.log(chalk.gray('-'.repeat(columns.reduce((sum, col) => sum + col.width, 0))));
93
+ rows.forEach((row) => {
94
+ const line = columns.map((col) => padCell(col.value(row), col.width)).join('');
95
+ logger.log(line);
96
+ });
97
+ logger.log('');
98
+ }
99
+
100
+ /**
101
+ * @param {Object|null|undefined} meta
102
+ */
103
+ function printListPaginationFooter(meta) {
104
+ if (!meta || !Number.isFinite(meta.totalItems)) {
105
+ return;
106
+ }
107
+ const page = meta.currentPage ?? meta.page ?? 1;
108
+ const pageSize = meta.pageSize ?? meta.page_size;
109
+ const totalPages = meta.totalPages ?? meta.total_pages;
110
+ const parts = [`total ${meta.totalItems}`];
111
+ if (pageSize) {
112
+ parts.unshift(`page ${page}`);
113
+ }
114
+ if (totalPages) {
115
+ parts.push(`${totalPages} pages`);
116
+ }
117
+ logger.log(chalk.gray(` ${parts.join(' · ')}\n`));
118
+ }
119
+
120
+ /**
121
+ * @param {string} title
122
+ * @param {Array<{ label: string, value: string }>} fields
123
+ */
124
+ function printIdentityRecord(title, fields) {
125
+ logger.log(sectionTitle(title));
126
+ fields.forEach(({ label, value }) => {
127
+ logger.log(` ${headerKeyValue(`${label}:`, value || '—')}`);
128
+ });
129
+ logger.log('');
130
+ }
131
+
132
+ /**
133
+ * @param {string} message
134
+ * @param {string[]} [hints]
135
+ */
136
+ function printIdentityEmpty(message, hints = []) {
137
+ logger.log(chalk.gray(` ${message}\n`));
138
+ if (hints.length > 0) {
139
+ logger.log(formatNextActions(hints));
140
+ logger.log('');
141
+ }
142
+ }
143
+
144
+ /**
145
+ * @param {string} entity
146
+ * @param {string} query
147
+ * @param {string[]} [hints]
148
+ */
149
+ function printIdentityNotFound(entity, query, hints = []) {
150
+ logger.error(formatBlockingError(`${entity} not found: ${query}`));
151
+ if (hints.length > 0) {
152
+ logger.log(formatNextActions(hints));
153
+ }
154
+ }
155
+
156
+ const USER_LIST_COLUMNS = [
157
+ { label: 'Id', width: 38, value: (u) => u.id || '—' },
158
+ { label: 'Username', width: 16, value: (u) => u.username || '—' },
159
+ { label: 'Name', width: 22, value: (u) => formatUserDisplayName(u) },
160
+ { label: 'Email', width: 34, value: (u) => u.email || '—' },
161
+ { label: 'Status', width: 10, value: (u) => u.status || '—' }
162
+ ];
163
+
164
+ const GROUP_LIST_COLUMNS = [
165
+ { label: 'Id', width: 38, value: (g) => g.id || '—' },
166
+ { label: 'Name', width: 28, value: (g) => g.name || '—' },
167
+ { label: 'Display name', width: 28, value: (g) => g.displayName || '—' }
168
+ ];
169
+
170
+ /**
171
+ * @param {Object[]} memberships
172
+ */
173
+ function printUserGroupsTable(memberships) {
174
+ printIdentityTable(
175
+ [
176
+ { label: 'Group key', width: 28, value: (m) => readFkKey(m.groupId) || '—' },
177
+ { label: 'Group name', width: 28, value: (m) => formatFkLabel(m.groupId) },
178
+ { label: 'Group id', width: 38, value: (m) => readFkId(m.groupId) || '—' },
179
+ { label: 'Role hint', width: 12, value: (m) => m.roleHint || '—' }
180
+ ],
181
+ memberships
182
+ );
183
+ }
184
+
185
+ /**
186
+ * @param {Object[]} memberships
187
+ */
188
+ function printGroupMembersTable(memberships) {
189
+ printIdentityTable(
190
+ [
191
+ { label: 'User key', width: 22, value: (m) => readFkKey(m.userId) || '—' },
192
+ { label: 'User name', width: 24, value: (m) => formatFkLabel(m.userId) },
193
+ { label: 'User id', width: 38, value: (m) => readFkId(m.userId) || '—' },
194
+ { label: 'Role hint', width: 12, value: (m) => m.roleHint || '—' }
195
+ ],
196
+ memberships
197
+ );
198
+ }
199
+
200
+ /**
201
+ * @param {Object} user
202
+ */
203
+ function printUserRecord(user) {
204
+ printIdentityRecord('User', [
205
+ { label: 'Id', value: user.id },
206
+ { label: 'Email', value: user.email },
207
+ { label: 'Username', value: user.username },
208
+ { label: 'Name', value: formatUserDisplayName(user) },
209
+ { label: 'Display name', value: user.displayName },
210
+ { label: 'Status', value: user.status },
211
+ { label: 'Verified', value: user.emailVerified ? 'yes' : 'no' }
212
+ ]);
213
+ }
214
+
215
+ /**
216
+ * @param {Object} group
217
+ */
218
+ function printGroupRecord(group) {
219
+ printIdentityRecord('Group', [
220
+ { label: 'Id', value: group.id },
221
+ { label: 'Name', value: group.name },
222
+ { label: 'Display name', value: group.displayName },
223
+ { label: 'Description', value: group.description }
224
+ ]);
225
+ }
226
+
227
+ module.exports = {
228
+ extractPaginatedList,
229
+ formatUserDisplayName,
230
+ printIdentityListHeader,
231
+ printIdentityTable,
232
+ printListPaginationFooter,
233
+ printIdentityRecord,
234
+ printIdentityEmpty,
235
+ printIdentityNotFound,
236
+ USER_LIST_COLUMNS,
237
+ GROUP_LIST_COLUMNS,
238
+ printUserGroupsTable,
239
+ printGroupMembersTable,
240
+ printUserRecord,
241
+ printGroupRecord,
242
+ readFkName
243
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @fileoverview Shared controller response helpers for identity modules
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * @param {Object} response
11
+ * @returns {any}
12
+ */
13
+ function unwrapControllerData(response) {
14
+ if (!response || response.success === false) {
15
+ return null;
16
+ }
17
+ return response?.data?.data ?? response?.data ?? response;
18
+ }
19
+
20
+ /**
21
+ * @param {Object} response
22
+ * @returns {string}
23
+ */
24
+ function apiErrorMessage(response) {
25
+ if (!response) {
26
+ return 'Request failed';
27
+ }
28
+ const err = response.error;
29
+ if (typeof err === 'string') {
30
+ return err;
31
+ }
32
+ if (err && typeof err === 'object') {
33
+ return err.formattedError || err.message || err.error || 'Request failed';
34
+ }
35
+ return response.formattedError || response.message || 'Request failed';
36
+ }
37
+
38
+ /**
39
+ * @param {Object} response
40
+ * @throws {Error}
41
+ */
42
+ function throwIfApiFailed(response) {
43
+ if (!response || response.success === false) {
44
+ throw new Error(apiErrorMessage(response));
45
+ }
46
+ }
47
+
48
+ module.exports = {
49
+ unwrapControllerData,
50
+ apiErrorMessage,
51
+ throwIfApiFailed
52
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @fileoverview User-facing identity sync failure messages
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * @param {Object|null|undefined} stats
11
+ * @returns {boolean}
12
+ */
13
+ function isZeroProgressSyncFailure(stats) {
14
+ const usersProcessed = Number(stats?.usersProcessed ?? 0);
15
+ const groupsProcessed = Number(stats?.groupsProcessed ?? 0);
16
+ return usersProcessed === 0 && groupsProcessed === 0;
17
+ }
18
+
19
+ /**
20
+ * Build a multi-line CLI message when controller sync stats report errors.
21
+ *
22
+ * @param {Object|null|undefined} stats
23
+ * @returns {string}
24
+ */
25
+ function buildSyncStatsFailureMessage(stats) {
26
+ const syncErrors = Number(stats?.errors ?? 0);
27
+ const usersProcessed = Number(stats?.usersProcessed ?? 0);
28
+ const groupsProcessed = Number(stats?.groupsProcessed ?? 0);
29
+
30
+ const lines = [
31
+ `Dataplane full sync reported ${syncErrors} error(s).`,
32
+ `usersProcessed=${usersProcessed} groupsProcessed=${groupsProcessed}.`
33
+ ];
34
+
35
+ if (isZeroProgressSyncFailure(stats)) {
36
+ lines.push(
37
+ '',
38
+ 'The controller accepted the sync request, but no identity data reached the dataplane.',
39
+ 'Common causes:',
40
+ ' • Expired or invalid dataplane client token (controller → dataplane auth)',
41
+ ' • Dataplane unreachable or unhealthy',
42
+ ' • Dataplane MISO_CLIENTID / MISO_CLIENTSECRET out of sync with the controller',
43
+ '',
44
+ 'Try:',
45
+ ' aifabrix auth status',
46
+ ' aifabrix run dataplane --reload',
47
+ 'Then retry:',
48
+ ' aifabrix identity sync -e dev --purge-cache',
49
+ '',
50
+ 'If it still fails, check miso-controller logs for the sync request.'
51
+ );
52
+ } else {
53
+ lines.push('Check miso-controller logs for the sync request.');
54
+ }
55
+
56
+ return lines.join('\n');
57
+ }
58
+
59
+ module.exports = {
60
+ isZeroProgressSyncFailure,
61
+ buildSyncStatsFailureMessage
62
+ };