新增案例库浏览器登录态

This commit is contained in:
czj
2026-07-08 17:02:37 +08:00
parent fd1b792f2c
commit b4472fe348
12 changed files with 646 additions and 75 deletions
+33 -1
View File
@@ -1,6 +1,10 @@
import { describe, expect, test } from "vitest";
import { requireApiAccess } from "../auth";
import {
apiSessionCookieName,
createApiSessionToken,
requireApiAccess,
} from "../auth";
describe("requireApiAccess", () => {
test("allows local test requests when auth is explicitly disabled", () => {
@@ -53,4 +57,32 @@ describe("requireApiAccess", () => {
expect(result.ok).toBe(true);
});
test("allows requests with a valid HttpOnly session cookie", () => {
const request = new Request("http://localhost/api/jobs", {
headers: {
cookie: `${apiSessionCookieName}=${createApiSessionToken("secret")}`,
},
});
const result = requireApiAccess(request, {
apiAccessKey: "secret",
authDisabled: false,
});
expect(result.ok).toBe(true);
});
test("rejects requests with an invalid session cookie", () => {
const request = new Request("http://localhost/api/jobs", {
headers: { cookie: `${apiSessionCookieName}=wrong` },
});
const result = requireApiAccess(request, {
apiAccessKey: "secret",
authDisabled: false,
});
expect(result.ok).toBe(false);
});
});
+47 -2
View File
@@ -1,7 +1,13 @@
import { createHmac, timingSafeEqual } from "node:crypto";
import { NextResponse } from "next/server";
import { getAppCloudflareEnv } from "../runtime/cloudflare";
export const apiSessionCookieName = "geo_api_session";
const sessionTokenMessage = "geo-agent-api-session:v1";
interface ApiAccessOptions {
apiAccessKey?: string;
authDisabled?: boolean;
@@ -33,7 +39,10 @@ export function requireApiAccess(
}
const provided = request.headers.get("x-api-key");
if (provided !== options.apiAccessKey) {
if (
provided !== options.apiAccessKey &&
!hasValidApiSession(request, options.apiAccessKey)
) {
return {
ok: false,
response: NextResponse.json({ error: "Unauthorized" }, { status: 401 }),
@@ -43,6 +52,42 @@ export function requireApiAccess(
return { ok: true };
}
function getConfiguredApiAccessKey() {
export function hasValidApiSession(request: Request, apiAccessKey: string) {
const cookieValue = parseCookieHeader(request.headers.get("cookie"))[
apiSessionCookieName
];
if (!cookieValue) return false;
return safeEqual(cookieValue, createApiSessionToken(apiAccessKey));
}
export function createApiSessionToken(apiAccessKey: string) {
return createHmac("sha256", apiAccessKey)
.update(sessionTokenMessage)
.digest("hex");
}
export function getConfiguredApiAccessKey() {
return getAppCloudflareEnv()?.API_ACCESS_KEY ?? process.env.API_ACCESS_KEY;
}
function parseCookieHeader(value: string | null) {
const cookies: Record<string, string> = {};
if (!value) return cookies;
for (const part of value.split(";")) {
const separator = part.indexOf("=");
if (separator < 0) continue;
const name = part.slice(0, separator).trim();
const cookieValue = part.slice(separator + 1).trim();
if (name) cookies[name] = decodeURIComponent(cookieValue);
}
return cookies;
}
function safeEqual(left: string, right: string) {
const leftBuffer = Buffer.from(left);
const rightBuffer = Buffer.from(right);
if (leftBuffer.length !== rightBuffer.length) return false;
return timingSafeEqual(leftBuffer, rightBuffer);
}