From a272d2bd18e5a069b286fe4d9793401f3b9ed105 Mon Sep 17 00:00:00 2001 From: Tarushi Date: Mon, 18 May 2026 14:04:15 +0530 Subject: [PATCH 1/5] chore: verify port status --- src/commands/ui-bundle/dev.ts | 70 ++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/src/commands/ui-bundle/dev.ts b/src/commands/ui-bundle/dev.ts index b8d4ec1..9ac69c2 100644 --- a/src/commands/ui-bundle/dev.ts +++ b/src/commands/ui-bundle/dev.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { randomUUID } from 'node:crypto'; import open from 'open'; import select from '@inquirer/select'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; @@ -103,44 +104,61 @@ export default class UiBundleDev extends SfCommand { } /** - * Check if a URL is reachable (returns true/false) - * Used to check if --url is already available before starting dev server + * Check the port status: is it available, or is a server already running there? + * If a server is running, verify its identity via the health check token. + * + * @returns 'available' if no server is running, 'verified' if the running server's + * token matches ours, 'unverified' if a server is running but cannot be confirmed. */ - private static async isUrlReachable(url: string): Promise { + private static async checkPortStatus(url: string): Promise<'available' | 'verified' | 'unverified'> { + const expectedToken = process.env.SF_LIVE_PREVIEW_TOKEN; + try { - const response = await fetch(url, { - method: 'HEAD', - signal: AbortSignal.timeout(3000), // 3 second timeout + const healthUrl = new URL(url); + healthUrl.searchParams.set('sfProxyHealthCheck', 'true'); + const response = await fetch(healthUrl.toString(), { + method: 'GET', + signal: AbortSignal.timeout(3000), }); - return response.ok; + if (!response.ok || !expectedToken) return 'unverified'; + const token = response.headers.get('X-Live-Preview-Token'); + return token === expectedToken ? 'verified' : 'unverified'; } catch { - return false; + return 'available'; } } /** - * Poll a URL until it is reachable or timeout. + * Poll a URL until our verified server is detected, or abort if a foreign server appears. + * Uses checkPortStatus on each iteration so every poll verifies identity via the + * X-Live-Preview-Token header — no race window between "is it up?" and "is it ours?". * - * @param url - URL to poll (HEAD request) + * @param url - URL to poll * @param timeoutMs - Max time to wait * @param intervalMs - Poll interval * @param start - Start timestamp (for recursion) - * @returns true if reachable within timeout + * @returns true if verified within timeout, false on timeout + * @throws SfError with name 'PortSquattingAbort' if a foreign server is detected */ - private static async pollUntilReachable( + private static async pollUntilVerified( url: string, timeoutMs: number, intervalMs = 500, start = Date.now() ): Promise { - if (await UiBundleDev.isUrlReachable(url)) { - return true; - } + const status = await UiBundleDev.checkPortStatus(url); + if (status === 'verified') return true; + // 'available' — server not up yet, keep polling + // 'unverified' — server may still be initializing (proxy plugin not ready), keep polling if (Date.now() - start >= timeoutMs) { + if (status === 'unverified') { + process.stderr.write(JSON.stringify({ error: 'PortSquattingAbort', port: url }) + '\n'); + throw new SfError('Aborted: unverified server on port.', 'PortSquattingAbort'); + } return false; } await new Promise((r) => setTimeout(r, intervalMs)); - return UiBundleDev.pollUntilReachable(url, timeoutMs, intervalMs, start); + return UiBundleDev.pollUntilVerified(url, timeoutMs, intervalMs, start); } /** @@ -175,6 +193,11 @@ export default class UiBundleDev extends SfCommand { // Logger respects SF_LOG_LEVEL environment variable this.logger = await Logger.child('UiBundleDev'); + // Ensure a live preview token exists — self-generate if the extension didn't provide one + if (!process.env.SF_LIVE_PREVIEW_TOKEN) { + process.env.SF_LIVE_PREVIEW_TOKEN = randomUUID(); + } + // Declare variables outside try block for catch block access let manifest: UiBundleManifest | null = null; let devServerUrl: string | null = null; @@ -282,12 +305,15 @@ export default class UiBundleDev extends SfCommand { ); } - // Check if URL is already reachable - const isReachable = await UiBundleDev.isUrlReachable(resolvedUrl); - if (isReachable) { + // Check port status: available, verified (our server), or unverified (foreign) + const portStatus = await UiBundleDev.checkPortStatus(resolvedUrl); + if (portStatus === 'verified') { devServerUrl = resolvedUrl; this.log(messages.getMessage('info.url-already-available', [resolvedUrl])); - this.logger.debug(`URL ${resolvedUrl} is reachable, skipping dev server startup`); + this.logger.debug(`URL ${resolvedUrl} is verified as our server, skipping dev server startup`); + } else if (portStatus === 'unverified') { + process.stderr.write(JSON.stringify({ error: 'PortSquattingAbort', port: resolvedUrl }) + '\n'); + throw new SfError('Aborted: unverified server on port.', 'PortSquattingAbort'); } else if (flags.url) { // User explicitly passed --url; assume server is already running at that URL // Fail immediately if unreachable (don't start dev server) @@ -343,8 +369,8 @@ export default class UiBundleDev extends SfCommand { this.devServerManager.start(); - // Poll until URL is reachable, or fail immediately on process error - const pollPromise = UiBundleDev.pollUntilReachable(resolvedUrl, 60_000); + // Poll until our server is verified, or fail on process error / port squatting + const pollPromise = UiBundleDev.pollUntilVerified(resolvedUrl, 60_000); const errorPromise = new Promise((_, reject) => { this.devServerManager!.once('error', (error: SfError | DevServerError) => { const devError = From 4457c46aa6dd173d7cad6198e63d71ba176a2417 Mon Sep 17 00:00:00 2001 From: Tarushi Date: Thu, 4 Jun 2026 18:19:01 +0530 Subject: [PATCH 2/5] test: add X-Live-Preview-Token to NUT test servers for port squatting --- test/commands/ui-bundle/devPort.nut.ts | 4 ++++ test/commands/ui-bundle/devWithUrl.nut.ts | 4 ++++ test/commands/ui-bundle/helpers/devServerUtils.ts | 14 +++++++++++++- .../ui-bundle/helpers/uiBundleProjectUtils.ts | 11 ++++++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/test/commands/ui-bundle/devPort.nut.ts b/test/commands/ui-bundle/devPort.nut.ts index b2736b6..812d466 100644 --- a/test/commands/ui-bundle/devPort.nut.ts +++ b/test/commands/ui-bundle/devPort.nut.ts @@ -57,6 +57,10 @@ describe('ui-bundle dev NUTs — Tier 2 port handling', function () { ); } + // Set a known token so test dev servers can respond with it on health checks, + // simulating the IDE extension → Vite plugin → CLI verification flow. + process.env.SF_LIVE_PREVIEW_TOKEN = 'test-token-port-nuts'; + session = await TestSession.create({ devhubAuthStrategy: 'NONE' }); targetOrg = authOrgViaUrl(); }); diff --git a/test/commands/ui-bundle/devWithUrl.nut.ts b/test/commands/ui-bundle/devWithUrl.nut.ts index 64a8833..68d5c4e 100644 --- a/test/commands/ui-bundle/devWithUrl.nut.ts +++ b/test/commands/ui-bundle/devWithUrl.nut.ts @@ -64,6 +64,10 @@ describe('ui-bundle dev NUTs — Tier 2 URL/proxy integration', function () { ); } + // Set a known token so test dev servers can respond with it on health checks, + // simulating the IDE extension → Vite plugin → CLI verification flow. + process.env.SF_LIVE_PREVIEW_TOKEN = 'test-token-url-nuts'; + session = await TestSession.create({ devhubAuthStrategy: 'NONE' }); targetOrg = authOrgViaUrl(); }); diff --git a/test/commands/ui-bundle/helpers/devServerUtils.ts b/test/commands/ui-bundle/helpers/devServerUtils.ts index a2a2e11..098d61f 100644 --- a/test/commands/ui-bundle/helpers/devServerUtils.ts +++ b/test/commands/ui-bundle/helpers/devServerUtils.ts @@ -164,10 +164,21 @@ export function occupyPort(port: number): Promise { /** * Start a plain HTTP server that serves static HTML content. * Used for proxy-only mode tests where the dev server is already running. + * Responds to ?sfProxyHealthCheck=true with X-Live-Preview-Token (mimics the + * Vite proxy plugin contract for port squatting verification). */ export function startTestHttpServer(port: number): Promise { return new Promise((resolve, reject) => { - const server = createHttpServer((_, res) => { + const server = createHttpServer((req, res) => { + const url = new URL(req.url ?? '/', `http://localhost:${port}`); + if (url.searchParams.get('sfProxyHealthCheck') === 'true') { + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'X-Live-Preview-Token': process.env.SF_LIVE_PREVIEW_TOKEN ?? '', + }); + res.end('OK'); + return; + } res.writeHead(200, { 'Content-Type': 'text/html' }); res.end('

Manual Dev Server

'); }); @@ -189,6 +200,7 @@ export function startViteProxyServer(port: number): Promise { res.writeHead(200, { 'Content-Type': 'text/plain', 'X-Salesforce-UiBundle-Proxy': 'true', + 'X-Live-Preview-Token': process.env.SF_LIVE_PREVIEW_TOKEN ?? '', }); res.end('OK'); return; diff --git a/test/commands/ui-bundle/helpers/uiBundleProjectUtils.ts b/test/commands/ui-bundle/helpers/uiBundleProjectUtils.ts index 0ae41db..caf98a2 100644 --- a/test/commands/ui-bundle/helpers/uiBundleProjectUtils.ts +++ b/test/commands/ui-bundle/helpers/uiBundleProjectUtils.ts @@ -143,7 +143,16 @@ export function writeManifest(projectDir: string, uiBundleName: string, manifest function createDevServerScript(uiBundleDir: string, port: number): string { const script = [ "const http = require('http');", - 'const server = http.createServer((_, res) => {', + 'const server = http.createServer((req, res) => {', + ` const url = new URL(req.url || '/', 'http://localhost:${port}');`, + " if (url.searchParams.get('sfProxyHealthCheck') === 'true') {", + ' res.writeHead(200, {', + " 'Content-Type': 'text/plain',", + " 'X-Live-Preview-Token': process.env.SF_LIVE_PREVIEW_TOKEN || '',", + ' });', + " res.end('OK');", + ' return;', + ' }', " res.writeHead(200, { 'Content-Type': 'text/html' });", " res.end('

Test Dev Server

');", '});', From fc797103bcc2bbb6e73ed7111b7697395b843124 Mon Sep 17 00:00:00 2001 From: ankitsinghkuntal09 Date: Mon, 8 Jun 2026 16:19:37 +0530 Subject: [PATCH 3/5] feat(port-squat): flag-gated backward-compat for token check @W-22838968 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes the version-gated approach (reading @salesforce/ui-bundle version from node_modules) — fragile across lockfile + Code Builder layouts. A server on port 5173 that responds OK but does NOT echo X-Live-Preview-Token is now classified as "legacy" (old webapp, or a passive squatter — indistinguishable to the CLI). Behavior under "legacy": - ALLOW_LEGACY_WEBAPPS_DEFAULT=true (current default): proceed, emit one loud warning per webapp per process (stderr structured JSON + SfCommand.warn()). - false / SF_UI_BUNDLE_ALLOW_LEGACY_WEBAPPS=false: abort with PortSquattingAbort. Once webapp adoption of the token-echoing release is high, flip the default to false; the liberal branch then becomes a one-line removal. Changes: - 4-state checkPortStatus: available | verified | legacy | foreign - classifyOccupiedPort centralizes the abort-vs-warn decision (shared by pre-flight one-shot and post-spawn polling) - emitLegacyWebappWarning emits {"warn":"LEGACY_WEBAPP_DETECTED",...} on stderr (consumed by the VS Code extension) + this.warn() to the terminal - Removed: MIN_TOKEN_SUPPORTED_VERSION, compareVersions, getWebappUiBundleVersion, isNewWebapp, isUrlReachable, pollUntilReachable, fs/path imports — all version-gating dead code Co-authored-by: Cursor --- src/commands/ui-bundle/dev.ts | 141 +++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 38 deletions(-) diff --git a/src/commands/ui-bundle/dev.ts b/src/commands/ui-bundle/dev.ts index 9ac69c2..3842c1c 100644 --- a/src/commands/ui-bundle/dev.ts +++ b/src/commands/ui-bundle/dev.ts @@ -29,6 +29,23 @@ import { discoverUiBundle, DEFAULT_DEV_COMMAND, type DiscoveredUiBundle } from ' Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-ui-bundle-dev', 'ui-bundle.dev'); +// Kill switch for backward compatibility. true = warn + allow webapps that +// don't echo X-Live-Preview-Token; false = strict, abort on missing token. +// Override at runtime with SF_UI_BUNDLE_ALLOW_LEGACY_WEBAPPS=false. Flip the +// default once webapp adoption is high; the liberal branch then becomes dead +// code and is a one-line removal. +const ALLOW_LEGACY_WEBAPPS_DEFAULT = true; + +function allowLegacyWebapps(): boolean { + const raw = process.env.SF_UI_BUNDLE_ALLOW_LEGACY_WEBAPPS; + if (raw == null) return ALLOW_LEGACY_WEBAPPS_DEFAULT; + const v = raw.trim().toLowerCase(); + return v !== 'false' && v !== '0' && v !== 'no'; +} + +type PortStatus = 'available' | 'verified' | 'legacy' | 'foreign'; +type PollResult = { mode: 'verified' | 'legacy' } | { mode: 'timeout' }; + export default class UiBundleDev extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); @@ -66,6 +83,8 @@ export default class UiBundleDev extends SfCommand { private devServerManager: DevServerManager | null = null; private proxyServer: ProxyServer | null = null; private logger: Logger | null = null; + /** Legacy-webapp warning fires once per CLI process. */ + private legacyWarningEmitted = false; /** * Open the proxy URL in the default browser @@ -104,13 +123,13 @@ export default class UiBundleDev extends SfCommand { } /** - * Check the port status: is it available, or is a server already running there? - * If a server is running, verify its identity via the health check token. - * - * @returns 'available' if no server is running, 'verified' if the running server's - * token matches ours, 'unverified' if a server is running but cannot be confirmed. + * Probe the dev-server URL and classify what's listening. + * Returns 'available' when no TCP response, 'verified' when the response + * echoes our X-Live-Preview-Token, 'legacy' on OK with no token header + * (old @salesforce/ui-bundle or a passive squatter; the caller decides via + * allowLegacyWebapps()), 'foreign' on non-OK status or token mismatch. */ - private static async checkPortStatus(url: string): Promise<'available' | 'verified' | 'unverified'> { + private static async checkPortStatus(url: string): Promise { const expectedToken = process.env.SF_LIVE_PREVIEW_TOKEN; try { @@ -120,42 +139,60 @@ export default class UiBundleDev extends SfCommand { method: 'GET', signal: AbortSignal.timeout(3000), }); - if (!response.ok || !expectedToken) return 'unverified'; + + if (!response.ok) return 'foreign'; + const token = response.headers.get('X-Live-Preview-Token'); - return token === expectedToken ? 'verified' : 'unverified'; + if (token == null) return 'legacy'; + if (expectedToken && token === expectedToken) return 'verified'; + return 'foreign'; } catch { return 'available'; } } /** - * Poll a URL until our verified server is detected, or abort if a foreign server appears. - * Uses checkPortStatus on each iteration so every poll verifies identity via the - * X-Live-Preview-Token header — no race window between "is it up?" and "is it ours?". - * - * @param url - URL to poll - * @param timeoutMs - Max time to wait - * @param intervalMs - Poll interval - * @param start - Start timestamp (for recursion) - * @returns true if verified within timeout, false on timeout - * @throws SfError with name 'PortSquattingAbort' if a foreign server is detected + * Resolve a non-'available' port status to an action: returns the effective + * mode ('verified' | 'legacy') or throws PortSquattingAbort for 'foreign' + * (always) and 'legacy' under strict mode. + */ + private static classifyOccupiedPort(status: 'verified' | 'legacy' | 'foreign', url: string): 'verified' | 'legacy' { + if (status === 'verified') return 'verified'; + if (status === 'legacy') { + if (allowLegacyWebapps()) return 'legacy'; + process.stderr.write( + JSON.stringify({ error: 'PortSquattingAbort', port: url, reason: 'strict-mode-legacy' }) + '\n' + ); + throw new SfError( + 'Aborted: server on port did not echo X-Live-Preview-Token and strict mode is enabled ' + + '(SF_UI_BUNDLE_ALLOW_LEGACY_WEBAPPS=false).', + 'PortSquattingAbort' + ); + } + // 'foreign' + process.stderr.write(JSON.stringify({ error: 'PortSquattingAbort', port: url, reason: 'foreign' }) + '\n'); + throw new SfError('Aborted: another server is on the port and failed identity verification.', 'PortSquattingAbort'); + } + + /** + * Poll the URL after spawn until it answers. Every poll re-runs identity + * verification via the X-Live-Preview-Token header (no "is it up vs. is it + * ours" race). Throws PortSquattingAbort on 'foreign' (always) or 'legacy' + * under strict mode; otherwise resolves with the effective mode or 'timeout'. */ private static async pollUntilVerified( url: string, timeoutMs: number, intervalMs = 500, start = Date.now() - ): Promise { + ): Promise { const status = await UiBundleDev.checkPortStatus(url); - if (status === 'verified') return true; - // 'available' — server not up yet, keep polling - // 'unverified' — server may still be initializing (proxy plugin not ready), keep polling + if (status !== 'available') { + const mode = UiBundleDev.classifyOccupiedPort(status, url); + return { mode }; + } if (Date.now() - start >= timeoutMs) { - if (status === 'unverified') { - process.stderr.write(JSON.stringify({ error: 'PortSquattingAbort', port: url }) + '\n'); - throw new SfError('Aborted: unverified server on port.', 'PortSquattingAbort'); - } - return false; + return { mode: 'timeout' }; } await new Promise((r) => setTimeout(r, intervalMs)); return UiBundleDev.pollUntilVerified(url, timeoutMs, intervalMs, start); @@ -305,15 +342,21 @@ export default class UiBundleDev extends SfCommand { ); } - // Check port status: available, verified (our server), or unverified (foreign) - const portStatus = await UiBundleDev.checkPortStatus(resolvedUrl); - if (portStatus === 'verified') { + // Pre-flight: classifyOccupiedPort throws on foreign / strict-mode legacy. + let portReachable = false; + const preFlightStatus = await UiBundleDev.checkPortStatus(resolvedUrl); + if (preFlightStatus !== 'available') { + const mode = UiBundleDev.classifyOccupiedPort(preFlightStatus, resolvedUrl); + portReachable = true; + if (mode === 'legacy') { + this.emitLegacyWebappWarning(resolvedUrl, selectedUiBundle.name); + } + } + + if (portReachable) { devServerUrl = resolvedUrl; this.log(messages.getMessage('info.url-already-available', [resolvedUrl])); - this.logger.debug(`URL ${resolvedUrl} is verified as our server, skipping dev server startup`); - } else if (portStatus === 'unverified') { - process.stderr.write(JSON.stringify({ error: 'PortSquattingAbort', port: resolvedUrl }) + '\n'); - throw new SfError('Aborted: unverified server on port.', 'PortSquattingAbort'); + this.logger.debug(`URL ${resolvedUrl} is already available, skipping dev server startup`); } else if (flags.url) { // User explicitly passed --url; assume server is already running at that URL // Fail immediately if unreachable (don't start dev server) @@ -369,9 +412,10 @@ export default class UiBundleDev extends SfCommand { this.devServerManager.start(); - // Poll until our server is verified, or fail on process error / port squatting + // pollUntilVerified throws on foreign / strict-mode legacy; otherwise + // resolves with { mode: 'verified' | 'legacy' | 'timeout' }. const pollPromise = UiBundleDev.pollUntilVerified(resolvedUrl, 60_000); - const errorPromise = new Promise((_, reject) => { + const errorPromise = new Promise((_, reject) => { this.devServerManager!.once('error', (error: SfError | DevServerError) => { const devError = 'devServerError' in error @@ -390,8 +434,11 @@ export default class UiBundleDev extends SfCommand { }); }); - const pollReached = await Promise.race([pollPromise, errorPromise]); - if (!pollReached) { + const pollResult = await Promise.race([pollPromise, errorPromise]); + if (pollResult.mode === 'legacy') { + this.emitLegacyWebappWarning(resolvedUrl, selectedUiBundle.name); + } + if (pollResult.mode === 'timeout') { // Timeout - capture context before cleanup nulls devServerManager const manager = this.devServerManager; const lastOutput = manager?.getLastOutput() ?? ''; @@ -642,6 +689,24 @@ export default class UiBundleDev extends SfCommand { } } + /** + * Emit the legacy-webapp warning on stderr (structured JSON for the VS Code + * extension) and via SfCommand.warn() (terminal/output channel). Idempotent. + */ + private emitLegacyWebappWarning(url: string, bundleName: string): void { + if (this.legacyWarningEmitted) return; + this.legacyWarningEmitted = true; + process.stderr.write( + JSON.stringify({ warn: 'LEGACY_WEBAPP_DETECTED', port: url, bundle: bundleName }) + '\n' + ); + this.warn( + `Legacy @salesforce/ui-bundle detected on ${url} for "${bundleName}". ` + + 'Live Preview is proceeding without token verification. ' + + 'Please update your webapp\'s @salesforce/ui-bundle dependency — ' + + 'strict mode will be enforced in a future release.' + ); + } + /** * Cleanup all resources (proxy, dev server, file watcher) */ From 140d330b678feba60cd7cbe2ad93cb16a017223b Mon Sep 17 00:00:00 2001 From: ankitsinghkuntal09 Date: Wed, 10 Jun 2026 12:01:30 +0530 Subject: [PATCH 4/5] feat(port-squat): tighten legacy warning message @W-22838968 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace verbose mechanism-leaking warning with a concise, actionable one. Per code review feedback — keep it minimal, no implementation details. --- src/commands/ui-bundle/dev.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/commands/ui-bundle/dev.ts b/src/commands/ui-bundle/dev.ts index 3842c1c..c05c0ac 100644 --- a/src/commands/ui-bundle/dev.ts +++ b/src/commands/ui-bundle/dev.ts @@ -700,10 +700,8 @@ export default class UiBundleDev extends SfCommand { JSON.stringify({ warn: 'LEGACY_WEBAPP_DETECTED', port: url, bundle: bundleName }) + '\n' ); this.warn( - `Legacy @salesforce/ui-bundle detected on ${url} for "${bundleName}". ` + - 'Live Preview is proceeding without token verification. ' + - 'Please update your webapp\'s @salesforce/ui-bundle dependency — ' + - 'strict mode will be enforced in a future release.' + `Outdated @salesforce/ui-bundle detected for "${bundleName}". ` + + 'Update the dependency — older versions will be unsupported in a future release.' ); } From b69c52494a8512fe9aed1ea66f1bf36de6b7707f Mon Sep 17 00:00:00 2001 From: ankitsinghkuntal09 Date: Wed, 10 Jun 2026 12:36:45 +0530 Subject: [PATCH 5/5] feat(port-squat): switch legacy warning to authenticity wording @W-22838968 Per code review (round 2): drop 'outdated dependency' framing and adopt authenticity-verification wording. Honest about what the CLI can actually assert (couldn't verify the server) without naming the verification mechanism. Mirrored on the VS Code extension side. Co-authored-by: Cursor --- src/commands/ui-bundle/dev.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/ui-bundle/dev.ts b/src/commands/ui-bundle/dev.ts index c05c0ac..cbd3f00 100644 --- a/src/commands/ui-bundle/dev.ts +++ b/src/commands/ui-bundle/dev.ts @@ -700,8 +700,8 @@ export default class UiBundleDev extends SfCommand { JSON.stringify({ warn: 'LEGACY_WEBAPP_DETECTED', port: url, bundle: bundleName }) + '\n' ); this.warn( - `Outdated @salesforce/ui-bundle detected for "${bundleName}". ` + - 'Update the dependency — older versions will be unsupported in a future release.' + `Unable to verify the authenticity of the dev server for "${bundleName}". ` + + 'Update @salesforce/ui-bundle to the latest version.' ); }