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