Dateien
VG-Environment/src/background.js
T

2207 Zeilen
62 KiB
JavaScript

console.log("VG-Observe background loaded");
// VG-Observe module registration lives in src/modules/vg-observe/module.js.
const OFFICIAL_IAB_GVL_URL =
"https://vendor-list.consensu.org/v3/vendor-list.json";
const EVIDENCE_RECORDING_SOURCE = "vendorget_background_mirror";
const GVL_AUTO_UPDATE_SOURCE = "extension_startup";
const AUTO_GVL_CHECK_THROTTLE_MS = 24 * 60 * 60 * 1000;
const AUTO_GVL_CHECK_STORAGE_KEY = "vendorgetAutoGvlUpdateStatus";
let isAutoGvlCheckRunning = false;
let lastAutoGvlCheckStartedAt = null;
let latestGvlUpdateStatus = null;
browser.runtime.onMessage.addListener((message, sender) =>
handleVendorGetMessage(message, sender)
);
browser.webRequest.onBeforeRequest.addListener(
handleObservedRequest,
{ urls: ["<all_urls>"] }
);
console.info("GVL auto update disabled; use manual sync");
async function handleVendorGetMessage(message, sender) {
if (!message) {
return null;
}
if (message.type === "export_gvl_evidence_json") {
return handleExportGvlEvidenceJsonMessage();
}
if (message.type === "export_gvl_revision_evidence_json") {
return handleExportGvlRevisionEvidenceJsonMessage(message);
}
if (message.type === "verify_gvl_revision_evidence_json") {
return handleVerifyGvlRevisionEvidenceJsonMessage(message);
}
if (message.type === "import_gvl_revision_evidence_json") {
return handleImportGvlRevisionEvidenceJsonMessage(message);
}
if (message.type === "mark_gvl_revision_evidence_vault_copy") {
return handleMarkGvlRevisionEvidenceVaultCopyMessage(message);
}
if (message.type === "import_gvl_evidence_json") {
return handleImportGvlEvidenceJsonMessage(message);
}
if (message.type === "gvl_import_json") {
return handleGvlImportJsonMessage(message);
}
if (message.type === "fetch_official_gvl") {
return handleFetchOfficialGvlMessage();
}
if (message.type === "export_evidence_json") {
return handleExportEvidenceJsonMessage();
}
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 === "get_latest_gvl_update_status") {
return handleGetLatestGvlUpdateStatusMessage();
}
if (message.type === "list_gvl_snapshots") {
return handleListGvlSnapshotsMessage();
}
if (message.type === "get_gvl_snapshot_summary") {
return handleGetGvlSnapshotSummaryMessage(message);
}
if (message.type === "rebuild_gvl_snapshot_normalized_data") {
return handleRebuildGvlSnapshotNormalizedDataMessage(message);
}
if (message.type === "list_gvl_vendors_for_snapshot") {
return handleListGvlVendorsForSnapshotMessage(message);
}
if (message.type === "get_gvl_vendor_detail") {
return handleGetGvlVendorDetailMessage(message);
}
if (message.type === "get_latest_consent_state") {
return handleGetLatestConsentStateMessage();
}
if (message.type === "list_recent_consent_states") {
return handleListRecentConsentStatesMessage();
}
if (message.type === "list_recent_observed_requests") {
return handleListRecentObservedRequestsMessage();
}
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("VG-Observe tcf ping", {
payload: message.payload.payload,
sender
});
return;
}
if (eventName !== "consent_capture") {
console.log("VG-Observe ignored event", message);
return;
}
if (isEvidenceWriteSuspended()) {
console.info("VG-Observe 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("VG-Observe 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 handleExportEvidenceJsonMessage() {
return {
success: true,
export: await exportVendorGetEvidenceJson()
};
}
async function handleExportGvlEvidenceJsonMessage() {
return {
success: true,
export: await exportVendorGetGvlEvidenceJson()
};
}
async function handleExportGvlRevisionEvidenceJsonMessage(message) {
try {
return {
success: true,
export: await exportVendorGetGvlRevisionEvidenceJson(
message?.payload?.snapshotSha256 ?? null
)
};
} catch (error) {
return {
success: false,
error: error?.message ?? String(error)
};
}
}
async function handleVerifyGvlRevisionEvidenceJsonMessage(message) {
try {
return {
success: true,
verification: await verifyVendorGetGvlRevisionEvidenceJson(
message?.payload?.export ?? null
)
};
} catch (error) {
return {
success: false,
error: error?.message ?? String(error)
};
}
}
async function handleImportGvlEvidenceJsonMessage(message) {
try {
return {
success: true,
import: await importVendorGetGvlEvidenceJson(message?.payload?.export)
};
} catch (error) {
return {
success: false,
error: error?.message ?? String(error)
};
}
}
async function handleImportGvlRevisionEvidenceJsonMessage(message) {
try {
const importResult = await importVendorGetGvlRevisionEvidenceJson(
message?.payload?.export ?? null
);
return {
success: importResult.imported,
import: importResult,
verification: importResult.verification,
error: importResult.imported ? null : "invalid_gvl_revision_evidence"
};
} catch (error) {
return {
success: false,
error: error?.message ?? String(error)
};
}
}
async function handleMarkGvlRevisionEvidenceVaultCopyMessage(message) {
try {
return {
success: true,
mark: await markVendorGetGvlRevisionEvidenceVaultCopy(
message?.payload?.snapshotSha256 ?? null,
message?.payload?.verification ?? null
)
};
} catch (error) {
return {
success: false,
error: error?.message ?? String(error)
};
}
}
async function handleGetLatestGvlUpdateStatusMessage() {
const db = await openVendorGetDb();
const latestSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
if (latestGvlUpdateStatus) {
return {
success: true,
status: {
...latestGvlUpdateStatus,
latestLocalVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
latestLocalSnapshotSha256: latestSnapshot?.sha256 ?? null,
latestLocalFetchedAt: latestSnapshot?.fetchedAt ?? null
}
};
}
const throttleState = await getAutoGvlCheckThrottleState();
const throttleDecision = shouldThrottleAutoGvlCheck(
throttleState?.lastAutoGvlCheckAt ?? null
);
return {
success: true,
status: {
checkedAt: null,
previousVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
currentVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
previousSnapshotSha256: latestSnapshot?.sha256 ?? null,
currentSnapshotSha256: latestSnapshot?.sha256 ?? null,
latestLocalVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
latestLocalSnapshotSha256: latestSnapshot?.sha256 ?? null,
latestLocalFetchedAt: latestSnapshot?.fetchedAt ?? null,
lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt,
result: "not_checked_since_background_start",
message: "Noch kein automatischer GVL-Update-Check seit Background-Start."
}
};
}
async function handleListGvlSnapshotsMessage() {
const db = await openVendorGetDb();
const snapshots = await listRecentGvlSnapshots(db, 25);
const snapshotsWithEvents = await Promise.all(
snapshots.map(async (snapshot) => {
const event = await getAnyGvlSnapshotEventBySha256(db, snapshot.sha256);
const provenanceState = getGvlEvidenceProvenanceState(snapshot);
return {
vendorListVersion: snapshot.vendorListVersion ?? null,
sha256: snapshot.sha256 ?? null,
fetchedAt: snapshot.fetchedAt ?? null,
sourceUrl: snapshot.sourceUrl ?? null,
provenance: provenanceState.provenance,
vaultCopyAvailable: provenanceState.vaultCopyAvailable,
workspaceDeleteAllowed: provenanceState.workspaceDeleteAllowed,
workspaceDeleteProtected: provenanceState.workspaceDeleteProtected,
eventType: event?.eventType ?? null,
eventCapturedAt: event?.capturedAt ?? null
};
})
);
return {
success: true,
gvlSnapshots: snapshotsWithEvents
};
}
async function handleGetGvlSnapshotSummaryMessage(message) {
const db = await openVendorGetDb();
const payload = message?.payload ?? {};
const snapshot = await getGvlSnapshotByIdentifier(db, {
sha256: payload.sha256 ?? null,
vendorListVersion: payload.vendorListVersion ?? null
});
if (!snapshot) {
return {
success: false,
error: "gvl_snapshot_not_found"
};
}
const vendorListVersion = snapshot.vendorListVersion ?? null;
const event = await getAnyGvlSnapshotEventBySha256(db, snapshot.sha256);
const counts = await countGvlNormalizedRecordsForVersion(
db,
vendorListVersion
);
const provenanceState = getGvlEvidenceProvenanceState(snapshot);
return {
success: true,
summary: {
vendorListVersion,
sha256: snapshot.sha256 ?? null,
fetchedAt: snapshot.fetchedAt ?? null,
sourceUrl: snapshot.sourceUrl ?? null,
eventType: event?.eventType ?? null,
eventCapturedAt: event?.capturedAt ?? null,
provenance: provenanceState.provenance,
vaultCopyAvailable: provenanceState.vaultCopyAvailable,
workspaceDeleteAllowed: provenanceState.workspaceDeleteAllowed,
workspaceDeleteProtected: provenanceState.workspaceDeleteProtected,
vendorCount: snapshot.vendorCount ?? counts.vendorCount,
snapshotVendorCount: snapshot.vendorCount ?? null,
normalizedVendorCount: counts.vendorCount,
purposeCount: snapshot.purposeCount ?? counts.purposeCount,
specialPurposeCount: counts.specialPurposeCount,
featureCount: counts.featureCount,
specialFeatureCount: counts.specialFeatureCount,
dataCategoryCount: counts.dataCategoryCount,
vendorRelationshipCount: counts.vendorRelationshipCount,
normalizedVendorRelationshipCount: counts.vendorRelationshipCount,
technicalFields: {
snapshotStore: "gvl_snapshots",
vendorListVersion: "vendorListVersion",
sha256: "sha256",
fetchedAt: "fetchedAt",
sourceUrl: "sourceUrl"
},
diagnostics: {
eventDiagnostics: event?.diagnostics ?? null
}
}
};
}
async function handleRebuildGvlSnapshotNormalizedDataMessage(message) {
const db = await openVendorGetDb();
const payload = message?.payload ?? {};
const snapshot = await getGvlSnapshotByIdentifier(db, {
sha256: payload.sha256 ?? null,
vendorListVersion: payload.vendorListVersion ?? null
});
if (!snapshot) {
return {
success: false,
error: "gvl_snapshot_not_found"
};
}
const normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
snapshot
);
const counts = await countGvlNormalizedRecordsForVersion(
db,
snapshot.vendorListVersion ?? null
);
return {
success: true,
snapshotSha256: snapshot.sha256 ?? null,
vendorListVersion: snapshot.vendorListVersion ?? null,
normalizationSummary,
counts
};
}
async function handleListGvlVendorsForSnapshotMessage(message) {
const db = await openVendorGetDb();
const payload = message?.payload ?? {};
const snapshot = await getGvlSnapshotByIdentifier(db, {
sha256: payload.sha256 ?? null,
vendorListVersion: payload.vendorListVersion ?? null
});
if (!snapshot) {
return {
success: false,
error: "gvl_snapshot_not_found"
};
}
const vendors = await listGvlVendorsForSnapshot(db, snapshot);
return {
success: true,
snapshotSha256: snapshot.sha256 ?? null,
vendorListVersion: snapshot.vendorListVersion ?? null,
vendors
};
}
function listGvlVendorsForSnapshot(db, snapshot) {
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlVendors], "readonly");
const vendorsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlVendors);
const vendorListVersionIndex = vendorsStore.index("vendorListVersion");
const cursorRequest = vendorListVersionIndex.openCursor(
IDBKeyRange.only(snapshot.vendorListVersion)
);
const vendors = [];
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor) {
return;
}
if (cursor.value?.snapshotSha256 === snapshot.sha256) {
vendors.push({
vendorId: cursor.value.vendorId ?? null,
name: cursor.value.name ?? null,
deletedDate: cursor.value.deletedDate ?? null,
snapshotSha256: cursor.value.snapshotSha256 ?? null
});
}
cursor.continue();
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => {
resolve(
vendors.sort((left, right) => {
return (
toComparableNumber(left.vendorId) -
toComparableNumber(right.vendorId)
);
})
);
};
});
}
async function handleGetGvlVendorDetailMessage(message) {
const db = await openVendorGetDb();
const vendorId = parseGvlVendorDetailId(message?.payload?.vendorId);
if (vendorId === null) {
return {
success: false,
error: "invalid_vendor_id"
};
}
const vendorRecord = await getLatestGvlVendorByVendorId(db, vendorId);
if (!vendorRecord) {
return {
success: false,
error: "gvl_vendor_not_found"
};
}
const snapshotSha256 = vendorRecord.snapshotSha256 ?? null;
const snapshot = snapshotSha256
? await getGvlSnapshotBySha256(db, snapshotSha256)
: null;
const rawGvlSha256 = snapshot?.rawGvlSha256 ?? null;
const rawEvidence = rawGvlSha256
? await getGvlRawEvidenceBySha256(db, rawGvlSha256)
: null;
const gvlInfo = await getGvlVendorDetailGvlInfo(db, vendorRecord);
return {
success: true,
vendorDetail: {
vendor: vendorRecord,
gvlInfo,
snapshot: buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256),
rawEvidence: buildGvlVendorDetailRawEvidenceSummary(
rawEvidence,
rawGvlSha256
)
}
};
}
async function getGvlVendorDetailGvlInfo(db, vendorRecord) {
const vendorListVersion = vendorRecord?.vendorListVersion ?? null;
const vendorId = vendorRecord?.vendorId ?? null;
if (vendorListVersion === null || vendorId === null) {
return buildEmptyGvlVendorDetailGvlInfo();
}
const relationships = await getGvlVendorRelationshipsForVendor(
db,
vendorListVersion,
vendorId
);
const catalogs = await getGvlVendorDetailCatalogs(db, vendorListVersion);
const rawVendor = vendorRecord?.rawVendor ?? {};
return {
purposes: buildGvlVendorDetailCatalogList(
rawVendor.purposes,
relationships.purpose,
catalogs.purposes
),
legIntPurposes: buildGvlVendorDetailCatalogList(
rawVendor.legIntPurposes,
relationships.legIntPurpose,
catalogs.purposes
),
flexiblePurposes: buildGvlVendorDetailCatalogList(
rawVendor.flexiblePurposes,
relationships.flexiblePurpose,
catalogs.purposes
),
specialPurposes: buildGvlVendorDetailCatalogList(
rawVendor.specialPurposes,
relationships.specialPurpose,
catalogs.specialPurposes
),
features: buildGvlVendorDetailCatalogList(
rawVendor.features,
relationships.feature,
catalogs.features
),
specialFeatures: buildGvlVendorDetailCatalogList(
rawVendor.specialFeatures,
relationships.specialFeature,
catalogs.specialFeatures
),
dataDeclaration: buildGvlVendorDetailCatalogList(
rawVendor.dataDeclaration,
[],
catalogs.dataCategories
),
dataCategoriesTextAvailable: catalogs.dataCategories.size > 0,
dataRetention: rawVendor.dataRetention ?? null,
overflow: rawVendor.overflow ?? null
};
}
function buildEmptyGvlVendorDetailGvlInfo() {
return {
purposes: [],
legIntPurposes: [],
flexiblePurposes: [],
specialPurposes: [],
features: [],
specialFeatures: [],
dataDeclaration: [],
dataCategoriesTextAvailable: false,
dataRetention: null,
overflow: null
};
}
function getGvlVendorRelationshipsForVendor(db, vendorListVersion, vendorId) {
return new Promise((resolve, reject) => {
const storeName = VENDORGET_STORE_NAMES.gvlVendorRelationships;
const tx = db.transaction([storeName], "readonly");
const relationshipsStore = tx.objectStore(storeName);
const vendorIdIndex = relationshipsStore.index("vendorId");
const cursorRequest = vendorIdIndex.openCursor(IDBKeyRange.only(vendorId));
const relationships = {};
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor) {
return;
}
const record = cursor.value;
if (record?.vendorListVersion === vendorListVersion) {
const relationshipType = record.relationshipType ?? "unknown";
if (!relationships[relationshipType]) {
relationships[relationshipType] = [];
}
relationships[relationshipType].push(record.relatedId);
}
cursor.continue();
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(relationships);
});
}
async function getGvlVendorDetailCatalogs(db, vendorListVersion) {
const [
purposes,
specialPurposes,
features,
specialFeatures,
dataCategories
] = await Promise.all([
getGvlCatalogMapForVersion(
db,
VENDORGET_STORE_NAMES.gvlPurposes,
vendorListVersion
),
getGvlCatalogMapForVersion(
db,
VENDORGET_STORE_NAMES.gvlSpecialPurposes,
vendorListVersion
),
getGvlCatalogMapForVersion(
db,
VENDORGET_STORE_NAMES.gvlFeatures,
vendorListVersion
),
getGvlCatalogMapForVersion(
db,
VENDORGET_STORE_NAMES.gvlSpecialFeatures,
vendorListVersion
),
getGvlCatalogMapForVersion(
db,
VENDORGET_STORE_NAMES.gvlDataCategories,
vendorListVersion
)
]);
return {
purposes,
specialPurposes,
features,
specialFeatures,
dataCategories
};
}
function getGvlCatalogMapForVersion(db, storeName, vendorListVersion) {
return new Promise((resolve, reject) => {
const tx = db.transaction([storeName], "readonly");
const catalogStore = tx.objectStore(storeName);
const vendorListVersionIndex = catalogStore.index("vendorListVersion");
const cursorRequest = vendorListVersionIndex.openCursor(
IDBKeyRange.only(vendorListVersion)
);
const catalogMap = new Map();
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor) {
return;
}
const record = cursor.value;
const catalogId = getGvlCatalogRecordId(record);
if (catalogId !== null) {
catalogMap.set(catalogId, record);
}
cursor.continue();
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(catalogMap);
});
}
function getGvlCatalogRecordId(record) {
const catalogId =
record?.catalogId ??
record?.purposeId ??
record?.specialPurposeId ??
record?.featureId ??
record?.specialFeatureId ??
record?.dataCategoryId ??
null;
if (catalogId === null || catalogId === undefined) {
return null;
}
const numericId = Number(catalogId);
return Number.isFinite(numericId) ? numericId : null;
}
function buildGvlVendorDetailCatalogList(rawIds, relationshipIds, catalogMap) {
const ids = mergeGvlVendorDetailIds(rawIds, relationshipIds);
return ids.map((id) => {
const catalogRecord = catalogMap.get(id) ?? null;
return {
id,
name: catalogRecord?.name ?? null,
description: catalogRecord?.description ?? null,
descriptionLegal: catalogRecord?.descriptionLegal ?? null,
illustrations: catalogRecord?.illustrations ?? null,
rawCatalog: catalogRecord?.rawCatalog ?? null
};
});
}
function mergeGvlVendorDetailIds(...sources) {
const ids = [];
const seen = new Set();
sources.forEach((source) => {
if (!Array.isArray(source)) {
return;
}
source.forEach((value) => {
const id = Number(value);
if (!Number.isFinite(id) || seen.has(id)) {
return;
}
seen.add(id);
ids.push(id);
});
});
return ids;
}
function parseGvlVendorDetailId(value) {
const vendorId = Number(value);
if (!Number.isInteger(vendorId) || vendorId <= 0) {
return null;
}
return vendorId;
}
function getLatestGvlVendorByVendorId(db, vendorId) {
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlVendors], "readonly");
const vendorsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlVendors);
const vendorIdIndex = vendorsStore.index("vendorId");
const cursorRequest = vendorIdIndex.openCursor(IDBKeyRange.only(vendorId));
const vendors = [];
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor) {
return;
}
vendors.push(cursor.value);
cursor.continue();
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => {
resolve(sortGvlVendorRecordsNewestFirst(vendors)[0] ?? null);
};
});
}
function sortGvlVendorRecordsNewestFirst(vendorRecords) {
return vendorRecords.slice().sort((left, right) => {
const fetchedAtComparison =
toComparableTime(right.snapshotFetchedAt) -
toComparableTime(left.snapshotFetchedAt);
if (fetchedAtComparison !== 0) {
return fetchedAtComparison;
}
return (
toComparableNumber(right.vendorListVersion) -
toComparableNumber(left.vendorListVersion)
);
});
}
function getGvlRawEvidenceBySha256(db, rawGvlSha256) {
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlRawEvidence], "readonly");
const rawEvidenceStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlRawEvidence);
const getRequest = rawEvidenceStore.get(rawGvlSha256);
let rawEvidenceOrNull = null;
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
rawEvidenceOrNull = getRequest.result ?? null;
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(rawEvidenceOrNull);
});
}
function buildGvlVendorDetailSnapshotSummary(snapshot, snapshotSha256) {
if (!snapshot) {
return {
snapshotSha256: snapshotSha256 ?? null,
rawGvlSha256: null,
vendorListVersion: null,
tcfPolicyVersion: null,
fetchedAt: null,
createdAt: null
};
}
return {
snapshotSha256: snapshot.sha256 ?? snapshotSha256 ?? null,
rawGvlSha256: snapshot.rawGvlSha256 ?? null,
vendorListVersion: snapshot.vendorListVersion ?? null,
tcfPolicyVersion: snapshot.tcfPolicyVersion ?? null,
fetchedAt: snapshot.fetchedAt ?? null,
createdAt: snapshot.createdAt ?? null
};
}
function buildGvlVendorDetailRawEvidenceSummary(rawEvidence, rawGvlSha256) {
if (!rawEvidence) {
return {
rawGvlSha256: rawGvlSha256 ?? null,
sourceUrl: null,
fetchedAt: null,
httpStatus: null,
contentType: null,
hasRawBody: false
};
}
return {
rawGvlSha256: rawEvidence.rawGvlSha256 ?? rawGvlSha256 ?? null,
sourceUrl: rawEvidence.sourceUrl ?? null,
fetchedAt: rawEvidence.fetchedAt ?? null,
httpStatus: rawEvidence.httpStatus ?? null,
contentType: rawEvidence.contentType ?? null,
hasRawBody: typeof rawEvidence.rawBody === "string"
};
}
async function handleGetLatestConsentStateMessage() {
const db = await openVendorGetDb();
const latestStateOrNull = await getLatestConsentState(db);
return {
success: true,
consentState: latestStateOrNull
};
}
async function handleListRecentConsentStatesMessage() {
const db = await openVendorGetDb();
const consentStates = await listRecentConsentStates(db, 25);
return {
success: true,
consentStates
};
}
async function handleListRecentObservedRequestsMessage() {
const db = await openVendorGetDb();
const observedRequests = await listRecentObservedRequests(db, 50);
return {
success: true,
observedRequests
};
}
function getLatestConsentState(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction(["consent_states"], "readonly");
const statesStore = tx.objectStore("consent_states");
const lastSeenAtIndex = statesStore.index("lastSeenAt");
const cursorRequest = lastSeenAtIndex.openCursor(null, "prev");
let latestStateOrNull = null;
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (cursor) {
latestStateOrNull = cursor.value;
}
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(latestStateOrNull);
});
}
function listRecentConsentStates(db, limit) {
return new Promise((resolve, reject) => {
const consentStates = [];
const tx = db.transaction(["consent_states"], "readonly");
const statesStore = tx.objectStore("consent_states");
const lastSeenAtIndex = statesStore.index("lastSeenAt");
const cursorRequest = lastSeenAtIndex.openCursor(null, "prev");
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor || consentStates.length >= limit) {
return;
}
consentStates.push(cursor.value);
if (consentStates.length < limit) {
cursor.continue();
}
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(consentStates);
});
}
function listRecentObservedRequests(db, limit) {
return new Promise((resolve, reject) => {
const observedRequests = [];
const tx = db.transaction(
[VENDORGET_STORE_NAMES.observedRequests],
"readonly"
);
const requestsStore = tx.objectStore(VENDORGET_STORE_NAMES.observedRequests);
const lastSeenAtIndex = requestsStore.index("lastSeenAt");
const cursorRequest = lastSeenAtIndex.openCursor(null, "prev");
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor || observedRequests.length >= limit) {
return;
}
observedRequests.push(cursor.value);
if (observedRequests.length < limit) {
cursor.continue();
}
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(observedRequests);
});
}
function listRecentGvlSnapshots(db, limit) {
return new Promise((resolve, reject) => {
const snapshots = [];
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
const vendorListVersionIndex = snapshotsStore.index("vendorListVersion");
const cursorRequest = vendorListVersionIndex.openCursor(null, "prev");
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor || snapshots.length >= limit) {
return;
}
snapshots.push(cursor.value);
if (snapshots.length < limit) {
cursor.continue();
}
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(snapshots);
});
}
function getAnyGvlSnapshotEventBySha256(db, sha256) {
if (!sha256) {
return Promise.resolve(null);
}
return new Promise((resolve, reject) => {
const tx = db.transaction(
[VENDORGET_STORE_NAMES.gvlSnapshotEvents],
"readonly"
);
const eventsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshotEvents);
const sha256Index = eventsStore.index("sha256");
const getRequest = sha256Index.get(sha256);
let eventOrNull = null;
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
eventOrNull = getRequest.result ?? null;
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(eventOrNull);
});
}
async function getGvlSnapshotByIdentifier(db, { sha256, vendorListVersion }) {
if (sha256) {
return getGvlSnapshotBySha256(db, sha256);
}
if (vendorListVersion !== null && vendorListVersion !== undefined) {
return getGvlSnapshotByVendorListVersion(db, vendorListVersion);
}
return null;
}
function getGvlSnapshotBySha256(db, sha256) {
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
const getRequest = snapshotsStore.get(sha256);
let snapshotOrNull = null;
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
snapshotOrNull = getRequest.result ?? null;
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(snapshotOrNull);
});
}
function getGvlSnapshotByVendorListVersion(db, vendorListVersion) {
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
const vendorListVersionIndex = snapshotsStore.index("vendorListVersion");
const getRequest = vendorListVersionIndex.get(vendorListVersion);
let snapshotOrNull = null;
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
snapshotOrNull = getRequest.result ?? null;
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(snapshotOrNull);
});
}
function countGvlNormalizedRecordsForVersion(db, vendorListVersion) {
if (vendorListVersion === null || vendorListVersion === undefined) {
return Promise.resolve({
vendorCount: 0,
purposeCount: 0,
specialPurposeCount: 0,
featureCount: 0,
specialFeatureCount: 0,
dataCategoryCount: 0,
vendorRelationshipCount: 0
});
}
const countDefinitions = [
["vendorCount", VENDORGET_STORE_NAMES.gvlVendors],
["purposeCount", VENDORGET_STORE_NAMES.gvlPurposes],
["specialPurposeCount", VENDORGET_STORE_NAMES.gvlSpecialPurposes],
["featureCount", VENDORGET_STORE_NAMES.gvlFeatures],
["specialFeatureCount", VENDORGET_STORE_NAMES.gvlSpecialFeatures],
["dataCategoryCount", VENDORGET_STORE_NAMES.gvlDataCategories],
[
"vendorRelationshipCount",
VENDORGET_STORE_NAMES.gvlVendorRelationships
]
];
return new Promise((resolve, reject) => {
const counts = {};
const tx = db.transaction(
countDefinitions.map((definition) => definition[1]),
"readonly"
);
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(counts);
for (const [countName, storeName] of countDefinitions) {
const store = tx.objectStore(storeName);
const vendorListVersionIndex = store.index("vendorListVersion");
const countRequest = vendorListVersionIndex.count(
IDBKeyRange.only(vendorListVersion)
);
countRequest.onsuccess = () => {
counts[countName] = countRequest.result;
};
}
});
}
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 {
rawBody,
rawJson,
rawGvlSha256,
fetchedAt,
contentType,
responseStatus
} =
await fetchOfficialGvlJson();
if (!isGvlImportCandidate(rawJson)) {
return {
success: false,
error: "invalid_gvl_json",
responseStatus: responseStatus
};
}
const db = await openVendorGetDb();
const currentVendorListVersion = rawJson.vendorListVersion ?? null;
const existingSnapshot = await getGvlSnapshotByVendorListVersion(
db,
currentVendorListVersion
);
const currentSnapshotSha256 =
await VendorGetGvlService.calculateGvlSnapshotSha256(rawJson);
let webProvenanceMark = null;
if (existingSnapshot?.sha256) {
if (
existingSnapshot.sha256 !== currentSnapshotSha256 ||
existingSnapshot.rawGvlSha256 !== rawGvlSha256
) {
return {
success: false,
error: "gvl_revision_evidence_conflict",
vendorListVersion: currentVendorListVersion,
existingSnapshotSha256: existingSnapshot.sha256 ?? null,
fetchedSnapshotSha256: currentSnapshotSha256,
existingRawGvlSha256: existingSnapshot.rawGvlSha256 ?? null,
fetchedRawGvlSha256: rawGvlSha256
};
}
await VendorGetGvlService.storeGvlRawEvidenceIfNew(db, {
rawGvlSha256,
sourceUrl: OFFICIAL_IAB_GVL_URL,
fetchedAt,
httpStatus: responseStatus,
contentType,
rawBody
});
webProvenanceMark = await markGvlRevisionEvidenceWebSource(
db,
existingSnapshot.sha256
);
}
const ingestResult = existingSnapshot
? null
: await ingestOfficialGvlSnapshotFromFetchedEvidence(db, {
rawBody,
rawJson,
rawGvlSha256,
fetchedAt,
contentType,
responseStatus,
ingestionSource: "official_iab_fetch"
});
const snapshot = existingSnapshot ?? ingestResult.snapshot;
const completeness = await getGvlSnapshotNormalizedCompleteness(
db,
snapshot
);
let normalizationSummary = null;
let syncStatus = "current_and_locally_available";
if (!existingSnapshot) {
normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
snapshot
);
syncStatus = "new_gvl_revision_stored_and_normalized";
} else if (!completeness.complete) {
normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
existingSnapshot
);
syncStatus = "known_gvl_rebuilt_from_local_evidence";
}
const counts = await countGvlNormalizedRecordsForVersion(
db,
snapshot.vendorListVersion ?? null
);
return {
success: true,
alreadyKnown: Boolean(existingSnapshot),
syncStatus,
vendorListVersion: snapshot.vendorListVersion,
sha256: snapshot.sha256,
webProvenanceMark,
normalizationSummary,
counts
};
} catch (error) {
console.warn("VG-Observe official GVL fetch failed", error);
return {
success: false,
error: "official_gvl_fetch_failed"
};
}
}
async function getGvlSnapshotNormalizedCompleteness(db, snapshot) {
const counts = await countGvlNormalizedRecordsForVersion(
db,
snapshot?.vendorListVersion ?? null
);
const expectedVendorCount = Number(snapshot?.vendorCount ?? 0);
const hasVendors =
expectedVendorCount > 0
? counts.vendorCount >= expectedVendorCount
: counts.vendorCount > 0;
const hasVendorRelationships = counts.vendorRelationshipCount > 0;
return {
complete: hasVendors && hasVendorRelationships,
counts
};
}
async function fetchOfficialGvlJson() {
const response = await fetch(OFFICIAL_IAB_GVL_URL, {
method: "GET",
cache: "no-store",
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache"
}
});
if (!response.ok) {
const error = new Error("official_gvl_fetch_failed");
error.responseStatus = response.status;
throw error;
}
const rawBody = await response.text();
const fetchedAt = new Date().toISOString();
const contentType = response.headers.get("Content-Type");
const rawGvlSha256 = await VendorGetGvlService.calculateRawGvlSha256(rawBody);
return {
rawBody,
rawJson: JSON.parse(rawBody),
rawGvlSha256: rawGvlSha256,
fetchedAt,
contentType,
responseStatus: response.status
};
}
async function ingestOfficialGvlSnapshotFromFetchedEvidence(
db,
{
rawBody,
rawJson,
rawGvlSha256,
fetchedAt,
contentType,
responseStatus,
ingestionSource,
diagnostics
}
) {
await VendorGetGvlService.storeGvlRawEvidenceIfNew(db, {
rawGvlSha256,
sourceUrl: OFFICIAL_IAB_GVL_URL,
fetchedAt,
httpStatus: responseStatus,
contentType,
rawBody
});
return VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
sourceUrl: OFFICIAL_IAB_GVL_URL,
fetchedAt,
rawGvlSha256: rawGvlSha256,
diagnostics: {
ingestionSource,
responseStatus: responseStatus,
...(diagnostics ?? {})
}
});
}
async function runStartupGvlAutoUpdateCheck() {
if (isAutoGvlCheckRunning) {
return;
}
isAutoGvlCheckRunning = true;
const throttleState = await getAutoGvlCheckThrottleState();
const throttleDecision = shouldThrottleAutoGvlCheck(
throttleState?.lastAutoGvlCheckAt ?? null
);
if (throttleDecision.throttled) {
await handleThrottledStartupGvlAutoUpdateCheck(
throttleState,
throttleDecision
);
isAutoGvlCheckRunning = false;
return;
}
lastAutoGvlCheckStartedAt = new Date().toISOString();
await storeAutoGvlCheckThrottleState({
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
lastAutoGvlCheckResult: "started"
});
const startedStatus = {
checkedAt: lastAutoGvlCheckStartedAt,
previousVendorListVersion: null,
currentVendorListVersion: null,
previousSnapshotSha256: null,
currentSnapshotSha256: null,
latestLocalVendorListVersion: null,
latestLocalSnapshotSha256: null,
latestLocalFetchedAt: null,
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
lastAutoGvlCheckStartedAt
),
result: "started",
message: "Automatischer GVL-Update-Check gestartet."
};
latestGvlUpdateStatus = startedStatus;
try {
const db = await openVendorGetDb();
const previousSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
const previousVendorListVersion =
previousSnapshot?.vendorListVersion ?? null;
const previousSnapshotSha256 = previousSnapshot?.sha256 ?? null;
const previousFetchedAt = previousSnapshot?.fetchedAt ?? null;
latestGvlUpdateStatus = {
checkedAt: lastAutoGvlCheckStartedAt,
previousVendorListVersion,
currentVendorListVersion: null,
previousSnapshotSha256,
currentSnapshotSha256: null,
latestLocalVendorListVersion: previousVendorListVersion,
latestLocalSnapshotSha256: previousSnapshotSha256,
latestLocalFetchedAt: previousFetchedAt,
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
lastAutoGvlCheckStartedAt
),
result: "started",
message: "Automatischer GVL-Update-Check gestartet."
};
await recordGvlAutoUpdateEvent(db, {
eventType: "gvl_auto_update_check_started",
checkedAt: lastAutoGvlCheckStartedAt,
previousSnapshot,
currentVendorListVersion: null,
currentSnapshotSha256: null,
result: "started"
});
const {
rawBody,
rawJson,
rawGvlSha256,
fetchedAt,
contentType,
responseStatus
} =
await fetchOfficialGvlJson();
if (!isGvlImportCandidate(rawJson)) {
throw new Error("invalid_gvl_json");
}
const currentVendorListVersion = rawJson.vendorListVersion ?? null;
const currentSnapshotSha256 =
await VendorGetGvlService.calculateGvlSnapshotSha256(rawJson);
const newVersionDetected = isNewerGvlVendorListVersion(
currentVendorListVersion,
previousVendorListVersion
);
const ingestResult = await ingestOfficialGvlSnapshotFromFetchedEvidence(
db,
{
rawBody,
rawJson,
rawGvlSha256,
fetchedAt,
contentType,
responseStatus,
ingestionSource: "official_iab_auto_update",
diagnostics: {
responseStatus: responseStatus,
updateCheckSource: GVL_AUTO_UPDATE_SOURCE,
checkedAt: lastAutoGvlCheckStartedAt,
previousVendorListVersion: previousVendorListVersion,
previousSnapshotSha256: previousSnapshotSha256,
previousFetchedAt: previousFetchedAt,
currentVendorListVersion: currentVendorListVersion,
currentSnapshotSha256: currentSnapshotSha256,
result: newVersionDetected ? "new_version_detected" : "no_change"
}
}
);
let normalizationSummary = null;
if (!ingestResult.alreadyKnown && newVersionDetected) {
normalizationSummary = await normalizeGvlSnapshotWithExistingPipeline(
ingestResult.snapshot
);
}
const result = buildGvlAutoUpdateResult({
newVersionDetected,
alreadyKnown: ingestResult.alreadyKnown
});
const status = {
checkedAt: lastAutoGvlCheckStartedAt,
previousVendorListVersion,
currentVendorListVersion,
previousSnapshotSha256,
currentSnapshotSha256,
latestLocalVendorListVersion: ingestResult.snapshot.vendorListVersion,
latestLocalSnapshotSha256: ingestResult.snapshot.sha256,
latestLocalFetchedAt: ingestResult.snapshot.fetchedAt ?? null,
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
lastAutoGvlCheckStartedAt
),
result,
message: buildGvlAutoUpdateMessage(result),
normalizationSummary
};
latestGvlUpdateStatus = status;
await storeAutoGvlCheckThrottleState({
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
lastAutoGvlCheckResult: result
});
await recordGvlAutoUpdateEvent(db, {
eventType: buildGvlAutoUpdateEventType(result),
checkedAt: lastAutoGvlCheckStartedAt,
previousSnapshot,
currentVendorListVersion,
currentSnapshotSha256,
currentSnapshot: ingestResult.snapshot,
result,
normalizationSummary
});
} catch (error) {
console.warn("VG-Observe automatic official GVL update check failed", error);
const checkedAt = lastAutoGvlCheckStartedAt ?? new Date().toISOString();
const errorMessage = error?.message ?? String(error);
latestGvlUpdateStatus = {
checkedAt,
previousVendorListVersion: latestGvlUpdateStatus?.previousVendorListVersion ?? null,
currentVendorListVersion: latestGvlUpdateStatus?.currentVendorListVersion ?? null,
previousSnapshotSha256: latestGvlUpdateStatus?.previousSnapshotSha256 ?? null,
currentSnapshotSha256: latestGvlUpdateStatus?.currentSnapshotSha256 ?? null,
latestLocalVendorListVersion:
latestGvlUpdateStatus?.latestLocalVendorListVersion ?? null,
latestLocalSnapshotSha256:
latestGvlUpdateStatus?.latestLocalSnapshotSha256 ?? null,
latestLocalFetchedAt: latestGvlUpdateStatus?.latestLocalFetchedAt ?? null,
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
nextAllowedAutoCheckAt: getNextAllowedAutoGvlCheckAt(
lastAutoGvlCheckStartedAt
),
result: "error",
message: "Auto-Check fehlgeschlagen",
error: errorMessage
};
await storeAutoGvlCheckThrottleState({
lastAutoGvlCheckAt: lastAutoGvlCheckStartedAt,
lastAutoGvlCheckResult: "error"
});
try {
const db = await openVendorGetDb();
const previousSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
await recordGvlAutoUpdateEvent(db, {
eventType: "gvl_auto_update_error",
checkedAt,
previousSnapshot,
currentVendorListVersion: null,
currentSnapshotSha256: null,
result: "error",
error: errorMessage
});
} catch (eventError) {
console.warn("VG-Observe automatic GVL error event failed", eventError);
}
} finally {
isAutoGvlCheckRunning = false;
}
}
async function handleThrottledStartupGvlAutoUpdateCheck(
throttleState,
throttleDecision
) {
const checkedAt = new Date().toISOString();
try {
const db = await openVendorGetDb();
const latestSnapshot = await getLatestGvlSnapshotByVendorListVersion(db);
latestGvlUpdateStatus = {
checkedAt,
previousVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
currentVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
previousSnapshotSha256: latestSnapshot?.sha256 ?? null,
currentSnapshotSha256: latestSnapshot?.sha256 ?? null,
latestLocalVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
latestLocalSnapshotSha256: latestSnapshot?.sha256 ?? null,
latestLocalFetchedAt: latestSnapshot?.fetchedAt ?? null,
lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt,
result: "throttled",
message: "Auto-Check wegen 24h-Throttling übersprungen."
};
await recordGvlAutoUpdateEvent(db, {
eventType: "gvl_auto_update_throttled",
checkedAt,
previousSnapshot: latestSnapshot,
currentVendorListVersion: latestSnapshot?.vendorListVersion ?? null,
currentSnapshotSha256: latestSnapshot?.sha256 ?? null,
result: "throttled",
throttleDiagnostics: {
lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
throttleMs: AUTO_GVL_CHECK_THROTTLE_MS,
nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt
}
});
} catch (error) {
console.warn("VG-Observe automatic GVL throttled event failed", error);
latestGvlUpdateStatus = {
checkedAt,
previousVendorListVersion: null,
currentVendorListVersion: null,
previousSnapshotSha256: null,
currentSnapshotSha256: null,
latestLocalVendorListVersion: null,
latestLocalSnapshotSha256: null,
latestLocalFetchedAt: null,
lastAutoGvlCheckAt: throttleState?.lastAutoGvlCheckAt ?? null,
nextAllowedAutoCheckAt: throttleDecision.nextAllowedAutoCheckAt,
result: "throttled",
message:
"Auto-Check wegen 24h-Throttling übersprungen; Status-Event konnte nicht geschrieben werden."
};
}
}
async function getAutoGvlCheckThrottleState() {
try {
const storedValue = await browser.storage.local.get(
AUTO_GVL_CHECK_STORAGE_KEY
);
return storedValue[AUTO_GVL_CHECK_STORAGE_KEY] ?? {};
} catch (error) {
console.warn("VG-Observe automatic GVL throttle state unavailable", error);
return {};
}
}
async function storeAutoGvlCheckThrottleState(state) {
try {
await browser.storage.local.set({
[AUTO_GVL_CHECK_STORAGE_KEY]: state
});
} catch (error) {
console.warn("VG-Observe automatic GVL throttle state write failed", error);
}
}
function shouldThrottleAutoGvlCheck(lastAutoGvlCheckAt) {
if (!lastAutoGvlCheckAt) {
return {
throttled: false,
nextAllowedAutoCheckAt: null
};
}
const lastCheckTime = Date.parse(lastAutoGvlCheckAt);
if (Number.isNaN(lastCheckTime)) {
return {
throttled: false,
nextAllowedAutoCheckAt: null
};
}
const nextAllowedTime = lastCheckTime + AUTO_GVL_CHECK_THROTTLE_MS;
const now = Date.now();
return {
throttled: now < nextAllowedTime,
nextAllowedAutoCheckAt: new Date(nextAllowedTime).toISOString()
};
}
function getNextAllowedAutoGvlCheckAt(lastAutoGvlCheckAt) {
if (!lastAutoGvlCheckAt) {
return null;
}
const lastCheckTime = Date.parse(lastAutoGvlCheckAt);
if (Number.isNaN(lastCheckTime)) {
return null;
}
return new Date(lastCheckTime + AUTO_GVL_CHECK_THROTTLE_MS).toISOString();
}
function getLatestGvlSnapshotByVendorListVersion(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction([VENDORGET_STORE_NAMES.gvlSnapshots], "readonly");
const snapshotsStore = tx.objectStore(VENDORGET_STORE_NAMES.gvlSnapshots);
const vendorListVersionIndex = snapshotsStore.index("vendorListVersion");
const cursorRequest = vendorListVersionIndex.openCursor(null, "prev");
let latestSnapshotOrNull = null;
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (cursor) {
latestSnapshotOrNull = cursor.value;
}
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(latestSnapshotOrNull);
});
}
function isNewerGvlVendorListVersion(currentVersion, previousVersion) {
if (currentVersion === null || currentVersion === undefined) {
return false;
}
if (previousVersion === null || previousVersion === undefined) {
return true;
}
return Number(currentVersion) > Number(previousVersion);
}
async function normalizeGvlSnapshotWithExistingPipeline(snapshot) {
return {
vendors: await normalizeGvlVendorsFromSnapshot(snapshot),
catalogs: await normalizeGvlCatalogsFromSnapshot(snapshot),
vendorRelationships:
await normalizeGvlVendorRelationshipsFromSnapshot(snapshot)
};
}
function buildGvlAutoUpdateResult({ newVersionDetected, alreadyKnown }) {
if (newVersionDetected && !alreadyKnown) {
return "stored";
}
if (newVersionDetected && alreadyKnown) {
return "already_known";
}
if (!newVersionDetected && !alreadyKnown) {
return "stored";
}
return "no_change";
}
function buildGvlAutoUpdateEventType(result) {
if (result === "stored") {
return "gvl_auto_update_stored";
}
if (result === "already_known") {
return "gvl_auto_update_detected";
}
return "gvl_auto_update_no_change";
}
function buildGvlAutoUpdateMessage(result) {
if (result === "stored") {
return "Neue offizielle IAB-Europe-Vendorliste gespeichert.";
}
if (result === "already_known") {
return "Offizielle IAB-Europe-Vendorliste war bereits lokal bekannt.";
}
return "Keine neuere offizielle IAB-Europe-Vendorliste gefunden.";
}
function recordGvlAutoUpdateEvent(
db,
{
eventType,
checkedAt,
previousSnapshot,
currentVendorListVersion,
currentSnapshotSha256,
currentSnapshot,
result,
normalizationSummary,
error,
throttleDiagnostics
}
) {
return VendorGetGvlService.recordGvlSnapshotEvent(db, {
eventType,
capturedAt: checkedAt,
vendorListVersion:
currentVendorListVersion ?? previousSnapshot?.vendorListVersion ?? null,
sha256: currentSnapshotSha256 ?? previousSnapshot?.sha256 ?? null,
sourceUrl: OFFICIAL_IAB_GVL_URL,
diagnostics: {
updateCheckSource: GVL_AUTO_UPDATE_SOURCE,
checkedAt,
previousVendorListVersion: previousSnapshot?.vendorListVersion ?? null,
currentVendorListVersion: currentVendorListVersion ?? null,
previousSnapshotSha256: previousSnapshot?.sha256 ?? null,
currentSnapshotSha256: currentSnapshotSha256 ?? null,
previousFetchedAt: previousSnapshot?.fetchedAt ?? null,
currentFetchedAt: currentSnapshot?.fetchedAt ?? null,
result,
vendorCountBefore: previousSnapshot?.vendorCount ?? null,
vendorCountAfter: currentSnapshot?.vendorCount ?? null,
purposeCountBefore: previousSnapshot?.purposeCount ?? null,
purposeCountAfter: currentSnapshot?.purposeCount ?? null,
normalizationSummary: normalizationSummary ?? null,
error: error ?? null,
...(throttleDiagnostics ?? {})
}
});
}
function buildConsentStateV1(rawCapture, sender, latestPingData) {
const decodedTcString = decodeTcStringCoreMetadata(rawCapture?.tcString ?? null);
// Firefox/CMP/browser storage remains the consent source of truth; VG-Observe
// 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);
});
}