Skip to content
File

Blob: src/worker/sites/assets.ts

typescript81 lines
1import { and, eq } from "drizzle-orm";
2 
3import type { Db } from "@/worker/db/d1/client";
4import { uploads } from "@/worker/db/d1/schema";
5import type { ResolvedPublishedPage, ResolvedSite } from "@/worker/lib/published-pages";
6import { renderSiteNotFoundDocumentHtml } from "@/sites/document";
7import { withSitesStaticDocumentPreloadHeaders } from "@/worker/sites/preload-headers";
8import { resolveSitesDocumentAssets } from "@/worker/sites/manifest";
9 
10export interface AssetGateArgs {
11 env: Pick<Env, "ASSETS" | "R2">;
12 db: Db;
13 site: ResolvedSite;
14 page: ResolvedPublishedPage;
15 uploadId: string;
16}
17 
18/**
19 * Serve `/_assets/<pageId>/<uploadId>` for a resolved site.
20 *
21 * All failure modes collapse to a 404 so existence cannot leak across sites,
22 * pages, or workspaces. Order matches the Class-A invariant: D1 publish-set
23 * resolution before any R2 read.
24 */
25export async function serveSiteAsset(args: AssetGateArgs): Promise<Response> {
26 const { env, db, site, page, uploadId } = args;
27 
28 const upload = await db
29 .select({
30 id: uploads.id,
31 content_type: uploads.content_type,
32 r2_key: uploads.r2_key,
33 })
34 .from(uploads)
35 .where(and(eq(uploads.id, uploadId), eq(uploads.workspace_id, site.workspace_id), eq(uploads.page_id, page.id)))
36 .get();
37 
38 if (!upload) return notFound(env, site);
39 
40 const object = await env.R2.get(upload.r2_key);
41 if (!object) return notFound(env, site);
42 
43 return new Response(object.body, {
44 headers: {
45 "Content-Type": upload.content_type,
46 "Content-Length": String(object.size),
47 "Cache-Control": "public, max-age=300, must-revalidate",
48 },
49 });
50}
51 
52async function notFound(env: Pick<Env, "ASSETS">, site: ResolvedSite): Promise<Response> {
53 const assets = await resolveSitesDocumentAssets(env);
54 if (!assets) {
55 return new Response("Sites assets unavailable", {
56 status: 500,
57 headers: {
58 "Content-Type": "text/plain; charset=utf-8",
59 "Cache-Control": "no-store",
60 },
61 });
62 }
63 
64 return new Response(
65 renderSiteNotFoundDocumentHtml({
66 site: { workspaceName: site.workspace_name, workspaceIcon: site.workspace_icon, homeHref: "/" },
67 assets,
68 }),
69 {
70 status: 404,
71 headers: withSitesStaticDocumentPreloadHeaders(
72 {
73 "Content-Type": "text/html; charset=utf-8",
74 "Cache-Control": "no-store",
75 },
76 assets,
77 ),
78 },
79 );
80}