183 lines
5.6 KiB
TypeScript
183 lines
5.6 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\.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) => {
|
|
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 {
|
|
// Log the request for debugging
|
|
console.log(
|
|
"Creating Daily room with API key:",
|
|
env.DAILY_API_KEY ? "present" : "missing",
|
|
)
|
|
|
|
const response = await fetch("https://api.daily.co/v1/rooms", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${env.DAILY_API_KEY}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: await request.text(),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text()
|
|
console.error("Daily API error:", errorText)
|
|
return new Response(`Daily API error: ${errorText}`, {
|
|
status: response.status,
|
|
})
|
|
}
|
|
|
|
const data = await response.json()
|
|
|
|
// Log successful response
|
|
console.log("Daily room created:", data)
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
...(data as Record<string, unknown>),
|
|
url: `https://${env.DAILY_DOMAIN}/${
|
|
(data as Record<string, unknown>).name
|
|
}`,
|
|
}),
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
// Add CORS headers specifically for this endpoint if needed
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
"Access-Control-Allow-Headers": "Content-Type",
|
|
},
|
|
},
|
|
)
|
|
} catch (error) {
|
|
console.error("Error creating Daily room:", error)
|
|
return new Response(
|
|
JSON.stringify({ error: "Failed to create Daily room" }),
|
|
{
|
|
status: 500,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
)
|
|
}
|
|
})
|
|
|
|
// export our router for cloudflare
|
|
export default router
|