@aifabrix/builder 2.42.1 → 2.43.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 (117) hide show
  1. package/README.md +1 -1
  2. package/bin/aifabrix.js +1 -1
  3. package/integration/hubspot-test/README.md +126 -0
  4. package/integration/{hubspot → hubspot-test}/application.json +6 -6
  5. package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
  6. package/integration/hubspot-test/env.template +4 -0
  7. package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
  8. package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
  9. package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
  10. package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
  11. package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
  12. package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
  13. package/integration/hubspot-test/rbac.json +166 -0
  14. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
  15. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
  16. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
  17. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
  18. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
  19. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
  20. package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
  22. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
  23. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
  24. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
  25. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
  26. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
  29. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
  30. package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
  31. package/integration/{hubspot → hubspot-test}/test.js +102 -59
  32. package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
  33. package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
  34. package/lib/api/external-test.api.js +1 -1
  35. package/lib/api/service-users.api.js +111 -2
  36. package/lib/api/types/service-users.types.js +41 -0
  37. package/lib/app/register.js +3 -1
  38. package/lib/app/rotate-secret.js +3 -0
  39. package/lib/cli/setup-app.js +2 -2
  40. package/lib/cli/setup-auth.js +19 -11
  41. package/lib/cli/setup-dev.js +62 -32
  42. package/lib/cli/setup-environment.js +6 -21
  43. package/lib/cli/setup-infra.js +13 -0
  44. package/lib/cli/setup-secrets.js +45 -6
  45. package/lib/cli/setup-service-user.js +146 -20
  46. package/lib/cli/setup-utility.js +12 -0
  47. package/lib/commands/auth-config.js +4 -8
  48. package/lib/commands/datasource.js +46 -1
  49. package/lib/commands/dev-init.js +1 -1
  50. package/lib/commands/repair-env-template.js +14 -8
  51. package/lib/commands/repair-rbac.js +25 -19
  52. package/lib/commands/repair.js +96 -30
  53. package/lib/commands/secrets-remove.js +1 -1
  54. package/lib/commands/secrets-validate.js +17 -4
  55. package/lib/commands/service-user.js +231 -2
  56. package/lib/commands/up-common.js +25 -0
  57. package/lib/commands/up-dataplane.js +2 -2
  58. package/lib/core/admin-secrets.js +2 -0
  59. package/lib/core/config.js +7 -5
  60. package/lib/core/ensure-encryption-key.js +1 -3
  61. package/lib/core/secrets.js +32 -9
  62. package/lib/core/templates.js +1 -1
  63. package/lib/datasource/abac-validator.js +157 -0
  64. package/lib/datasource/field-reference-validator.js +74 -36
  65. package/lib/datasource/log-viewer.js +221 -0
  66. package/lib/datasource/resolve-app.js +109 -0
  67. package/lib/datasource/test-e2e.js +11 -20
  68. package/lib/datasource/test-integration.js +42 -22
  69. package/lib/datasource/validate.js +5 -2
  70. package/lib/external-system/generator.js +12 -8
  71. package/lib/external-system/test-system-level.js +1 -1
  72. package/lib/generator/external-controller-manifest.js +3 -3
  73. package/lib/generator/external.js +7 -7
  74. package/lib/generator/helpers.js +13 -9
  75. package/lib/generator/index.js +4 -4
  76. package/lib/generator/split.js +45 -10
  77. package/lib/generator/wizard.js +9 -6
  78. package/lib/infrastructure/helpers.js +50 -35
  79. package/lib/infrastructure/index.js +39 -23
  80. package/lib/schema/env-config.yaml +19 -2
  81. package/lib/schema/external-datasource.schema.json +11 -1
  82. package/lib/utils/app-config-resolver.js +23 -1
  83. package/lib/utils/config-paths.js +48 -4
  84. package/lib/utils/credential-secrets-env.js +16 -1
  85. package/lib/utils/env-map.js +7 -3
  86. package/lib/utils/error-formatter.js +37 -0
  87. package/lib/utils/external-env-template.js +180 -0
  88. package/lib/utils/external-system-display.js +43 -0
  89. package/lib/utils/external-system-validators.js +2 -2
  90. package/lib/utils/help-builder.js +3 -5
  91. package/lib/utils/local-secrets.js +26 -3
  92. package/lib/utils/paths.js +2 -1
  93. package/lib/utils/secrets-generator.js +2 -2
  94. package/lib/utils/secrets-utils.js +4 -0
  95. package/lib/utils/secure-file-permissions.js +91 -0
  96. package/lib/utils/token-manager.js +36 -3
  97. package/lib/utils/yaml-preserve.js +59 -1
  98. package/lib/validation/env-template-auth.js +50 -2
  99. package/lib/validation/external-manifest-validator.js +8 -0
  100. package/lib/validation/validate.js +8 -0
  101. package/lib/validation/validator.js +10 -13
  102. package/package.json +5 -1
  103. package/templates/applications/dataplane/env.template +5 -1
  104. package/templates/applications/miso-controller/application.yaml +1 -1
  105. package/templates/applications/miso-controller/env.template +13 -2
  106. package/templates/external-system/env.template.hbs +22 -0
  107. package/integration/hubspot/README.md +0 -102
  108. package/integration/hubspot/env.template +0 -4
  109. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
  110. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
  111. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
  112. /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
  113. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
  114. /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
  115. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
  116. /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
  117. /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
@@ -23,8 +23,8 @@ const { resolveEnvironment } = require('../../lib/core/config');
23
23
 
24
24
  const execFileAsync = promisify(execFile);
25
25
 
26
- /** Single source for test config: integration/hubspot/.env */
27
- const HUBSPOT_DIR = path.join(process.cwd(), 'integration', 'hubspot');
26
+ /** Single source for test config: integration/hubspot-test/.env */
27
+ const HUBSPOT_DIR = path.join(process.cwd(), 'integration', 'hubspot-test');
28
28
  const LOCAL_ENV_PATH = path.join(HUBSPOT_DIR, '.env');
29
29
  const ARTIFACT_DIR = path.join(HUBSPOT_DIR, 'test-artifacts');
30
30
  const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
@@ -96,10 +96,10 @@ function printUsage() {
96
96
  // eslint-disable-next-line no-console
97
97
  console.log([
98
98
  'Usage:',
99
- ' node integration/hubspot/test.js',
100
- ' node integration/hubspot/test.js --test "1.1"',
101
- ' node integration/hubspot/test.js --type positive',
102
- ' node integration/hubspot/test.js --type negative --verbose',
99
+ ' node integration/hubspot-test/test.js',
100
+ ' node integration/hubspot-test/test.js --test "1.1"',
101
+ ' node integration/hubspot-test/test.js --type positive',
102
+ ' node integration/hubspot-test/test.js --type negative --verbose',
103
103
  '',
104
104
  'Options:',
105
105
  ' --test <id[,id]> Run specific test IDs',
@@ -266,7 +266,7 @@ async function loadEnvFile(envPath, options) {
266
266
  process.env[key] = value;
267
267
  }
268
268
  }
269
- // Map common .env keys so tests and wizard use credentials from integration/hubspot/.env
269
+ // Map common .env keys so tests and wizard use credentials from integration/hubspot-test/.env
270
270
  if (process.env.CLIENTID && process.env.HUBSPOT_CLIENT_ID === undefined) {
271
271
  process.env.HUBSPOT_CLIENT_ID = process.env.CLIENTID;
272
272
  }
@@ -280,7 +280,7 @@ async function loadEnvFile(envPath, options) {
280
280
 
281
281
  /**
282
282
  * Load test config (controller, environment, dataplane, openapi file).
283
- * Reads integration/hubspot/.env; missing CONTROLLER_URL/ENVIRONMENT fall back to
283
+ * Reads integration/hubspot-test/.env; missing CONTROLLER_URL/ENVIRONMENT fall back to
284
284
  * the same resolution as the CLI (af auth status) so tests use the same controller.
285
285
  * @async
286
286
  * @function loadTestConfigFromEnv
@@ -502,7 +502,20 @@ async function checkAppDirectory(appPath) {
502
502
  }
503
503
 
504
504
  /**
505
- * Validates required files exist
505
+ * Resolves application config path (application.json or application.yaml)
506
+ * @param {string} appPath - Application directory path
507
+ * @returns {string|null} Path to config file or null if neither exists
508
+ */
509
+ async function resolveApplicationConfigPath(appPath) {
510
+ const jsonPath = path.join(appPath, 'application.json');
511
+ const yamlPath = path.join(appPath, 'application.yaml');
512
+ if (await fileExists(jsonPath)) return jsonPath;
513
+ if (await fileExists(yamlPath)) return yamlPath;
514
+ return null;
515
+ }
516
+
517
+ /**
518
+ * Validates required files exist (application config can be application.json or application.yaml)
506
519
  * @async
507
520
  * @function validateRequiredFiles
508
521
  * @param {string} appPath - Application directory path
@@ -511,8 +524,12 @@ async function checkAppDirectory(appPath) {
511
524
  * @throws {Error} If required files are missing
512
525
  */
513
526
  async function validateRequiredFiles(appPath, entries) {
514
- const requiredFiles = ['application.yaml', 'env.template', 'README.md', 'deploy.js'];
527
+ const applicationConfigPath = await resolveApplicationConfigPath(appPath);
528
+ const requiredFiles = ['env.template', 'README.md', 'deploy.js'];
515
529
  const missingFiles = [];
530
+ if (!applicationConfigPath) {
531
+ missingFiles.push('application.yaml or application.json');
532
+ }
516
533
  for (const fileName of requiredFiles) {
517
534
  const filePath = path.join(appPath, fileName);
518
535
  const exists = await fileExists(filePath);
@@ -557,11 +574,19 @@ function validateDeployFiles(appPath, entries) {
557
574
  * @throws {Error} If file contents are invalid
558
575
  */
559
576
  async function validateFileContents(appPath, deployFiles) {
577
+ const configPath = await resolveApplicationConfigPath(appPath);
578
+ if (!configPath) {
579
+ throw new Error('Application config (application.yaml or application.json) not found');
580
+ }
560
581
  try {
561
- const variablesContent = await fs.readFile(path.join(appPath, 'application.yaml'), 'utf8');
562
- yaml.load(variablesContent);
582
+ const content = await fs.readFile(configPath, 'utf8');
583
+ if (configPath.endsWith('.json')) {
584
+ JSON.parse(content);
585
+ } else {
586
+ yaml.load(content);
587
+ }
563
588
  } catch (error) {
564
- throw new Error(`Invalid YAML syntax in application.yaml: ${error.message}`);
589
+ throw new Error(`Invalid syntax in ${path.basename(configPath)}: ${error.message}`);
565
590
  }
566
591
  for (const fileName of deployFiles) {
567
592
  try {
@@ -597,22 +622,23 @@ async function validateGeneratedFiles(appName) {
597
622
  * @returns {Promise<Object>} Snapshot of file contents keyed by path
598
623
  */
599
624
  async function captureExternalSnapshot(appPath) {
600
- const variablesPath = path.join(appPath, 'application.yaml');
601
- const variablesContent = await fs.readFile(variablesPath, 'utf8');
602
- const variables = yaml.load(variablesContent);
625
+ const configPath = await resolveApplicationConfigPath(appPath);
626
+ if (!configPath) {
627
+ throw new Error(`Application config not found in ${appPath}`);
628
+ }
629
+ const content = await fs.readFile(configPath, 'utf8');
630
+ const variables = configPath.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
603
631
 
604
632
  if (!variables || !variables.externalIntegration) {
605
- throw new Error(`externalIntegration block not found in ${variablesPath}`);
633
+ throw new Error(`externalIntegration block not found in ${configPath}`);
606
634
  }
607
635
 
608
- const systemFiles = variables.externalIntegration.systems || [];
609
- const datasourceFiles = variables.externalIntegration.dataSources || [];
610
636
  const fileNames = [
611
- 'application.yaml',
637
+ path.basename(configPath),
612
638
  'env.template',
613
639
  'README.md',
614
- ...systemFiles,
615
- ...datasourceFiles
640
+ ...(variables.externalIntegration.systems || []),
641
+ ...(variables.externalIntegration.dataSources || [])
616
642
  ];
617
643
 
618
644
  const rbacPath = path.join(appPath, 'rbac.yml');
@@ -658,7 +684,7 @@ function compareSnapshots(before, after) {
658
684
  * @returns {boolean} True if test app name
659
685
  */
660
686
  function isTestAppName(appName) {
661
- return appName.startsWith('hubspot-test-');
687
+ return appName.startsWith('wizard-e2e-');
662
688
  }
663
689
 
664
690
  /**
@@ -769,6 +795,20 @@ async function testDownloadAndSplit(appName, context, options) {
769
795
  logSuccess('Download and split workflow validated.');
770
796
  }
771
797
 
798
+ /**
799
+ * Returns a skip message only when the wizard failure is due to dataplane unreachable (no service).
800
+ * Does not skip for auth/session errors when dataplane is up (so tests fail with the real error).
801
+ * @param {string} errorOutput - Combined stdout + stderr from wizard command
802
+ * @returns {string|null} Skip message or null
803
+ */
804
+ function getWizardEnvironmentSkipMessage(errorOutput) {
805
+ if (errorOutput.includes('Failed to discover dataplane URL') ||
806
+ errorOutput.includes('Application not found')) {
807
+ return 'Dataplane service not found in environment. Deploy dataplane service to the controller.';
808
+ }
809
+ return null;
810
+ }
811
+
772
812
  /**
773
813
  * Runs wizard and validates generated files
774
814
  * @async
@@ -778,12 +818,17 @@ async function testDownloadAndSplit(appName, context, options) {
778
818
  * @param {Object} context - Test context
779
819
  * @param {Object} options - Options object
780
820
  * @returns {Promise<void>} Resolves when wizard completes and files are validated
821
+ * @throws {SkipTestError} If wizard fails due to environment (dataplane/auth)
781
822
  * @throws {Error} If wizard fails or validation fails
782
823
  */
783
824
  async function runWizardAndValidate(configPath, appName, context, options) {
784
825
  const result = await runWizard(configPath, context, options);
785
826
  if (!result.success) {
786
827
  const errorOutput = `${result.stdout}\n${result.stderr}`;
828
+ const skipMsg = getWizardEnvironmentSkipMessage(errorOutput);
829
+ if (skipMsg) {
830
+ throw new SkipTestError(skipMsg);
831
+ }
787
832
  throw new Error(`Wizard failed for ${appName}:\n${errorOutput}`);
788
833
  }
789
834
 
@@ -884,18 +929,18 @@ function buildPositiveTestCases(context) {
884
929
  throw new Error(`OpenAPI file not found: ${context.openapiFile}`);
885
930
  }
886
931
  ensureEnvVar('CONTROLLER_URL', context.controllerUrl);
887
- // Try to run wizard - if dataplane discovery fails, skip the test
932
+ // Try to run wizard - if dataplane/auth fails, skip the test
888
933
  const result = await runWizard(configPath, context, options);
889
934
  if (!result.success) {
890
935
  const errorOutput = `${result.stdout}\n${result.stderr}`;
891
- if (errorOutput.includes('Failed to discover dataplane URL') ||
892
- errorOutput.includes('Application not found')) {
893
- throw new SkipTestError('Dataplane service not found in environment. Deploy dataplane service to the controller.');
936
+ const skipMsg = getWizardEnvironmentSkipMessage(errorOutput);
937
+ if (skipMsg) {
938
+ throw new SkipTestError(skipMsg);
894
939
  }
895
940
  throw new Error(`Wizard failed: ${errorOutput}`);
896
941
  }
897
- await validateGeneratedFiles('hubspot-test-e2e');
898
- await cleanupAppArtifacts('hubspot-test-e2e', options);
942
+ await validateGeneratedFiles('wizard-e2e-e2e');
943
+ await cleanupAppArtifacts('wizard-e2e-e2e', options);
899
944
  }
900
945
  },
901
946
  {
@@ -908,11 +953,11 @@ function buildPositiveTestCases(context) {
908
953
  throw new Error(`Missing config file: ${configPath}`);
909
954
  }
910
955
  try {
911
- await ensureDataplaneUrl(context, 'hubspot-test-platform');
956
+ await ensureDataplaneUrl(context, 'wizard-e2e-platform');
912
957
  } catch (error) {
913
958
  throw new SkipTestError(`Dataplane service not found: ${error.message}`);
914
959
  }
915
- await runWizardAndValidate(configPath, 'hubspot-test-platform', context, options);
960
+ await runWizardAndValidate(configPath, 'wizard-e2e-platform', context, options);
916
961
  }
917
962
  },
918
963
  {
@@ -922,14 +967,14 @@ function buildPositiveTestCases(context) {
922
967
  run: async(options) => {
923
968
  let dataplaneUrl;
924
969
  try {
925
- dataplaneUrl = await ensureDataplaneUrl(context, 'hubspot-test-env-vars');
970
+ dataplaneUrl = await ensureDataplaneUrl(context, 'wizard-e2e-env-vars');
926
971
  } catch (error) {
927
972
  throw new SkipTestError(`Dataplane service not found: ${error.message}`);
928
973
  }
929
974
  ensureEnvVar('CONTROLLER_URL', context.controllerUrl);
930
975
  ensureEnvVar('DATAPLANE_URL', dataplaneUrl);
931
976
  const configPath = await writeWizardConfig('wizard-hubspot-env-vars', {
932
- appName: 'hubspot-test-env-vars',
977
+ appName: 'wizard-e2e-env-vars',
933
978
  mode: 'create-system',
934
979
  source: {
935
980
  type: 'openapi-file',
@@ -941,7 +986,7 @@ function buildPositiveTestCases(context) {
941
986
  environment: context.environment
942
987
  }
943
988
  });
944
- await runWizardAndValidate(configPath, 'hubspot-test-env-vars', context, options);
989
+ await runWizardAndValidate(configPath, 'wizard-e2e-env-vars', context, options);
945
990
  }
946
991
  }
947
992
  ];
@@ -966,7 +1011,7 @@ function buildRealDataTestCases(context) {
966
1011
  requireEnvVars(['HUBSPOT_CLIENT_ID', 'HUBSPOT_CLIENT_SECRET']);
967
1012
  ensureEnvVar('HUBSPOT_TOKEN_URL', 'https://api.hubapi.com/oauth/v1/token');
968
1013
  const configPath = await writeWizardConfig('wizard-hubspot-credential-real', {
969
- appName: 'hubspot-test-credential-real',
1014
+ appName: 'wizard-e2e-credential-real',
970
1015
  mode: 'create-system',
971
1016
  source: {
972
1017
  type: 'openapi-file',
@@ -975,7 +1020,7 @@ function buildRealDataTestCases(context) {
975
1020
  credential: {
976
1021
  action: 'create',
977
1022
  config: {
978
- key: 'hubspot-test-cred-real',
1023
+ key: 'wizard-e2e-cred-real',
979
1024
  displayName: 'HubSpot Test Credential (Real)',
980
1025
  type: 'OAUTH2',
981
1026
  config: {
@@ -992,7 +1037,7 @@ function buildRealDataTestCases(context) {
992
1037
  }
993
1038
  }
994
1039
  });
995
- await runWizardAndValidate(configPath, 'hubspot-test-credential-real', context, options);
1040
+ await runWizardAndValidate(configPath, 'wizard-e2e-credential-real', context, options);
996
1041
  }
997
1042
  }
998
1043
  ];
@@ -1038,7 +1083,7 @@ function buildNegativeConfigTestCases(context) {
1038
1083
  name: 'Missing source block',
1039
1084
  run: async(options) => {
1040
1085
  const configPath = await writeWizardConfig('wizard-invalid-missing-source', {
1041
- appName: 'hubspot-test-negative-missing-source',
1086
+ appName: 'wizard-e2e-negative-missing-source',
1042
1087
  mode: 'create-system'
1043
1088
  });
1044
1089
  await runWizardExpectFailure(configPath, context, options, 'Missing required field: source');
@@ -1050,7 +1095,7 @@ function buildNegativeConfigTestCases(context) {
1050
1095
  name: 'Invalid source type',
1051
1096
  run: async(options) => {
1052
1097
  const configPath = await writeWizardConfig('wizard-invalid-source', {
1053
- appName: 'hubspot-test-negative-source',
1098
+ appName: 'wizard-e2e-negative-source',
1054
1099
  mode: 'create-system',
1055
1100
  source: { type: 'invalid-type' }
1056
1101
  });
@@ -1063,7 +1108,7 @@ function buildNegativeConfigTestCases(context) {
1063
1108
  name: 'Invalid mode',
1064
1109
  run: async(options) => {
1065
1110
  const configPath = await writeWizardConfig('wizard-invalid-mode', {
1066
- appName: 'hubspot-test-negative-mode',
1111
+ appName: 'wizard-e2e-negative-mode',
1067
1112
  mode: 'invalid-mode',
1068
1113
  source: { type: 'known-platform', platform: 'hubspot' }
1069
1114
  });
@@ -1076,7 +1121,7 @@ function buildNegativeConfigTestCases(context) {
1076
1121
  name: 'Known platform missing platform',
1077
1122
  run: async(options) => {
1078
1123
  const configPath = await writeWizardConfig('wizard-invalid-known-platform', {
1079
- appName: 'hubspot-test-negative-platform',
1124
+ appName: 'wizard-e2e-negative-platform',
1080
1125
  mode: 'create-system',
1081
1126
  source: { type: 'known-platform' }
1082
1127
  });
@@ -1089,7 +1134,7 @@ function buildNegativeConfigTestCases(context) {
1089
1134
  name: 'Missing OpenAPI file path',
1090
1135
  run: async(options) => {
1091
1136
  const configPath = await writeWizardConfig('wizard-invalid-openapi-file', {
1092
- appName: 'hubspot-test-negative-openapi',
1137
+ appName: 'wizard-e2e-negative-openapi',
1093
1138
  mode: 'create-system',
1094
1139
  source: { type: 'openapi-file', filePath: '/tmp/does-not-exist.json' }
1095
1140
  });
@@ -1102,7 +1147,7 @@ function buildNegativeConfigTestCases(context) {
1102
1147
  name: 'OpenAPI URL missing url',
1103
1148
  run: async(options) => {
1104
1149
  const configPath = await writeWizardConfig('wizard-invalid-openapi-url', {
1105
- appName: 'hubspot-test-negative-openapi-url',
1150
+ appName: 'wizard-e2e-negative-openapi-url',
1106
1151
  mode: 'create-system',
1107
1152
  source: { type: 'openapi-url' }
1108
1153
  });
@@ -1126,7 +1171,7 @@ function buildNegativeCredentialTestCases(context) {
1126
1171
  name: 'Add datasource missing systemIdOrKey',
1127
1172
  run: async(options) => {
1128
1173
  const configPath = await writeWizardConfig('wizard-invalid-add-datasource', {
1129
- appName: 'hubspot-test-negative-add-datasource',
1174
+ appName: 'wizard-e2e-negative-add-datasource',
1130
1175
  mode: 'add-datasource',
1131
1176
  source: { type: 'known-platform', platform: 'hubspot' }
1132
1177
  });
@@ -1139,7 +1184,7 @@ function buildNegativeCredentialTestCases(context) {
1139
1184
  name: 'Credential select missing credentialIdOrKey',
1140
1185
  run: async(options) => {
1141
1186
  const configPath = await writeWizardConfig('wizard-invalid-credential-select', {
1142
- appName: 'hubspot-test-negative-credential-select',
1187
+ appName: 'wizard-e2e-negative-credential-select',
1143
1188
  mode: 'create-system',
1144
1189
  source: { type: 'known-platform', platform: 'hubspot' },
1145
1190
  credential: { action: 'select' }
@@ -1153,7 +1198,7 @@ function buildNegativeCredentialTestCases(context) {
1153
1198
  name: 'Credential create missing config',
1154
1199
  run: async(options) => {
1155
1200
  const configPath = await writeWizardConfig('wizard-invalid-credential-create', {
1156
- appName: 'hubspot-test-negative-credential-create',
1201
+ appName: 'wizard-e2e-negative-credential-create',
1157
1202
  mode: 'create-system',
1158
1203
  source: { type: 'known-platform', platform: 'hubspot' },
1159
1204
  credential: { action: 'create' }
@@ -1176,13 +1221,7 @@ function buildNegativeCredentialTestCases(context) {
1176
1221
  * @throws {Error} If validation succeeds or expected message not found
1177
1222
  */
1178
1223
  async function runValidationExpectFailure(appName, context, options, expectedMessage = null) {
1179
- const validateArgs = [
1180
- 'bin/aifabrix.js',
1181
- 'validate',
1182
- appName,
1183
- '--type',
1184
- 'external'
1185
- ];
1224
+ const validateArgs = ['bin/aifabrix.js', 'validate', appName];
1186
1225
  const result = await runCommand('node', validateArgs, options);
1187
1226
  if (result.success) {
1188
1227
  throw new Error('Expected validation to fail, but it succeeded.');
@@ -1363,7 +1402,7 @@ function buildNegativeRbacTestCases(context) {
1363
1402
  type: 'negative',
1364
1403
  name: 'RBAC missing role referenced in permissions',
1365
1404
  run: async(options) => {
1366
- const appName = 'hubspot-test-negative-rbac-missing-role';
1405
+ const appName = 'wizard-e2e-negative-rbac-missing-role';
1367
1406
  const appPath = await createSystemForNegativeTest(appName, 'wizard-valid-for-rbac-test', context, options);
1368
1407
  await corruptSystemFileWithInvalidRole(appPath);
1369
1408
  await runValidationExpectFailure(appName, context, options, 'references role "non-existent-role" which does not exist');
@@ -1375,7 +1414,11 @@ function buildNegativeRbacTestCases(context) {
1375
1414
  type: 'negative',
1376
1415
  name: 'RBAC invalid YAML syntax',
1377
1416
  run: async(options) => {
1378
- const appName = 'hubspot-test-negative-rbac-invalid-yaml';
1417
+ const appName = 'wizard-e2e-negative-rbac-invalid-yaml';
1418
+ const appDir = path.join(process.cwd(), 'integration', appName);
1419
+ if (fsSync.existsSync(appDir)) {
1420
+ await fs.rm(appDir, { recursive: true, force: true });
1421
+ }
1379
1422
  const appPath = await createSystemForNegativeTest(appName, 'wizard-valid-for-rbac-yaml-test', context, options);
1380
1423
  await corruptRbacFile(appPath);
1381
1424
  await runValidationExpectFailure(appName, context, options, 'Invalid YAML syntax in rbac.yaml');
@@ -1409,7 +1452,7 @@ function buildNegativeDimensionTestCases(context) {
1409
1452
  createTestCase(
1410
1453
  '2.14',
1411
1454
  'Datasource missing dimensions in fieldMappings',
1412
- 'hubspot-test-negative-dimension-missing',
1455
+ 'wizard-e2e-negative-dimension-missing',
1413
1456
  'wizard-valid-for-dimension-test',
1414
1457
  corruptDatasourceRemoveDimensions,
1415
1458
  'Missing required property "dimensions"'
@@ -1417,7 +1460,7 @@ function buildNegativeDimensionTestCases(context) {
1417
1460
  createTestCase(
1418
1461
  '2.15',
1419
1462
  'Datasource invalid dimension key pattern',
1420
- 'hubspot-test-negative-dimension-invalid-key',
1463
+ 'wizard-e2e-negative-dimension-invalid-key',
1421
1464
  'wizard-valid-for-dimension-key-test',
1422
1465
  corruptDatasourceInvalidDimensionKey,
1423
1466
  'Must be at most 40 characters'
@@ -1425,7 +1468,7 @@ function buildNegativeDimensionTestCases(context) {
1425
1468
  createTestCase(
1426
1469
  '2.16',
1427
1470
  'Datasource invalid attribute path pattern',
1428
- 'hubspot-test-negative-dimension-invalid-path',
1471
+ 'wizard-e2e-negative-dimension-invalid-path',
1429
1472
  'wizard-valid-for-dimension-path-test',
1430
1473
  corruptDatasourceInvalidAttributePath,
1431
1474
  'must match pattern'
@@ -1433,7 +1476,7 @@ function buildNegativeDimensionTestCases(context) {
1433
1476
  createTestCase(
1434
1477
  '2.17',
1435
1478
  'Datasource dimensions as array instead of object',
1436
- 'hubspot-test-negative-dimension-array',
1479
+ 'wizard-e2e-negative-dimension-array',
1437
1480
  'wizard-valid-for-dimension-array-test',
1438
1481
  corruptDatasourceDimensionsAsArray,
1439
1482
  'Expected object, got undefined'
@@ -1,8 +1,8 @@
1
- appName: hubspot-test-e2e
1
+ appName: wizard-e2e-e2e
2
2
  mode: create-system
3
3
  source:
4
4
  type: openapi-file
5
- filePath: /workspace/aifabrix-builder/integration/hubspot/companies.json
5
+ filePath: /workspace/aifabrix-builder/integration/hubspot-test/companies.json
6
6
  credential:
7
7
  action: skip
8
8
  preferences:
@@ -1,4 +1,4 @@
1
- appName: hubspot-test-platform
1
+ appName: wizard-e2e-platform
2
2
  mode: create-system
3
3
  source:
4
4
  type: known-platform
@@ -17,7 +17,7 @@ const { ApiClient } = require('./index');
17
17
  * @async
18
18
  * @function testDatasourceE2E
19
19
  * @param {string} dataplaneUrl - Dataplane base URL
20
- * @param {string} sourceIdOrKey - Source ID or datasource key (e.g. hubspot-test-v4-contacts)
20
+ * @param {string} sourceIdOrKey - Source ID or datasource key (e.g. hubspot-test-contacts)
21
21
  * @param {Object} authConfig - Authentication configuration (must have token or apiKey; client creds rejected)
22
22
  * @param {Object} [body] - Optional request body (e.g. includeDebug, testCrud, recordId, cleanup, primaryKeyValue)
23
23
  * @param {Object} [options] - Optional options
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @fileoverview Service users API functions (create service user with one-time secret)
2
+ * @fileoverview Service users API functions (create, list, rotate-secret, delete, update)
3
3
  * @author AI Fabrix Team
4
4
  * @version 2.0.0
5
5
  */
@@ -36,6 +36,115 @@ async function createServiceUser(controllerUrl, authConfig, body) {
36
36
  });
37
37
  }
38
38
 
39
+ /**
40
+ * List service users with optional pagination and search
41
+ * GET /api/v1/service-users
42
+ * @requiresPermission {Controller} service-user:read
43
+ * @async
44
+ * @function listServiceUsers
45
+ * @param {string} controllerUrl - Controller base URL
46
+ * @param {Object} authConfig - Authentication configuration (bearer or client-credentials)
47
+ * @param {Object} [options] - Query options
48
+ * @param {number} [options.page] - Page number
49
+ * @param {number} [options.pageSize] - Items per page
50
+ * @param {string} [options.sort] - Sort field/direction
51
+ * @param {string} [options.filter] - Filter expression
52
+ * @param {string} [options.search] - Search term
53
+ * @returns {Promise<Object>} Response with data (array), meta, links
54
+ * @throws {Error} If request fails (401/403 or network)
55
+ */
56
+ async function listServiceUsers(controllerUrl, authConfig, options = {}) {
57
+ const client = new ApiClient(controllerUrl, authConfig);
58
+ const params = {};
59
+ if (options.page !== undefined && options.page !== null) params.page = options.page;
60
+ if (options.pageSize !== undefined && options.pageSize !== null) params.pageSize = options.pageSize;
61
+ if (options.sort) params.sort = options.sort;
62
+ if (options.filter) params.filter = options.filter;
63
+ if (options.search) params.search = options.search;
64
+ return await client.get('/api/v1/service-users', { params });
65
+ }
66
+
67
+ /**
68
+ * Regenerate (rotate) secret for a service user. New secret is returned once only.
69
+ * POST /api/v1/service-users/{id}/regenerate-secret
70
+ * @requiresPermission {Controller} service-user:update
71
+ * @async
72
+ * @function regenerateSecretServiceUser
73
+ * @param {string} controllerUrl - Controller base URL
74
+ * @param {Object} authConfig - Authentication configuration
75
+ * @param {string} id - Service user ID (UUID)
76
+ * @returns {Promise<Object>} Response with data.clientSecret
77
+ * @throws {Error} If request fails (401/403/404 or network)
78
+ */
79
+ async function regenerateSecretServiceUser(controllerUrl, authConfig, id) {
80
+ const client = new ApiClient(controllerUrl, authConfig);
81
+ return await client.post(`/api/v1/service-users/${encodeURIComponent(id)}/regenerate-secret`);
82
+ }
83
+
84
+ /**
85
+ * Delete (deactivate) a service user
86
+ * DELETE /api/v1/service-users/{id}
87
+ * @requiresPermission {Controller} service-user:delete
88
+ * @async
89
+ * @function deleteServiceUser
90
+ * @param {string} controllerUrl - Controller base URL
91
+ * @param {Object} authConfig - Authentication configuration
92
+ * @param {string} id - Service user ID (UUID)
93
+ * @returns {Promise<Object>} Response (data may be null)
94
+ * @throws {Error} If request fails (401/403/404 or network)
95
+ */
96
+ async function deleteServiceUser(controllerUrl, authConfig, id) {
97
+ const client = new ApiClient(controllerUrl, authConfig);
98
+ return await client.delete(`/api/v1/service-users/${encodeURIComponent(id)}`);
99
+ }
100
+
101
+ /**
102
+ * Update group assignments for a service user
103
+ * PUT /api/v1/service-users/{id}/groups
104
+ * @requiresPermission {Controller} service-user:update
105
+ * @async
106
+ * @function updateGroupsServiceUser
107
+ * @param {string} controllerUrl - Controller base URL
108
+ * @param {Object} authConfig - Authentication configuration
109
+ * @param {string} id - Service user ID (UUID)
110
+ * @param {Object} body - Request body
111
+ * @param {string[]} body.groupNames - Group names to set
112
+ * @returns {Promise<Object>} Response with data.id, data.groupNames
113
+ * @throws {Error} If request fails (400/401/403/404 or network)
114
+ */
115
+ async function updateGroupsServiceUser(controllerUrl, authConfig, id, body) {
116
+ const client = new ApiClient(controllerUrl, authConfig);
117
+ return await client.put(`/api/v1/service-users/${encodeURIComponent(id)}/groups`, {
118
+ body: { groupNames: body.groupNames }
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Update redirect URIs for a service user (min 1). Controller merges in its callback URL.
124
+ * PUT /api/v1/service-users/{id}/redirect-uris
125
+ * @requiresPermission {Controller} service-user:update
126
+ * @async
127
+ * @function updateRedirectUrisServiceUser
128
+ * @param {string} controllerUrl - Controller base URL
129
+ * @param {Object} authConfig - Authentication configuration
130
+ * @param {string} id - Service user ID (UUID)
131
+ * @param {Object} body - Request body
132
+ * @param {string[]} body.redirectUris - Redirect URIs (min 1)
133
+ * @returns {Promise<Object>} Response with data.id, data.redirectUris
134
+ * @throws {Error} If request fails (400/401/403/404 or network)
135
+ */
136
+ async function updateRedirectUrisServiceUser(controllerUrl, authConfig, id, body) {
137
+ const client = new ApiClient(controllerUrl, authConfig);
138
+ return await client.put(`/api/v1/service-users/${encodeURIComponent(id)}/redirect-uris`, {
139
+ body: { redirectUris: body.redirectUris }
140
+ });
141
+ }
142
+
39
143
  module.exports = {
40
- createServiceUser
144
+ createServiceUser,
145
+ listServiceUsers,
146
+ regenerateSecretServiceUser,
147
+ deleteServiceUser,
148
+ updateGroupsServiceUser,
149
+ updateRedirectUrisServiceUser
41
150
  };
@@ -22,3 +22,44 @@
22
22
  * @property {boolean} [success] - Optional wrapper flag
23
23
  * @property {string} [createdAt] - Optional creation timestamp (ISO 8601)
24
24
  */
25
+
26
+ /**
27
+ * Single service user item in list response
28
+ * @typedef {Object} ListServiceUserItem
29
+ * @property {string} id - Service user ID (UUID)
30
+ * @property {string} [username] - Username
31
+ * @property {string} [email] - Email
32
+ * @property {string} [clientId] - OAuth2 client ID
33
+ * @property {boolean} [active] - Whether the service user is active
34
+ */
35
+
36
+ /**
37
+ * List service users response (Controller GET /api/v1/service-users)
38
+ * @typedef {Object} ListServiceUsersResponse
39
+ * @property {ListServiceUserItem[]} data - Array of service users
40
+ * @property {Object} [meta] - Pagination metadata (e.g. total, page, pageSize)
41
+ * @property {Object} [links] - Pagination links
42
+ */
43
+
44
+ /**
45
+ * Regenerate secret response (Controller POST .../regenerate-secret). clientSecret is one-time-only.
46
+ * @typedef {Object} RegenerateSecretServiceUserResponse
47
+ * @property {Object} data - Response data
48
+ * @property {string} data.clientSecret - New client secret (shown once only)
49
+ */
50
+
51
+ /**
52
+ * Update groups response (Controller PUT .../groups)
53
+ * @typedef {Object} UpdateGroupsServiceUserResponse
54
+ * @property {Object} data - Response data
55
+ * @property {string} data.id - Service user ID
56
+ * @property {string[]} data.groupNames - Updated group names
57
+ */
58
+
59
+ /**
60
+ * Update redirect URIs response (Controller PUT .../redirect-uris)
61
+ * @typedef {Object} UpdateRedirectUrisServiceUserResponse
62
+ * @property {Object} data - Response data
63
+ * @property {string} data.id - Service user ID
64
+ * @property {string[]} data.redirectUris - Updated redirect URIs
65
+ */
@@ -150,7 +150,9 @@ async function registerApplication(appKey, options = {}) {
150
150
  logger.log(chalk.blue('📋 Registering application...\n'));
151
151
 
152
152
  const { resolveControllerUrl } = require('../utils/controller-url');
153
- const { resolveEnvironment } = require('../core/config');
153
+ const config = require('../core/config');
154
+ await config.ensureSecretsEncryptionKey();
155
+ const { resolveEnvironment } = config;
154
156
 
155
157
  // Load application config
156
158
  const { variables, created } = await loadVariablesYaml(appKey);
@@ -339,6 +339,9 @@ async function executeRotation(appKey, actualControllerUrl, environment, token)
339
339
  async function rotateSecret(appKey, _options = {}) {
340
340
  logger.log(chalk.yellow('⚠️ This will invalidate the old ClientSecret!\n'));
341
341
 
342
+ const { ensureSecretsEncryptionKey } = require('../core/config');
343
+ await ensureSecretsEncryptionKey();
344
+
342
345
  const { controllerUrl, environment } = await resolveControllerAndEnvironment();
343
346
  const config = await getConfig();
344
347
  const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
@@ -140,12 +140,12 @@ Examples:
140
140
  $ aifabrix wizard my-integration --silent Run headless with integration/my-integration/wizard.yaml (no prompts)
141
141
  $ aifabrix wizard -a my-integration Same as above (app name set)
142
142
  $ aifabrix wizard --config wizard.yaml Run headless from a wizard config file
143
- $ aifabrix wizard hubspot-test-v2 --debug Enable debug output and save debug manifests on validation failure
143
+ $ aifabrix wizard hubspot-test --debug Enable debug output and save debug manifests on validation failure
144
144
 
145
145
  Config path: When appName is provided, integration/<appName>/wizard.yaml is used for load/save and error.log.
146
146
  To change settings after a run, edit that file and run "aifabrix wizard <app>" again.
147
147
  Headless config must include: appName, mode (create-system|add-datasource), source (type + filePath/url/platform).
148
- See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`;
148
+ See integration/hubspot-test/wizard-hubspot-e2e.yaml for an example.`;
149
149
  program.command('wizard [appName]')
150
150
  .description('Create or extend external systems (OpenAPI, MCP, or known platforms like HubSpot) via guided steps or a config file')
151
151
  .option('-a, --app <app>', 'Application name (synonym for positional appName)')