refactor(browser): share playwright route context for debug/storage routes

This commit is contained in:
Peter Steinberger
2026-02-18 21:57:54 +00:00
parent c4eaf7d0c2
commit 5d98c2ae7e
3 changed files with 464 additions and 463 deletions

View File

@@ -2,7 +2,12 @@ import crypto from "node:crypto";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { BrowserRouteContext } from "../server-context.js"; 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 { DEFAULT_TRACE_DIR, resolvePathWithinRoot } from "./path-output.js";
import type { BrowserRouteRegistrar } from "./types.js"; import type { BrowserRouteRegistrar } from "./types.js";
import { toBoolean, toStringOrEmpty } from "./utils.js"; import { toBoolean, toStringOrEmpty } from "./utils.js";
@@ -12,151 +17,133 @@ export function registerBrowserAgentDebugRoutes(
ctx: BrowserRouteContext, ctx: BrowserRouteContext,
) { ) {
app.get("/console", async (req, res) => { app.get("/console", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx); const targetId = resolveTargetIdFromQuery(req.query);
if (!profileCtx) {
return;
}
const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : "";
const level = typeof req.query.level === "string" ? req.query.level : ""; const level = typeof req.query.level === "string" ? req.query.level : "";
try { await withPlaywrightRouteContext({
const tab = await profileCtx.ensureTabAvailable(targetId || undefined); req,
const pw = await requirePwAi(res, "console messages"); res,
if (!pw) { ctx,
return; targetId,
} feature: "console messages",
const messages = await pw.getConsoleMessagesViaPlaywright({ run: async ({ cdpUrl, tab, pw }) => {
cdpUrl: profileCtx.profile.cdpUrl, const messages = await pw.getConsoleMessagesViaPlaywright({
targetId: tab.targetId, cdpUrl,
level: level.trim() || undefined, targetId: tab.targetId,
}); level: level.trim() || undefined,
res.json({ ok: true, messages, targetId: tab.targetId }); });
} catch (err) { res.json({ ok: true, messages, targetId: tab.targetId });
handleRouteError(ctx, res, err); },
} });
}); });
app.get("/errors", async (req, res) => { app.get("/errors", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx); const targetId = resolveTargetIdFromQuery(req.query);
if (!profileCtx) {
return;
}
const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : "";
const clear = toBoolean(req.query.clear) ?? false; const clear = toBoolean(req.query.clear) ?? false;
try { await withPlaywrightRouteContext({
const tab = await profileCtx.ensureTabAvailable(targetId || undefined); req,
const pw = await requirePwAi(res, "page errors"); res,
if (!pw) { ctx,
return; targetId,
} feature: "page errors",
const result = await pw.getPageErrorsViaPlaywright({ run: async ({ cdpUrl, tab, pw }) => {
cdpUrl: profileCtx.profile.cdpUrl, const result = await pw.getPageErrorsViaPlaywright({
targetId: tab.targetId, cdpUrl,
clear, targetId: tab.targetId,
}); clear,
res.json({ ok: true, targetId: tab.targetId, ...result }); });
} catch (err) { res.json({ ok: true, targetId: tab.targetId, ...result });
handleRouteError(ctx, res, err); },
} });
}); });
app.get("/requests", async (req, res) => { app.get("/requests", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx); const targetId = resolveTargetIdFromQuery(req.query);
if (!profileCtx) {
return;
}
const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : "";
const filter = typeof req.query.filter === "string" ? req.query.filter : ""; const filter = typeof req.query.filter === "string" ? req.query.filter : "";
const clear = toBoolean(req.query.clear) ?? false; const clear = toBoolean(req.query.clear) ?? false;
try { await withPlaywrightRouteContext({
const tab = await profileCtx.ensureTabAvailable(targetId || undefined); req,
const pw = await requirePwAi(res, "network requests"); res,
if (!pw) { ctx,
return; targetId,
} feature: "network requests",
const result = await pw.getNetworkRequestsViaPlaywright({ run: async ({ cdpUrl, tab, pw }) => {
cdpUrl: profileCtx.profile.cdpUrl, const result = await pw.getNetworkRequestsViaPlaywright({
targetId: tab.targetId, cdpUrl,
filter: filter.trim() || undefined, targetId: tab.targetId,
clear, filter: filter.trim() || undefined,
}); clear,
res.json({ ok: true, targetId: tab.targetId, ...result }); });
} catch (err) { res.json({ ok: true, targetId: tab.targetId, ...result });
handleRouteError(ctx, res, err); },
} });
}); });
app.post("/trace/start", async (req, res) => { app.post("/trace/start", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined; const targetId = resolveTargetIdFromBody(body);
const screenshots = toBoolean(body.screenshots) ?? undefined; const screenshots = toBoolean(body.screenshots) ?? undefined;
const snapshots = toBoolean(body.snapshots) ?? undefined; const snapshots = toBoolean(body.snapshots) ?? undefined;
const sources = toBoolean(body.sources) ?? undefined; const sources = toBoolean(body.sources) ?? undefined;
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "trace start"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.traceStartViaPlaywright({ feature: "trace start",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.traceStartViaPlaywright({
screenshots, cdpUrl,
snapshots, targetId: tab.targetId,
sources, screenshots,
}); snapshots,
res.json({ ok: true, targetId: tab.targetId }); sources,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.post("/trace/stop", async (req, res) => { app.post("/trace/stop", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined; const targetId = resolveTargetIdFromBody(body);
const out = toStringOrEmpty(body.path) || ""; const out = toStringOrEmpty(body.path) || "";
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "trace stop"); req,
if (!pw) { res,
return; ctx,
} targetId,
const id = crypto.randomUUID(); feature: "trace stop",
const dir = DEFAULT_TRACE_DIR; run: async ({ cdpUrl, tab, pw }) => {
await fs.mkdir(dir, { recursive: true }); const id = crypto.randomUUID();
const tracePathResult = resolvePathWithinRoot({ const dir = DEFAULT_TRACE_DIR;
rootDir: dir, await fs.mkdir(dir, { recursive: true });
requestedPath: out, const tracePathResult = resolvePathWithinRoot({
scopeLabel: "trace directory", rootDir: dir,
defaultFileName: `browser-trace-${id}.zip`, requestedPath: out,
}); scopeLabel: "trace directory",
if (!tracePathResult.ok) { defaultFileName: `browser-trace-${id}.zip`,
res.status(400).json({ error: tracePathResult.error }); });
return; if (!tracePathResult.ok) {
} res.status(400).json({ error: tracePathResult.error });
const tracePath = tracePathResult.path; return;
await pw.traceStopViaPlaywright({ }
cdpUrl: profileCtx.profile.cdpUrl, const tracePath = tracePathResult.path;
targetId: tab.targetId, await pw.traceStopViaPlaywright({
path: tracePath, cdpUrl,
}); targetId: tab.targetId,
res.json({ path: tracePath,
ok: true, });
targetId: tab.targetId, res.json({
path: path.resolve(tracePath), ok: true,
}); targetId: tab.targetId,
} catch (err) { path: path.resolve(tracePath),
handleRouteError(ctx, res, err); });
} },
});
}); });
} }

View File

@@ -22,6 +22,16 @@ export function readBody(req: BrowserRequest): Record<string, unknown> {
return body; return body;
} }
export function resolveTargetIdFromBody(body: Record<string, unknown>): string | undefined {
const targetId = typeof body.targetId === "string" ? body.targetId.trim() : "";
return targetId || undefined;
}
export function resolveTargetIdFromQuery(query: Record<string, unknown>): string | undefined {
const targetId = typeof query.targetId === "string" ? query.targetId.trim() : "";
return targetId || undefined;
}
export function handleRouteError(ctx: BrowserRouteContext, res: BrowserResponse, err: unknown) { export function handleRouteError(ctx: BrowserRouteContext, res: BrowserResponse, err: unknown) {
const mapped = ctx.mapTabError(err); const mapped = ctx.mapTabError(err);
if (mapped) { if (mapped) {
@@ -66,3 +76,68 @@ export async function requirePwAi(
); );
return null; return null;
} }
type RouteTabContext = {
profileCtx: ProfileContext;
tab: Awaited<ReturnType<ProfileContext["ensureTabAvailable"]>>;
cdpUrl: string;
};
type RouteTabPwContext = RouteTabContext & {
pw: PwAiModule;
};
type RouteWithTabParams<T> = {
req: BrowserRequest;
res: BrowserResponse;
ctx: BrowserRouteContext;
targetId?: string;
run: (ctx: RouteTabContext) => Promise<T>;
};
export async function withRouteTabContext<T>(
params: RouteWithTabParams<T>,
): Promise<T | undefined> {
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<T> = {
req: BrowserRequest;
res: BrowserResponse;
ctx: BrowserRouteContext;
targetId?: string;
feature: string;
run: (ctx: RouteTabPwContext) => Promise<T>;
};
export async function withPlaywrightRouteContext<T>(
params: RouteWithPwParams<T>,
): Promise<T | undefined> {
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 });
},
});
}

View File

@@ -1,60 +1,29 @@
import type { BrowserRouteContext } from "../server-context.js"; import type { BrowserRouteContext } from "../server-context.js";
import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js"; import {
import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js"; readBody,
resolveTargetIdFromBody,
resolveTargetIdFromQuery,
withPlaywrightRouteContext,
} from "./agent.shared.js";
import type { BrowserRouteRegistrar } from "./types.js";
import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js"; import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
type StorageKind = "local" | "session"; type StorageKind = "local" | "session";
function resolveBodyTargetId(body: unknown): string | undefined { export function parseStorageKind(raw: string): StorageKind | null {
if (!body || typeof body !== "object" || Array.isArray(body)) {
return undefined;
}
const targetId = toStringOrEmpty((body as Record<string, unknown>).targetId);
return targetId || undefined;
}
function parseStorageKind(raw: string): StorageKind | null {
if (raw === "local" || raw === "session") { if (raw === "local" || raw === "session") {
return raw; return raw;
} }
return null; return null;
} }
function parseStorageMutationRequest( export function parseStorageMutationRequest(
kindParam: unknown, kindParam: unknown,
body: unknown, body: Record<string, unknown>,
): { kind: StorageKind | null; targetId: string | undefined } { ): { kind: StorageKind | null; targetId: string | undefined } {
return { return {
kind: parseStorageKind(toStringOrEmpty(kindParam)), kind: parseStorageKind(toStringOrEmpty(kindParam)),
targetId: resolveBodyTargetId(body), targetId: resolveTargetIdFromBody(body),
};
}
function resolveStorageMutationContext(params: {
req: BrowserRequest;
res: BrowserResponse;
ctx: BrowserRouteContext;
}): {
profileCtx: NonNullable<ReturnType<typeof resolveProfileContext>>;
body: Record<string, unknown>;
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,
}; };
} }
@@ -63,34 +32,26 @@ export function registerBrowserAgentStorageRoutes(
ctx: BrowserRouteContext, ctx: BrowserRouteContext,
) { ) {
app.get("/cookies", async (req, res) => { app.get("/cookies", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx); const targetId = resolveTargetIdFromQuery(req.query);
if (!profileCtx) { await withPlaywrightRouteContext({
return; req,
} res,
const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; ctx,
try { targetId,
const tab = await profileCtx.ensureTabAvailable(targetId || undefined); feature: "cookies",
const pw = await requirePwAi(res, "cookies"); run: async ({ cdpUrl, tab, pw }) => {
if (!pw) { const result = await pw.cookiesGetViaPlaywright({
return; cdpUrl,
} targetId: tab.targetId,
const result = await pw.cookiesGetViaPlaywright({ });
cdpUrl: profileCtx.profile.cdpUrl, res.json({ ok: true, targetId: tab.targetId, ...result });
targetId: tab.targetId, },
}); });
res.json({ ok: true, targetId: tab.targetId, ...result });
} catch (err) {
handleRouteError(ctx, res, err);
}
}); });
app.post("/cookies/set", async (req, res) => { app.post("/cookies/set", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
const cookie = const cookie =
body.cookie && typeof body.cookie === "object" && !Array.isArray(body.cookie) body.cookie && typeof body.cookie === "object" && !Array.isArray(body.cookie)
? (body.cookie as Record<string, unknown>) ? (body.cookie as Record<string, unknown>)
@@ -98,176 +59,170 @@ export function registerBrowserAgentStorageRoutes(
if (!cookie) { if (!cookie) {
return jsonError(res, 400, "cookie is required"); return jsonError(res, 400, "cookie is required");
} }
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "cookies set"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.cookiesSetViaPlaywright({ feature: "cookies set",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.cookiesSetViaPlaywright({
cookie: { cdpUrl,
name: toStringOrEmpty(cookie.name), targetId: tab.targetId,
value: toStringOrEmpty(cookie.value), cookie: {
url: toStringOrEmpty(cookie.url) || undefined, name: toStringOrEmpty(cookie.name),
domain: toStringOrEmpty(cookie.domain) || undefined, value: toStringOrEmpty(cookie.value),
path: toStringOrEmpty(cookie.path) || undefined, url: toStringOrEmpty(cookie.url) || undefined,
expires: toNumber(cookie.expires) ?? undefined, domain: toStringOrEmpty(cookie.domain) || undefined,
httpOnly: toBoolean(cookie.httpOnly) ?? undefined, path: toStringOrEmpty(cookie.path) || undefined,
secure: toBoolean(cookie.secure) ?? undefined, expires: toNumber(cookie.expires) ?? undefined,
sameSite: httpOnly: toBoolean(cookie.httpOnly) ?? undefined,
cookie.sameSite === "Lax" || cookie.sameSite === "None" || cookie.sameSite === "Strict" secure: toBoolean(cookie.secure) ?? undefined,
? cookie.sameSite sameSite:
: undefined, cookie.sameSite === "Lax" ||
}, cookie.sameSite === "None" ||
}); cookie.sameSite === "Strict"
res.json({ ok: true, targetId: tab.targetId }); ? cookie.sameSite
} catch (err) { : undefined,
handleRouteError(ctx, res, err); },
} });
res.json({ ok: true, targetId: tab.targetId });
},
});
}); });
app.post("/cookies/clear", async (req, res) => { app.post("/cookies/clear", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "cookies clear"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.cookiesClearViaPlaywright({ feature: "cookies clear",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.cookiesClearViaPlaywright({
}); cdpUrl,
res.json({ ok: true, targetId: tab.targetId }); targetId: tab.targetId,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.get("/storage/:kind", async (req, res) => { app.get("/storage/:kind", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const kind = parseStorageKind(toStringOrEmpty(req.params.kind)); const kind = parseStorageKind(toStringOrEmpty(req.params.kind));
if (!kind) { if (!kind) {
return jsonError(res, 400, "kind must be local|session"); return jsonError(res, 400, "kind must be local|session");
} }
const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; const targetId = resolveTargetIdFromQuery(req.query);
const key = typeof req.query.key === "string" ? req.query.key : ""; const key = toStringOrEmpty(req.query.key);
try {
const tab = await profileCtx.ensureTabAvailable(targetId || undefined); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "storage get"); req,
if (!pw) { res,
return; ctx,
} targetId,
const result = await pw.storageGetViaPlaywright({ feature: "storage get",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, const result = await pw.storageGetViaPlaywright({
kind, cdpUrl,
key: key.trim() || undefined, targetId: tab.targetId,
}); kind,
res.json({ ok: true, targetId: tab.targetId, ...result }); key: key.trim() || undefined,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId, ...result });
} },
});
}); });
app.post("/storage/:kind/set", async (req, res) => { app.post("/storage/:kind/set", async (req, res) => {
const mutation = resolveStorageMutationContext({ req, res, ctx }); const body = readBody(req);
if (!mutation) { const parsed = parseStorageMutationRequest(req.params.kind, body);
return; 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); const key = toStringOrEmpty(body.key);
if (!key) { if (!key) {
return jsonError(res, 400, "key is required"); return jsonError(res, 400, "key is required");
} }
const value = typeof body.value === "string" ? body.value : ""; const value = typeof body.value === "string" ? body.value : "";
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "storage set"); req,
if (!pw) { res,
return; ctx,
} targetId: parsed.targetId,
await pw.storageSetViaPlaywright({ feature: "storage set",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.storageSetViaPlaywright({
kind, cdpUrl,
key, targetId: tab.targetId,
value, kind,
}); key,
res.json({ ok: true, targetId: tab.targetId }); value,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.post("/storage/:kind/clear", async (req, res) => { app.post("/storage/:kind/clear", async (req, res) => {
const mutation = resolveStorageMutationContext({ req, res, ctx }); const body = readBody(req);
if (!mutation) { const parsed = parseStorageMutationRequest(req.params.kind, body);
return; if (!parsed.kind) {
} return jsonError(res, 400, "kind must be local|session");
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 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) => { app.post("/set/offline", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
const offline = toBoolean(body.offline); const offline = toBoolean(body.offline);
if (offline === undefined) { if (offline === undefined) {
return jsonError(res, 400, "offline is required"); return jsonError(res, 400, "offline is required");
} }
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "offline"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.setOfflineViaPlaywright({ feature: "offline",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.setOfflineViaPlaywright({
offline, cdpUrl,
}); targetId: tab.targetId,
res.json({ ok: true, targetId: tab.targetId }); offline,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.post("/set/headers", async (req, res) => { app.post("/set/headers", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
const headers = const headers =
body.headers && typeof body.headers === "object" && !Array.isArray(body.headers) body.headers && typeof body.headers === "object" && !Array.isArray(body.headers)
? (body.headers as Record<string, unknown>) ? (body.headers as Record<string, unknown>)
@@ -275,98 +230,90 @@ export function registerBrowserAgentStorageRoutes(
if (!headers) { if (!headers) {
return jsonError(res, 400, "headers is required"); return jsonError(res, 400, "headers is required");
} }
const parsed: Record<string, string> = {}; const parsed: Record<string, string> = {};
for (const [k, v] of Object.entries(headers)) { for (const [k, v] of Object.entries(headers)) {
if (typeof v === "string") { if (typeof v === "string") {
parsed[k] = v; parsed[k] = v;
} }
} }
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "headers"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.setExtraHTTPHeadersViaPlaywright({ feature: "headers",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.setExtraHTTPHeadersViaPlaywright({
headers: parsed, cdpUrl,
}); targetId: tab.targetId,
res.json({ ok: true, targetId: tab.targetId }); headers: parsed,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.post("/set/credentials", async (req, res) => { app.post("/set/credentials", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
const clear = toBoolean(body.clear) ?? false; const clear = toBoolean(body.clear) ?? false;
const username = toStringOrEmpty(body.username) || undefined; const username = toStringOrEmpty(body.username) || undefined;
const password = typeof body.password === "string" ? body.password : undefined; const password = typeof body.password === "string" ? body.password : undefined;
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "http credentials"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.setHttpCredentialsViaPlaywright({ feature: "http credentials",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.setHttpCredentialsViaPlaywright({
username, cdpUrl,
password, targetId: tab.targetId,
clear, username,
}); password,
res.json({ ok: true, targetId: tab.targetId }); clear,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.post("/set/geolocation", async (req, res) => { app.post("/set/geolocation", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
const clear = toBoolean(body.clear) ?? false; const clear = toBoolean(body.clear) ?? false;
const latitude = toNumber(body.latitude); const latitude = toNumber(body.latitude);
const longitude = toNumber(body.longitude); const longitude = toNumber(body.longitude);
const accuracy = toNumber(body.accuracy) ?? undefined; const accuracy = toNumber(body.accuracy) ?? undefined;
const origin = toStringOrEmpty(body.origin) || undefined; const origin = toStringOrEmpty(body.origin) || undefined;
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "geolocation"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.setGeolocationViaPlaywright({ feature: "geolocation",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.setGeolocationViaPlaywright({
latitude, cdpUrl,
longitude, targetId: tab.targetId,
accuracy, latitude,
origin, longitude,
clear, accuracy,
}); origin,
res.json({ ok: true, targetId: tab.targetId }); clear,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.post("/set/media", async (req, res) => { app.post("/set/media", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
const schemeRaw = toStringOrEmpty(body.colorScheme); const schemeRaw = toStringOrEmpty(body.colorScheme);
const colorScheme = const colorScheme =
schemeRaw === "dark" || schemeRaw === "light" || schemeRaw === "no-preference" schemeRaw === "dark" || schemeRaw === "light" || schemeRaw === "no-preference"
@@ -377,104 +324,96 @@ export function registerBrowserAgentStorageRoutes(
if (colorScheme === undefined) { if (colorScheme === undefined) {
return jsonError(res, 400, "colorScheme must be dark|light|no-preference|none"); return jsonError(res, 400, "colorScheme must be dark|light|no-preference|none");
} }
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "media emulation"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.emulateMediaViaPlaywright({ feature: "media emulation",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.emulateMediaViaPlaywright({
colorScheme, cdpUrl,
}); targetId: tab.targetId,
res.json({ ok: true, targetId: tab.targetId }); colorScheme,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.post("/set/timezone", async (req, res) => { app.post("/set/timezone", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
const timezoneId = toStringOrEmpty(body.timezoneId); const timezoneId = toStringOrEmpty(body.timezoneId);
if (!timezoneId) { if (!timezoneId) {
return jsonError(res, 400, "timezoneId is required"); return jsonError(res, 400, "timezoneId is required");
} }
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "timezone"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.setTimezoneViaPlaywright({ feature: "timezone",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.setTimezoneViaPlaywright({
timezoneId, cdpUrl,
}); targetId: tab.targetId,
res.json({ ok: true, targetId: tab.targetId }); timezoneId,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.post("/set/locale", async (req, res) => { app.post("/set/locale", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
const locale = toStringOrEmpty(body.locale); const locale = toStringOrEmpty(body.locale);
if (!locale) { if (!locale) {
return jsonError(res, 400, "locale is required"); return jsonError(res, 400, "locale is required");
} }
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "locale"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.setLocaleViaPlaywright({ feature: "locale",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.setLocaleViaPlaywright({
locale, cdpUrl,
}); targetId: tab.targetId,
res.json({ ok: true, targetId: tab.targetId }); locale,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
app.post("/set/device", async (req, res) => { app.post("/set/device", async (req, res) => {
const profileCtx = resolveProfileContext(req, res, ctx);
if (!profileCtx) {
return;
}
const body = readBody(req); const body = readBody(req);
const targetId = resolveBodyTargetId(body); const targetId = resolveTargetIdFromBody(body);
const name = toStringOrEmpty(body.name); const name = toStringOrEmpty(body.name);
if (!name) { if (!name) {
return jsonError(res, 400, "name is required"); return jsonError(res, 400, "name is required");
} }
try {
const tab = await profileCtx.ensureTabAvailable(targetId); await withPlaywrightRouteContext({
const pw = await requirePwAi(res, "device emulation"); req,
if (!pw) { res,
return; ctx,
} targetId,
await pw.setDeviceViaPlaywright({ feature: "device emulation",
cdpUrl: profileCtx.profile.cdpUrl, run: async ({ cdpUrl, tab, pw }) => {
targetId: tab.targetId, await pw.setDeviceViaPlaywright({
name, cdpUrl,
}); targetId: tab.targetId,
res.json({ ok: true, targetId: tab.targetId }); name,
} catch (err) { });
handleRouteError(ctx, res, err); res.json({ ok: true, targetId: tab.targetId });
} },
});
}); });
} }