diff --git a/manifest.json b/manifest.json index 396897e..983a0bb 100644 --- a/manifest.json +++ b/manifest.json @@ -40,6 +40,7 @@ "src/background/gvl-service.js", "src/core/binary-utils.js", "src/core/tcf-core-metadata-decoder.js", + "src/core/consent-diff.js", "src/background.js" ] }, diff --git a/src/background.js b/src/background.js index c4b3fb8..3aafc9b 100644 --- a/src/background.js +++ b/src/background.js @@ -224,6 +224,7 @@ async function handleVendorGetMessage(message, sender) { } const latestPingData = tabId !== null ? getLatestTcfPing(tabId) : null; + const activeConsentCaptureSession = getActiveConsentCaptureSession(sender); const consentState = buildConsentStateV1( message.payload.payload, @@ -235,7 +236,7 @@ async function handleVendorGetMessage(message, sender) { stableStringify(consentState.fingerprintSource) ); - const captureSessionId = getActiveConsentCaptureSessionId(sender); + const captureSessionId = activeConsentCaptureSession?.captureSessionId ?? null; rememberLatestConsentState(consentState); @@ -245,12 +246,19 @@ async function handleVendorGetMessage(message, sender) { ); if (captureSessionId) { - await persistConsentExecutionEvent({ + const consentExecutionEvent = await persistConsentExecutionEvent({ captureSessionId, consentState, rawCapture: message.payload.payload, sender }); + + await persistConsentDiffEvent({ + captureSessionId, + providerAnnouncementEvent: + activeConsentCaptureSession?.providerAnnouncementEvents?.[0] ?? null, + consentExecutionEvent + }); } completeConsentCaptureSession(sender); @@ -1044,17 +1052,20 @@ async function handleStartPreConsentCaptureMessage(message) { session.status = "recording"; session.captureSessionId = createCaptureSessionId(); + session.providerAnnouncementEvents = []; updateConsentCaptureBadge(session); const bufferedEvents = session.bufferedPreConsentEvents; session.bufferedPreConsentEvents = []; for (const bufferedEvent of bufferedEvents) { - await persistProviderAnnouncementEvent({ + const providerAnnouncementEvent = await persistProviderAnnouncementEvent({ captureSessionId: session.captureSessionId, rawCapture: bufferedEvent.rawCapture, sender: bufferedEvent.sender }); + + session.providerAnnouncementEvents.push(providerAnnouncementEvent); } return { @@ -2393,6 +2404,7 @@ function startAttentionConsentCaptureSession(rawCapture, sender) { status: "attention", captureSessionId: null, bufferedPreConsentEvents: [], + providerAnnouncementEvents: [], timeoutId: null, page: buildConsentEventPageContext(rawCapture, sender), detectedAt: new Date().toISOString(), @@ -2440,11 +2452,14 @@ async function handlePreConsentTcfEvent(rawCapture, sender) { } if (session?.status === "recording") { - await persistProviderAnnouncementEvent({ + const providerAnnouncementEvent = await persistProviderAnnouncementEvent({ captureSessionId: session.captureSessionId, rawCapture, sender }); + session.providerAnnouncementEvents = + session.providerAnnouncementEvents ?? []; + session.providerAnnouncementEvents.push(providerAnnouncementEvent); return; } @@ -2452,6 +2467,12 @@ async function handlePreConsentTcfEvent(rawCapture, sender) { } function getActiveConsentCaptureSessionId(sender) { + const session = getActiveConsentCaptureSession(sender); + + return session?.captureSessionId ?? null; +} + +function getActiveConsentCaptureSession(sender) { const sessionKey = getConsentCaptureSessionKey(sender); const session = sessionKey ? consentCaptureSessions.get(sessionKey) : null; @@ -2459,7 +2480,7 @@ function getActiveConsentCaptureSessionId(sender) { return null; } - return session.captureSessionId ?? null; + return session; } function hasCompletedConsentCaptureSessionForTab(tabId) { @@ -2492,6 +2513,7 @@ function completeConsentCaptureSession(sender) { status: "completed", captureSessionId: null, bufferedPreConsentEvents: [], + providerAnnouncementEvents: [], timeoutId: null }; @@ -3094,16 +3116,59 @@ async function persistConsentExecutionEvent({ }); } +async function persistConsentDiffEvent({ + captureSessionId, + providerAnnouncementEvent, + consentExecutionEvent +}) { + const db = await openVendorGetDb(); + const now = new Date().toISOString(); + const consentDiff = buildConsentDiff({ + captureSessionId, + providerAnnouncement: providerAnnouncementEvent, + consentExecution: consentExecutionEvent, + derivedAt: now + }); + + return addConsentEventRecord(db, { + eventType: "consent_diff", + capturedAt: now, + recordedAt: now, + recordingSource: EVIDENCE_RECORDING_SOURCE, + captureSessionId, + stateFingerprint: consentExecutionEvent?.stateFingerprint ?? null, + page: consentExecutionEvent?.page ?? null, + rawEventName: "consent_diff", + ruleId: CONSENT_DIFF_RULE_ID, + ruleVersion: CONSENT_DIFF_RULE_VERSION, + consentDiff, + diagnostics: { + consentEvidenceRole: "consentDiff", + ruleId: CONSENT_DIFF_RULE_ID, + ruleVersion: CONSENT_DIFF_RULE_VERSION, + sourceProviderAnnouncementId: providerAnnouncementEvent?.id ?? null, + sourceConsentExecutionId: consentExecutionEvent?.id ?? null + } + }); +} + function addConsentEventRecord(db, record) { return new Promise((resolve, reject) => { const tx = db.transaction(["consent_events"], "readwrite"); const eventsStore = tx.objectStore("consent_events"); const request = eventsStore.add(record); + let storedRecord = record; request.onerror = () => reject(request.error); + request.onsuccess = () => { + storedRecord = { + ...record, + id: request.result + }; + }; tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error); - tx.oncomplete = () => resolve(record); + tx.oncomplete = () => resolve(storedRecord); }); } diff --git a/src/consent-explorer/consent-explorer.css b/src/consent-explorer/consent-explorer.css index 7c94b2f..316679d 100644 --- a/src/consent-explorer/consent-explorer.css +++ b/src/consent-explorer/consent-explorer.css @@ -27,6 +27,7 @@ body { h1, h2, h3, +h4, p { margin: 0; } @@ -49,6 +50,11 @@ h3 { color: #cbd5e1; } +h4 { + font-size: 12px; + color: #cbd5e1; +} + p { max-width: 760px; font-size: 13px; @@ -139,6 +145,165 @@ th { margin-top: 18px; } +.finding-overview { + display: grid; + gap: 10px; + margin-bottom: 16px; +} + +.finding-note { + max-width: 900px; +} + +.finding-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1px; + margin: 0; + overflow: hidden; + border: 1px solid #334155; + border-radius: 4px; + background: #334155; + font-size: 13px; +} + +.finding-list div { + display: grid; + grid-template-columns: minmax(160px, 220px) 1fr; + gap: 10px; + min-width: 0; + padding: 10px 12px; + background: #1f2937; +} + +.finding-list dt { + color: #cbd5e1; + font-weight: 700; +} + +.finding-list dd { + margin: 0; + overflow-wrap: anywhere; + color: #e5edf5; +} + +.consent-diff { + display: grid; + gap: 10px; + margin-top: 10px; + padding: 10px; + border: 1px solid #334155; + border-radius: 4px; + background: #172033; +} + +.compact-definition-list { + display: grid; + gap: 6px; + margin: 0; + font-size: 12px; +} + +.compact-definition-list div { + display: grid; + grid-template-columns: minmax(150px, 220px) 1fr; + gap: 10px; + min-width: 0; +} + +.compact-definition-list dt { + color: #cbd5e1; + font-weight: 700; +} + +.compact-definition-list dd { + margin: 0; + overflow-wrap: anywhere; +} + +.diff-note { + max-width: none; + font-size: 12px; + color: #cbd5e1; +} + +.consent-diff-primary { + display: grid; + gap: 8px; +} + +.consent-diff-findings { + display: grid; + gap: 6px; + margin: 0; + padding: 0; + list-style: none; +} + +.consent-diff-findings li { + padding: 9px 10px; + border: 1px solid #334155; + border-radius: 4px; + color: #e5edf5; + background: #1f2937; + overflow-wrap: anywhere; +} + +.consent-diff-finding-details summary { + cursor: pointer; + font-weight: 700; +} + +.consent-diff-finding-body { + display: grid; + gap: 8px; + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #334155; +} + +.consent-change-categories, +.consent-change-category-body { + display: grid; + gap: 10px; +} + +.consent-change-category { + padding: 8px 10px; + border: 1px solid #334155; + border-radius: 4px; + background: #172033; +} + +.consent-change-category summary, +.consent-change-collapsed-list summary { + cursor: pointer; + font-weight: 700; +} + +.consent-change-list-block { + display: grid; + gap: 6px; +} + +.inline-id-list { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 0; + padding: 0; + list-style: none; +} + +.inline-id-list li { + padding: 3px 6px; + border: 1px solid #334155; + border-radius: 3px; + background: #0f172a; + font-size: 12px; + color: #e5edf5; +} + .inspector-table th, .inspector-table td { vertical-align: top; @@ -227,4 +392,12 @@ th { .inspector-table .inspector-explanation { width: auto; } + + .finding-list { + grid-template-columns: 1fr; + } + + .finding-list div { + grid-template-columns: 1fr; + } } diff --git a/src/consent-explorer/consent-explorer.html b/src/consent-explorer/consent-explorer.html index 9837215..a5bf673 100644 --- a/src/consent-explorer/consent-explorer.html +++ b/src/consent-explorer/consent-explorer.html @@ -47,6 +47,7 @@