Skip to the content

Capsid menu

workers/app.ts

53 lines and 6 functions in DrDustinEdwards/carrel, at commit d5d5e82 on 2026-10-24. This file on GitHub.

Functions

The functions of app.ts
FunctionLinesComplexityCoveredCRAPMutation
<anonymous:20>20 to 2010%OkCRAP 2No data: mutation testing has not run on this repo yet
fetch25 to 4330%WatchCRAP 12No data: mutation testing has not run on this repo yet
<anonymous:35>35 to 4110%OkCRAP 2No data: mutation testing has not run on this repo yet
scheduled45 to 5720%WatchCRAP 6No data: mutation testing has not run on this repo yet
<anonymous:49>49 to 4910%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:56>56 to 5610%OkCRAP 2No data: mutation testing has not run on this repo yet

Imports

It imports app/lib/backup-checks.ts, app/lib/backup.server.ts, app/lib/context.ts, app/lib/google/refresh.server.ts, app/lib/health.server.ts, app/lib/inbox-report.server.ts, app/lib/mcp/door.ts, app/lib/mcp/server.ts, app/lib/refresh.server.ts, app/lib/sites.server.ts, app/lib/social/queue.server.ts, workers/csp-report.ts, workers/gate.ts, workers/security-headers.ts. No module of this repo imports it.

Source

import { createRequestHandler, RouterContextProvider } from "react-router";

import { cloudflareContext, nonceContext, viewerContext } from "~/lib/context";
import { BACKUP_CRON } from "~/lib/backup-checks";
import { runBackup } from "~/lib/backup.server";
import { refreshGoogle } from "~/lib/google/refresh.server";
import { runHealth } from "~/lib/health.server";
import { reportInbox } from "~/lib/inbox-report.server";
import { aiDoor, isMcpHost, mcpOrigin } from "~/lib/mcp/door";
import { carrelOrigin } from "~/lib/mcp/server";
import { refreshAllSites } from "~/lib/refresh.server";
import { siteOrigins } from "~/lib/sites.server";
import { processSocial } from "~/lib/social/queue.server";

import { cspReport, isCspReport } from "./csp-report";
import { gate } from "./gate";
import { newNonce, withDoorSecurityHeaders, withSecurityHeaders } from "./security-headers";

const requestHandler = createRequestHandler(
  () => import("virtual:react-router/server-build"),
  import.meta.env.MODE,
);

export default {
  async fetch(request, env, ctx) {
    // Two doors, told apart by the hostname alone. The AI door (carrel-mcp.dustinedwards.info) speaks
    // OAuth and MCP and nothing else, plus the CSP report sink; the pages are never served there.
    // Everything else is the browser door, behind Access. Every response on either carries the
    // security headers (workers/security-headers.ts), refusals and redirects included.
    if (isMcpHost(request, env)) {
      const response = isCspReport(request) ? await cspReport(request, carrelOrigin(env)) : await aiDoor(request, env, ctx);
      return withDoorSecurityHeaders(response, mcpOrigin(env));
    }
    const nonce = newNonce();
    const response = await gate(request, env, async (req, viewer) => {
      const context = new RouterContextProvider();
      context.set(cloudflareContext, { env, ctx });
      context.set(viewerContext, viewer);
      context.set(nonceContext, nonce);
      return requestHandler(req, context);
    });
    return withSecurityHeaders(response, nonce, { imageOrigins: siteOrigins(env), reportOrigin: mcpOrigin(env) });
  },

  scheduled(controller, env, ctx) {
    // The nightly cron runs the dump alone. A failure is logged and left for the health check, whose
    // backup age keeps growing, to report; it is never swallowed.
    if (controller.cron === BACKUP_CRON) {
      ctx.waitUntil(runBackup(env).catch((error) => console.error(JSON.stringify({ backup: "failed", error: String(error) }))));
      return;
    }
    ctx.waitUntil(runHealth(env));
    ctx.waitUntil(refreshAllSites(env));
    ctx.waitUntil(refreshGoogle(env));
    ctx.waitUntil(reportInbox(env));
    ctx.waitUntil(processSocial(env).catch((error) => console.error(JSON.stringify({ social: "tick-failed", error: String(error) }))));
  },
} satisfies ExportedHandler<Env>;