feat: complete TASK-WP0-03 browser gate

This commit is contained in:
suyx
2026-07-27 18:23:20 +08:00
parent dbe3e73b91
commit 878788b1e2
25 changed files with 3227 additions and 21 deletions
+3 -2
View File
@@ -3,10 +3,11 @@ import { describe, expect, it } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
describe("Fastify skeleton", () => {
it("becomes ready without adding a product route", async () => {
it("becomes ready with only the Supervisor health exception", async () => {
const app = await createApp();
await app.ready();
expect(app.printRoutes()).not.toContain("health");
expect(app.printRoutes()).toContain("healthz");
expect(app.printRoutes()).not.toContain("api/v1/health");
await app.close();
});
});
+12 -6
View File
@@ -14,11 +14,14 @@ afterAll(async () => {
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_API;
if (!evidenceDirectory) return;
const app = await createApp();
const app = await createApp({ browserGate: false });
await app.ready();
const document = app.swagger();
const response = await app.inject({
headers: { "x-correlation-id": "018f6d52-3348-7a3a-a741-7f65f31f9218" },
headers: {
host: "127.0.0.1:43121",
"x-correlation-id": "018f6d52-3348-7a3a-a741-7f65f31f9218",
},
method: "GET",
url: "/api/v1/bootstrap",
});
@@ -52,7 +55,7 @@ afterAll(async () => {
describe("TDD-WP0-API-001 schema envelope", () => {
it("generates the committed OpenAPI 3.1 snapshot from runtime schemas", async () => {
const app = await createApp();
const app = await createApp({ browserGate: false });
await app.ready();
const document = app.swagger();
@@ -79,16 +82,19 @@ describe("TDD-WP0-API-001 schema envelope", () => {
});
it("echoes a valid correlation UUID and replaces an invalid one", async () => {
const app = await createApp();
const app = await createApp({ browserGate: false });
await app.ready();
const accepted = await app.inject({
headers: { "x-correlation-id": "018f6d52-3348-7a3a-a741-7f65f31f9218" },
headers: {
host: "127.0.0.1:43121",
"x-correlation-id": "018f6d52-3348-7a3a-a741-7f65f31f9218",
},
method: "GET",
url: "/api/v1/bootstrap",
});
const replaced = await app.inject({
headers: { "x-correlation-id": "not-a-uuid" },
headers: { host: "127.0.0.1:43121", "x-correlation-id": "not-a-uuid" },
method: "GET",
url: "/api/v1/bootstrap",
});
+257
View File
@@ -0,0 +1,257 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { afterAll, describe, expect, it, vi } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
import {
browserSupportFixture,
testBrowserSupportRelease,
} from "../helpers/browser-support-fixture.js";
const supportedEdge = browserSupportFixture({
brand: "Microsoft Edge",
fullVersion: "150.0.4078.99",
});
const rejectedIdentityCases = [
{
expectedReason: "platform_unsupported",
fixture: browserSupportFixture({
brand: "Microsoft Edge",
fullVersion: "150.0.4078.99",
platform: "macOS",
}),
name: "non-Windows platform",
},
{
expectedReason: "brand_unsupported",
fixture: browserSupportFixture({ brand: "Opera", fullVersion: "150.0.0.0" }),
name: "other brand",
},
{
expectedReason: "version_unsupported",
fixture: browserSupportFixture({ brand: "Microsoft Edge", fullVersion: "149.0.0.0" }),
name: "wrong major",
},
] as const;
const unavailableIdentityCases = [
{ mutate: () => ({ headers: { host: "127.0.0.1:43121" }, payload: supportedEdge.body }), name: "missing UA-CH" },
{
mutate: () => ({
headers: { ...supportedEdge.headers, "sec-ch-ua-full-version-list": '"broken"' },
payload: supportedEdge.body,
}),
name: "unparseable UA-CH",
},
{
mutate: () => ({
headers: supportedEdge.headers,
payload: { ...supportedEdge.body, platform: "macOS" },
}),
name: "conflicting UA-CH",
},
] as const;
function supportCookie(response: { headers: Record<string, string | string[] | undefined> }) {
const header = response.headers["set-cookie"];
const value = Array.isArray(header) ? header[0] : header;
return value?.split(";", 1)[0];
}
afterAll(async () => {
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_BRW_BLOCKED;
if (!evidenceDirectory) return;
const responses = [];
const app = await createApp({ browserSupportRelease: testBrowserSupportRelease } as never);
for (const testCase of rejectedIdentityCases) {
const response = await app.inject({
headers: testCase.fixture.headers,
method: "POST",
payload: testCase.fixture.body,
url: "/api/v1/support/check",
});
responses.push({ body: response.json(), name: testCase.name, status_code: response.statusCode });
}
for (const testCase of unavailableIdentityCases) {
const input = testCase.mutate();
const response = await app.inject({
headers: input.headers,
method: "POST",
payload: input.payload,
url: "/api/v1/support/check",
});
responses.push({ body: response.json(), name: testCase.name, status_code: response.statusCode });
}
const bootstrap = vi.fn(() => ({
app_version: "0.0.0",
dependencies: [],
model_summary: {
config_set_version: null,
configured_default_model_id: null,
recommended_model_id: null,
runtime_availability_version: null,
},
public_features: [],
}));
const rawApp = await createApp({ bootstrap, browserSupportRelease: testBrowserSupportRelease } as never);
const rawResponse = await rawApp.inject({
headers: { host: "127.0.0.1:43121", "user-agent": "raw-client" },
method: "GET",
url: "/api/v1/bootstrap",
});
responses.push({ body: rawResponse.json(), name: "raw product API", status_code: rawResponse.statusCode });
await rawApp.close();
await app.close();
if (responses.some(({ status_code }) => status_code !== 426) || bootstrap.mock.calls.length !== 0) {
throw new Error("Hard-block evidence did not preserve the 426 zero-business-access boundary.");
}
mkdirSync(evidenceDirectory, { recursive: true });
writeFileSync(
resolve(evidenceDirectory, "response.json"),
`${JSON.stringify({ cases: responses, status: "passed" }, null, 2)}\n`,
);
writeFileSync(
resolve(evidenceDirectory, "db-access.json"),
`${JSON.stringify({ business_provider_calls: 0, reads: 0, status: "passed", writes: 0 }, null, 2)}\n`,
);
writeFileSync(
resolve(evidenceDirectory, "external-calls.json"),
`${JSON.stringify({ configured_clients: [], observed_calls: 0, status: "passed" }, null, 2)}\n`,
);
});
describe("TDD-WP0-BRW-001 supported browser contract", () => {
it("cross-checks UA-CH and issues only a short-lived signed support cookie", async () => {
const app = await createApp({ browserSupportRelease: testBrowserSupportRelease } as never);
const checked = await app.inject({
headers: supportedEdge.headers,
method: "POST",
payload: supportedEdge.body,
url: "/api/v1/support/check",
});
expect(checked.statusCode).toBe(200);
expect(checked.json()).toEqual({
app_version: "1.2.3-test",
browser: { brand: "Microsoft Edge", major: 150 },
status: "supported",
supported_browsers: [
{ brand: "Google Chrome", major: 150 },
{ brand: "Microsoft Edge", major: 150 },
],
});
expect(checked.headers["set-cookie"]).toContain("dada_browser_support=");
expect(checked.headers["set-cookie"]).toContain("HttpOnly");
expect(checked.headers["set-cookie"]).toContain("SameSite=Strict");
expect(checked.headers["set-cookie"]).toContain("Max-Age=86400");
const cookie = supportCookie(checked);
expect(cookie).toBeDefined();
const product = await app.inject({
headers: {
cookie,
host: "127.0.0.1:43121",
"sec-ch-ua": supportedEdge.headers["sec-ch-ua"],
},
method: "GET",
url: "/api/v1/bootstrap",
});
expect(product.statusCode).toBe(200);
await app.close();
const restarted = await createApp({ browserSupportRelease: testBrowserSupportRelease } as never);
const staleCookie = await restarted.inject({
headers: {
cookie,
host: "127.0.0.1:43121",
"sec-ch-ua": supportedEdge.headers["sec-ch-ua"],
},
method: "GET",
url: "/api/v1/bootstrap",
});
expect(staleCookie.statusCode).toBe(426);
await restarted.close();
});
});
describe("TDD-WP0-BRW-002 hard block", () => {
it.each(rejectedIdentityCases)("returns 426 for $name", async ({ expectedReason, fixture }) => {
const app = await createApp({ browserSupportRelease: testBrowserSupportRelease } as never);
const response = await app.inject({
headers: fixture.headers,
method: "POST",
payload: fixture.body,
url: "/api/v1/support/check",
});
expect(response.statusCode).toBe(426);
expect(response.json()).toMatchObject({
error: {
code: "BROWSER_UNSUPPORTED",
details: {
reason: expectedReason,
supported_browsers: [
{ brand: "Google Chrome", major: 150 },
{ brand: "Microsoft Edge", major: 150 },
],
},
message_key: "browser.unsupported",
},
});
expect(response.body).not.toContain("Opera");
await app.close();
});
it.each(unavailableIdentityCases)("fails closed for $name", async ({ mutate }) => {
const app = await createApp({ browserSupportRelease: testBrowserSupportRelease } as never);
const input = mutate();
const response = await app.inject({
headers: input.headers,
method: "POST",
payload: input.payload,
url: "/api/v1/support/check",
});
expect(response.statusCode).toBe(426);
expect(response.json()).toMatchObject({
error: { details: { reason: "identity_unavailable" } },
});
await app.close();
});
it("blocks raw product API access before any business provider runs", async () => {
const bootstrap = vi.fn(() => ({
app_version: "0.0.0",
dependencies: [],
model_summary: {
config_set_version: null,
configured_default_model_id: null,
recommended_model_id: null,
runtime_availability_version: null,
},
public_features: [],
}));
const app = await createApp({
bootstrap,
browserSupportRelease: testBrowserSupportRelease,
} as never);
const response = await app.inject({
headers: { host: "127.0.0.1:43121", "user-agent": "Mozilla/5.0 Chrome/150" },
method: "GET",
url: "/api/v1/bootstrap",
});
expect(response.statusCode).toBe(426);
expect(response.json()).toMatchObject({
error: {
code: "BROWSER_UNSUPPORTED",
details: { reason: "identity_unavailable" },
},
});
expect(bootstrap).not.toHaveBeenCalled();
await app.close();
});
});
+78
View File
@@ -0,0 +1,78 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
describe("TDD-WP0-BND-001 loopback origin", () => {
it("serves only the minimal gate shell on the fixed loopback authority", async () => {
const app = await createApp();
const response = await app.inject({
headers: { host: "127.0.0.1:43121" },
method: "GET",
url: "/",
});
expect(response.statusCode).toBe(200);
expect(response.headers["accept-ch"]).toContain("Sec-CH-UA-Full-Version-List");
expect(response.headers["content-security-policy"]).toContain("default-src 'self'");
expect(response.body).toContain("当前浏览器无法使用 Dada");
expect(response.body).not.toContain("/src/main.tsx");
expect(response.body).toContain("没有“仍然继续”入口,也不会提供绕过令牌或参数。");
expect(response.body).not.toMatch(/<(?:a|button)[^>]*>[^<]*/u);
await app.close();
});
it.each([
{
headers: { host: "192.168.1.10:43121" },
method: "GET" as const,
name: "LAN Host",
url: "/",
},
{
headers: {
host: "127.0.0.1:43121",
origin: "http://attacker.invalid",
},
method: "POST" as const,
name: "foreign Origin",
url: "/api/v1/support/check",
},
{
headers: {
"access-control-request-method": "POST",
host: "127.0.0.1:43121",
origin: "http://attacker.invalid",
},
method: "OPTIONS" as const,
name: "CORS preflight",
url: "/api/v1/support/check",
},
])("rejects $name without a CORS grant", async ({ headers, method, url }) => {
const app = await createApp();
const response = await app.inject({ headers, method, url });
expect(response.statusCode).toBe(426);
expect(response.headers).not.toHaveProperty("access-control-allow-origin");
expect(response.json()).toMatchObject({
error: {
code: "BROWSER_UNSUPPORTED",
details: { reason: "identity_unavailable" },
message_key: "browser.unsupported",
},
});
await app.close();
});
it("keeps the production and Supervisor endpoint fixed", () => {
const main = readFileSync("apps/api/src/main.ts", "utf8");
const supervisor = readFileSync("supervisor/Dada.Supervisor/LoopbackEndpoint.cs", "utf8");
expect(main).toContain('host: "127.0.0.1"');
expect(main).toContain("port: 43121");
expect(supervisor).toContain('Host = "127.0.0.1"');
expect(supervisor).toContain("Port = 43121");
expect(supervisor).not.toContain("0.0.0.0");
});
});
+5 -1
View File
@@ -13,7 +13,11 @@ let vite: ViteDevServer;
let webUrl: string;
test.beforeAll(async () => {
app = await createApp({ eventHub });
app = await createApp({
browserGate: false,
eventHub,
networkBoundary: { allowTestPort: true },
});
app.get("/__test/entity", async (request) => ({ entity_ref: (request.query as { ref?: string }).ref ?? null }));
app.get("/__test/models", async () => ({ source: "rest" }));
const apiUrl = await app.listen({ host: "127.0.0.1", port: 0 });
+101
View File
@@ -0,0 +1,101 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { expect, test } from "@playwright/test";
import { createApp } from "../../apps/api/src/app.js";
test("a real Edge profile passes the candidate support flow", async ({ browser, context, page }) => {
const fullVersion = browser.version();
const release = {
appVersion: "1.2.3-candidate",
browsers: [
{ brand: "Google Chrome", fullVersion: "150.0.0.0" },
{ brand: "Microsoft Edge", fullVersion },
],
} as const;
const app = await createApp({
browserSupportRelease: release,
networkBoundary: { allowTestPort: true },
} as never);
const url = await app.listen({ host: "127.0.0.1", port: 0 });
try {
await page.goto(url);
await expect.poll(() => page.locator("html").getAttribute("data-support-status")).toBe("supported");
await expect(page.locator("h1")).toHaveText("浏览器支持检查已通过");
await expect(page.locator("#status-code")).toBeHidden();
await expect(page.locator("#no-bypass")).toBeHidden();
await expect(page.locator("#supported-value")).toContainText("Google Chrome");
await expect(page.locator("#supported-value")).toContainText("Microsoft Edge");
const bootstrapStatus = await page.evaluate(async () => (await fetch("/api/v1/bootstrap")).status);
expect(bootstrapStatus).toBe(200);
const cookie = (await context.cookies()).find(({ name }) => name === "dada_browser_support");
expect(cookie).toMatchObject({ httpOnly: true, sameSite: "Strict" });
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_BRW_SUPPORTED;
if (evidenceDirectory) {
mkdirSync(resolve(evidenceDirectory, "edge", "screenshots"), { recursive: true });
await page.screenshot({
path: resolve(evidenceDirectory, "edge", "screenshots", "supported.png"),
});
writeFileSync(
resolve(evidenceDirectory, "edge", "environment.json"),
`${JSON.stringify({ browser: "Microsoft Edge", full_version: fullVersion, release_kind: "test_candidate", status: "partial" }, null, 2)}\n`,
);
writeFileSync(
resolve(evidenceDirectory, "edge", "response.json"),
`${JSON.stringify({ bootstrap_status: bootstrapStatus, cookie_http_only: cookie?.httpOnly, status: "passed" }, null, 2)}\n`,
);
}
} finally {
await app.close();
}
});
test("the hard-block shell is usable without a product bundle", async ({ browser, page }) => {
const currentMajor = Number.parseInt(browser.version().split(".")[0], 10);
const app = await createApp({
browserSupportRelease: {
appVersion: "1.2.3-test",
browsers: [
{ brand: "Google Chrome", fullVersion: `${currentMajor + 1}.0.0.0` },
{ brand: "Microsoft Edge", fullVersion: `${currentMajor + 1}.0.0.0` },
],
},
networkBoundary: { allowTestPort: true },
} as never);
const url = await app.listen({ host: "127.0.0.1", port: 0 });
try {
await page.goto(url);
await expect.poll(() => page.locator("html").getAttribute("data-support-status")).toBe("blocked");
await expect(page.locator("#reason-value")).toHaveText("version_unsupported");
await expect(page.getByText("没有“仍然继续”入口,也不会提供绕过令牌或参数。")).toBeVisible();
await expect(page.locator('script[src*="/src/main"]')).toHaveCount(0);
await page.keyboard.press("Tab");
await expect(page.locator("h1")).toBeFocused();
await page.keyboard.press("Tab");
await expect(page.locator("summary")).toBeFocused();
await page.keyboard.press("Tab");
await expect(page.getByRole("button", { name: "重新检测" })).toBeFocused();
await page.keyboard.press("Tab");
await expect(page.getByRole("button", { name: "复制本机地址" })).toBeFocused();
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_BRW_BLOCKED;
if (evidenceDirectory) {
mkdirSync(resolve(evidenceDirectory, "screenshots"), { recursive: true });
await page.screenshot({ path: resolve(evidenceDirectory, "screenshots", "blocked.png") });
}
await page.setViewportSize({ height: 844, width: 390 });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
await expect(page.getByRole("button", { name: "重新检测" })).toBeVisible();
if (evidenceDirectory) {
await page.screenshot({ path: resolve(evidenceDirectory, "screenshots", "blocked-mobile.png") });
}
} finally {
await app.close();
}
});
+49
View File
@@ -0,0 +1,49 @@
export const testBrowserSupportRelease = {
appVersion: "1.2.3-test",
browsers: [
{ brand: "Google Chrome", fullVersion: "150.0.7339.1" },
{ brand: "Microsoft Edge", fullVersion: "150.0.4078.99" },
],
} as const;
interface BrowserIdentityFixture {
brand: string;
fullVersion: string;
platform?: string;
}
function major(version: string) {
return version.split(".")[0];
}
function serialize(values: Array<{ brand: string; version: string }>) {
return values.map(({ brand, version }) => `"${brand}";v="${version}"`).join(", ");
}
export function browserSupportFixture(input: BrowserIdentityFixture) {
const platform = input.platform ?? "Windows";
const brands = [
{ brand: "Not_A Brand", version: "99" },
{ brand: "Chromium", version: major(input.fullVersion) },
{ brand: input.brand, version: major(input.fullVersion) },
];
const fullVersionList = [
{ brand: "Not_A Brand", version: "99.0.0.0" },
{ brand: "Chromium", version: input.fullVersion },
{ brand: input.brand, version: input.fullVersion },
];
return {
body: {
brands,
full_version_list: fullVersionList,
platform,
},
headers: {
host: "127.0.0.1:43121",
origin: "http://127.0.0.1:43121",
"sec-ch-ua": serialize(brands),
"sec-ch-ua-full-version-list": serialize(fullVersionList),
"sec-ch-ua-platform": `"${platform}"`,
},
};
}