From fa5109b06d8e01fda393a5c17f8a44ff78d43974 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 01:27:26 +0000 Subject: [PATCH 1/3] feat(web): add proxy for supabase storage images - Add /api/images/$ server route to proxy requests to supabase storage - Replace all direct supabase storage URLs with proxy URLs in apps/web - Keeps og.tsx edge function unchanged as it runs on Netlify edge Co-Authored-By: yujonglee --- apps/web/src/components/footer.tsx | 2 +- apps/web/src/components/header.tsx | 4 +- apps/web/src/components/video-thumbnail.tsx | 2 +- apps/web/src/routes/__root.tsx | 6 +- apps/web/src/routes/_view/about.tsx | 40 ++++++------- apps/web/src/routes/_view/brand.tsx | 8 +-- apps/web/src/routes/_view/changelog/$slug.tsx | 4 +- apps/web/src/routes/_view/changelog/index.tsx | 4 +- apps/web/src/routes/_view/download/index.tsx | 2 +- apps/web/src/routes/_view/index.tsx | 53 +++++++---------- apps/web/src/routes/_view/press-kit.tsx | 12 ++-- apps/web/src/routes/_view/pricing.tsx | 2 +- .../src/routes/_view/product/ai-assistant.tsx | 4 +- .../routes/_view/product/ai-notetaking.tsx | 59 +++++++------------ apps/web/src/routes/_view/product/bot.tsx | 2 +- .../src/routes/_view/product/mini-apps.tsx | 8 +-- .../src/routes/_view/product/workflows.tsx | 6 +- apps/web/src/routes/_view/vs/$slug.tsx | 2 +- apps/web/src/routes/api/images.$.ts | 44 ++++++++++++++ apps/web/src/routes/auth.tsx | 2 +- apps/web/src/routes/join-waitlist.tsx | 6 +- 21 files changed, 142 insertions(+), 130 deletions(-) create mode 100644 apps/web/src/routes/api/images.$.ts diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index f7f9a1ec37..e7d5a5b03d 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -20,7 +20,7 @@ export function Footer() {
Hyprnote diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index a3489e80b2..70782f8df7 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -45,7 +45,7 @@ export function Header() { className="font-semibold text-2xl font-serif hover:scale-105 transition-transform mr-4" > Hyprnote @@ -145,7 +145,7 @@ export function Header() { className="sm:hidden font-semibold text-2xl font-serif hover:scale-105 transition-transform" > Hyprnote diff --git a/apps/web/src/components/video-thumbnail.tsx b/apps/web/src/components/video-thumbnail.tsx index 899640e65c..aa2add8b45 100644 --- a/apps/web/src/components/video-thumbnail.tsx +++ b/apps/web/src/components/video-thumbnail.tsx @@ -24,7 +24,7 @@ export function VideoThumbnail({ > ()({ { property: "og:url", content: "https://hyprnote.com" }, { property: "og:image", - content: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/hyprnote/og-image.jpg", + content: "/api/images/hyprnote/og-image.jpg", }, { property: "og:image:width", content: "1200" }, { property: "og:image:height", content: "630" }, @@ -48,8 +47,7 @@ export const Route = createRootRouteWithContext()({ { name: "twitter:url", content: "https://hyprnote.com" }, { name: "twitter:image", - content: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/hyprnote/og-image.jpg", + content: "/api/images/hyprnote/og-image.jpg", }, ], links: [{ rel: "stylesheet", href: appCss }], diff --git a/apps/web/src/routes/_view/about.tsx b/apps/web/src/routes/_view/about.tsx index 387ead3d7d..d284a0318f 100644 --- a/apps/web/src/routes/_view/about.tsx +++ b/apps/web/src/routes/_view/about.tsx @@ -48,8 +48,7 @@ const founders = [ name: "John Jeong", role: "Chief Wisdom Seeker", bio: "I love designing simple and intuitive user interfaces.", - image: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/john.png", + image: "/api/images/team/john.png", links: { twitter: "https://x.com/computeless", github: "https://github.com/computelesscomputer", @@ -62,8 +61,7 @@ const founders = [ name: "Yujong Lee", role: "Chief OSS Lover", bio: "I am super bullish about open-source software.", - image: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/yujong.png", + image: "/api/images/team/yujong.png", links: { twitter: "https://x.com/yujonglee", github: "https://github.com/yujonglee", @@ -77,72 +75,72 @@ const teamPhotos = [ { id: "john-1", name: "john-1.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/john-1.jpg", + url: "/api/images/team/john-1.jpg", }, { id: "john-2", name: "john-2.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/john-2.jpg", + url: "/api/images/team/john-2.jpg", }, { id: "palo-alto-1", name: "palo-alto-1.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/palo-alto-1.jpg", + url: "/api/images/team/palo-alto-1.jpg", }, { id: "palo-alto-2", name: "palo-alto-2.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/palo-alto-2.jpg", + url: "/api/images/team/palo-alto-2.jpg", }, { id: "palo-alto-3", name: "palo-alto-3.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/palo-alto-3.jpg", + url: "/api/images/team/palo-alto-3.jpg", }, { id: "palo-alto-4", name: "palo-alto-4.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/palo-alto-4.jpg", + url: "/api/images/team/palo-alto-4.jpg", }, { id: "sadang", name: "sadang.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/sadang.jpg", + url: "/api/images/team/sadang.jpg", }, { id: "yc-0", name: "yc-0.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/yc-0.jpg", + url: "/api/images/team/yc-0.jpg", }, { id: "yc-1", name: "yc-1.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/yc-1.jpg", + url: "/api/images/team/yc-1.jpg", }, { id: "yc-2", name: "yc-2.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/yc-2.jpg", + url: "/api/images/team/yc-2.jpg", }, { id: "yujong-1", name: "yujong-1.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/yujong-1.jpg", + url: "/api/images/team/yujong-1.jpg", }, { id: "yujong-2", name: "yujong-2.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/yujong-2.jpg", + url: "/api/images/team/yujong-2.jpg", }, { id: "yujong-3", name: "yujong-3.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/yujong-3.jpg", + url: "/api/images/team/yujong-3.jpg", }, { id: "yujong-4", name: "yujong-4.jpg", - url: "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/team/yujong-4.jpg", + url: "/api/images/team/yujong-4.jpg", }, ]; @@ -279,7 +277,7 @@ function OurStoryGrid({ >
Our Story
Our Story void }) {
Hyprnote Signature 0; const iconUrl = isPrerelease - ? "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/icons/nightly-icon.png" - : "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/icons/stable-icon.png"; + ? "/api/images/icons/nightly-icon.png" + : "/api/images/icons/stable-icon.png"; return ( 0; const iconUrl = isPrerelease - ? "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/icons/nightly-icon.png" - : "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/icons/stable-icon.png"; + ? "/api/images/icons/nightly-icon.png" + : "/api/images/icons/stable-icon.png"; return (
Hyprnote
No bots interface @@ -687,7 +680,7 @@ export function CoolStuffSection() {
No internet interface @@ -711,7 +704,7 @@ export function CoolStuffSection() {
No bots interface @@ -729,7 +722,7 @@ export function CoolStuffSection() {
No internet interface @@ -1093,7 +1086,7 @@ export function MainFeaturesSection({
Hyprnote ) : ( {`${feature.title} @@ -1368,7 +1361,7 @@ function DetailsMobileCarousel({ /> ) : ( {`${feature.title} @@ -1466,7 +1459,7 @@ function DetailsTabletView({ /> ) : ( {`${detailsFeatures[selectedDetail].title} @@ -1569,7 +1562,7 @@ function DetailsDesktopView() { ) : ( {`${selectedFeature.title} @@ -1586,15 +1579,13 @@ function ManifestoSection() {

@@ -1622,14 +1613,14 @@ function ManifestoSection() {
John Jeong Yujong Lee Hyprnote Signature
Hyprnote
@@ -113,12 +113,12 @@ function Component() { /> {appIcon ? ( Hyprnote diff --git a/apps/web/src/routes/_view/pricing.tsx b/apps/web/src/routes/_view/pricing.tsx index 3f4ea5e9ab..b143fd40fd 100644 --- a/apps/web/src/routes/_view/pricing.tsx +++ b/apps/web/src/routes/_view/pricing.tsx @@ -330,7 +330,7 @@ function CTASection() {
Hyprnote AI assistant AI assistant @@ -419,7 +419,7 @@ function CTASection() {
Hyprnote
AI notetaking in action @@ -626,7 +621,7 @@ function TranscriptionSection() {
On-device transcription @@ -670,7 +665,7 @@ function TranscriptionSection() {
On-device transcription @@ -1033,8 +1028,7 @@ function SearchSection() {
{ const baseCollaborators = [ { name: "Alex Johnson", - avatar: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/mock/alex-johnson.png", + avatar: "/api/images/mock/alex-johnson.png", scope: "Can view", }, { name: "Jessica Lee", - avatar: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/mock/jessica-lee.png", + avatar: "/api/images/mock/jessica-lee.png", scope: "Can edit", }, { name: "Sarah Chen", - avatar: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/mock/sarah-chen.png", + avatar: "/api/images/mock/sarah-chen.png", scope: "Can edit", }, { name: "Michael Park", - avatar: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/mock/michael-park.png", + avatar: "/api/images/mock/michael-park.png", scope: "Can view", }, { name: "Emily Rodriguez", - avatar: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/mock/emily-rodriguez.png", + avatar: "/api/images/mock/emily-rodriguez.png", scope: "Can edit", }, ]; const davidKim = { name: "David Kim", - avatar: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/mock/david-kim.png", + avatar: "/api/images/mock/david-kim.png", scope: davidScope, }; @@ -2079,35 +2067,30 @@ const floatingPanelTabs = [ title: "Compact Mode", description: "Minimal overlay that indicates recording is active. Stays out of your way.", - image: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/hyprnote/float-compact.jpg", + image: "/api/images/hyprnote/float-compact.jpg", }, { title: "Memos", description: "Take quick notes during the meeting without losing focus on the conversation.", - image: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/hyprnote/float-memos.jpg", + image: "/api/images/hyprnote/float-memos.jpg", }, { title: "Transcript", description: "Watch the live transcript as the conversation unfolds in real-time.", - image: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/hyprnote/float-transcript.jpg", + image: "/api/images/hyprnote/float-transcript.jpg", }, { title: "Live Insights", description: "Rolling summary of the past 5 minutes with AI suggestions and next steps.", - image: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/hyprnote/float-insights.jpg", + image: "/api/images/hyprnote/float-insights.jpg", }, { title: "Chat", description: "Ask questions and get instant answers during the meeting.", - image: - "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images/hyprnote/float-chat.jpg", + image: "/api/images/hyprnote/float-chat.jpg", }, ]; @@ -2329,7 +2312,7 @@ function CTASection() {
Hyprnote
Hyprnote
Hyprnote Hyprnote
{integration.name}
Integration
Hyprnote diff --git a/apps/web/src/routes/api/images.$.ts b/apps/web/src/routes/api/images.$.ts new file mode 100644 index 0000000000..875a66a68a --- /dev/null +++ b/apps/web/src/routes/api/images.$.ts @@ -0,0 +1,44 @@ +import { createFileRoute } from "@tanstack/react-router"; + +const SUPABASE_STORAGE_URL = + "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images"; + +export const Route = createFileRoute("/api/images/$")({ + server: { + handlers: { + GET: async ({ params }) => { + const path = params._splat; + + if (!path) { + return new Response("Not found", { status: 404 }); + } + + const url = `${SUPABASE_STORAGE_URL}/${path}`; + + const response = await fetch(url); + + if (!response.ok) { + return new Response("Not found", { status: response.status }); + } + + const contentType = response.headers.get("content-type"); + const cacheControl = response.headers.get("cache-control"); + + const headers: HeadersInit = {}; + if (contentType) { + headers["Content-Type"] = contentType; + } + if (cacheControl) { + headers["Cache-Control"] = cacheControl; + } else { + headers["Cache-Control"] = "public, max-age=31536000, immutable"; + } + + return new Response(response.body, { + status: 200, + headers, + }); + }, + }, + }, +}); diff --git a/apps/web/src/routes/auth.tsx b/apps/web/src/routes/auth.tsx index 7aafc028cc..c2d374b966 100644 --- a/apps/web/src/routes/auth.tsx +++ b/apps/web/src/routes/auth.tsx @@ -62,7 +62,7 @@ function Header() { ])} > Hyprnote
{children} From 8f026dfae0fe21eddf08060a0970509ccc880fac Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 01:39:27 +0000 Subject: [PATCH 2/3] fix(web): add path sanitization and improve error handling in image proxy - Add sanitizePath function to validate and sanitize the path parameter - Reject paths with .., backslashes, empty segments, or unsafe characters - Only allow alphanumeric characters, dots, hyphens, and underscores - Return 404 for upstream 404 errors, 502 for other upstream errors Co-Authored-By: yujonglee --- apps/web/src/routes/api/images.$.ts | 39 ++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/web/src/routes/api/images.$.ts b/apps/web/src/routes/api/images.$.ts index 875a66a68a..18347369d9 100644 --- a/apps/web/src/routes/api/images.$.ts +++ b/apps/web/src/routes/api/images.$.ts @@ -3,22 +3,53 @@ import { createFileRoute } from "@tanstack/react-router"; const SUPABASE_STORAGE_URL = "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images"; +const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/; + +function sanitizePath(raw: string | undefined): string | null { + if (!raw) return null; + + let decoded: string; + try { + decoded = decodeURIComponent(raw); + } catch { + return null; + } + + if (decoded.startsWith("/") || decoded.includes("\\")) { + return null; + } + + const segments = decoded.split("/"); + if (segments.length === 0) return null; + + for (const segment of segments) { + if (!segment) return null; + if (segment === "." || segment === "..") return null; + if (!SAFE_SEGMENT.test(segment)) return null; + } + + return segments.join("/"); +} + export const Route = createFileRoute("/api/images/$")({ server: { handlers: { GET: async ({ params }) => { - const path = params._splat; + const sanitizedPath = sanitizePath(params._splat); - if (!path) { + if (!sanitizedPath) { return new Response("Not found", { status: 404 }); } - const url = `${SUPABASE_STORAGE_URL}/${path}`; + const url = `${SUPABASE_STORAGE_URL}/${sanitizedPath}`; const response = await fetch(url); if (!response.ok) { - return new Response("Not found", { status: response.status }); + if (response.status === 404) { + return new Response("Not found", { status: 404 }); + } + return new Response("Upstream service error", { status: 502 }); } const contentType = response.headers.get("content-type"); From 735e8c89b702e4d7b86ad827a7fc752b1d691f6f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 01:43:05 +0000 Subject: [PATCH 3/3] fix(web): allow + character in image proxy path for symbol+logo.png Co-Authored-By: yujonglee --- apps/web/src/routes/api/images.$.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/routes/api/images.$.ts b/apps/web/src/routes/api/images.$.ts index 18347369d9..334dee2f85 100644 --- a/apps/web/src/routes/api/images.$.ts +++ b/apps/web/src/routes/api/images.$.ts @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; const SUPABASE_STORAGE_URL = "https://ijoptyyjrfqwaqhyxkxj.supabase.co/storage/v1/object/public/public_images"; -const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/; +const SAFE_SEGMENT = /^[A-Za-z0-9._+-]+$/; function sanitizePath(raw: string | undefined): string | null { if (!raw) return null;