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,125 +17,108 @@ 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",
run: async ({ cdpUrl, tab, pw }) => {
const messages = await pw.getConsoleMessagesViaPlaywright({ const messages = await pw.getConsoleMessagesViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
level: level.trim() || undefined, level: level.trim() || undefined,
}); });
res.json({ ok: true, messages, targetId: tab.targetId }); res.json({ ok: true, messages, targetId: tab.targetId });
} catch (err) { },
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",
run: async ({ cdpUrl, tab, pw }) => {
const result = await pw.getPageErrorsViaPlaywright({ const result = await pw.getPageErrorsViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
clear, clear,
}); });
res.json({ ok: true, targetId: tab.targetId, ...result }); res.json({ ok: true, targetId: tab.targetId, ...result });
} catch (err) { },
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",
run: async ({ cdpUrl, tab, pw }) => {
const result = await pw.getNetworkRequestsViaPlaywright({ const result = await pw.getNetworkRequestsViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
filter: filter.trim() || undefined, filter: filter.trim() || undefined,
clear, clear,
}); });
res.json({ ok: true, targetId: tab.targetId, ...result }); res.json({ ok: true, targetId: tab.targetId, ...result });
} catch (err) { },
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,
feature: "trace start",
run: async ({ cdpUrl, tab, pw }) => {
await pw.traceStartViaPlaywright({ await pw.traceStartViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
screenshots, screenshots,
snapshots, snapshots,
sources, sources,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "trace stop",
run: async ({ cdpUrl, tab, pw }) => {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const dir = DEFAULT_TRACE_DIR; const dir = DEFAULT_TRACE_DIR;
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
@@ -146,7 +134,7 @@ export function registerBrowserAgentDebugRoutes(
} }
const tracePath = tracePathResult.path; const tracePath = tracePathResult.path;
await pw.traceStopViaPlaywright({ await pw.traceStopViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
path: tracePath, path: tracePath,
}); });
@@ -155,8 +143,7 @@ export function registerBrowserAgentDebugRoutes(
targetId: tab.targetId, targetId: tab.targetId,
path: path.resolve(tracePath), path: path.resolve(tracePath),
}); });
} catch (err) { },
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) {
return;
}
const result = await pw.cookiesGetViaPlaywright({ const result = await pw.cookiesGetViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
}); });
res.json({ ok: true, targetId: tab.targetId, ...result }); 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,14 +59,16 @@ 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,
feature: "cookies set",
run: async ({ cdpUrl, tab, pw }) => {
await pw.cookiesSetViaPlaywright({ await pw.cookiesSetViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
cookie: { cookie: {
name: toStringOrEmpty(cookie.name), name: toStringOrEmpty(cookie.name),
@@ -117,157 +80,149 @@ export function registerBrowserAgentStorageRoutes(
httpOnly: toBoolean(cookie.httpOnly) ?? undefined, httpOnly: toBoolean(cookie.httpOnly) ?? undefined,
secure: toBoolean(cookie.secure) ?? undefined, secure: toBoolean(cookie.secure) ?? undefined,
sameSite: sameSite:
cookie.sameSite === "Lax" || cookie.sameSite === "None" || cookie.sameSite === "Strict" cookie.sameSite === "Lax" ||
cookie.sameSite === "None" ||
cookie.sameSite === "Strict"
? cookie.sameSite ? cookie.sameSite
: undefined, : undefined,
}, },
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "cookies clear",
run: async ({ cdpUrl, tab, pw }) => {
await pw.cookiesClearViaPlaywright({ await pw.cookiesClearViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "storage get",
run: async ({ cdpUrl, tab, pw }) => {
const result = await pw.storageGetViaPlaywright({ const result = await pw.storageGetViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
kind, kind,
key: key.trim() || undefined, key: key.trim() || undefined,
}); });
res.json({ ok: true, targetId: tab.targetId, ...result }); res.json({ ok: true, targetId: tab.targetId, ...result });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "storage set",
run: async ({ cdpUrl, tab, pw }) => {
await pw.storageSetViaPlaywright({ await pw.storageSetViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
kind, kind,
key, key,
value, value,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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;
} }
const kind = parsed.kind;
await withPlaywrightRouteContext({
req,
res,
ctx,
targetId: parsed.targetId,
feature: "storage clear",
run: async ({ cdpUrl, tab, pw }) => {
await pw.storageClearViaPlaywright({ await pw.storageClearViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
kind, kind,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "offline",
run: async ({ cdpUrl, tab, pw }) => {
await pw.setOfflineViaPlaywright({ await pw.setOfflineViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
offline, offline,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,78 +230,75 @@ 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,
feature: "headers",
run: async ({ cdpUrl, tab, pw }) => {
await pw.setExtraHTTPHeadersViaPlaywright({ await pw.setExtraHTTPHeadersViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
headers: parsed, headers: parsed,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "http credentials",
run: async ({ cdpUrl, tab, pw }) => {
await pw.setHttpCredentialsViaPlaywright({ await pw.setHttpCredentialsViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
username, username,
password, password,
clear, clear,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "geolocation",
run: async ({ cdpUrl, tab, pw }) => {
await pw.setGeolocationViaPlaywright({ await pw.setGeolocationViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
latitude, latitude,
longitude, longitude,
@@ -355,18 +307,13 @@ export function registerBrowserAgentStorageRoutes(
clear, clear,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "media emulation",
run: async ({ cdpUrl, tab, pw }) => {
await pw.emulateMediaViaPlaywright({ await pw.emulateMediaViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
colorScheme, colorScheme,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "timezone",
run: async ({ cdpUrl, tab, pw }) => {
await pw.setTimezoneViaPlaywright({ await pw.setTimezoneViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
timezoneId, timezoneId,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "locale",
run: async ({ cdpUrl, tab, pw }) => {
await pw.setLocaleViaPlaywright({ await pw.setLocaleViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
locale, locale,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
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,
feature: "device emulation",
run: async ({ cdpUrl, tab, pw }) => {
await pw.setDeviceViaPlaywright({ await pw.setDeviceViaPlaywright({
cdpUrl: profileCtx.profile.cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
name, name,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
} catch (err) { },
handleRouteError(ctx, res, err); });
}
}); });
} }