mirror of
https://github.com/steel-dev/cli.git
synced 2026-09-14 20:47:34 +08:00
733 lines
18 KiB
TypeScript
733 lines
18 KiB
TypeScript
import {BrowserAdapterError} from './errors.js';
|
|
import {
|
|
createSessionFromApi,
|
|
getCaptchaStatusFromApi,
|
|
getSessionFromApi,
|
|
listSessionsFromApi,
|
|
releaseSession,
|
|
resolveExplicitApiUrl,
|
|
solveSessionCaptchaFromApi,
|
|
} from './lifecycle/api-client.js';
|
|
import {parseBrowserPassthroughBootstrapFlags} from './lifecycle/bootstrap-flags.js';
|
|
import {
|
|
readSteelProfile,
|
|
validateProfileName,
|
|
writeSteelProfile,
|
|
} from './lifecycle/profile-store.js';
|
|
import {
|
|
buildDeadSessionMessage,
|
|
getSessionId,
|
|
isNotFoundApiError,
|
|
isSessionLive,
|
|
resolveSessionMode,
|
|
toSessionSummary,
|
|
} from './lifecycle/session-policy.js';
|
|
import {
|
|
clearActiveSessionState,
|
|
readSessionState,
|
|
resolveCandidateSessionId,
|
|
resolveNameFromState,
|
|
setActiveSessionState,
|
|
withSessionStateLock,
|
|
} from './lifecycle/state-store.js';
|
|
import type {
|
|
BrowserSessionEndpointOptions,
|
|
BrowserSessionMode,
|
|
BrowserSessionSummary,
|
|
CaptchaStatusValue,
|
|
CaptchaType,
|
|
GetBrowserSessionCaptchaStatusOptions,
|
|
GetBrowserSessionCaptchaStatusResult,
|
|
SolveBrowserSessionCaptchaOptions,
|
|
SolveBrowserSessionCaptchaResult,
|
|
StartBrowserSessionOptions,
|
|
StopBrowserSessionOptions,
|
|
StopBrowserSessionResult,
|
|
UnknownRecord,
|
|
} from './lifecycle/types.js';
|
|
|
|
export {parseBrowserPassthroughBootstrapFlags};
|
|
export type {
|
|
BrowserSessionMode,
|
|
BrowserSessionSummary,
|
|
CaptchaStatusValue,
|
|
CaptchaType,
|
|
GetBrowserSessionCaptchaStatusOptions,
|
|
GetBrowserSessionCaptchaStatusResult,
|
|
SolveBrowserSessionCaptchaOptions,
|
|
SolveBrowserSessionCaptchaResult,
|
|
StartBrowserSessionOptions,
|
|
StopBrowserSessionOptions,
|
|
StopBrowserSessionResult,
|
|
};
|
|
|
|
async function tryGetLiveSession(
|
|
mode: BrowserSessionMode,
|
|
sessionId: string,
|
|
environment: NodeJS.ProcessEnv,
|
|
apiUrl?: string | null,
|
|
): Promise<UnknownRecord | null> {
|
|
try {
|
|
const session = await getSessionFromApi(
|
|
mode,
|
|
sessionId,
|
|
environment,
|
|
apiUrl,
|
|
);
|
|
return isSessionLive(session) ? session : null;
|
|
} catch (error) {
|
|
if (isNotFoundApiError(error)) {
|
|
return null;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function startBrowserSession(
|
|
options: StartBrowserSessionOptions = {},
|
|
): Promise<BrowserSessionSummary> {
|
|
const environment = options.environment || process.env;
|
|
const apiUrl = resolveExplicitApiUrl(options.apiUrl);
|
|
const mode = resolveSessionMode(options.local, apiUrl, null, environment);
|
|
const sessionName = options.sessionName?.trim() || null;
|
|
const deadSessionBehavior = options.deadSessionBehavior || 'recreate';
|
|
|
|
while (true) {
|
|
const candidateSessionId = await withSessionStateLock(
|
|
async state => resolveCandidateSessionId(state, mode, sessionName),
|
|
{write: false},
|
|
);
|
|
|
|
if (candidateSessionId) {
|
|
const existingSession = await tryGetLiveSession(
|
|
mode,
|
|
candidateSessionId,
|
|
environment,
|
|
apiUrl,
|
|
);
|
|
|
|
if (existingSession) {
|
|
const claimedExistingSession = await withSessionStateLock(
|
|
async state => {
|
|
const latestCandidateSessionId = resolveCandidateSessionId(
|
|
state,
|
|
mode,
|
|
sessionName,
|
|
);
|
|
if (latestCandidateSessionId !== candidateSessionId) {
|
|
return false;
|
|
}
|
|
|
|
setActiveSessionState(state, mode, candidateSessionId, sessionName);
|
|
return true;
|
|
},
|
|
);
|
|
|
|
if (claimedExistingSession) {
|
|
return toSessionSummary(
|
|
existingSession,
|
|
mode,
|
|
sessionName,
|
|
environment,
|
|
);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (deadSessionBehavior === 'error') {
|
|
throw new BrowserAdapterError(
|
|
'SESSION_NOT_FOUND',
|
|
buildDeadSessionMessage(candidateSessionId, {
|
|
local: options.local,
|
|
apiUrl,
|
|
sessionName,
|
|
timeoutMs: options.timeoutMs,
|
|
headless: options.headless,
|
|
region: options.region,
|
|
solveCaptcha: options.solveCaptcha,
|
|
}),
|
|
);
|
|
}
|
|
|
|
await withSessionStateLock(async state => {
|
|
const latestCandidateSessionId = resolveCandidateSessionId(
|
|
state,
|
|
mode,
|
|
sessionName,
|
|
);
|
|
if (latestCandidateSessionId === candidateSessionId) {
|
|
clearActiveSessionState(state, mode, candidateSessionId);
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
|
|
let resolvedProfileId: string | undefined;
|
|
let storedChromeProfile: string | undefined;
|
|
|
|
if (options.profileName) {
|
|
const nameError = validateProfileName(options.profileName);
|
|
if (nameError) {
|
|
throw new BrowserAdapterError('INVALID_BROWSER_ARGS', nameError);
|
|
}
|
|
|
|
const stored = await readSteelProfile(options.profileName, environment);
|
|
resolvedProfileId = stored?.profileId;
|
|
storedChromeProfile = stored?.chromeProfile;
|
|
}
|
|
|
|
const persistProfile = Boolean(
|
|
options.profileName && options.updateProfile,
|
|
);
|
|
|
|
let createdSession: UnknownRecord;
|
|
try {
|
|
createdSession = await createSessionFromApi(
|
|
mode,
|
|
{
|
|
stealth: options.stealth,
|
|
proxyUrl: options.proxyUrl,
|
|
timeoutMs: options.timeoutMs,
|
|
headless: options.headless,
|
|
region: options.region,
|
|
solveCaptcha: options.solveCaptcha,
|
|
profileId: resolvedProfileId,
|
|
persistProfile,
|
|
namespace: options.namespace,
|
|
credentials: options.credentials,
|
|
},
|
|
environment,
|
|
apiUrl,
|
|
);
|
|
} catch (error) {
|
|
if (
|
|
options.profileName &&
|
|
resolvedProfileId &&
|
|
isNotFoundApiError(error)
|
|
) {
|
|
console.error(`Warning: Profile not found. Creating a new profile.`);
|
|
createdSession = await createSessionFromApi(
|
|
mode,
|
|
{
|
|
stealth: options.stealth,
|
|
proxyUrl: options.proxyUrl,
|
|
timeoutMs: options.timeoutMs,
|
|
headless: options.headless,
|
|
region: options.region,
|
|
solveCaptcha: options.solveCaptcha,
|
|
persistProfile,
|
|
namespace: options.namespace,
|
|
credentials: options.credentials,
|
|
},
|
|
environment,
|
|
apiUrl,
|
|
);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const createdSessionId = getSessionId(createdSession);
|
|
|
|
if (!createdSessionId) {
|
|
throw new BrowserAdapterError(
|
|
'API_ERROR',
|
|
'Failed to create a session because the API did not return a session id.',
|
|
);
|
|
}
|
|
|
|
const claimedCreatedSession = await withSessionStateLock(async state => {
|
|
const latestCandidateSessionId = resolveCandidateSessionId(
|
|
state,
|
|
mode,
|
|
sessionName,
|
|
);
|
|
if (latestCandidateSessionId) {
|
|
return false;
|
|
}
|
|
|
|
if (sessionName) {
|
|
state.namedSessions[mode][sessionName] = createdSessionId;
|
|
}
|
|
setActiveSessionState(state, mode, createdSessionId, sessionName);
|
|
return true;
|
|
});
|
|
|
|
if (claimedCreatedSession) {
|
|
if (options.profileName) {
|
|
const returnedProfileId = createdSession['profileId'];
|
|
if (typeof returnedProfileId === 'string') {
|
|
await writeSteelProfile(
|
|
options.profileName,
|
|
returnedProfileId,
|
|
environment,
|
|
storedChromeProfile,
|
|
);
|
|
}
|
|
}
|
|
|
|
return toSessionSummary(createdSession, mode, sessionName, environment);
|
|
}
|
|
|
|
try {
|
|
await releaseSession(mode, createdSessionId, environment, apiUrl);
|
|
} catch {
|
|
// Continue with the state-selected session if cleanup fails.
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function listBrowserSessions(
|
|
options?: BrowserSessionEndpointOptions,
|
|
): Promise<BrowserSessionSummary[]> {
|
|
const environment = options?.environment || process.env;
|
|
const apiUrl = resolveExplicitApiUrl(options?.apiUrl);
|
|
const mode = resolveSessionMode(options?.local, apiUrl, null, environment);
|
|
const state = await readSessionState();
|
|
const sessions = await listSessionsFromApi(mode, environment, apiUrl);
|
|
|
|
return sessions.map(session => {
|
|
const sessionId = getSessionId(session);
|
|
const sessionName = sessionId
|
|
? resolveNameFromState(state, mode, sessionId)
|
|
: null;
|
|
return toSessionSummary(session, mode, sessionName, environment);
|
|
});
|
|
}
|
|
|
|
function resolveTargetSession(
|
|
options: {sessionName?: string},
|
|
mode: BrowserSessionMode,
|
|
state: Awaited<ReturnType<typeof readSessionState>>,
|
|
): {sessionId: string | null; sessionName: string | null} {
|
|
const sessionName = options.sessionName?.trim();
|
|
if (sessionName) {
|
|
return {
|
|
sessionId: state.namedSessions[mode][sessionName] || null,
|
|
sessionName,
|
|
};
|
|
}
|
|
|
|
if (state.activeSessionMode === mode && state.activeSessionId) {
|
|
return {
|
|
sessionId: state.activeSessionId,
|
|
sessionName: state.activeSessionName,
|
|
};
|
|
}
|
|
|
|
return {
|
|
sessionId: null,
|
|
sessionName: null,
|
|
};
|
|
}
|
|
|
|
export async function getActiveBrowserLiveUrl(
|
|
options?: BrowserSessionEndpointOptions,
|
|
): Promise<string | null> {
|
|
const environment = options?.environment || process.env;
|
|
const apiUrl = resolveExplicitApiUrl(options?.apiUrl);
|
|
const state = await readSessionState();
|
|
const mode = resolveSessionMode(
|
|
options?.local,
|
|
apiUrl,
|
|
state.activeSessionMode,
|
|
environment,
|
|
);
|
|
|
|
const targetSession = resolveTargetSession(
|
|
{sessionName: options?.sessionName},
|
|
mode,
|
|
state,
|
|
);
|
|
if (!targetSession.sessionId) {
|
|
return null;
|
|
}
|
|
|
|
const session = await tryGetLiveSession(
|
|
mode,
|
|
targetSession.sessionId,
|
|
environment,
|
|
apiUrl,
|
|
);
|
|
if (!session) {
|
|
return null;
|
|
}
|
|
|
|
const summary = toSessionSummary(
|
|
session,
|
|
mode,
|
|
targetSession.sessionName,
|
|
environment,
|
|
);
|
|
return summary.viewerUrl;
|
|
}
|
|
|
|
export async function stopBrowserSession(
|
|
options: StopBrowserSessionOptions = {},
|
|
): Promise<StopBrowserSessionResult> {
|
|
const environment = options.environment || process.env;
|
|
const apiUrl = resolveExplicitApiUrl(options.apiUrl);
|
|
const initialState = await withSessionStateLock(
|
|
async state => {
|
|
const mode = resolveSessionMode(
|
|
options.local,
|
|
apiUrl,
|
|
state.activeSessionMode,
|
|
environment,
|
|
);
|
|
|
|
return {
|
|
mode,
|
|
targetSessionId: resolveTargetSession(
|
|
{sessionName: options.sessionName},
|
|
mode,
|
|
state,
|
|
).sessionId,
|
|
};
|
|
},
|
|
{write: false},
|
|
);
|
|
const mode = initialState.mode;
|
|
|
|
if (options.all) {
|
|
const sessions = await listSessionsFromApi(mode, environment, apiUrl);
|
|
const liveSessionIds = sessions
|
|
.filter(session => isSessionLive(session))
|
|
.map(session => getSessionId(session))
|
|
.filter((sessionId): sessionId is string => Boolean(sessionId));
|
|
|
|
for (const sessionId of liveSessionIds) {
|
|
try {
|
|
await releaseSession(mode, sessionId, environment, apiUrl);
|
|
} catch {
|
|
// Continue best-effort release for all sessions.
|
|
}
|
|
}
|
|
|
|
await withSessionStateLock(async state => {
|
|
for (const sessionId of liveSessionIds) {
|
|
clearActiveSessionState(state, mode, sessionId);
|
|
}
|
|
});
|
|
|
|
return {
|
|
mode,
|
|
all: true,
|
|
stoppedSessionIds: liveSessionIds,
|
|
};
|
|
}
|
|
|
|
const targetSessionId = initialState.targetSessionId;
|
|
if (!targetSessionId) {
|
|
return {
|
|
mode,
|
|
all: false,
|
|
stoppedSessionIds: [],
|
|
};
|
|
}
|
|
|
|
await releaseSession(mode, targetSessionId, environment, apiUrl);
|
|
await withSessionStateLock(async state => {
|
|
clearActiveSessionState(state, mode, targetSessionId);
|
|
});
|
|
|
|
return {
|
|
mode,
|
|
all: false,
|
|
stoppedSessionIds: [targetSessionId],
|
|
};
|
|
}
|
|
|
|
function resolveSolveCaptchaSessionId(
|
|
options: SolveBrowserSessionCaptchaOptions,
|
|
mode: BrowserSessionMode,
|
|
state: Awaited<ReturnType<typeof readSessionState>>,
|
|
): string | null {
|
|
const explicitSessionId = options.sessionId?.trim();
|
|
if (explicitSessionId) {
|
|
return explicitSessionId;
|
|
}
|
|
|
|
const sessionName = options.sessionName?.trim();
|
|
if (sessionName) {
|
|
return state.namedSessions[mode][sessionName] || null;
|
|
}
|
|
|
|
if (state.activeSessionMode === mode && state.activeSessionId) {
|
|
return state.activeSessionId;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function solveBrowserSessionCaptcha(
|
|
options: SolveBrowserSessionCaptchaOptions = {},
|
|
): Promise<SolveBrowserSessionCaptchaResult> {
|
|
const environment = options.environment || process.env;
|
|
const apiUrl = resolveExplicitApiUrl(options.apiUrl);
|
|
const state = await readSessionState();
|
|
const mode = resolveSessionMode(
|
|
options.local,
|
|
apiUrl,
|
|
state.activeSessionMode,
|
|
environment,
|
|
);
|
|
const sessionId = resolveSolveCaptchaSessionId(options, mode, state);
|
|
|
|
if (!sessionId) {
|
|
throw new BrowserAdapterError(
|
|
'SESSION_NOT_FOUND',
|
|
'No target browser session found for CAPTCHA solving. Pass `--session-id`, pass `--session <name>`, or start a session first with `steel browser start --session <name>`.',
|
|
);
|
|
}
|
|
|
|
const rawResponse = await solveSessionCaptchaFromApi(
|
|
mode,
|
|
sessionId,
|
|
{
|
|
pageId: options.pageId,
|
|
url: options.url,
|
|
taskId: options.taskId,
|
|
},
|
|
environment,
|
|
apiUrl,
|
|
);
|
|
const success = rawResponse['success'];
|
|
const message = rawResponse['message'];
|
|
|
|
return {
|
|
mode,
|
|
sessionId,
|
|
success: typeof success === 'boolean' ? success : false,
|
|
message: typeof message === 'string' ? message : null,
|
|
raw: rawResponse,
|
|
};
|
|
}
|
|
|
|
const DEFAULT_CAPTCHA_WAIT_TIMEOUT_MS = 60_000;
|
|
const DEFAULT_CAPTCHA_POLL_INTERVAL_MS = 1_000;
|
|
|
|
const KNOWN_CAPTCHA_TYPES: CaptchaType[] = [
|
|
'recaptchaV2',
|
|
'recaptchaV3',
|
|
'turnstile',
|
|
'image_to_text',
|
|
];
|
|
|
|
function wait(milliseconds: number): Promise<void> {
|
|
return new Promise(resolve => {
|
|
setTimeout(resolve, milliseconds);
|
|
});
|
|
}
|
|
|
|
function normalizeCaptchaStatus(pages: UnknownRecord[]): {
|
|
status: CaptchaStatusValue;
|
|
types: CaptchaType[];
|
|
} {
|
|
if (!pages.length) {
|
|
return {status: 'none', types: []};
|
|
}
|
|
|
|
const allTasks: UnknownRecord[] = [];
|
|
let anySolving = false;
|
|
|
|
for (const page of pages) {
|
|
if (page['isSolvingCaptcha'] === true) {
|
|
anySolving = true;
|
|
}
|
|
|
|
const tasks = page['tasks'];
|
|
if (Array.isArray(tasks)) {
|
|
for (const task of tasks) {
|
|
if (task && typeof task === 'object') {
|
|
allTasks.push(task as UnknownRecord);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!allTasks.length) {
|
|
return {status: anySolving ? 'solving' : 'none', types: []};
|
|
}
|
|
|
|
const solvingTypes: Set<CaptchaType> = new Set();
|
|
const failedTypes: Set<CaptchaType> = new Set();
|
|
let hasSolving = false;
|
|
let hasFailed = false;
|
|
let hasSolved = false;
|
|
|
|
for (const task of allTasks) {
|
|
const taskStatus = task['status'];
|
|
const taskType = task['type'];
|
|
const captchaType = KNOWN_CAPTCHA_TYPES.includes(taskType as CaptchaType)
|
|
? (taskType as CaptchaType)
|
|
: null;
|
|
|
|
if (
|
|
taskStatus === 'solving' ||
|
|
taskStatus === 'detected' ||
|
|
taskStatus === 'validating'
|
|
) {
|
|
hasSolving = true;
|
|
if (captchaType) {
|
|
solvingTypes.add(captchaType);
|
|
}
|
|
} else if (
|
|
taskStatus === 'failed_to_solve' ||
|
|
taskStatus === 'failed_to_detect' ||
|
|
taskStatus === 'validation_failed'
|
|
) {
|
|
hasFailed = true;
|
|
if (captchaType) {
|
|
failedTypes.add(captchaType);
|
|
}
|
|
} else if (taskStatus === 'solved') {
|
|
hasSolved = true;
|
|
}
|
|
}
|
|
|
|
if (hasSolving || anySolving) {
|
|
return {status: 'solving', types: Array.from(solvingTypes)};
|
|
}
|
|
|
|
if (hasFailed) {
|
|
return {status: 'failed', types: Array.from(failedTypes)};
|
|
}
|
|
|
|
if (hasSolved) {
|
|
return {status: 'solved', types: []};
|
|
}
|
|
|
|
return {status: 'none', types: []};
|
|
}
|
|
|
|
function isTerminalCaptchaStatus(status: CaptchaStatusValue): boolean {
|
|
return status === 'solved' || status === 'failed' || status === 'none';
|
|
}
|
|
|
|
export async function getBrowserSessionCaptchaStatus(
|
|
options: GetBrowserSessionCaptchaStatusOptions = {},
|
|
): Promise<GetBrowserSessionCaptchaStatusResult> {
|
|
const environment = options.environment || process.env;
|
|
const apiUrl = resolveExplicitApiUrl(options.apiUrl);
|
|
const state = await readSessionState();
|
|
const mode = resolveSessionMode(
|
|
options.local,
|
|
apiUrl,
|
|
state.activeSessionMode,
|
|
environment,
|
|
);
|
|
const sessionId = resolveSolveCaptchaSessionId(options, mode, state);
|
|
|
|
if (!sessionId) {
|
|
throw new BrowserAdapterError(
|
|
'SESSION_NOT_FOUND',
|
|
'No target browser session found for CAPTCHA status. Pass `--session-id`, pass `--session <name>`, or start a session first with `steel browser start --session <name>`.',
|
|
);
|
|
}
|
|
|
|
const timeout = options.timeout ?? DEFAULT_CAPTCHA_WAIT_TIMEOUT_MS;
|
|
const interval = options.interval ?? DEFAULT_CAPTCHA_POLL_INTERVAL_MS;
|
|
const startTime = Date.now();
|
|
|
|
while (true) {
|
|
const pages = await getCaptchaStatusFromApi(
|
|
mode,
|
|
sessionId,
|
|
{pageId: options.pageId},
|
|
environment,
|
|
apiUrl,
|
|
);
|
|
|
|
const {status, types} = normalizeCaptchaStatus(pages);
|
|
|
|
if (!options.wait || isTerminalCaptchaStatus(status)) {
|
|
return {
|
|
mode,
|
|
sessionId,
|
|
status,
|
|
types,
|
|
raw: {pages},
|
|
};
|
|
}
|
|
|
|
if (Date.now() - startTime >= timeout) {
|
|
throw new BrowserAdapterError(
|
|
'API_ERROR',
|
|
`CAPTCHA status polling timed out after ${timeout}ms. Last status: ${status}`,
|
|
);
|
|
}
|
|
|
|
await wait(interval);
|
|
}
|
|
}
|
|
|
|
export type BrowserPassthroughBootstrap = {
|
|
argv: string[];
|
|
sessionName: string | null;
|
|
};
|
|
|
|
const SESSION_FREE_SUBCOMMANDS = new Set(['auth', 'device', 'session']);
|
|
|
|
function isSessionFreePassthrough(passthroughArgv: string[]): boolean {
|
|
const firstToken = passthroughArgv.find(a => !a.startsWith('-'));
|
|
return firstToken !== undefined && SESSION_FREE_SUBCOMMANDS.has(firstToken);
|
|
}
|
|
|
|
export async function bootstrapBrowserPassthroughArgv(
|
|
browserArgv: string[],
|
|
environment: NodeJS.ProcessEnv = process.env,
|
|
): Promise<BrowserPassthroughBootstrap> {
|
|
const parsed = parseBrowserPassthroughBootstrapFlags(browserArgv);
|
|
|
|
if (parsed.options.autoConnect || parsed.options.cdpTarget) {
|
|
return {
|
|
argv: parsed.passthroughArgv,
|
|
sessionName: parsed.options.sessionName,
|
|
};
|
|
}
|
|
|
|
if (
|
|
parsed.passthroughArgv.includes('--help') ||
|
|
parsed.passthroughArgv.includes('-h')
|
|
) {
|
|
return {argv: parsed.passthroughArgv, sessionName: null};
|
|
}
|
|
|
|
if (isSessionFreePassthrough(parsed.passthroughArgv)) {
|
|
return {argv: parsed.passthroughArgv, sessionName: null};
|
|
}
|
|
|
|
const session = await startBrowserSession({
|
|
local: parsed.options.local,
|
|
apiUrl: parsed.options.apiUrl || undefined,
|
|
sessionName: parsed.options.sessionName || undefined,
|
|
stealth: parsed.options.stealth,
|
|
proxyUrl: parsed.options.proxyUrl || undefined,
|
|
timeoutMs: parsed.options.timeoutMs || undefined,
|
|
headless: parsed.options.headless || undefined,
|
|
region: parsed.options.region || undefined,
|
|
solveCaptcha: parsed.options.solveCaptcha || undefined,
|
|
namespace: parsed.options.namespace || undefined,
|
|
credentials: parsed.options.credentials || undefined,
|
|
deadSessionBehavior: 'error',
|
|
environment,
|
|
profileName: parsed.options.profileName || undefined,
|
|
updateProfile: parsed.options.updateProfile || undefined,
|
|
});
|
|
|
|
if (!session.connectUrl) {
|
|
throw new BrowserAdapterError(
|
|
'API_ERROR',
|
|
`Could not resolve a connect URL for session ${session.id}.`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
argv: [...parsed.passthroughArgv, '--cdp', session.connectUrl],
|
|
sessionName: parsed.options.sessionName,
|
|
};
|
|
}
|