-
Notifications
You must be signed in to change notification settings - Fork 190
Expand file tree
/
Copy pathbuildSalesforceUrl.ts
More file actions
108 lines (90 loc) · 3.56 KB
/
Copy pathbuildSalesforceUrl.ts
File metadata and controls
108 lines (90 loc) · 3.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { createSign } from 'node:crypto'
import {
getSfLwcClientId,
getSfLwcInstanceUrl,
getSfLwcJwtPrivateKey,
getSfLwcUsername,
} from '../../../../scripts/lib/secrets.ts'
export type SalesforceApp = 'lwc' | 'experience-cloud' | 'experience-cloud-headmarkup'
const salesforceHomePath = '/lightning/app/c__SF_LWC_App/page/home'
const experienceSitePaths: Record<Exclude<SalesforceApp, 'lwc'>, string> = {
'experience-cloud': '/sfexperiencecloud/',
'experience-cloud-headmarkup': '/sfexperienceheadmarkup/',
}
let salesforceLwcSession: Promise<SalesforceLwcSession> | undefined
export interface SalesforceLwcSession {
instanceUrl: string
accessToken: string
}
export async function buildSalesforceUrl(app: SalesforceApp): Promise<string> {
return app === 'lwc' ? await buildSalesforceLwcUrl() : buildSalesforceExperienceUrl(app)
}
export function getSalesforceLwcSession(): Promise<SalesforceLwcSession> {
salesforceLwcSession ??= buildSalesforceLwcJwtSession()
return salesforceLwcSession
}
async function buildSalesforceLwcUrl(): Promise<string> {
const { instanceUrl } = await getSalesforceLwcSession()
return new URL(salesforceHomePath, instanceUrl).href
}
async function buildSalesforceLwcJwtSession(): Promise<SalesforceLwcSession> {
const clientId = getSfLwcClientId()
const instanceUrl = getSfLwcInstanceUrl()
const jwtPrivateKey = getSfLwcJwtPrivateKey()
const username = getSfLwcUsername()
if (!clientId || !instanceUrl || !jwtPrivateKey || !username) {
throw new Error('Salesforce credentials are not set')
}
const privateKey = Buffer.from(jwtPrivateKey, 'base64').toString('utf8')
const accessToken = await getAccessToken(clientId, username, instanceUrl, privateKey)
return { instanceUrl, accessToken }
}
async function getAccessToken(
clientId: string,
username: string,
instanceUrl: string,
privateKey: string
): Promise<string> {
const header = Buffer.from(JSON.stringify({ alg: 'RS256' })).toString('base64url')
const payload = Buffer.from(
JSON.stringify({
iss: clientId,
sub: username,
aud: instanceUrl,
// JWT expiry is set to 3 minutes, enough to complete the token exchange.
exp: Math.floor(Date.now() / 1000) + 180,
})
).toString('base64url')
const sign = createSign('RSA-SHA256')
sign.update(`${header}.${payload}`)
const jwt = `${header}.${payload}.${sign.sign(privateKey, 'base64url')}`
const response = await fetch(`${instanceUrl}/services/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwt,
}),
})
if (!response.ok) {
throw new Error(`Salesforce token request failed (${response.status}): ${await response.text()}`)
}
const json = (await response.json()) as Record<string, string>
const accessToken = json['access_token']
if (!accessToken) {
throw new Error('Salesforce token response missing access_token')
}
return accessToken
}
// Unlike the Lightning app, the Experience Cloud site is public, so we don't need to
// authenticate or exchange a frontdoor token: we can derive the site URL directly from the
// org's instance URL.
function buildSalesforceExperienceUrl(app: Exclude<SalesforceApp, 'lwc'>): string {
const instanceUrl = getSfLwcInstanceUrl()
if (!instanceUrl) {
console.error('Salesforce credentials are not set')
return ''
}
const siteDomain = instanceUrl.replace('.my.salesforce.com', '.my.site.com')
return `${siteDomain}${experienceSitePaths[app]}`
}