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: [""] } ); 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); }); }