feat: complete TASK-WP0-03 browser gate
This commit is contained in:
+184
-4
@@ -1,4 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { randomBytes, randomUUID } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import {
|
||||
BootstrapResponseSchema,
|
||||
@@ -11,13 +13,28 @@ import {
|
||||
SseEventSchema,
|
||||
StableEngineeringErrorCodeSchema,
|
||||
StateSseEventSchema,
|
||||
createErrorEnvelope,
|
||||
isCorrelationId,
|
||||
type BootstrapResponse,
|
||||
} from "@dada/shared-contracts";
|
||||
import swagger from "@fastify/swagger";
|
||||
import Fastify from "fastify";
|
||||
import Fastify, { type FastifyReply } from "fastify";
|
||||
|
||||
import {
|
||||
BrowserSupportRequestSchema,
|
||||
BrowserSupportSuccessSchema,
|
||||
BrowserUnsupportedReasonSchema,
|
||||
browserSupportCookieMaxAgeSeconds,
|
||||
browserSupportCookieName,
|
||||
checkBrowserSupport,
|
||||
createBrowserSupportCookie,
|
||||
supportedBrowserSummary,
|
||||
verifyBrowserSupportCookie,
|
||||
type BrowserSupportRelease,
|
||||
type BrowserUnsupportedReason,
|
||||
} from "./browser-support.js";
|
||||
import { EventHub } from "./event-hub.js";
|
||||
import { isAllowedNetworkRequest, type NetworkBoundaryOptions } from "./network-boundary.js";
|
||||
|
||||
const defaultBootstrap: BootstrapResponse = {
|
||||
app_version: "0.0.0",
|
||||
@@ -33,18 +50,80 @@ const defaultBootstrap: BootstrapResponse = {
|
||||
|
||||
export interface CreateAppOptions {
|
||||
bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
|
||||
browserGate?: boolean;
|
||||
browserSupportRelease?: BrowserSupportRelease;
|
||||
browserSupportSecret?: Buffer;
|
||||
eventHub?: EventHub;
|
||||
networkBoundary?: NetworkBoundaryOptions;
|
||||
}
|
||||
|
||||
const supportGateDirectory = resolve("apps/web/support-gate");
|
||||
const supportGateHtml = readFileSync(resolve(supportGateDirectory, "index.html"), "utf8");
|
||||
const supportGateCss = readFileSync(resolve(supportGateDirectory, "support-gate.css"), "utf8");
|
||||
const supportGateJavaScript = readFileSync(resolve(supportGateDirectory, "support-gate.js"), "utf8");
|
||||
const clientHints = "Sec-CH-UA, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform";
|
||||
const contentSecurityPolicy = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self'",
|
||||
"style-src 'self'",
|
||||
"img-src 'self' blob:",
|
||||
"font-src 'self' blob:",
|
||||
"connect-src 'self'",
|
||||
"object-src 'none'",
|
||||
"base-uri 'none'",
|
||||
"frame-ancestors 'none'",
|
||||
].join("; ");
|
||||
|
||||
function requestCorrelationId(headers: Record<string, string | string[] | undefined>) {
|
||||
const header = headers["x-correlation-id"];
|
||||
const candidate = Array.isArray(header) ? header[0] : header;
|
||||
return isCorrelationId(candidate) ? candidate : randomUUID();
|
||||
}
|
||||
|
||||
function headerValue(value: string | string[] | undefined) {
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
function isSupportGateRequest(method: string, path: string) {
|
||||
if (method === "POST" && path === "/api/v1/support/check") return true;
|
||||
if (method !== "GET" && method !== "HEAD") return false;
|
||||
return (
|
||||
path === "/" ||
|
||||
path === "/app" ||
|
||||
path.startsWith("/app/") ||
|
||||
path === "/admin" ||
|
||||
path.startsWith("/admin/") ||
|
||||
path === "/support-gate.css" ||
|
||||
path === "/support-gate.js" ||
|
||||
path === "/RELEASE.json" ||
|
||||
path === "/healthz"
|
||||
);
|
||||
}
|
||||
|
||||
function sendBrowserUnsupported(
|
||||
reply: FastifyReply,
|
||||
correlationId: string,
|
||||
reason: BrowserUnsupportedReason,
|
||||
release: BrowserSupportRelease | undefined,
|
||||
) {
|
||||
return reply.code(426).send(
|
||||
createErrorEnvelope({
|
||||
code: "BROWSER_UNSUPPORTED",
|
||||
correlationId,
|
||||
details: {
|
||||
reason,
|
||||
supported_browsers: supportedBrowserSummary(release),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function createApp(options: CreateAppOptions = {}) {
|
||||
const eventHub = options.eventHub ?? new EventHub();
|
||||
const bootstrap = options.bootstrap ?? (() => defaultBootstrap);
|
||||
const browserGate = options.browserGate ?? true;
|
||||
const browserSupportSecret = options.browserSupportSecret ?? randomBytes(32);
|
||||
const browserSupportRelease = options.browserSupportRelease;
|
||||
const app = Fastify({
|
||||
genReqId: (request) => requestCorrelationId(request.headers),
|
||||
logger: false,
|
||||
@@ -70,6 +149,9 @@ export async function createApp(options: CreateAppOptions = {}) {
|
||||
StableEngineeringErrorCodeSchema,
|
||||
ErrorDetailsSchema,
|
||||
ErrorEnvelopeSchema,
|
||||
BrowserUnsupportedReasonSchema,
|
||||
BrowserSupportRequestSchema,
|
||||
BrowserSupportSuccessSchema,
|
||||
BootstrapResponseSchema,
|
||||
StateSseEventSchema,
|
||||
ModelConfigSseEventSchema,
|
||||
@@ -79,11 +161,107 @@ export async function createApp(options: CreateAppOptions = {}) {
|
||||
app.addSchema(schema);
|
||||
}
|
||||
|
||||
app.addHook("onRequest", (request, reply, done) => {
|
||||
app.addHook("onRequest", async (request, reply) => {
|
||||
reply.header("X-Correlation-Id", request.id);
|
||||
done();
|
||||
reply.header("Accept-CH", clientHints);
|
||||
reply.header("Cache-Control", "no-store");
|
||||
reply.header("Content-Security-Policy", contentSecurityPolicy);
|
||||
reply.header("X-Content-Type-Options", "nosniff");
|
||||
|
||||
const host = headerValue(request.headers.host);
|
||||
const origin = headerValue(request.headers.origin);
|
||||
if (!isAllowedNetworkRequest({ host, method: request.method, origin }, options.networkBoundary)) {
|
||||
return sendBrowserUnsupported(reply, request.id, "identity_unavailable", browserSupportRelease);
|
||||
}
|
||||
|
||||
const path = request.url.split("?", 1)[0] ?? "/";
|
||||
if (!browserGate || isSupportGateRequest(request.method, path)) return;
|
||||
const verified = verifyBrowserSupportCookie({
|
||||
cookieHeader: headerValue(request.headers.cookie),
|
||||
release: browserSupportRelease,
|
||||
secChUa: headerValue(request.headers["sec-ch-ua"]),
|
||||
secret: browserSupportSecret,
|
||||
});
|
||||
if (!verified.supported) {
|
||||
return sendBrowserUnsupported(reply, request.id, verified.reason, browserSupportRelease);
|
||||
}
|
||||
});
|
||||
|
||||
for (const route of ["/", "/app", "/app/*", "/admin", "/admin/*"]) {
|
||||
app.get(route, { schema: { hide: true } }, async (_request, reply) => {
|
||||
reply.type("text/html; charset=utf-8");
|
||||
return supportGateHtml;
|
||||
});
|
||||
}
|
||||
app.get("/support-gate.css", { schema: { hide: true } }, async (_request, reply) => {
|
||||
reply.type("text/css; charset=utf-8");
|
||||
return supportGateCss;
|
||||
});
|
||||
app.get("/support-gate.js", { schema: { hide: true } }, async (_request, reply) => {
|
||||
reply.type("text/javascript; charset=utf-8");
|
||||
return supportGateJavaScript;
|
||||
});
|
||||
app.get("/RELEASE.json", { schema: { hide: true } }, async () => ({
|
||||
app_version: browserSupportRelease?.appVersion ?? null,
|
||||
browsers: supportedBrowserSummary(browserSupportRelease),
|
||||
}));
|
||||
app.get("/healthz", { schema: { hide: true } }, async () => ({
|
||||
bind_scope: "loopback",
|
||||
port: 43121,
|
||||
status: "ready",
|
||||
}));
|
||||
|
||||
app.post(
|
||||
"/api/v1/support/check",
|
||||
{
|
||||
attachValidation: true,
|
||||
schema: {
|
||||
body: BrowserSupportRequestSchema,
|
||||
operationId: "checkBrowserSupport",
|
||||
response: {
|
||||
200: BrowserSupportSuccessSchema,
|
||||
426: ErrorEnvelopeSchema,
|
||||
},
|
||||
tags: ["Browser support"],
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const checked = checkBrowserSupport(
|
||||
request.validationError ? undefined : request.body,
|
||||
{
|
||||
secChUa: headerValue(request.headers["sec-ch-ua"]),
|
||||
secChUaFullVersionList: headerValue(request.headers["sec-ch-ua-full-version-list"]),
|
||||
secChUaPlatform: headerValue(request.headers["sec-ch-ua-platform"]),
|
||||
},
|
||||
browserSupportRelease,
|
||||
);
|
||||
if (!checked.supported || !browserSupportRelease) {
|
||||
return sendBrowserUnsupported(
|
||||
reply,
|
||||
request.id,
|
||||
checked.supported ? "version_unsupported" : checked.reason,
|
||||
browserSupportRelease,
|
||||
);
|
||||
}
|
||||
|
||||
const cookie = createBrowserSupportCookie(
|
||||
browserSupportSecret,
|
||||
browserSupportRelease,
|
||||
checked.identity,
|
||||
);
|
||||
reply.header(
|
||||
"Set-Cookie",
|
||||
`${browserSupportCookieName}=${cookie}; Max-Age=${browserSupportCookieMaxAgeSeconds}; Path=/; HttpOnly; SameSite=Strict`,
|
||||
);
|
||||
return {
|
||||
app_version: browserSupportRelease.appVersion,
|
||||
browser: checked.identity,
|
||||
status: "supported" as const,
|
||||
supported_browsers: supportedBrowserSummary(browserSupportRelease),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/v1/bootstrap",
|
||||
{
|
||||
@@ -91,6 +269,7 @@ export async function createApp(options: CreateAppOptions = {}) {
|
||||
operationId: "getBootstrap",
|
||||
response: {
|
||||
200: BootstrapResponseSchema,
|
||||
426: ErrorEnvelopeSchema,
|
||||
},
|
||||
tags: ["Bootstrap"],
|
||||
},
|
||||
@@ -112,6 +291,7 @@ export async function createApp(options: CreateAppOptions = {}) {
|
||||
},
|
||||
description: "Non-sensitive state change hints. REST remains authoritative.",
|
||||
},
|
||||
426: ErrorEnvelopeSchema,
|
||||
},
|
||||
tags: ["State events"],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import { Type, type Static } from "@sinclair/typebox";
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
|
||||
export const browserSupportCookieName = "dada_browser_support";
|
||||
export const browserSupportCookieMaxAgeSeconds = 24 * 60 * 60;
|
||||
|
||||
export const BrowserUnsupportedReasonSchema = Type.Union(
|
||||
[
|
||||
Type.Literal("platform_unsupported"),
|
||||
Type.Literal("brand_unsupported"),
|
||||
Type.Literal("version_unsupported"),
|
||||
Type.Literal("identity_unavailable"),
|
||||
],
|
||||
{ $id: "BrowserUnsupportedReason" },
|
||||
);
|
||||
const BrowserBrandVersionSchema = Type.Object(
|
||||
{
|
||||
brand: Type.String({ maxLength: 80 }),
|
||||
version: Type.String({ maxLength: 80 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
const SupportedBrowserSummarySchema = Type.Object(
|
||||
{
|
||||
brand: Type.Union([Type.Literal("Google Chrome"), Type.Literal("Microsoft Edge")]),
|
||||
major: Type.Integer({ minimum: 1 }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
export const BrowserSupportRequestSchema = Type.Object(
|
||||
{
|
||||
brands: Type.Array(BrowserBrandVersionSchema, { maxItems: 16 }),
|
||||
full_version_list: Type.Array(BrowserBrandVersionSchema, { maxItems: 16 }),
|
||||
platform: Type.String({ maxLength: 40 }),
|
||||
},
|
||||
{ additionalProperties: false, $id: "BrowserSupportRequest" },
|
||||
);
|
||||
export const BrowserSupportSuccessSchema = Type.Object(
|
||||
{
|
||||
app_version: Type.String({ maxLength: 80 }),
|
||||
browser: SupportedBrowserSummarySchema,
|
||||
status: Type.Literal("supported"),
|
||||
supported_browsers: Type.Array(SupportedBrowserSummarySchema, { maxItems: 2 }),
|
||||
},
|
||||
{ additionalProperties: false, $id: "BrowserSupportSuccess" },
|
||||
);
|
||||
|
||||
export type BrowserUnsupportedReason = Static<typeof BrowserUnsupportedReasonSchema>;
|
||||
export type BrowserSupportRequest = Static<typeof BrowserSupportRequestSchema>;
|
||||
type SupportedBrand = "Google Chrome" | "Microsoft Edge";
|
||||
|
||||
export interface BrowserSupportRelease {
|
||||
appVersion: string;
|
||||
browsers: ReadonlyArray<{
|
||||
brand: SupportedBrand;
|
||||
fullVersion: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface BrowserIdentity {
|
||||
brand: SupportedBrand;
|
||||
major: number;
|
||||
}
|
||||
|
||||
type BrowserSupportResult =
|
||||
| { identity: BrowserIdentity; supported: true }
|
||||
| { reason: BrowserUnsupportedReason; supported: false };
|
||||
|
||||
const supportedBrands = new Set<SupportedBrand>(["Google Chrome", "Microsoft Edge"]);
|
||||
const fullVersionPattern = /^[1-9][0-9]*\.[0-9]+\.[0-9]+\.[0-9]+$/;
|
||||
|
||||
function major(version: string) {
|
||||
if (!/^[1-9][0-9]*(?:\.[0-9]+)*$/.test(version)) return undefined;
|
||||
const first = version.split(".")[0];
|
||||
if (!first) return undefined;
|
||||
const value = Number.parseInt(first, 10);
|
||||
return Number.isSafeInteger(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function parsedHeaderList(value: string | undefined) {
|
||||
if (!value) return undefined;
|
||||
const entries: Array<{ brand: string; version: string }> = [];
|
||||
const remainder = value.replace(/"([^"]+)"\s*;\s*v="([^"]+)"/g, (_match, brand, version) => {
|
||||
entries.push({ brand, version });
|
||||
return "";
|
||||
});
|
||||
if (entries.length === 0 || !/^[\s,]*$/.test(remainder)) return undefined;
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parsedPlatform(value: string | undefined) {
|
||||
const match = value?.match(/^"([^"]+)"$/);
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
function normalizedList(entries: Array<{ brand: string; version: string }>) {
|
||||
return [...entries]
|
||||
.sort((left, right) => left.brand.localeCompare(right.brand) || left.version.localeCompare(right.version))
|
||||
.map(({ brand, version }) => `${brand}\u0000${version}`)
|
||||
.join("\u0001");
|
||||
}
|
||||
|
||||
function supportedIdentity(entries: Array<{ brand: string; version: string }>) {
|
||||
const matches = entries.filter(({ brand }) => supportedBrands.has(brand as SupportedBrand));
|
||||
if (matches.length !== 1) return undefined;
|
||||
const entry = matches[0];
|
||||
if (!entry) return undefined;
|
||||
const parsedMajor = major(entry.version);
|
||||
if (!parsedMajor) return undefined;
|
||||
return { brand: entry.brand as SupportedBrand, major: parsedMajor };
|
||||
}
|
||||
|
||||
export function supportedBrowserSummary(release: BrowserSupportRelease | undefined) {
|
||||
if (!release) return [];
|
||||
return release.browsers.map(({ brand, fullVersion }) => ({ brand, major: major(fullVersion)! }));
|
||||
}
|
||||
|
||||
export function validateBrowserSupportRelease(value: unknown): value is BrowserSupportRelease {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const release = value as BrowserSupportRelease;
|
||||
if (typeof release.appVersion !== "string" || release.appVersion.length === 0 || release.appVersion.length > 80) {
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(release.browsers) || release.browsers.length !== 2) return false;
|
||||
const brands = new Set(release.browsers.map(({ brand }) => brand));
|
||||
return (
|
||||
brands.size === 2 &&
|
||||
brands.has("Google Chrome") &&
|
||||
brands.has("Microsoft Edge") &&
|
||||
release.browsers.every(
|
||||
({ brand, fullVersion }) => supportedBrands.has(brand) && fullVersionPattern.test(fullVersion),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function readBrowserSupportRelease(path: string) {
|
||||
try {
|
||||
const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
||||
return validateBrowserSupportRelease(value) ? value : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function checkBrowserSupport(
|
||||
body: unknown,
|
||||
headers: {
|
||||
secChUa: string | undefined;
|
||||
secChUaFullVersionList: string | undefined;
|
||||
secChUaPlatform: string | undefined;
|
||||
},
|
||||
release: BrowserSupportRelease | undefined,
|
||||
): BrowserSupportResult {
|
||||
if (!Value.Check(BrowserSupportRequestSchema, body)) {
|
||||
return { reason: "identity_unavailable", supported: false };
|
||||
}
|
||||
const request = body as BrowserSupportRequest;
|
||||
const headerBrands = parsedHeaderList(headers.secChUa);
|
||||
const headerFullVersions = parsedHeaderList(headers.secChUaFullVersionList);
|
||||
const headerPlatform = parsedPlatform(headers.secChUaPlatform);
|
||||
if (!headerBrands || !headerFullVersions || !headerPlatform) {
|
||||
return { reason: "identity_unavailable", supported: false };
|
||||
}
|
||||
if (
|
||||
normalizedList(headerBrands) !== normalizedList(request.brands) ||
|
||||
normalizedList(headerFullVersions) !== normalizedList(request.full_version_list) ||
|
||||
headerPlatform !== request.platform
|
||||
) {
|
||||
return { reason: "identity_unavailable", supported: false };
|
||||
}
|
||||
if (request.platform !== "Windows") return { reason: "platform_unsupported", supported: false };
|
||||
|
||||
const lowIdentity = supportedIdentity(request.brands);
|
||||
const fullIdentity = supportedIdentity(request.full_version_list);
|
||||
if (!lowIdentity && request.brands.every(({ brand }) => !supportedBrands.has(brand as SupportedBrand))) {
|
||||
return { reason: "brand_unsupported", supported: false };
|
||||
}
|
||||
if (
|
||||
!lowIdentity ||
|
||||
!fullIdentity ||
|
||||
lowIdentity.brand !== fullIdentity.brand ||
|
||||
lowIdentity.major !== fullIdentity.major
|
||||
) {
|
||||
return { reason: "identity_unavailable", supported: false };
|
||||
}
|
||||
|
||||
const supported = release?.browsers.find(({ brand }) => brand === fullIdentity.brand);
|
||||
if (!supported || major(supported.fullVersion) !== fullIdentity.major) {
|
||||
return { reason: "version_unsupported", supported: false };
|
||||
}
|
||||
return { identity: fullIdentity, supported: true };
|
||||
}
|
||||
|
||||
function signature(secret: Buffer, encodedPayload: string) {
|
||||
return createHmac("sha256", secret).update(encodedPayload).digest("base64url");
|
||||
}
|
||||
|
||||
export function createBrowserSupportCookie(
|
||||
secret: Buffer,
|
||||
release: BrowserSupportRelease,
|
||||
identity: BrowserIdentity,
|
||||
issuedAtSeconds = Math.floor(Date.now() / 1000),
|
||||
) {
|
||||
const encodedPayload = Buffer.from(
|
||||
JSON.stringify({
|
||||
app_version: release.appVersion,
|
||||
brand: identity.brand,
|
||||
issued_at: issuedAtSeconds,
|
||||
major: identity.major,
|
||||
}),
|
||||
).toString("base64url");
|
||||
return `${encodedPayload}.${signature(secret, encodedPayload)}`;
|
||||
}
|
||||
|
||||
function cookieValue(cookieHeader: string | undefined) {
|
||||
for (const pair of cookieHeader?.split(";") ?? []) {
|
||||
const [name, ...rest] = pair.trim().split("=");
|
||||
if (name === browserSupportCookieName) return rest.join("=");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function verifyBrowserSupportCookie(input: {
|
||||
cookieHeader: string | undefined;
|
||||
nowSeconds?: number;
|
||||
release: BrowserSupportRelease | undefined;
|
||||
secChUa: string | undefined;
|
||||
secret: Buffer;
|
||||
}) {
|
||||
if (!input.release) return { reason: "version_unsupported" as const, supported: false as const };
|
||||
const value = cookieValue(input.cookieHeader);
|
||||
if (!value) return { reason: "identity_unavailable" as const, supported: false as const };
|
||||
const [encodedPayload, encodedSignature, extra] = value.split(".");
|
||||
if (!encodedPayload || !encodedSignature || extra) {
|
||||
return { reason: "identity_unavailable" as const, supported: false as const };
|
||||
}
|
||||
const expected = Buffer.from(signature(input.secret, encodedPayload));
|
||||
const actual = Buffer.from(encodedSignature);
|
||||
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
||||
return { reason: "identity_unavailable" as const, supported: false as const };
|
||||
}
|
||||
|
||||
let payload: { app_version?: unknown; brand?: unknown; issued_at?: unknown; major?: unknown };
|
||||
try {
|
||||
payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
|
||||
} catch {
|
||||
return { reason: "identity_unavailable" as const, supported: false as const };
|
||||
}
|
||||
const now = input.nowSeconds ?? Math.floor(Date.now() / 1000);
|
||||
if (
|
||||
payload.app_version !== input.release.appVersion ||
|
||||
!supportedBrands.has(payload.brand as SupportedBrand) ||
|
||||
!Number.isSafeInteger(payload.major) ||
|
||||
!Number.isSafeInteger(payload.issued_at) ||
|
||||
(payload.issued_at as number) > now ||
|
||||
now - (payload.issued_at as number) > browserSupportCookieMaxAgeSeconds
|
||||
) {
|
||||
return { reason: "identity_unavailable" as const, supported: false as const };
|
||||
}
|
||||
|
||||
const headerBrands = parsedHeaderList(input.secChUa);
|
||||
const currentIdentity = headerBrands ? supportedIdentity(headerBrands) : undefined;
|
||||
if (!currentIdentity) return { reason: "identity_unavailable" as const, supported: false as const };
|
||||
if (currentIdentity.brand !== payload.brand) {
|
||||
return { reason: "identity_unavailable" as const, supported: false as const };
|
||||
}
|
||||
const supported = input.release.browsers.find(({ brand }) => brand === currentIdentity.brand);
|
||||
if (currentIdentity.major !== payload.major || major(supported?.fullVersion ?? "") !== currentIdentity.major) {
|
||||
return { reason: "version_unsupported" as const, supported: false as const };
|
||||
}
|
||||
return { identity: currentIdentity, supported: true as const };
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createApp } from "./app.js";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const app = await createApp();
|
||||
import { createApp } from "./app.js";
|
||||
import { readBrowserSupportRelease } from "./browser-support.js";
|
||||
|
||||
const browserSupportRelease = readBrowserSupportRelease(resolve("RELEASE.json"));
|
||||
const app = await createApp(browserSupportRelease ? { browserSupportRelease } : {});
|
||||
|
||||
await app.listen({
|
||||
host: "127.0.0.1",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export const DADA_LOOPBACK_HOST = "127.0.0.1";
|
||||
export const DADA_LOOPBACK_PORT = 43121;
|
||||
|
||||
export interface NetworkBoundaryOptions {
|
||||
allowTestPort?: boolean;
|
||||
}
|
||||
|
||||
interface NetworkRequest {
|
||||
host: string | undefined;
|
||||
method: string;
|
||||
origin: string | undefined;
|
||||
}
|
||||
|
||||
export function isAllowedNetworkRequest(
|
||||
request: NetworkRequest,
|
||||
options: NetworkBoundaryOptions = {},
|
||||
) {
|
||||
if (request.method === "OPTIONS" || !request.host) return false;
|
||||
const allowedHost = options.allowTestPort
|
||||
? /^127\.0\.0\.1:[1-9][0-9]{0,4}$/.test(request.host)
|
||||
: request.host === `${DADA_LOOPBACK_HOST}:${DADA_LOOPBACK_PORT}`;
|
||||
if (!allowedHost) return false;
|
||||
|
||||
if (request.origin !== undefined && request.origin !== `http://${request.host}`) return false;
|
||||
if (!["GET", "HEAD"].includes(request.method) && request.origin === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user