canvas-website/worker/worker.ts

185 lines
5.5 KiB
TypeScript

import { handleUnfurlRequest } from "cloudflare-workers-unfurl"
import { AutoRouter, cors, error, IRequest } from "itty-router"
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
import { Environment } from "./types"
// make sure our sync durable object is made available to cloudflare
export { TldrawDurableObject } from "./TldrawDurableObject"
// Define security headers
const securityHeaders = {
"Content-Security-Policy":
"default-src 'self'; connect-src 'self' wss: https:; img-src 'self' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
}
// we use itty-router (https://itty.dev/) to handle routing. in this example we turn on CORS because
// we're hosting the worker separately to the client. you should restrict this to your own domain.
const { preflight, corsify } = cors({
origin: (origin) => {
const allowedOrigins = [
"https://jeffemmett.com",
"https://www.jeffemmett.com",
"https://jeffemmett-canvas.jeffemmett.workers.dev",
"https://jeffemmett.com/board/*",
]
// Always allow if no origin (like from a local file)
if (!origin) return "*"
// Check exact matches
if (allowedOrigins.includes(origin)) {
return origin
}
// For development - check if it's a localhost or local IP
if (
origin.match(
/^http:\/\/(localhost|127\.0\.0\.1|192\.168\.|169\.254\.|10\.)/,
)
) {
return origin
}
return undefined
},
allowMethods: ["GET", "POST", "OPTIONS", "UPGRADE"],
allowHeaders: [
"Content-Type",
"Authorization",
"Upgrade",
"Connection",
"Sec-WebSocket-Key",
"Sec-WebSocket-Version",
"Sec-WebSocket-Extensions",
"Sec-WebSocket-Protocol",
],
maxAge: 86400,
credentials: true,
})
const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
before: [preflight],
finally: [
(response) => {
// Add security headers to all responses except WebSocket upgrades
if (response.status !== 101) {
Object.entries(securityHeaders).forEach(([key, value]) => {
response.headers.set(key, value)
})
}
return corsify(response)
},
],
catch: (e: Error) => {
// Silently handle WebSocket errors, but log other errors
if (e.message?.includes("WebSocket")) {
console.debug("WebSocket error:", e)
return new Response(null, { status: 400 })
}
console.error(e)
return error(e)
},
})
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
.get("/connect/:roomId", (request, env) => {
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
return room.fetch(request.url, {
headers: request.headers,
body: request.body,
method: request.method,
})
})
// assets can be uploaded to the bucket under /uploads:
.post("/uploads/:uploadId", handleAssetUpload)
// they can be retrieved from the bucket too:
.get("/uploads/:uploadId", handleAssetDownload)
// bookmarks need to extract metadata from pasted URLs:
.get("/unfurl", handleUnfurlRequest)
.get("/room/:roomId", (request, env) => {
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
return room.fetch(request.url, {
headers: request.headers,
body: request.body,
method: request.method,
})
})
.post("/room/:roomId", async (request, env) => {
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
return room.fetch(request.url, {
method: "POST",
body: request.body,
})
})
.post("/daily/rooms", async (request, env) => {
try {
const { name, properties } = (await request.json()) as {
name: string
properties: Record<string, unknown>
}
// Create a room using Daily.co API
const dailyResponse = await fetch("https://api.daily.co/v1/rooms", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.DAILY_API_KEY}`,
},
body: JSON.stringify({
name,
properties,
}),
})
const dailyData = await dailyResponse.json()
if (!dailyResponse.ok) {
return new Response(
JSON.stringify({
message:
(dailyData as any).info || "Failed to create Daily.co room",
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
)
}
return new Response(
JSON.stringify({
url: `https://${env.DAILY_DOMAIN}/${(dailyData as any).name}`,
}),
{
headers: { "Content-Type": "application/json" },
},
)
} catch (error) {
return new Response(
JSON.stringify({
message: error instanceof Error ? error.message : "Unknown error",
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
)
}
})
// export our router for cloudflare
export default router