Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@salesforce/sf-plugins-core": "^11.2.4",
"@inquirer/select": "^2.4.7",
"@inquirer/prompts": "^5.3.8",
"axios": "^1.7.7",
"chalk": "^5.3.0",
"lwc": "7.1.3",
"lwr": "0.14.3",
Expand Down
4 changes: 2 additions & 2 deletions src/commands/lightning/dev/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export default class LightningDevSite extends SfCommand<void> {
}
}

// Pass the org auth token so LWR can make authenticated requests to core
const authToken = org.getConnection().accessToken ?? '';
// Establish a valid access token for this site
const authToken = await selectedSite.setupAuth();

// Start the dev server
await expDev({
Expand Down
116 changes: 116 additions & 0 deletions src/shared/experience/expSite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { Org, SfError } from '@salesforce/core';
import axios from 'axios';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we used axios over native node fetch (just curious).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really, it was just a little easier to get working initially. Axios has some nice extra features that help with debugging like interceptors. But yeah I could switch over to using fetch I guess


export type SiteMetadata = {
bundleName: string;
Expand Down Expand Up @@ -94,6 +95,28 @@ export class ExperienceSite {
return experienceSites;
}

/**
* Esablish a valid token for this local development session
*
* @returns sid token for proxied site requests
*/
public async setupAuth(): Promise<string> {
let sidToken = ''; // Default to guest user access only

// Use environment variable for now if users want to just have guest access only
if (process.env.SITE_GUEST_ACCESS !== 'true') {
try {
const networkId = await this.getNetworkId();
sidToken = await this.getNewSidToken(networkId);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to establish authentication for site', e);
}
}

return sidToken;
}

public async isUpdateAvailable(): Promise<boolean> {
const localMetadata = this.getLocalMetadata();
if (!localMetadata) {
Expand Down Expand Up @@ -225,6 +248,99 @@ export class ExperienceSite {

return resourcePath;
}

private async getNetworkId(): Promise<string> {
const conn = this.org.getConnection();
// Query the Network object for the network with the given site name
const result = await conn.query<{ Id: string }>(`SELECT Id FROM Network WHERE Name = '${this.siteDisplayName}'`);

const record = result.records[0];
if (record) {
let networkId = record.Id;
// Subtract the last three characters from the Network ID
networkId = networkId.substring(0, networkId.length - 3);
return networkId;
} else {
throw new Error(`NetworkId for site: '${this.siteDisplayName}' could not be found`);
}
}

private async getNewSidToken(networkId: string): Promise<string> {
// Get the connection and access token from the org
const conn = this.org.getConnection();
const orgId = this.org.getOrgId();

// Not sure if we need to do this
const orgIdMinus3 = orgId.substring(0, orgId.length - 3);
const accessToken = conn.accessToken;
const instanceUrl = conn.instanceUrl; // Org URL

// Make the GET request without following redirects
if (accessToken) {
// TODO should we try and refresh auth here?
// await conn.refreshAuth();

// Call out to the switcher servlet to establish a session
const switchUrl = `${instanceUrl}/servlet/networks/switch?networkId=${networkId}`;
const cookies = [`sid=${accessToken}`, `oid=${orgIdMinus3}`].join('; ').trim();
let response = await axios.get(switchUrl, {
headers: {
Cookie: cookies,
},
withCredentials: true,
maxRedirects: 0, // Prevent axios from following redirects
validateStatus: (status) => status >= 200 && status < 400, // Accept 3xx status codes
});

// Extract the Location callback header
const locationHeader = response.headers['location'] as string;
if (locationHeader) {
// Parse the URL to extract the 'sid' parameter
const urlObj = new URL(locationHeader);
const sid = urlObj.searchParams.get('sid') ?? '';
const cookies2 = ['__Secure-has-sid=1', `sid=${sid}`, `oid=${orgIdMinus3}`].join('; ').trim();

// Request the location header to establish our session with the servlet
response = await axios.get(urlObj.toString(), {
headers: {
Cookie: cookies2,
},
withCredentials: true,
maxRedirects: 0, // Prevent axios from following redirects
validateStatus: (status) => status >= 200 && status < 400, // Accept 3xx status codes
});
const setCookieHeader = response.headers['set-cookie'];
if (setCookieHeader) {
// Find the 'sid' cookie in the set-cookie header
const sidCookie = setCookieHeader.find((cookieStr: string) => cookieStr.startsWith('sid='));
if (sidCookie) {
// Extract the sid value from the set-cookie string
const sidMatch = sidCookie.match(/sid=([^;]+)/);
if (sidMatch?.[1]) {
const sidToken = sidMatch[1];
return sidToken;
}
}
}
}

// if we can't establish a valid session this way, lets just warn the user and utilize the guest user context for the site
// eslint-disable-next-line no-console
console.warn(
`Warning: could not establish valid auth token for your site '${this.siteDisplayName}'.` +
'Local Dev proxied requests to your site may fail or return data from the guest user context.'
);

return ''; // Site will be guest user access only
}

// Not sure what scenarios we don't have an access token at all, but lets output a separate message here so we can distinguish these edge cases
// eslint-disable-next-line no-console
console.warn(
'Warning: sf cli org connection missing accessToken. Local Dev proxied requests to your site may fail or return data from the guest user context.'
);
return '';
}
}

/**
Expand Down
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6126,6 +6126,15 @@ axe-core@^4.6.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59"
integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==

axios@^1.7.7:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"

axobject-query@^3.1.1:
version "3.2.4"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.4.tgz#6dfba930294ea14d7d2fc68b9d007211baedb94c"
Expand Down Expand Up @@ -8695,6 +8704,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==

follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==

for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
Expand Down Expand Up @@ -12991,6 +13005,11 @@ proxy-addr@~2.0.7:
forwarded "0.2.0"
ipaddr.js "1.9.1"

proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==

psl@^1.1.33:
version "1.9.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
Expand Down