@salesforce/ui-bundle-template-feature-react-agentforce-conversation-client 11.47.0 → 11.48.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 +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-agentforce-conversation-client/package.json +4 -4
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/dist/scripts/org-setup-config-schema.mjs +17 -0
- package/dist/scripts/org-setup.mjs +511 -1
- package/package.json +2 -2
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.48.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.47.1...v11.48.0) (2026-08-05)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **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))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [11.47.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.47.0...v11.47.1) (2026-08-05)
|
|
16
|
+
|
|
17
|
+
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
## [11.47.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.46.1...v11.47.0) (2026-08-04)
|
|
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.
|
|
22
|
-
"@salesforce/ui-bundle": "^11.
|
|
21
|
+
"@salesforce/platform-sdk": "^11.48.0",
|
|
22
|
+
"@salesforce/ui-bundle": "^11.48.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.
|
|
49
|
-
"@salesforce/vite-plugin-ui-bundle": "^11.
|
|
48
|
+
"@salesforce/graphiti": "^11.48.0",
|
|
49
|
+
"@salesforce/vite-plugin-ui-bundle": "^11.48.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",
|
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.48.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.
|
|
9
|
+
"version": "11.48.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
|
@@ -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.
|
|
3
|
+
"version": "11.48.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.
|
|
29
|
+
"@salesforce/agentforce-conversation-client": "^11.48.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/react": "^19.2.7",
|