Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions backend/src/releaseManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,11 +736,24 @@ function fallbackGitCommit(releaseRoot: string): string {
}
}

function developmentRuntimeReleaseIdentity(releaseRoot: string): RuntimeReleaseIdentity {
const commit = fallbackGitCommit(releaseRoot);
return {
backendCommit: commit,
frontendCommit: commit,
ready: true,
source: commit === "unknown" ? "unknown" : "git",
};
}

export async function loadRuntimeReleaseIdentity(
releaseRoot = PROCESS_RELEASE_ROOT,
environment = process.env.NODE_ENV,
backendBuildCommit = getBackendBuildCommit()
): Promise<RuntimeReleaseIdentity> {
if (environment !== "production" && backendBuildCommit === "development") {
return developmentRuntimeReleaseIdentity(releaseRoot);
}
try {
const realReleaseRoot = await fsp.realpath(releaseRoot);
const manifest = await loadReleaseManifest(realReleaseRoot);
Expand Down Expand Up @@ -786,18 +799,19 @@ export async function loadRuntimeReleaseIdentity(
source: "manifest",
};
} catch (error) {
const commit = fallbackGitCommit(releaseRoot);
const isMissing = (error as NodeJS.ErrnoException).code === "ENOENT";
const isDevelopmentFallback = environment !== "production" && isMissing;
if (isDevelopmentFallback) {
return developmentRuntimeReleaseIdentity(releaseRoot);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
const commit = fallbackGitCommit(releaseRoot);
return {
backendCommit: commit,
frontendCommit: commit,
...(!isDevelopmentFallback && {
issue: isMissing
? ("manifest-missing" as const)
: ("manifest-invalid" as const),
}),
ready: isDevelopmentFallback,
issue: isMissing
? ("manifest-missing" as const)
: ("manifest-invalid" as const),
ready: false,
source: commit === "unknown" ? "unknown" : "git",
};
}
Expand Down
47 changes: 38 additions & 9 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { routes } from "./routes.ts";
import { isProductionDeploymentCutoverActive } from "./services/deploymentCutoverState.ts";
import { validateTotpStorageConfig } from "./services/multiFactorAuth.ts";
import { validateWebAuthnConfig } from "./services/webAuthn.ts";
import { staticFileResponse } from "./staticFileResponse.ts";

interface DashboardSocketData {
closeHandlers: Array<() => void>;
Expand All @@ -45,6 +46,9 @@ interface DashboardSocketRequest {
const SERVER_IDLE_TIMEOUT_SECONDS = 240;
const DEPLOYMENT_CUTOVER_SOCKET_CLOSE_CODE = 1012;
const DEPLOYMENT_CUTOVER_SOCKET_CLOSE_REASON = "Dashboard release cutover in progress";
const IMMUTABLE_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
const REVALIDATED_ASSET_CACHE_CONTROL = "no-cache";
const HASHED_ASSET_NAME = /-[\da-z]{8}\.[\da-z]+$/iu;

function dashboardSocketRequest(data: string | Buffer): DashboardSocketRequest {
try {
Expand Down Expand Up @@ -265,21 +269,37 @@ export function createServer(
}
return withRequestSecurity(
request,
await staticResponse(url.pathname),
await staticResponse(request, url.pathname),
server
);
},
websocket,
});
}

async function fileResponse(filePath: string, contentType?: string): Promise<Response> {
const headers: Record<string, string> = { "Cache-Control": "no-store" };
if (contentType) headers["Content-Type"] = contentType;
return new Response(Bun.file(filePath), { headers });
function cacheControlForStaticFile(frontendRoot: string, filePath: string): string {
const relativePath = path.relative(frontendRoot, filePath);
const isHashedAsset =
relativePath.startsWith(`assets${path.sep}`) &&
HASHED_ASSET_NAME.test(path.basename(relativePath));
return isHashedAsset
? IMMUTABLE_ASSET_CACHE_CONTROL
: REVALIDATED_ASSET_CACHE_CONTROL;
}

async function staticResponse(pathname: string): Promise<Response> {
async function fileResponse(
request: Request,
frontendRoot: string,
filePath: string,
contentType?: string
): Promise<Response> {
return staticFileResponse(request, filePath, {
cacheControl: cacheControlForStaticFile(frontendRoot, filePath),
contentType,
});
}

async function staticResponse(request: Request, pathname: string): Promise<Response> {
let decodedPath: string;
try {
decodedPath = decodeURIComponent(pathname.replace(/^\/+/u, "")).replace(
Expand All @@ -296,6 +316,9 @@ async function staticResponse(pathname: string): Promise<Response> {
if (decodedPathname === "/health") {
return new Response("Not found", { status: 404 });
}
if (/\.(?:br|gz)$/iu.test(decodedPathname)) {
return new Response("Not found", { status: 404 });
}

const frontendPath = resolveFrontendPath();
const indexPath = path.join(frontendPath, "index.html");
Expand Down Expand Up @@ -335,7 +358,9 @@ async function staticResponse(pathname: string): Promise<Response> {
!hasHiddenStaticSegment(relativeRealPath)
) {
const stat = await fsp.stat(realDirectPath);
if (stat.isFile()) return fileResponse(realDirectPath);
if (stat.isFile()) {
return fileResponse(request, realRoot, realDirectPath);
}
}
} catch {
// Continue with hashed asset lookup or SPA routing below.
Expand All @@ -357,7 +382,9 @@ async function staticResponse(pathname: string): Promise<Response> {
return new Response("Not found", { status: 404 });
}
const stat = await fsp.stat(realAssetPath);
if (stat.isFile()) return fileResponse(realAssetPath);
if (stat.isFile()) {
return fileResponse(request, realRoot, realAssetPath);
}
} catch {
return new Response("Not found", { status: 404 });
}
Expand All @@ -377,7 +404,9 @@ async function staticResponse(pathname: string): Promise<Response> {
return new Response("Not found", { status: 404 });
}
const stat = await fsp.stat(realIndexPath);
if (stat.isFile()) return fileResponse(realIndexPath, "text/html");
if (stat.isFile()) {
return fileResponse(request, realRoot, realIndexPath, "text/html");
}
} catch {
// Fall through to a generic not-found response.
}
Expand Down
19 changes: 16 additions & 3 deletions backend/src/services/pullRequestPreviewHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
readSync,
realpathSync,
renameSync,
rmdirSync,
rmSync,
writeFileSync,
} from "node:fs";
Expand Down Expand Up @@ -596,7 +597,7 @@ function previewWorktreePath(config: PullRequestPreviewConfig): string {
return config.managedWorktreePath;
}

async function unregisterMissingPreviewWorktree(
async function unregisterPreviewWorktreeIfRegistered(
config: PullRequestPreviewConfig,
worktreePath: string,
signal?: AbortSignal
Expand Down Expand Up @@ -641,7 +642,11 @@ async function removePreviewWorktree(
throw new Error("Refusing to remove an unmanaged preview worktree");
}
if (!existsSync(resolvedWorktreePath)) {
return unregisterMissingPreviewWorktree(config, resolvedWorktreePath, signal);
return unregisterPreviewWorktreeIfRegistered(
config,
resolvedWorktreePath,
signal
);
}
if (!isRealDirectory(resolvedWorktreePath)) {
throw new Error("Preview worktree path must be a real directory");
Expand Down Expand Up @@ -709,6 +714,14 @@ async function ensurePreviewWorktree(
if (!isRealDirectory(worktreePath)) {
throw new Error("Preview worktree path must be a real directory");
}
if (readdirSync(worktreePath).length === 0) {
await unregisterPreviewWorktreeIfRegistered(config, worktreePath, signal);
if (existsSync(worktreePath)) {
rmdirSync(worktreePath);
}
}
}
if (existsSync(worktreePath)) {
const { stdout: registeredRoot } = await runCommand(
"git",
["-C", worktreePath, "rev-parse", "--show-toplevel"],
Expand All @@ -732,7 +745,7 @@ async function ensurePreviewWorktree(
signal,
});
} else {
await unregisterMissingPreviewWorktree(config, worktreePath, signal);
await unregisterPreviewWorktreeIfRegistered(config, worktreePath, signal);
await runCommand(
"git",
[
Expand Down
164 changes: 164 additions & 0 deletions backend/src/staticFileResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type { Stats } from "node:fs";
import fs from "node:fs/promises";

interface StaticFileResponseOptions {
cacheControl: string;
contentType?: string;
}

type ContentEncoding = "br" | "gzip";

interface StaticRepresentation {
contentEncoding?: ContentEncoding;
filePath: string;
stat: Stats;
}

const COMPRESSION_SIDECARS: ReadonlyArray<{
contentEncoding: ContentEncoding;
extension: ".br" | ".gz";
}> = [
{ contentEncoding: "br", extension: ".br" },
{ contentEncoding: "gzip", extension: ".gz" },
];

function encodingQuality(headerValue: string | null, encoding: string): number {
if (!headerValue) return 0;

let explicitQuality: number | undefined;
let wildcardQuality: number | undefined;
for (const item of headerValue.split(",")) {
const [rawEncoding = "", ...parameters] = item.split(";");
const normalizedEncoding = rawEncoding.trim().toLowerCase();
let quality = 1;
for (const parameter of parameters) {
const normalizedParameter = parameter.trim().toLowerCase();
if (normalizedParameter.startsWith("q")) {
const match = normalizedParameter.match(
/^q\s*=\s*(0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/u
);
quality = match ? Number(match[1]) : 0;
}
}
if (normalizedEncoding === encoding) explicitQuality = quality;
if (normalizedEncoding === "*") wildcardQuality = quality;
}

return explicitQuality ?? wildcardQuality ?? 0;
}

async function compressedRepresentations(
filePath: string
): Promise<StaticRepresentation[]> {
const representations: StaticRepresentation[] = [];
for (const { contentEncoding, extension } of COMPRESSION_SIDECARS) {
const sidecarPath = `${filePath}${extension}`;
try {
const stat = await fs.lstat(sidecarPath);
if (stat.isFile()) {
representations.push({
contentEncoding,
filePath: sidecarPath,
stat,
});
}
} catch {
// This representation was not generated for the source file.
}
}
return representations;
}

function preferredRepresentation(
request: Request,
source: StaticRepresentation,
compressed: StaticRepresentation[]
): StaticRepresentation {
return (
compressed
.map((representation, priority) => ({
priority,
quality: encodingQuality(
request.headers.get("accept-encoding"),
representation.contentEncoding ?? ""
),
representation,
}))
.filter(({ quality }) => quality > 0)
.toSorted(
(left, right) =>
right.quality - left.quality || left.priority - right.priority
)[0]?.representation ?? source
);
}

function entityTagFor(representation: StaticRepresentation): string {
const encodingSuffix = representation.contentEncoding
? `-${representation.contentEncoding}`
: "";
return `W/"${representation.stat.size.toString(16)}-${Math.trunc(
representation.stat.mtimeMs
).toString(16)}${encodingSuffix}"`;
}

function normalizedEntityTag(entityTag: string): string {
return entityTag.trim().replace(/^W\//iu, "");
}

function isEntityTagMatch(headerValue: string, entityTag: string): boolean {
const normalizedCurrent = normalizedEntityTag(entityTag);
return headerValue.split(",").some((candidate) => {
const trimmed = candidate.trim();
return trimmed === "*" || normalizedEntityTag(trimmed) === normalizedCurrent;
});
}

function isNotModified(request: Request, entityTag: string, modifiedAt: Date): boolean {
if (!["GET", "HEAD"].includes(request.method.toUpperCase())) return false;

const ifNoneMatch = request.headers.get("if-none-match");
if (ifNoneMatch !== null) {
return isEntityTagMatch(ifNoneMatch, entityTag);
}

const ifModifiedSince = request.headers.get("if-modified-since");
if (!ifModifiedSince) return false;
const conditionalTimestamp = Date.parse(ifModifiedSince);
return (
Number.isFinite(conditionalTimestamp) &&
Math.floor(modifiedAt.getTime() / 1000) <= Math.floor(conditionalTimestamp / 1000)
);
}

/**
* Serves a verified static file with conditional requests and negotiated
* precompressed representations.
*/
export async function staticFileResponse(
request: Request,
filePath: string,
{ cacheControl, contentType }: StaticFileResponseOptions
): Promise<Response> {
const sourceStat = await fs.stat(filePath);
const source: StaticRepresentation = { filePath, stat: sourceStat };
const compressed = await compressedRepresentations(filePath);
const representation = preferredRepresentation(request, source, compressed);
const entityTag = entityTagFor(representation);
const headers = new Headers({
"Cache-Control": cacheControl,
ETag: entityTag,
"Last-Modified": sourceStat.mtime.toUTCString(),
});

const resolvedContentType = contentType ?? Bun.file(filePath).type;
if (resolvedContentType) headers.set("Content-Type", resolvedContentType);
if (compressed.length > 0) headers.set("Vary", "Accept-Encoding");
if (representation.contentEncoding) {
headers.set("Content-Encoding", representation.contentEncoding);
}

if (isNotModified(request, entityTag, sourceStat.mtime)) {
return new Response(undefined, { headers, status: 304 });
}
return new Response(Bun.file(representation.filePath), { headers });
}
Loading