diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 968e2600..98505306 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -10,6 +10,7 @@ import { db, schema } from "./db/index.js"; import { runMigrations } from "./db/migrate.js"; import { captureException, initAnalytics, shutdownAnalytics } from "./lib/analytics.js"; import { startCleanupCron } from "./lib/cleanup.js"; +import { buildCsp } from "./lib/csp.js"; import { ensureAiDirs, recoverInterruptedInstalls } from "./lib/feature-status.js"; import { shutdownWorkerPool } from "./lib/worker-pool.js"; import { requirePermission } from "./permissions.js"; @@ -127,10 +128,7 @@ app.addHook("onSend", async (_request, reply) => { reply.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); if (process.env.NODE_ENV === "production") { reply.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); - const csp = _request.url.startsWith("/api/docs") - ? "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self'; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'" - : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https://tile.openstreetmap.org; connect-src 'self'; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"; - reply.header("Content-Security-Policy", csp); + reply.header("Content-Security-Policy", buildCsp(_request.url.startsWith("/api/docs"))); } }); diff --git a/apps/api/src/lib/csp.ts b/apps/api/src/lib/csp.ts new file mode 100644 index 00000000..b5bfa269 --- /dev/null +++ b/apps/api/src/lib/csp.ts @@ -0,0 +1,17 @@ +const POSTHOG_ORIGINS = ["https://us.i.posthog.com", "https://us-assets.i.posthog.com"]; +const SENTRY_ORIGINS = ["https://*.ingest.us.sentry.io"]; +const SCALAR_FONT_ORIGIN = "https://fonts.scalar.com"; + +export function buildCsp(isDocs: boolean): string { + const connectSrc = ["'self'", ...POSTHOG_ORIGINS, ...SENTRY_ORIGINS].join(" "); + const fontSrc = isDocs ? `'self' data: ${SCALAR_FONT_ORIGIN}` : "'self' data:"; + const scriptSrc = isDocs + ? "'self' 'unsafe-inline' https://us-assets.i.posthog.com" + : "'self' https://us-assets.i.posthog.com"; + + if (isDocs) { + return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src ${connectSrc}; font-src ${fontSrc}; object-src 'none'; base-uri 'self'; form-action 'self'`; + } + + return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https://tile.openstreetmap.org; connect-src ${connectSrc}; font-src ${fontSrc}; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`; +} diff --git a/apps/api/src/routes/branding.ts b/apps/api/src/routes/branding.ts index ae8e81be..720c9a62 100644 --- a/apps/api/src/routes/branding.ts +++ b/apps/api/src/routes/branding.ts @@ -16,7 +16,7 @@ import { db, schema } from "../db/index.js"; import { ensureSharpCompat } from "../lib/heic-converter.js"; import { requirePermission } from "../permissions.js"; -const BRANDING_DIR = join(process.cwd(), "data", "branding"); +const BRANDING_DIR = join(env.FILES_STORAGE_PATH, "branding"); const LOGO_PATH = join(BRANDING_DIR, "logo.png"); const maxLogoSize = env.MAX_LOGO_SIZE_KB * 1024; diff --git a/apps/web/src/components/settings/settings-dialog.tsx b/apps/web/src/components/settings/settings-dialog.tsx index a70b7fe9..b331826f 100644 --- a/apps/web/src/components/settings/settings-dialog.tsx +++ b/apps/web/src/components/settings/settings-dialog.tsx @@ -2292,6 +2292,7 @@ function AuditLogSection() { function ToolsSection() { const [disabledTools, setDisabledTools] = useState([]); const [loading, setLoading] = useState(true); + const [loadFailed, setLoadFailed] = useState(false); const [saving, setSaving] = useState(false); const [search, setSearch] = useState(""); const [showRestartBanner, setShowRestartBanner] = useState(false); @@ -2302,8 +2303,9 @@ function ToolsSection() { setDisabledTools( data.settings.disabledTools ? JSON.parse(data.settings.disabledTools) : [], ); + setLoadFailed(false); }) - .catch(() => {}) + .catch(() => setLoadFailed(true)) .finally(() => setLoading(false)); }, []); @@ -2424,11 +2426,17 @@ function ToolsSection() {

)} + {loadFailed && ( +
+ Failed to load tool settings. Saving is disabled to prevent data loss. +
+ )} +