Skip to content

Commit

Permalink
feat: save identity data to sf's config and to the server (#89)
Browse files Browse the repository at this point in the history
* feat: save identity data to sf's config and to the server

* Update messages/shared.utils.md

Co-authored-by: Kevin Hawkins <[email protected]>

* chore: update after code review

* chore: fix tests

* chore: touch up after rebase

* chore: refactor usage of connection object. fix more tests

* chore: remove interface

* chore: nit

* chore: nit

---------

Co-authored-by: Kevin Hawkins <[email protected]>
  • Loading branch information
sfdctaka and khawkins authored Jul 12, 2024
1 parent 40c57a8 commit f435624
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 83 deletions.
12 changes: 12 additions & 0 deletions messages/lightning.preview.app.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ Type of device to emulate in preview.

For mobile virtual devices, specify the device ID to preview. If omitted, the first available virtual device will be used.

# error.username

Org must have a valid user

# error.identitydata

Couldn't find identity data while generating preview arguments

# error.identitydata.entityid

Couldn't find entity ID while generating preview arguments

# error.no-project

This command is required to run from within a Salesforce project directory. %s
Expand Down
4 changes: 2 additions & 2 deletions messages/shared.utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ The workspace name of the local lwc dev server

Valid workspace value is "SalesforceCLI" OR "mrt"

# config-utils.token-desc
# config-utils.data-desc

The Base64-encoded identity token of the local web server
The identity data is a data structure that links the local web server's identity token to the user's configured Salesforce orgs.

# config-utils.cert-desc

Expand Down
60 changes: 53 additions & 7 deletions src/commands/lightning/preview/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import path from 'node:path';
import * as readline from 'node:readline';
import { Logger, Messages, SfProject } from '@salesforce/core';
import { Connection, Logger, Messages, SfProject } from '@salesforce/core';
import {
AndroidAppPreviewConfig,
AndroidVirtualDevice,
Expand All @@ -21,6 +21,7 @@ import chalk from 'chalk';
import { OrgUtils } from '../../../shared/orgUtils.js';
import { startLWCServer } from '../../../lwc-dev-server/index.js';
import { PreviewUtils } from '../../../shared/previewUtils.js';
import { ConfigUtils, IdentityTokenService } from '../../../shared/configUtils.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.preview.app');
Expand All @@ -38,6 +39,22 @@ export const androidSalesforceAppPreviewConfig = {

const maxInt32 = 2_147_483_647; // maximum 32-bit signed integer value

class AppServerIdentityTokenService implements IdentityTokenService {
private connection: Connection;
public constructor(connection: Connection) {
this.connection = connection;
}

public async saveTokenToServer(token: string): Promise<string> {
const sobject = this.connection.sobject('UserLocalWebServerIdentity');
const result = await sobject.insert({ LocalWebServerIdentityToken: token });
if (result.success) {
return result.id;
}
throw new Error('Could not save the token to the server');
}
}

export default class LightningPreviewApp extends SfCommand<void> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
Expand Down Expand Up @@ -157,13 +174,23 @@ export default class LightningPreviewApp extends SfCommand<void> {
return Promise.reject(new Error(messages.getMessage('error.no-project', [(error as Error)?.message ?? ''])));
}

logger.debug('Configuring local web server identity');
const connection = targetOrg.getConnection(undefined);
const username = connection.getUsername();
if (!username) {
return Promise.reject(new Error(messages.getMessage('error.username')));
}

const tokenService = new AppServerIdentityTokenService(connection);
const token = await ConfigUtils.getOrCreateIdentityToken(username, tokenService);

let appId: string | undefined;
if (appName) {
logger.debug(`Determining App Id for ${appName}`);

// The appName is optional but if the user did provide an appName then it must be
// a valid one.... meaning that it should resolve to a valid appId.
appId = await OrgUtils.getAppId(targetOrg.getConnection(undefined), appName);
appId = await OrgUtils.getAppId(connection, appName);
if (!appId) {
return Promise.reject(new Error(messages.getMessage('error.fetching.app-id', [appName])));
}
Expand All @@ -179,13 +206,17 @@ export default class LightningPreviewApp extends SfCommand<void> {
const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, serverPort, logger);
logger.debug(`Local Dev Server url is ${ldpServerUrl}`);

const entityId = await PreviewUtils.getEntityId(username);

if (platform === Platform.desktop) {
await this.desktopPreview(sfdxProjectRootPath, serverPort, ldpServerUrl, appId, logger);
await this.desktopPreview(sfdxProjectRootPath, serverPort, token, entityId, ldpServerUrl, appId, logger);
} else {
await this.mobilePreview(
platform,
sfdxProjectRootPath,
serverPort,
token,
entityId,
ldpServerUrl,
appName,
appId,
Expand All @@ -198,6 +229,8 @@ export default class LightningPreviewApp extends SfCommand<void> {
private async desktopPreview(
sfdxProjectRootPath: string,
serverPort: number,
token: string,
entityId: string,
ldpServerUrl: string,
appId: string | undefined,
logger: Logger
Expand Down Expand Up @@ -230,10 +263,15 @@ export default class LightningPreviewApp extends SfCommand<void> {
this.log(`\n${messages.getMessage('trust.local.dev.server')}`);
}

const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments(ldpServerUrl, appId, targetOrg);
const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments(
ldpServerUrl,
entityId,
appId,
targetOrg
);

// Start the LWC Dev Server
await startLWCServer(logger, sfdxProjectRootPath, serverPort);
await startLWCServer(logger, sfdxProjectRootPath, token, serverPort);

// Open the browser and navigate to the right page
await this.config.runCommand('org:open', launchArguments);
Expand All @@ -243,6 +281,8 @@ export default class LightningPreviewApp extends SfCommand<void> {
platform: Platform.ios | Platform.android,
sfdxProjectRootPath: string,
serverPort: number,
token: string,
entityId: string,
ldpServerUrl: string,
appName: string | undefined,
appId: string | undefined,
Expand Down Expand Up @@ -316,11 +356,17 @@ export default class LightningPreviewApp extends SfCommand<void> {
}

// Start the LWC Dev Server
await startLWCServer(logger, sfdxProjectRootPath, serverPort, certData);

await startLWCServer(logger, sfdxProjectRootPath, token, serverPort, certData);

// Launch the native app for previewing (launchMobileApp will show its own spinner)
// eslint-disable-next-line camelcase
appConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments(ldpServerUrl, appName, appId);
appConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments(
ldpServerUrl,
entityId,
appName,
appId
);
await PreviewUtils.launchMobileApp(platform, appConfig, resolvedDeviceId, emulatorPort, bundlePath, logger);
} finally {
// stop progress & spinner UX (that may still be running in case of an error)
Expand Down
12 changes: 6 additions & 6 deletions src/configMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ConfigPropertyMeta, ConfigValue, Messages } from '@salesforce/core';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils');
const IDENTITY_TOKEN_DESC = messages.getMessage('config-utils.token-desc');
const IDENTITY_DATA_DESC = messages.getMessage('config-utils.data-desc');
const LOCAL_DEV_SERVER_CERT_DESC = messages.getMessage('config-utils.cert-desc');
const LOCAL_DEV_SERVER_CERT_ERROR_MESSAGE = messages.getMessage('config-utils.cert-error-message');
const LOCAL_DEV_SERVER_PORT_DESC = messages.getMessage('config-utils.port-desc');
Expand All @@ -27,10 +27,10 @@ export type SerializedSSLCertificateData = {

export const enum ConfigVars {
/**
* The Base64-encoded identity token of the local web server, used to
* validate the web server's identity to the hmr-client.
* The identity data is a data structure that links the local web server's
* identity token to the user's configured Salesforce orgs.
*/
LOCAL_WEB_SERVER_IDENTITY_TOKEN = 'local-web-server-identity-token',
LOCAL_WEB_SERVER_IDENTITY_DATA = 'local-web-server-identity-data',

/**
* The SSL certificate data to be used by local dev server
Expand All @@ -50,8 +50,8 @@ export const enum ConfigVars {

export default [
{
key: ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN,
description: IDENTITY_TOKEN_DESC,
key: ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA,
description: IDENTITY_DATA_DESC,
hidden: true,
encrypted: true,
},
Expand Down
12 changes: 6 additions & 6 deletions src/lwc-dev-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ function mapLogLevel(cliLogLevel: number): number {
async function createLWCServerConfig(
logger: Logger,
rootDir: string,
token: string,
serverPort?: number,
certData?: SSLCertificateData,
workspace?: Workspace,
token?: string
workspace?: Workspace
): Promise<ServerConfig> {
const sfdxConfig = path.resolve(rootDir, 'sfdx-project.json');

Expand Down Expand Up @@ -83,7 +83,7 @@ async function createLWCServerConfig(
paths: namespacePaths,
// use custom workspace if any is provided, or fetch from config file (if any), otherwise use the default workspace
workspace: workspace ?? (await ConfigUtils.getLocalDevServerWorkspace()) ?? LOCAL_DEV_SERVER_DEFAULT_WORKSPACE,
identityToken: token ?? (await ConfigUtils.getOrCreateIdentityToken()),
identityToken: token,
logLevel: mapLogLevel(logger.getLevel()),
};

Expand All @@ -100,12 +100,12 @@ async function createLWCServerConfig(
export async function startLWCServer(
logger: Logger,
rootDir: string,
token: string,
serverPort?: number,
certData?: SSLCertificateData,
workspace?: Workspace,
token?: string
workspace?: Workspace
): Promise<LWCServer> {
const config = await createLWCServerConfig(logger, rootDir, serverPort, certData, workspace, token);
const config = await createLWCServerConfig(logger, rootDir, token, serverPort, certData, workspace);

logger.trace(`Starting LWC Dev Server with config: ${JSON.stringify(config)}`);
let lwcDevServer: LWCServer | null = await startLwcDevServer(config);
Expand Down
82 changes: 55 additions & 27 deletions src/shared/configUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,29 @@ import { CryptoUtils, SSLCertificateData } from '@salesforce/lwc-dev-mobile-core
import { Config, ConfigAggregator } from '@salesforce/core';
import configMeta, { ConfigVars, SerializedSSLCertificateData } from './../configMeta.js';

export type IdentityTokenService = {
saveTokenToServer(token: string): Promise<string>;
};

export const LOCAL_DEV_SERVER_DEFAULT_PORT = 8081;
export const LOCAL_DEV_SERVER_DEFAULT_WORKSPACE = Workspace.SfCli;

export type LocalWebServerIdentityData = {
identityToken: string;
usernameToServerEntityIdMap: Record<string, string>;
};

export class ConfigUtils {
static #config: Config;
static #localConfig: Config;
static #globalConfig: Config;

public static async getConfig(): Promise<Config> {
if (this.#config) {
return this.#config;
public static async getLocalConfig(): Promise<Config> {
if (this.#localConfig) {
return this.#localConfig;
}
this.#config = await Config.create({ isGlobal: false });
this.#localConfig = await Config.create({ isGlobal: false });
Config.addAllowedProperties(configMeta);
return this.#config;
return this.#localConfig;
}

public static async getGlobalConfig(): Promise<Config> {
Expand All @@ -35,32 +44,39 @@ export class ConfigUtils {
return this.#globalConfig;
}

public static async getOrCreateIdentityToken(): Promise<string> {
let token = await this.getIdentityToken();
if (!token) {
token = CryptoUtils.generateIdentityToken();
await this.writeIdentityToken(token);
public static async getOrCreateIdentityToken(username: string, tokenService: IdentityTokenService): Promise<string> {
let identityData = await this.getIdentityData();
if (!identityData) {
const token = CryptoUtils.generateIdentityToken();
const entityId = await tokenService.saveTokenToServer(token);
identityData = {
identityToken: token,
usernameToServerEntityIdMap: {},
};
identityData.usernameToServerEntityIdMap[username] = entityId;
await this.writeIdentityData(identityData);
return token;
} else {
let entityId = identityData.usernameToServerEntityIdMap[username];
if (!entityId) {
entityId = await tokenService.saveTokenToServer(identityData.identityToken);
identityData.usernameToServerEntityIdMap[username] = entityId;
await this.writeIdentityData(identityData);
}
return identityData.identityToken;
}
return token;
}

public static async getIdentityToken(): Promise<string | undefined> {
const config = await ConfigAggregator.create({ customConfigMeta: configMeta });
// Need to reload to make sure the values read are decrypted
await config.reload();
const identityToken = config.getPropertyValue(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN);

return identityToken as string;
}

public static async writeIdentityToken(token: string): Promise<void> {
const config = await this.getConfig();
config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN, token);
public static async writeIdentityData(identityData: LocalWebServerIdentityData): Promise<void> {
const config = await this.getLocalConfig();
// TODO: JSON needs to be stringified in order for config.write to encrypt. When config.write()
// can encrypt JSON data to write it into config we shall remove stringify().
config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA, JSON.stringify(identityData));
await config.write();
}

public static async getCertData(): Promise<SSLCertificateData | undefined> {
const config = await this.getGlobalConfig();
const config = await this.getLocalConfig();
const serializedData = config.get(ConfigVars.LOCAL_DEV_SERVER_HTTPS_CERT_DATA) as SerializedSSLCertificateData;
if (serializedData) {
const deserializedData: SSLCertificateData = {
Expand Down Expand Up @@ -89,16 +105,28 @@ export class ConfigUtils {
}

public static async getLocalDevServerPort(): Promise<number | undefined> {
const config = await this.getConfig();
const config = await this.getLocalConfig();
const configPort = config.get(ConfigVars.LOCAL_DEV_SERVER_PORT) as number;

return configPort;
}

public static async getLocalDevServerWorkspace(): Promise<Workspace | undefined> {
const config = await this.getConfig();
const config = await this.getLocalConfig();
const configWorkspace = config.get(ConfigVars.LOCAL_DEV_SERVER_WORKSPACE) as Workspace;

return configWorkspace;
}

public static async getIdentityData(): Promise<LocalWebServerIdentityData | undefined> {
const config = await ConfigAggregator.create({ customConfigMeta: configMeta });
// Need to reload to make sure the values read are decrypted
await config.reload();
const identityJson = config.getPropertyValue(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA);

if (identityJson) {
return JSON.parse(identityJson as string) as LocalWebServerIdentityData;
}
return undefined;
}
}
Loading

0 comments on commit f435624

Please sign in to comment.