Initialize VG-Environment from stable VG-IV baseline

Dieser Commit ist enthalten in:
2026-05-21 19:58:08 +02:00
Commit a1a8147ae2
27 geänderte Dateien mit 3981 neuen und 0 gelöschten Zeilen
+39
Datei anzeigen
@@ -0,0 +1,39 @@
# OS
Thumbs.db
Desktop.ini
# VS Code / Codium
.vscode/
# Node / tooling
node_modules/
# Build / package artifacts
dist/
build/
*.zip
# Logs
*.log
# Temporary exports / captures
exports/
captures/
har/
*.har
# Local test data
tmp/
temp/
# Firefox extension debug artifacts
web-ext-artifacts/
# SQLite / DB dumps
*.sqlite
*.sqlite3
*.db
# Environment files
.env
.env.*
Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 1.4 MiB

+68
Datei anzeigen
@@ -0,0 +1,68 @@
{
"manifest_version": 3,
"name": "VendorGet",
"version": "4.0.0",
"description": "Local TCF/CMP transparency and evidence tool",
"permissions": [
"storage",
"tabs",
"cookies",
"scripting",
"webRequest"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"scripts": [
"src/background/db/db-constants.js",
"src/background/db/db-core.js",
"src/background/gvl/gvl-vendor-normalizer.js",
"src/background/gvl/gvl-vendor-relationship-normalizer.js",
"src/background/gvl/gvl-catalog-normalizer.js",
"src/background/db/db-retention.js",
"src/background/db/db-record-locks.js",
"src/background/utils.js",
"src/background/settings.js",
"src/background/maintenance-guard.js",
"src/background/consent-memory.js",
"src/background/request-fingerprint.js",
"src/background/request-observer.js",
"src/background/gvl-service.js",
"src/background.js"
]
},
"action": {
"default_title": "VendorGet-IV",
"default_popup": "src/popup/popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": [
"src/content/tcf-listener.js"
],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
{
"resources": [
"src/injected/tcf-bridge.js"
],
"matches": ["<all_urls>"]
}
],
"browser_specific_settings": {
"gecko": {
"id": "vendorget@local"
}
}
}
+569
Datei anzeigen
@@ -0,0 +1,569 @@
console.log("VendorGet-IV background loaded");
const OFFICIAL_IAB_GVL_URL =
"https://vendor-list.consensu.org/v3/vendor-list.json";
const EVIDENCE_RECORDING_SOURCE = "vendorget_background_mirror";
browser.runtime.onMessage.addListener((message, sender) =>
handleVendorGetMessage(message, sender)
);
browser.webRequest.onBeforeRequest.addListener(
handleObservedRequest,
{ urls: ["<all_urls>"] }
);
async function handleVendorGetMessage(message, sender) {
if (!message) {
return null;
}
if (message.type === "gvl_import_json") {
return handleGvlImportJsonMessage(message);
}
if (message.type === "fetch_official_gvl") {
return handleFetchOfficialGvlMessage();
}
if (message.type === "start_evidence_maintenance_session") {
return startEvidenceMaintenanceSession(message?.payload?.source);
}
if (message.type === "refresh_evidence_maintenance_session") {
return refreshEvidenceMaintenanceSession(message?.payload?.source);
}
if (message.type === "end_evidence_maintenance_session") {
return endEvidenceMaintenanceSession(message?.payload?.source);
}
if (message.type === "get_evidence_maintenance_status") {
return getEvidenceMaintenanceStatus();
}
if (message.type === "get_evidence_retention_status") {
return handleGetEvidenceRetentionStatusMessage();
}
if (message.type === "purge_unlocked_evidence_records") {
return handlePurgeUnlockedEvidenceRecordsMessage();
}
if (message.type === "delete_all_evidence_database") {
return handleDeleteAllEvidenceDatabaseMessage();
}
if (message.type === "lock_all_evidence_records") {
return lockAllEvidenceRecords(
message?.payload?.reason ?? "dsar_used",
message?.payload?.note ?? null
);
}
if (message.type === "unlock_all_evidence_records") {
return unlockAllEvidenceRecords();
}
if (message.type !== "vendorget_capture") {
return;
}
if (!(await isConsentCaptureEnabled())) {
return;
}
const eventName = message?.payload?.eventName ?? null;
const tabId = sender?.tab?.id ?? null;
if (eventName === "tcf_ping") {
const pingData = message?.payload?.payload?.data ?? null;
if (tabId !== null && pingData) {
rememberLatestTcfPing(tabId, pingData);
}
console.log("VendorGet-IV tcf ping", {
payload: message.payload.payload,
sender
});
return;
}
if (eventName !== "consent_capture") {
console.log("VendorGet-IV ignored event", message);
return;
}
if (isEvidenceWriteSuspended()) {
console.info("VendorGet-IV evidence write skipped: maintenance mode");
return;
}
const latestPingData = tabId !== null ? getLatestTcfPing(tabId) : null;
const consentState = buildConsentStateV1(
message.payload.payload,
sender,
latestPingData
);
consentState.stateFingerprint = await sha256Hex(
stableStringify(consentState.fingerprintSource)
);
rememberLatestConsentState(consentState);
const result = await persistConsentState(
consentState,
message.payload.payload?.rawTcData ?? null
);
console.log("VendorGet-IV consent state persisted", result);
}
async function handleGetEvidenceRetentionStatusMessage() {
const db = await openVendorGetDb();
const totalCount = await countEvidenceRecords(db);
const lockedCount = await countLockedEvidenceRecords(db);
const storeCounts = await getEvidenceStoreCounts(db);
return {
success: true,
totalCount,
lockedCount,
unlockedCount: totalCount - lockedCount,
storeCounts
};
}
async function handlePurgeUnlockedEvidenceRecordsMessage() {
const db = await openVendorGetDb();
return purgeUnlockedEvidenceRecords(db);
}
function handleDeleteAllEvidenceDatabaseMessage() {
return deleteVendorGetDatabase();
}
async function handleGvlImportJsonMessage(message) {
const rawJson = message?.payload?.rawJson ?? null;
if (!isGvlImportCandidate(rawJson)) {
return {
success: false,
error: "invalid_gvl_json"
};
}
const db = await openVendorGetDb();
const result = await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
sourceUrl: message?.payload?.sourceUrl ?? null,
diagnostics: {
importSource: "local_file"
}
});
return {
success: true,
alreadyKnown: result.alreadyKnown,
vendorListVersion: result.snapshot.vendorListVersion,
sha256: result.snapshot.sha256
};
}
function isGvlImportCandidate(value) {
return (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
value.vendorListVersion !== undefined &&
value.vendors &&
typeof value.vendors === "object" &&
!Array.isArray(value.vendors)
);
}
async function handleFetchOfficialGvlMessage() {
try {
const response = await fetch(OFFICIAL_IAB_GVL_URL, {
method: "GET",
cache: "no-store"
});
if (!response.ok) {
return {
success: false,
error: "official_gvl_fetch_failed",
responseStatus: response.status
};
}
const rawJson = await response.json();
if (!isGvlImportCandidate(rawJson)) {
return {
success: false,
error: "invalid_gvl_json",
responseStatus: response.status
};
}
const db = await openVendorGetDb();
const result = await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
sourceUrl: OFFICIAL_IAB_GVL_URL,
fetchedAt: new Date().toISOString(),
diagnostics: {
ingestionSource: "official_iab_fetch",
responseStatus: response.status
}
});
return {
success: true,
alreadyKnown: result.alreadyKnown,
vendorListVersion: result.snapshot.vendorListVersion,
sha256: result.snapshot.sha256
};
} catch (error) {
console.warn("VendorGet-IV official GVL fetch failed", error);
return {
success: false,
error: "official_gvl_fetch_failed"
};
}
}
function buildConsentStateV1(rawCapture, sender, latestPingData) {
const decodedTcString = decodeTcStringCoreMetadata(rawCapture?.tcString ?? null);
// Firefox/CMP/browser storage remains the consent source of truth; VG-IV
// stores an evidentiary mirror of the observed TCF state.
const state = {
schemaVersion: 1,
capturedAt: new Date().toISOString(),
page: {
url: rawCapture?.url ?? sender?.tab?.url ?? sender?.url ?? null,
origin: rawCapture?.origin ?? sender?.origin ?? null,
tabId: sender?.tab?.id ?? null,
frameId: sender?.frameId ?? null,
incognito: sender?.tab?.incognito ?? null,
cookieStoreId: sender?.tab?.cookieStoreId ?? null
},
cmp: {
cmpId:
rawCapture?.cmpId ??
decodedTcString?.cmpId ??
latestPingData?.cmpId ??
null,
cmpVersion:
rawCapture?.cmpVersion ??
decodedTcString?.cmpVersion ??
latestPingData?.cmpVersion ??
null,
tcfPolicyVersion:
rawCapture?.tcfPolicyVersion ??
decodedTcString?.tcfPolicyVersion ??
latestPingData?.tcfPolicyVersion ??
null,
gdprApplies: rawCapture?.gdprApplies ?? latestPingData?.gdprApplies ?? null,
isServiceSpecific:
rawCapture?.isServiceSpecific ??
decodedTcString?.isServiceSpecific ??
null,
useNonStandardTexts: rawCapture?.useNonStandardTexts ?? null,
publisherCC:
rawCapture?.publisherCC ??
decodedTcString?.publisherCC ??
null,
purposeOneTreatment:
rawCapture?.purposeOneTreatment ??
decodedTcString?.purposeOneTreatment ??
null
},
observation: {
eventStatus: rawCapture?.eventStatus ?? null,
cmpStatus: rawCapture?.cmpStatus ?? latestPingData?.cmpStatus ?? null
},
gvl: {
vendorListVersion:
rawCapture?.vendorListVersion ??
rawCapture?.gvlVersion ??
latestPingData?.gvlVersion ??
latestPingData?.vendorListVersion ??
decodedTcString?.vendorListVersion ??
null
},
consent: {
tcString: rawCapture?.tcString ?? null,
addtlConsent: rawCapture?.addtlConsent ?? null
},
purposes: {
consents: rawCapture?.purpose?.consents ?? {},
legitimateInterests: rawCapture?.purpose?.legitimateInterests ?? {}
},
vendors: {
consents: rawCapture?.vendor?.consents ?? {},
legitimateInterests: rawCapture?.vendor?.legitimateInterests ?? {},
disclosedVendors:
rawCapture?.vendor?.disclosedVendors ??
rawCapture?.disclosedVendors ??
{}
},
specialFeatureOptins: rawCapture?.specialFeatureOptins ?? {},
publisher: {
restrictions: rawCapture?.publisher?.restrictions ?? {},
consents: rawCapture?.publisher?.consents ?? {},
legitimateInterests: rawCapture?.publisher?.legitimateInterests ?? {}
},
diagnostics: {
bridgeTimestampUtc: rawCapture?.timestampUtc ?? null,
rawTopLevelKeys: Object.keys(rawCapture ?? {}),
decodedTcStringCore: decodedTcString,
latestPingData: latestPingData
},
fingerprintSource: null,
stateFingerprint: null
};
state.fingerprintSource = buildFingerprintSource(state);
return state;
}
function buildFingerprintSource(consentState) {
return {
cmp: consentState.cmp,
gvl: consentState.gvl,
consent: consentState.consent,
purposes: consentState.purposes,
vendors: consentState.vendors,
specialFeatureOptins: consentState.specialFeatureOptins,
publisher: consentState.publisher
};
}
async function persistConsentState(consentState, rawTcData) {
const db = await openVendorGetDb();
const now = new Date().toISOString();
return new Promise((resolve, reject) => {
const tx = db.transaction(["consent_states", "consent_events"], "readwrite");
const statesStore = tx.objectStore("consent_states");
const eventsStore = tx.objectStore("consent_events");
const getRequest = statesStore.get(consentState.stateFingerprint);
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
const existingState = getRequest.result;
if (existingState) {
existingState.recordedAt =
existingState.recordedAt ?? existingState.createdAt ?? now;
existingState.recordingSource =
existingState.recordingSource ?? EVIDENCE_RECORDING_SOURCE;
existingState.lastSeenAt = now;
existingState.seenCount = (existingState.seenCount ?? 1) + 1;
existingState.updatedAt = now;
statesStore.put(existingState);
eventsStore.add({
eventType: "duplicate_state",
capturedAt: consentState.capturedAt,
recordedAt: now,
recordingSource: EVIDENCE_RECORDING_SOURCE,
stateFingerprint: consentState.stateFingerprint,
page: consentState.page,
rawEventName: "consent_capture",
rawTcData: rawTcData,
diagnostics: consentState.diagnostics
});
resolve({
action: "duplicate_state_updated",
stateFingerprint: consentState.stateFingerprint,
seenCount: existingState.seenCount
});
return;
}
const newStateRecord = {
...consentState,
recordedAt: now,
recordingSource: EVIDENCE_RECORDING_SOURCE,
firstSeenAt: now,
lastSeenAt: now,
seenCount: 1,
createdAt: now,
updatedAt: now
};
statesStore.add(newStateRecord);
eventsStore.add({
eventType: "new_state",
capturedAt: consentState.capturedAt,
recordedAt: now,
recordingSource: EVIDENCE_RECORDING_SOURCE,
stateFingerprint: consentState.stateFingerprint,
page: consentState.page,
rawEventName: "consent_capture",
rawTcData: rawTcData,
diagnostics: consentState.diagnostics
});
resolve({
action: "new_state_inserted",
stateFingerprint: consentState.stateFingerprint,
seenCount: 1
});
};
tx.onerror = () => reject(tx.error);
});
}
async function persistObservedRequest(observedRequest) {
const db = await openVendorGetDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(["observed_requests"], "readwrite");
const requestsStore = tx.objectStore("observed_requests");
const getRequest = requestsStore.get(observedRequest.requestFingerprint);
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
const existingRequest = getRequest.result;
if (existingRequest) {
const updatedRequest = {
...existingRequest,
requestFingerprintSource: observedRequest.requestFingerprintSource,
recordedAt: existingRequest.recordedAt ?? observedRequest.recordedAt,
recordingSource:
existingRequest.recordingSource ?? observedRequest.recordingSource,
lastSeenAt: observedRequest.lastSeenAt,
seenCount: (existingRequest.seenCount ?? 1) + 1,
request: observedRequest.request,
consentParams: observedRequest.consentParams,
context: observedRequest.context,
correlation: observedRequest.correlation
};
requestsStore.put(updatedRequest);
resolve({
action: "observed_request_updated",
requestFingerprint: observedRequest.requestFingerprint,
seenCount: updatedRequest.seenCount
});
return;
}
requestsStore.add(observedRequest);
resolve({
action: "observed_request_inserted",
requestFingerprint: observedRequest.requestFingerprint,
seenCount: observedRequest.seenCount
});
};
tx.onerror = () => reject(tx.error);
});
}
function decodeTcStringCoreMetadata(tcString) {
if (!tcString || typeof tcString !== "string") {
return null;
}
const coreSegment = tcString.split(".")[0];
try {
const bits = base64UrlToBits(coreSegment);
return {
version: bitsToInt(bits, 0, 6),
cmpId: bitsToInt(bits, 78, 12),
cmpVersion: bitsToInt(bits, 90, 12),
consentScreen: bitsToInt(bits, 102, 6),
consentLanguage: bitsToString(bits, 108, 12),
vendorListVersion: bitsToInt(bits, 120, 12),
tcfPolicyVersion: bitsToInt(bits, 132, 6),
isServiceSpecific: bitsToBoolean(bits, 138),
useNonStandardStacks: bitsToBoolean(bits, 139),
purposeOneTreatment: bitsToBoolean(bits, 200),
publisherCC: bitsToString(bits, 201, 12)
};
} catch (error) {
console.warn("VendorGet-IV could not decode TC string core metadata", error);
return null;
}
}
function base64UrlToBits(value) {
const base64 = value
.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(Math.ceil(value.length / 4) * 4, "=");
const binary = atob(base64);
let bits = "";
for (let index = 0; index < binary.length; index += 1) {
bits += binary
.charCodeAt(index)
.toString(2)
.padStart(8, "0");
}
return bits;
}
function bitsToInt(bits, start, length) {
return parseInt(bits.slice(start, start + length), 2);
}
function bitsToBoolean(bits, index) {
return bits[index] === "1";
}
function bitsToString(bits, start, length) {
let result = "";
for (let index = start; index < start + length; index += 6) {
const charCode = bitsToInt(bits, index, 6) + 65;
result += String.fromCharCode(charCode);
}
return result;
}
+46
Datei anzeigen
@@ -0,0 +1,46 @@
const latestTcfPingByTab = new Map();
const latestConsentStateByTabFrame = new Map();
function rememberLatestTcfPing(tabId, pingData) {
latestTcfPingByTab.set(tabId, pingData);
}
function getLatestTcfPing(tabId) {
return latestTcfPingByTab.get(tabId) ?? null;
}
function rememberLatestConsentState(consentState) {
const key = buildTabFrameKey(
consentState?.page?.tabId,
consentState?.page?.frameId
);
if (key === null) {
return;
}
latestConsentStateByTabFrame.set(key, {
stateFingerprint: consentState.stateFingerprint,
capturedAt: consentState.capturedAt,
tabId: consentState.page.tabId,
frameId: consentState.page.frameId
});
}
function getLatestConsentStateForRequest(tabId, frameId) {
const key = buildTabFrameKey(tabId, frameId);
if (key === null) {
return null;
}
return latestConsentStateByTabFrame.get(key) ?? null;
}
function buildTabFrameKey(tabId, frameId) {
if (tabId === null || tabId === undefined || frameId === null || frameId === undefined) {
return null;
}
return `${tabId}:${frameId}`;
}
+34
Datei anzeigen
@@ -0,0 +1,34 @@
"use strict";
const VENDORGET_DB_NAME = "vendorget_iv";
const VENDORGET_DB_VERSION = 5;
const VENDORGET_STORE_NAMES = {
consentStates: "consent_states",
consentEvents: "consent_events",
observedRequests: "observed_requests",
gvlSnapshots: "gvl_snapshots",
gvlSnapshotEvents: "gvl_snapshot_events",
gvlVendors: "gvl_vendors",
gvlPurposes: "gvl_purposes",
gvlSpecialPurposes: "gvl_special_purposes",
gvlFeatures: "gvl_features",
gvlSpecialFeatures: "gvl_special_features",
gvlDataCategories: "gvl_data_categories",
gvlVendorRelationships: "gvl_vendor_relationships"
};
const VENDORGET_EVIDENCE_STORE_NAMES = [
VENDORGET_STORE_NAMES.consentStates,
VENDORGET_STORE_NAMES.consentEvents,
VENDORGET_STORE_NAMES.observedRequests,
VENDORGET_STORE_NAMES.gvlSnapshots,
VENDORGET_STORE_NAMES.gvlSnapshotEvents,
VENDORGET_STORE_NAMES.gvlVendors,
VENDORGET_STORE_NAMES.gvlPurposes,
VENDORGET_STORE_NAMES.gvlSpecialPurposes,
VENDORGET_STORE_NAMES.gvlFeatures,
VENDORGET_STORE_NAMES.gvlSpecialFeatures,
VENDORGET_STORE_NAMES.gvlDataCategories,
VENDORGET_STORE_NAMES.gvlVendorRelationships
];
+204
Datei anzeigen
@@ -0,0 +1,204 @@
"use strict";
let vendorGetDbPromise = null;
function openVendorGetDb() {
if (vendorGetDbPromise) {
return vendorGetDbPromise;
}
vendorGetDbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(VENDORGET_DB_NAME, VENDORGET_DB_VERSION);
request.onerror = () => reject(request.error);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains("consent_states")) {
const consentStates = db.createObjectStore("consent_states", {
keyPath: "stateFingerprint"
});
consentStates.createIndex("pageOrigin", "page.origin", { unique: false });
consentStates.createIndex("firstSeenAt", "firstSeenAt", { unique: false });
consentStates.createIndex("lastSeenAt", "lastSeenAt", { unique: false });
}
if (!db.objectStoreNames.contains("consent_events")) {
const consentEvents = db.createObjectStore("consent_events", {
keyPath: "id",
autoIncrement: true
});
consentEvents.createIndex("stateFingerprint", "stateFingerprint", {
unique: false
});
consentEvents.createIndex("eventType", "eventType", { unique: false });
consentEvents.createIndex("capturedAt", "capturedAt", { unique: false });
consentEvents.createIndex("pageOrigin", "page.origin", { unique: false });
}
if (!db.objectStoreNames.contains("observed_requests")) {
const observedRequests = db.createObjectStore("observed_requests", {
keyPath: "requestFingerprint"
});
observedRequests.createIndex("lastSeenAt", "lastSeenAt", {
unique: false
});
observedRequests.createIndex("firstSeenAt", "firstSeenAt", {
unique: false
});
observedRequests.createIndex("requestOrigin", "request.origin", {
unique: false
});
observedRequests.createIndex("requestType", "request.type", {
unique: false
});
observedRequests.createIndex("requestThirdParty", "request.thirdParty", {
unique: false
});
observedRequests.createIndex(
"correlationStateFingerprint",
"correlation.stateFingerprint",
{ unique: false }
);
}
ensureGvlStores(db);
};
request.onsuccess = () => resolve(request.result);
});
return vendorGetDbPromise;
}
async function closeVendorGetDb() {
if (!vendorGetDbPromise) {
return;
}
try {
const db = await vendorGetDbPromise;
db.close();
} finally {
vendorGetDbPromise = null;
}
}
async function deleteVendorGetDatabase() {
await closeVendorGetDb();
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(VENDORGET_DB_NAME);
request.onerror = () => reject(request.error);
request.onblocked = () => reject(new Error("database_delete_blocked"));
request.onsuccess = () => {
resolve({
success: true
});
};
});
}
function ensureGvlStores(db) {
if (!db.objectStoreNames.contains("gvl_snapshots")) {
const gvlSnapshots = db.createObjectStore("gvl_snapshots", {
keyPath: "sha256"
});
gvlSnapshots.createIndex("gvlRevision", "gvlRevision", { unique: false });
gvlSnapshots.createIndex("vendorListVersion", "vendorListVersion", {
unique: false
});
gvlSnapshots.createIndex("fetchedAt", "fetchedAt", { unique: false });
gvlSnapshots.createIndex("sourceUrl", "sourceUrl", { unique: false });
}
if (!db.objectStoreNames.contains("gvl_snapshot_events")) {
const gvlSnapshotEvents = db.createObjectStore("gvl_snapshot_events", {
keyPath: "id",
autoIncrement: true
});
gvlSnapshotEvents.createIndex("eventType", "eventType", { unique: false });
gvlSnapshotEvents.createIndex("capturedAt", "capturedAt", { unique: false });
gvlSnapshotEvents.createIndex("gvlRevision", "gvlRevision", {
unique: false
});
gvlSnapshotEvents.createIndex("vendorListVersion", "vendorListVersion", {
unique: false
});
gvlSnapshotEvents.createIndex("sha256", "sha256", { unique: false });
gvlSnapshotEvents.createIndex("sourceUrl", "sourceUrl", { unique: false });
}
ensureGvlRelationshipStores(db);
}
function ensureGvlRelationshipStores(db) {
const gvlRelationshipStoreDefinitions = [
{
name: "gvl_vendors",
indexes: [
"vendorListVersion",
"vendorId",
"name",
"policyUrl",
"deletedDate"
]
},
{
name: "gvl_purposes",
indexes: ["vendorListVersion", "purposeId", "name"]
},
{
name: "gvl_special_purposes",
indexes: ["vendorListVersion", "specialPurposeId", "name"]
},
{
name: "gvl_features",
indexes: ["vendorListVersion", "featureId", "name"]
},
{
name: "gvl_special_features",
indexes: ["vendorListVersion", "specialFeatureId", "name"]
},
{
name: "gvl_data_categories",
indexes: ["vendorListVersion", "dataCategoryId", "name"]
},
{
name: "gvl_vendor_relationships",
indexes: [
"vendorListVersion",
"vendorId",
"relationshipType",
"relatedId"
]
}
];
gvlRelationshipStoreDefinitions.forEach((storeDefinition) => {
if (db.objectStoreNames.contains(storeDefinition.name)) {
return;
}
const objectStore = db.createObjectStore(storeDefinition.name, {
keyPath: "id"
});
storeDefinition.indexes.forEach((indexName) => {
objectStore.createIndex(indexName, indexName, { unique: false });
});
});
}
+67
Datei anzeigen
@@ -0,0 +1,67 @@
"use strict";
async function lockAllEvidenceRecords(reason, note) {
const db = await openVendorGetDb();
const lockedAt = new Date().toISOString();
const recordLockReason = reason ?? "dsar_used";
const recordLockNote = note ?? null;
return updateAllEvidenceRecords((record) => ({
...record,
bolRecordLock: true,
recordLockReason,
recordLockedAt: lockedAt,
recordLockNote
}), "lockedCount", db);
}
async function unlockAllEvidenceRecords() {
const db = await openVendorGetDb();
const unlockedAt = new Date().toISOString();
return updateAllEvidenceRecords((record) => ({
...record,
bolRecordLock: false,
recordLockReason: null,
recordLockNote: null,
recordLockedAt: null,
recordUnlockedAt: unlockedAt
}), "unlockedCount", db);
}
function updateAllEvidenceRecords(buildUpdatedRecord, countKey, db) {
return new Promise((resolve, reject) => {
let updatedCount = 0;
const tx = db.transaction(VENDORGET_EVIDENCE_STORE_NAMES, "readwrite");
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => {
resolve({
success: true,
[countKey]: updatedCount
});
};
for (const storeName of VENDORGET_EVIDENCE_STORE_NAMES) {
const cursorRequest = tx.objectStore(storeName).openCursor();
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor) {
return;
}
const updateRequest = cursor.update(buildUpdatedRecord(cursor.value));
updateRequest.onerror = () => {
reject(updateRequest.error);
};
updateRequest.onsuccess = () => {
updatedCount += 1;
cursor.continue();
};
};
}
});
}
+118
Datei anzeigen
@@ -0,0 +1,118 @@
"use strict";
function countEvidenceRecords(db) {
return countRecordsInStores(db, VENDORGET_EVIDENCE_STORE_NAMES);
}
function countLockedEvidenceRecords(db) {
return countRecordsMatching(db, VENDORGET_EVIDENCE_STORE_NAMES, (record) => {
return record?.bolRecordLock === true;
});
}
function getEvidenceStoreCounts(db) {
return new Promise((resolve, reject) => {
const storeCounts = {};
const tx = db.transaction(VENDORGET_EVIDENCE_STORE_NAMES, "readonly");
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(storeCounts);
for (const storeName of VENDORGET_EVIDENCE_STORE_NAMES) {
const countRequest = tx.objectStore(storeName).count();
countRequest.onsuccess = () => {
storeCounts[storeName] = countRequest.result;
};
}
});
}
function purgeUnlockedEvidenceRecords(db) {
return new Promise((resolve, reject) => {
let deletedCount = 0;
let keptLockedCount = 0;
const tx = db.transaction(VENDORGET_EVIDENCE_STORE_NAMES, "readwrite");
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => {
resolve({
success: true,
deletedCount,
keptLockedCount
});
};
for (const storeName of VENDORGET_EVIDENCE_STORE_NAMES) {
const cursorRequest = tx.objectStore(storeName).openCursor();
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor) {
return;
}
if (cursor.value?.bolRecordLock === true) {
keptLockedCount += 1;
cursor.continue();
return;
}
deletedCount += 1;
cursor.delete();
cursor.continue();
};
}
});
}
function countRecordsInStores(db, storeNames) {
return new Promise((resolve, reject) => {
let totalCount = 0;
const tx = db.transaction(storeNames, "readonly");
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(totalCount);
for (const storeName of storeNames) {
const countRequest = tx.objectStore(storeName).count();
countRequest.onsuccess = () => {
totalCount += countRequest.result;
};
}
});
}
function countRecordsMatching(db, storeNames, predicate) {
return new Promise((resolve, reject) => {
let totalCount = 0;
const tx = db.transaction(storeNames, "readonly");
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(totalCount);
for (const storeName of storeNames) {
const cursorRequest = tx.objectStore(storeName).openCursor();
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor) {
return;
}
if (predicate(cursor.value)) {
totalCount += 1;
}
cursor.continue();
};
}
});
}
+156
Datei anzeigen
@@ -0,0 +1,156 @@
async function calculateGvlSnapshotSha256(rawJson) {
const stableValue = normalizeGvlSnapshotValueForHash(rawJson);
const data = new TextEncoder().encode(stableValue);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
async function buildGvlSnapshotRecord(rawJson, sourceUrl, fetchedAt) {
const gvlJson = normalizeGvlSnapshotValueForMetadata(rawJson);
return {
sha256: await calculateGvlSnapshotSha256(rawJson),
vendorListVersion: gvlJson?.vendorListVersion ?? null,
gvlSpecificationVersion: gvlJson?.gvlSpecificationVersion ?? null,
tcfPolicyVersion: gvlJson?.tcfPolicyVersion ?? null,
fetchedAt: fetchedAt,
sourceUrl: sourceUrl,
rawJson: rawJson,
vendorCount: countObjectEntries(gvlJson?.vendors),
purposeCount: countObjectEntries(gvlJson?.purposes),
// Existing GVL snapshots already use createdAt as the local mirror timestamp;
// keep that field instead of duplicating it as recordedAt.
createdAt: new Date().toISOString()
};
}
function storeGvlSnapshotIfNew(db, snapshot) {
return new Promise((resolve, reject) => {
const tx = db.transaction(["gvl_snapshots"], "readwrite");
const snapshotsStore = tx.objectStore("gvl_snapshots");
const getRequest = snapshotsStore.get(snapshot.sha256);
let result = null;
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
if (getRequest.result) {
result = {
stored: false,
sha256: snapshot.sha256,
vendorListVersion: snapshot.vendorListVersion
};
return;
}
snapshotsStore.add(snapshot);
result = {
stored: true,
sha256: snapshot.sha256,
vendorListVersion: snapshot.vendorListVersion
};
};
tx.onerror = () => reject(tx.error);
tx.oncomplete = () => resolve(result);
});
}
function recordGvlSnapshotEvent(db, event) {
return new Promise((resolve, reject) => {
const tx = db.transaction(["gvl_snapshot_events"], "readwrite");
const eventsStore = tx.objectStore("gvl_snapshot_events");
const addRequest = eventsStore.add({
eventType: event.eventType,
capturedAt: event.capturedAt ?? new Date().toISOString(),
vendorListVersion: event.vendorListVersion,
sha256: event.sha256,
sourceUrl: event.sourceUrl,
diagnostics: event.diagnostics ?? null
});
let eventId = null;
addRequest.onerror = () => reject(addRequest.error);
addRequest.onsuccess = () => {
eventId = addRequest.result;
};
tx.onerror = () => reject(tx.error);
tx.oncomplete = () => resolve(eventId);
});
}
async function ingestGvlSnapshot(db, rawJson, options = {}) {
const snapshot = await buildGvlSnapshotRecord(
rawJson,
options.sourceUrl ?? null,
options.fetchedAt ?? null
);
const storeResult = await storeGvlSnapshotIfNew(db, snapshot);
const alreadyKnown = !storeResult.stored;
await recordGvlSnapshotEvent(db, {
eventType: alreadyKnown
? "gvl_snapshot_already_known"
: "gvl_snapshot_ingested",
capturedAt: new Date().toISOString(),
vendorListVersion: snapshot.vendorListVersion,
sha256: snapshot.sha256,
sourceUrl: snapshot.sourceUrl,
diagnostics: {
ingestionSource: "manual_pipeline",
...(options.diagnostics ?? {}),
alreadyKnown: alreadyKnown
}
});
return {
snapshot: snapshot,
storeResult: storeResult,
alreadyKnown: alreadyKnown
};
}
function normalizeGvlSnapshotValueForHash(rawJson) {
if (typeof rawJson !== "string") {
return stableStringify(rawJson);
}
try {
return stableStringify(JSON.parse(rawJson));
} catch (error) {
return rawJson;
}
}
function normalizeGvlSnapshotValueForMetadata(rawJson) {
if (typeof rawJson !== "string") {
return rawJson;
}
try {
return JSON.parse(rawJson);
} catch (error) {
return null;
}
}
function countObjectEntries(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return 0;
}
return Object.keys(value).length;
}
globalThis.VendorGetGvlService = {
calculateGvlSnapshotSha256,
buildGvlSnapshotRecord,
storeGvlSnapshotIfNew,
recordGvlSnapshotEvent,
ingestGvlSnapshot
};
@@ -0,0 +1,348 @@
"use strict";
async function normalizeGvlCatalogsFromSnapshot(snapshot) {
const summary = {
ok: false,
vendorListVersion: snapshot?.vendorListVersion ?? null,
snapshotSha256: snapshot?.sha256 ?? null,
snapshotFetchedAt: snapshot?.fetchedAt ?? null,
gvlLastUpdated: null,
catalogsSeen: 0,
catalogsWritten: 0,
skipped: 0,
error: null
};
try {
if (!snapshot) {
return warnAndReturnGvlCatalogSummary(summary, "missing_snapshot");
}
const gvl = parseGvlCatalogRawJson(snapshot.rawJson);
if (!gvl) {
return warnAndReturnGvlCatalogSummary(
summary,
"invalid_snapshot_raw_json"
);
}
const vendorListVersion =
gvl.vendorListVersion ?? snapshot.vendorListVersion ?? null;
const normalizedAt = new Date().toISOString();
summary.vendorListVersion = vendorListVersion;
summary.gvlLastUpdated = gvl.lastUpdated ?? null;
if (vendorListVersion === null || vendorListVersion === undefined) {
return warnAndReturnGvlCatalogSummary(
summary,
"missing_vendor_list_version"
);
}
const recordsByStoreName = buildGvlCatalogRecordsByStoreName({
gvl: gvl,
snapshot: snapshot,
vendorListVersion: vendorListVersion,
normalizedAt: normalizedAt,
summary: summary
});
summary.catalogsWritten = await putGvlCatalogRecords(recordsByStoreName);
summary.ok = true;
return summary;
} catch (error) {
summary.error = error?.message ?? String(error);
console.warn("GVL catalog normalization failed", {
error: error,
snapshotSha256: summary.snapshotSha256,
vendorListVersion: summary.vendorListVersion
});
return summary;
}
}
async function normalizeLatestGvlCatalogs() {
const summary = {
ok: false,
vendorListVersion: null,
snapshotSha256: null,
snapshotFetchedAt: null,
gvlLastUpdated: null,
catalogsSeen: 0,
catalogsWritten: 0,
skipped: 0,
error: null
};
try {
const snapshot = await getLatestGvlCatalogSnapshot();
if (!snapshot) {
return warnAndReturnGvlCatalogSummary(
summary,
"no_gvl_snapshots_found"
);
}
return normalizeGvlCatalogsFromSnapshot(snapshot);
} catch (error) {
summary.error = error?.message ?? String(error);
console.warn("Latest GVL catalog normalization failed", error);
return summary;
}
}
function parseGvlCatalogRawJson(rawJson) {
if (!rawJson) {
return null;
}
if (typeof rawJson === "string") {
try {
return JSON.parse(rawJson);
} catch (error) {
console.warn("GVL snapshot rawJson is not valid JSON", error);
return null;
}
}
if (typeof rawJson === "object" && !Array.isArray(rawJson)) {
return rawJson;
}
return null;
}
function buildGvlCatalogRecordsByStoreName({
gvl,
snapshot,
vendorListVersion,
normalizedAt,
summary
}) {
const recordsByStoreName = {};
getGvlCatalogDefinitions().forEach((definition) => {
const catalog = gvl[definition.gvlField];
if (catalog === undefined || catalog === null) {
return;
}
if (typeof catalog !== "object" || Array.isArray(catalog)) {
summary.skipped += 1;
return;
}
Object.entries(catalog).forEach(([catalogKey, catalogEntry]) => {
summary.catalogsSeen += 1;
if (
!catalogEntry ||
typeof catalogEntry !== "object" ||
Array.isArray(catalogEntry)
) {
summary.skipped += 1;
return;
}
const catalogId =
catalogEntry.id ?? parseGvlCatalogNumericId(catalogKey);
if (catalogId === null || catalogId === undefined) {
summary.skipped += 1;
return;
}
if (!recordsByStoreName[definition.storeName]) {
recordsByStoreName[definition.storeName] = [];
}
recordsByStoreName[definition.storeName].push(
buildGvlCatalogRecord({
definition: definition,
catalogEntry: catalogEntry,
catalogId: catalogId,
vendorListVersion: vendorListVersion,
snapshot: snapshot,
gvl: gvl,
normalizedAt: normalizedAt
})
);
});
});
return recordsByStoreName;
}
function getGvlCatalogDefinitions() {
return [
{
entityType: "purpose",
gvlField: "purposes",
storeName: VENDORGET_STORE_NAMES.gvlPurposes,
keySegment: "purpose"
},
{
entityType: "specialPurpose",
gvlField: "specialPurposes",
storeName: VENDORGET_STORE_NAMES.gvlSpecialPurposes,
keySegment: "specialPurpose"
},
{
entityType: "feature",
gvlField: "features",
storeName: VENDORGET_STORE_NAMES.gvlFeatures,
keySegment: "feature"
},
{
entityType: "specialFeature",
gvlField: "specialFeatures",
storeName: VENDORGET_STORE_NAMES.gvlSpecialFeatures,
keySegment: "specialFeature"
}
];
}
function buildGvlCatalogRecord({
definition,
catalogEntry,
catalogId,
vendorListVersion,
snapshot,
gvl,
normalizedAt
}) {
return {
id: `${vendorListVersion}:${definition.keySegment}:${catalogId}`,
entityType: definition.entityType,
vendorListVersion: vendorListVersion,
catalogId: catalogId,
name: catalogEntry.name ?? null,
description: catalogEntry.description ?? null,
descriptionLegal: catalogEntry.descriptionLegal ?? null,
illustrations: catalogEntry.illustrations ?? null,
snapshotSha256: snapshot.sha256,
snapshotFetchedAt: snapshot.fetchedAt ?? null,
gvlLastUpdated: gvl.lastUpdated ?? null,
normalizedAt: normalizedAt,
rawCatalog: catalogEntry
};
}
function parseGvlCatalogNumericId(value) {
const numericId = Number(value);
if (!Number.isFinite(numericId)) {
return null;
}
return numericId;
}
async function putGvlCatalogRecords(recordsByStoreName) {
const storeNames = Object.keys(recordsByStoreName);
if (!storeNames.length) {
return 0;
}
const db = await openVendorGetDb();
storeNames.forEach((storeName) => {
if (!db.objectStoreNames.contains(storeName)) {
throw new Error(`missing_gvl_catalog_store:${storeName}`);
}
});
return new Promise((resolve, reject) => {
const tx = db.transaction(storeNames, "readwrite");
let written = 0;
storeNames.forEach((storeName) => {
const objectStore = tx.objectStore(storeName);
recordsByStoreName[storeName].forEach((record) => {
const putRequest = objectStore.put(record);
putRequest.onsuccess = () => {
written += 1;
};
});
});
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(written);
});
}
async function getLatestGvlCatalogSnapshot() {
const db = await openVendorGetDb();
if (!db.objectStoreNames.contains(VENDORGET_STORE_NAMES.gvlSnapshots)) {
throw new Error("missing_gvl_snapshots_store");
}
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
const getAllRequest = snapshotsStore.getAll();
getAllRequest.onerror = () => reject(getAllRequest.error);
getAllRequest.onsuccess = () => {
const snapshots = getAllRequest.result ?? [];
resolve(sortGvlCatalogSnapshotsNewestFirst(snapshots)[0] ?? null);
};
});
}
function sortGvlCatalogSnapshotsNewestFirst(snapshots) {
return snapshots.slice().sort((left, right) => {
const fetchedAtComparison =
toGvlCatalogComparableTime(right.fetchedAt) -
toGvlCatalogComparableTime(left.fetchedAt);
if (fetchedAtComparison !== 0) {
return fetchedAtComparison;
}
return (
toGvlCatalogComparableNumber(right.vendorListVersion) -
toGvlCatalogComparableNumber(left.vendorListVersion)
);
});
}
function toGvlCatalogComparableTime(value) {
const timestamp = Date.parse(value ?? "");
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function toGvlCatalogComparableNumber(value) {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : 0;
}
function warnAndReturnGvlCatalogSummary(summary, error) {
summary.error = error;
console.warn("GVL catalog normalization skipped", {
error: error,
snapshotSha256: summary.snapshotSha256,
vendorListVersion: summary.vendorListVersion
});
return summary;
}
globalThis.normalizeGvlCatalogsFromSnapshot = normalizeGvlCatalogsFromSnapshot;
globalThis.normalizeLatestGvlCatalogs = normalizeLatestGvlCatalogs;
@@ -0,0 +1,288 @@
"use strict";
async function normalizeGvlVendorsFromSnapshot(snapshot) {
const summary = {
ok: false,
vendorListVersion: snapshot?.vendorListVersion ?? null,
snapshotSha256: snapshot?.sha256 ?? null,
snapshotFetchedAt: snapshot?.fetchedAt ?? null,
gvlLastUpdated: null,
vendorsSeen: 0,
vendorsWritten: 0,
skipped: 0,
error: null
};
try {
if (!snapshot) {
return warnAndReturnGvlVendorNormalizerSummary(
summary,
"missing_snapshot"
);
}
const gvl = parseGvlSnapshotRawJson(snapshot.rawJson);
if (!gvl) {
return warnAndReturnGvlVendorNormalizerSummary(
summary,
"invalid_snapshot_raw_json"
);
}
const vendorListVersion =
gvl.vendorListVersion ?? snapshot.vendorListVersion ?? null;
const vendors = gvl.vendors;
const normalizedAt = new Date().toISOString();
summary.vendorListVersion = vendorListVersion;
summary.gvlLastUpdated = gvl.lastUpdated ?? null;
if (vendorListVersion === null || vendorListVersion === undefined) {
return warnAndReturnGvlVendorNormalizerSummary(
summary,
"missing_vendor_list_version"
);
}
if (!vendors || typeof vendors !== "object" || Array.isArray(vendors)) {
return warnAndReturnGvlVendorNormalizerSummary(summary, "missing_vendors");
}
const vendorRecords = Object.entries(vendors).reduce(
(records, [vendorKey, vendor]) => {
summary.vendorsSeen += 1;
if (!vendor || typeof vendor !== "object" || Array.isArray(vendor)) {
summary.skipped += 1;
return records;
}
const vendorId = vendor.id ?? parseNumericVendorId(vendorKey);
if (vendorId === null || vendorId === undefined) {
summary.skipped += 1;
return records;
}
records.push(
buildGvlVendorRecord({
vendor: vendor,
vendorId: vendorId,
vendorListVersion: vendorListVersion,
snapshot: snapshot,
gvl: gvl,
normalizedAt: normalizedAt
})
);
return records;
},
[]
);
summary.vendorsWritten = await putGvlVendorRecords(vendorRecords);
summary.ok = true;
return summary;
} catch (error) {
summary.error = error?.message ?? String(error);
console.warn("GVL vendor normalization failed", {
error: error,
snapshotSha256: summary.snapshotSha256,
vendorListVersion: summary.vendorListVersion
});
return summary;
}
}
async function normalizeLatestGvlSnapshotVendors() {
const summary = {
ok: false,
vendorListVersion: null,
snapshotSha256: null,
snapshotFetchedAt: null,
gvlLastUpdated: null,
vendorsSeen: 0,
vendorsWritten: 0,
skipped: 0,
error: null
};
try {
const snapshot = await getLatestGvlSnapshot();
if (!snapshot) {
return warnAndReturnGvlVendorNormalizerSummary(
summary,
"no_gvl_snapshots_found"
);
}
return normalizeGvlVendorsFromSnapshot(snapshot);
} catch (error) {
summary.error = error?.message ?? String(error);
console.warn("Latest GVL vendor normalization failed", error);
return summary;
}
}
function parseGvlSnapshotRawJson(rawJson) {
if (!rawJson) {
return null;
}
if (typeof rawJson === "string") {
try {
return JSON.parse(rawJson);
} catch (error) {
console.warn("GVL snapshot rawJson is not valid JSON", error);
return null;
}
}
if (typeof rawJson === "object" && !Array.isArray(rawJson)) {
return rawJson;
}
return null;
}
function buildGvlVendorRecord({
vendor,
vendorId,
vendorListVersion,
snapshot,
gvl,
normalizedAt
}) {
return {
id: `${vendorListVersion}:vendor:${vendorId}`,
entityType: "vendor",
vendorListVersion: vendorListVersion,
vendorId: vendorId,
snapshotSha256: snapshot.sha256,
snapshotFetchedAt: snapshot.fetchedAt ?? null,
gvlLastUpdated: gvl.lastUpdated ?? null,
tcfPolicyVersion: gvl.tcfPolicyVersion ?? null,
gvlSpecificationVersion: gvl.gvlSpecificationVersion ?? null,
normalizedAt: normalizedAt,
name: vendor.name ?? null,
policyUrl: vendor.policyUrl ?? null,
deletedDate: vendor.deletedDate ?? null,
cookieMaxAgeSeconds: vendor.cookieMaxAgeSeconds ?? null,
usesCookies: vendor.usesCookies ?? null,
usesNonCookieAccess: vendor.usesNonCookieAccess ?? null,
deviceStorageDisclosureUrl: vendor.deviceStorageDisclosureUrl ?? null,
legitimateInterestDisclosureUrl:
vendor.legitimateInterestDisclosureUrl ?? null,
domains: vendor.domains ?? null,
rawVendor: vendor
};
}
function parseNumericVendorId(value) {
const vendorId = Number(value);
if (!Number.isFinite(vendorId)) {
return null;
}
return vendorId;
}
async function putGvlVendorRecords(vendorRecords) {
if (!vendorRecords.length) {
return 0;
}
const db = await openVendorGetDb();
if (!db.objectStoreNames.contains(VENDORGET_STORE_NAMES.gvlVendors)) {
throw new Error("missing_gvl_vendors_store");
}
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlVendors], "readwrite");
const vendorsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlVendors);
let written = 0;
vendorRecords.forEach((record) => {
const putRequest = vendorsStore.put(record);
putRequest.onsuccess = () => {
written += 1;
};
});
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(written);
});
}
async function getLatestGvlSnapshot() {
const db = await openVendorGetDb();
if (!db.objectStoreNames.contains(VENDORGET_STORE_NAMES.gvlSnapshots)) {
throw new Error("missing_gvl_snapshots_store");
}
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
const getAllRequest = snapshotsStore.getAll();
getAllRequest.onerror = () => reject(getAllRequest.error);
getAllRequest.onsuccess = () => {
const snapshots = getAllRequest.result ?? [];
resolve(sortGvlSnapshotsNewestFirst(snapshots)[0] ?? null);
};
});
}
function sortGvlSnapshotsNewestFirst(snapshots) {
return snapshots.slice().sort((left, right) => {
const fetchedAtComparison =
toComparableTime(right.fetchedAt) - toComparableTime(left.fetchedAt);
if (fetchedAtComparison !== 0) {
return fetchedAtComparison;
}
return (
toComparableNumber(right.vendorListVersion) -
toComparableNumber(left.vendorListVersion)
);
});
}
function toComparableTime(value) {
const timestamp = Date.parse(value ?? "");
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function toComparableNumber(value) {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : 0;
}
function warnAndReturnGvlVendorNormalizerSummary(summary, error) {
summary.error = error;
console.warn("GVL vendor normalization skipped", {
error: error,
snapshotSha256: summary.snapshotSha256,
vendorListVersion: summary.vendorListVersion
});
return summary;
}
globalThis.normalizeGvlVendorsFromSnapshot = normalizeGvlVendorsFromSnapshot;
globalThis.normalizeLatestGvlSnapshotVendors = normalizeLatestGvlSnapshotVendors;
@@ -0,0 +1,350 @@
"use strict";
async function normalizeGvlVendorRelationshipsFromSnapshot(snapshot) {
const summary = {
ok: false,
vendorListVersion: snapshot?.vendorListVersion ?? null,
snapshotSha256: snapshot?.sha256 ?? null,
snapshotFetchedAt: snapshot?.fetchedAt ?? null,
gvlLastUpdated: null,
vendorsSeen: 0,
relationshipsSeen: 0,
relationshipsWritten: 0,
skipped: 0,
error: null
};
try {
if (!snapshot) {
return warnAndReturnGvlVendorRelationshipSummary(
summary,
"missing_snapshot"
);
}
const gvl = parseGvlVendorRelationshipRawJson(snapshot.rawJson);
if (!gvl) {
return warnAndReturnGvlVendorRelationshipSummary(
summary,
"invalid_snapshot_raw_json"
);
}
const vendorListVersion =
gvl.vendorListVersion ?? snapshot.vendorListVersion ?? null;
const vendors = gvl.vendors;
const normalizedAt = new Date().toISOString();
summary.vendorListVersion = vendorListVersion;
summary.gvlLastUpdated = gvl.lastUpdated ?? null;
if (vendorListVersion === null || vendorListVersion === undefined) {
return warnAndReturnGvlVendorRelationshipSummary(
summary,
"missing_vendor_list_version"
);
}
if (!vendors || typeof vendors !== "object" || Array.isArray(vendors)) {
return warnAndReturnGvlVendorRelationshipSummary(
summary,
"missing_vendors"
);
}
const relationshipRecords = Object.entries(vendors).reduce(
(records, [vendorKey, vendor]) => {
summary.vendorsSeen += 1;
if (!vendor || typeof vendor !== "object" || Array.isArray(vendor)) {
summary.skipped += 1;
return records;
}
const vendorId =
vendor.id ?? parseGvlVendorRelationshipNumericId(vendorKey);
if (vendorId === null || vendorId === undefined) {
summary.skipped += 1;
return records;
}
getGvlVendorRelationshipDefinitions().forEach((definition) => {
const relatedIds = vendor[definition.vendorField];
if (relatedIds === undefined || relatedIds === null) {
return;
}
if (!Array.isArray(relatedIds)) {
summary.skipped += 1;
return;
}
relatedIds.forEach((relatedId) => {
if (relatedId === null || relatedId === undefined) {
summary.skipped += 1;
return;
}
summary.relationshipsSeen += 1;
records.push(
buildGvlVendorRelationshipRecord({
vendorListVersion: vendorListVersion,
vendorId: vendorId,
relationshipType: definition.relationshipType,
relatedId: relatedId,
snapshot: snapshot,
gvl: gvl,
normalizedAt: normalizedAt
})
);
});
});
return records;
},
[]
);
summary.relationshipsWritten = await putGvlVendorRelationshipRecords(
relationshipRecords
);
summary.ok = true;
return summary;
} catch (error) {
summary.error = error?.message ?? String(error);
console.warn("GVL vendor relationship normalization failed", {
error: error,
snapshotSha256: summary.snapshotSha256,
vendorListVersion: summary.vendorListVersion
});
return summary;
}
}
async function normalizeLatestGvlVendorRelationships() {
const summary = {
ok: false,
vendorListVersion: null,
snapshotSha256: null,
snapshotFetchedAt: null,
gvlLastUpdated: null,
vendorsSeen: 0,
relationshipsSeen: 0,
relationshipsWritten: 0,
skipped: 0,
error: null
};
try {
const snapshot = await getLatestGvlVendorRelationshipSnapshot();
if (!snapshot) {
return warnAndReturnGvlVendorRelationshipSummary(
summary,
"no_gvl_snapshots_found"
);
}
return normalizeGvlVendorRelationshipsFromSnapshot(snapshot);
} catch (error) {
summary.error = error?.message ?? String(error);
console.warn("Latest GVL vendor relationship normalization failed", error);
return summary;
}
}
function parseGvlVendorRelationshipRawJson(rawJson) {
if (!rawJson) {
return null;
}
if (typeof rawJson === "string") {
try {
return JSON.parse(rawJson);
} catch (error) {
console.warn("GVL snapshot rawJson is not valid JSON", error);
return null;
}
}
if (typeof rawJson === "object" && !Array.isArray(rawJson)) {
return rawJson;
}
return null;
}
function getGvlVendorRelationshipDefinitions() {
return [
{
vendorField: "purposes",
relationshipType: "purpose"
},
{
vendorField: "legIntPurposes",
relationshipType: "legIntPurpose"
},
{
vendorField: "flexiblePurposes",
relationshipType: "flexiblePurpose"
},
{
vendorField: "specialPurposes",
relationshipType: "specialPurpose"
},
{
vendorField: "features",
relationshipType: "feature"
},
{
vendorField: "specialFeatures",
relationshipType: "specialFeature"
}
];
}
function buildGvlVendorRelationshipRecord({
vendorListVersion,
vendorId,
relationshipType,
relatedId,
snapshot,
gvl,
normalizedAt
}) {
return {
id: `${vendorListVersion}:vendor:${vendorId}:${relationshipType}:${relatedId}`,
vendorListVersion: vendorListVersion,
vendorId: vendorId,
relationshipType: relationshipType,
relatedId: relatedId,
snapshotSha256: snapshot.sha256,
snapshotFetchedAt: snapshot.fetchedAt ?? null,
gvlLastUpdated: gvl.lastUpdated ?? null,
normalizedAt: normalizedAt
};
}
function parseGvlVendorRelationshipNumericId(value) {
const numericId = Number(value);
if (!Number.isFinite(numericId)) {
return null;
}
return numericId;
}
async function putGvlVendorRelationshipRecords(relationshipRecords) {
if (!relationshipRecords.length) {
return 0;
}
const db = await openVendorGetDb();
if (
!db.objectStoreNames.contains(
VENDORGET_STORE_NAMES.gvlVendorRelationships
)
) {
throw new Error("missing_gvl_vendor_relationships_store");
}
return new Promise((resolve, reject) => {
const tx = db.transaction(
[VENDORGET_STORE_NAMES.gvlVendorRelationships],
"readwrite"
);
const relationshipsStore = tx.objectStore(
VENDORGET_STORE_NAMES.gvlVendorRelationships
);
let written = 0;
relationshipRecords.forEach((record) => {
const putRequest = relationshipsStore.put(record);
putRequest.onsuccess = () => {
written += 1;
};
});
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(written);
});
}
async function getLatestGvlVendorRelationshipSnapshot() {
const db = await openVendorGetDb();
if (!db.objectStoreNames.contains(VENDORGET_STORE_NAMES.gvlSnapshots)) {
throw new Error("missing_gvl_snapshots_store");
}
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
const getAllRequest = snapshotsStore.getAll();
getAllRequest.onerror = () => reject(getAllRequest.error);
getAllRequest.onsuccess = () => {
const snapshots = getAllRequest.result ?? [];
resolve(
sortGvlVendorRelationshipSnapshotsNewestFirst(snapshots)[0] ?? null
);
};
});
}
function sortGvlVendorRelationshipSnapshotsNewestFirst(snapshots) {
return snapshots.slice().sort((left, right) => {
const fetchedAtComparison =
toGvlVendorRelationshipComparableTime(right.fetchedAt) -
toGvlVendorRelationshipComparableTime(left.fetchedAt);
if (fetchedAtComparison !== 0) {
return fetchedAtComparison;
}
return (
toGvlVendorRelationshipComparableNumber(right.vendorListVersion) -
toGvlVendorRelationshipComparableNumber(left.vendorListVersion)
);
});
}
function toGvlVendorRelationshipComparableTime(value) {
const timestamp = Date.parse(value ?? "");
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function toGvlVendorRelationshipComparableNumber(value) {
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : 0;
}
function warnAndReturnGvlVendorRelationshipSummary(summary, error) {
summary.error = error;
console.warn("GVL vendor relationship normalization skipped", {
error: error,
snapshotSha256: summary.snapshotSha256,
vendorListVersion: summary.vendorListVersion
});
return summary;
}
globalThis.normalizeGvlVendorRelationshipsFromSnapshot =
normalizeGvlVendorRelationshipsFromSnapshot;
globalThis.normalizeLatestGvlVendorRelationships =
normalizeLatestGvlVendorRelationships;
+88
Datei anzeigen
@@ -0,0 +1,88 @@
"use strict";
const EVIDENCE_MAINTENANCE_TTL_MS = 15 * 1000;
let evidenceMaintenanceSession = null;
function startEvidenceMaintenanceSession(source) {
evidenceMaintenanceSession = buildEvidenceMaintenanceSession(source);
return getEvidenceMaintenanceStatus();
}
function refreshEvidenceMaintenanceSession(source) {
evidenceMaintenanceSession = buildEvidenceMaintenanceSession(source);
return getEvidenceMaintenanceStatus();
}
function endEvidenceMaintenanceSession(source) {
cleanupExpiredEvidenceMaintenanceSession();
if (
evidenceMaintenanceSession &&
evidenceMaintenanceSession.source === normalizeMaintenanceSource(source)
) {
evidenceMaintenanceSession = null;
}
return getEvidenceMaintenanceStatus();
}
function isEvidenceWriteSuspended() {
cleanupExpiredEvidenceMaintenanceSession();
return evidenceMaintenanceSession !== null;
}
function getEvidenceMaintenanceStatus() {
cleanupExpiredEvidenceMaintenanceSession();
if (!evidenceMaintenanceSession) {
return {
success: true,
evidenceWriteSuspended: false,
source: null,
lastHeartbeatAt: null,
expiresAt: null
};
}
return {
success: true,
evidenceWriteSuspended: true,
source: evidenceMaintenanceSession.source,
lastHeartbeatAt: evidenceMaintenanceSession.lastHeartbeatAt,
expiresAt: evidenceMaintenanceSession.expiresAt
};
}
function buildEvidenceMaintenanceSession(source) {
const now = Date.now();
const lastHeartbeatAt = new Date(now).toISOString();
const expiresAt = new Date(now + EVIDENCE_MAINTENANCE_TTL_MS).toISOString();
return {
source: normalizeMaintenanceSource(source),
lastHeartbeatAt: lastHeartbeatAt,
expiresAt: expiresAt
};
}
function cleanupExpiredEvidenceMaintenanceSession() {
if (!evidenceMaintenanceSession) {
return;
}
const expiresAt = Date.parse(evidenceMaintenanceSession.expiresAt);
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
evidenceMaintenanceSession = null;
}
}
function normalizeMaintenanceSource(source) {
return typeof source === "string" && source.trim()
? source.trim()
: "unknown";
}
+59
Datei anzeigen
@@ -0,0 +1,59 @@
const VOLATILE_REQUEST_QUERY_PARAMS = new Set([
"ts",
"timestamp",
"_",
"cb",
"cachebuster",
"rnd",
"random",
"sid",
"session",
"sessionid",
"rid",
"reqid",
"auctionId",
"bidId",
"impId",
"correlator",
"ord"
]);
function buildObservedRequestFingerprintSource(details) {
const url = new URL(details.url);
const stableQueryParams = {};
Array.from(url.searchParams.keys())
.sort()
.forEach((key) => {
if (
VOLATILE_REQUEST_QUERY_PARAMS.has(key) ||
key === "gdpr" ||
key === "gdpr_consent" ||
key === "addtlConsent"
) {
return;
}
stableQueryParams[key] = url.searchParams.getAll(key).sort();
});
return {
origin: url.origin,
pathname: url.pathname,
method: details.method,
type: details.type,
thirdParty: details.thirdParty,
consentParams: {
gdpr: url.searchParams.get("gdpr"),
gdpr_consent: url.searchParams.get("gdpr_consent"),
addtlConsent: url.searchParams.get("addtlConsent")
},
stableQueryParams: stableQueryParams
};
}
async function buildObservedRequestFingerprint(details) {
return sha256Hex(
stableStringify(buildObservedRequestFingerprintSource(details))
);
}
+150
Datei anzeigen
@@ -0,0 +1,150 @@
const REQUEST_DEDUPE_WINDOW_MS = 60 * 1000;
const REQUEST_CORRELATION_WINDOW_MS = 5 * 60 * 1000;
const OBSERVED_REQUEST_RECORDING_SOURCE = "vendorget_background_mirror";
const observedRequestFingerprints = new Map();
async function handleObservedRequest(details) {
if (!(await isRequestMonitoringEnabled())) {
return;
}
if (!hasConsentQueryParam(details.url)) {
return;
}
if (isEvidenceWriteSuspended()) {
console.info("VendorGet-IV evidence write skipped: maintenance mode");
return;
}
// The browser request is the primary observation; VG-IV records a mirror copy
// with its own recordedAt timestamp for provenance.
const requestFingerprint = await buildObservedRequestFingerprint(details);
const requestFingerprintSource = buildObservedRequestFingerprintSource(details);
const observedAt = new Date(details.timeStamp).toISOString();
const recordedAt = new Date().toISOString();
const correlation = buildObservedRequestCorrelation(
details.tabId,
details.frameId,
observedAt
);
const now = Date.now();
const existingFingerprint = observedRequestFingerprints.get(requestFingerprint);
const observedRequest = {
schemaVersion: 1,
requestFingerprint: requestFingerprint,
requestFingerprintSource: requestFingerprintSource,
recordedAt: recordedAt,
recordingSource: OBSERVED_REQUEST_RECORDING_SOURCE,
firstSeenAt: observedAt,
lastSeenAt: observedAt,
seenCount: 1,
request: {
url: details.url,
origin: requestFingerprintSource.origin,
pathname: requestFingerprintSource.pathname,
method: details.method,
type: details.type,
thirdParty: details.thirdParty
},
consentParams: {
gdpr: requestFingerprintSource.consentParams.gdpr,
gdpr_consent: requestFingerprintSource.consentParams.gdpr_consent,
addtlConsent: requestFingerprintSource.consentParams.addtlConsent
},
context: {
tabId: details.tabId,
frameId: details.frameId
},
correlation: correlation
};
if (
existingFingerprint &&
now - existingFingerprint.lastSeenAt < REQUEST_DEDUPE_WINDOW_MS
) {
existingFingerprint.lastSeenAt = now;
existingFingerprint.seenCount += 1;
await persistObservedRequest(observedRequest);
return;
}
const seenCount = existingFingerprint
? existingFingerprint.seenCount + 1
: 1;
observedRequestFingerprints.set(requestFingerprint, {
lastSeenAt: now,
seenCount: seenCount
});
await persistObservedRequest(observedRequest);
const normalizedObject = {
observedAt: observedAt,
recordedAt: recordedAt,
requestId: details.requestId,
tabId: details.tabId,
frameId: details.frameId,
url: details.url,
method: details.method,
type: details.type,
thirdParty: details.thirdParty,
requestFingerprint: requestFingerprint,
seenCount: seenCount,
requestFingerprintSource: requestFingerprintSource
};
console.log("VendorGet-IV observed request", normalizedObject);
}
function hasConsentQueryParam(url) {
try {
const params = new URL(url).searchParams;
return (
params.has("gdpr") ||
params.has("gdpr_consent") ||
params.has("addtlConsent")
);
} catch (error) {
return false;
}
}
function buildObservedRequestCorrelation(tabId, frameId, observedAt) {
const latestConsentState = getLatestConsentStateForRequest(tabId, frameId);
if (!latestConsentState) {
return buildEmptyObservedRequestCorrelation();
}
const requestTime = Date.parse(observedAt);
const consentTime = Date.parse(latestConsentState.capturedAt);
if (!Number.isFinite(requestTime) || !Number.isFinite(consentTime)) {
return buildEmptyObservedRequestCorrelation();
}
const deltaMs = requestTime - consentTime;
if (deltaMs < 0 || deltaMs > REQUEST_CORRELATION_WINDOW_MS) {
return buildEmptyObservedRequestCorrelation();
}
return {
stateFingerprint: latestConsentState.stateFingerprint,
deltaMs: deltaMs,
method: "latest_in_memory_same_tab_frame",
windowMs: REQUEST_CORRELATION_WINDOW_MS
};
}
function buildEmptyObservedRequestCorrelation() {
return {
stateFingerprint: null,
deltaMs: null,
method: null,
windowMs: REQUEST_CORRELATION_WINDOW_MS
};
}
+36
Datei anzeigen
@@ -0,0 +1,36 @@
const DEFAULT_VENDORGET_SETTINGS = {
consentCaptureEnabled: true,
requestMonitoringEnabled: false
};
const VENDORGET_SETTINGS_STORAGE_KEY = "vendorgetSettings";
async function getVendorGetSettings() {
const storedSettings = await browser.storage.local.get(
VENDORGET_SETTINGS_STORAGE_KEY
);
return {
...DEFAULT_VENDORGET_SETTINGS,
...(storedSettings[VENDORGET_SETTINGS_STORAGE_KEY] ?? {})
};
}
async function setVendorGetSetting(key, value) {
const settings = await getVendorGetSettings();
await browser.storage.local.set({
[VENDORGET_SETTINGS_STORAGE_KEY]: {
...settings,
[key]: value
}
});
}
async function isConsentCaptureEnabled() {
return (await getVendorGetSettings()).consentCaptureEnabled;
}
async function isRequestMonitoringEnabled() {
return (await getVendorGetSettings()).requestMonitoringEnabled;
}
+30
Datei anzeigen
@@ -0,0 +1,30 @@
async function sha256Hex(input) {
const data = new TextEncoder().encode(input);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
function stableStringify(value) {
return JSON.stringify(sortObjectKeys(value));
}
function sortObjectKeys(value) {
if (Array.isArray(value)) {
return value.map(sortObjectKeys);
}
if (value && typeof value === "object") {
return Object.keys(value)
.sort()
.reduce((result, key) => {
result[key] = sortObjectKeys(value[key]);
return result;
}, {});
}
return value;
}
+20
Datei anzeigen
@@ -0,0 +1,20 @@
console.log("VendorGet content listener loaded:", window.location.href);
window.addEventListener("VendorGetFromPage", async (event) => {
console.log("VendorGet message from page:", event.detail);
await browser.runtime.sendMessage({
type: "vendorget_capture",
payload: event.detail
});
});
const script = document.createElement("script");
script.src = browser.runtime.getURL("src/injected/tcf-bridge.js");
script.onload = () => script.remove();
(document.documentElement || document.head || document.body).appendChild(script);
+208
Datei anzeigen
@@ -0,0 +1,208 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
font-family: Arial, sans-serif;
color: #e5edf5;
background: #111827;
}
.dashboard {
width: min(1040px, 100%);
margin: 0 auto;
padding: 24px;
}
.dashboard-header {
display: grid;
gap: 8px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #334155;
}
h1,
h2,
p {
margin: 0;
}
h1 {
font-size: 24px;
font-weight: 700;
}
h2 {
margin-bottom: 12px;
font-size: 15px;
font-weight: 700;
}
.dashboard-status {
min-height: 18px;
font-size: 13px;
color: #cbd5e1;
}
.dashboard-notice {
max-width: 760px;
padding: 10px 12px;
border: 1px solid #334155;
border-radius: 4px;
background: #182231;
}
.maintenance-status {
display: grid;
gap: 10px;
max-width: 760px;
padding: 12px;
border: 1px solid #3f6f56;
border-radius: 4px;
background: #14251d;
}
.maintenance-status strong {
font-size: 14px;
color: #bbf7d0;
}
.maintenance-status dl {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin: 0;
}
.maintenance-status dt {
margin: 0 0 4px;
font-size: 11px;
color: #cbd5e1;
}
.maintenance-status dd {
margin: 0;
font-size: 12px;
font-weight: 700;
word-break: break-word;
}
.panel {
margin-bottom: 22px;
padding-bottom: 20px;
border-bottom: 1px solid #334155;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin: 0;
}
.metric-grid div {
min-width: 0;
padding: 12px;
border: 1px solid #334155;
border-radius: 4px;
background: #1f2937;
}
.metric-grid dt {
margin: 0 0 8px;
font-size: 12px;
color: #cbd5e1;
}
.metric-grid dd {
margin: 0;
font-size: 24px;
font-weight: 700;
word-break: break-word;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
background: #1f2937;
}
th,
td {
padding: 10px 12px;
border: 1px solid #334155;
text-align: left;
}
th {
color: #cbd5e1;
font-weight: 700;
background: #182231;
}
td:last-child,
th:last-child {
width: 140px;
text-align: right;
}
p {
max-width: 720px;
font-size: 13px;
line-height: 1.5;
color: #cbd5e1;
}
.retention-actions,
.admin-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.admin-status {
min-height: 18px;
margin-top: 12px;
font-size: 13px;
line-height: 1.4;
color: #cbd5e1;
}
button {
padding: 8px 10px;
border: 1px solid #475569;
border-radius: 4px;
font: inherit;
font-size: 13px;
color: #e5edf5;
background: #1f2937;
}
button:disabled {
cursor: default;
opacity: 0.65;
}
@media (max-width: 640px) {
.dashboard {
padding: 16px;
}
.metric-grid {
grid-template-columns: 1fr;
}
.maintenance-status dl {
grid-template-columns: 1fr 1fr;
}
.retention-actions,
.admin-actions {
display: grid;
}
}
+133
Datei anzeigen
@@ -0,0 +1,133 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VendorGet-IV Evidence Dashboard</title>
<link rel="stylesheet" href="dashboard.css">
</head>
<body>
<main class="dashboard">
<header class="dashboard-header">
<h1>VendorGet-IV Evidence Dashboard</h1>
<div id="dashboard-status" class="dashboard-status" aria-live="polite">
Loading evidence status
</div>
<p class="dashboard-notice">
Verwaltungsbereich: Lesen und manuelle Aktionen bleiben verfügbar.
VG-IV dokumentiert browserseitige Consent-/TCF-Zustände als
evidenzielle Spiegelung.
</p>
<section class="maintenance-status" aria-label="Verwaltungsmodus">
<strong>Verwaltungsmodus aktiv: Hintergrundaufzeichnung ist pausiert.</strong>
<dl>
<div>
<dt>Write Suspend</dt>
<dd id="maintenance-write-suspend">-</dd>
</div>
<div>
<dt>Quelle</dt>
<dd id="maintenance-source">-</dd>
</div>
<div>
<dt>Heartbeat</dt>
<dd id="maintenance-heartbeat">-</dd>
</div>
<div>
<dt>Ablauf</dt>
<dd id="maintenance-expires">-</dd>
</div>
</dl>
</section>
</header>
<section class="panel" aria-labelledby="overview-title">
<h2 id="overview-title">Overview</h2>
<dl class="metric-grid">
<div>
<dt>Total Evidence Records</dt>
<dd id="total-count">-</dd>
</div>
<div>
<dt>Locked Records</dt>
<dd id="locked-count">-</dd>
</div>
<div>
<dt>Unlocked Records</dt>
<dd id="unlocked-count">-</dd>
</div>
</dl>
</section>
<section class="panel" aria-labelledby="stores-title">
<h2 id="stores-title">Evidence Stores</h2>
<table>
<thead>
<tr>
<th scope="col">Store</th>
<th scope="col">Records</th>
</tr>
</thead>
<tbody>
<tr>
<td>Consent States</td>
<td id="store-consent-states">-</td>
</tr>
<tr>
<td>Consent Events</td>
<td id="store-consent-events">-</td>
</tr>
<tr>
<td>Observed Requests</td>
<td id="store-observed-requests">-</td>
</tr>
<tr>
<td>GVL Snapshots</td>
<td id="store-gvl-snapshots">-</td>
</tr>
<tr>
<td>GVL Snapshot Events</td>
<td id="store-gvl-snapshot-events">-</td>
</tr>
</tbody>
</table>
</section>
<section class="panel" aria-labelledby="retention-title">
<h2 id="retention-title">Retention Status</h2>
<p>
Locked records are protected from partial purge. Full deletion still
requires explicit confirmation.
</p>
<div class="retention-actions">
<button id="lock-all-button" type="button">
Alle Evidenzen als DSGVO-/DSAR-relevant markieren
</button>
<button id="unlock-all-button" type="button">
Alle Evidenz-Sperren entfernen
</button>
</div>
</section>
<section class="panel" aria-labelledby="administration-title">
<h2 id="administration-title">Administration</h2>
<div class="admin-actions">
<button id="gvl-fetch-official-button" type="button">
Official IAB GVL Fetch
</button>
<button id="gvl-import-button" type="button">
GVL JSON importieren
</button>
<button id="evidence-delete-button" type="button">
Evidence Delete
</button>
</div>
<div id="admin-status" class="admin-status" aria-live="polite">
Bereit
</div>
</section>
</main>
<script src="dashboard.js"></script>
</body>
</html>
+436
Datei anzeigen
@@ -0,0 +1,436 @@
"use strict";
const EVIDENCE_MAINTENANCE_SOURCE = "dashboard";
const EVIDENCE_MAINTENANCE_HEARTBEAT_MS = 5 * 1000;
const dashboardStatus = document.getElementById("dashboard-status");
const totalCount = document.getElementById("total-count");
const lockedCount = document.getElementById("locked-count");
const unlockedCount = document.getElementById("unlocked-count");
const maintenanceWriteSuspend = document.getElementById(
"maintenance-write-suspend"
);
const maintenanceSource = document.getElementById("maintenance-source");
const maintenanceHeartbeat = document.getElementById("maintenance-heartbeat");
const maintenanceExpires = document.getElementById("maintenance-expires");
const lockAllButton = document.getElementById("lock-all-button");
const unlockAllButton = document.getElementById("unlock-all-button");
const gvlFetchOfficialButton = document.getElementById(
"gvl-fetch-official-button"
);
const gvlImportButton = document.getElementById("gvl-import-button");
const evidenceDeleteButton = document.getElementById("evidence-delete-button");
const adminStatus = document.getElementById("admin-status");
const gvlImportFileInput = document.createElement("input");
gvlImportFileInput.type = "file";
gvlImportFileInput.accept = ".json,application/json";
gvlImportFileInput.hidden = true;
let evidenceMaintenanceHeartbeatId = null;
const storeCells = {
consent_states: document.getElementById("store-consent-states"),
consent_events: document.getElementById("store-consent-events"),
observed_requests: document.getElementById("store-observed-requests"),
gvl_snapshots: document.getElementById("store-gvl-snapshots"),
gvl_snapshot_events: document.getElementById("store-gvl-snapshot-events")
};
document.addEventListener("DOMContentLoaded", async () => {
document.body.appendChild(gvlImportFileInput);
await startEvidenceMaintenanceMode();
evidenceMaintenanceHeartbeatId = setInterval(() => {
void refreshEvidenceMaintenanceMode();
}, EVIDENCE_MAINTENANCE_HEARTBEAT_MS);
lockAllButton.addEventListener("click", async () => {
await handleLockAllClick();
});
unlockAllButton.addEventListener("click", async () => {
await handleUnlockAllClick();
});
gvlFetchOfficialButton.addEventListener("click", async () => {
await fetchOfficialGvl();
});
gvlImportButton.addEventListener("click", () => {
gvlImportFileInput.value = "";
gvlImportFileInput.click();
});
gvlImportFileInput.addEventListener("change", async () => {
const file = gvlImportFileInput.files?.[0] ?? null;
if (!file) {
return;
}
await importGvlFile(file);
});
evidenceDeleteButton.addEventListener("click", async () => {
await handleEvidenceDeleteClick();
});
await renderEvidenceStatus();
});
window.addEventListener("beforeunload", () => {
endEvidenceMaintenanceMode();
});
window.addEventListener("pagehide", () => {
endEvidenceMaintenanceMode();
});
async function startEvidenceMaintenanceMode() {
try {
const status = await sendEvidenceMaintenanceMessage(
"start_evidence_maintenance_session"
);
renderEvidenceMaintenanceStatus(status);
} catch (error) {
renderEvidenceMaintenanceUnavailable();
console.warn("VendorGet-IV maintenance start failed", error);
}
}
async function refreshEvidenceMaintenanceMode() {
try {
const status = await sendEvidenceMaintenanceMessage(
"refresh_evidence_maintenance_session"
);
renderEvidenceMaintenanceStatus(status);
} catch (error) {
renderEvidenceMaintenanceUnavailable();
console.warn("VendorGet-IV maintenance heartbeat failed", error);
}
}
function endEvidenceMaintenanceMode() {
if (evidenceMaintenanceHeartbeatId !== null) {
clearInterval(evidenceMaintenanceHeartbeatId);
evidenceMaintenanceHeartbeatId = null;
}
void sendEvidenceMaintenanceMessage("end_evidence_maintenance_session").catch(
(error) => {
console.warn("VendorGet-IV maintenance end failed", error);
}
);
}
async function sendEvidenceMaintenanceMessage(type) {
const status = await browser.runtime.sendMessage({
type: type,
payload: {
source: EVIDENCE_MAINTENANCE_SOURCE
}
});
if (!status?.success) {
throw new Error(status?.error ?? `${type}_failed`);
}
return status;
}
function renderEvidenceMaintenanceStatus(status) {
maintenanceWriteSuspend.textContent = status.evidenceWriteSuspended
? "aktiv"
: "inaktiv";
maintenanceSource.textContent = status.source ?? "-";
maintenanceHeartbeat.textContent = formatMaintenanceTimestamp(
status.lastHeartbeatAt
);
maintenanceExpires.textContent = formatMaintenanceTimestamp(status.expiresAt);
}
function renderEvidenceMaintenanceUnavailable() {
maintenanceWriteSuspend.textContent = "unbekannt";
maintenanceSource.textContent = "-";
maintenanceHeartbeat.textContent = "-";
maintenanceExpires.textContent = "-";
}
async function renderEvidenceStatus() {
try {
const status = await browser.runtime.sendMessage({
type: "get_evidence_retention_status"
});
if (!status?.success) {
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
}
totalCount.textContent = String(status.totalCount);
lockedCount.textContent = String(status.lockedCount);
unlockedCount.textContent = String(status.unlockedCount);
renderStoreCounts(status.storeCounts ?? {});
renderStatusMessage("Evidence status loaded");
} catch (error) {
renderStatusMessage("Evidence status could not be loaded");
console.warn("VendorGet-IV dashboard status failed", error);
}
}
function renderStoreCounts(storeCounts) {
for (const [storeName, cell] of Object.entries(storeCells)) {
cell.textContent = String(storeCounts[storeName] ?? 0);
}
}
async function fetchOfficialGvl() {
gvlFetchOfficialButton.disabled = true;
renderAdminStatus("Fetching official IAB GVL...");
try {
const result = await browser.runtime.sendMessage({
type: "fetch_official_gvl"
});
if (!result?.success) {
throw new Error(result?.error ?? "official_gvl_fetch_failed");
}
await renderEvidenceStatus();
renderAdminStatus(
"Fetched successfully - " +
`${result.alreadyKnown ? "already known" : "newly stored"} - ` +
`vendorListVersion ${result.vendorListVersion ?? "n/a"} - ` +
`sha256 ${shortenSha256(result.sha256)}`
);
} catch (error) {
renderAdminStatus("Official GVL fetch failed");
console.warn("VendorGet-IV official GVL fetch failed", error);
} finally {
gvlFetchOfficialButton.disabled = false;
}
}
async function importGvlFile(file) {
gvlImportButton.disabled = true;
renderAdminStatus("Import läuft...");
try {
const fileContent = await readFileAsText(file);
const rawJson = JSON.parse(fileContent);
if (!isGvlImportCandidate(rawJson)) {
throw new Error("invalid_gvl_json");
}
const result = await browser.runtime.sendMessage({
type: "gvl_import_json",
payload: {
rawJson: rawJson,
sourceUrl: "local-file-import"
}
});
if (!result?.success) {
throw new Error(result?.error ?? "gvl_import_failed");
}
await renderEvidenceStatus();
renderAdminStatus(
`${result.alreadyKnown ? "already known" : "imported"} - ` +
`vendorListVersion ${result.vendorListVersion ?? "n/a"} - ` +
`sha256 ${shortenSha256(result.sha256)}`
);
} catch (error) {
renderAdminStatus("Import fehlgeschlagen");
console.warn("VendorGet-IV GVL import failed", error);
} finally {
gvlImportButton.disabled = false;
}
}
function readFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(reader.error);
reader.onload = () => resolve(reader.result);
reader.readAsText(file);
});
}
function isGvlImportCandidate(value) {
return (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
value.vendorListVersion !== undefined &&
value.vendors &&
typeof value.vendors === "object" &&
!Array.isArray(value.vendors)
);
}
async function handleEvidenceDeleteClick() {
evidenceDeleteButton.disabled = true;
try {
const status = await getEvidenceRetentionStatus();
const confirmed = confirm(
"Alle lokal gespeicherten VG-IV-Evidenzdaten wirklich löschen?"
);
if (!confirmed) {
renderAdminStatus("Löschung abgebrochen");
return;
}
if (status.lockedCount === 0) {
await deleteAllEvidenceDatabase();
await renderEvidenceStatus();
renderAdminStatus("Evidenzdaten gelöscht");
return;
}
const deleteLockedRecords = confirm(
`Achtung: ${status.lockedCount} Datensätze wurden als ` +
"DSGVO-/DSAR-relevant markiert. Sollen auch diese Datensätze " +
"wirklich gelöscht werden?"
);
if (deleteLockedRecords) {
await deleteAllEvidenceDatabase();
await renderEvidenceStatus();
renderAdminStatus("Evidenzdaten gelöscht");
return;
}
const result = await browser.runtime.sendMessage({
type: "purge_unlocked_evidence_records"
});
if (!result?.success) {
throw new Error(result?.error ?? "purge_unlocked_evidence_records_failed");
}
await renderEvidenceStatus();
renderAdminStatus(
`${result.deletedCount} Datensätze gelöscht, ` +
`${result.keptLockedCount} gesperrte Datensätze behalten`
);
} catch (error) {
renderAdminStatus("Löschung fehlgeschlagen");
console.warn("VendorGet-IV evidence delete failed", error);
} finally {
evidenceDeleteButton.disabled = false;
}
}
async function getEvidenceRetentionStatus() {
const status = await browser.runtime.sendMessage({
type: "get_evidence_retention_status"
});
if (!status?.success) {
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
}
return status;
}
async function deleteAllEvidenceDatabase() {
const result = await browser.runtime.sendMessage({
type: "delete_all_evidence_database"
});
if (!result?.success) {
throw new Error(result?.error ?? "delete_all_evidence_database_failed");
}
}
async function handleLockAllClick() {
const confirmed = confirm(
"Alle vorhandenen VG-IV-Evidenzdatensätze als DSGVO-/DSAR-relevant markieren?"
);
if (!confirmed) {
renderStatusMessage("Record lock update cancelled");
return;
}
await runRecordLockAction({
type: "lock_all_evidence_records",
payload: {
reason: "dsar_used",
note: null
}
});
}
async function handleUnlockAllClick() {
const confirmed = confirm(
"Alle VG-IV-Evidenzsperren wirklich entfernen?"
);
if (!confirmed) {
renderStatusMessage("Record lock update cancelled");
return;
}
await runRecordLockAction({
type: "unlock_all_evidence_records"
});
}
async function runRecordLockAction(message) {
setRecordLockButtonsDisabled(true);
try {
const result = await browser.runtime.sendMessage(message);
if (!result?.success) {
throw new Error(result?.error ?? `${message.type}_failed`);
}
await renderEvidenceStatus();
} catch (error) {
renderStatusMessage("Record lock update failed");
console.warn("VendorGet-IV dashboard record lock update failed", error);
} finally {
setRecordLockButtonsDisabled(false);
}
}
function setRecordLockButtonsDisabled(disabled) {
lockAllButton.disabled = disabled;
unlockAllButton.disabled = disabled;
}
function renderStatusMessage(message) {
dashboardStatus.textContent = message;
}
function renderAdminStatus(message) {
adminStatus.textContent = message;
}
function shortenSha256(value) {
if (!value) {
return "n/a";
}
return `${value.slice(0, 12)}...`;
}
function formatMaintenanceTimestamp(value) {
if (!value) {
return "-";
}
return value;
}
+146
Datei anzeigen
@@ -0,0 +1,146 @@
(function () {
console.log("VendorGet injected bridge loaded:", window.location.href);
if (typeof window.__tcfapi !== "function") {
console.log("VendorGet: __tcfapi not found");
return;
}
console.log("VendorGet: __tcfapi found");
function emitToContentScript(eventName, payload) {
window.dispatchEvent(new CustomEvent("VendorGetFromPage", {
detail: {
eventName: eventName,
payload: payload
}
}));
}
function cloneSerializable(value, seen) {
if (value === undefined) {
return null;
}
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return value;
}
if (typeof value === "function" || typeof value === "symbol") {
return undefined;
}
if (typeof value !== "object") {
return undefined;
}
const seenValues = seen || new WeakSet();
if (seenValues.has(value)) {
return "[Circular]";
}
seenValues.add(value);
if (Array.isArray(value)) {
return value.map(function (item) {
const clonedItem = cloneSerializable(item, seenValues);
return clonedItem === undefined ? null : clonedItem;
});
}
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
return undefined;
}
const result = {};
Object.keys(value).forEach(function (key) {
let propertyValue;
try {
propertyValue = value[key];
} catch (error) {
return;
}
const clonedValue = cloneSerializable(propertyValue, seenValues);
if (clonedValue !== undefined) {
result[key] = clonedValue;
}
});
return result;
}
window.__tcfapi("ping", 2, function (pingData, pingSuccess) {
console.log("VendorGet __tcfapi ping:", {
success: pingSuccess,
data: pingData
});
emitToContentScript("tcf_ping", {
success: pingSuccess,
data: pingData
});
});
window.__tcfapi("addEventListener", 2, function (tcData, success) {
if (!success || !tcData) {
return;
}
console.log("VendorGet raw event:", tcData);
if (tcData.eventStatus === "useractioncomplete") {
const capture = {
url: window.location.href,
origin: window.location.origin,
timestampUtc: new Date().toISOString(),
cmpId: tcData.cmpId,
cmpVersion: tcData.cmpVersion,
gdprApplies: tcData.gdprApplies,
tcfPolicyVersion: tcData.tcfPolicyVersion,
vendorListVersion: tcData.vendorListVersion,
tcString: tcData.tcString,
eventStatus: tcData.eventStatus,
cmpStatus: tcData.cmpStatus,
isServiceSpecific: tcData.isServiceSpecific,
useNonStandardTexts: tcData.useNonStandardTexts,
publisherCC: tcData.publisherCC,
purposeOneTreatment: tcData.purposeOneTreatment,
purpose: tcData.purpose,
vendor: tcData.vendor,
specialFeatureOptins: tcData.specialFeatureOptins,
publisher: tcData.publisher,
addtlConsent: tcData.addtlConsent,
rawTcData: cloneSerializable(tcData)
};
console.log("VendorGet CONSENT CAPTURE:", capture);
emitToContentScript("consent_capture", capture);
}
});
})();
+177
Datei anzeigen
@@ -0,0 +1,177 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 280px;
font-family: Arial, sans-serif;
color: #e5edf5;
background: #111827;
}
.popup {
padding: 14px;
}
h1 {
margin: 0 0 12px;
font-size: 17px;
font-weight: 700;
}
.status {
display: grid;
gap: 6px;
margin-bottom: 14px;
}
.status-row {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 14px;
padding: 7px 0;
border-bottom: 1px solid #334155;
font-size: 13px;
}
.toggle {
display: grid;
grid-template-columns: 42px 34px;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 700;
}
.toggle input {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
}
.toggle span[id$="-status"] {
width: 42px;
text-align: right;
}
.toggle-slider {
position: relative;
width: 34px;
height: 18px;
border-radius: 999px;
background: #475569;
transition: background 120ms ease;
}
.toggle-slider::before {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.25);
transition: transform 120ms ease;
}
.toggle input:checked ~ .toggle-slider {
background: #22c55e;
}
.toggle input:checked ~ .toggle-slider::before {
transform: translateX(16px);
}
.toggle input:focus-visible ~ .toggle-slider {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.toggle input:disabled ~ .toggle-slider {
opacity: 0.65;
}
.evidence-retention {
display: grid;
gap: 10px;
}
.evidence-retention h2 {
margin: 0;
font-size: 13px;
font-weight: 700;
}
.maintenance-warning {
padding: 8px 10px;
border: 1px solid #3f6f56;
border-radius: 4px;
font-size: 12px;
font-weight: 700;
line-height: 1.35;
color: #bbf7d0;
background: #14251d;
}
.maintenance-warning[hidden] {
display: none;
}
.evidence-counts {
display: grid;
gap: 6px;
margin: 0;
}
.evidence-counts div {
display: grid;
grid-template-columns: 1fr auto;
align-items: baseline;
gap: 10px;
min-width: 0;
}
.evidence-counts dt {
margin: 0;
font-size: 12px;
color: #cbd5e1;
}
.evidence-counts dd {
margin: 0;
min-width: 28px;
font-size: 13px;
font-weight: 700;
text-align: right;
word-break: break-word;
}
.retention-status {
min-height: 16px;
font-size: 12px;
line-height: 1.35;
color: #cbd5e1;
word-break: break-word;
}
button {
width: 100%;
padding: 8px 10px;
border: 1px solid #475569;
border-radius: 4px;
font: inherit;
font-size: 13px;
color: #e5edf5;
background: #1f2937;
}
button:disabled {
cursor: default;
opacity: 0.65;
}
+75
Datei anzeigen
@@ -0,0 +1,75 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VendorGet-IV</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<main class="popup">
<h1>VendorGet-IV</h1>
<section class="status" aria-label="Status">
<div class="status-row">
<span>Consent-Capture</span>
<label class="toggle">
<input id="consent-capture-toggle" type="checkbox">
<span id="consent-capture-status">aktiv</span>
<span class="toggle-slider" aria-hidden="true"></span>
</label>
</div>
<div class="status-row">
<span>Request-Monitoring</span>
<label class="toggle">
<input id="request-monitoring-toggle" type="checkbox">
<span id="request-monitoring-status">aus</span>
<span class="toggle-slider" aria-hidden="true"></span>
</label>
</div>
</section>
<section class="evidence-retention" aria-label="Evidenzdaten">
<h2>Evidenzdaten</h2>
<div id="maintenance-warning" class="maintenance-warning" hidden>
Aufzeichnung pausiert: Dashboard geöffnet
</div>
<dl class="evidence-counts">
<div>
<dt>Consent States</dt>
<dd id="evidence-consent-states-count">-</dd>
</div>
<div>
<dt>Consent Events</dt>
<dd id="evidence-consent-events-count">-</dd>
</div>
<div>
<dt>Recorded Observations</dt>
<dd id="evidence-observed-requests-count">-</dd>
</div>
<div>
<dt>GVL Snapshots</dt>
<dd id="evidence-gvl-snapshots-count">-</dd>
</div>
<div>
<dt>GVL Events</dt>
<dd id="evidence-gvl-snapshot-events-count">-</dd>
</div>
<div>
<dt>Gesperrt (DSGVO)</dt>
<dd id="evidence-locked-count">-</dd>
</div>
</dl>
<button id="evidence-dashboard-button" type="button">
Evidence Dashboard öffnen
</button>
<div id="evidence-retention-status" class="retention-status" aria-live="polite">
Status wird geladen
</div>
</section>
</main>
<script src="../background/settings.js"></script>
<script src="popup.js"></script>
</body>
</html>
+136
Datei anzeigen
@@ -0,0 +1,136 @@
"use strict";
const requestMonitoringToggle = document.getElementById(
"request-monitoring-toggle"
);
const requestMonitoringStatus = document.getElementById(
"request-monitoring-status"
);
const consentCaptureToggle = document.getElementById("consent-capture-toggle");
const consentCaptureStatus = document.getElementById("consent-capture-status");
const maintenanceWarning = document.getElementById("maintenance-warning");
const evidenceLockedCount = document.getElementById("evidence-locked-count");
const evidenceDashboardButton = document.getElementById(
"evidence-dashboard-button"
);
const evidenceRetentionStatus = document.getElementById(
"evidence-retention-status"
);
const evidenceStoreCountCells = {
consent_states: document.getElementById("evidence-consent-states-count"),
consent_events: document.getElementById("evidence-consent-events-count"),
observed_requests: document.getElementById("evidence-observed-requests-count"),
gvl_snapshots: document.getElementById("evidence-gvl-snapshots-count"),
gvl_snapshot_events: document.getElementById(
"evidence-gvl-snapshot-events-count"
)
};
document.addEventListener("DOMContentLoaded", async () => {
await renderSettings();
await renderEvidenceMaintenanceStatus();
await renderEvidenceRetentionStatus();
consentCaptureToggle.addEventListener("change", async () => {
consentCaptureToggle.disabled = true;
await setVendorGetSetting(
"consentCaptureEnabled",
consentCaptureToggle.checked
);
await renderSettings();
consentCaptureToggle.disabled = false;
});
requestMonitoringToggle.addEventListener("change", async () => {
requestMonitoringToggle.disabled = true;
await setVendorGetSetting(
"requestMonitoringEnabled",
requestMonitoringToggle.checked
);
await renderSettings();
requestMonitoringToggle.disabled = false;
});
evidenceDashboardButton.addEventListener("click", async () => {
await browser.tabs.create({
url: browser.runtime.getURL("src/dashboard/dashboard.html")
});
});
});
async function renderSettings() {
const settings = await getVendorGetSettings();
const consentCaptureEnabled = settings.consentCaptureEnabled;
const requestMonitoringEnabled = settings.requestMonitoringEnabled;
consentCaptureToggle.checked = consentCaptureEnabled;
consentCaptureStatus.textContent = consentCaptureEnabled ? "aktiv" : "aus";
requestMonitoringToggle.checked = requestMonitoringEnabled;
requestMonitoringStatus.textContent = requestMonitoringEnabled
? "aktiv"
: "aus";
}
async function renderEvidenceRetentionStatus() {
try {
const status = await getEvidenceRetentionStatus();
renderEvidenceStoreCounts(status.storeCounts ?? {});
evidenceLockedCount.textContent = formatStatusValue(status.lockedCount);
renderEvidenceRetentionMessage("Bereit");
} catch (error) {
renderEvidenceStoreCounts({});
evidenceLockedCount.textContent = "-";
renderEvidenceRetentionMessage("Status konnte nicht geladen werden");
console.warn("VendorGet-IV evidence retention status failed", error);
}
}
async function renderEvidenceMaintenanceStatus() {
try {
const status = await browser.runtime.sendMessage({
type: "get_evidence_maintenance_status"
});
if (!status?.success) {
throw new Error(status?.error ?? "get_evidence_maintenance_status_failed");
}
maintenanceWarning.hidden = !status.evidenceWriteSuspended;
} catch (error) {
maintenanceWarning.hidden = true;
console.warn("VendorGet-IV maintenance status failed", error);
}
}
async function getEvidenceRetentionStatus() {
const status = await browser.runtime.sendMessage({
type: "get_evidence_retention_status"
});
if (!status?.success) {
throw new Error(status?.error ?? "get_evidence_retention_status_failed");
}
return status;
}
function renderEvidenceStoreCounts(storeCounts) {
for (const [storeName, cell] of Object.entries(evidenceStoreCountCells)) {
cell.textContent = formatStatusValue(storeCounts[storeName]);
}
}
function formatStatusValue(value) {
return Number.isFinite(value) ? String(value) : "-";
}
function renderEvidenceRetentionMessage(message) {
evidenceRetentionStatus.textContent = message;
}
Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 213 KiB