import { accountDb, repoDb } from "./scoped-db";
import type { ScopedValue } from "./scoped-db";
import { devMailMode } from "./notify/dev-mail";
export const DEV_SCOPED_SQL_PATH = "/__dev/scoped-sql";
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
type Body = { scope: "repo" | "account"; id: string; sql: string; params?: ScopedValue[] };
function isBody(value: unknown): value is Body {
if (typeof value !== "object" || value === null) return false;
const body = value as Record<string, unknown>;
return (
(body.scope === "repo" || body.scope === "account") &&
typeof body.id === "string" &&
typeof body.sql === "string" &&
(body.params === undefined || Array.isArray(body.params))
);
}
/**
* E2E harness access to a RepoState / AccountState database, the scoped counterpart of `wrangler d1 execute`
* (those tables are no longer in D1). Same gate as the dev mail sink: 404 unless EMAIL_DEV_SINK=kv, PUBLIC_URL
* is loopback, and the request targets a loopback host. POST `{ scope, id, sql, params }`, returns the rows.
*/
export async function handleDevScopedSql(request: Request, url: URL): Promise<Response | null> {
if (url.pathname !== DEV_SCOPED_SQL_PATH) return null;
const host = url.hostname.toLowerCase();
const loopback = LOCAL_HOSTS.has(host) || host.endsWith(".localhost");
if (devMailMode() !== "kv" || !loopback || request.method !== "POST") {
return new Response("Not found\n", { status: 404 });
}
const body: unknown = await request.json().catch(() => null);
if (!isBody(body)) return new Response("Bad request\n", { status: 400 });
const db = body.scope === "repo" ? repoDb(body.id) : accountDb(body.id);
return Response.json(await db.all(body.sql, ...(body.params ?? [])));
}