diff --git a/packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts b/packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts index 0eb3f3879afa..901e52af1968 100644 --- a/packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts +++ b/packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts @@ -5,12 +5,11 @@ import os from 'os' import { agent } from '@packages/network' import { PUBLIC_KEY_VERSION } from '../../constants' import { createWriteStream } from 'fs' -import { verifySignatureFromFile } from '../../encryption' const pkg = require('@packages/root') const _delay = linearDelay(500) -export const getCyPromptBundle = async ({ cyPromptUrl, projectId, bundlePath }: { cyPromptUrl: string, projectId?: string, bundlePath: string }) => { +export const getCyPromptBundle = async ({ cyPromptUrl, projectId, bundlePath }: { cyPromptUrl: string, projectId?: string, bundlePath: string }): Promise => { let responseSignature: string | null = null await (asyncRetry(async () => { @@ -56,9 +55,5 @@ export const getCyPromptBundle = async ({ cyPromptUrl, projectId, bundlePath }: throw new Error('Unable to get cy-prompt signature') } - const verified = await verifySignatureFromFile(bundlePath, responseSignature) - - if (!verified) { - throw new Error('Unable to verify cy-prompt signature') - } + return responseSignature } diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts index 2809e815d7f0..bd0e15502399 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts @@ -13,11 +13,11 @@ import { ensureCyPromptBundle } from './ensure_cy_prompt_bundle' import chokidar from 'chokidar' import { getCloudMetadata } from '../get_cloud_metadata' import type { CyPromptAuthenticatedUserShape } from '@packages/types' - +import crypto from 'crypto' const debug = Debug('cypress:server:cy-prompt-lifecycle-manager') export class CyPromptLifecycleManager { - private static hashLoadingMap: Map> = new Map() + private static hashLoadingMap: Map>> = new Map() private static watcher: chokidar.FSWatcher | null = null private cyPromptManagerPromise?: Promise<{ cyPromptManager?: CyPromptManager @@ -124,6 +124,7 @@ export class CyPromptLifecycleManager { }): Promise<{ cyPromptManager?: CyPromptManager, error?: Error }> { let cyPromptHash: string let cyPromptPath: string + let manifest: Record const currentProjectOptions = await getProjectOptions() const projectId = currentProjectOptions.projectSlug @@ -148,15 +149,27 @@ export class CyPromptLifecycleManager { CyPromptLifecycleManager.hashLoadingMap.set(cyPromptHash, hashLoadingPromise) } - await hashLoadingPromise + manifest = await hashLoadingPromise } else { cyPromptPath = process.env.CYPRESS_LOCAL_CY_PROMPT_PATH cyPromptHash = 'local' + manifest = {} } const serverFilePath = path.join(cyPromptPath, 'server', 'index.js') const script = await readFile(serverFilePath, 'utf8') + const expectedHash = manifest[path.join('server', 'index.js')] + + // TODO: once the services have deployed, we should remove this check + if (expectedHash) { + const actualHash = crypto.createHash('sha256').update(script).digest('hex') + + if (!process.env.CYPRESS_LOCAL_CY_PROMPT_PATH && actualHash !== expectedHash) { + throw new Error('Invalid hash for cy prompt server script') + } + } + const cyPromptManager = new CyPromptManager() const { cloudUrl } = await getCloudMetadata(cloudDataSource) @@ -172,6 +185,7 @@ export class CyPromptLifecycleManager { asyncRetry, }, getProjectOptions, + manifest, }) debug('cy prompt is ready') diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts index 7b72d466f7a7..89d786275bbf 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts @@ -3,6 +3,7 @@ import type { Router } from 'express' import Debug from 'debug' import { requireScript } from '../require_script' import type { Socket } from 'socket.io' +import crypto, { BinaryLike } from 'crypto' interface CyPromptServer { default: CyPromptServerDefaultShape } @@ -18,6 +19,7 @@ interface SetupOptions { record?: boolean key?: string }> + manifest: Record } const debug = Debug('cypress:server:cy-prompt') @@ -26,7 +28,7 @@ export class CyPromptManager implements CyPromptManagerShape { status: CyPromptStatus = 'NOT_INITIALIZED' private _cyPromptServer: CyPromptServerShape | undefined - async setup ({ script, cyPromptPath, cyPromptHash, getProjectOptions, cloudApi }: SetupOptions): Promise { + async setup ({ script, cyPromptPath, cyPromptHash, getProjectOptions, cloudApi, manifest }: SetupOptions): Promise { const { createCyPromptServer } = requireScript(script).default this._cyPromptServer = await createCyPromptServer({ @@ -34,6 +36,18 @@ export class CyPromptManager implements CyPromptManagerShape { cyPromptPath, cloudApi, getProjectOptions, + manifest, + verifyHash: (contents: BinaryLike, expectedHash: string) => { + // If we are running locally, we don't need to verify the signature. This + // environment variable will get stripped in the binary. + if (process.env.CYPRESS_LOCAL_CY_PROMPT_PATH) { + return true + } + + const actualHash = crypto.createHash('sha256').update(contents).digest('hex') + + return actualHash === expectedHash + }, }) this.status = 'INITIALIZED' diff --git a/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts b/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts index 8691fbea863d..339d40d8fd21 100644 --- a/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts +++ b/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts @@ -1,8 +1,9 @@ -import { remove, ensureDir } from 'fs-extra' +import { remove, ensureDir, readFile } from 'fs-extra' import tar from 'tar' import { getCyPromptBundle } from '../api/cy-prompt/get_cy_prompt_bundle' import path from 'path' +import { verifySignature } from '../encryption' const DOWNLOAD_TIMEOUT = 30000 @@ -21,7 +22,7 @@ interface EnsureCyPromptBundleOptions { * @param options.projectId - The project ID of the cy prompt bundle * @param options.downloadTimeoutMs - The timeout for the cy prompt bundle download */ -export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectId, downloadTimeoutMs = DOWNLOAD_TIMEOUT }: EnsureCyPromptBundleOptions) => { +export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectId, downloadTimeoutMs = DOWNLOAD_TIMEOUT }: EnsureCyPromptBundleOptions): Promise> => { const bundlePath = path.join(cyPromptPath, 'bundle.tar') // First remove cyPromptPath to ensure we have a clean slate @@ -30,7 +31,7 @@ export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectI let timeoutId: NodeJS.Timeout - await Promise.race([ + const responseSignature = await Promise.race([ getCyPromptBundle({ cyPromptUrl, projectId, @@ -43,10 +44,21 @@ export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectI }), ]).finally(() => { clearTimeout(timeoutId) - }) + }) as string await tar.extract({ file: bundlePath, cwd: cyPromptPath, }) + + const manifestPath = path.join(cyPromptPath, 'manifest.json') + const manifestContents = await readFile(manifestPath, 'utf8') + + const verified = await verifySignature(manifestContents, responseSignature) + + if (!verified) { + throw new Error('Unable to verify cy-prompt signature') + } + + return JSON.parse(manifestContents) } diff --git a/packages/server/lib/cloud/encryption.ts b/packages/server/lib/cloud/encryption.ts index b8fc892788be..2b762d0c6b1a 100644 --- a/packages/server/lib/cloud/encryption.ts +++ b/packages/server/lib/cloud/encryption.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto' +import crypto, { BinaryLike } from 'crypto' import { TextEncoder, promisify } from 'util' import { generalDecrypt, GeneralJWE } from 'jose' import base64Url from 'base64url' @@ -37,7 +37,7 @@ export interface EncryptRequestData { secretKey: crypto.KeyObject } -export function verifySignature (body: string, signature: string, publicKey?: crypto.KeyObject) { +export function verifySignature (body: BinaryLike, signature: string, publicKey?: crypto.KeyObject) { const verify = crypto.createVerify('SHA256') verify.update(body) diff --git a/packages/server/test/unit/cloud/api/cy-prompt/get_cy_prompt_bundle_spec.ts b/packages/server/test/unit/cloud/api/cy-prompt/get_cy_prompt_bundle_spec.ts index 35fe19437b07..e4d92c1885ae 100644 --- a/packages/server/test/unit/cloud/api/cy-prompt/get_cy_prompt_bundle_spec.ts +++ b/packages/server/test/unit/cloud/api/cy-prompt/get_cy_prompt_bundle_spec.ts @@ -7,13 +7,11 @@ describe('getCyPromptBundle', () => { let readStream: Readable let createWriteStreamStub: sinon.SinonStub let crossFetchStub: sinon.SinonStub - let verifySignatureFromFileStub: sinon.SinonStub let getCyPromptBundle: typeof import('../../../../../lib/cloud/api/cy-prompt/get_cy_prompt_bundle').getCyPromptBundle beforeEach(() => { createWriteStreamStub = sinon.stub() crossFetchStub = sinon.stub() - verifySignatureFromFileStub = sinon.stub() readStream = Readable.from('console.log("cy-prompt script")') writeResult = '' @@ -31,9 +29,6 @@ describe('getCyPromptBundle', () => { createWriteStream: createWriteStreamStub, }, 'cross-fetch': crossFetchStub, - '../../encryption': { - verifySignatureFromFile: verifySignatureFromFileStub, - }, 'os': { platform: () => 'linux', }, @@ -57,11 +52,9 @@ describe('getCyPromptBundle', () => { }, }) - verifySignatureFromFileStub.resolves(true) - const projectId = '12345' - await getCyPromptBundle({ cyPromptUrl: 'http://localhost:1234/cy-prompt/bundle/abc.tgz', projectId, bundlePath: '/tmp/cypress/cy-prompt/abc/bundle.tar' }) + const responseSignature = await getCyPromptBundle({ cyPromptUrl: 'http://localhost:1234/cy-prompt/bundle/abc.tgz', projectId, bundlePath: '/tmp/cypress/cy-prompt/abc/bundle.tar' }) expect(crossFetchStub).to.be.calledWith('http://localhost:1234/cy-prompt/bundle/abc.tgz', { agent: sinon.match.any, @@ -79,7 +72,7 @@ describe('getCyPromptBundle', () => { expect(writeResult).to.eq('console.log("cy-prompt script")') - expect(verifySignatureFromFileStub).to.be.calledWith('/tmp/cypress/cy-prompt/abc/bundle.tar', '159') + expect(responseSignature).to.eq('159') }) it('downloads the cy-prompt bundle and extracts it after 1 fetch failure', async () => { @@ -97,11 +90,9 @@ describe('getCyPromptBundle', () => { }, }) - verifySignatureFromFileStub.resolves(true) - const projectId = '12345' - await getCyPromptBundle({ cyPromptUrl: 'http://localhost:1234/cy-prompt/bundle/abc.tgz', projectId, bundlePath: '/tmp/cypress/cy-prompt/abc/bundle.tar' }) + const responseSignature = await getCyPromptBundle({ cyPromptUrl: 'http://localhost:1234/cy-prompt/bundle/abc.tgz', projectId, bundlePath: '/tmp/cypress/cy-prompt/abc/bundle.tar' }) expect(crossFetchStub).to.be.calledWith('http://localhost:1234/cy-prompt/bundle/abc.tgz', { agent: sinon.match.any, @@ -119,7 +110,7 @@ describe('getCyPromptBundle', () => { expect(writeResult).to.eq('console.log("cy-prompt script")') - expect(verifySignatureFromFileStub).to.be.calledWith('/tmp/cypress/cy-prompt/abc/bundle.tar', '159') + expect(responseSignature).to.eq('159') }) it('throws an error and returns a cy-prompt manager in error state if the fetch fails more than twice', async () => { @@ -172,47 +163,6 @@ describe('getCyPromptBundle', () => { }) }) - it('throws an error and returns a cy-prompt manager in error state if the signature verification fails', async () => { - verifySignatureFromFileStub.resolves(false) - - crossFetchStub.resolves({ - ok: true, - statusText: 'OK', - body: readStream, - headers: { - get: (header) => { - if (header === 'x-cypress-signature') { - return '159' - } - }, - }, - }) - - verifySignatureFromFileStub.resolves(false) - - const projectId = '12345' - - await expect(getCyPromptBundle({ cyPromptUrl: 'http://localhost:1234/cy-prompt/bundle/abc.tgz', projectId, bundlePath: '/tmp/cypress/cy-prompt/abc/bundle.tar' })).to.be.rejected - - expect(writeResult).to.eq('console.log("cy-prompt script")') - - expect(crossFetchStub).to.be.calledWith('http://localhost:1234/cy-prompt/bundle/abc.tgz', { - agent: sinon.match.any, - method: 'GET', - headers: { - 'x-route-version': '1', - 'x-cypress-signature': '1', - 'x-cypress-project-slug': '12345', - 'x-cypress-cy-prompt-mount-version': '1', - 'x-os-name': 'linux', - 'x-cypress-version': '1.2.3', - }, - encrypt: 'signed', - }) - - expect(verifySignatureFromFileStub).to.be.calledWith('/tmp/cypress/cy-prompt/abc/bundle.tar', '159') - }) - it('throws an error if there is no signature in the response headers', async () => { crossFetchStub.resolves({ ok: true, diff --git a/packages/server/test/unit/cloud/cy-prompt/CyPromptLifecycleManager_spec.ts b/packages/server/test/unit/cloud/cy-prompt/CyPromptLifecycleManager_spec.ts index 091d9fd0081b..e86bed83ad75 100644 --- a/packages/server/test/unit/cloud/cy-prompt/CyPromptLifecycleManager_spec.ts +++ b/packages/server/test/unit/cloud/cy-prompt/CyPromptLifecycleManager_spec.ts @@ -24,6 +24,7 @@ describe('CyPromptLifecycleManager', () => { let watcherStub: sinon.SinonStub = sinon.stub() let watcherOnStub: sinon.SinonStub = sinon.stub() let watcherCloseStub: sinon.SinonStub = sinon.stub() + const mockContents: string = 'console.log("cy-prompt script")' beforeEach(() => { postCyPromptSessionStub = sinon.stub() @@ -51,7 +52,7 @@ describe('CyPromptLifecycleManager', () => { }, }, 'fs-extra': { - readFile: readFileStub.resolves('console.log("cy-prompt script")'), + readFile: readFileStub.resolves(mockContents), }, 'chokidar': { watch: watcherStub.returns({ @@ -122,6 +123,12 @@ describe('CyPromptLifecycleManager', () => { }) }) + const mockManifest = { + 'server/index.js': 'c3c4ab913ca059819549f105e756a4c4471df19abef884ce85eafc7b7970e7b4', + } + + ensureCyPromptBundleStub.resolves(mockManifest) + await cyPromptReadyPromise expect(mockCtx.update).to.be.calledOnce @@ -142,6 +149,7 @@ describe('CyPromptLifecycleManager', () => { asyncRetry, }, getProjectOptions: sinon.match.func, + manifest: mockManifest, }) expect(postCyPromptSessionStub).to.be.calledWith({ @@ -167,6 +175,12 @@ describe('CyPromptLifecycleManager', () => { }) }) + const mockManifest = { + 'server/index.js': 'c3c4ab913ca059819549f105e756a4c4471df19abef884ce85eafc7b7970e7b4', + } + + ensureCyPromptBundleStub.resolves(mockManifest) + const cyPromptManager1 = await cyPromptReadyPromise1 cyPromptLifecycleManager.initializeCyPromptManager({ @@ -205,6 +219,7 @@ describe('CyPromptLifecycleManager', () => { asyncRetry, }, getProjectOptions: sinon.match.func, + manifest: mockManifest, }) expect(postCyPromptSessionStub).to.be.calledWith({ @@ -248,6 +263,7 @@ describe('CyPromptLifecycleManager', () => { asyncRetry, }, getProjectOptions: sinon.match.func, + manifest: {}, }) expect(postCyPromptSessionStub).to.be.calledWith({ @@ -304,6 +320,14 @@ describe('CyPromptLifecycleManager', () => { }) describe('registerCyPromptReadyListener', () => { + beforeEach(() => { + const mockManifest = { + 'server/index.js': 'c3c4ab913ca059819549f105e756a4c4471df19abef884ce85eafc7b7970e7b4', + } + + ensureCyPromptBundleStub.resolves(mockManifest) + }) + it('registers a listener that will be called when cy-prompt is ready', () => { const listener = sinon.stub() diff --git a/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts b/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts index b56720b915ae..7e52fe117be3 100644 --- a/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts +++ b/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts @@ -31,6 +31,9 @@ describe('lib/cloud/cy-prompt', () => { cyPromptHash: 'abcdefg', projectSlug: '1234', cloudApi: {} as any, + manifest: { + 'server/index.js': 'abcdefg', + }, getProjectOptions: () => { return Promise.resolve({ user: { diff --git a/packages/server/test/unit/cloud/cy-prompt/ensure_cy_prompt_bundle_spec.ts b/packages/server/test/unit/cloud/cy-prompt/ensure_cy_prompt_bundle_spec.ts index cba1100c9dfa..2b254b9bd0a0 100644 --- a/packages/server/test/unit/cloud/cy-prompt/ensure_cy_prompt_bundle_spec.ts +++ b/packages/server/test/unit/cloud/cy-prompt/ensure_cy_prompt_bundle_spec.ts @@ -9,12 +9,20 @@ describe('ensureCyPromptBundle', () => { let ensureStub: sinon.SinonStub = sinon.stub() let extractStub: sinon.SinonStub = sinon.stub() let getCyPromptBundleStub: sinon.SinonStub = sinon.stub() + let readFileStub: sinon.SinonStub = sinon.stub() + let verifySignatureStub: sinon.SinonStub = sinon.stub() + const mockResponseSignature = '159' + const mockManifest = { + 'server/index.js': 'abcdefg', + } beforeEach(() => { rmStub = sinon.stub() ensureStub = sinon.stub() extractStub = sinon.stub() getCyPromptBundleStub = sinon.stub() + readFileStub = sinon.stub() + verifySignatureStub = sinon.stub() ensureCyPromptBundle = (proxyquire('../lib/cloud/cy-prompt/ensure_cy_prompt_bundle', { os: { @@ -24,12 +32,16 @@ describe('ensureCyPromptBundle', () => { 'fs-extra': { remove: rmStub.resolves(), ensureDir: ensureStub.resolves(), + readFile: readFileStub.resolves(JSON.stringify(mockManifest)), }, tar: { extract: extractStub.resolves(), }, '../api/cy-prompt/get_cy_prompt_bundle': { - getCyPromptBundle: getCyPromptBundleStub.resolves(), + getCyPromptBundle: getCyPromptBundleStub.resolves(mockResponseSignature), + }, + '../encryption': { + verifySignature: verifySignatureStub.resolves(true), }, })).ensureCyPromptBundle }) @@ -38,7 +50,7 @@ describe('ensureCyPromptBundle', () => { const cyPromptPath = path.join(os.tmpdir(), 'cypress', 'cy-prompt', '123') const bundlePath = path.join(cyPromptPath, 'bundle.tar') - await ensureCyPromptBundle({ + const manifest = await ensureCyPromptBundle({ cyPromptPath, cyPromptUrl: 'https://cypress.io/cy-prompt', projectId: '123', @@ -46,6 +58,7 @@ describe('ensureCyPromptBundle', () => { expect(rmStub).to.be.calledWith(cyPromptPath) expect(ensureStub).to.be.calledWith(cyPromptPath) + expect(readFileStub).to.be.calledWith(path.join(cyPromptPath, 'manifest.json'), 'utf8') expect(getCyPromptBundleStub).to.be.calledWith({ cyPromptUrl: 'https://cypress.io/cy-prompt', projectId: '123', @@ -56,6 +69,22 @@ describe('ensureCyPromptBundle', () => { file: bundlePath, cwd: cyPromptPath, }) + + expect(verifySignatureStub).to.be.calledWith(JSON.stringify(mockManifest), mockResponseSignature) + + expect(manifest).to.deep.eq(mockManifest) + }) + + it('should throw an error if the cy prompt bundle signature is invalid', async () => { + verifySignatureStub.resolves(false) + + const ensureCyPromptBundlePromise = ensureCyPromptBundle({ + cyPromptPath: '/tmp/cypress/cy-prompt/123', + cyPromptUrl: 'https://cypress.io/cy-prompt', + projectId: '123', + }) + + await expect(ensureCyPromptBundlePromise).to.be.rejectedWith('Unable to verify cy-prompt signature') }) it('should throw an error if the cy prompt bundle download times out', async () => { diff --git a/packages/types/src/cy-prompt/cy-prompt-server-types.ts b/packages/types/src/cy-prompt/cy-prompt-server-types.ts index e2aeb7d752ed..e9a4d4e22cc1 100644 --- a/packages/types/src/cy-prompt/cy-prompt-server-types.ts +++ b/packages/types/src/cy-prompt/cy-prompt-server-types.ts @@ -8,6 +8,7 @@ import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.d' import type { Router } from 'express' import type { AxiosInstance } from 'axios' import type { Socket } from 'socket.io' +import type { BinaryLike } from 'crypto' export type CyPromptCommands = ProtocolMapping.Commands @@ -57,6 +58,8 @@ export interface CyPromptServerOptions { cyPromptPath: string projectSlug?: string cloudApi: CyPromptCloudApi + manifest: Record + verifyHash: (contents: BinaryLike, expectedHash: string) => boolean getProjectOptions: () => Promise } diff --git a/scripts/after-pack-hook.js b/scripts/after-pack-hook.js index ea0e365c283a..562ba2904fc4 100644 --- a/scripts/after-pack-hook.js +++ b/scripts/after-pack-hook.js @@ -19,6 +19,8 @@ const { getStudioFileSource, validateStudioFile, getIndexJscHash, + getCyPromptFileSource, + validateCyPromptFile, DUMMY_INDEX_JSC_HASH, } = require('./binary/binary-sources') const verify = require('../cli/lib/tasks/verify') @@ -100,6 +102,12 @@ module.exports = async function (params) { const StudioLifecycleManagerPath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/studio/StudioLifecycleManager.ts') const StudioLifecycleManagerFileSource = await getStudioFileSource(StudioLifecycleManagerPath) + // Remove local cy prompt env + const cyPromptLifecycleManagerPath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts') + const cyPromptLifecycleManagerFileSource = await getCyPromptFileSource(cyPromptLifecycleManagerPath) + const cyPromptManagerPath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/cy-prompt/CyPromptManager.ts') + const cyPromptManagerFileSource = await getCyPromptFileSource(cyPromptManagerPath) + await Promise.all([ fs.writeFile(encryptionFilePath, encryptionFileSource), fs.writeFile(cloudEnvironmentFilePath, cloudEnvironmentFileSource), @@ -107,6 +115,8 @@ module.exports = async function (params) { fs.writeFile(cloudProtocolFilePath, cloudProtocolFileSource), fs.writeFile(reportStudioErrorPath, reportStudioErrorFileSource), fs.writeFile(StudioLifecycleManagerPath, StudioLifecycleManagerFileSource), + fs.writeFile(cyPromptLifecycleManagerPath, cyPromptLifecycleManagerFileSource), + fs.writeFile(cyPromptManagerPath, cyPromptManagerFileSource), fs.writeFile(path.join(outputFolder, 'index.js'), binaryEntryPointSource), ]) @@ -121,6 +131,8 @@ module.exports = async function (params) { validateProtocolFile(cloudProtocolFilePath), validateStudioFile(reportStudioErrorPath), validateStudioFile(StudioLifecycleManagerPath), + validateCyPromptFile(cyPromptLifecycleManagerPath), + validateCyPromptFile(cyPromptManagerPath), ]) await flipFuses( diff --git a/scripts/binary/binary-sources.js b/scripts/binary/binary-sources.js index a6196eea0943..7e5d0efad461 100644 --- a/scripts/binary/binary-sources.js +++ b/scripts/binary/binary-sources.js @@ -119,6 +119,14 @@ const getStudioFileSource = async (studioFilePath) => { return fileContents.replaceAll('process.env.CYPRESS_LOCAL_STUDIO_PATH', 'undefined') } +const getCyPromptFileSource = async (cyPromptFilePath) => { + const fileContents = await fs.readFile(cyPromptFilePath, 'utf8') + + if (!fileContents.includes('process.env.CYPRESS_LOCAL_CY_PROMPT_PATH')) { + throw new Error(`Expected to find CYPRESS_LOCAL_CY_PROMPT_PATH in cy prompt file`) + } +} + const validateProtocolFile = async (protocolFilePath) => { const afterReplaceProtocol = await fs.readFile(protocolFilePath, 'utf8') @@ -135,6 +143,14 @@ const validateStudioFile = async (studioFilePath) => { } } +const validateCyPromptFile = async (cyPromptFilePath) => { + const afterReplaceCyPrompt = await fs.readFile(cyPromptFilePath, 'utf8') + + if (afterReplaceCyPrompt.includes('process.env.CYPRESS_LOCAL_CY_PROMPT_PATH')) { + throw new Error(`Expected process.env.CYPRESS_LOCAL_CY_PROMPT_PATH to be stripped from cy prompt file`) + } +} + module.exports = { getBinaryEntryPointSource, getBinaryByteNodeEntryPointSource, @@ -147,6 +163,8 @@ module.exports = { validateProtocolFile, getStudioFileSource, validateStudioFile, + getCyPromptFileSource, + validateCyPromptFile, getIndexJscHash, DUMMY_INDEX_JSC_HASH, }