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