Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 9 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
"$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": {
"includes": ["src/playground/**", "scripts/**", "*.mjs", "*.json", "*.jsonc", "!public"]
"includes": [
"src/playground/**",
"scripts/**",
"test/**",
"*.mjs",
"*.json",
"*.jsonc",
"!public"
]
},
"formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 100 },
"linter": { "enabled": true, "rules": { "preset": "recommended" } }
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"typecheck": "astro sync && tsc --noEmit -p tsconfig.json",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"check": "pnpm lint && pnpm typecheck"
"test": "node --test test/*.test.ts",
"check": "pnpm lint && pnpm typecheck && pnpm test"
},
"dependencies": {
"@astrojs/cloudflare": "14.3.4",
Expand Down
42 changes: 42 additions & 0 deletions src/playground/demo-media-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Demo images by their media URL, without a session (`/media/<sha16>/<slug>.png`).
*
* Studio's article preview is a sandboxed document (its CSP carries `sandbox`), so its image
* requests are cross-site and leave out the session cookie (`SameSite=Lax`). Without a session
* the playground answers `/media/…` with a redirect to the landing page, and the preview showed a
* broken image for every cover. Uploads are refused in the playground, so its only media are the
* demo images, whose bytes are public static files already (`public/demo/`): answer those here.
*/
import { DEMO_MEDIA, DEMO_SITE_ID } from "./demo-media.ts";

const BY_PREFIX = new Map(DEMO_MEDIA.map((m) => [m.sha256.slice(0, 16), m]));
const MEDIA_PATH = /^\/media\/([0-9a-f]{16})\/[^/]+\.png$/;

/** The static file behind a demo image's media URL (`/demo/media/<site>/<sha256>`), if it is one. */
export function demoMediaFile(pathname: string): string | undefined {
const prefix = MEDIA_PATH.exec(pathname)?.[1];
const media = prefix ? BY_PREFIX.get(prefix) : undefined;
return media ? `/demo/media/${DEMO_SITE_ID}/${media.sha256}` : undefined;
}

/** A demo image's media response from the static assets, or undefined for any other request. */
export async function serveDemoMedia(
request: Request,
assets: { fetch(request: Request): Promise<Response> },
): Promise<Response | undefined> {
if (request.method !== "GET" && request.method !== "HEAD") return undefined;
const url = new URL(request.url);
const file = demoMediaFile(url.pathname);
if (!file) return undefined;
const res = await assets.fetch(new Request(new URL(file, url), { method: request.method }));
if (!res.ok) return undefined;
return new Response(request.method === "HEAD" ? null : res.body, {
headers: {
"content-type": "image/png",
"content-disposition": "inline",
"content-security-policy": "default-src 'none'",
"x-content-type-options": "nosniff",
"cache-control": "public, max-age=3600",
},
});
}
6 changes: 6 additions & 0 deletions src/playground/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "@cloudhal/cloudflare/workerd-prelude";
import { handle } from "@astrojs/cloudflare/handler";
import { serveMedia, withRequestStore } from "@cloudhal/astro-renderer/runtime";
import { createPlaygroundWorker, type PlaygroundEnv } from "@cloudhal/cloudflare/playground";
import { serveDemoMedia } from "./demo-media-route.ts";
import { DEMO_MEDIA_KEYS, seedDemo } from "./demo-pack.ts";
import { LANDING_PATH, landingPage } from "./landing.ts";

Expand Down Expand Up @@ -56,6 +57,11 @@ export default {
return env.ASSETS.fetch(new Request(new URL("/index.html", url), { headers: req.headers }));
},
} as unknown as Fetcher;
// Demo images answer without a session: Studio's sandboxed preview sends no cookie.
if (enabled) {
const media = await serveDemoMedia(request, env.ASSETS);
if (media) return media;
}
return playground.worker.fetch(request, {
...env,
ASSETS: assets,
Expand Down
41 changes: 41 additions & 0 deletions test/demo-media-route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// `node --test`: demo images answer by their media URL without a session (demo-media-route.ts).
import assert from "node:assert/strict";
import { test } from "node:test";
import { DEMO_MEDIA } from "../src/playground/demo-media.ts";
import { demoMediaFile, serveDemoMedia } from "../src/playground/demo-media-route.ts";

const forest = DEMO_MEDIA.find((m) => m.slug === "forest-light");
const url = (path: string) => `https://try.cloudhal.com${path}`;
const mediaPath = `/media/${forest?.sha256.slice(0, 16)}/forest-light.png`;

test("maps a demo image's media URL to its static file, and nothing else", () => {
assert.equal(demoMediaFile(mediaPath), `/demo/media/site_playgrounddemo/${forest?.sha256}`);
assert.equal(demoMediaFile("/media/0000000000000000/other.png"), undefined);
assert.equal(demoMediaFile(`/media/${forest?.sha256.slice(0, 16)}/x.png.sha256`), undefined);
assert.equal(demoMediaFile("/s/01ABC/media/x.png"), undefined);
});

test("serves the file as an inert PNG to a request without a session cookie", async () => {
const asked: string[] = [];
const assets = {
async fetch(req: Request) {
asked.push(new URL(req.url).pathname);
return new Response(new Uint8Array([137, 80, 78, 71]));
},
};
const res = await serveDemoMedia(new Request(url(mediaPath)), assets);
assert.equal(res?.status, 200);
assert.equal(res?.headers.get("content-type"), "image/png");
assert.equal(res?.headers.get("x-content-type-options"), "nosniff");
assert.deepEqual(asked, [`/demo/media/site_playgrounddemo/${forest?.sha256}`]);
assert.equal(
await serveDemoMedia(new Request(url("/media/0000000000000000/a.png")), assets),
undefined,
);
assert.equal(
await serveDemoMedia(new Request(url(mediaPath), { method: "POST" }), assets),
undefined,
);
const missing = { fetch: async () => new Response(null, { status: 404 }) };
assert.equal(await serveDemoMedia(new Request(url(mediaPath)), missing), undefined);
});
Loading