refactor(browser): share playwright route context for debug/storage routes
This commit is contained in:
@@ -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),
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,16 @@ export function readBody(req: BrowserRequest): Record<string, unknown> {
|
||||
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) {
|
||||
const mapped = ctx.mapTabError(err);
|
||||
if (mapped) {
|
||||
@@ -66,3 +76,68 @@ export async function requirePwAi(
|
||||
);
|
||||
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 });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>).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<string, unknown>,
|
||||
): { 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<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,
|
||||
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<string, unknown>)
|
||||
@@ -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<string, unknown>)
|
||||
@@ -275,98 +230,90 @@ export function registerBrowserAgentStorageRoutes(
|
||||
if (!headers) {
|
||||
return jsonError(res, 400, "headers is required");
|
||||
}
|
||||
|
||||
const parsed: Record<string, string> = {};
|
||||
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 });
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user