mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
fix(ui): keep private and no-store responses out of offline cache
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
be9c775d9e
commit
46321e2421
2 files changed
+48
-3
No files matched your search
+5
-3
@@ -24,8 +24,9 @@ self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET requests and API calls
|
||||
if (request.method !== "GET" || url.pathname.startsWith("/api")) {
|
||||
// Explicitly private requests must bypass BOTH cache writes and offline
|
||||
// fallback, including extension endpoints outside the host /api namespace.
|
||||
if (request.method !== "GET" || url.pathname.startsWith("/api") || request.cache === "no-store") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,7 +34,8 @@ self.addEventListener("fetch", (event) => {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
if (response.ok && url.origin === self.location.origin) {
|
||||
const cacheControl = response.headers.get("cache-control") ?? "";
|
||||
if (response.ok && url.origin === self.location.origin && !/(?:^|,)\s*(?:no-store|private)(?:\s*(?:,|=)|\s*$)/i.test(cacheControl)) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// @vitest-environment node
|
||||
import { readFileSync } from "node:fs";
|
||||
import vm from "node:vm";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
function worker() {
|
||||
const handlers = new Map<string, (event: unknown) => void>();
|
||||
const put = vi.fn();
|
||||
const match = vi.fn();
|
||||
const fetch = vi.fn().mockResolvedValue(new Response("public asset"));
|
||||
vm.runInNewContext(readFileSync(new URL("../../public/sw.js", import.meta.url), "utf8"), {
|
||||
self: { location: { origin: "https://example.test" }, addEventListener: (type: string, fn: (event: unknown) => void) => handlers.set(type, fn) },
|
||||
URL, Response, fetch, caches: { open: async () => ({ put }), match },
|
||||
});
|
||||
const request = (cache: RequestCache = "default") => {
|
||||
const respondWith = vi.fn();
|
||||
handlers.get("fetch")!({ request: new Request("https://example.test/extension/history", { cache }), respondWith });
|
||||
return respondWith;
|
||||
};
|
||||
return { request, fetch, put, match };
|
||||
}
|
||||
|
||||
describe("service worker privacy boundaries", () => {
|
||||
it("bypasses both caching and offline fallback for a no-store request outside /api", () => {
|
||||
const w = worker();
|
||||
expect(w.request("no-store")).not.toHaveBeenCalled();
|
||||
expect(w.fetch).not.toHaveBeenCalled();
|
||||
expect(w.match).not.toHaveBeenCalled();
|
||||
expect(w.put).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each(["no-store", "max-age=0, no-store", "private", 'private="Set-Cookie"', "PRIVATE, max-age=60"])("never caches a response marked %s", async directive => {
|
||||
const w = worker();
|
||||
w.fetch.mockResolvedValue(new Response("personal content", { headers: { "cache-control": directive } }));
|
||||
const response = await w.request().mock.calls[0]![0];
|
||||
expect(await response.text()).toBe("personal content");
|
||||
expect(w.put).not.toHaveBeenCalled();
|
||||
});
|
||||
it("keeps public asset offline caching", async () => {
|
||||
const w = worker();
|
||||
await w.request().mock.calls[0]![0];
|
||||
expect(w.put).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user