From b700846a9c6f69b8dd7e3be899c9a939bac1cfe2 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 12:52:20 -0500 Subject: [PATCH 01/16] fixing production env --- package copy.json | 69 ----------------------------------------------- package.json | 14 +++++----- 2 files changed, 7 insertions(+), 76 deletions(-) delete mode 100644 package copy.json diff --git a/package copy.json b/package copy.json deleted file mode 100644 index 0c4f574..0000000 --- a/package copy.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "jeffemmett", - "version": "1.0.0", - "description": "Jeff Emmett's personal website", - "type": "module", - "scripts": { - "dev": "concurrently --kill-others --names client,worker --prefix-colors blue,red \"yarn dev:client\" \"yarn dev:worker\"", - "dev:client": "vite --host --port 5173", - "dev:worker": "wrangler dev --local --port 5172 --ip 0.0.0.0", - "build": "tsc && vite build && wrangler deploy", - "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview", - "deploy": "yarn build && vercel deploy --prod" - }, - "keywords": [], - "author": "Jeff Emmett", - "license": "ISC", - "dependencies": { - "@dimforge/rapier2d": "^0.11.2", - "@tldraw/assets": "^2.0.0", - "@tldraw/tldraw": "^3.4.1", - "@tldraw/sync": "^2.4.6", - "@tldraw/sync-core": "^2.4.6", - "@tldraw/tlschema": "^2.4.6", - "@types/markdown-it": "^14.1.1", - "@vercel/analytics": "^1.2.2", - "@whereby.com/browser-sdk": "^3.9.2", - "cloudflare-workers-unfurl": "^0.0.7", - "crdts": "^0.2.0", - "gray-matter": "^4.0.3", - "itty-router": "^5.0.17", - "lodash.throttle": "^4.1.1", - "markdown-it": "^14.1.0", - "markdown-it-latex2img": "^0.0.6", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^4.1.2", - "react-router-dom": "^6.22.3", - "tldraw": "^2.4.6", - "use-local-storage-state": "^19.5.0", - "vercel": "^39.1.1" - }, - "devDependencies": { - "@biomejs/biome": "1.4.1", - "@cloudflare/types": "^6.29.1", - "@cloudflare/workers-types": "^4.20240821.1", - "@types/lodash.throttle": "^4", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^5.59.0", - "@typescript-eslint/parser": "^5.59.0", - "@vitejs/plugin-react": "^4.0.3", - "@vitejs/plugin-react-swc": "^3.6.0", - "concurrently": "^8.2.2", - "eslint": "^8.38.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.3.4", - "typescript": "^5.6.3", - "vite": "^5.3.3", - "vite-plugin-static-copy": "^1.0.6", - "vite-plugin-top-level-await": "^1.3.1", - "vite-plugin-wasm": "^3.2.2", - "wrangler": "^3.88.0" - }, - "resolutions": { - "react": "^18.2.0", - "@types/react": "^18.2.0" - } -} \ No newline at end of file diff --git a/package.json b/package.json index 4c50064..208b7b6 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "license": "ISC", "dependencies": { "@dimforge/rapier2d": "^0.11.2", - "@tldraw/assets": "^3.5.0", - "@tldraw/sync": "^3.4.1", - "@tldraw/sync-core": "^3.4.1", - "@tldraw/tldraw": "^3.4.1", - "@tldraw/tlschema": "^3.4.1", + "@tldraw/assets": "^3.6.0", + "@tldraw/sync": "^3.6.0", + "@tldraw/sync-core": "^3.6.0", + "@tldraw/tldraw": "^3.6.0", + "@tldraw/tlschema": "^3.6.0", "@types/markdown-it": "^14.1.1", "@vercel/analytics": "^1.2.2", "cloudflare-workers-unfurl": "^0.0.7", @@ -35,7 +35,7 @@ "react-dom": "^18.2.0", "react-error-boundary": "^4.1.2", "react-router-dom": "^6.22.3", - "tldraw": "^3.4.1", + "tldraw": "^3.6.0", "use-local-storage-state": "^19.5.0", "vercel": "^39.1.1" }, @@ -61,4 +61,4 @@ "vite-plugin-wasm": "^3.2.2", "wrangler": "^3.88.0" } -} +} \ No newline at end of file From 02124ce92011ccf84a5df7ba2a3bb31d8f18c802 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 12:58:25 -0500 Subject: [PATCH 02/16] fix CORS policy --- worker/TldrawDurableObject.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index 5e4a496..a101989 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -67,15 +67,32 @@ export class TldrawDurableObject { } return this.handleConnect(request) }) - .get('/room/:roomId', async () => { + .get('/room/:roomId', async (request) => { const room = await this.getRoom() const snapshot = room.getCurrentSnapshot() - return new Response(JSON.stringify(snapshot.documents)) + return new Response(JSON.stringify(snapshot.documents), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': request.headers.get('Origin') || '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Max-Age': '86400', + } + }) }) .post('/room/:roomId', async (request) => { const records = await request.json() as TLRecord[] const mergedRecords = await this.mergeCrdtState(records) - return new Response(JSON.stringify(Array.from(mergedRecords))) + + return new Response(JSON.stringify(Array.from(mergedRecords)), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': request.headers.get('Origin') || '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Max-Age': '86400', + } + }) }) // `fetch` is the entry point for all requests to the Durable Object From 3a2a38c0b6d7a5be7bc8a1122e54230050be976d Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 12:58:46 -0500 Subject: [PATCH 03/16] fix CORS policy --- worker/worker.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worker/worker.ts b/worker/worker.ts index 50c4da9..b99b8ca 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -35,7 +35,10 @@ const { preflight, corsify } = cors({ // 10.*.*.* with any port /^http:\/\/10\.\d+\.\d+\.\d+:\d+$/, // Production domain - /^https:\/\/jeffemmett\.com$/ + /^https:\/\/jeffemmett\.com$/, + /^https:\/\/www\.jeffemmett\.com$/, + // Worker domain + /^https:\/\/jeffemmett-canvas\.jeffemmett\.workers\.dev$/ ] // Check if origin matches any of our patterns From 0eb44072190f21cdad1ce65499a86486a7d026ab Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:15:38 -0500 Subject: [PATCH 04/16] fix worker deployment --- .gitignore | 1 + src/components/Board.tsx | 2 +- worker/worker.ts | 29 +++++++++++++++++++---------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 46f7caa..efa4db0 100644 --- a/.gitignore +++ b/.gitignore @@ -180,6 +180,7 @@ dist # Environment variables .env* +.env.development !.env.example .vercel diff --git a/src/components/Board.tsx b/src/components/Board.tsx index b584ae8..9d2f9ee 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -27,7 +27,7 @@ import { useCameraControls } from '@/hooks/useCameraControls' import { zoomToSelection } from '../ui-overrides' //const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev` -export const WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev'; +export const WORKER_URL = import.meta.env.VITE_TLDRAW_WORKER_URL || 'https://jeffemmett-canvas.jeffemmett.workers.dev'; const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools diff --git a/worker/worker.ts b/worker/worker.ts index b99b8ca..4acdcf2 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -21,7 +21,11 @@ const securityHeaders = { // we're hosting the worker separately to the client. you should restrict this to your own domain. const { preflight, corsify } = cors({ origin: (origin) => { - if (!origin) return undefined + const allowedOrigins = [ + 'https://jeffemmett.com', + 'https://www.jeffemmett.com', + 'https://jeffemmett-canvas.jeffemmett.workers.dev' + ]; const allowedPatterns = [ // Localhost with any port @@ -41,14 +45,20 @@ const { preflight, corsify } = cors({ /^https:\/\/jeffemmett-canvas\.jeffemmett\.workers\.dev$/ ] - // Check if origin matches any of our patterns - const isAllowed = allowedPatterns.some(pattern => - pattern instanceof RegExp - ? pattern.test(origin) - : pattern === origin - ) - return isAllowed ? origin : undefined + if (!origin) return undefined; + + // Check exact matches first + if (allowedOrigins.includes(origin)) { + return origin; + } + + // Then check patterns + if (allowedPatterns.some(pattern => pattern.test(origin))) { + return origin; + } + + return undefined; }, allowMethods: ['GET', 'POST', 'OPTIONS', 'UPGRADE'], allowHeaders: [ @@ -59,8 +69,7 @@ const { preflight, corsify } = cors({ 'Sec-WebSocket-Key', 'Sec-WebSocket-Version', 'Sec-WebSocket-Extensions', - 'Sec-WebSocket-Protocol', - ...Object.keys(securityHeaders) + 'Sec-WebSocket-Protocol' ], maxAge: 86400, }) From 6f6c924f6606e2f304a0e43af1f5f5d48f080076 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:27:37 -0500 Subject: [PATCH 05/16] fix VITE_ worker URL --- src/components/Board.tsx | 7 ++-- vite.config.ts | 72 +++++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 9d2f9ee..c4d22c7 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -26,8 +26,11 @@ import { components, uiOverrides } from '@/ui-overrides' import { useCameraControls } from '@/hooks/useCameraControls' import { zoomToSelection } from '../ui-overrides' -//const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev` -export const WORKER_URL = import.meta.env.VITE_TLDRAW_WORKER_URL || 'https://jeffemmett-canvas.jeffemmett.workers.dev'; +// Default to production URL if env var isn't available +const DEFAULT_WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev'; +export const WORKER_URL = typeof import.meta.env.VITE_TLDRAW_WORKER_URL === 'string' + ? import.meta.env.VITE_TLDRAW_WORKER_URL + : DEFAULT_WORKER_URL; const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools diff --git a/vite.config.ts b/vite.config.ts index 3927bd7..7aba30b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,38 +1,48 @@ import { markdownPlugin } from './build/markdownPlugin'; -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import wasm from "vite-plugin-wasm"; import topLevelAwait from "vite-plugin-top-level-await"; import { viteStaticCopy } from 'vite-plugin-static-copy'; -export default defineConfig({ - envPrefix: ['VITE_'], - plugins: [ - react(), - wasm(), - topLevelAwait(), - markdownPlugin, - viteStaticCopy({ - targets: [ - { - src: 'src/posts/', - dest: '.' - } - ] - }) - ], - server: { - host: '0.0.0.0', - port: 5173, - }, - build: { - sourcemap: true, - }, - base: '/', - publicDir: 'src/public', - resolve: { - alias: { - '@': '/src', +export default defineConfig(({ mode }) => { + // Load env file based on `mode` in the current working directory. + const env = loadEnv(mode, process.cwd(), ''); + + return { + envPrefix: ['VITE_'], + plugins: [ + react(), + wasm(), + topLevelAwait(), + markdownPlugin, + viteStaticCopy({ + targets: [ + { + src: 'src/posts/', + dest: '.' + } + ] + }) + ], + server: { + host: '0.0.0.0', + port: 5173, }, - }, -}) + build: { + sourcemap: true, + }, + base: '/', + publicDir: 'src/public', + resolve: { + alias: { + '@': '/src', + }, + }, + define: { + 'import.meta.env.VITE_TLDRAW_WORKER_URL': JSON.stringify( + env.VITE_TLDRAW_WORKER_URL || 'https://jeffemmett-canvas.jeffemmett.workers.dev' + ) + } + }; +}); From c4198e1faff4b98c83fa52aa42282f0af128d63d Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:31:37 -0500 Subject: [PATCH 06/16] add vite env types --- src/vite-env.d.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..f90643f 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,9 @@ /// + +interface ImportMetaEnv { + readonly VITE_TLDRAW_WORKER_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} From 97b00c1569cf765c20e6b322efeb3f6ec5e7c246 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:43:56 -0500 Subject: [PATCH 07/16] fix prod env --- src/components/Board.tsx | 5 +-- vite.config.ts | 68 +++++++++++++++++----------------------- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/src/components/Board.tsx b/src/components/Board.tsx index c4d22c7..669f970 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -27,10 +27,7 @@ import { useCameraControls } from '@/hooks/useCameraControls' import { zoomToSelection } from '../ui-overrides' // Default to production URL if env var isn't available -const DEFAULT_WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev'; -export const WORKER_URL = typeof import.meta.env.VITE_TLDRAW_WORKER_URL === 'string' - ? import.meta.env.VITE_TLDRAW_WORKER_URL - : DEFAULT_WORKER_URL; +export const WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev'; const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools diff --git a/vite.config.ts b/vite.config.ts index 7aba30b..81c5b98 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,44 +5,34 @@ import wasm from "vite-plugin-wasm"; import topLevelAwait from "vite-plugin-top-level-await"; import { viteStaticCopy } from 'vite-plugin-static-copy'; -export default defineConfig(({ mode }) => { - // Load env file based on `mode` in the current working directory. - const env = loadEnv(mode, process.cwd(), ''); - - return { - envPrefix: ['VITE_'], - plugins: [ - react(), - wasm(), - topLevelAwait(), - markdownPlugin, - viteStaticCopy({ - targets: [ - { - src: 'src/posts/', - dest: '.' - } - ] - }) - ], - server: { - host: '0.0.0.0', - port: 5173, +export default defineConfig({ + envPrefix: ['VITE_'], + plugins: [ + react(), + wasm(), + topLevelAwait(), + markdownPlugin, + viteStaticCopy({ + targets: [ + { + src: 'src/posts/', + dest: '.' + } + ] + }) + ], + server: { + host: '0.0.0.0', + port: 5173, + }, + build: { + sourcemap: true, + }, + base: '/', + publicDir: 'src/public', + resolve: { + alias: { + '@': '/src', }, - build: { - sourcemap: true, - }, - base: '/', - publicDir: 'src/public', - resolve: { - alias: { - '@': '/src', - }, - }, - define: { - 'import.meta.env.VITE_TLDRAW_WORKER_URL': JSON.stringify( - env.VITE_TLDRAW_WORKER_URL || 'https://jeffemmett-canvas.jeffemmett.workers.dev' - ) - } - }; + } }); From 71fc07133a2270f4a71610ebb51abe1c20b3dd40 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:33:31 -0500 Subject: [PATCH 08/16] fix CORS for prod env --- worker/worker.ts | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/worker/worker.ts b/worker/worker.ts index 4acdcf2..473d4ae 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -27,24 +27,14 @@ const { preflight, corsify } = cors({ 'https://jeffemmett-canvas.jeffemmett.workers.dev' ]; - const allowedPatterns = [ - // Localhost with any port + // Development patterns + const devPatterns = [ /^http:\/\/localhost:\d+$/, - // 127.0.0.1 with any port /^http:\/\/127\.0\.0\.1:\d+$/, - // 192.168.*.* with any port /^http:\/\/192\.168\.\d+\.\d+:\d+$/, - // 169.254.*.* with any port /^http:\/\/169\.254\.\d+\.\d+:\d+$/, - // 10.*.*.* with any port - /^http:\/\/10\.\d+\.\d+\.\d+:\d+$/, - // Production domain - /^https:\/\/jeffemmett\.com$/, - /^https:\/\/www\.jeffemmett\.com$/, - // Worker domain - /^https:\/\/jeffemmett-canvas\.jeffemmett\.workers\.dev$/ - ] - + /^http:\/\/10\.\d+\.\d+\.\d+:\d+$/ + ]; if (!origin) return undefined; @@ -53,8 +43,8 @@ const { preflight, corsify } = cors({ return origin; } - // Then check patterns - if (allowedPatterns.some(pattern => pattern.test(origin))) { + // Then check development patterns + if (process.env.NODE_ENV === 'development' && devPatterns.some(pattern => pattern.test(origin))) { return origin; } @@ -72,6 +62,7 @@ const { preflight, corsify } = cors({ 'Sec-WebSocket-Protocol' ], maxAge: 86400, + credentials: true }) const router = AutoRouter({ before: [preflight], From 632e7979a226927016db412ad4908609bfb29a14 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:39:57 -0500 Subject: [PATCH 09/16] fix CORS --- worker/TldrawDurableObject.ts | 16 +++++++++++++++- worker/worker.ts | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index a101989..e958bb6 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -97,7 +97,21 @@ export class TldrawDurableObject { // `fetch` is the entry point for all requests to the Durable Object fetch(request: Request): Response | Promise { - return this.router.fetch(request) + try { + return this.router.fetch(request) + } catch (err) { + console.error('Error in DO fetch:', err); + return new Response(JSON.stringify({ + error: 'Internal Server Error', + message: (err as Error).message + }), { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + } + }); + } } // what happens when someone tries to connect to this room? diff --git a/worker/worker.ts b/worker/worker.ts index 473d4ae..c81ad19 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -133,3 +133,39 @@ const router = AutoRouter({ // export our router for cloudflare export default router + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400', +}; + +// Add CORS headers to all responses +function handleCors(request: Request) { + // Handle preflight requests + if (request.method === 'OPTIONS') { + return new Response(null, { + headers: corsHeaders + }); + } + + return null; +} + +// Modify the fetch handler +async function handleRequest(request: Request) { + // Handle CORS preflight + const corsResult = handleCors(request); + if (corsResult) return corsResult; + + // Handle the actual request + const response = await router.handle(request); + + // Add CORS headers to the response + Object.entries(corsHeaders).forEach(([key, value]) => { + response.headers.set(key, value); + }); + + return response; +} From 3006e85375fb2a60c2be1796476eeebfb6b75efa Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:57:05 -0500 Subject: [PATCH 10/16] fix prod env --- worker/TldrawDurableObject.ts | 6 +++++- worker/worker.ts | 18 +++++------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index e958bb6..a362bca 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -108,7 +108,11 @@ export class TldrawDurableObject { status: 500, headers: { 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, UPGRADE', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Upgrade, Connection', + 'Access-Control-Max-Age': '86400', + 'Access-Control-Allow-Credentials': 'true' } }); } diff --git a/worker/worker.ts b/worker/worker.ts index c81ad19..c319f2d 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -27,24 +27,16 @@ const { preflight, corsify } = cors({ 'https://jeffemmett-canvas.jeffemmett.workers.dev' ]; - // Development patterns - const devPatterns = [ - /^http:\/\/localhost:\d+$/, - /^http:\/\/127\.0\.0\.1:\d+$/, - /^http:\/\/192\.168\.\d+\.\d+:\d+$/, - /^http:\/\/169\.254\.\d+\.\d+:\d+$/, - /^http:\/\/10\.\d+\.\d+\.\d+:\d+$/ - ]; + // Always allow if no origin (like from a local file) + if (!origin) return '*'; - if (!origin) return undefined; - - // Check exact matches first + // Check exact matches if (allowedOrigins.includes(origin)) { return origin; } - // Then check development patterns - if (process.env.NODE_ENV === 'development' && devPatterns.some(pattern => pattern.test(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; } From 08175d3a7c18bb10bc3baa523a7b255b9564f4c2 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:03:53 -0500 Subject: [PATCH 11/16] fix CORS --- worker/TldrawDurableObject.ts | 11 ++++++++++- worker/worker.ts | 36 ----------------------------------- 2 files changed, 10 insertions(+), 37 deletions(-) diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index a362bca..79640ef 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -136,7 +136,16 @@ export class TldrawDurableObject { room.handleSocketConnect({ sessionId, socket: serverWebSocket }) // return the websocket connection to the client - return new Response(null, { status: 101, webSocket: clientWebSocket }) + return new Response(null, { + status: 101, + webSocket: clientWebSocket, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, UPGRADE', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Credentials': 'true' + } + }); } getRoom() { diff --git a/worker/worker.ts b/worker/worker.ts index c319f2d..00456a0 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -125,39 +125,3 @@ const router = AutoRouter({ // export our router for cloudflare export default router - -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Max-Age': '86400', -}; - -// Add CORS headers to all responses -function handleCors(request: Request) { - // Handle preflight requests - if (request.method === 'OPTIONS') { - return new Response(null, { - headers: corsHeaders - }); - } - - return null; -} - -// Modify the fetch handler -async function handleRequest(request: Request) { - // Handle CORS preflight - const corsResult = handleCors(request); - if (corsResult) return corsResult; - - // Handle the actual request - const response = await router.handle(request); - - // Add CORS headers to the response - Object.entries(corsHeaders).forEach(([key, value]) => { - response.headers.set(key, value); - }); - - return response; -} From 39e6cccc3f92bf43e43bdcec5fe94317860fdd44 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:10:25 -0500 Subject: [PATCH 12/16] fix CORS --- worker/TldrawDurableObject.ts | 17 +++++++---------- worker/worker.ts | 6 +++++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index 79640ef..7bf5ce0 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -120,32 +120,29 @@ export class TldrawDurableObject { // what happens when someone tries to connect to this room? async handleConnect(request: IRequest): Promise { - // extract query params from request const sessionId = request.query.sessionId as string if (!sessionId) return error(400, 'Missing sessionId') - // Create the websocket pair for the client const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair() - // @ts-ignore serverWebSocket.accept() - // load the room, or retrieve it if it's already loaded const room = await this.getRoom() - - // connect the client to the room room.handleSocketConnect({ sessionId, socket: serverWebSocket }) - // return the websocket connection to the client + const origin = request.headers.get('Origin') || '*' + return new Response(null, { status: 101, webSocket: clientWebSocket, headers: { - 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, UPGRADE', 'Access-Control-Allow-Headers': '*', - 'Access-Control-Allow-Credentials': 'true' + 'Access-Control-Allow-Credentials': 'true', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade' } - }); + }) } getRoom() { diff --git a/worker/worker.ts b/worker/worker.ts index 00456a0..b51aee2 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -76,7 +76,11 @@ const router = AutoRouter({ .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 }) + return room.fetch(request.url, { + headers: request.headers, + body: request.body, + method: request.method + }) }) // assets can be uploaded to the bucket under /uploads: From 111be039074aed93b5062038f4ba2427c728866e Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:23:56 -0500 Subject: [PATCH 13/16] swap persistentboard with Tldraw native sync --- src/components/Board copy.tsx | 123 ------------------------------- src/components/Board.tsx | 16 ++-- src/hooks/useGSetState.ts | 40 ---------- src/hooks/useLocalStorageRoom.ts | 20 ----- src/hooks/usePersistentBoard.ts | 92 ----------------------- worker/TldrawDurableObject.ts | 19 +++-- 6 files changed, 24 insertions(+), 286 deletions(-) delete mode 100644 src/components/Board copy.tsx delete mode 100644 src/hooks/useGSetState.ts delete mode 100644 src/hooks/useLocalStorageRoom.ts delete mode 100644 src/hooks/usePersistentBoard.ts diff --git a/src/components/Board copy.tsx b/src/components/Board copy.tsx deleted file mode 100644 index b0c851d..0000000 --- a/src/components/Board copy.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useSync } from '@tldraw/sync' -import { - AssetRecordType, - getHashForString, - TLBookmarkAsset, - Tldraw, - // useLocalStorageState, -} from 'tldraw' -import { useParams } from 'react-router-dom' -import useLocalStorageState from 'use-local-storage-state' -import { ChatBoxTool } from '@/tools/ChatBoxTool' -import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil' -import { VideoChatTool } from '@/tools/VideoChatTool' -import { VideoChatShape } from '@/shapes/VideoChatShapeUtil' -import { multiplayerAssetStore } from '../client/multiplayerAssetStore' -import { customSchema } from '../../worker/TldrawDurableObject' -import { EmbedShape } from '@/shapes/EmbedShapeUtil' -import { EmbedTool } from '@/tools/EmbedTool' - -import React, { useEffect, useState } from 'react'; -import { ChatBox } from '@/shapes/ChatBoxShapeUtil'; -import { components, uiOverrides } from '@/ui-overrides' - -const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev` - -const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] -const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools - -export function Board() { - const { slug } = useParams<{ slug: string }>(); // Ensure this is inside the Board component - const roomId = slug || 'default-room'; // Declare roomId here - - const store = useSync({ - uri: `${WORKER_URL}/connect/${roomId}`, - assets: multiplayerAssetStore, - shapeUtils: shapeUtils, - schema: customSchema, - }); - - const [isChatBoxVisible, setChatBoxVisible] = useState(false); - const [userName, setUserName] = useState(''); - const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility - - const handleNameChange = (event: React.ChangeEvent) => { - setUserName(event.target.value); - }; - - const [persistedStore, setPersistedStore] = useLocalStorageState('board-store', { defaultValue: store } - ) - - useEffect(() => { - setPersistedStore(store); - }, [store]); - - return ( -
- { - editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) - editor.setCurrentTool('hand') - }} - /> - {isChatBoxVisible && ( -
- - -
- )} - {isVideoChatVisible && ( // Render the button to join video chat - - )} -
- ) -} - -// How does our server handle bookmark unfurling? -async function unfurlBookmarkUrl({ url }: { url: string }): Promise { - const asset: TLBookmarkAsset = { - id: AssetRecordType.createId(getHashForString(url)), - typeName: 'asset', - type: 'bookmark', - meta: {}, - props: { - src: url, - description: '', - image: '', - favicon: '', - title: '', - }, - } - - try { - const response = await fetch(`${WORKER_URL}/unfurl?url=${encodeURIComponent(url)}`) - const data = await response.json() as { description: string, image: string, favicon: string, title: string } - - asset.props.description = data?.description ?? '' - asset.props.image = data?.image ?? '' - asset.props.favicon = data?.favicon ?? '' - asset.props.title = data?.title ?? '' - } catch (e) { - console.error(e) - } - - return asset -} diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 669f970..dc2ee77 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -19,6 +19,7 @@ import { multiplayerAssetStore } from '../client/multiplayerAssetStore' import { customSchema } from '../../worker/TldrawDurableObject' import { EmbedShape } from '@/shapes/EmbedShapeUtil' import { EmbedTool } from '@/tools/EmbedTool' +import { defaultShapeUtils, defaultBindingUtils } from 'tldraw' import React, { useState, useEffect, useCallback } from 'react'; import { ChatBox } from '@/shapes/ChatBoxShapeUtil'; @@ -32,16 +33,19 @@ export const WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev'; const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools -// Add these imports -import { useGSetState } from '@/hooks/useGSetState'; -import { useLocalStorageRoom } from '@/hooks/useLocalStorageRoom'; -import { usePersistentBoard } from '@/hooks/usePersistentBoard'; - export function Board() { const { slug } = useParams<{ slug: string }>(); const roomId = slug || 'default-room'; - const store = usePersistentBoard(roomId); + + const store = useSync({ + uri: `${WORKER_URL}/connect/${roomId}`, + assets: multiplayerAssetStore, + shapeUtils: [...shapeUtils, ...defaultShapeUtils], + // Add default bindings if you're using them + bindingUtils: [...defaultBindingUtils], + }) + const [editor, setEditor] = useState(null) const { zoomToFrame, copyFrameLink, copyLocationLink, revertCamera } = useCameraControls(editor) diff --git a/src/hooks/useGSetState.ts b/src/hooks/useGSetState.ts deleted file mode 100644 index e825fa1..0000000 --- a/src/hooks/useGSetState.ts +++ /dev/null @@ -1,40 +0,0 @@ -import useLocalStorageState from 'use-local-storage-state'; -import GSet from 'crdts/src/G-Set'; -import { TLRecord } from 'tldraw'; -import { useRef, useCallback } from 'react'; - -export function useGSetState(roomId: string) { - const [localSet, setLocalSet] = useLocalStorageState(`gset-${roomId}`, { - defaultValue: [] - }); - - // Keep GSet instance in a ref to persist between renders - const gsetRef = useRef>(); - if (!gsetRef.current) { - gsetRef.current = new GSet(); - // Initialize G-Set with local data - if (localSet && Array.isArray(localSet)) { - localSet.forEach(record => gsetRef.current?.add(record)); - } - } - - const addRecord = useCallback((record: TLRecord) => { - if (!gsetRef.current) return; - gsetRef.current.add(record); - setLocalSet(Array.from(gsetRef.current.values())); - }, [setLocalSet]); - - const merge = useCallback((remoteSet: Set) => { - if (!gsetRef.current) return new Set(); - remoteSet.forEach(record => gsetRef.current?.add(record)); - setLocalSet(Array.from(gsetRef.current.values())); - return gsetRef.current.values(); - }, [setLocalSet]); - - return { - values: gsetRef.current.values(), - add: addRecord, - merge, - localSet - }; -} \ No newline at end of file diff --git a/src/hooks/useLocalStorageRoom.ts b/src/hooks/useLocalStorageRoom.ts deleted file mode 100644 index 946fef5..0000000 --- a/src/hooks/useLocalStorageRoom.ts +++ /dev/null @@ -1,20 +0,0 @@ -import useLocalStorageState from 'use-local-storage-state'; -import { TLRecord, createTLStore, SerializedStore } from 'tldraw'; -import { customSchema } from '../../worker/TldrawDurableObject'; - -export function useLocalStorageRoom(roomId: string) { - const [records, setRecords] = useLocalStorageState>(`tldraw-room-${roomId}`, { - defaultValue: createTLStore({ schema: customSchema }).serialize() - }); - - const store = createTLStore({ - schema: customSchema, - initialData: records, - }); - - return { - store, - records, - setRecords - }; -} \ No newline at end of file diff --git a/src/hooks/usePersistentBoard.ts b/src/hooks/usePersistentBoard.ts deleted file mode 100644 index 128bf51..0000000 --- a/src/hooks/usePersistentBoard.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useSync } from '@tldraw/sync' -import { useState, useEffect, useCallback, useRef } from 'react' -import { customSchema } from '../../worker/TldrawDurableObject' -import { multiplayerAssetStore } from '../client/multiplayerAssetStore' -import { useGSetState } from './useGSetState' -import { useLocalStorageRoom } from './useLocalStorageRoom' -import { TLRecord } from 'tldraw' -import { WORKER_URL } from '../components/Board' - -export function usePersistentBoard(roomId: string) { - const [isOnline, setIsOnline] = useState(navigator.onLine) - const { store: localStore, records, setRecords } = useLocalStorageRoom(roomId) - const { values, add, merge } = useGSetState(roomId) - const initialSyncRef = useRef(false) - const mergeInProgressRef = useRef(false) - - const syncedStore = useSync({ - uri: `${WORKER_URL.replace('https://', 'wss://')}/connect/${roomId}`, - schema: customSchema, - assets: multiplayerAssetStore, - }) - - useEffect(() => { - const handleOnline = () => setIsOnline(true) - const handleOffline = () => setIsOnline(false) - - window.addEventListener('online', handleOnline) - window.addEventListener('offline', handleOffline) - - return () => { - window.removeEventListener('online', handleOnline) - window.removeEventListener('offline', handleOffline) - } - }, []) - - const mergeRecords = useCallback((records: Set) => { - if (mergeInProgressRef.current || records.size === 0) return - - try { - mergeInProgressRef.current = true - merge(records) - if (!isOnline && localStore) { - setRecords(localStore.serialize()) - } - } finally { - mergeInProgressRef.current = false - } - }, [isOnline, localStore, merge, setRecords]) - - useEffect(() => { - if (!syncedStore?.store || !localStore) return - - if (isOnline && !initialSyncRef.current) { - initialSyncRef.current = true - const serverRecords = Object.values(syncedStore.store.allRecords()) - if (serverRecords.length > 0) { - mergeRecords(new Set(serverRecords)) - } - - const unsubscribe = syncedStore.store.listen((event) => { - if ('changes' in event) { - const changedRecords = Object.values(event.changes) - if (changedRecords.length > 0) { - mergeRecords(new Set(changedRecords)) - } - } - }) - - return () => unsubscribe() - } else if (!isOnline) { - const currentRecords = Object.values(localStore.allRecords()) - if (currentRecords.length > 0) { - mergeRecords(new Set(currentRecords)) - } - } - }, [isOnline, syncedStore?.store, localStore, mergeRecords]) - - const addRecord = useCallback((record: TLRecord) => { - if (!record) return - add(record) - if (!isOnline && localStore) { - setRecords(localStore.serialize()) - } - }, [add, isOnline, localStore, setRecords]) - - return { - store: isOnline ? syncedStore?.store : localStore, - isOnline, - addRecord, - mergeRecords - } -} \ No newline at end of file diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index 7bf5ce0..2f97034 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -5,7 +5,7 @@ import { TLRecord, TLShape, createTLSchema, - // defaultBindingSchemas, + defaultBindingSchemas, defaultShapeSchemas, } from '@tldraw/tlschema' import { AutoRouter, IRequest, error } from 'itty-router' @@ -20,11 +20,20 @@ import GSet from 'crdts/src/G-Set' export const customSchema = createTLSchema({ shapes: { ...defaultShapeSchemas, - ChatBox: ChatBoxShape, - VideoChat: VideoChatShape, - Embed: EmbedShape + ChatBox: { + props: ChatBoxShape.props, + migrations: ChatBoxShape.migrations, + }, + VideoChat: { + props: VideoChatShape.props, + migrations: VideoChatShape.migrations, + }, + Embed: { + props: EmbedShape.props, + migrations: EmbedShape.migrations, + }, }, - // bindings: { ...defaultBindingSchemas }, + bindings: defaultBindingSchemas, }) // each whiteboard room is hosted in a DurableObject: From 110fc19b94d896368f5458dc0ba2cb5ef739b1d7 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:35:53 -0500 Subject: [PATCH 14/16] one more attempt --- src/components/Board.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Board.tsx b/src/components/Board.tsx index dc2ee77..3dea2ef 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -1,4 +1,5 @@ import { useSync } from '@tldraw/sync' +import { useMemo } from 'react' import { AssetRecordType, getHashForString, @@ -10,7 +11,6 @@ import { TLUiEventSource, } from 'tldraw' import { useParams } from 'react-router-dom' -import useLocalStorageState from 'use-local-storage-state' import { ChatBoxTool } from '@/tools/ChatBoxTool' import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil' import { VideoChatTool } from '@/tools/VideoChatTool' @@ -38,14 +38,14 @@ export function Board() { const { slug } = useParams<{ slug: string }>(); const roomId = slug || 'default-room'; - const store = useSync({ + const storeConfig = useMemo(() => ({ uri: `${WORKER_URL}/connect/${roomId}`, assets: multiplayerAssetStore, shapeUtils: [...shapeUtils, ...defaultShapeUtils], - // Add default bindings if you're using them bindingUtils: [...defaultBindingUtils], - }) + }), [roomId]); + const store = useSync(storeConfig); const [editor, setEditor] = useState(null) const { zoomToFrame, copyFrameLink, copyLocationLink, revertCamera } = useCameraControls(editor) From 2e0a05ab32fcbacd23d654181b6a3c5e08ee93ac Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:50:37 -0500 Subject: [PATCH 15/16] fix vite config --- vite.config.ts | 3 ++ worker/TldrawDurableObject.ts | 61 ++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 81c5b98..48b47b5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -34,5 +34,8 @@ export default defineConfig({ alias: { '@': '/src', }, + }, + define: { + 'import.meta.env.VITE_WORKER_URL': JSON.stringify(process.env.VITE_WORKER_URL) } }); diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index 2f97034..66c725a 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -129,29 +129,52 @@ export class TldrawDurableObject { // what happens when someone tries to connect to this room? async handleConnect(request: IRequest): Promise { - const sessionId = request.query.sessionId as string - if (!sessionId) return error(400, 'Missing sessionId') + if (!this.roomId) { + return new Response('Room not initialized', { status: 400 }); + } - const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair() - serverWebSocket.accept() + const sessionId = request.query.sessionId as string; + if (!sessionId) { + return new Response('Missing sessionId', { status: 400 }); + } - const room = await this.getRoom() - room.handleSocketConnect({ sessionId, socket: serverWebSocket }) + const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair(); - const origin = request.headers.get('Origin') || '*' + try { + serverWebSocket.accept(); + const room = await this.getRoom(); - return new Response(null, { - status: 101, - webSocket: clientWebSocket, - headers: { - 'Access-Control-Allow-Origin': origin, - 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, UPGRADE', - 'Access-Control-Allow-Headers': '*', - 'Access-Control-Allow-Credentials': 'true', - 'Upgrade': 'websocket', - 'Connection': 'Upgrade' - } - }) + // Handle socket connection with proper error boundaries + room.handleSocketConnect({ + sessionId, + socket: { + send: serverWebSocket.send.bind(serverWebSocket), + close: serverWebSocket.close.bind(serverWebSocket), + addEventListener: serverWebSocket.addEventListener.bind(serverWebSocket), + removeEventListener: serverWebSocket.removeEventListener.bind(serverWebSocket), + readyState: serverWebSocket.readyState, + } + }); + + return new Response(null, { + status: 101, + webSocket: clientWebSocket, + headers: { + 'Access-Control-Allow-Origin': request.headers.get('Origin') || '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, UPGRADE', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Credentials': 'true', + 'Upgrade': 'websocket', + 'Connection': 'Upgrade' + } + }); + } catch (error) { + console.error('WebSocket connection error:', error); + serverWebSocket.close(1011, 'Failed to initialize connection'); + return new Response('Failed to establish WebSocket connection', { + status: 500 + }); + } } getRoom() { From e286a120f172bc0071cc835c954bdf61747118a3 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 16:02:10 -0500 Subject: [PATCH 16/16] maybe this works --- worker/worker.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/worker/worker.ts b/worker/worker.ts index b51aee2..6c0d900 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -24,7 +24,8 @@ const { preflight, corsify } = cors({ const allowedOrigins = [ 'https://jeffemmett.com', 'https://www.jeffemmett.com', - 'https://jeffemmett-canvas.jeffemmett.workers.dev' + 'https://jeffemmett-canvas.jeffemmett.workers.dev', + 'https://jeffemmett.com/board/*', ]; // Always allow if no origin (like from a local file) @@ -92,11 +93,14 @@ const router = AutoRouter({ // bookmarks need to extract metadata from pasted URLs: .get('/unfurl', handleUnfurlRequest) - .get('/room/:roomId', async (request, env) => { + .get('/room/:roomId', (request, env) => { const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId) const room = env.TLDRAW_DURABLE_OBJECT.get(id) - const response = await room.fetch(request.url) - return response + return room.fetch(request.url, { + headers: request.headers, + body: request.body, + method: request.method + }) }) .post('/room/:roomId', async (request, env) => {