@aifabrix/builder 2.45.0 → 2.45.5

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 (180) hide show
  1. package/.cursor/rules/docs-rules.mdc +3 -1
  2. package/integration/hubspot-test/test-artifacts/wizard-hubspot-credential-real.yaml +1 -1
  3. package/integration/hubspot-test/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
  4. package/integration/hubspot-test/wizard-hubspot-e2e.yaml +1 -1
  5. package/jest.projects.js +44 -1
  6. package/lib/api/agent-metadata-validation.api.js +93 -0
  7. package/lib/api/audit-api-helpers.js +68 -0
  8. package/lib/api/audit.api.js +119 -0
  9. package/lib/api/auth-cache.api.js +44 -0
  10. package/lib/api/certificates.api.js +4 -4
  11. package/lib/api/credential.api.js +2 -2
  12. package/lib/api/dataplane-health.api.js +137 -0
  13. package/lib/api/dataplane-sync.api.js +27 -0
  14. package/lib/api/datasources-core.api.js +21 -18
  15. package/lib/api/datasources-extended.api.js +22 -22
  16. package/lib/api/dimensions.api.js +3 -0
  17. package/lib/api/external-systems.api.js +13 -13
  18. package/lib/api/governance-scenarios.api.js +39 -0
  19. package/lib/api/groups.api.js +79 -0
  20. package/lib/api/index.js +45 -2
  21. package/lib/api/pipeline.api.js +4 -4
  22. package/lib/api/protection.api.js +157 -0
  23. package/lib/api/records-bulk.api.js +30 -0
  24. package/lib/api/records-search-parse.js +49 -0
  25. package/lib/api/records-search.api.js +29 -0
  26. package/lib/api/types/agent-metadata-validation.types.js +67 -0
  27. package/lib/api/types/audit.types.js +72 -0
  28. package/lib/api/types/dataplane-health.types.js +27 -0
  29. package/lib/api/types/dimensions.types.js +2 -0
  30. package/lib/api/types/governance-scenarios.types.js +39 -0
  31. package/lib/api/types/protection.types.js +69 -0
  32. package/lib/api/types/records-bulk.types.js +35 -0
  33. package/lib/api/types/records-search.types.js +28 -0
  34. package/lib/api/user-groups.api.js +50 -0
  35. package/lib/api/users.api.js +95 -0
  36. package/lib/api/validation-run.api.js +3 -3
  37. package/lib/api/validation-runner.js +3 -3
  38. package/lib/api/wizard-platform.api.js +3 -3
  39. package/lib/api/wizard.api.js +21 -21
  40. package/lib/app/deploy.js +34 -19
  41. package/lib/cli/index.js +4 -2
  42. package/lib/cli/infra-guided.js +22 -3
  43. package/lib/cli/setup-app.help.js +50 -2
  44. package/lib/cli/setup-app.js +33 -24
  45. package/lib/cli/setup-app.test-commands.js +80 -21
  46. package/lib/cli/setup-auth.js +30 -3
  47. package/lib/cli/setup-external-system.js +37 -3
  48. package/lib/cli/setup-utility-help-after.js +45 -0
  49. package/lib/cli/setup-utility-resolve.js +2 -2
  50. package/lib/cli/setup-utility.js +99 -81
  51. package/lib/commands/auth-status-dataplane-version.js +231 -0
  52. package/lib/commands/auth-status-display.js +94 -0
  53. package/lib/commands/auth-status.js +70 -6
  54. package/lib/commands/credential-push.js +10 -5
  55. package/lib/commands/datasource-load-export-cli.js +178 -0
  56. package/lib/commands/datasource-test-trust-cli.js +114 -0
  57. package/lib/commands/datasource-unified-test-cli.js +25 -17
  58. package/lib/commands/datasource-unified-test-cli.options.js +18 -3
  59. package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +5 -5
  60. package/lib/commands/datasource-verify-audit-cli.js +96 -0
  61. package/lib/commands/datasource.js +68 -1
  62. package/lib/commands/dimension-value.js +43 -13
  63. package/lib/commands/dimension.js +38 -7
  64. package/lib/commands/identity-apply-sync.js +131 -0
  65. package/lib/commands/identity-group-cli.js +146 -0
  66. package/lib/commands/identity-membership-role-cache.js +175 -0
  67. package/lib/commands/identity-shared.js +94 -0
  68. package/lib/commands/identity-user-cli.js +158 -0
  69. package/lib/commands/identity.js +43 -0
  70. package/lib/commands/login.js +46 -16
  71. package/lib/commands/protection-cli-leaves.js +286 -0
  72. package/lib/commands/protection.js +50 -0
  73. package/lib/commands/repair.js +13 -3
  74. package/lib/commands/test-e2e-external.js +40 -9
  75. package/lib/commands/test-governance-cli-options.js +47 -0
  76. package/lib/commands/test-governance-command-action.js +55 -0
  77. package/lib/commands/test-governance-external.js +88 -0
  78. package/lib/commands/test-trust-cli-options.js +32 -0
  79. package/lib/commands/test-trust-command-action.js +118 -0
  80. package/lib/commands/test-trust-external.js +127 -0
  81. package/lib/commands/upload.js +34 -13
  82. package/lib/commands/wizard-core.js +3 -0
  83. package/lib/core/config-device-dataplane.js +185 -0
  84. package/lib/datasource/agent-trust-debug-log.js +45 -0
  85. package/lib/datasource/agent-trust-fetch.js +115 -0
  86. package/lib/datasource/agent-trust-map.js +101 -0
  87. package/lib/datasource/agent-trust-mock.js +44 -0
  88. package/lib/datasource/agent-trust-run.js +93 -0
  89. package/lib/datasource/agent-trust-timeout.js +25 -0
  90. package/lib/datasource/audit-evidence-display.js +80 -0
  91. package/lib/datasource/audit-evidence-extract-debug.js +103 -0
  92. package/lib/datasource/audit-evidence-extract.js +177 -0
  93. package/lib/datasource/audit-evidence-matrix-rows.js +158 -0
  94. package/lib/datasource/audit-evidence-operation-aliases.js +63 -0
  95. package/lib/datasource/audit-evidence-poll.js +109 -0
  96. package/lib/datasource/audit-evidence-run.js +177 -0
  97. package/lib/datasource/audit-evidence-verify-helpers.js +167 -0
  98. package/lib/datasource/audit-evidence-verify-matrix.js +177 -0
  99. package/lib/datasource/audit-evidence-verify.js +108 -0
  100. package/lib/datasource/bulk-loader-run-batches.js +88 -0
  101. package/lib/datasource/bulk-loader-service-retry.js +52 -0
  102. package/lib/datasource/bulk-loader-service.js +127 -0
  103. package/lib/datasource/capability/run-capability-dimension.js +2 -1
  104. package/lib/datasource/datasource-exporter-resolve.js +82 -0
  105. package/lib/datasource/datasource-exporter-service.js +144 -0
  106. package/lib/datasource/datasource-identifier.js +148 -0
  107. package/lib/datasource/datasource-load-export-context.js +92 -0
  108. package/lib/datasource/integration-context.js +6 -4
  109. package/lib/datasource/local-data-paths.js +145 -0
  110. package/lib/datasource/log-cleaner.js +241 -0
  111. package/lib/datasource/log-viewer.js +111 -6
  112. package/lib/datasource/record-file-parser.js +85 -0
  113. package/lib/datasource/record-mapper.js +129 -0
  114. package/lib/datasource/resolve-app.js +74 -55
  115. package/lib/datasource/test-e2e.js +26 -2
  116. package/lib/datasource/unified-validation-run-resolve.js +45 -12
  117. package/lib/datasource/unified-validation-run.js +43 -17
  118. package/lib/datasource/validate.js +37 -20
  119. package/lib/external-system/test-system-level.js +25 -4
  120. package/lib/external-system/test.js +3 -1
  121. package/lib/governance/governance-pack-loader.js +70 -0
  122. package/lib/governance/governance-run-display.js +119 -0
  123. package/lib/identity/identity-apply-core.js +108 -0
  124. package/lib/identity/identity-apply-phases.js +244 -0
  125. package/lib/identity/identity-apply-service.js +77 -0
  126. package/lib/identity/identity-csv-parser.js +114 -0
  127. package/lib/internal/fs-real-sync.js +29 -1
  128. package/lib/protection/auth-context.js +45 -0
  129. package/lib/protection/convert-batch.js +90 -0
  130. package/lib/protection/load.js +41 -0
  131. package/lib/protection/paths.js +158 -0
  132. package/lib/protection/preflight-datasource-ready.js +46 -0
  133. package/lib/protection/protection-create-probe-lines.js +142 -0
  134. package/lib/protection/protection-create-probes.js +95 -0
  135. package/lib/protection/protection-create-scaffold.js +281 -0
  136. package/lib/protection/protection-display-helpers.js +445 -0
  137. package/lib/protection/protection-display.js +215 -0
  138. package/lib/protection/protection-preset-registry.js +97 -0
  139. package/lib/protection/report-exit.js +62 -0
  140. package/lib/protection/resolve.js +202 -0
  141. package/lib/protection/run-commands-validate.js +130 -0
  142. package/lib/protection/run-commands.js +216 -0
  143. package/lib/protection/run-protection-create-helpers.js +134 -0
  144. package/lib/protection/run-protection-create-probes-flow.js +43 -0
  145. package/lib/protection/run-protection-create.js +191 -0
  146. package/lib/protection/scope.js +22 -0
  147. package/lib/protection/sync-after-upload.js +64 -0
  148. package/lib/protection/upload-batch.js +127 -0
  149. package/lib/protection/validate-batch.js +113 -0
  150. package/lib/protection/validate-local.js +65 -0
  151. package/lib/resolvers/dimension-file.js +29 -4
  152. package/lib/schema/external-datasource.schema.json +47 -13
  153. package/lib/schema/flag-map-validation-run.json +2 -2
  154. package/lib/schema/protection.schema.json +374 -0
  155. package/lib/schema/type/credential-field-registry.json +636 -0
  156. package/lib/schema/wizard-config.schema.json +1 -1
  157. package/lib/utils/agent-trust-run-display.js +231 -0
  158. package/lib/utils/agent-trust-run-exit.js +59 -0
  159. package/lib/utils/agent-trust-wait-ui.js +94 -0
  160. package/lib/utils/cli-exit-codes.js +30 -0
  161. package/lib/utils/cli-sync-options.js +33 -0
  162. package/lib/utils/credential-push-ui.js +90 -0
  163. package/lib/utils/dataplane-cli-version-gate.js +202 -0
  164. package/lib/utils/dataplane-cli-version-help.js +74 -0
  165. package/lib/utils/datasource-export-display.js +137 -0
  166. package/lib/utils/datasource-load-display.js +174 -0
  167. package/lib/utils/datasource-load-export-ui.js +120 -0
  168. package/lib/utils/datasource-test-run-capability-scope.js +3 -3
  169. package/lib/utils/datasource-test-run-display.js +3 -3
  170. package/lib/utils/datasource-test-run-tty-log.js +5 -1
  171. package/lib/utils/help-builder.js +30 -24
  172. package/lib/utils/manifest-source-emit.js +22 -0
  173. package/lib/utils/semver-compare.js +127 -0
  174. package/lib/utils/upload-pipeline-ui.js +91 -0
  175. package/lib/utils/validation-poll-ui.js +14 -7
  176. package/lib/validation/wizard-config-validator.js +58 -19
  177. package/package.json +1 -1
  178. package/templates/applications/dataplane/env.template +13 -1
  179. package/templates/applications/dataplane/rbac.yaml +4 -0
  180. package/templates/external-system/README.md.hbs +56 -8
@@ -16,7 +16,7 @@ const { getIntegrationPath } = require('../utils/paths');
16
16
  const { runUnifiedDatasourceValidation } = require('../datasource/unified-validation-run');
17
17
 
18
18
  async function runOneDatasourceKey(appName, datasourceKey, opts) {
19
- const { environment, runType, debug, timeoutMs } = opts;
19
+ const { environment, runType, debug, timeoutMs, authConfig, dataplaneUrl } = opts;
20
20
  try {
21
21
  const uni = await runUnifiedDatasourceValidation(datasourceKey, {
22
22
  app: appName,
@@ -26,7 +26,9 @@ async function runOneDatasourceKey(appName, datasourceKey, opts) {
26
26
  verbose: false,
27
27
  timeout: timeoutMs,
28
28
  async: true,
29
- noAsync: false
29
+ noAsync: false,
30
+ authConfig,
31
+ dataplaneUrl
30
32
  });
31
33
 
32
34
  if (uni.apiError) {
@@ -89,7 +91,17 @@ async function maybeWriteSystemRunLog(appName, systemKey, datasourceKeys, runTyp
89
91
  * @param {number} [params.timeout] - Request timeout
90
92
  * @returns {Promise<{success: boolean, datasourceResults: Object[]}>}
91
93
  */
92
- async function runSystemLevelTest({ appName, systemKey, datasourceKeys, environment, runType, debug, timeout }) {
94
+ async function runSystemLevelTest({
95
+ appName,
96
+ systemKey,
97
+ datasourceKeys,
98
+ environment,
99
+ runType,
100
+ debug,
101
+ timeout,
102
+ authConfig,
103
+ dataplaneUrl
104
+ }) {
93
105
  if (!Array.isArray(datasourceKeys) || datasourceKeys.length === 0) {
94
106
  return { success: true, datasourceResults: [] };
95
107
  }
@@ -100,7 +112,16 @@ async function runSystemLevelTest({ appName, systemKey, datasourceKeys, environm
100
112
  // Keep sequential to avoid spiking external APIs / dataplane concurrency in CLI runs.
101
113
  // (Plan allows parallel later, but UX needs stable logs first.)
102
114
  // eslint-disable-next-line no-await-in-loop -- intentional sequential execution
103
- results.push(await runOneDatasourceKey(appName, key, { environment, runType, debug, timeoutMs }));
115
+ results.push(
116
+ await runOneDatasourceKey(appName, key, {
117
+ environment,
118
+ runType,
119
+ debug,
120
+ timeoutMs,
121
+ authConfig,
122
+ dataplaneUrl
123
+ })
124
+ );
104
125
  }
105
126
 
106
127
  const success = results.every(r => r.success);
@@ -486,7 +486,9 @@ async function testExternalSystemIntegration(appName, options = {}) {
486
486
  environment: options.environment,
487
487
  runType: 'integration',
488
488
  debug: options.debug,
489
- timeout
489
+ timeout,
490
+ authConfig,
491
+ dataplaneUrl
490
492
  });
491
493
  results.success = systemResult.success;
492
494
  results.datasourceResults = systemResult.datasourceResults;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @fileoverview Load governance scenario pack YAML from integration folder
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const yaml = require('js-yaml');
12
+ const { getIntegrationPath } = require('../utils/paths');
13
+
14
+ /**
15
+ * Resolve pack file path for test-governance.
16
+ * @param {string} systemKey
17
+ * @param {Object} options
18
+ * @param {string} [options.pack] - Explicit path (absolute or relative to integration root)
19
+ * @param {string} [options.app] - Integration folder override
20
+ * @returns {string}
21
+ */
22
+ function resolveGovernancePackPath(systemKey, options = {}) {
23
+ const integrationPath = options.app
24
+ ? path.isAbsolute(options.app)
25
+ ? options.app
26
+ : path.resolve(process.cwd(), options.app)
27
+ : getIntegrationPath(systemKey);
28
+
29
+ if (options.pack) {
30
+ const packPath = path.isAbsolute(options.pack)
31
+ ? options.pack
32
+ : path.join(integrationPath, options.pack);
33
+ if (!fs.existsSync(packPath)) {
34
+ throw new Error(`Scenario pack not found: ${packPath}`);
35
+ }
36
+ return packPath;
37
+ }
38
+
39
+ const candidates = [
40
+ path.join(integrationPath, 'scenarios', 'default.yaml'),
41
+ path.join(integrationPath, 'scenarios', `${systemKey}-v1.yaml`)
42
+ ];
43
+ for (const candidate of candidates) {
44
+ if (fs.existsSync(candidate)) {
45
+ return candidate;
46
+ }
47
+ }
48
+ throw new Error(
49
+ `No scenario pack found under ${path.join(integrationPath, 'scenarios')}. ` +
50
+ 'Use --pack <path> (e.g. scenarios/test-protection-v1.yaml).'
51
+ );
52
+ }
53
+
54
+ /**
55
+ * @param {string} packPath
56
+ * @returns {Object}
57
+ */
58
+ function loadGovernancePackYaml(packPath) {
59
+ const raw = fs.readFileSync(packPath, 'utf8');
60
+ const doc = yaml.load(raw);
61
+ if (!doc || typeof doc !== 'object') {
62
+ throw new Error(`Invalid scenario pack YAML: ${packPath}`);
63
+ }
64
+ return doc;
65
+ }
66
+
67
+ module.exports = {
68
+ resolveGovernancePackPath,
69
+ loadGovernancePackYaml
70
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @fileoverview TTY display for governance scenario acceptance runs
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
+ sectionTitle,
13
+ headerKeyValue,
14
+ metadata: metaGray,
15
+ formatSuccessParagraph,
16
+ formatBlockingError
17
+ } = require('../utils/cli-test-layout-chalk');
18
+
19
+ const SEP = '────────────────────────────────';
20
+
21
+ /**
22
+ * @param {'pass'|'fail'} status
23
+ * @returns {string}
24
+ */
25
+ function scenarioGlyph(status) {
26
+ return status === 'pass' ? chalk.green('✔') : chalk.red('✖');
27
+ }
28
+
29
+ /**
30
+ * @param {import('../api/types/governance-scenarios.types').GovernanceScenarioResult} scenario
31
+ * @param {boolean} verbose
32
+ */
33
+ function logScenarioVerdict(scenario, verbose) {
34
+ logger.log(` ${scenarioGlyph(scenario.status)} ${scenario.verdict}`);
35
+ if (!verbose) return;
36
+ logger.log(
37
+ metaGray(
38
+ ` Subject: ${scenario.subjectDisplayName} (${scenario.subjectUserId})`
39
+ )
40
+ );
41
+ if (scenario.subjectGroups && scenario.subjectGroups.length > 0) {
42
+ logger.log(metaGray(` Groups: ${scenario.subjectGroups.join(', ')}`));
43
+ }
44
+ logger.log(
45
+ metaGray(
46
+ ` Visible keys: ${scenario.visibleKeyCount} · excluded ABAC: ${scenario.excludedAbac}` +
47
+ (scenario.auditRef ? ` · audit: ${scenario.auditRef}` : '')
48
+ )
49
+ );
50
+ if (scenario.unexpectedVisibleKeys && scenario.unexpectedVisibleKeys.length > 0) {
51
+ logger.log(
52
+ metaGray(` Unexpected: ${scenario.unexpectedVisibleKeys.join(', ')}`)
53
+ );
54
+ }
55
+ if (scenario.missingRequiredKeys && scenario.missingRequiredKeys.length > 0) {
56
+ logger.log(metaGray(` Missing: ${scenario.missingRequiredKeys.join(', ')}`));
57
+ }
58
+ if (scenario.fixHint) {
59
+ logger.log(metaGray(` Fix: ${scenario.fixHint}`));
60
+ }
61
+ }
62
+
63
+ /**
64
+ * @param {string} systemKey
65
+ * @param {import('../api/types/governance-scenarios.types').GovernanceScenariosRunResponse} result
66
+ * @param {{ verbose?: boolean, packPath?: string }} opts
67
+ */
68
+ function displayGovernanceRunTTY(systemKey, result, opts = {}) {
69
+ const agg = result.summary.failed > 0 ? 'fail' : 'ok';
70
+ logger.log(SEP);
71
+ logger.log(sectionTitle('Governed search acceptance (key sets only — no record payload)'));
72
+ logger.log(headerKeyValue('System:', systemKey));
73
+ logger.log(headerKeyValue('Pack:', result.packKey));
74
+ if (opts.packPath) {
75
+ logger.log(metaGray(` File: ${opts.packPath}`));
76
+ }
77
+ logger.log(
78
+ headerKeyValue(
79
+ 'Summary:',
80
+ `${result.summary.passed}/${result.summary.total} passed`
81
+ )
82
+ );
83
+ logger.log(metaGray(' Does not call vendor APIs (use test-e2e for connectivity).'));
84
+ logger.log('');
85
+ for (const scenario of result.scenarios || []) {
86
+ logScenarioVerdict(scenario, opts.verbose === true);
87
+ }
88
+ logger.log('');
89
+ if (agg === 'ok') {
90
+ logger.log(formatSuccessParagraph('All governance scenarios passed'));
91
+ } else {
92
+ logger.log(formatBlockingError(`${result.summary.failed} scenario(s) failed`));
93
+ }
94
+ logger.log(SEP);
95
+ }
96
+
97
+ /**
98
+ * @param {import('../api/types/governance-scenarios.types').GovernanceScenariosRunResponse} result
99
+ */
100
+ function printGovernanceRunJson(result) {
101
+ logger.log(JSON.stringify(result, null, 2));
102
+ }
103
+
104
+ /**
105
+ * @param {{ total: number, passed: number, failed: number }} summary
106
+ * @returns {number}
107
+ */
108
+ function exitCodeFromGovernanceSummary(summary) {
109
+ if (!summary || summary.failed > 0) {
110
+ return 1;
111
+ }
112
+ return 0;
113
+ }
114
+
115
+ module.exports = {
116
+ displayGovernanceRunTTY,
117
+ printGovernanceRunJson,
118
+ exitCodeFromGovernanceSummary
119
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @fileoverview Shared helpers for identity apply
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
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
+ }
49
+
50
+ /**
51
+ * @param {Object|null|undefined} stats
52
+ * @param {{ allowEmptySync?: boolean }} [opts]
53
+ * @throws {Error}
54
+ */
55
+ function assertSyncStatsOk(stats, opts = {}) {
56
+ const syncErrors = Number(stats?.errors ?? 0);
57
+ 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
+ );
63
+ }
64
+ const processed = Number(stats?.usersProcessed ?? 0);
65
+ if (processed === 0 && !opts.allowEmptySync) {
66
+ throw new Error(
67
+ 'Dataplane full sync completed with usersProcessed=0. Use --allow-empty-sync to accept, or fix identity on the controller.'
68
+ );
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @async
74
+ * @param {string} controllerUrl
75
+ * @param {Object} authConfig
76
+ * @param {{ csvId?: string, email: string }} userSpec
77
+ * @returns {Promise<Object|null>}
78
+ */
79
+ async function findExistingUser(controllerUrl, authConfig, userSpec) {
80
+ const csvId = String(userSpec.csvId || '').trim();
81
+ if (csvId) {
82
+ const byId = await getUser(controllerUrl, authConfig, csvId);
83
+ const row = unwrapControllerData(byId);
84
+ if (row && row.id) {
85
+ return row;
86
+ }
87
+ }
88
+ 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)) {
97
+ return null;
98
+ }
99
+ return list.find((u) => String(u.email || '').toLowerCase() === email.toLowerCase()) || null;
100
+ }
101
+
102
+ module.exports = {
103
+ unwrapControllerData,
104
+ apiErrorMessage,
105
+ throwIfApiFailed,
106
+ assertSyncStatsOk,
107
+ findExistingUser
108
+ };
@@ -0,0 +1,244 @@
1
+ /**
2
+ * @fileoverview Phased execution for identity apply
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { getGroup, createGroup } = require('../api/groups.api');
10
+ const { createUser } = require('../api/users.api');
11
+ const { addUserToGroup } = require('../api/user-groups.api');
12
+ const { clearAuthCache } = require('../api/auth-cache.api');
13
+ const { fullSyncToDataplane } = require('../api/dataplane-sync.api');
14
+ const {
15
+ unwrapControllerData,
16
+ throwIfApiFailed,
17
+ assertSyncStatsOk,
18
+ apiErrorMessage,
19
+ findExistingUser
20
+ } = require('./identity-apply-core');
21
+
22
+ /**
23
+ * @param {string} msg
24
+ * @returns {boolean}
25
+ */
26
+ function isAlreadyMemberError(msg) {
27
+ return /already a member|409|Conflict/i.test(String(msg || ''));
28
+ }
29
+
30
+ /**
31
+ * @async
32
+ * @param {string} controllerUrl
33
+ * @param {Object} authConfig
34
+ * @param {{ name: string, displayName: string }} spec
35
+ * @param {boolean} dryRun
36
+ * @returns {Promise<{ id: string, created: boolean }>}
37
+ */
38
+ async function ensureGroup(controllerUrl, authConfig, spec, dryRun) {
39
+ const name = spec.name;
40
+ const existingRes = await getGroup(controllerUrl, authConfig, name);
41
+ const existing = unwrapControllerData(existingRes);
42
+ if (existing && existing.id) {
43
+ return { id: existing.id, created: false };
44
+ }
45
+ if (dryRun) {
46
+ return { id: `(dry-run:${name})`, created: true };
47
+ }
48
+ const createdRes = await createGroup(controllerUrl, authConfig, {
49
+ name,
50
+ displayName: spec.displayName || name
51
+ });
52
+ throwIfApiFailed(createdRes);
53
+ const created = unwrapControllerData(createdRes);
54
+ return { id: created.id, created: true };
55
+ }
56
+
57
+ /**
58
+ * @async
59
+ * @param {string} controllerUrl
60
+ * @param {Object} authConfig
61
+ * @param {Object} userSpec
62
+ * @param {boolean} dryRun
63
+ * @returns {Promise<{ id: string, created: boolean }>}
64
+ */
65
+ async function ensureUser(controllerUrl, authConfig, userSpec, dryRun) {
66
+ const existing = await findExistingUser(controllerUrl, authConfig, userSpec);
67
+ if (existing && existing.id) {
68
+ return { id: existing.id, created: false };
69
+ }
70
+ if (dryRun) {
71
+ return { id: `(dry-run:${userSpec.email})`, created: true };
72
+ }
73
+ const body = {
74
+ email: userSpec.email,
75
+ firstName: userSpec.firstName,
76
+ lastName: userSpec.lastName,
77
+ displayName: userSpec.displayName,
78
+ username: userSpec.username,
79
+ status: 'active'
80
+ };
81
+ Object.keys(body).forEach((k) => {
82
+ if (body[k] === undefined || body[k] === '') {
83
+ delete body[k];
84
+ }
85
+ });
86
+ const createdRes = await createUser(controllerUrl, authConfig, body);
87
+ throwIfApiFailed(createdRes);
88
+ const created = unwrapControllerData(createdRes);
89
+ return { id: created.id, created: true };
90
+ }
91
+
92
+ /**
93
+ * @async
94
+ * @param {string} controllerUrl
95
+ * @param {Object} authConfig
96
+ * @param {Map} groups
97
+ * @param {boolean} dryRun
98
+ * @returns {Promise<{ map: Map<string, string>, created: number, processed: number }>}
99
+ */
100
+ async function applyGroupsPhase(controllerUrl, authConfig, groups, dryRun) {
101
+ const map = new Map();
102
+ let created = 0;
103
+ let processed = 0;
104
+ for (const spec of groups.values()) {
105
+ const out = await ensureGroup(controllerUrl, authConfig, spec, dryRun);
106
+ map.set(spec.name, out.id);
107
+ processed += 1;
108
+ if (out.created) {
109
+ created += 1;
110
+ }
111
+ }
112
+ return { map, created, processed };
113
+ }
114
+
115
+ /**
116
+ * @async
117
+ * @param {string} controllerUrl
118
+ * @param {Object} authConfig
119
+ * @param {Map} users
120
+ * @param {boolean} dryRun
121
+ * @returns {Promise<{ map: Map<string, string>, created: number, processed: number }>}
122
+ */
123
+ async function applyUsersPhase(controllerUrl, authConfig, users, dryRun) {
124
+ const map = new Map();
125
+ let created = 0;
126
+ let processed = 0;
127
+ for (const [userKey, userSpec] of users.entries()) {
128
+ const out = await ensureUser(controllerUrl, authConfig, userSpec, dryRun);
129
+ map.set(userKey, out.id);
130
+ processed += 1;
131
+ if (out.created) {
132
+ created += 1;
133
+ }
134
+ }
135
+ return { map, created, processed };
136
+ }
137
+
138
+ /**
139
+ * @async
140
+ * @param {string} controllerUrl
141
+ * @param {Object} authConfig
142
+ * @param {Object[]} memberships
143
+ * @param {Map<string, string>} userIdByKey
144
+ * @param {Map<string, string>} groupIdByName
145
+ * @param {boolean} dryRun
146
+ * @returns {Promise<{ created: number, processed: number, skipped: number }>}
147
+ */
148
+ /**
149
+ * @async
150
+ * @param {string} controllerUrl
151
+ * @param {Object} authConfig
152
+ * @param {Object} m
153
+ * @param {Map<string, string>} userIdByKey
154
+ * @param {Map<string, string>} groupIdByName
155
+ * @param {boolean} dryRun
156
+ * @returns {Promise<'created'|'skipped'|'dry'>}
157
+ */
158
+ async function applyOneMembership(controllerUrl, authConfig, m, userIdByKey, groupIdByName, dryRun) {
159
+ const userId = userIdByKey.get(m.userKey);
160
+ const groupId = groupIdByName.get(m.groupName);
161
+ if (!userId || !groupId) {
162
+ return 'skipped';
163
+ }
164
+ if (dryRun) {
165
+ return 'dry';
166
+ }
167
+ const res = await addUserToGroup(controllerUrl, authConfig, userId, groupId, {});
168
+ if (res && res.success !== false) {
169
+ return 'created';
170
+ }
171
+ if (isAlreadyMemberError(apiErrorMessage(res))) {
172
+ return 'skipped';
173
+ }
174
+ throw new Error(apiErrorMessage(res));
175
+ }
176
+
177
+ async function applyMembershipsPhase(
178
+ controllerUrl,
179
+ authConfig,
180
+ memberships,
181
+ userIdByKey,
182
+ groupIdByName,
183
+ dryRun
184
+ ) {
185
+ let created = 0;
186
+ let processed = 0;
187
+ let skipped = 0;
188
+ for (const m of memberships) {
189
+ const userId = userIdByKey.get(m.userKey);
190
+ const groupId = groupIdByName.get(m.groupName);
191
+ if (!userId || !groupId) {
192
+ continue;
193
+ }
194
+ processed += 1;
195
+ const outcome = await applyOneMembership(
196
+ controllerUrl,
197
+ authConfig,
198
+ m,
199
+ userIdByKey,
200
+ groupIdByName,
201
+ dryRun
202
+ );
203
+ if (outcome === 'created' || outcome === 'dry') {
204
+ created += 1;
205
+ } else if (outcome === 'skipped') {
206
+ skipped += 1;
207
+ }
208
+ }
209
+ return { created, processed, skipped };
210
+ }
211
+
212
+ /**
213
+ * @async
214
+ * @param {string} controllerUrl
215
+ * @param {Object} authConfig
216
+ * @param {Object} opts
217
+ * @returns {Promise<Object|null>}
218
+ */
219
+ async function maybePurgeAndSync(controllerUrl, authConfig, opts) {
220
+ if (opts.dryRun) {
221
+ return null;
222
+ }
223
+ if (opts.purgeCache) {
224
+ const cacheRes = await clearAuthCache(controllerUrl, authConfig);
225
+ throwIfApiFailed(cacheRes);
226
+ }
227
+ if (!opts.sync || !opts.envKey) {
228
+ return null;
229
+ }
230
+ const syncRes = await fullSyncToDataplane(controllerUrl, authConfig, opts.envKey);
231
+ throwIfApiFailed(syncRes);
232
+ const stats = unwrapControllerData(syncRes);
233
+ assertSyncStatsOk(stats, { allowEmptySync: opts.allowEmptySync });
234
+ return stats;
235
+ }
236
+
237
+ module.exports = {
238
+ ensureGroup,
239
+ ensureUser,
240
+ applyGroupsPhase,
241
+ applyUsersPhase,
242
+ applyMembershipsPhase,
243
+ maybePurgeAndSync
244
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @fileoverview Bulk identity apply (CSV) against miso-controller
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { parseUsersCsvFile, buildApplyPlanFromRows } = require('./identity-csv-parser');
10
+ const {
11
+ applyGroupsPhase,
12
+ applyUsersPhase,
13
+ applyMembershipsPhase,
14
+ maybePurgeAndSync
15
+ } = require('./identity-apply-phases');
16
+
17
+ const {
18
+ unwrapControllerData,
19
+ apiErrorMessage,
20
+ throwIfApiFailed,
21
+ findExistingUser
22
+ } = require('./identity-apply-core');
23
+
24
+ /**
25
+ * @async
26
+ * @param {string} controllerUrl
27
+ * @param {Object} authConfig
28
+ * @param {Object} options
29
+ * @returns {Promise<Object>}
30
+ */
31
+ async function runIdentityApply(controllerUrl, authConfig, options) {
32
+ const { rows } = parseUsersCsvFile(options.filePath, options.filterPrefix);
33
+ if (!rows.length) {
34
+ throw new Error('No data rows to apply after parsing CSV');
35
+ }
36
+ const plan = buildApplyPlanFromRows(rows);
37
+ const dryRun = Boolean(options.dryRun);
38
+
39
+ const groupPhase = await applyGroupsPhase(controllerUrl, authConfig, plan.groups, dryRun);
40
+ const userPhase = await applyUsersPhase(controllerUrl, authConfig, plan.users, dryRun);
41
+ const memPhase = await applyMembershipsPhase(
42
+ controllerUrl,
43
+ authConfig,
44
+ plan.memberships,
45
+ userPhase.map,
46
+ groupPhase.map,
47
+ dryRun
48
+ );
49
+
50
+ const syncStats = await maybePurgeAndSync(controllerUrl, authConfig, {
51
+ dryRun,
52
+ purgeCache: Boolean(options.purgeCache),
53
+ sync: Boolean(options.sync),
54
+ envKey: options.envKey,
55
+ allowEmptySync: Boolean(options.allowEmptySync)
56
+ });
57
+
58
+ return {
59
+ groupsProcessed: groupPhase.processed,
60
+ groupsCreated: groupPhase.created,
61
+ usersProcessed: userPhase.processed,
62
+ usersCreated: userPhase.created,
63
+ membershipsProcessed: memPhase.processed,
64
+ membershipsCreated: memPhase.created,
65
+ membershipsSkipped: memPhase.skipped,
66
+ dryRun,
67
+ syncStats
68
+ };
69
+ }
70
+
71
+ module.exports = {
72
+ unwrapControllerData,
73
+ apiErrorMessage,
74
+ throwIfApiFailed,
75
+ runIdentityApply,
76
+ findExistingUser
77
+ };