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.
+
+ )}
+