Add verifiable GVL revision evidence packages

Dieser Commit ist enthalten in:
2026-06-09 19:43:06 +02:00
Ursprung 08d5a6ccc2
Commit f8a23ba643
2 geänderte Dateien mit 462 neuen und 0 gelöschten Zeilen
Datei anzeigen
+462
Datei anzeigen
@@ -0,0 +1,462 @@
"use strict";
const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT =
"vendorget-gvl-evidence-export";
const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT =
"vendorget-gvl-revision-evidence";
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
const VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND = "iab-gvl-revision";
const VENDORGET_GVL_EVIDENCE_STORE_NAMES = [
VENDORGET_STORE_NAMES.gvlRawEvidence,
VENDORGET_STORE_NAMES.gvlSnapshots,
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
];
const VENDORGET_GVL_EVIDENCE_ARRAY_NAMES = [
"gvl_raw_evidence",
"gvl_snapshots",
"gvl_vendors",
"gvl_purposes",
"gvl_special_purposes",
"gvl_features",
"gvl_special_features",
"gvl_data_categories",
"gvl_vendor_relationships"
];
const VENDORGET_GVL_NORMALIZED_STORE_NAMES = [
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
];
async function exportVendorGetGvlEvidenceJson() {
const db = await openVendorGetDb();
const exportedStores = await readGvlEvidenceStores(db);
return {
metadata: {
exportFormat: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT,
exportFormatVersion: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION,
exportedAt: new Date().toISOString(),
dbName: db.name,
dbVersion: db.version
},
...exportedStores
};
}
async function exportVendorGetGvlRevisionEvidenceJson(snapshotSha256) {
if (!snapshotSha256) {
throw new Error("missing_snapshot_sha256");
}
const db = await openVendorGetDb();
const snapshot = await getGvlEvidenceRecordByKey(
db,
VENDORGET_STORE_NAMES.gvlSnapshots,
snapshotSha256
);
if (!snapshot) {
throw new Error("gvl_snapshot_not_found");
}
const rawGvlSha256 = snapshot.rawGvlSha256 ?? null;
const rawEvidence = rawGvlSha256
? await getGvlEvidenceRecordByKey(
db,
VENDORGET_STORE_NAMES.gvlRawEvidence,
rawGvlSha256
)
: null;
const normalized = await readGvlRevisionNormalizedRecords(db, snapshotSha256);
const exportedAt = new Date();
const exportedAtUtcCompact = formatGvlEvidenceUtcCompact(exportedAt);
const payload = {
metadata: {
exportFormat: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT,
exportFormatVersion: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION,
exportedAt: exportedAt.toISOString(),
exportedAtUtcCompact,
dbName: db.name,
dbVersion: db.version,
vendorListVersion: snapshot.vendorListVersion ?? null,
snapshotSha256: snapshot.sha256 ?? snapshot.snapshotSha256 ?? null,
rawGvlSha256,
contentKind: VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND
},
rawEvidence,
snapshot,
normalized
};
const exportPayloadSha256 = await calculateGvlEvidencePayloadSha256(payload);
return {
...payload,
metadata: {
...payload.metadata,
exportPayloadSha256
}
};
}
async function verifyVendorGetGvlRevisionEvidenceJson(exportContainer) {
const errors = [];
const metadata = exportContainer?.metadata ?? {};
const normalized = exportContainer?.normalized ?? {};
const rawBody = exportContainer?.rawEvidence?.rawBody ?? null;
const snapshot = exportContainer?.snapshot ?? null;
const snapshotSha256 = metadata.snapshotSha256 ?? null;
const rawGvlSha256 = metadata.rawGvlSha256 ?? null;
if (!exportContainer || typeof exportContainer !== "object") {
errors.push("invalid_export_container");
}
if (metadata.exportFormat !== VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT) {
errors.push("invalid_export_format");
}
if (
metadata.exportFormatVersion !==
VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION
) {
errors.push("unsupported_export_format_version");
}
if (metadata.contentKind !== VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND) {
errors.push("invalid_content_kind");
}
if (metadata.vendorListVersion === null || metadata.vendorListVersion === undefined) {
errors.push("missing_vendor_list_version");
}
if (!snapshotSha256) {
errors.push("missing_snapshot_sha256");
}
if (!rawGvlSha256) {
errors.push("missing_raw_gvl_sha256");
}
if (typeof rawBody !== "string") {
errors.push("missing_raw_body");
} else if ((await VendorGetGvlService.calculateRawGvlSha256(rawBody)) !== rawGvlSha256) {
errors.push("raw_body_sha256_mismatch");
}
const snapshotRecordSha256 = snapshot?.sha256 ?? snapshot?.snapshotSha256 ?? null;
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
errors.push("missing_snapshot");
} else if (snapshotRecordSha256 !== snapshotSha256) {
errors.push("snapshot_sha256_mismatch");
} else if (
(await VendorGetGvlService.calculateGvlSnapshotSha256(snapshot.rawJson)) !==
snapshotSha256
) {
errors.push("snapshot_sha256_mismatch");
}
for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
if (!Array.isArray(normalized[storeName])) {
errors.push(`missing_normalized_store_${storeName}`);
continue;
}
for (const record of normalized[storeName]) {
if (record?.snapshotSha256 !== snapshotSha256) {
errors.push(`normalized_record_snapshot_mismatch_${storeName}`);
break;
}
}
}
if (!metadata.exportPayloadSha256) {
errors.push("missing_export_payload_sha256");
} else {
const recalculatedPayloadSha256 =
await calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(exportContainer);
if (recalculatedPayloadSha256 !== metadata.exportPayloadSha256) {
errors.push("export_payload_sha256_mismatch");
}
}
return {
valid: errors.length === 0,
errors,
vendorListVersion: metadata.vendorListVersion ?? null,
snapshotSha256,
rawGvlSha256,
exportPayloadSha256: metadata.exportPayloadSha256 ?? null,
normalizedCounts: countGvlRevisionNormalizedRecords(normalized)
};
}
function getGvlEvidenceRecordByKey(db, storeName, key) {
return new Promise((resolve, reject) => {
const tx = db.transaction([storeName], "readonly");
const getRequest = tx.objectStore(storeName).get(key);
let record = null;
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
record = getRequest.result ?? null;
};
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(record);
});
}
function readGvlRevisionNormalizedRecords(db, snapshotSha256) {
return new Promise((resolve, reject) => {
const normalized = {};
const tx = db.transaction(VENDORGET_GVL_NORMALIZED_STORE_NAMES, "readonly");
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(normalized);
for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
const records = [];
const cursorRequest = tx
.objectStore(storeName)
.index("snapshotSha256")
.openCursor(IDBKeyRange.only(snapshotSha256));
normalized[storeName] = records;
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor) {
return;
}
records.push(cursor.value);
cursor.continue();
};
}
});
}
async function calculateGvlEvidencePayloadSha256(payload) {
return sha256Hex(stableStringify(payload));
}
async function calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(
exportContainer
) {
const metadata = {
...(exportContainer?.metadata ?? {})
};
delete metadata.exportPayloadSha256;
return calculateGvlEvidencePayloadSha256({
...exportContainer,
metadata
});
}
function countGvlRevisionNormalizedRecords(normalized) {
return Object.fromEntries(
VENDORGET_GVL_NORMALIZED_STORE_NAMES.map((storeName) => [
storeName,
Array.isArray(normalized?.[storeName]) ? normalized[storeName].length : 0
])
);
}
function formatGvlEvidenceUtcCompact(date) {
return [
date.getUTCFullYear(),
padGvlEvidenceDatePart(date.getUTCMonth() + 1),
padGvlEvidenceDatePart(date.getUTCDate()),
"T",
padGvlEvidenceDatePart(date.getUTCHours()),
padGvlEvidenceDatePart(date.getUTCMinutes()),
padGvlEvidenceDatePart(date.getUTCSeconds()),
"Z"
].join("");
}
function padGvlEvidenceDatePart(value) {
return String(value).padStart(2, "0");
}
function readGvlEvidenceStores(db) {
return new Promise((resolve, reject) => {
const exportContainer = {};
const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readonly");
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => resolve(exportContainer);
for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
const records = [];
const cursorRequest = tx.objectStore(storeName).openCursor();
exportContainer[storeName] = records;
cursorRequest.onerror = () => reject(cursorRequest.error);
cursorRequest.onsuccess = () => {
const cursor = cursorRequest.result;
if (!cursor) {
return;
}
records.push(cursor.value);
cursor.continue();
};
}
});
}
async function importVendorGetGvlEvidenceJson(importContainer) {
validateVendorGetGvlEvidenceImport(importContainer);
const db = await openVendorGetDb();
return importGvlEvidenceStores(db, importContainer);
}
function validateVendorGetGvlEvidenceImport(importContainer) {
if (!importContainer || typeof importContainer !== "object") {
throw new Error("invalid_gvl_evidence_export");
}
const metadata = importContainer.metadata ?? {};
if (metadata.exportFormat !== VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT) {
throw new Error("invalid_gvl_evidence_export_format");
}
if (
metadata.exportFormatVersion !==
VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION
) {
throw new Error("unsupported_gvl_evidence_export_version");
}
for (const arrayName of VENDORGET_GVL_EVIDENCE_ARRAY_NAMES) {
if (!Array.isArray(importContainer[arrayName])) {
throw new Error(`missing_gvl_evidence_store_${arrayName}`);
}
}
}
function importGvlEvidenceStores(db, importContainer) {
return new Promise((resolve, reject) => {
const counts = buildEmptyGvlEvidenceImportCounts();
const seenKeysByStore = new Map();
const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readwrite");
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
tx.oncomplete = () => {
resolve({
importedAt: new Date().toISOString(),
counts
});
};
for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
importGvlEvidenceStoreRecords(
tx.objectStore(storeName),
importContainer[storeName],
counts[storeName],
getSeenKeysForStore(seenKeysByStore, storeName)
);
}
});
}
function buildEmptyGvlEvidenceImportCounts() {
return Object.fromEntries(
VENDORGET_GVL_EVIDENCE_STORE_NAMES.map((storeName) => [
storeName,
{
read: 0,
inserted: 0,
skippedExisting: 0,
skippedInvalid: 0
}
])
);
}
function getSeenKeysForStore(seenKeysByStore, storeName) {
if (!seenKeysByStore.has(storeName)) {
seenKeysByStore.set(storeName, new Set());
}
return seenKeysByStore.get(storeName);
}
function importGvlEvidenceStoreRecords(objectStore, records, counts, seenKeys) {
for (const record of records) {
counts.read += 1;
const key = getGvlEvidenceRecordKey(objectStore, record);
const keySignature = JSON.stringify(key);
if (key === null || seenKeys.has(keySignature)) {
counts.skippedInvalid += 1;
continue;
}
seenKeys.add(keySignature);
const getRequest = objectStore.get(key);
getRequest.onsuccess = () => {
if (getRequest.result !== undefined) {
counts.skippedExisting += 1;
return;
}
const addRequest = objectStore.add(record);
addRequest.onsuccess = () => {
counts.inserted += 1;
};
};
}
}
function getGvlEvidenceRecordKey(objectStore, record) {
if (!record || typeof record !== "object" || Array.isArray(record)) {
return null;
}
if (objectStore.name === VENDORGET_STORE_NAMES.gvlSnapshots) {
return record.sha256 ?? record.snapshotSha256 ?? null;
}
const keyPath = objectStore.keyPath;
if (typeof keyPath !== "string") {
return null;
}
return record[keyPath] ?? null;
}