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:
@@ -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(() => {
|
||||
|
||||
22
packages/cookie-banner/src/globals.d.ts
vendored
22
packages/cookie-banner/src/globals.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -24,12 +24,14 @@ type CookieSource string
|
||||
const (
|
||||
CookieSourceScript CookieSource = "SCRIPT"
|
||||
CookieSourcePreExisting CookieSource = "PRE_EXISTING"
|
||||
CookieSourceHTTP CookieSource = "HTTP"
|
||||
)
|
||||
|
||||
func CookieSources() []CookieSource {
|
||||
return []CookieSource{
|
||||
CookieSourceScript,
|
||||
CookieSourcePreExisting,
|
||||
CookieSourceHTTP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +55,8 @@ func (s *CookieSource) Scan(value any) error {
|
||||
*s = CookieSourceScript
|
||||
case CookieSourcePreExisting:
|
||||
*s = CookieSourcePreExisting
|
||||
case CookieSourceHTTP:
|
||||
*s = CookieSourceHTTP
|
||||
default:
|
||||
return fmt.Errorf("invalid CookieSource value: %q", v)
|
||||
}
|
||||
@@ -62,7 +66,8 @@ func (s *CookieSource) Scan(value any) error {
|
||||
func (s CookieSource) Value() (driver.Value, error) {
|
||||
switch s {
|
||||
case CookieSourceScript,
|
||||
CookieSourcePreExisting:
|
||||
CookieSourcePreExisting,
|
||||
CookieSourceHTTP:
|
||||
return string(s), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid CookieSource: %s", s)
|
||||
|
||||
15
pkg/coredata/migrations/20260506T144600Z.sql
Normal file
15
pkg/coredata/migrations/20260506T144600Z.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
--
|
||||
-- Permission to use, copy, modify, and/or distribute this software for any
|
||||
-- purpose with or without fee is hereby granted, provided that the above
|
||||
-- copyright notice and this permission notice appear in all copies.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
-- PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
ALTER TYPE cookie_source ADD VALUE IF NOT EXISTS 'HTTP';
|
||||
@@ -266,6 +266,8 @@ func (h *Handler) handleReportDetectedCookies(w http.ResponseWriter, r *http.Req
|
||||
switch strings.TrimSpace(c.Source) {
|
||||
case "pre-existing":
|
||||
source = coredata.CookieSourcePreExisting
|
||||
case "http":
|
||||
source = coredata.CookieSourceHTTP
|
||||
default:
|
||||
source = coredata.CookieSourceScript
|
||||
}
|
||||
@@ -358,6 +360,8 @@ func (h *Handler) handleReportDetectedTrackers(w http.ResponseWriter, r *http.Re
|
||||
switch strings.TrimSpace(c.Source) {
|
||||
case "pre-existing":
|
||||
source = coredata.CookieSourcePreExisting
|
||||
case "http":
|
||||
source = coredata.CookieSourceHTTP
|
||||
default:
|
||||
source = coredata.CookieSourceScript
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user