1/**
2 * Media & messaging worker — Browser Rendering, Images, Stream, Secrets
3 * Store, and outbound Email, one route per feature. Under `alchemy dev`
4 * every binding is served by a local simulator (Chrome for BROWSER, sharp
5 * for IMAGES, an on-disk store for STREAM and email) except `IMAGES_REMOTE`,
6 * which opted into `dev: { remote: true }` and proxies to the real Images
7 * service. The inbound `email()` handler lives on {@link InboxWorker} — see
8 * the note there.
9 */
10import puppeteer from "@cloudflare/puppeteer";
11import type * as cf from "@cloudflare/workers-types";
12import { EmailMessage } from "cloudflare:email";
13
14const PAGE_HTML =
15 "<html><head><title>Cloudflare Dev</title></head>" +
16 "<body><h1>Hello from the local browser</h1></body></html>";
17
18interface SendEmailLike {
19 send(message: unknown): Promise<{ messageId: string }>;
20}
21
22interface Env {
23 BROWSER: unknown;
24 IMAGES: cf.ImagesBinding;
25 IMAGES_REMOTE: cf.ImagesBinding;
26 STREAM: cf.StreamBinding;
27 EMAIL: SendEmailLike;
28 API_KEY: { get(): Promise<string> };
29}
30
31const imagesInfo = async (images: cf.ImagesBinding, request: Request) => {
32 const info = await images.info(
33 request.body as unknown as cf.ReadableStream<Uint8Array>,
34 );
35 return Response.json(info);
36};
37
38export default {
39 async fetch(request: Request, env: Env): Promise<Response> {
40 const url = new URL(request.url);
41 try {
42 switch (url.pathname) {
43 case "/secret": {
44 // Secrets Store secret via the `secrets_store_secret` binding —
45 // the exact seeded value round-trips.
46 return Response.json({ value: await env.API_KEY.get() });
47 }
48 case "/browser/title": {
49 // Browser Rendering the way real users drive it — through
50 // @cloudflare/puppeteer. Locally this launches a real Chrome.
51 const browser = await puppeteer.launch(env.BROWSER as any);
52 try {
53 const page = await browser.newPage();
54 await page.setContent(PAGE_HTML);
55 const title = await page.title();
56 const heading = await page.$eval("h1", (el) => el.textContent);
57 return Response.json({ title, heading });
58 } finally {
59 await browser.close();
60 }
61 }
62 case "/images/info":
63 return imagesInfo(env.IMAGES, request);
64 case "/images/info-remote":
65 // Same call, but through the `Alchemy.remote()`-piped binding —
66 // served by the REAL Images service even during `alchemy dev`.
67 return imagesInfo(env.IMAGES_REMOTE, request);
68 case "/images/transform": {
69 const width = Number(url.searchParams.get("width") ?? "4");
70 const output = await env.IMAGES.input(
71 request.body as unknown as cf.ReadableStream<Uint8Array>,
72 )
73 .transform({ width })
74 .output({ format: "image/png" });
75 return output.response() as unknown as Response;
76 }
77 case "/stream/upload": {
78 // The local simulator (like Miniflare's stream plugin) accepts a
79 // ReadableStream of the video bytes in place of a URL; the
80 // published types only declare the URL overload.
81 const upload = env.STREAM.upload as unknown as (
82 input: ReadableStream | string,
83 params?: cf.StreamUrlUploadParams,
84 ) => Promise<cf.StreamVideo>;
85 const video = await upload(
86 request.body as unknown as ReadableStream,
87 {
88 meta: { title: "cloudflare-dev-example" },
89 creator: "alchemy",
90 },
91 );
92 return Response.json(video);
93 }
94 case "/stream/details": {
95 const video = await env.STREAM.video(
96 url.searchParams.get("id")!,
97 ).details();
98 return Response.json(video);
99 }
100 case "/stream/delete": {
101 await env.STREAM.video(url.searchParams.get("id")!).delete();
102 return Response.json({ deleted: true });
103 }
104 case "/email/send": {
105 // send_email binding: locally the message is persisted as an
106 // `.eml` under `.alchemy/local/email` instead of being delivered.
107 const marker = url.searchParams.get("marker") ?? "no-marker";
108 const raw = [
109 "From: sender <sender@example.com>",
110 "To: recipient <allowed@example.com>",
111 `Message-ID: <${marker}@example.com>`,
112 "MIME-Version: 1.0",
113 "Content-Type: text/plain",
114 "",
115 `marker:${marker}`,
116 ].join("\r\n");
117 const result = await env.EMAIL.send(
118 new EmailMessage("sender@example.com", "allowed@example.com", raw),
119 );
120 return Response.json({ ok: true, messageId: result.messageId });
121 }
122 default:
123 return new Response("ok");
124 }
125 } catch (e) {
126 return Response.json(
127 { ok: false, message: e instanceof Error ? e.message : String(e) },
128 { status: 500 },
129 );
130 }
131 },
132};