mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
The production CSP had connect-src/script-src/font-src set to 'self' only, silently blocking all analytics and error reporting in production while working fine in dev (where CSP is not applied). CSP fixes: - Add PostHog ingest + assets origins to connect-src and script-src - Add Sentry ingest origin to connect-src - Add Scalar fonts origin to font-src for API docs pages - Extract CSP construction into testable buildCsp() function Silent failure hardening: - Settings/features stores now set loadError flag and allow retry on subsequent fetch() calls instead of permanently caching failed state - Analytics init no longer sets initialized=true before the try block, allowing retry on failure - Settings dialog Tools section disables save button when settings failed to load, preventing accidental config wipe - Branding logo storage moved from process.cwd() to FILES_STORAGE_PATH so logos persist across Docker container recreation Test coverage: - 16 CSP directive tests covering all external service domains - Store retry-on-error behavior tests for settings and features stores - Analytics init retry-after-failure test
68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { buildCsp } from "../../../apps/api/src/lib/csp.js";
|
|
|
|
function parseDirective(csp: string, directive: string): string[] {
|
|
const match = csp.match(new RegExp(`${directive}\\s+([^;]+)`));
|
|
return match ? match[1].trim().split(/\s+/) : [];
|
|
}
|
|
|
|
describe("buildCsp", () => {
|
|
describe("connect-src allows analytics domains", () => {
|
|
it.each([true, false])("includes PostHog ingest (isDocs=%s)", (isDocs) => {
|
|
const sources = parseDirective(buildCsp(isDocs), "connect-src");
|
|
expect(sources).toContain("https://us.i.posthog.com");
|
|
});
|
|
|
|
it.each([true, false])("includes PostHog assets (isDocs=%s)", (isDocs) => {
|
|
const sources = parseDirective(buildCsp(isDocs), "connect-src");
|
|
expect(sources).toContain("https://us-assets.i.posthog.com");
|
|
});
|
|
|
|
it.each([true, false])("includes Sentry ingest (isDocs=%s)", (isDocs) => {
|
|
const sources = parseDirective(buildCsp(isDocs), "connect-src");
|
|
expect(sources).toContain("https://*.ingest.us.sentry.io");
|
|
});
|
|
|
|
it.each([true, false])("keeps self (isDocs=%s)", (isDocs) => {
|
|
expect(parseDirective(buildCsp(isDocs), "connect-src")).toContain("'self'");
|
|
});
|
|
});
|
|
|
|
describe("script-src allows PostHog config loader", () => {
|
|
it.each([true, false])("includes PostHog assets origin (isDocs=%s)", (isDocs) => {
|
|
const sources = parseDirective(buildCsp(isDocs), "script-src");
|
|
expect(sources).toContain("https://us-assets.i.posthog.com");
|
|
});
|
|
|
|
it("docs pages allow unsafe-inline for Scalar", () => {
|
|
expect(parseDirective(buildCsp(true), "script-src")).toContain("'unsafe-inline'");
|
|
});
|
|
|
|
it("app pages do not allow unsafe-inline", () => {
|
|
expect(parseDirective(buildCsp(false), "script-src")).not.toContain("'unsafe-inline'");
|
|
});
|
|
});
|
|
|
|
describe("font-src allows Scalar docs fonts", () => {
|
|
it("docs pages include Scalar fonts origin", () => {
|
|
const sources = parseDirective(buildCsp(true), "font-src");
|
|
expect(sources).toContain("https://fonts.scalar.com");
|
|
});
|
|
|
|
it("app pages do not include Scalar fonts origin", () => {
|
|
const sources = parseDirective(buildCsp(false), "font-src");
|
|
expect(sources).not.toContain("https://fonts.scalar.com");
|
|
});
|
|
});
|
|
|
|
it("includes frame-ancestors none for app pages but not docs", () => {
|
|
expect(buildCsp(false)).toContain("frame-ancestors 'none'");
|
|
expect(buildCsp(true)).not.toContain("frame-ancestors");
|
|
});
|
|
|
|
it("allows OpenStreetMap tiles in img-src for app pages", () => {
|
|
const sources = parseDirective(buildCsp(false), "img-src");
|
|
expect(sources).toContain("https://tile.openstreetmap.org");
|
|
});
|
|
});
|