diff --git a/package.json b/package.json index 91aa99e8..256df665 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@oclif/core": "^3.26.6", "@salesforce/core": "^7.3.9", "@salesforce/kit": "^3.1.2", - "@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.3", + "@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.4", "@salesforce/sf-plugins-core": "^9.1.1", "@inquirer/select": "^2.3.5", "chalk": "^5.3.0", @@ -58,6 +58,7 @@ "oclif": { "commands": "./lib/commands", "bin": "sf", + "configMeta": "./lib/configMeta", "topicSeparator": " ", "devPlugins": [ "@oclif/plugin-help", diff --git a/src/configMeta.ts b/src/configMeta.ts new file mode 100644 index 00000000..a0015ea3 --- /dev/null +++ b/src/configMeta.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import type { ConfigPropertyMeta } from '@salesforce/core'; + +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. + */ + LOCAL_WEB_SERVER_IDENTITY_TOKEN = 'local-web-server-identity-token', +} + +export default [ + { + key: ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN, + description: 'The Base64-encoded identity token of the local web server', + hidden: true, + encrypted: true, + }, +] as ConfigPropertyMeta[]; diff --git a/src/shared/identityUtils.ts b/src/shared/identityUtils.ts new file mode 100644 index 00000000..45c37edd --- /dev/null +++ b/src/shared/identityUtils.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { CryptoUtils } from '@salesforce/lwc-dev-mobile-core'; +import { Config, ConfigAggregator } from '@salesforce/core'; +import configMeta, { ConfigVars } from './../configMeta.js'; + +export class IdentityUtils { + public static async getOrCreateIdentityToken(): Promise { + let token = await this.getIdentityToken(); + if (!token) { + token = CryptoUtils.generateIdentityToken(); + await this.writeIdentityToken(token); + } + return token; + } + + public static async getIdentityToken(): Promise { + 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 { + const config = await Config.create({ isGlobal: false }); + Config.addAllowedProperties(configMeta); + config.set(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN, token); + await config.write(); + } +} diff --git a/test/shared/identityUtils.test.ts b/test/shared/identityUtils.test.ts new file mode 100644 index 00000000..6ca502ff --- /dev/null +++ b/test/shared/identityUtils.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +import { expect } from 'chai'; +import { Config, ConfigAggregator } from '@salesforce/core'; +import { TestContext } from '@salesforce/core/testSetup'; +import { CryptoUtils } from '@salesforce/lwc-dev-mobile-core'; +import { IdentityUtils } from '../../src/shared/identityUtils.js'; +import { ConfigVars } from '../../src/configMeta.js'; + +describe('identityUtils', () => { + const $$ = new TestContext(); + + afterEach(() => { + $$.restore(); + }); + + it('getOrCreateIdentityToken resolves if token is found', async () => { + const fakeIdentityToken = 'fake identity token'; + $$.SANDBOX.stub(IdentityUtils, 'getIdentityToken').resolves(fakeIdentityToken); + + const resolved = await IdentityUtils.getOrCreateIdentityToken(); + expect(resolved).to.equal(fakeIdentityToken); + }); + + it('getOrCreateIdentityToken resolves and writeIdentityToken is called when there is no token', async () => { + const fakeIdentityToken = 'fake identity token'; + $$.SANDBOX.stub(IdentityUtils, 'getIdentityToken').resolves(undefined); + $$.SANDBOX.stub(CryptoUtils, 'generateIdentityToken').resolves(fakeIdentityToken); + const writeIdentityTokenStub = $$.SANDBOX.stub(IdentityUtils, 'writeIdentityToken').resolves(); + + const resolved = await IdentityUtils.getOrCreateIdentityToken(); + expect(resolved).to.equal(fakeIdentityToken); + expect(writeIdentityTokenStub.calledOnce).to.be.true; + }); + + it('getIdentityToken resolves to undefined when identity token is not available', async () => { + $$.SANDBOX.stub(ConfigAggregator, 'create').resolves(ConfigAggregator.prototype); + $$.SANDBOX.stub(ConfigAggregator.prototype, 'reload').resolves(); + $$.SANDBOX.stub(ConfigAggregator.prototype, 'getPropertyValue').returns(undefined); + const resolved = await IdentityUtils.getIdentityToken(); + + expect(resolved).to.equal(undefined); + }); + + it('getIdentityToken resolves to a string when identity token is available', async () => { + const fakeIdentityToken = 'fake identity token'; + $$.SANDBOX.stub(ConfigAggregator, 'create').resolves(ConfigAggregator.prototype); + $$.SANDBOX.stub(ConfigAggregator.prototype, 'reload').resolves(); + $$.SANDBOX.stub(ConfigAggregator.prototype, 'getPropertyValue').returns(fakeIdentityToken); + + const resolved = await IdentityUtils.getIdentityToken(); + expect(resolved).to.equal(fakeIdentityToken); + }); + + it('writeIdentityToken resolves', async () => { + const fakeIdentityToken = 'fake identity token'; + $$.SANDBOX.stub(Config, 'create').withArgs($$.SANDBOX.match.any).resolves(Config.prototype); + $$.SANDBOX.stub(Config, 'addAllowedProperties').withArgs($$.SANDBOX.match.any); + $$.SANDBOX.stub(Config.prototype, 'set').withArgs( + ConfigVars.LOCAL_WEB_SERVER_IDENTITY_TOKEN, + $$.SANDBOX.match.string + ); + $$.SANDBOX.stub(Config.prototype, 'write').resolves(); + + const resolved = await IdentityUtils.writeIdentityToken(fakeIdentityToken); + expect(resolved).to.equal(undefined); + }); +}); diff --git a/yarn.lock b/yarn.lock index 455cd7ce..e22b8854 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4304,10 +4304,10 @@ "@salesforce/ts-types" "^2.0.9" tslib "^2.6.3" -"@salesforce/lwc-dev-mobile-core@4.0.0-alpha.3": - version "4.0.0-alpha.3" - resolved "https://registry.yarnpkg.com/@salesforce/lwc-dev-mobile-core/-/lwc-dev-mobile-core-4.0.0-alpha.3.tgz#7485689c78e97b53ba88ce714db11bbeacfde192" - integrity sha512-7o7n6tuTshqwmfym2vy89IvMB8rKUcqDL7mg/nQ77wP4OxAjtj+mQzamcLz1nQU7QI4529RbkBvXe/2R0zbXBA== +"@salesforce/lwc-dev-mobile-core@4.0.0-alpha.4": + version "4.0.0-alpha.4" + resolved "https://registry.yarnpkg.com/@salesforce/lwc-dev-mobile-core/-/lwc-dev-mobile-core-4.0.0-alpha.4.tgz#5e020cb3222f2603a01248359d43b3d6ca4062e3" + integrity sha512-gNEQQr4QIIdXgGmSr6820Y2/HR7YqwxBjf0Z/PKr/iNoQ24Agc1QGMfUysc5H7/Bmx69GPF9wq7ahbNjSvB+Kg== dependencies: "@oclif/core" "^3.26.6" "@salesforce/core" "^7.3.6"