Squashed commit of the following:

commit b2b0470a1d9267d042ec0ac66523c6635bf5b199
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:13:38 2026 +0200

    chore: update .gitignore to include .vite-hooks and modify pnpm-lock.yaml for dependencies

commit d28fadb5cd8706c874e616102878b4a394ec84c1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:08:04 2026 +0200

    fix: remove timestamp conflict guard

commit c6998d9dbab19d09d3c8054feef1d2e4117555eb
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 12:11:51 2026 +0200

    chore(release): v5.1.5

commit f33d168711804880e1f12e88d24290aae16cc258
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:35 2026 +0200

    revert: compose.yml

commit d961e6535811a10c335525fb33a08d03e737278d
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:08 2026 +0200

    refactor(agent): replace 'revert' terminology with 'restore' for clarity, resolves #3086

commit 17f351171be218e33f01c469d95e4164d4c8dc57
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:10:41 2026 +0200

    refactor(pdf): simplify sidebar section filtering and update summary feature logic

commit d55179b9d76879e3204de185e8b53fadd0a107ed
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:53:37 2026 +0200

    chore: update pnpm-lock.yaml and turbo.json

commit 7cade6980e1a04352536bd44ef773f338c4ef599
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:38:30 2026 +0200

    fix(polyfill): add tested polyfill for Map Upsert methods

commit 26d175bb9c53d93225d1e907678445252c13d660
Merge: 1cf33dc6c 5b1297fa2
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:23:29 2026 +0200

    Merge remote-tracking branch 'origin/main' into feat/explore-hono-orpc-migration

    # Conflicts:
    #	packages/api/src/services/agent-url.ts
    #	packages/runtime-externals/package.json

commit 1cf33dc6c9d81735730ad656e16dab6501c6d6a1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:22:12 2026 +0200

    chore: preserve branch changes before main sync

commit b380a4b00fdbcdd81ff4f8ef72b330fd027ccda5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Mon May 18 07:50:28 2026 +0200

    chore: lot of fixes for monorepo migration

commit 8fcf0ec64e1c29572ebaff494338368bfcf75760
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 13:57:17 2026 +0200

    chore: update knip version and refine web app routing with new SEO endpoints

commit 234e68086ff15610a93877354c98e2c020364533
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 12:10:06 2026 +0200

    refactor(auth): update OAuth routes to include API prefix and remove unused schema endpoint

commit 91c84b9a8496b0ce21d71cae9f8b2a027638c9ac
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:54:29 2026 +0200

    chore: update dependencies and enhance PWA metadata in web app

commit 150117d4a5a9dd6cd92c64891aad8cae90f6a7af
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:12:35 2026 +0200

    docs: revise manifest-only pwa testing scope

commit 6b939a55661aec9dd8122b184e4b60a5c7325fb5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:11:33 2026 +0200

    docs: add manifest-only pwa design

commit 1422e1fc96c400948b273210a1067251087d15d4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:05:04 2026 +0200

    chore(dev): simplify server proxy config

commit bc2ff5a9f6fda41e6c40333c8f163aa23a6c5e48
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:50 2026 +0200

    docs: add unsafe oauth redirect plan

commit 445359ebe9b96c1515bf1c4c3f73ba8a8448ec12
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:34 2026 +0200

    feat(auth): add unsafe oauth redirect flag

commit 73fffdd24598e56b2793f7657919bc794835892e
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:55:02 2026 +0200

    docs: design unsafe oauth redirect flag

commit c0066aa19c15fc8a4c8e5179ed49889c117519f4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:22:04 2026 +0200

    chore: update translation source paths

commit 9033da082418d252aafd6c2eed72f71f014be3d9
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:09:25 2026 +0200

    refactor(arch): react spa + hono migration

commit 6f27936c11bda895977dc63ee550c3346d4ce24b
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 01:10:47 2026 +0200

    docs: add docker nightly tagging design

commit ecc1fd9a88a0ee1dca2f1977dfc17f74527fe1da
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Thu May 14 20:05:44 2026 +0200

    feat: migrate to hono spa server
This commit is contained in:
Amruth Pillai
2026-05-19 13:14:21 +02:00
parent 5b1297fa2b
commit 62f8270b3e
518 changed files with 29398 additions and 26871 deletions
+124
View File
@@ -0,0 +1,124 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
handleAuth: vi.fn(),
handleOAuth: vi.fn(),
handleRpc: vi.fn(),
handleOpenApi: vi.fn(),
handleHealth: vi.fn(),
handleUpload: vi.fn(),
handleMcp: vi.fn(),
handleMcpServerCard: vi.fn(),
handleOAuthAuthorizationServer: vi.fn(),
handleOAuthProtectedResource: vi.fn(),
handleOpenIdConfiguration: vi.fn(),
handleWellKnownFallback: vi.fn(),
handleRobots: vi.fn(),
handleSitemap: vi.fn(),
handleLlms: vi.fn(),
serveWebDistStatic: vi.fn(),
handleWebApp: vi.fn(),
handleWebAppHead: vi.fn(),
}));
vi.mock("./auth", () => ({
handleAuth: mocks.handleAuth,
handleOAuth: mocks.handleOAuth,
}));
vi.mock("./health", () => ({
handleHealth: mocks.handleHealth,
}));
vi.mock("../rpc/handler", () => ({
handleRpc: mocks.handleRpc,
}));
vi.mock("../openapi/handler", () => ({
handleOpenApi: mocks.handleOpenApi,
}));
vi.mock("../openapi/metadata", () => ({
handleMcpServerCard: mocks.handleMcpServerCard,
handleOAuthAuthorizationServer: mocks.handleOAuthAuthorizationServer,
handleOAuthProtectedResource: mocks.handleOAuthProtectedResource,
handleOpenIdConfiguration: mocks.handleOpenIdConfiguration,
handleWellKnownFallback: mocks.handleWellKnownFallback,
}));
vi.mock("../static/uploads", () => ({
handleUpload: mocks.handleUpload,
}));
vi.mock("../static/seo", () => ({
handleRobots: mocks.handleRobots,
handleSitemap: mocks.handleSitemap,
handleLlms: mocks.handleLlms,
}));
vi.mock("../static/web", () => ({
serveWebDistStatic: mocks.serveWebDistStatic,
handleWebApp: mocks.handleWebApp,
handleWebAppHead: mocks.handleWebAppHead,
}));
vi.mock("../mcp/handler", () => ({
handleMcp: mocks.handleMcp,
}));
beforeEach(() => {
vi.clearAllMocks();
mocks.handleAuth.mockResolvedValue(new Response("auth"));
mocks.handleOAuth.mockResolvedValue(new Response("oauth"));
mocks.handleRpc.mockResolvedValue(new Response("rpc"));
mocks.handleOpenApi.mockResolvedValue(new Response("openapi"));
mocks.handleHealth.mockReturnValue(new Response("health"));
mocks.handleUpload.mockResolvedValue(new Response("upload"));
mocks.handleMcp.mockResolvedValue(new Response("mcp"));
mocks.handleMcpServerCard.mockReturnValue(new Response("server-card"));
mocks.handleOAuthAuthorizationServer.mockReturnValue(new Response("oauth-authorization-server"));
mocks.handleOAuthProtectedResource.mockReturnValue(new Response("oauth-protected-resource"));
mocks.handleOpenIdConfiguration.mockReturnValue(new Response("openid-configuration"));
mocks.handleWellKnownFallback.mockReturnValue(new Response("well-known"));
mocks.handleRobots.mockReturnValue(new Response("robots"));
mocks.handleSitemap.mockReturnValue(new Response("sitemap"));
mocks.handleLlms.mockReturnValue(new Response("llms"));
mocks.serveWebDistStatic.mockResolvedValue(undefined);
mocks.handleWebApp.mockResolvedValue(new Response("web"));
mocks.handleWebAppHead.mockReturnValue(new Response(null));
});
describe("createApp", () => {
it("routes /api/auth/oauth to the OAuth bridge before the Better Auth wildcard", async () => {
const { createApp } = await import("./app");
const app = createApp();
const request = new Request("http://localhost:3001/api/auth/oauth?client_id=test-client");
const response = await app.fetch(request);
await expect(response.text()).resolves.toBe("oauth");
expect(mocks.handleOAuth).toHaveBeenCalledWith(request);
expect(mocks.handleAuth).not.toHaveBeenCalled();
});
it.each([
["GET", "/robots.txt", "robots", mocks.handleRobots],
["HEAD", "/robots.txt", "", mocks.handleRobots],
["GET", "/sitemap.xml", "sitemap", mocks.handleSitemap],
["HEAD", "/sitemap.xml", "", mocks.handleSitemap],
["GET", "/llms.txt", "llms", mocks.handleLlms],
["HEAD", "/llms.txt", "", mocks.handleLlms],
])("routes %s %s before the static fallback", async (method, pathname, expectedBody, handler) => {
const { createApp } = await import("./app");
const app = createApp();
const request = new Request(`http://localhost:3001${pathname}`, { method });
const response = await app.fetch(request);
await expect(response.text()).resolves.toBe(expectedBody);
expect(handler).toHaveBeenCalledWith({ head: method === "HEAD" });
expect(mocks.serveWebDistStatic).not.toHaveBeenCalled();
expect(mocks.handleWebApp).not.toHaveBeenCalled();
expect(mocks.handleWebAppHead).not.toHaveBeenCalled();
});
});
+52
View File
@@ -0,0 +1,52 @@
import { Hono } from "hono";
import { handleMcp } from "../mcp/handler";
import { handleOpenApi } from "../openapi/handler";
import {
handleMcpServerCard,
handleOAuthAuthorizationServer,
handleOAuthProtectedResource,
handleOpenIdConfiguration,
handleWellKnownFallback,
} from "../openapi/metadata";
import { handleRpc } from "../rpc/handler";
import { handleSchemaJson } from "../static/schema";
import { handleLlms, handleRobots, handleSitemap } from "../static/seo";
import { handleUpload } from "../static/uploads";
import { handleWebApp, handleWebAppHead, serveWebDistStatic } from "../static/web";
import { handleAuth, handleOAuth } from "./auth";
import { handleHealth } from "./health";
export function createApp() {
const app = new Hono();
app.all("/api/rpc", (c) => handleRpc(c.req.raw));
app.all("/api/rpc/*", (c) => handleRpc(c.req.raw));
app.all("/api/openapi", (c) => handleOpenApi(c.req.raw));
app.all("/api/openapi/*", (c) => handleOpenApi(c.req.raw));
app.get("/api/auth/oauth", (c) => handleOAuth(c.req.raw));
app.all("/api/auth/*", (c) => handleAuth(c.req.raw));
app.get("/api/health", () => handleHealth());
app.get("/api/uploads/*", (c) => handleUpload(c.req.raw));
app.get("/uploads/*", (c) => handleUpload(c.req.raw));
app.get("/schema.json", () => handleSchemaJson());
app.all("/mcp", (c) => handleMcp(c.req.raw));
app.all("/mcp/*", (c) => handleMcp(c.req.raw));
app.get("/.well-known/mcp/server-card.json", () => handleMcpServerCard());
app.get("/.well-known/oauth-authorization-server", (c) => handleOAuthAuthorizationServer(c.req.raw));
app.get("/.well-known/oauth-authorization-server/*", (c) => handleOAuthAuthorizationServer(c.req.raw));
app.get("/.well-known/openid-configuration", (c) => handleOpenIdConfiguration(c.req.raw));
app.get("/.well-known/oauth-protected-resource", () => handleOAuthProtectedResource());
app.get("/.well-known/oauth-protected-resource/*", () => handleOAuthProtectedResource());
app.all("/.well-known/*", () => handleWellKnownFallback());
app.on(["GET", "HEAD"], "/robots.txt", (c) => handleRobots({ head: c.req.method === "HEAD" }));
app.on(["GET", "HEAD"], "/sitemap.xml", (c) => handleSitemap({ head: c.req.method === "HEAD" }));
app.on(["GET", "HEAD"], "/llms.txt", (c) => handleLlms({ head: c.req.method === "HEAD" }));
app.use("/*", serveWebDistStatic);
app.on(["GET"], "/*", (c) => handleWebApp(c.req.raw));
app.on(["HEAD"], "/*", (c) => handleWebAppHead(c.req.raw));
return app;
}
+97
View File
@@ -0,0 +1,97 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
getSession: vi.fn(),
handler: vi.fn(),
env: {
SERVER_PORT: 3001,
APP_URL: "http://localhost:3000",
FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI: false,
},
}));
vi.mock("@reactive-resume/auth/config", () => ({
auth: {
api: {
getSession: mocks.getSession,
},
handler: mocks.handler,
},
}));
vi.mock("@reactive-resume/db/client", () => ({ db: {} }));
vi.mock("@reactive-resume/db/schema", () => ({ oauthClient: {}, verification: {} }));
vi.mock("@reactive-resume/env/server", () => ({
env: mocks.env,
}));
beforeEach(() => {
vi.clearAllMocks();
mocks.env.FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI = false;
mocks.handler.mockResolvedValue(new Response("ok"));
});
describe("handleAuth", () => {
it("rejects untrusted dynamic OAuth redirect URIs in safe mode", async () => {
const { handleAuth } = await import("./auth");
const response = await handleAuth(
new Request("http://localhost:3001/api/auth/oauth2/register", {
method: "POST",
body: JSON.stringify({ redirect_uris: ["https://evil.example.com/callback"] }),
headers: { "content-type": "application/json" },
}),
);
expect(response.status).toBe(400);
await expect(response.json()).resolves.toEqual({
error: "invalid_redirect_uri",
error_description: "redirect_uri is not allowed",
});
expect(mocks.handler).not.toHaveBeenCalled();
});
it("forwards custom-scheme dynamic OAuth redirect URIs when unsafe mode is enabled", async () => {
const { handleAuth } = await import("./auth");
mocks.env.FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI = true;
const response = await handleAuth(
new Request("http://localhost:3001/api/auth/oauth2/register", {
method: "POST",
body: JSON.stringify({ redirect_uris: ["myapp://callback"] }),
headers: { "content-type": "application/json" },
}),
);
expect(response.status).toBe(200);
expect(mocks.handler).toHaveBeenCalledOnce();
});
});
describe("handleOAuth", () => {
it("redirects unauthenticated users to the same-origin login route", async () => {
const { handleOAuth } = await import("./auth");
mocks.getSession.mockResolvedValueOnce(null);
const response = await handleOAuth(
new Request(
"http://localhost:3001/api/auth/oauth?client_id=test-client&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&state=abc&exp=123&sig=456",
),
);
expect(response.status).toBe(302);
const location = response.headers.get("Location");
expect(location).toMatch(/^\/auth\/login\?/);
const loginUrl = new URL(location ?? "", "http://localhost:3000");
const callbackUrl = new URL(loginUrl.searchParams.get("callbackURL") ?? "", "http://localhost:3000");
expect(loginUrl.origin).toBe("http://localhost:3000");
expect(callbackUrl.pathname).toBe("/api/auth/oauth");
expect(callbackUrl.searchParams.get("client_id")).toBe("test-client");
expect(callbackUrl.searchParams.get("redirect_uri")).toBe("https://example.com/callback");
expect(callbackUrl.searchParams.get("state")).toBe("abc");
expect(callbackUrl.searchParams.has("exp")).toBe(false);
expect(callbackUrl.searchParams.has("sig")).toBe(false);
});
});
+212
View File
@@ -0,0 +1,212 @@
import crypto from "node:crypto";
import { eq } from "drizzle-orm";
import { auth } from "@reactive-resume/auth/config";
import { db } from "@reactive-resume/db/client";
import { oauthClient, verification } from "@reactive-resume/db/schema";
import { env } from "@reactive-resume/env/server";
import { generateId } from "@reactive-resume/utils/string";
import { isAllowedOAuthRedirectUri } from "@reactive-resume/utils/url-security.node";
const oauthAuthorizeSanitizedParams = [
"prompt",
"redirect_uri",
"client_id",
"code_challenge",
"code_challenge_method",
"response_type",
"scope",
"state",
"resource",
] as const;
function sanitizeOAuthAuthorizeRequest(request: Request): Request {
if (request.method !== "GET") return request;
const url = new URL(request.url);
if (!url.pathname.endsWith("/oauth2/authorize")) return request;
const sanitizeValue = (value: string) =>
value
.replace(/[\r\n\t]+/g, " ")
.replace(/\s+/g, " ")
.trim();
const sanitizeParam = (key: string) => {
const value = url.searchParams.get(key);
if (!value) return;
url.searchParams.set(key, sanitizeValue(value));
};
for (const key of oauthAuthorizeSanitizedParams) sanitizeParam(key);
const redirectUri = url.searchParams.get("redirect_uri");
if (redirectUri && !URL.canParse(redirectUri)) {
try {
const decodedRedirectUri = decodeURIComponent(redirectUri);
if (URL.canParse(decodedRedirectUri)) {
url.searchParams.set("redirect_uri", decodedRedirectUri);
}
} catch {
// Ignore malformed encoded values and let Better Auth validation handle them.
}
}
if (url.toString() === request.url) return request;
return new Request(url.toString(), request);
}
async function defaultPublicClientRegistration(request: Request): Promise<Request> {
if (request.method !== "POST") return request;
const url = new URL(request.url);
if (!url.pathname.endsWith("/oauth2/register")) return request;
const cloned = request.clone();
let body: Record<string, unknown>;
try {
body = await cloned.json();
} catch {
return request;
}
if (!request.headers.get("authorization")) {
body.token_endpoint_auth_method = "none";
}
return new Request(url.toString(), {
method: request.method,
headers: request.headers,
body: JSON.stringify(body),
});
}
async function validateDynamicClientRegistrationRequest(request: Request): Promise<Response | undefined> {
if (request.method !== "POST") return;
const url = new URL(request.url);
if (!url.pathname.endsWith("/oauth2/register")) return;
const cloned = request.clone();
let body: Record<string, unknown>;
try {
body = await cloned.json();
} catch {
return Response.json({ message: "Invalid registration payload" }, { status: 400 });
}
const oauthTrustedOrigins = [new URL(env.APP_URL).origin.toLowerCase()];
const redirectUris = Array.isArray(body.redirect_uris) ? body.redirect_uris : [];
for (const redirectUri of redirectUris) {
if (
typeof redirectUri !== "string" ||
!isAllowedOAuthRedirectUri(redirectUri, oauthTrustedOrigins, {
allowUnsafe: env.FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI,
})
) {
return Response.json(
{ error: "invalid_redirect_uri", error_description: "redirect_uri is not allowed" },
{ status: 400 },
);
}
}
}
export async function handleAuth(request: Request) {
const registrationValidationError = await validateDynamicClientRegistrationRequest(request);
if (registrationValidationError) return registrationValidationError;
const sanitizedRequest = sanitizeOAuthAuthorizeRequest(request);
const finalRequest = await defaultPublicClientRegistration(sanitizedRequest);
return auth.handler(finalRequest);
}
function generateCode() {
return crypto.randomBytes(32).toString("base64url");
}
function hashCode(code: string) {
return crypto.createHash("sha256").update(code).digest("base64url");
}
export async function handleOAuth(request: Request) {
const session = await auth.api.getSession({ headers: request.headers });
const url = new URL(request.url);
if (session?.user) {
const clientId = url.searchParams.get("client_id");
const redirectUri = url.searchParams.get("redirect_uri");
const state = url.searchParams.get("state");
const scope = url.searchParams.get("scope");
const codeChallenge = url.searchParams.get("code_challenge");
const codeChallengeMethod = url.searchParams.get("code_challenge_method");
if (!clientId || !redirectUri) {
return Response.json({ error: "missing client_id or redirect_uri" }, { status: 400 });
}
const [client] = await db.select().from(oauthClient).where(eq(oauthClient.clientId, clientId)).limit(1);
if (!client) {
return Response.json({ error: "invalid client" }, { status: 400 });
}
if (!client.redirectUris.includes(redirectUri)) {
return Response.json({ error: "invalid redirect_uri" }, { status: 400 });
}
const code = generateCode();
const hashedCode = hashCode(code);
const now = new Date();
const expiresAt = new Date(now.getTime() + 600_000);
await db.insert(verification).values({
id: generateId(),
identifier: hashedCode,
value: JSON.stringify({
type: "authorization_code",
query: {
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
scope,
state,
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
},
userId: session.user.id,
sessionId: session.session.id,
authTime: new Date(session.session.createdAt).getTime(),
}),
expiresAt,
createdAt: now,
updatedAt: now,
});
const callbackUrl = new URL(redirectUri);
callbackUrl.searchParams.set("code", code);
if (state) callbackUrl.searchParams.set("state", state);
callbackUrl.searchParams.set("iss", `${env.APP_URL}/api/auth`);
return new Response(null, {
status: 302,
headers: { Location: callbackUrl.toString() },
});
}
const loginUrl = new URL("/auth/login", env.APP_URL);
const oauthParams = new URLSearchParams();
for (const [key, value] of url.searchParams) {
if (!["exp", "sig"].includes(key)) {
oauthParams.set(key, value);
}
}
loginUrl.searchParams.set("callbackURL", `/api/auth/oauth?${oauthParams.toString()}`);
return new Response(null, {
status: 302,
headers: { Location: `${loginUrl.pathname}${loginUrl.search}` },
});
}
+22
View File
@@ -0,0 +1,22 @@
export function getCookie(request: Request, name: string): string | undefined {
const cookieHeader = request.headers.get("cookie");
if (!cookieHeader) return;
for (const part of cookieHeader.split(";")) {
const [rawName, ...rawValue] = part.trim().split("=");
if (rawName === name && rawValue.length > 0) return rawValue.join("=");
}
}
export function mergeResponseHeaders(response: Response, headers: Headers): Response {
if ([...headers].length === 0) return response;
const nextHeaders = new Headers(response.headers);
for (const [key, value] of headers) nextHeaders.append(key, value);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: nextHeaders,
});
}
+102
View File
@@ -0,0 +1,102 @@
import { sql } from "drizzle-orm";
import { getStorageService } from "@reactive-resume/api/features/storage";
import { db } from "@reactive-resume/db/client";
const HEALTHCHECK_TIMEOUT_MS = 1_500;
type CheckResult = {
status: "healthy" | "unhealthy";
latencyMs: number;
error?: string;
[key: string]: unknown;
};
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
return "Unknown error";
}
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
let timeoutId: NodeJS.Timeout | undefined;
const timeout = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
});
try {
return await Promise.race([promise, timeout]);
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
async function runCheck(check: () => Promise<object>): Promise<CheckResult> {
const startedAt = performance.now();
try {
const data = await withTimeout(check(), HEALTHCHECK_TIMEOUT_MS);
const latencyMs = Math.round(performance.now() - startedAt);
const result = data as { status?: string };
if (result.status === "unhealthy") return { ...(data as object), status: "unhealthy", latencyMs };
return { ...(data as object), status: "healthy", latencyMs };
} catch (error) {
return {
status: "unhealthy",
error: getErrorMessage(error),
latencyMs: Math.round(performance.now() - startedAt),
};
}
}
async function checkDatabase() {
try {
await db.execute(sql`SELECT 1`);
return { status: "healthy" };
} catch (error) {
return {
status: "unhealthy",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
async function checkStorage() {
try {
const storageService = getStorageService();
return await storageService.healthcheck();
} catch (error) {
return {
status: "unhealthy",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
export async function handleHealth() {
const [database, storage] = await Promise.all([runCheck(checkDatabase), runCheck(checkStorage)]);
const status = [database, storage].some((check) => check.status === "unhealthy") ? "unhealthy" : "healthy";
const checks = {
service: "reactive-resume",
version: process.env.npm_package_version,
status,
timestamp: new Date().toISOString(),
uptime: `${process.uptime().toFixed(2)}s`,
database,
storage,
};
if (status === "unhealthy") {
console.warn("[Healthcheck]", { route: "/api/health", database, storage });
}
const headers = new Headers();
const body = JSON.stringify(checks);
headers.set("Content-Type", "application/json; charset=UTF-8");
headers.set("Content-Length", Buffer.byteLength(body, "utf-8").toString());
return new Response(body, {
headers,
status: checks.status === "unhealthy" ? 503 : 200,
});
}