Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
93 lines
3.6 KiB
TypeScript
93 lines
3.6 KiB
TypeScript
/**
|
|
* 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<string, string[]> {
|
|
const map: Record<string, string[]> = {};
|
|
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/**");
|
|
});
|
|
});
|