diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/CookieBannerResourcesPage.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/CookieBannerResourcesPage.tsx
index 3e6d7d639..3fdfb9e49 100644
--- a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/CookieBannerResourcesPage.tsx
+++ b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/CookieBannerResourcesPage.tsx
@@ -170,6 +170,7 @@ export default function CookieBannerResourcesPage({
+
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx
index 04dd89153..a53a3da80 100644
--- a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx
+++ b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx
@@ -133,6 +133,7 @@ function resourceTypeLabel(type: string, __: (s: string) => string): string {
case "BEACON": return __("Beacon");
case "FETCH": return __("Fetch");
case "MEDIA": return __("Media");
+ case "SERVICE_WORKER": return __("Service Worker");
default: return type;
}
}
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx
index e555e9b00..9e5d2f7e8 100644
--- a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx
+++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx
@@ -130,6 +130,7 @@ function trackerTypeLabel(type: string, __: (s: string) => string): string {
case "LOCAL_STORAGE": return __("localStorage");
case "SESSION_STORAGE": return __("sessionStorage");
case "INDEXED_DB": return __("IndexedDB");
+ case "CACHE_STORAGE": return __("Cache Storage");
default: return type;
}
}
diff --git a/packages/cookie-banner/src/detectors/storage-detector.ts b/packages/cookie-banner/src/detectors/storage-detector.ts
index 5520010ee..7ae7a3444 100644
--- a/packages/cookie-banner/src/detectors/storage-detector.ts
+++ b/packages/cookie-banner/src/detectors/storage-detector.ts
@@ -19,7 +19,11 @@ import { getInitiatorURL } from "./initiator";
interface DetectedStorageEntry {
key: string;
- storage_type: "local_storage" | "session_storage" | "indexed_db";
+ storage_type:
+ | "local_storage"
+ | "session_storage"
+ | "indexed_db"
+ | "cache_storage";
value_size: number | null;
initiator_url?: string;
}
@@ -42,6 +46,7 @@ export class StorageDetector implements Detector {
private timer: ReturnType | null = null;
private originalSetItem: typeof Storage.prototype.setItem | null = null;
private originalIDBOpen: typeof IDBFactory.prototype.open | null = null;
+ private originalCachesOpen: typeof CacheStorage.prototype.open | null = null;
constructor(baseUrl: URL, bannerId: string) {
this.reportUrl = new URL(`${bannerId}/report`, baseUrl);
@@ -51,7 +56,9 @@ export class StorageDetector implements Detector {
start(): void {
this.wrapStorage();
this.wrapIndexedDB();
+ this.wrapCacheStorage();
this.scanExisting();
+ this.scanCacheStorage();
}
stop(): void {
@@ -72,6 +79,10 @@ export class StorageDetector implements Detector {
IDBFactory.prototype.open = this.originalIDBOpen;
this.originalIDBOpen = null;
}
+ if (this.originalCachesOpen && typeof caches !== "undefined") {
+ caches.open = this.originalCachesOpen;
+ this.originalCachesOpen = null;
+ }
}
private wrapStorage(): void {
@@ -107,6 +118,20 @@ export class StorageDetector implements Detector {
};
}
+ private wrapCacheStorage(): void {
+ if (typeof caches === "undefined") return;
+
+ const originalOpen = caches.open.bind(caches);
+ this.originalCachesOpen = originalOpen;
+
+ const self = this;
+
+ caches.open = function (name: string): Promise {
+ self.onCacheStorageOpen(name);
+ return originalOpen(name);
+ };
+ }
+
private onStorageWrite(
key: string,
value: string,
@@ -143,6 +168,37 @@ export class StorageDetector implements Detector {
this.scheduleFlush();
}
+ private onCacheStorageOpen(name: string): void {
+ const reportKey = `cache_storage:${name}`;
+ if (this.reported.has(reportKey)) return;
+
+ this.reported.add(reportKey);
+ this.pending.set(reportKey, {
+ key: name,
+ storage_type: "cache_storage",
+ value_size: null,
+ });
+ this.scheduleFlush();
+ }
+
+ // scanCacheStorage enumerates pre-existing cache buckets created
+ // before the SDK loaded. Service workers commonly create their
+ // caches eagerly on `install`, so without this scan we would miss
+ // any cache bucket whose creation predates the banner script.
+ private scanCacheStorage(): void {
+ if (typeof caches === "undefined") return;
+ caches
+ .keys()
+ .then((names) => {
+ for (const name of names) {
+ this.onCacheStorageOpen(name);
+ }
+ })
+ .catch(() => {
+ // Insecure context or storage partition errors -- ignore.
+ });
+ }
+
private scanExisting(): void {
this.scanStorage(localStorage, "local_storage");
this.scanStorage(sessionStorage, "session_storage");
diff --git a/packages/cookie-banner/src/detectors/third-party-detector.ts b/packages/cookie-banner/src/detectors/third-party-detector.ts
index d0171bd45..b985003d6 100644
--- a/packages/cookie-banner/src/detectors/third-party-detector.ts
+++ b/packages/cookie-banner/src/detectors/third-party-detector.ts
@@ -24,7 +24,8 @@ type ResourceType =
| "font"
| "beacon"
| "fetch"
- | "media";
+ | "media"
+ | "service_worker";
interface DetectedResourceEntry {
url: string;
@@ -80,6 +81,7 @@ export class ThirdPartyDetector implements Detector {
private timer: ReturnType | null = null;
private observer: MutationObserver | null = null;
private perfObserver: PerformanceObserver | null = null;
+ private originalSWRegister: typeof ServiceWorkerContainer.prototype.register | null = null;
constructor(baseUrl: URL, bannerId: string) {
this.reportUrl = new URL(`${bannerId}/report`, baseUrl);
@@ -91,6 +93,8 @@ export class ThirdPartyDetector implements Detector {
this.scanExisting();
this.observeMutations();
this.observePerformance();
+ this.wrapServiceWorker();
+ this.scanServiceWorkers();
}
stop(): void {
@@ -112,6 +116,15 @@ export class ThirdPartyDetector implements Detector {
this.perfObserver.disconnect();
this.perfObserver = null;
}
+
+ if (
+ this.originalSWRegister
+ && typeof navigator !== "undefined"
+ && navigator.serviceWorker
+ ) {
+ navigator.serviceWorker.register = this.originalSWRegister;
+ this.originalSWRegister = null;
+ }
}
private scanExisting(): void {
@@ -179,6 +192,50 @@ export class ThirdPartyDetector implements Detector {
}
}
+ // wrapServiceWorker intercepts navigator.serviceWorker.register so
+ // each registration -- even ones initiated by third-party SDKs --
+ // surfaces as a tracker_resource entry keyed on the worker script
+ // origin+path.
+ private wrapServiceWorker(): void {
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return;
+
+ const sw = navigator.serviceWorker;
+ const originalRegister = sw.register.bind(sw);
+ this.originalSWRegister = originalRegister;
+
+ const self = this;
+
+ sw.register = function (
+ scriptURL: string | URL,
+ options?: RegistrationOptions,
+ ): Promise {
+ const url = typeof scriptURL === "string" ? scriptURL : scriptURL.toString();
+ self.processResource(url, "service_worker");
+ return originalRegister(scriptURL, options);
+ };
+ }
+
+ // scanServiceWorkers enumerates registrations that pre-date the SDK
+ // (e.g. installed on a previous visit, restored from cache).
+ private scanServiceWorkers(): void {
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) return;
+
+ navigator.serviceWorker
+ .getRegistrations()
+ .then((registrations) => {
+ for (const r of registrations) {
+ const url
+ = r.active?.scriptURL
+ ?? r.installing?.scriptURL
+ ?? r.waiting?.scriptURL;
+ if (url) this.processResource(url, "service_worker");
+ }
+ })
+ .catch(() => {
+ // Some browsers throw NotSupportedError in insecure contexts.
+ });
+ }
+
private processResource(src: string, resourceType: ResourceType): void {
if (EXTENSION_URL_RE.test(src)) return;
diff --git a/pkg/cmd/tracker-resource/create/create.go b/pkg/cmd/tracker-resource/create/create.go
index 0c428c929..838da54c4 100644
--- a/pkg/cmd/tracker-resource/create/create.go
+++ b/pkg/cmd/tracker-resource/create/create.go
@@ -96,6 +96,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
huh.NewOption("Beacon", "BEACON"),
huh.NewOption("Fetch / XHR", "FETCH"),
huh.NewOption("Media", "MEDIA"),
+ huh.NewOption("Service Worker", "SERVICE_WORKER"),
).
Value(&flagResourceType).Run(); err != nil {
return err
@@ -161,7 +162,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().StringVar(&flagCategoryID, "category-id", "", "Cookie category ID (required)")
_ = cmd.MarkFlagRequired("category-id")
- cmd.Flags().StringVar(&flagResourceType, "resource-type", "", "Resource type: SCRIPT, IFRAME, IMAGE, STYLESHEET, FONT, BEACON, FETCH or MEDIA (required)")
+ cmd.Flags().StringVar(&flagResourceType, "resource-type", "", "Resource type: SCRIPT, IFRAME, IMAGE, STYLESHEET, FONT, BEACON, FETCH, MEDIA or SERVICE_WORKER (required)")
cmd.Flags().StringVar(&flagOrigin, "origin", "", "Origin URL (required)")
cmd.Flags().StringVar(&flagPath, "path", "", "Resource path (required)")
cmd.Flags().StringVar(&flagDisplayName, "display-name", "", "Display name (required)")
diff --git a/pkg/coredata/migrations/20260511T064746Z.sql b/pkg/coredata/migrations/20260511T064746Z.sql
new file mode 100644
index 000000000..54bdc4aa7
--- /dev/null
+++ b/pkg/coredata/migrations/20260511T064746Z.sql
@@ -0,0 +1,21 @@
+-- Copyright (c) 2026 Probo Inc .
+--
+-- 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.
+
+-- A registered service worker is a URL-shaped artifact, so it lives
+-- in tracker_resources alongside scripts/iframes.
+ALTER TYPE tracker_resource_type ADD VALUE IF NOT EXISTS 'SERVICE_WORKER';
+
+-- A Cache Storage bucket is just a named string identifier with no
+-- URL, so it lives in detected_trackers like the other storage types.
+ALTER TYPE tracker_type ADD VALUE IF NOT EXISTS 'CACHE_STORAGE';
diff --git a/pkg/coredata/tracker_resource_type.go b/pkg/coredata/tracker_resource_type.go
index d23a1d1b9..f43a294be 100644
--- a/pkg/coredata/tracker_resource_type.go
+++ b/pkg/coredata/tracker_resource_type.go
@@ -22,14 +22,15 @@ import (
type TrackerResourceType string
const (
- TrackerResourceTypeScript TrackerResourceType = "SCRIPT"
- TrackerResourceTypeIframe TrackerResourceType = "IFRAME"
- TrackerResourceTypeImage TrackerResourceType = "IMAGE"
- TrackerResourceTypeStylesheet TrackerResourceType = "STYLESHEET"
- TrackerResourceTypeFont TrackerResourceType = "FONT"
- TrackerResourceTypeBeacon TrackerResourceType = "BEACON"
- TrackerResourceTypeFetch TrackerResourceType = "FETCH"
- TrackerResourceTypeMedia TrackerResourceType = "MEDIA"
+ TrackerResourceTypeScript TrackerResourceType = "SCRIPT"
+ TrackerResourceTypeIframe TrackerResourceType = "IFRAME"
+ TrackerResourceTypeImage TrackerResourceType = "IMAGE"
+ TrackerResourceTypeStylesheet TrackerResourceType = "STYLESHEET"
+ TrackerResourceTypeFont TrackerResourceType = "FONT"
+ TrackerResourceTypeBeacon TrackerResourceType = "BEACON"
+ TrackerResourceTypeFetch TrackerResourceType = "FETCH"
+ TrackerResourceTypeMedia TrackerResourceType = "MEDIA"
+ TrackerResourceTypeServiceWorker TrackerResourceType = "SERVICE_WORKER"
)
func TrackerResourceTypes() []TrackerResourceType {
@@ -42,6 +43,7 @@ func TrackerResourceTypes() []TrackerResourceType {
TrackerResourceTypeBeacon,
TrackerResourceTypeFetch,
TrackerResourceTypeMedia,
+ TrackerResourceTypeServiceWorker,
}
}
@@ -77,6 +79,8 @@ func (s *TrackerResourceType) Scan(value any) error {
*s = TrackerResourceTypeFetch
case TrackerResourceTypeMedia:
*s = TrackerResourceTypeMedia
+ case TrackerResourceTypeServiceWorker:
+ *s = TrackerResourceTypeServiceWorker
default:
return fmt.Errorf("invalid TrackerResourceType value: %q", v)
}
@@ -92,7 +96,8 @@ func (s TrackerResourceType) Value() (driver.Value, error) {
TrackerResourceTypeFont,
TrackerResourceTypeBeacon,
TrackerResourceTypeFetch,
- TrackerResourceTypeMedia:
+ TrackerResourceTypeMedia,
+ TrackerResourceTypeServiceWorker:
return string(s), nil
default:
return nil, fmt.Errorf("invalid TrackerResourceType: %s", s)
diff --git a/pkg/coredata/tracker_type.go b/pkg/coredata/tracker_type.go
index feca1f145..876c7b4b7 100644
--- a/pkg/coredata/tracker_type.go
+++ b/pkg/coredata/tracker_type.go
@@ -26,6 +26,7 @@ const (
TrackerTypeLocalStorage TrackerType = "LOCAL_STORAGE"
TrackerTypeSessionStorage TrackerType = "SESSION_STORAGE"
TrackerTypeIndexedDB TrackerType = "INDEXED_DB"
+ TrackerTypeCacheStorage TrackerType = "CACHE_STORAGE"
)
func TrackerTypes() []TrackerType {
@@ -34,6 +35,7 @@ func TrackerTypes() []TrackerType {
TrackerTypeLocalStorage,
TrackerTypeSessionStorage,
TrackerTypeIndexedDB,
+ TrackerTypeCacheStorage,
}
}
@@ -61,6 +63,8 @@ func (s *TrackerType) Scan(value any) error {
*s = TrackerTypeSessionStorage
case TrackerTypeIndexedDB:
*s = TrackerTypeIndexedDB
+ case TrackerTypeCacheStorage:
+ *s = TrackerTypeCacheStorage
default:
return fmt.Errorf("invalid TrackerType value: %q", v)
}
@@ -72,7 +76,8 @@ func (s TrackerType) Value() (driver.Value, error) {
case TrackerTypeCookie,
TrackerTypeLocalStorage,
TrackerTypeSessionStorage,
- TrackerTypeIndexedDB:
+ TrackerTypeIndexedDB,
+ TrackerTypeCacheStorage:
return string(s), nil
default:
return nil, fmt.Errorf("invalid TrackerType: %s", s)
diff --git a/pkg/server/api/console/v1/graphql/cookie_banner.graphql b/pkg/server/api/console/v1/graphql/cookie_banner.graphql
index 3a9842479..b7541a3e5 100644
--- a/pkg/server/api/console/v1/graphql/cookie_banner.graphql
+++ b/pkg/server/api/console/v1/graphql/cookie_banner.graphql
@@ -52,6 +52,10 @@ enum TrackerType
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerTypeIndexedDB"
)
+ CACHE_STORAGE
+ @goEnum(
+ value: "go.probo.inc/probo/pkg/coredata.TrackerTypeCacheStorage"
+ )
}
enum CookieBannerOrderField
@@ -332,6 +336,10 @@ enum TrackerResourceType
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeMedia"
)
+ SERVICE_WORKER
+ @goEnum(
+ value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeServiceWorker"
+ )
}
enum TrackerResourceOrderField
diff --git a/pkg/server/api/cookiebanner/v1/handler.go b/pkg/server/api/cookiebanner/v1/handler.go
index 918066394..3956a31b3 100644
--- a/pkg/server/api/cookiebanner/v1/handler.go
+++ b/pkg/server/api/cookiebanner/v1/handler.go
@@ -423,6 +423,8 @@ func (h *Handler) handleReportDetectedTrackers(w http.ResponseWriter, r *http.Re
storageType = coredata.TrackerTypeSessionStorage
case "indexed_db":
storageType = coredata.TrackerTypeIndexedDB
+ case "cache_storage":
+ storageType = coredata.TrackerTypeCacheStorage
default:
continue
}
@@ -457,6 +459,8 @@ func (h *Handler) handleReportDetectedTrackers(w http.ResponseWriter, r *http.Re
resourceType = coredata.TrackerResourceTypeFetch
case "media":
resourceType = coredata.TrackerResourceTypeMedia
+ case "service_worker":
+ resourceType = coredata.TrackerResourceTypeServiceWorker
default:
continue
}
diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml
index 9f2a3a393..d27f2abce 100644
--- a/pkg/server/api/mcp/v1/specification.yaml
+++ b/pkg/server/api/mcp/v1/specification.yaml
@@ -9450,7 +9450,7 @@ components:
description: Cookie category ID
tracker_type:
type: string
- enum: [COOKIE, LOCAL_STORAGE, SESSION_STORAGE, INDEXED_DB]
+ enum: [COOKIE, LOCAL_STORAGE, SESSION_STORAGE, INDEXED_DB, CACHE_STORAGE]
description: Type of tracker
pattern:
type: string
@@ -9522,7 +9522,7 @@ components:
description: Cookie category ID
resource_type:
type: string
- enum: [SCRIPT, IFRAME, IMAGE, STYLESHEET, FONT, BEACON, FETCH, MEDIA]
+ enum: [SCRIPT, IFRAME, IMAGE, STYLESHEET, FONT, BEACON, FETCH, MEDIA, SERVICE_WORKER]
description: Type of tracked resource
origin:
type: string
@@ -10046,7 +10046,7 @@ components:
$ref: "#/components/schemas/GID"
tracker_type:
type: string
- enum: [COOKIE, LOCAL_STORAGE, SESSION_STORAGE, INDEXED_DB]
+ enum: [COOKIE, LOCAL_STORAGE, SESSION_STORAGE, INDEXED_DB, CACHE_STORAGE]
pattern:
type: string
match_type:
@@ -10186,7 +10186,7 @@ components:
$ref: "#/components/schemas/GID"
resource_type:
type: string
- enum: [SCRIPT, IFRAME, IMAGE, STYLESHEET, FONT, BEACON, FETCH, MEDIA]
+ enum: [SCRIPT, IFRAME, IMAGE, STYLESHEET, FONT, BEACON, FETCH, MEDIA, SERVICE_WORKER]
origin:
type: string
path: