diff --git a/src/browser/routes/agent.debug.ts b/src/browser/routes/agent.debug.ts index cda2978cb..fab517d95 100644 --- a/src/browser/routes/agent.debug.ts +++ b/src/browser/routes/agent.debug.ts @@ -2,7 +2,12 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import type { BrowserRouteContext } from "../server-context.js"; -import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js"; +import { + readBody, + resolveTargetIdFromBody, + resolveTargetIdFromQuery, + withPlaywrightRouteContext, +} from "./agent.shared.js"; import { DEFAULT_TRACE_DIR, resolvePathWithinRoot } from "./path-output.js"; import type { BrowserRouteRegistrar } from "./types.js"; import { toBoolean, toStringOrEmpty } from "./utils.js"; @@ -12,151 +17,133 @@ export function registerBrowserAgentDebugRoutes( ctx: BrowserRouteContext, ) { app.get("/console", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } - const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; + const targetId = resolveTargetIdFromQuery(req.query); const level = typeof req.query.level === "string" ? req.query.level : ""; - try { - const tab = await profileCtx.ensureTabAvailable(targetId || undefined); - const pw = await requirePwAi(res, "console messages"); - if (!pw) { - return; - } - const messages = await pw.getConsoleMessagesViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - level: level.trim() || undefined, - }); - res.json({ ok: true, messages, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "console messages", + run: async ({ cdpUrl, tab, pw }) => { + const messages = await pw.getConsoleMessagesViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + level: level.trim() || undefined, + }); + res.json({ ok: true, messages, targetId: tab.targetId }); + }, + }); }); app.get("/errors", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } - const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; + const targetId = resolveTargetIdFromQuery(req.query); const clear = toBoolean(req.query.clear) ?? false; - try { - const tab = await profileCtx.ensureTabAvailable(targetId || undefined); - const pw = await requirePwAi(res, "page errors"); - if (!pw) { - return; - } - const result = await pw.getPageErrorsViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - clear, - }); - res.json({ ok: true, targetId: tab.targetId, ...result }); - } catch (err) { - handleRouteError(ctx, res, err); - } + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "page errors", + run: async ({ cdpUrl, tab, pw }) => { + const result = await pw.getPageErrorsViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + clear, + }); + res.json({ ok: true, targetId: tab.targetId, ...result }); + }, + }); }); app.get("/requests", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } - const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; + const targetId = resolveTargetIdFromQuery(req.query); const filter = typeof req.query.filter === "string" ? req.query.filter : ""; const clear = toBoolean(req.query.clear) ?? false; - try { - const tab = await profileCtx.ensureTabAvailable(targetId || undefined); - const pw = await requirePwAi(res, "network requests"); - if (!pw) { - return; - } - const result = await pw.getNetworkRequestsViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - filter: filter.trim() || undefined, - clear, - }); - res.json({ ok: true, targetId: tab.targetId, ...result }); - } catch (err) { - handleRouteError(ctx, res, err); - } + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "network requests", + run: async ({ cdpUrl, tab, pw }) => { + const result = await pw.getNetworkRequestsViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + filter: filter.trim() || undefined, + clear, + }); + res.json({ ok: true, targetId: tab.targetId, ...result }); + }, + }); }); app.post("/trace/start", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = toStringOrEmpty(body.targetId) || undefined; + const targetId = resolveTargetIdFromBody(body); const screenshots = toBoolean(body.screenshots) ?? undefined; const snapshots = toBoolean(body.snapshots) ?? undefined; const sources = toBoolean(body.sources) ?? undefined; - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "trace start"); - if (!pw) { - return; - } - await pw.traceStartViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - screenshots, - snapshots, - sources, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "trace start", + run: async ({ cdpUrl, tab, pw }) => { + await pw.traceStartViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + screenshots, + snapshots, + sources, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/trace/stop", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = toStringOrEmpty(body.targetId) || undefined; + const targetId = resolveTargetIdFromBody(body); const out = toStringOrEmpty(body.path) || ""; - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "trace stop"); - if (!pw) { - return; - } - const id = crypto.randomUUID(); - const dir = DEFAULT_TRACE_DIR; - await fs.mkdir(dir, { recursive: true }); - const tracePathResult = resolvePathWithinRoot({ - rootDir: dir, - requestedPath: out, - scopeLabel: "trace directory", - defaultFileName: `browser-trace-${id}.zip`, - }); - if (!tracePathResult.ok) { - res.status(400).json({ error: tracePathResult.error }); - return; - } - const tracePath = tracePathResult.path; - await pw.traceStopViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - path: tracePath, - }); - res.json({ - ok: true, - targetId: tab.targetId, - path: path.resolve(tracePath), - }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "trace stop", + run: async ({ cdpUrl, tab, pw }) => { + const id = crypto.randomUUID(); + const dir = DEFAULT_TRACE_DIR; + await fs.mkdir(dir, { recursive: true }); + const tracePathResult = resolvePathWithinRoot({ + rootDir: dir, + requestedPath: out, + scopeLabel: "trace directory", + defaultFileName: `browser-trace-${id}.zip`, + }); + if (!tracePathResult.ok) { + res.status(400).json({ error: tracePathResult.error }); + return; + } + const tracePath = tracePathResult.path; + await pw.traceStopViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + path: tracePath, + }); + res.json({ + ok: true, + targetId: tab.targetId, + path: path.resolve(tracePath), + }); + }, + }); }); } diff --git a/src/browser/routes/agent.shared.ts b/src/browser/routes/agent.shared.ts index d230c72e3..aee566965 100644 --- a/src/browser/routes/agent.shared.ts +++ b/src/browser/routes/agent.shared.ts @@ -22,6 +22,16 @@ export function readBody(req: BrowserRequest): Record { return body; } +export function resolveTargetIdFromBody(body: Record): string | undefined { + const targetId = typeof body.targetId === "string" ? body.targetId.trim() : ""; + return targetId || undefined; +} + +export function resolveTargetIdFromQuery(query: Record): string | undefined { + const targetId = typeof query.targetId === "string" ? query.targetId.trim() : ""; + return targetId || undefined; +} + export function handleRouteError(ctx: BrowserRouteContext, res: BrowserResponse, err: unknown) { const mapped = ctx.mapTabError(err); if (mapped) { @@ -66,3 +76,68 @@ export async function requirePwAi( ); return null; } + +type RouteTabContext = { + profileCtx: ProfileContext; + tab: Awaited>; + cdpUrl: string; +}; + +type RouteTabPwContext = RouteTabContext & { + pw: PwAiModule; +}; + +type RouteWithTabParams = { + req: BrowserRequest; + res: BrowserResponse; + ctx: BrowserRouteContext; + targetId?: string; + run: (ctx: RouteTabContext) => Promise; +}; + +export async function withRouteTabContext( + params: RouteWithTabParams, +): Promise { + const profileCtx = resolveProfileContext(params.req, params.res, params.ctx); + if (!profileCtx) { + return undefined; + } + try { + const tab = await profileCtx.ensureTabAvailable(params.targetId); + return await params.run({ + profileCtx, + tab, + cdpUrl: profileCtx.profile.cdpUrl, + }); + } catch (err) { + handleRouteError(params.ctx, params.res, err); + return undefined; + } +} + +type RouteWithPwParams = { + req: BrowserRequest; + res: BrowserResponse; + ctx: BrowserRouteContext; + targetId?: string; + feature: string; + run: (ctx: RouteTabPwContext) => Promise; +}; + +export async function withPlaywrightRouteContext( + params: RouteWithPwParams, +): Promise { + return await withRouteTabContext({ + req: params.req, + res: params.res, + ctx: params.ctx, + targetId: params.targetId, + run: async ({ profileCtx, tab, cdpUrl }) => { + const pw = await requirePwAi(params.res, params.feature); + if (!pw) { + return undefined as T | undefined; + } + return await params.run({ profileCtx, tab, cdpUrl, pw }); + }, + }); +} diff --git a/src/browser/routes/agent.storage.ts b/src/browser/routes/agent.storage.ts index c88e44ceb..f9d28c65f 100644 --- a/src/browser/routes/agent.storage.ts +++ b/src/browser/routes/agent.storage.ts @@ -1,60 +1,29 @@ import type { BrowserRouteContext } from "../server-context.js"; -import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js"; -import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js"; +import { + readBody, + resolveTargetIdFromBody, + resolveTargetIdFromQuery, + withPlaywrightRouteContext, +} from "./agent.shared.js"; +import type { BrowserRouteRegistrar } from "./types.js"; import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js"; type StorageKind = "local" | "session"; -function resolveBodyTargetId(body: unknown): string | undefined { - if (!body || typeof body !== "object" || Array.isArray(body)) { - return undefined; - } - const targetId = toStringOrEmpty((body as Record).targetId); - return targetId || undefined; -} - -function parseStorageKind(raw: string): StorageKind | null { +export function parseStorageKind(raw: string): StorageKind | null { if (raw === "local" || raw === "session") { return raw; } return null; } -function parseStorageMutationRequest( +export function parseStorageMutationRequest( kindParam: unknown, - body: unknown, + body: Record, ): { kind: StorageKind | null; targetId: string | undefined } { return { kind: parseStorageKind(toStringOrEmpty(kindParam)), - targetId: resolveBodyTargetId(body), - }; -} - -function resolveStorageMutationContext(params: { - req: BrowserRequest; - res: BrowserResponse; - ctx: BrowserRouteContext; -}): { - profileCtx: NonNullable>; - body: Record; - kind: StorageKind; - targetId: string | undefined; -} | null { - const profileCtx = resolveProfileContext(params.req, params.res, params.ctx); - if (!profileCtx) { - return null; - } - const body = readBody(params.req); - const parsed = parseStorageMutationRequest(params.req.params.kind, body); - if (!parsed.kind) { - jsonError(params.res, 400, "kind must be local|session"); - return null; - } - return { - profileCtx, - body, - kind: parsed.kind, - targetId: parsed.targetId, + targetId: resolveTargetIdFromBody(body), }; } @@ -63,34 +32,26 @@ export function registerBrowserAgentStorageRoutes( ctx: BrowserRouteContext, ) { app.get("/cookies", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } - const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; - try { - const tab = await profileCtx.ensureTabAvailable(targetId || undefined); - const pw = await requirePwAi(res, "cookies"); - if (!pw) { - return; - } - const result = await pw.cookiesGetViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - }); - res.json({ ok: true, targetId: tab.targetId, ...result }); - } catch (err) { - handleRouteError(ctx, res, err); - } + const targetId = resolveTargetIdFromQuery(req.query); + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "cookies", + run: async ({ cdpUrl, tab, pw }) => { + const result = await pw.cookiesGetViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + }); + res.json({ ok: true, targetId: tab.targetId, ...result }); + }, + }); }); app.post("/cookies/set", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); + const targetId = resolveTargetIdFromBody(body); const cookie = body.cookie && typeof body.cookie === "object" && !Array.isArray(body.cookie) ? (body.cookie as Record) @@ -98,176 +59,170 @@ export function registerBrowserAgentStorageRoutes( if (!cookie) { return jsonError(res, 400, "cookie is required"); } - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "cookies set"); - if (!pw) { - return; - } - await pw.cookiesSetViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - cookie: { - name: toStringOrEmpty(cookie.name), - value: toStringOrEmpty(cookie.value), - url: toStringOrEmpty(cookie.url) || undefined, - domain: toStringOrEmpty(cookie.domain) || undefined, - path: toStringOrEmpty(cookie.path) || undefined, - expires: toNumber(cookie.expires) ?? undefined, - httpOnly: toBoolean(cookie.httpOnly) ?? undefined, - secure: toBoolean(cookie.secure) ?? undefined, - sameSite: - cookie.sameSite === "Lax" || cookie.sameSite === "None" || cookie.sameSite === "Strict" - ? cookie.sameSite - : undefined, - }, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "cookies set", + run: async ({ cdpUrl, tab, pw }) => { + await pw.cookiesSetViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + cookie: { + name: toStringOrEmpty(cookie.name), + value: toStringOrEmpty(cookie.value), + url: toStringOrEmpty(cookie.url) || undefined, + domain: toStringOrEmpty(cookie.domain) || undefined, + path: toStringOrEmpty(cookie.path) || undefined, + expires: toNumber(cookie.expires) ?? undefined, + httpOnly: toBoolean(cookie.httpOnly) ?? undefined, + secure: toBoolean(cookie.secure) ?? undefined, + sameSite: + cookie.sameSite === "Lax" || + cookie.sameSite === "None" || + cookie.sameSite === "Strict" + ? cookie.sameSite + : undefined, + }, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/cookies/clear", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "cookies clear"); - if (!pw) { - return; - } - await pw.cookiesClearViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + const targetId = resolveTargetIdFromBody(body); + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "cookies clear", + run: async ({ cdpUrl, tab, pw }) => { + await pw.cookiesClearViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.get("/storage/:kind", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const kind = parseStorageKind(toStringOrEmpty(req.params.kind)); if (!kind) { return jsonError(res, 400, "kind must be local|session"); } - const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; - const key = typeof req.query.key === "string" ? req.query.key : ""; - try { - const tab = await profileCtx.ensureTabAvailable(targetId || undefined); - const pw = await requirePwAi(res, "storage get"); - if (!pw) { - return; - } - const result = await pw.storageGetViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - kind, - key: key.trim() || undefined, - }); - res.json({ ok: true, targetId: tab.targetId, ...result }); - } catch (err) { - handleRouteError(ctx, res, err); - } + const targetId = resolveTargetIdFromQuery(req.query); + const key = toStringOrEmpty(req.query.key); + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "storage get", + run: async ({ cdpUrl, tab, pw }) => { + const result = await pw.storageGetViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + kind, + key: key.trim() || undefined, + }); + res.json({ ok: true, targetId: tab.targetId, ...result }); + }, + }); }); app.post("/storage/:kind/set", async (req, res) => { - const mutation = resolveStorageMutationContext({ req, res, ctx }); - if (!mutation) { - return; + const body = readBody(req); + const parsed = parseStorageMutationRequest(req.params.kind, body); + if (!parsed.kind) { + return jsonError(res, 400, "kind must be local|session"); } - const { profileCtx, body, kind, targetId } = mutation; + const kind = parsed.kind; const key = toStringOrEmpty(body.key); if (!key) { return jsonError(res, 400, "key is required"); } const value = typeof body.value === "string" ? body.value : ""; - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "storage set"); - if (!pw) { - return; - } - await pw.storageSetViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - kind, - key, - value, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId: parsed.targetId, + feature: "storage set", + run: async ({ cdpUrl, tab, pw }) => { + await pw.storageSetViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + kind, + key, + value, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/storage/:kind/clear", async (req, res) => { - const mutation = resolveStorageMutationContext({ req, res, ctx }); - if (!mutation) { - return; - } - const { profileCtx, kind, targetId } = mutation; - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "storage clear"); - if (!pw) { - return; - } - await pw.storageClearViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - kind, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); + const body = readBody(req); + const parsed = parseStorageMutationRequest(req.params.kind, body); + if (!parsed.kind) { + return jsonError(res, 400, "kind must be local|session"); } + const kind = parsed.kind; + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId: parsed.targetId, + feature: "storage clear", + run: async ({ cdpUrl, tab, pw }) => { + await pw.storageClearViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + kind, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/set/offline", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); + const targetId = resolveTargetIdFromBody(body); const offline = toBoolean(body.offline); if (offline === undefined) { return jsonError(res, 400, "offline is required"); } - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "offline"); - if (!pw) { - return; - } - await pw.setOfflineViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - offline, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "offline", + run: async ({ cdpUrl, tab, pw }) => { + await pw.setOfflineViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + offline, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/set/headers", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); + const targetId = resolveTargetIdFromBody(body); const headers = body.headers && typeof body.headers === "object" && !Array.isArray(body.headers) ? (body.headers as Record) @@ -275,98 +230,90 @@ export function registerBrowserAgentStorageRoutes( if (!headers) { return jsonError(res, 400, "headers is required"); } + const parsed: Record = {}; for (const [k, v] of Object.entries(headers)) { if (typeof v === "string") { parsed[k] = v; } } - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "headers"); - if (!pw) { - return; - } - await pw.setExtraHTTPHeadersViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - headers: parsed, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "headers", + run: async ({ cdpUrl, tab, pw }) => { + await pw.setExtraHTTPHeadersViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + headers: parsed, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/set/credentials", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); + const targetId = resolveTargetIdFromBody(body); const clear = toBoolean(body.clear) ?? false; const username = toStringOrEmpty(body.username) || undefined; const password = typeof body.password === "string" ? body.password : undefined; - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "http credentials"); - if (!pw) { - return; - } - await pw.setHttpCredentialsViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - username, - password, - clear, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "http credentials", + run: async ({ cdpUrl, tab, pw }) => { + await pw.setHttpCredentialsViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + username, + password, + clear, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/set/geolocation", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); + const targetId = resolveTargetIdFromBody(body); const clear = toBoolean(body.clear) ?? false; const latitude = toNumber(body.latitude); const longitude = toNumber(body.longitude); const accuracy = toNumber(body.accuracy) ?? undefined; const origin = toStringOrEmpty(body.origin) || undefined; - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "geolocation"); - if (!pw) { - return; - } - await pw.setGeolocationViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - latitude, - longitude, - accuracy, - origin, - clear, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "geolocation", + run: async ({ cdpUrl, tab, pw }) => { + await pw.setGeolocationViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + latitude, + longitude, + accuracy, + origin, + clear, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/set/media", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); + const targetId = resolveTargetIdFromBody(body); const schemeRaw = toStringOrEmpty(body.colorScheme); const colorScheme = schemeRaw === "dark" || schemeRaw === "light" || schemeRaw === "no-preference" @@ -377,104 +324,96 @@ export function registerBrowserAgentStorageRoutes( if (colorScheme === undefined) { return jsonError(res, 400, "colorScheme must be dark|light|no-preference|none"); } - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "media emulation"); - if (!pw) { - return; - } - await pw.emulateMediaViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - colorScheme, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "media emulation", + run: async ({ cdpUrl, tab, pw }) => { + await pw.emulateMediaViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + colorScheme, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/set/timezone", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); + const targetId = resolveTargetIdFromBody(body); const timezoneId = toStringOrEmpty(body.timezoneId); if (!timezoneId) { return jsonError(res, 400, "timezoneId is required"); } - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "timezone"); - if (!pw) { - return; - } - await pw.setTimezoneViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - timezoneId, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "timezone", + run: async ({ cdpUrl, tab, pw }) => { + await pw.setTimezoneViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + timezoneId, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/set/locale", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); + const targetId = resolveTargetIdFromBody(body); const locale = toStringOrEmpty(body.locale); if (!locale) { return jsonError(res, 400, "locale is required"); } - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "locale"); - if (!pw) { - return; - } - await pw.setLocaleViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - locale, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "locale", + run: async ({ cdpUrl, tab, pw }) => { + await pw.setLocaleViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + locale, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); app.post("/set/device", async (req, res) => { - const profileCtx = resolveProfileContext(req, res, ctx); - if (!profileCtx) { - return; - } const body = readBody(req); - const targetId = resolveBodyTargetId(body); + const targetId = resolveTargetIdFromBody(body); const name = toStringOrEmpty(body.name); if (!name) { return jsonError(res, 400, "name is required"); } - try { - const tab = await profileCtx.ensureTabAvailable(targetId); - const pw = await requirePwAi(res, "device emulation"); - if (!pw) { - return; - } - await pw.setDeviceViaPlaywright({ - cdpUrl: profileCtx.profile.cdpUrl, - targetId: tab.targetId, - name, - }); - res.json({ ok: true, targetId: tab.targetId }); - } catch (err) { - handleRouteError(ctx, res, err); - } + + await withPlaywrightRouteContext({ + req, + res, + ctx, + targetId, + feature: "device emulation", + run: async ({ cdpUrl, tab, pw }) => { + await pw.setDeviceViaPlaywright({ + cdpUrl, + targetId: tab.targetId, + name, + }); + res.json({ ok: true, targetId: tab.targetId }); + }, + }); }); }