Skip to content

Commit adf08e4

Browse files
authored
fix(web): allow integration logos through CSP (#117)
## Summary - allow the exact Composio logo origin in the browser `img-src` policy so skill cards render their brand marks - remove the redundant Next.js `remotePatterns` entry because these SVGs use `unoptimized` and bypass the optimizer - route browser takeover through Cheatcode's authenticated preview proxy instead of adding Daytona vendor hosts to `frame-src` ## Architecture The web app remains deny-by-default at the browser boundary. Integration logo URLs and the CSP origin share one constant. All sandbox iframes, including noVNC browser takeover, now stay behind the existing `*.trycheatcode.com` preview-proxy boundary. ## Decisions Made | Decision | Choice | Alternative | Reasoning | |---|---|---|---| | Logo permission | exact `https://logos.composio.dev` `img-src` origin | wildcard image hosts | smallest permission that serves the catalog | | Next image config | remove `remotePatterns` | maintain two allowlists | `unoptimized` returns the raw source and does not use the optimizer allowlist | | Browser takeover | owned preview proxy | allow Daytona preview apexes in `frame-src` | avoids vendor-domain CSP expansion and matches the documented preview architecture | ## Edge Cases Handled | Scenario | Handling | |---|---| | malformed or unusual integration slug | URL path segment is encoded before rendering | | logo endpoint fails | existing initials fallback remains in place | | noVNC WebSocket traffic | existing preview proxy WebSocket relay remains the transport | | local preview routing | existing local preview handoff supports the generated proxy URL | ## How to Review 1. Review `apps/web/next.config.ts` and `apps/web/src/lib/integration-logo.ts` for the CSP boundary. 2. Review `integration-brand-logo.tsx` for the shared URL builder. 3. Review `project-sandbox-content.ts` for the browser-takeover proxy alignment. ## Verification - [x] `pnpm lint` - [x] `pnpm typecheck` - [x] `pnpm turbo build --force` - [x] `pnpm turbo skills:build` - [x] `pnpm deadcode` - [x] `pnpm architecture:check` - [x] optimized local server returned the production CSP with the Composio origin - [x] Chrome loaded `https://logos.composio.dev/api/gmail` under that CSP at 128×128
1 parent b947a51 commit adf08e4

4 files changed

Lines changed: 16 additions & 5 deletions

File tree

apps/agent-worker/src/durable-objects/project-sandbox-content.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,16 @@ async function browserTakeoverResult(
378378
password: string,
379379
): Promise<ProjectBrowserTakeoverResult> {
380380
const id = await runtime.ensureSandbox();
381-
const signed = await runtime.client().getSignedPreviewUrl(id, port, input.expiresInSeconds);
381+
const preview = await buildPreviewUrl({
382+
hostname: runtime.previewHostname(),
383+
port,
384+
sandboxId: id,
385+
secret: await runtime.previewSecret(),
386+
});
382387
return {
383388
expiresAt: new Date(Date.now() + input.expiresInSeconds * 1_000).toISOString(),
384389
takeoverId: input.takeoverId,
385-
url: noVncSessionUrl(signed.url, password),
390+
url: noVncSessionUrl(preview.url, password),
386391
};
387392
}
388393

apps/web/next.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
22
import { fileURLToPath } from "node:url";
33
import { parseWebBuildEnvironment, WEB_APPLICATION_ENV_KEYS } from "@cheatcode/env/web-config";
44
import type { NextConfig } from "next";
5+
import { INTEGRATION_LOGO_ORIGIN } from "./src/lib/integration-logo";
56

67
const REPOSITORY_ROOT = fileURLToPath(new URL("../..", import.meta.url));
78
const { loadEnvConfig } = createRequire(import.meta.url)("@next/env") as typeof import("@next/env");
@@ -50,7 +51,7 @@ const CONTENT_SECURITY_POLICY = [
5051
"frame-ancestors 'none'",
5152
`script-src 'self' 'unsafe-inline' ${CLERK_FRONTEND_ORIGIN} https://challenges.cloudflare.com`,
5253
"style-src 'self' 'unsafe-inline'",
53-
"img-src 'self' data: blob: https://img.clerk.com",
54+
`img-src 'self' data: blob: https://img.clerk.com ${INTEGRATION_LOGO_ORIGIN}`,
5455
"font-src 'self' data:",
5556
"media-src 'self' data: blob:",
5657
`connect-src 'self' ${GATEWAY_ORIGIN} ${PREVIEW_HTTPS_ORIGIN} ${PREVIEW_WSS_ORIGIN} ${CLERK_FRONTEND_ORIGIN} ${CLERK_WEBSOCKET_ORIGIN}`,
@@ -87,7 +88,6 @@ const nextConfig = {
8788
images: {
8889
qualities: [75],
8990
minimumCacheTTL: 14_400,
90-
remotePatterns: [{ hostname: "logos.composio.dev", protocol: "https" }],
9191
},
9292
} satisfies NextConfig;
9393

apps/web/src/components/skills/integration-brand-logo.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import Image from "next/image";
44
import { useState } from "react";
5+
import { integrationLogoUrl } from "@/lib/integration-logo";
56
import { cn } from "@/lib/ui/cn";
67

78
const DARK_INVERT_LOGOS = new Set(["dub", "github", "notion"]);
@@ -36,7 +37,7 @@ export function IntegrationBrandLogo({
3637
height={size === "menu" ? 16 : 20}
3738
loading="eager"
3839
onError={() => setHasFailed(true)}
39-
src={`https://logos.composio.dev/api/${slug}`}
40+
src={integrationLogoUrl(slug)}
4041
unoptimized
4142
width={size === "menu" ? 16 : 20}
4243
/>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const INTEGRATION_LOGO_ORIGIN = "https://logos.composio.dev";
2+
3+
export function integrationLogoUrl(slug: string): string {
4+
return `${INTEGRATION_LOGO_ORIGIN}/api/${encodeURIComponent(slug)}`;
5+
}

0 commit comments

Comments
 (0)