/** * Guards the shipped webview security configuration. * * `csp` was `null` and the asset protocol was scoped to the whole storage root, * which is the directory holding the SQLite database and the encrypted-token * fallback file. Both are one-character regressions away and neither is visible * in any behavioural test, so they are asserted here instead: the restrictive * half of the policy must stay restrictive, and the permissive half must keep * the schemes playback actually needs. * * TRACES: UR-012, UR-071 | DR-198 | UT-193 */ import { describe, it, expect } from "vitest"; import { readFileSync } from "fs"; import { resolve } from "path"; const config = JSON.parse( readFileSync(resolve(__dirname, "../src-tauri/tauri.conf.json"), "utf-8"), ); const security = config.app.security; /** Split a CSP string into `directive -> sources`. */ function directives(csp: string): Record { const map: Record = {}; for (const part of csp.split(";")) { const [name, ...sources] = part.trim().split(/\s+/); if (name) map[name] = sources; } return map; } describe("tauri.conf.json CSP", () => { it("is set at all — a null CSP hands any injected script the full IPC surface", () => { expect(typeof security.csp).toBe("string"); expect(security.csp.length).toBeGreaterThan(0); }); const csp = directives(security.csp as string); it("locks down script execution", () => { // Tauri injects a nonce for SvelteKit's inline bootstrap script at build // time, so 'self' alone is enough and inline/eval must never be re-added. expect(csp["script-src"]).toEqual(["'self'"]); expect(csp["object-src"]).toEqual(["'none'"]); expect(csp["frame-src"]).toEqual(["'none'"]); expect(csp["base-uri"]).toEqual(["'self'"]); expect(csp["default-src"]).toEqual(["'self'"]); }); it("keeps the schemes playback and thumbnails depend on", () => { // The asset protocol under both names convertFileSrc emits. expect(csp["img-src"]).toContain("asset:"); expect(csp["img-src"]).toContain("http://asset.localhost"); expect(csp["media-src"]).toContain("asset:"); // hls.js: MSE object URLs, and its demuxer worker built from a blob. expect(csp["media-src"]).toContain("blob:"); expect(csp["worker-src"]).toContain("blob:"); // The token-guarded loopback media server (DR-137). expect(csp["media-src"]).toContain("http://127.0.0.1:*"); // Tauri's invoke transport. expect(csp["connect-src"]).toContain("ipc:"); expect(csp["connect-src"]).toContain("http://ipc.localhost"); // The user's Jellyfin server: an arbitrary run-time origin, http on a LAN. for (const directive of ["img-src", "media-src", "connect-src"]) { expect(csp[directive]).toContain("http:"); expect(csp[directive]).toContain("https:"); } }); it("never widens a data directive into script execution", () => { for (const [name, sources] of Object.entries(csp)) { if (name === "script-src" || name === "worker-src") { expect(sources).not.toContain("'unsafe-eval'"); expect(sources).not.toContain("'unsafe-inline'"); } // A bare `*` would re-admit every scheme, including file:. expect(sources).not.toContain("*"); } }); }); describe("tauri.conf.json asset protocol scope", () => { const scope: string[] = security.assetProtocol.scope; it("covers only the thumbnail cache, not the storage root", () => { expect(scope).toEqual(["$APPDATA/thumbnails/**"]); // The database and the encrypted-token fallback live directly in $APPDATA. expect(scope).not.toContain("$APPDATA/**"); }); });