Initialize VG-Environment from stable VG-IV baseline

Dieser Commit ist enthalten in:
2026-05-21 19:58:08 +02:00
Commit a1a8147ae2
27 geänderte Dateien mit 3981 neuen und 0 gelöschten Zeilen
+569
Datei anzeigen
@@ -0,0 +1,569 @@
console.log("VendorGet-IV background loaded");
const OFFICIAL_IAB_GVL_URL =
"https://vendor-list.consensu.org/v3/vendor-list.json";
const EVIDENCE_RECORDING_SOURCE = "vendorget_background_mirror";
browser.runtime.onMessage.addListener((message, sender) =>
handleVendorGetMessage(message, sender)
);
browser.webRequest.onBeforeRequest.addListener(
handleObservedRequest,
{ urls: ["<all_urls>"] }
);
async function handleVendorGetMessage(message, sender) {
if (!message) {
return null;
}
if (message.type === "gvl_import_json") {
return handleGvlImportJsonMessage(message);
}
if (message.type === "fetch_official_gvl") {
return handleFetchOfficialGvlMessage();
}
if (message.type === "start_evidence_maintenance_session") {
return startEvidenceMaintenanceSession(message?.payload?.source);
}
if (message.type === "refresh_evidence_maintenance_session") {
return refreshEvidenceMaintenanceSession(message?.payload?.source);
}
if (message.type === "end_evidence_maintenance_session") {
return endEvidenceMaintenanceSession(message?.payload?.source);
}
if (message.type === "get_evidence_maintenance_status") {
return getEvidenceMaintenanceStatus();
}
if (message.type === "get_evidence_retention_status") {
return handleGetEvidenceRetentionStatusMessage();
}
if (message.type === "purge_unlocked_evidence_records") {
return handlePurgeUnlockedEvidenceRecordsMessage();
}
if (message.type === "delete_all_evidence_database") {
return handleDeleteAllEvidenceDatabaseMessage();
}
if (message.type === "lock_all_evidence_records") {
return lockAllEvidenceRecords(
message?.payload?.reason ?? "dsar_used",
message?.payload?.note ?? null
);
}
if (message.type === "unlock_all_evidence_records") {
return unlockAllEvidenceRecords();
}
if (message.type !== "vendorget_capture") {
return;
}
if (!(await isConsentCaptureEnabled())) {
return;
}
const eventName = message?.payload?.eventName ?? null;
const tabId = sender?.tab?.id ?? null;
if (eventName === "tcf_ping") {
const pingData = message?.payload?.payload?.data ?? null;
if (tabId !== null && pingData) {
rememberLatestTcfPing(tabId, pingData);
}
console.log("VendorGet-IV tcf ping", {
payload: message.payload.payload,
sender
});
return;
}
if (eventName !== "consent_capture") {
console.log("VendorGet-IV ignored event", message);
return;
}
if (isEvidenceWriteSuspended()) {
console.info("VendorGet-IV evidence write skipped: maintenance mode");
return;
}
const latestPingData = tabId !== null ? getLatestTcfPing(tabId) : null;
const consentState = buildConsentStateV1(
message.payload.payload,
sender,
latestPingData
);
consentState.stateFingerprint = await sha256Hex(
stableStringify(consentState.fingerprintSource)
);
rememberLatestConsentState(consentState);
const result = await persistConsentState(
consentState,
message.payload.payload?.rawTcData ?? null
);
console.log("VendorGet-IV consent state persisted", result);
}
async function handleGetEvidenceRetentionStatusMessage() {
const db = await openVendorGetDb();
const totalCount = await countEvidenceRecords(db);
const lockedCount = await countLockedEvidenceRecords(db);
const storeCounts = await getEvidenceStoreCounts(db);
return {
success: true,
totalCount,
lockedCount,
unlockedCount: totalCount - lockedCount,
storeCounts
};
}
async function handlePurgeUnlockedEvidenceRecordsMessage() {
const db = await openVendorGetDb();
return purgeUnlockedEvidenceRecords(db);
}
function handleDeleteAllEvidenceDatabaseMessage() {
return deleteVendorGetDatabase();
}
async function handleGvlImportJsonMessage(message) {
const rawJson = message?.payload?.rawJson ?? null;
if (!isGvlImportCandidate(rawJson)) {
return {
success: false,
error: "invalid_gvl_json"
};
}
const db = await openVendorGetDb();
const result = await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
sourceUrl: message?.payload?.sourceUrl ?? null,
diagnostics: {
importSource: "local_file"
}
});
return {
success: true,
alreadyKnown: result.alreadyKnown,
vendorListVersion: result.snapshot.vendorListVersion,
sha256: result.snapshot.sha256
};
}
function isGvlImportCandidate(value) {
return (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
value.vendorListVersion !== undefined &&
value.vendors &&
typeof value.vendors === "object" &&
!Array.isArray(value.vendors)
);
}
async function handleFetchOfficialGvlMessage() {
try {
const response = await fetch(OFFICIAL_IAB_GVL_URL, {
method: "GET",
cache: "no-store"
});
if (!response.ok) {
return {
success: false,
error: "official_gvl_fetch_failed",
responseStatus: response.status
};
}
const rawJson = await response.json();
if (!isGvlImportCandidate(rawJson)) {
return {
success: false,
error: "invalid_gvl_json",
responseStatus: response.status
};
}
const db = await openVendorGetDb();
const result = await VendorGetGvlService.ingestGvlSnapshot(db, rawJson, {
sourceUrl: OFFICIAL_IAB_GVL_URL,
fetchedAt: new Date().toISOString(),
diagnostics: {
ingestionSource: "official_iab_fetch",
responseStatus: response.status
}
});
return {
success: true,
alreadyKnown: result.alreadyKnown,
vendorListVersion: result.snapshot.vendorListVersion,
sha256: result.snapshot.sha256
};
} catch (error) {
console.warn("VendorGet-IV official GVL fetch failed", error);
return {
success: false,
error: "official_gvl_fetch_failed"
};
}
}
function buildConsentStateV1(rawCapture, sender, latestPingData) {
const decodedTcString = decodeTcStringCoreMetadata(rawCapture?.tcString ?? null);
// Firefox/CMP/browser storage remains the consent source of truth; VG-IV
// stores an evidentiary mirror of the observed TCF state.
const state = {
schemaVersion: 1,
capturedAt: new Date().toISOString(),
page: {
url: rawCapture?.url ?? sender?.tab?.url ?? sender?.url ?? null,
origin: rawCapture?.origin ?? sender?.origin ?? null,
tabId: sender?.tab?.id ?? null,
frameId: sender?.frameId ?? null,
incognito: sender?.tab?.incognito ?? null,
cookieStoreId: sender?.tab?.cookieStoreId ?? null
},
cmp: {
cmpId:
rawCapture?.cmpId ??
decodedTcString?.cmpId ??
latestPingData?.cmpId ??
null,
cmpVersion:
rawCapture?.cmpVersion ??
decodedTcString?.cmpVersion ??
latestPingData?.cmpVersion ??
null,
tcfPolicyVersion:
rawCapture?.tcfPolicyVersion ??
decodedTcString?.tcfPolicyVersion ??
latestPingData?.tcfPolicyVersion ??
null,
gdprApplies: rawCapture?.gdprApplies ?? latestPingData?.gdprApplies ?? null,
isServiceSpecific:
rawCapture?.isServiceSpecific ??
decodedTcString?.isServiceSpecific ??
null,
useNonStandardTexts: rawCapture?.useNonStandardTexts ?? null,
publisherCC:
rawCapture?.publisherCC ??
decodedTcString?.publisherCC ??
null,
purposeOneTreatment:
rawCapture?.purposeOneTreatment ??
decodedTcString?.purposeOneTreatment ??
null
},
observation: {
eventStatus: rawCapture?.eventStatus ?? null,
cmpStatus: rawCapture?.cmpStatus ?? latestPingData?.cmpStatus ?? null
},
gvl: {
vendorListVersion:
rawCapture?.vendorListVersion ??
rawCapture?.gvlVersion ??
latestPingData?.gvlVersion ??
latestPingData?.vendorListVersion ??
decodedTcString?.vendorListVersion ??
null
},
consent: {
tcString: rawCapture?.tcString ?? null,
addtlConsent: rawCapture?.addtlConsent ?? null
},
purposes: {
consents: rawCapture?.purpose?.consents ?? {},
legitimateInterests: rawCapture?.purpose?.legitimateInterests ?? {}
},
vendors: {
consents: rawCapture?.vendor?.consents ?? {},
legitimateInterests: rawCapture?.vendor?.legitimateInterests ?? {},
disclosedVendors:
rawCapture?.vendor?.disclosedVendors ??
rawCapture?.disclosedVendors ??
{}
},
specialFeatureOptins: rawCapture?.specialFeatureOptins ?? {},
publisher: {
restrictions: rawCapture?.publisher?.restrictions ?? {},
consents: rawCapture?.publisher?.consents ?? {},
legitimateInterests: rawCapture?.publisher?.legitimateInterests ?? {}
},
diagnostics: {
bridgeTimestampUtc: rawCapture?.timestampUtc ?? null,
rawTopLevelKeys: Object.keys(rawCapture ?? {}),
decodedTcStringCore: decodedTcString,
latestPingData: latestPingData
},
fingerprintSource: null,
stateFingerprint: null
};
state.fingerprintSource = buildFingerprintSource(state);
return state;
}
function buildFingerprintSource(consentState) {
return {
cmp: consentState.cmp,
gvl: consentState.gvl,
consent: consentState.consent,
purposes: consentState.purposes,
vendors: consentState.vendors,
specialFeatureOptins: consentState.specialFeatureOptins,
publisher: consentState.publisher
};
}
async function persistConsentState(consentState, rawTcData) {
const db = await openVendorGetDb();
const now = new Date().toISOString();
return new Promise((resolve, reject) => {
const tx = db.transaction(["consent_states", "consent_events"], "readwrite");
const statesStore = tx.objectStore("consent_states");
const eventsStore = tx.objectStore("consent_events");
const getRequest = statesStore.get(consentState.stateFingerprint);
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
const existingState = getRequest.result;
if (existingState) {
existingState.recordedAt =
existingState.recordedAt ?? existingState.createdAt ?? now;
existingState.recordingSource =
existingState.recordingSource ?? EVIDENCE_RECORDING_SOURCE;
existingState.lastSeenAt = now;
existingState.seenCount = (existingState.seenCount ?? 1) + 1;
existingState.updatedAt = now;
statesStore.put(existingState);
eventsStore.add({
eventType: "duplicate_state",
capturedAt: consentState.capturedAt,
recordedAt: now,
recordingSource: EVIDENCE_RECORDING_SOURCE,
stateFingerprint: consentState.stateFingerprint,
page: consentState.page,
rawEventName: "consent_capture",
rawTcData: rawTcData,
diagnostics: consentState.diagnostics
});
resolve({
action: "duplicate_state_updated",
stateFingerprint: consentState.stateFingerprint,
seenCount: existingState.seenCount
});
return;
}
const newStateRecord = {
...consentState,
recordedAt: now,
recordingSource: EVIDENCE_RECORDING_SOURCE,
firstSeenAt: now,
lastSeenAt: now,
seenCount: 1,
createdAt: now,
updatedAt: now
};
statesStore.add(newStateRecord);
eventsStore.add({
eventType: "new_state",
capturedAt: consentState.capturedAt,
recordedAt: now,
recordingSource: EVIDENCE_RECORDING_SOURCE,
stateFingerprint: consentState.stateFingerprint,
page: consentState.page,
rawEventName: "consent_capture",
rawTcData: rawTcData,
diagnostics: consentState.diagnostics
});
resolve({
action: "new_state_inserted",
stateFingerprint: consentState.stateFingerprint,
seenCount: 1
});
};
tx.onerror = () => reject(tx.error);
});
}
async function persistObservedRequest(observedRequest) {
const db = await openVendorGetDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(["observed_requests"], "readwrite");
const requestsStore = tx.objectStore("observed_requests");
const getRequest = requestsStore.get(observedRequest.requestFingerprint);
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
const existingRequest = getRequest.result;
if (existingRequest) {
const updatedRequest = {
...existingRequest,
requestFingerprintSource: observedRequest.requestFingerprintSource,
recordedAt: existingRequest.recordedAt ?? observedRequest.recordedAt,
recordingSource:
existingRequest.recordingSource ?? observedRequest.recordingSource,
lastSeenAt: observedRequest.lastSeenAt,
seenCount: (existingRequest.seenCount ?? 1) + 1,
request: observedRequest.request,
consentParams: observedRequest.consentParams,
context: observedRequest.context,
correlation: observedRequest.correlation
};
requestsStore.put(updatedRequest);
resolve({
action: "observed_request_updated",
requestFingerprint: observedRequest.requestFingerprint,
seenCount: updatedRequest.seenCount
});
return;
}
requestsStore.add(observedRequest);
resolve({
action: "observed_request_inserted",
requestFingerprint: observedRequest.requestFingerprint,
seenCount: observedRequest.seenCount
});
};
tx.onerror = () => reject(tx.error);
});
}
function decodeTcStringCoreMetadata(tcString) {
if (!tcString || typeof tcString !== "string") {
return null;
}
const coreSegment = tcString.split(".")[0];
try {
const bits = base64UrlToBits(coreSegment);
return {
version: bitsToInt(bits, 0, 6),
cmpId: bitsToInt(bits, 78, 12),
cmpVersion: bitsToInt(bits, 90, 12),
consentScreen: bitsToInt(bits, 102, 6),
consentLanguage: bitsToString(bits, 108, 12),
vendorListVersion: bitsToInt(bits, 120, 12),
tcfPolicyVersion: bitsToInt(bits, 132, 6),
isServiceSpecific: bitsToBoolean(bits, 138),
useNonStandardStacks: bitsToBoolean(bits, 139),
purposeOneTreatment: bitsToBoolean(bits, 200),
publisherCC: bitsToString(bits, 201, 12)
};
} catch (error) {
console.warn("VendorGet-IV could not decode TC string core metadata", error);
return null;
}
}
function base64UrlToBits(value) {
const base64 = value
.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(Math.ceil(value.length / 4) * 4, "=");
const binary = atob(base64);
let bits = "";
for (let index = 0; index < binary.length; index += 1) {
bits += binary
.charCodeAt(index)
.toString(2)
.padStart(8, "0");
}
return bits;
}
function bitsToInt(bits, start, length) {
return parseInt(bits.slice(start, start + length), 2);
}
function bitsToBoolean(bits, index) {
return bits[index] === "1";
}
function bitsToString(bits, start, length) {
let result = "";
for (let index = start; index < start + length; index += 6) {
const charCode = bitsToInt(bits, index, 6) + 65;
result += String.fromCharCode(charCode);
}
return result;
}