From c731d5b1f50745c93109832972bd59f9b4a5fb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=93=E6=A9=A6?= <107380090+dangzhuotong@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:52:57 +0000 Subject: [PATCH] fix: accept the configured web host in share URL parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-hosted Slack unfurls and /api/oembed dropped /s/ links because parseCapShareUrl only allowed cap.so and cap.link. Fixes #2546 Co-authored-by: 卓橦 --- apps/web/__tests__/unit/oembed.test.ts | 26 +++++++++++++++++++++++++- apps/web/lib/oembed.ts | 18 +++++++++++++++++- apps/web/lib/share-video-metadata.ts | 4 ++-- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/apps/web/__tests__/unit/oembed.test.ts b/apps/web/__tests__/unit/oembed.test.ts index eba9f7021f2..b9d0b3a82f8 100644 --- a/apps/web/__tests__/unit/oembed.test.ts +++ b/apps/web/__tests__/unit/oembed.test.ts @@ -1,8 +1,16 @@ import { Video } from "@cap/web-domain"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { buildOEmbedVideo, parseCapShareUrl } from "@/lib/oembed"; import type { PublicShareVideo } from "@/lib/public-share-video"; +const serverEnvMock = vi.hoisted(() => + vi.fn(() => ({ WEB_URL: "https://www.video.example.com" })), +); + +vi.mock("@cap/env", () => ({ + serverEnv: serverEnvMock, +})); + const video: PublicShareVideo = { id: Video.VideoId.make("abc123"), name: "Cap walkthrough", @@ -20,13 +28,19 @@ describe("Cap oEmbed", () => { ["https://www.cap.so/s/abc123?t=3", "abc123"], ["https://cap.link/abc123", "abc123"], ["https://www.cap.link/abc123?foo=bar", "abc123"], + ["https://video.example.com/s/abc123", "abc123"], + ["https://www.video.example.com/s/abc123?t=3", "abc123"], ])("parses supported share URL %s", (url, expected) => { expect(parseCapShareUrl(url)).toBe(expected); }); it.each([ "http://cap.so/s/abc123", + "http://video.example.com/s/abc123", "https://example.com/s/abc123", + "https://video.example.com/abc123", + "https://video.example.com/embed/abc123", + "https://cap.link/s/abc123", "https://cap.so/embed/abc123", "https://cap.link/video/demo.mp4", "https://cap.so/s/abc123/extra", @@ -35,6 +49,16 @@ describe("Cap oEmbed", () => { expect(parseCapShareUrl(url)).toBeNull(); }); + it("ignores an unreadable WEB_URL instead of failing the parse", () => { + serverEnvMock.mockImplementationOnce(() => { + throw new Error("Invalid environment variables"); + }); + + expect(parseCapShareUrl("https://video.example.com/s/abc123")).toBeNull(); + expect(parseCapShareUrl("https://cap.so/s/abc123")).toBe("abc123"); + expect(parseCapShareUrl("https://cap.link/abc123")).toBe("abc123"); + }); + it("returns a responsive video response with preserved dimensions", () => { const result = buildOEmbedVideo({ video, diff --git a/apps/web/lib/oembed.ts b/apps/web/lib/oembed.ts index d6d72965a2b..ca0b3a0f459 100644 --- a/apps/web/lib/oembed.ts +++ b/apps/web/lib/oembed.ts @@ -1,3 +1,4 @@ +import { serverEnv } from "@cap/env"; import type { PublicShareVideo } from "@/lib/public-share-video"; const DEFAULT_WIDTH = 800; @@ -49,6 +50,17 @@ const scaleDimensions = ({ }; }; +const configuredShareHost = () => { + try { + return new URL(serverEnv().WEB_URL).hostname + .toLowerCase() + .replace(/^www\./, ""); + } catch { + // A missing or invalid WEB_URL must not reject cap.so / cap.link links. + return undefined; + } +}; + export const parseCapShareUrl = (value: string) => { let url: URL; try { @@ -62,7 +74,11 @@ export const parseCapShareUrl = (value: string) => { const segments = url.pathname.split("/").filter(Boolean); let videoId: string | undefined; - if (hostname === "cap.so" && segments.length === 2 && segments[0] === "s") { + if ( + segments.length === 2 && + segments[0] === "s" && + (hostname === "cap.so" || hostname === configuredShareHost()) + ) { videoId = segments[1]; } if ( diff --git a/apps/web/lib/share-video-metadata.ts b/apps/web/lib/share-video-metadata.ts index fe832eb2155..769055e0a85 100644 --- a/apps/web/lib/share-video-metadata.ts +++ b/apps/web/lib/share-video-metadata.ts @@ -18,8 +18,8 @@ export type ShareVideoMetadataInput = { /** * Two surfaces only answer on the default Cap origin: `proxy.ts` redirects * `/embed/` away from a custom domain, and `parseCapShareUrl` accepts share - * URLs on `cap.so` and `cap.link` alone, so `/api/oembed` rejects a custom - * domain `url`. Defaults to `webUrl`. + * URLs on `cap.so`, `cap.link`, and the configured `WEB_URL` host, so + * `/api/oembed` rejects a different custom-domain `url`. Defaults to `webUrl`. */ canonicalWebUrl?: string; advertiseIframelyPlayer?: boolean;