@salesforce/ui-bundle-template-feature-react-agentforce-conversation-client 11.47.1 → 11.49.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.
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [11.49.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.48.0...v11.49.0) (2026-08-05)
7
+
8
+ **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
9
+
10
+
11
+
12
+
13
+
14
+ ## [11.48.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.47.1...v11.48.0) (2026-08-05)
15
+
16
+
17
+ ### Features
18
+
19
+ * **auth:** upmerge React authentication feature (IDP/SSO + MFA) from webapps-develop @W-23707015 ([#846](https://github.com/salesforce-experience-platform-emu/webapps/issues/846)) ([4179304](https://github.com/salesforce-experience-platform-emu/webapps/commit/4179304b8a36eeca6ae599398fa7e73ee1bbb266))
20
+
21
+
22
+
6
23
  ## [11.47.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.47.0...v11.47.1) (2026-08-05)
7
24
 
8
25
  **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
@@ -18,8 +18,8 @@
18
18
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
19
19
  },
20
20
  "dependencies": {
21
- "@salesforce/platform-sdk": "^11.47.1",
22
- "@salesforce/ui-bundle": "^11.47.1",
21
+ "@salesforce/platform-sdk": "^11.49.0",
22
+ "@salesforce/ui-bundle": "^11.49.0",
23
23
  "@tailwindcss/vite": "^4.1.17",
24
24
  "class-variance-authority": "^0.7.1",
25
25
  "clsx": "^2.1.1",
@@ -45,8 +45,8 @@
45
45
  "@graphql-eslint/eslint-plugin": "^4.1.0",
46
46
  "@graphql-tools/utils": "^11.0.0",
47
47
  "@playwright/test": "^1.49.0",
48
- "@salesforce/graphiti": "^11.47.1",
49
- "@salesforce/vite-plugin-ui-bundle": "^11.47.1",
48
+ "@salesforce/graphiti": "^11.49.0",
49
+ "@salesforce/vite-plugin-ui-bundle": "^11.49.0",
50
50
  "@testing-library/jest-dom": "^6.6.3",
51
51
  "@testing-library/react": "^16.1.0",
52
52
  "@testing-library/user-event": "^14.5.2",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "11.47.1",
3
+ "version": "11.49.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
9
- "version": "11.47.1",
9
+ "version": "11.49.0",
10
10
  "license": "SEE LICENSE IN LICENSE.txt",
11
11
  "dependencies": {
12
12
  "fast-xml-parser": "^5.9.3",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "11.47.1",
3
+ "version": "11.49.0",
4
4
  "description": "Base SFDX project template",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "publishConfig": {
@@ -62,6 +62,23 @@ export const ConfigSchema = z
62
62
  })
63
63
  .strict()
64
64
  .optional(),
65
+ socialLogin: z
66
+ .object({
67
+ // Profile to add to NetworkMemberGroup so SSO-registered users can access
68
+ // the community (e.g. "Customer Community User"). Also enables the
69
+ // "Allow standard external profiles" org setting.
70
+ communityMemberProfile: z.string().min(1),
71
+ // DeveloperName of AuthProvider (OAuth) or SamlSsoConfig (SAML) records to link to the site's AuthConfig.
72
+ // Required because React (Site Container) sites hide SSO config in Admin UI.
73
+ authProviderNames: z.array(z.string().min(1)).min(1),
74
+ // Permission set to assign to community users created via SSO.
75
+ // Required because getCurrentUser() calls /chatter/users/me which needs
76
+ // API access (PermissionsApiEnabled) that the community profile lacks.
77
+ // Typically the app's "*_Guest_User_Api_Access" permset.
78
+ communityUserPermset: z.string().min(1).optional(),
79
+ })
80
+ .strict()
81
+ .optional(),
65
82
  })
66
83
  .strict();
67
84
 
@@ -46,6 +46,7 @@ import {
46
46
  existsSync,
47
47
  readFileSync,
48
48
  writeFileSync,
49
+ mkdirSync,
49
50
  mkdtempSync,
50
51
  rmSync,
51
52
  openSync,
@@ -286,6 +287,7 @@ function parseArgs() {
286
287
  skipGraphql: false,
287
288
  skipUIBundleBuild: false,
288
289
  skipSelfReg: false,
290
+ skipSocialLogin: false,
289
291
  };
290
292
  for (let i = 0; i < args.length; i++) {
291
293
  if (args[i] === '--target-org' && args[i + 1]) {
@@ -299,6 +301,7 @@ function parseArgs() {
299
301
  else if (args[i] === '--skip-role') flags.skipRole = true;
300
302
  else if (args[i] === '--skip-data') flags.skipData = true;
301
303
  else if (args[i] === '--skip-self-reg') flags.skipSelfReg = true;
304
+ else if (args[i] === '--skip-social-login') flags.skipSocialLogin = true;
302
305
  else if (args[i] === '--skip-graphql') flags.skipGraphql = true;
303
306
  else if (args[i] === '--skip-ui-bundle-build') flags.skipUIBundleBuild = true;
304
307
  // --skip-login and --skip-dev are retired: login is now an unconditional
@@ -322,6 +325,7 @@ Options:
322
325
  --permset-name <name> Assign only this permission set (repeatable). Default: all sets under permissionsets/
323
326
  --skip-deploy Do not deploy metadata
324
327
  --skip-permset Do not assign permission set
328
+ --skip-social-login Do not enable social login (auth providers + community profile)
325
329
  --skip-data Do not prepare data or run data import
326
330
  --skip-graphql Do not fetch schema or run GraphQL codegen
327
331
  --skip-ui-bundle-build Do not npm install / build the UI bundle
@@ -521,6 +525,435 @@ function loadSelfRegConfig(config) {
521
525
  };
522
526
  }
523
527
 
528
+ /**
529
+ * Social login config, read from the already-validated config.
530
+ *
531
+ * Config shape:
532
+ * {
533
+ * "socialLogin": {
534
+ * "communityMemberProfile": "Customer Community User",
535
+ * "authProviderNames": ["Google", "Facebook", "My_SAML_Provider"],
536
+ * "communityUserPermset": "myapp_Guest_User_Api_Access" // optional
537
+ * }
538
+ * }
539
+ *
540
+ * - communityMemberProfile: profile added to NetworkMemberGroup (required for access)
541
+ * - authProviderNames: DeveloperNames of AuthProvider (OAuth) or SamlSsoConfig (SAML) records to link to site
542
+ * - communityUserPermset: permset assigned to community users so getCurrentUser()
543
+ * works (requires PermissionsApiEnabled for /chatter/users/me via /sf/api/)
544
+ *
545
+ * Returns null if no "socialLogin" section exists in config (the step is hidden).
546
+ */
547
+ function loadSocialLoginConfig(config) {
548
+ const section = config.socialLogin;
549
+ if (!section) return null;
550
+ return {
551
+ communityMemberProfile: section.communityMemberProfile,
552
+ authProviderNames: section.authProviderNames,
553
+ communityUserPermset: section.communityUserPermset || null,
554
+ };
555
+ }
556
+
557
+ /**
558
+ * Enable the "Allow using standard external profiles for self-registration,
559
+ * user creation, and login" org setting via Metadata API deploy.
560
+ *
561
+ * This setting is required for SSO registration handlers to create users with
562
+ * standard community profiles (e.g. "Customer Community User"). Without it,
563
+ * auth providers return FIELD_INTEGRITY_EXCEPTION on user insert.
564
+ *
565
+ * The correct metadata field is `enableOotbProfExtUserOpsEnable` in
566
+ * CommunitiesSettings. The REST/Tooling API approaches also fail on many
567
+ * org types. Deploying via Metadata API is the most reliable method.
568
+ */
569
+ function enableExternalProfiles(targetOrg) {
570
+ const dir = mkdtempSync(join(tmpdir(), APEX_TMP_PREFIX));
571
+ try {
572
+ // Create a minimal sfdx project with just the Communities setting
573
+ const settingsDir = join(dir, 'force-app', 'main', 'default', 'settings');
574
+ mkdirSync(settingsDir, { recursive: true });
575
+
576
+ writeFileSync(join(dir, 'sfdx-project.json'), JSON.stringify({
577
+ packageDirectories: [{ path: 'force-app', default: true }],
578
+ sourceApiVersion: '68.0',
579
+ }));
580
+
581
+ writeFileSync(join(settingsDir, 'Communities.settings-meta.xml'), [
582
+ '<?xml version="1.0" encoding="UTF-8"?>',
583
+ '<CommunitiesSettings xmlns="http://soap.sforce.com/2006/04/metadata">',
584
+ ' <enableOotbProfExtUserOpsEnable>true</enableOotbProfExtUserOpsEnable>',
585
+ '</CommunitiesSettings>',
586
+ ].join('\n'));
587
+
588
+ const result = spawnSync('sf', [
589
+ 'project', 'deploy', 'start',
590
+ '--target-org', targetOrg,
591
+ '--source-dir', join(dir, 'force-app'),
592
+ '--json',
593
+ ], { cwd: dir, stdio: 'pipe', shell: true, timeout: 120000 });
594
+
595
+ const out = result.stdout?.toString() || '';
596
+ let deployResult;
597
+ try { deployResult = JSON.parse(out); } catch { deployResult = null; }
598
+
599
+ if (deployResult?.status === 0 || deployResult?.result?.status === 'Succeeded') {
600
+ console.log(' Enabled "Allow using standard external profiles" org setting.');
601
+ } else {
602
+ // Check if it failed because the setting is already enabled or not deployable
603
+ const errMsg = deployResult?.message || deployResult?.result?.details?.componentFailures?.[0]?.problem || '';
604
+ if (errMsg.includes('already') || errMsg.includes('enableOotbProfExtUserOpsEnable')) {
605
+ console.log(' Warning: could not deploy "Allow standard external profiles" setting.');
606
+ console.log(` Detail: ${errMsg}`);
607
+ console.log(' The setting may already be active. Enable manually via Setup > Digital Experiences > Settings if needed.');
608
+ } else {
609
+ console.log(' Warning: could not enable "Allow standard external profiles".');
610
+ console.log(` Deploy result: ${errMsg || 'unknown error'}`);
611
+ console.log(' Enable manually via Setup > Digital Experiences > Settings.');
612
+ }
613
+ }
614
+ } finally {
615
+ try { rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Enable Auth Providers for a community by creating AuthConfigProviders junction
621
+ * records via Anonymous Apex + REST API callouts.
622
+ *
623
+ * React (Site Container) sites intentionally hide SSO configuration in the Admin
624
+ * UI, so this must be done programmatically. The approach:
625
+ * 1. Query the AuthConfig record for the site (one per community).
626
+ * 2. Query AuthProvider records by DeveloperName.
627
+ * 3. Query SamlSsoConfig records by DeveloperName (for SAML providers).
628
+ * 4. Check existing AuthConfigProviders to avoid duplicates.
629
+ * 5. Create missing AuthConfigProviders via REST API (DML is not allowed on this object).
630
+ *
631
+ * AuthConfig.AuthOptionsAuthProvider auto-flips to true when AuthConfigProviders
632
+ * are inserted — no separate update needed.
633
+ *
634
+ * @param {string[]} authProviderNames - DeveloperNames of AuthProvider or SamlSsoConfig records
635
+ * @param {string} siteName - Name of the Network/community
636
+ * @param {string} targetOrg - Target org alias
637
+ */
638
+ function enableAuthProvidersForSite(authProviderNames, siteName, targetOrg) {
639
+ // DML is not allowed on AuthConfigProviders, so we use Anonymous Apex with
640
+ // HTTP callouts to the REST API to create the junction records.
641
+ // Whitelist each DeveloperName before it is interpolated into the generated
642
+ // Apex `DeveloperName IN (…)` literal — same call-site guard the data-import
643
+ // path applies with validateApiName, and defense-in-depth alongside the
644
+ // up-front socialLogin validation in main(). loadSocialLoginConfig already
645
+ // rejects an empty list, so the resulting literal is never empty here.
646
+ const providerNamesLiteral = authProviderNames
647
+ .map((n) => `'${validateSoqlName(n, 'socialLogin.authProviderNames entry')}'`)
648
+ .join(', ');
649
+
650
+ const apex = `
651
+ // Enable Auth Providers (OAuth + SAML) for community "${siteName}"
652
+ Http http = new Http();
653
+ String baseUrl = Url.getOrgDomainUrl().toExternalForm();
654
+ String token = UserInfo.getSessionId();
655
+
656
+ // Step 1: Find the AuthConfig for this site
657
+ List<AuthConfig> authConfigs = [
658
+ SELECT Id, Url
659
+ FROM AuthConfig
660
+ WHERE Url LIKE '%${siteName}%'
661
+ LIMIT 1
662
+ ];
663
+ if (authConfigs.isEmpty()) {
664
+ System.debug('ERROR_NO_AUTHCONFIG');
665
+ return;
666
+ }
667
+ AuthConfig ac = authConfigs[0];
668
+ System.debug('AUTHCONFIG_FOUND:' + ac.Id + ':' + ac.Url);
669
+
670
+ // Step 2: Query AuthProvider records by DeveloperName (OAuth providers)
671
+ List<AuthProvider> oauthProviders = [
672
+ SELECT Id, DeveloperName, FriendlyName
673
+ FROM AuthProvider
674
+ WHERE DeveloperName IN (${providerNamesLiteral})
675
+ ];
676
+ System.debug('OAUTH_PROVIDERS_FOUND:' + oauthProviders.size());
677
+
678
+ // Step 3: Query SamlSsoConfig records by DeveloperName (SAML providers)
679
+ List<SamlSsoConfig> samlProviders = [
680
+ SELECT Id, DeveloperName
681
+ FROM SamlSsoConfig
682
+ WHERE DeveloperName IN (${providerNamesLiteral})
683
+ ];
684
+ System.debug('SAML_PROVIDERS_FOUND:' + samlProviders.size());
685
+
686
+ // Combine both into a unified list with (Id, DeveloperName, Type)
687
+ List<Map<String, String>> allProviders = new List<Map<String, String>>();
688
+ for (AuthProvider ap : oauthProviders) {
689
+ allProviders.add(new Map<String, String>{
690
+ 'Id' => ap.Id,
691
+ 'DeveloperName' => ap.DeveloperName,
692
+ 'Type' => 'OAuth'
693
+ });
694
+ }
695
+ for (SamlSsoConfig sp : samlProviders) {
696
+ allProviders.add(new Map<String, String>{
697
+ 'Id' => sp.Id,
698
+ 'DeveloperName' => sp.DeveloperName,
699
+ 'Type' => 'SAML'
700
+ });
701
+ }
702
+
703
+ if (allProviders.isEmpty()) {
704
+ System.debug('ERROR_NO_PROVIDERS');
705
+ return;
706
+ }
707
+ System.debug('TOTAL_PROVIDERS_FOUND:' + allProviders.size());
708
+
709
+ // Step 4: Check existing AuthConfigProviders to avoid duplicates
710
+ Set<Id> existingProviderIds = new Set<Id>();
711
+ for (AuthConfigProviders acp : [
712
+ SELECT AuthProviderId FROM AuthConfigProviders WHERE AuthConfigId = :ac.Id
713
+ ]) {
714
+ existingProviderIds.add(acp.AuthProviderId);
715
+ }
716
+
717
+ // Step 5: Create missing junction records via REST API
718
+ Integer inserted = 0;
719
+ for (Map<String, String> provider : allProviders) {
720
+ String providerId = provider.get('Id');
721
+ String providerName = provider.get('DeveloperName');
722
+ String providerType = provider.get('Type');
723
+
724
+ if (existingProviderIds.contains(providerId)) {
725
+ System.debug('ALREADY_LINKED:' + providerName + ':' + providerType);
726
+ continue;
727
+ }
728
+ HttpRequest req = new HttpRequest();
729
+ req.setEndpoint(baseUrl + '/services/data/v62.0/sobjects/AuthConfigProviders');
730
+ req.setMethod('POST');
731
+ req.setHeader('Authorization', 'Bearer ' + token);
732
+ req.setHeader('Content-Type', 'application/json');
733
+ req.setBody('{"AuthConfigId":"' + ac.Id + '","AuthProviderId":"' + providerId + '"}');
734
+ HttpResponse res = http.send(req);
735
+ if (res.getStatusCode() == 201 || res.getStatusCode() == 200) {
736
+ System.debug('INSERTED:' + providerName + ':' + providerType);
737
+ inserted++;
738
+ } else {
739
+ System.debug('INSERT_FAILED:' + providerName + ':' + providerType + ':' + res.getStatusCode() + ':' + res.getBody());
740
+ }
741
+ }
742
+ if (inserted == 0 && existingProviderIds.size() >= allProviders.size()) {
743
+ System.debug('ALL_ALREADY_LINKED');
744
+ } else {
745
+ System.debug('TOTAL_INSERTED:' + inserted);
746
+ }
747
+ `;
748
+
749
+ const apexOut = withApexTempDir((writeApex) => {
750
+ const tmpApex = writeApex('auth-providers.apex', apex);
751
+ const result = spawnSync('sf', [
752
+ 'apex', 'run', '--target-org', targetOrg, '--file', tmpApex,
753
+ ], { cwd: ROOT, stdio: 'pipe', shell: true, timeout: 60000 });
754
+ const out = result.stdout?.toString() || '';
755
+ const err = result.stderr?.toString() || '';
756
+ if (result.status !== 0 && !out.includes('Compiled successfully')) {
757
+ process.stderr.write(err || out);
758
+ throw new StepError('failed to execute AuthConfigProviders apex');
759
+ }
760
+ return out;
761
+ });
762
+
763
+ // Parse output for status — match only |DEBUG| lines to avoid false positives
764
+ // from the "Execute Anonymous:" source echo that sf apex run always prints.
765
+ if (apexOut.match(/\|DEBUG\|ERROR_NO_AUTHCONFIG/)) {
766
+ throw new StepError(`no AuthConfig found for site "${siteName}" — ensure the site is published/active`);
767
+ }
768
+ if (apexOut.match(/\|DEBUG\|ERROR_NO_PROVIDERS/)) {
769
+ throw new StepError(`no AuthProvider or SamlSsoConfig records found for names: ${authProviderNames.join(', ')} — create them in Setup first`);
770
+ }
771
+
772
+ // Check for insert failures
773
+ const insertFails = [...apexOut.matchAll(/\|DEBUG\|INSERT_FAILED:([^:]+):(OAuth|SAML):(\d+):([^\n]+)/g)];
774
+ if (insertFails.length > 0) {
775
+ for (const m of insertFails) {
776
+ console.error(` ✖ ${m[1]} (${m[2]}) — HTTP ${m[3]}: ${m[4].trim()}`);
777
+ }
778
+ throw new StepError(`failed to link ${insertFails.length} Auth Provider(s) to site AuthConfig`);
779
+ }
780
+
781
+ const totalMatch = apexOut.match(/\|DEBUG\|TOTAL_INSERTED:(\d+)/);
782
+ if (totalMatch) {
783
+ console.log(` Linked ${totalMatch[1]} Auth Provider(s) to site "${siteName}".`);
784
+ } else if (apexOut.match(/\|DEBUG\|ALL_ALREADY_LINKED/)) {
785
+ console.log(` All Auth Provider(s) already linked to site "${siteName}"; no changes needed.`);
786
+ }
787
+
788
+ // Log individual provider statuses
789
+ const alreadyLinked = [...apexOut.matchAll(/\|DEBUG\|ALREADY_LINKED:([^:]+):(OAuth|SAML)/g)];
790
+ for (const m of alreadyLinked) {
791
+ console.log(` ✔ ${m[1]} (${m[2]}, already linked)`);
792
+ }
793
+ const inserted = [...apexOut.matchAll(/\|DEBUG\|INSERTED:([^:]+):(OAuth|SAML)/g)];
794
+ for (const m of inserted) {
795
+ console.log(` + ${m[1]} (${m[2]}, newly linked)`);
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Add a community user profile to the NetworkMemberGroup so that SSO-registered
801
+ * users (who get assigned this profile by the registration handler) can access
802
+ * the community.
803
+ *
804
+ * Without this, users created by the SSO registration handler get
805
+ * "NO_ACCESS: User was not authorized for the community".
806
+ *
807
+ * @param {string} profileName - Profile name to add (e.g. "Customer Community User")
808
+ * @param {string} siteName - Name of the Network/community
809
+ * @param {string} targetOrg - Target org alias
810
+ */
811
+ function addCommunityMemberProfile(profileName, siteName, targetOrg) {
812
+ // Query Network Id
813
+ const netQuery = `SELECT Id FROM Network WHERE Name = '${siteName}'`;
814
+ const netResult = spawnSync('sf', [
815
+ 'data', 'query', '--query', netQuery, '--target-org', targetOrg, '--json',
816
+ ], { cwd: ROOT, encoding: 'utf8' });
817
+ let networkId = null;
818
+ if (netResult.status === 0) {
819
+ try {
820
+ const json = JSON.parse(netResult.stdout);
821
+ networkId = json.result?.records?.[0]?.Id || null;
822
+ } catch { /* fall through */ }
823
+ }
824
+ if (!networkId) {
825
+ throw new StepError(`could not find Network "${siteName}" in org`);
826
+ }
827
+
828
+ // Query Profile Id
829
+ const profQuery = `SELECT Id FROM Profile WHERE Name = '${profileName}'`;
830
+ const profResult = spawnSync('sf', [
831
+ 'data', 'query', '--query', profQuery, '--target-org', targetOrg, '--json',
832
+ ], { cwd: ROOT, encoding: 'utf8' });
833
+ let profileId = null;
834
+ if (profResult.status === 0) {
835
+ try {
836
+ const json = JSON.parse(profResult.stdout);
837
+ profileId = json.result?.records?.[0]?.Id || null;
838
+ } catch { /* fall through */ }
839
+ }
840
+ if (!profileId) {
841
+ throw new StepError(`could not find Profile "${profileName}" in org`);
842
+ }
843
+
844
+ // Check if already a member
845
+ const memberQuery = `SELECT Id FROM NetworkMemberGroup WHERE NetworkId = '${networkId}' AND ParentId = '${profileId}'`;
846
+ const memberResult = spawnSync('sf', [
847
+ 'data', 'query', '--query', memberQuery, '--target-org', targetOrg, '--json',
848
+ ], { cwd: ROOT, encoding: 'utf8' });
849
+ let alreadyExists = false;
850
+ if (memberResult.status === 0) {
851
+ try {
852
+ const json = JSON.parse(memberResult.stdout);
853
+ alreadyExists = (json.result?.records?.length || 0) > 0;
854
+ } catch { /* proceed to create */ }
855
+ }
856
+ if (alreadyExists) {
857
+ console.log(` Profile "${profileName}" is already a member of community "${siteName}"; skipping.`);
858
+ return;
859
+ }
860
+
861
+ // Create NetworkMemberGroup record
862
+ const createResult = spawnSync('sf', [
863
+ 'data', 'create', 'record',
864
+ '--sobject', 'NetworkMemberGroup',
865
+ '--values', `NetworkId='${networkId}' ParentId='${profileId}'`,
866
+ '--target-org', targetOrg,
867
+ '--json',
868
+ ], { cwd: ROOT, encoding: 'utf8' });
869
+ if (createResult.status !== 0) {
870
+ const out = (createResult.stderr || '') + (createResult.stdout || '');
871
+ if (out) process.stderr.write(out);
872
+ throw new StepError(`failed to add profile "${profileName}" to NetworkMemberGroup for "${siteName}"`);
873
+ }
874
+ try {
875
+ const json = JSON.parse(createResult.stdout);
876
+ const id = json.result?.id;
877
+ console.log(` Added profile "${profileName}" to community "${siteName}" (NetworkMemberGroup: ${id}).`);
878
+ } catch {
879
+ console.log(` Added profile "${profileName}" to community "${siteName}".`);
880
+ }
881
+ }
882
+
883
+ /**
884
+ * Assign a permission set to all existing community users with a given profile.
885
+ *
886
+ * After SSO login, community users need the app's API-enabled permset so that
887
+ * getCurrentUser() (which calls /chatter/users/me via the /sf/api/ proxy) works.
888
+ * Without PermissionsApiEnabled the proxy returns API_DISABLED_FOR_ORG and the
889
+ * React app treats the user as unauthenticated.
890
+ *
891
+ * This is idempotent: already-assigned users are skipped.
892
+ */
893
+ function assignPermsetToCommunityUsers(permsetName, profileName, targetOrg) {
894
+ // Resolve PermissionSet Id
895
+ const psQuery = `SELECT Id FROM PermissionSet WHERE Name = '${permsetName}'`;
896
+ const psResult = spawnSync('sf', [
897
+ 'data', 'query', '--query', psQuery, '--target-org', targetOrg, '--json',
898
+ ], { cwd: ROOT, encoding: 'utf8' });
899
+ let permsetId = null;
900
+ if (psResult.status === 0) {
901
+ try {
902
+ const json = JSON.parse(psResult.stdout);
903
+ permsetId = json.result?.records?.[0]?.Id || null;
904
+ } catch { /* fall through */ }
905
+ }
906
+ if (!permsetId) {
907
+ throw new StepError(`could not find PermissionSet "${permsetName}" in org`);
908
+ }
909
+
910
+ // Find community users on the given profile who do NOT already have the permset
911
+ const usersQuery = [
912
+ `SELECT Id, Username FROM User`,
913
+ `WHERE Profile.Name = '${profileName}'`,
914
+ `AND IsActive = true`,
915
+ `AND UserType IN ('CspLitePortal', 'CustomerSuccess', 'PowerCustomerSuccess')`,
916
+ `AND Id NOT IN (SELECT AssigneeId FROM PermissionSetAssignment WHERE PermissionSetId = '${permsetId}')`,
917
+ ].join(' ');
918
+ const usersResult = spawnSync('sf', [
919
+ 'data', 'query', '--query', usersQuery, '--target-org', targetOrg, '--json',
920
+ ], { cwd: ROOT, encoding: 'utf8' });
921
+ let users = [];
922
+ if (usersResult.status === 0) {
923
+ try {
924
+ const json = JSON.parse(usersResult.stdout);
925
+ users = json.result?.records || [];
926
+ } catch { /* empty list */ }
927
+ }
928
+
929
+ if (users.length === 0) {
930
+ console.log(` No community users on profile "${profileName}" need permset "${permsetName}"; skipping.`);
931
+ return;
932
+ }
933
+
934
+ // Assign the permset to each user
935
+ let assigned = 0;
936
+ for (const user of users) {
937
+ const createResult = spawnSync('sf', [
938
+ 'data', 'create', 'record',
939
+ '--sobject', 'PermissionSetAssignment',
940
+ '--values', `PermissionSetId='${permsetId}' AssigneeId='${user.Id}'`,
941
+ '--target-org', targetOrg,
942
+ '--json',
943
+ ], { cwd: ROOT, encoding: 'utf8' });
944
+ if (createResult.status === 0) {
945
+ assigned++;
946
+ } else {
947
+ // Log but don't fail — user might already have it via race condition
948
+ const out = (createResult.stdout || '') + (createResult.stderr || '');
949
+ if (!out.includes('DUPLICATE_VALUE')) {
950
+ console.warn(` Warning: could not assign "${permsetName}" to ${user.Username}: ${out.slice(0, 120)}`);
951
+ }
952
+ }
953
+ }
954
+ console.log(` Assigned permset "${permsetName}" to ${assigned}/${users.length} community user(s).`);
955
+ }
956
+
524
957
  /**
525
958
  * Ensure the self-registration profile is listed in networkMemberGroups.
526
959
  * This must happen BEFORE the initial deploy so that the profile is a recognised
@@ -1319,6 +1752,7 @@ async function main() {
1319
1752
  skipPermset: argSkipPermset,
1320
1753
  skipRole: argSkipRole,
1321
1754
  skipSelfReg: argSkipSelfReg,
1755
+ skipSocialLogin: argSkipSocialLogin,
1322
1756
  skipData: argSkipData,
1323
1757
  skipGraphql: argSkipGraphql,
1324
1758
  skipUIBundleBuild: argSkipUIBundleBuild,
@@ -1352,6 +1786,8 @@ async function main() {
1352
1786
  const hasRoleConfig = roleConfig !== null;
1353
1787
  const selfRegConfig = loadSelfRegConfig(config);
1354
1788
  const hasSelfRegConfig = selfRegConfig !== null;
1789
+ const socialLoginConfig = loadSocialLoginConfig(config);
1790
+ const hasSocialLoginConfig = socialLoginConfig !== null;
1355
1791
 
1356
1792
  // Validate the selfRegProfile name for SOQL-safety up front, alongside the
1357
1793
  // config validation and BEFORE any org mutation (login/deploy). A quote /
@@ -1367,6 +1803,30 @@ async function main() {
1367
1803
  }
1368
1804
  }
1369
1805
 
1806
+ // Same fail-fast treatment for the socialLogin identifiers, alongside the
1807
+ // self-reg guard above and BEFORE any org mutation. communityMemberProfile,
1808
+ // every authProviderNames entry, and the optional communityUserPermset are all
1809
+ // interpolated into generated SOQL / CLI value strings — the Profile and
1810
+ // PermissionSet `WHERE Name = '…'` lookups, the AuthProvider/SamlSsoConfig
1811
+ // `DeveloperName IN (…)` list, and the NetworkMemberGroup/User queries. These
1812
+ // are developer-authored identifiers the app ships, so a quote / backslash /
1813
+ // control char is a config mistake to surface loudly here rather than a value
1814
+ // to silently escape (same rationale as validateSoqlName's self-reg use).
1815
+ if (hasSocialLoginConfig) {
1816
+ try {
1817
+ validateSoqlName(socialLoginConfig.communityMemberProfile, 'socialLogin.communityMemberProfile');
1818
+ for (const providerName of socialLoginConfig.authProviderNames ?? []) {
1819
+ validateSoqlName(providerName, 'socialLogin.authProviderNames entry');
1820
+ }
1821
+ if (socialLoginConfig.communityUserPermset) {
1822
+ validateSoqlName(socialLoginConfig.communityUserPermset, 'socialLogin.communityUserPermset');
1823
+ }
1824
+ } catch (err) {
1825
+ console.error(`Invalid org-setup.config.json:\n - ${err.message}`);
1826
+ process.exit(1);
1827
+ }
1828
+ }
1829
+
1370
1830
  // failFast is a fixed, implementer-owned classification — NOT
1371
1831
  // user-configurable. A fail-fast failure aborts the run immediately; a
1372
1832
  // skippable failure is recorded and the run continues. The exit code is
@@ -1381,6 +1841,7 @@ async function main() {
1381
1841
  { key: 'permset', label: permsetStepLabel, enabled: !argSkipPermset, available: true, failFast: false },
1382
1842
  { key: 'role', label: `Role — assign "${roleConfig?.roleName ?? '?'}" to current user`, enabled: !argSkipRole && hasRoleConfig, available: hasRoleConfig, failFast: false },
1383
1843
  { key: 'selfReg', label: 'Self-Registration — enable for site', enabled: !argSkipSelfReg && hasSelfRegConfig, available: hasSelfRegConfig, failFast: false },
1844
+ { key: 'socialLogin', label: 'Social Login — enable auth providers + community profile', enabled: !argSkipSocialLogin && hasSocialLoginConfig, available: hasSocialLoginConfig, failFast: false },
1384
1845
  { key: 'data', label: 'Data — delete + import records via Apex', enabled: !argSkipData && hasDataPlan, available: hasDataPlan, failFast: true },
1385
1846
  { key: 'graphql', label: 'GraphQL — schema introspect + codegen', enabled: !argSkipGraphql, available: true, failFast: true },
1386
1847
  ];
@@ -1396,6 +1857,7 @@ async function main() {
1396
1857
  const skipPermset = !on.permset;
1397
1858
  const skipRole = !on.role;
1398
1859
  const skipSelfReg = !on.selfReg;
1860
+ const skipSocialLogin = !on.socialLogin;
1399
1861
  const skipData = !on.data;
1400
1862
  const skipGraphql = !on.graphql;
1401
1863
 
@@ -1405,11 +1867,12 @@ async function main() {
1405
1867
 
1406
1868
  console.log('Setup — target org:', targetOrg, '| UI bundle:', uiBundleDir ?? '(none)');
1407
1869
  console.log(
1408
- 'Steps: login=always deploy=%s permset=%s role=%s selfReg=%s data=%s graphql=%s uiBundle=%s',
1870
+ 'Steps: login=always deploy=%s permset=%s role=%s selfReg=%s socialLogin=%s data=%s graphql=%s uiBundle=%s',
1409
1871
  !skipDeploy,
1410
1872
  !skipPermset,
1411
1873
  !skipRole,
1412
1874
  !skipSelfReg,
1875
+ !skipSocialLogin,
1413
1876
  doData,
1414
1877
  !skipGraphql,
1415
1878
  !skipUIBundleBuild
@@ -1622,6 +2085,53 @@ async function main() {
1622
2085
  recordSkipped(selfRegStep, selfRegStep.available ? 'not selected' : 'no config');
1623
2086
  }
1624
2087
 
2088
+ const socialLoginStep = stepDefs.find((s) => s.key === 'socialLogin');
2089
+ if (!skipSocialLogin) {
2090
+ console.log('\n--- Social Login — enable auth providers + community profile ---');
2091
+ await runStep(socialLoginStep, targetOrg, () => {
2092
+ const siteName = deriveSiteName();
2093
+ if (!siteName) {
2094
+ throw new StepError(
2095
+ 'socialLogin is configured but no networks/<siteName>.network-meta.xml was found to derive the site',
2096
+ );
2097
+ }
2098
+
2099
+ const totalSubSteps = socialLoginConfig.communityUserPermset ? 4 : 3;
2100
+
2101
+ // Sub-step 1: Enable "Allow using standard external profiles" org setting.
2102
+ // This is required so the SSO registration handler can create users with
2103
+ // standard community profiles (e.g. "Customer Community User").
2104
+ console.log(` [1/${totalSubSteps}] Enabling "Allow standard external profiles" setting...`);
2105
+ enableExternalProfiles(targetOrg);
2106
+
2107
+ // Sub-step 2: Link Auth Providers to the site's AuthConfig.
2108
+ // React (Site Container) sites hide SSO config in Admin UI, so this must
2109
+ // be done programmatically via AuthConfig/AuthConfigProviders records.
2110
+ console.log(` [2/${totalSubSteps}] Linking Auth Providers to site AuthConfig...`);
2111
+ enableAuthProvidersForSite(socialLoginConfig.authProviderNames, siteName, targetOrg);
2112
+
2113
+ // Sub-step 3: Add the community member profile to NetworkMemberGroup.
2114
+ // Without this, users created by the registration handler get
2115
+ // "NO_ACCESS: User was not authorized for the community".
2116
+ console.log(` [3/${totalSubSteps}] Adding community member profile to NetworkMemberGroup...`);
2117
+ addCommunityMemberProfile(socialLoginConfig.communityMemberProfile, siteName, targetOrg);
2118
+
2119
+ // Sub-step 4 (optional): Assign API-enabled permset to community users.
2120
+ // Without this, getCurrentUser() fails because /chatter/users/me requires
2121
+ // PermissionsApiEnabled which the standard community profile lacks.
2122
+ if (socialLoginConfig.communityUserPermset) {
2123
+ console.log(` [4/${totalSubSteps}] Assigning API permset to community users...`);
2124
+ assignPermsetToCommunityUsers(
2125
+ socialLoginConfig.communityUserPermset,
2126
+ socialLoginConfig.communityMemberProfile,
2127
+ targetOrg,
2128
+ );
2129
+ }
2130
+ });
2131
+ } else {
2132
+ recordSkipped(socialLoginStep, socialLoginStep.available ? 'not selected' : 'no config');
2133
+ }
2134
+
1625
2135
  const dataStep = stepDefs.find((s) => s.key === 'data');
1626
2136
  if (doData) {
1627
2137
  await runStep(dataStep, targetOrg, () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-feature-react-agentforce-conversation-client",
3
- "version": "11.47.1",
3
+ "version": "11.49.0",
4
4
  "description": "Embedded Agentforce conversation client feature for UI Bundles",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",
@@ -26,7 +26,7 @@
26
26
  "clean": "rm -rf dist"
27
27
  },
28
28
  "dependencies": {
29
- "@salesforce/agentforce-conversation-client": "^11.47.1"
29
+ "@salesforce/agentforce-conversation-client": "^11.49.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/react": "^19.2.7",