340 lines
10 KiB
TypeScript
340 lines
10 KiB
TypeScript
import { randomBytes, randomUUID } from "node:crypto";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
import {
|
|
BootstrapResponseSchema,
|
|
CorrelationIdSchema,
|
|
ErrorDetailsSchema,
|
|
ErrorEnvelopeSchema,
|
|
GenerationErrorCategorySchema,
|
|
ModelConfigSseEventSchema,
|
|
ModelRuntimeSseEventSchema,
|
|
SseEventSchema,
|
|
StableEngineeringErrorCodeSchema,
|
|
StateSseEventSchema,
|
|
createErrorEnvelope,
|
|
isCorrelationId,
|
|
type BootstrapResponse,
|
|
} from "@dada/shared-contracts";
|
|
import swagger from "@fastify/swagger";
|
|
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 type { PublicAssetResolver } from "./local-data-root.js";
|
|
import { isAllowedNetworkRequest, type NetworkBoundaryOptions } from "./network-boundary.js";
|
|
|
|
const defaultBootstrap: BootstrapResponse = {
|
|
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: [],
|
|
};
|
|
|
|
export interface CreateAppOptions {
|
|
bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
|
|
browserGate?: boolean;
|
|
browserSupportRelease?: BrowserSupportRelease;
|
|
browserSupportSecret?: Buffer;
|
|
eventHub?: EventHub;
|
|
networkBoundary?: NetworkBoundaryOptions;
|
|
publicAssets?: PublicAssetResolver;
|
|
}
|
|
|
|
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,
|
|
});
|
|
|
|
await app.register(swagger, {
|
|
openapi: {
|
|
info: {
|
|
title: "Dada P0-A",
|
|
version: "0.0.0",
|
|
},
|
|
openapi: "3.1.0",
|
|
},
|
|
refResolver: {
|
|
buildLocalReference: (schema, _baseUri, _fragment, index) =>
|
|
typeof schema.$id === "string" ? schema.$id : `schema-${index}`,
|
|
},
|
|
});
|
|
|
|
for (const schema of [
|
|
CorrelationIdSchema,
|
|
GenerationErrorCategorySchema,
|
|
StableEngineeringErrorCodeSchema,
|
|
ErrorDetailsSchema,
|
|
ErrorEnvelopeSchema,
|
|
BrowserUnsupportedReasonSchema,
|
|
BrowserSupportRequestSchema,
|
|
BrowserSupportSuccessSchema,
|
|
BootstrapResponseSchema,
|
|
StateSseEventSchema,
|
|
ModelConfigSseEventSchema,
|
|
ModelRuntimeSseEventSchema,
|
|
SseEventSchema,
|
|
]) {
|
|
app.addSchema(schema);
|
|
}
|
|
|
|
app.addHook("onRequest", async (request, reply) => {
|
|
reply.header("X-Correlation-Id", request.id);
|
|
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.get(
|
|
"/api/v1/assets/public/:resourceVersion/:assetId",
|
|
{ schema: { hide: true } },
|
|
async (request, reply) => {
|
|
const { assetId, resourceVersion } = request.params as { assetId?: string; resourceVersion?: string };
|
|
const resource = assetId && resourceVersion
|
|
? options.publicAssets?.read(resourceVersion, assetId)
|
|
: undefined;
|
|
if (!resource) return reply.code(404).send();
|
|
reply.type(resource.mimeType);
|
|
reply.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
reply.header("Content-Disposition", "inline");
|
|
reply.header("ETag", `"sha256-${resource.sha256}"`);
|
|
return resource.bytes;
|
|
},
|
|
);
|
|
|
|
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",
|
|
{
|
|
schema: {
|
|
operationId: "getBootstrap",
|
|
response: {
|
|
200: BootstrapResponseSchema,
|
|
426: ErrorEnvelopeSchema,
|
|
},
|
|
tags: ["Bootstrap"],
|
|
},
|
|
},
|
|
async () => bootstrap(),
|
|
);
|
|
|
|
app.get(
|
|
"/api/v1/events",
|
|
{
|
|
schema: {
|
|
operationId: "getEvents",
|
|
response: {
|
|
200: {
|
|
content: {
|
|
"text/event-stream": {
|
|
schema: SseEventSchema,
|
|
},
|
|
},
|
|
description: "Non-sensitive state change hints. REST remains authoritative.",
|
|
},
|
|
426: ErrorEnvelopeSchema,
|
|
},
|
|
tags: ["State events"],
|
|
},
|
|
},
|
|
(request, reply) => {
|
|
reply.hijack();
|
|
reply.raw.setHeader("Cache-Control", "no-store");
|
|
reply.raw.setHeader("Connection", "keep-alive");
|
|
reply.raw.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
reply.raw.setHeader("X-Correlation-Id", request.id);
|
|
reply.raw.writeHead(200);
|
|
reply.raw.write(": connected\n\n");
|
|
|
|
const unsubscribe = eventHub.connect(
|
|
(event) => {
|
|
reply.raw.write(`id: ${event.event_id}\n`);
|
|
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
},
|
|
() => reply.raw.end(),
|
|
);
|
|
request.raw.once("close", unsubscribe);
|
|
},
|
|
);
|
|
|
|
return app;
|
|
}
|