Detect HTTP-header cookies via CookieStore change event

Progressive enhancement for Chromium browsers: listen on the
CookieStore change event to catch cookies set by Set-Cookie HTTP
response headers, which the document.cookie setter hook cannot see.
Adds a new "http" cookie source through the full stack.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-06 18:54:50 +04:00
parent a4cb61366f
commit c0d0221be1
5 changed files with 81 additions and 2 deletions

View File

@@ -20,7 +20,7 @@ import { fetchJSON } from "../http";
interface DetectedCookieEntry {
name: string;
max_age_seconds: number | null;
source: "script" | "pre-existing";
source: "script" | "pre-existing" | "http";
}
const DEBOUNCE_MS = 2_000;
@@ -39,6 +39,7 @@ export class CookieDetector implements Detector {
private readonly pending: Map<string, DetectedCookieEntry> = new Map();
private timer: ReturnType<typeof setTimeout> | null = null;
private originalDescriptor: PropertyDescriptor | null = null;
private cookieStoreHandler: ((event: CookieChangeEvent) => void) | null = null;
constructor(baseUrl: URL, bannerId: string, knownNames: Set<string>) {
this.reportUrl = new URL(`${bannerId}/report`, baseUrl);
@@ -70,6 +71,7 @@ export class CookieDetector implements Detector {
});
this.scanExisting();
this.observeCookieStore();
}
stop(): void {
@@ -82,6 +84,11 @@ export class CookieDetector implements Detector {
this.flush();
}
if (this.cookieStoreHandler && typeof cookieStore !== "undefined") {
cookieStore.removeEventListener("change", this.cookieStoreHandler);
this.cookieStoreHandler = null;
}
if (this.originalDescriptor) {
Object.defineProperty(document, "cookie", this.originalDescriptor);
this.originalDescriptor = null;
@@ -120,6 +127,32 @@ export class CookieDetector implements Detector {
}
}
private observeCookieStore(): void {
if (typeof cookieStore === "undefined" || typeof cookieStore.addEventListener !== "function") {
return;
}
this.cookieStoreHandler = (event: CookieChangeEvent) => {
for (const cookie of event.changed) {
if (this.knownNames.has(cookie.name) || this.reported.has(cookie.name)) continue;
const maxAge = cookie.expires
? Math.round((cookie.expires - Date.now()) / 1000)
: null;
this.reported.add(cookie.name);
this.pending.set(cookie.name, {
name: cookie.name,
max_age_seconds: maxAge && maxAge > 0 ? maxAge : null,
source: "http",
});
}
if (this.pending.size > 0) this.scheduleFlush();
};
cookieStore.addEventListener("change", this.cookieStoreHandler);
}
private scheduleFlush(): void {
if (this.timer) return;
this.timer = setTimeout(() => {

View File

@@ -13,3 +13,25 @@
// PERFORMANCE OF THIS SOFTWARE.
declare const __SDK_VERSION__: string;
interface CookieListItem {
name: string;
value: string;
domain: string | null;
path: string;
expires: number | null;
secure: boolean;
sameSite: "strict" | "lax" | "none";
}
interface CookieChangeEvent extends Event {
changed: readonly CookieListItem[];
deleted: readonly CookieListItem[];
}
interface CookieStoreEventTarget extends EventTarget {
addEventListener(type: "change", listener: (event: CookieChangeEvent) => void): void;
removeEventListener(type: "change", listener: (event: CookieChangeEvent) => void): void;
}
declare const cookieStore: CookieStoreEventTarget | undefined;