@duckduckgo/autoconsent 16.63.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/addon-firefox/background.bundle.js +1 -0
  3. package/dist/addon-firefox/compact-rules.json +1 -1
  4. package/dist/addon-firefox/content.bundle.js +162 -35
  5. package/dist/addon-firefox/manifest.json +1 -1
  6. package/dist/addon-firefox/popup.bundle.js +1 -0
  7. package/dist/addon-firefox/rule-index.json +1 -1
  8. package/dist/addon-firefox/rules.json +1 -1
  9. package/dist/addon-mv3/background.bundle.js +1 -0
  10. package/dist/addon-mv3/compact-rules.json +1 -1
  11. package/dist/addon-mv3/content.bundle.js +162 -35
  12. package/dist/addon-mv3/manifest.json +1 -1
  13. package/dist/addon-mv3/popup.bundle.js +1 -0
  14. package/dist/addon-mv3/rule-index.json +1 -1
  15. package/dist/addon-mv3/rules.json +1 -1
  16. package/dist/autoconsent.cjs.js +162 -35
  17. package/dist/autoconsent.esm.js +162 -35
  18. package/dist/autoconsent.playwright.js +165 -35
  19. package/dist/autoconsent.standalone.js +163 -36
  20. package/dist/types/heuristics.d.ts +12 -3
  21. package/dist/types/types.d.ts +12 -1
  22. package/lib/cmps/base.ts +5 -1
  23. package/lib/heuristics.ts +240 -45
  24. package/lib/types.ts +16 -1
  25. package/lib/utils.ts +1 -0
  26. package/package.json +1 -1
  27. package/readme.md +1 -1
  28. package/rules/autoconsent/asrock.json +44 -0
  29. package/rules/autoconsent/consentmo.json +5 -2
  30. package/rules/autoconsent/cookie-script.json +34 -15
  31. package/rules/autoconsent/epaas.json +18 -1
  32. package/rules/autoconsent/fnsp-cookieconsent.json +30 -0
  33. package/rules/autoconsent/fod-fujitv.json +13 -0
  34. package/rules/autoconsent/hkdir.json +33 -0
  35. package/rules/autoconsent/jokul-cookie-consent.json +28 -0
  36. package/rules/autoconsent/lego.json +27 -5
  37. package/rules/autoconsent/mars-cookies-prompt.json +20 -0
  38. package/rules/autoconsent/niedziela-pl.json +35 -0
  39. package/rules/autoconsent/nikkeibp.json +10 -0
  40. package/rules/autoconsent/onecom-cookie-banner.json +25 -0
  41. package/rules/autoconsent/oslosportslager.no.json +34 -0
  42. package/rules/autoconsent/plex.tv.json +52 -0
  43. package/rules/autoconsent/radio-canada.json +28 -0
  44. package/rules/autoconsent/regjeringen.no.json +13 -0
  45. package/rules/autoconsent/roll20.json +50 -0
  46. package/rules/autoconsent/sirdata.json +8 -3
  47. package/rules/autoconsent/site-notifications-cookie-usage.json +25 -0
  48. package/rules/autoconsent/sparebank1-optin.json +10 -0
  49. package/rules/autoconsent/tri-table.json +6 -2
  50. package/rules/autoconsent/vinnova-se.json +50 -0
  51. package/rules/autoconsent/vortex-consent.json +33 -0
  52. package/rules/compact-rules.json +1 -1
  53. package/rules/rule-index.json +1 -1
  54. package/rules/rules.json +1 -1
  55. package/tests/asrock.spec.ts +3 -0
  56. package/tests/consentmo.spec.ts +2 -0
  57. package/tests/cookie-script.spec.ts +5 -0
  58. package/tests/epaas.spec.ts +3 -0
  59. package/tests/fnsp-cookieconsent.spec.ts +9 -0
  60. package/tests/fod-fujitv.spec.ts +3 -0
  61. package/tests/hkdir.spec.ts +10 -0
  62. package/tests/jokul-cookie-consent.spec.ts +3 -0
  63. package/tests/mars-cookies-prompt.spec.ts +3 -0
  64. package/tests/niedziela-pl.spec.ts +3 -0
  65. package/tests/nikkeibp.spec.ts +8 -0
  66. package/tests/onecom-cookie-banner.spec.ts +5 -0
  67. package/tests/oslosportslager.no.spec.ts +3 -0
  68. package/tests/plex.tv.spec.ts +5 -0
  69. package/tests/radio-canada.spec.ts +15 -0
  70. package/tests/regjeringen.no.spec.ts +3 -0
  71. package/tests/roll20.spec.ts +3 -0
  72. package/tests/sirdata.spec.ts +13 -1
  73. package/tests/site-notifications-cookie-usage.spec.ts +3 -0
  74. package/tests/sparebank1-optin.spec.ts +8 -0
  75. package/tests/tri-table.spec.ts +1 -1
  76. package/tests/vinnova-se.spec.ts +6 -0
  77. package/tests/vortex-consent.spec.ts +18 -0
  78. package/tests-wtr/heuristics/get-actionable-popups.ts +41 -7
  79. package/tests-wtr/heuristics/get-potential-popups.test.ts +235 -4
  80. package/tests-wtr/heuristics/heuristic-cmp.ts +16 -1
  81. package/rules/generated/auto_CA_plex.tv_ee4.json +0 -40
  82. package/rules/generated/auto_CA_support.plex.tv_gn3.json +0 -40
  83. package/tests/generated/auto_CA_plex.tv_ee4.spec.ts +0 -2
  84. package/tests/generated/auto_CA_support.plex.tv_gn3.spec.ts +0 -6
@@ -151,6 +151,7 @@
151
151
  },
152
152
  performanceLoggingEnabled: false,
153
153
  heuristicPopupSearchTimeout: 100,
154
+ enableHeuristicHitTest: false,
154
155
  heuristicMode: "off"
155
156
  // heuristic disabled by default
156
157
  };
@@ -1326,28 +1327,70 @@
1326
1327
  // lib/heuristics.ts
1327
1328
  var BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
1328
1329
  var POPUP_SEARCH_MAX_TIME = 100;
1329
- function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME) {
1330
- const acceptedLevels = mode === "tier2" ? ["reject", "tier1", "tier2"] : mode === "tier1" ? ["reject", "tier1"] : ["reject"];
1331
- const popups = getPotentialPopups(timeout).filter((popup) => {
1332
- const popupText = popup.text?.trim();
1333
- if (!popupText || isExcludedPopup(popupText) || !hasHeuristicPatterns(popupText)) {
1334
- return false;
1335
- }
1336
- popup.regexClassification = classifyPopup(popup.buttons);
1337
- return popup.regexClassification !== void 0 && acceptedLevels.includes(popup.regexClassification);
1338
- });
1339
- return popups.sort((a, b) => (a.regexClassification ?? "") > (b.regexClassification ?? "") ? 1 : -1);
1330
+ var ACCEPTED_LEVELS = {
1331
+ off: [],
1332
+ reject: ["reject"],
1333
+ tier1: ["reject", "tier1"],
1334
+ tier2: ["reject", "tier1", "tier2"]
1335
+ };
1336
+ function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME, hitTest = false) {
1337
+ const acceptedLevels = ACCEPTED_LEVELS[mode];
1338
+ const isActionable = (popup) => acceptedLevels.includes(popup.regexClassification) && isConsentPopup(popup);
1339
+ const popups = searchPopups(timeout, hitTest, DEFAULT_HIT_TEST_GRID, true).filter(isActionable);
1340
+ return popups.sort((a, b) => classificationRank(a) - classificationRank(b));
1340
1341
  }
1341
- function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1342
+ function searchPopups(timeout, hitTest, hitTestGrid, stopOnReject = false) {
1342
1343
  if (!isTopFrame()) {
1343
- if (window.parent && window.parent !== window.top) {
1344
- return [];
1345
- }
1346
- const frameElement = getFramePopupElement(document);
1347
- const popup = frameElement && toPopupData(frameElement);
1348
- return popup ? [popup] : [];
1344
+ return getFramePopups();
1349
1345
  }
1350
1346
  const deadline = performance.now() + timeout;
1347
+ const hitTestPopups = hitTest ? findPopupsByHitTest(deadline, hitTestGrid) : [];
1348
+ if (stopOnReject && hitTestPopups.some((p) => p.regexClassification === "reject" && isConsentPopup(p))) {
1349
+ return hitTestPopups;
1350
+ }
1351
+ const walkPopups = collectPositionedPopups(deadline);
1352
+ return [...walkPopups, ...mergeWalkOverlaps(hitTestPopups, walkPopups)];
1353
+ }
1354
+ var consentPopups = /* @__PURE__ */ new WeakMap();
1355
+ function classificationRank(popup) {
1356
+ return ACCEPTED_LEVELS.tier2.indexOf(popup.regexClassification);
1357
+ }
1358
+ function isConsentPopup(popup) {
1359
+ if (!ACCEPTED_LEVELS.tier2.includes(popup.regexClassification)) {
1360
+ return false;
1361
+ }
1362
+ let result = consentPopups.get(popup);
1363
+ if (result === void 0) {
1364
+ const popupText = popup.text.trim();
1365
+ result = !!popupText && !isExcludedPopup(popupText) && hasHeuristicPatterns(popupText);
1366
+ consentPopups.set(popup, result);
1367
+ }
1368
+ return result;
1369
+ }
1370
+ function mergeWalkOverlaps(hitTestPopups, walkPopups) {
1371
+ return hitTestPopups.filter((h) => {
1372
+ const hitIsConsent = isConsentPopup(h);
1373
+ const absorbs = (p) => !hitIsConsent || isConsentPopup(p) && classificationRank(p) <= classificationRank(h);
1374
+ const walkPopup = walkPopups.find((p) => composedOverlaps(p.element, h.element) && absorbs(p));
1375
+ if (walkPopup) {
1376
+ (walkPopup.hitPoints ??= []).push(...h.hitPoints ?? []);
1377
+ }
1378
+ return !walkPopup;
1379
+ });
1380
+ }
1381
+ function getFramePopups() {
1382
+ if (window.parent && window.parent !== window.top) {
1383
+ return [];
1384
+ }
1385
+ const frameElement = getFramePopupElement(document);
1386
+ const popup = frameElement && toPopupData(frameElement);
1387
+ if (!popup) {
1388
+ return [];
1389
+ }
1390
+ popup.source = "frame";
1391
+ return [popup];
1392
+ }
1393
+ function collectPositionedPopups(deadline) {
1351
1394
  const { elements, shadowHosts } = getPopupLikeElements(deadline);
1352
1395
  const potentialPopups = [];
1353
1396
  const expanded = [];
@@ -1363,17 +1406,86 @@
1363
1406
  if (root !== popup) {
1364
1407
  expanded.push(root.element);
1365
1408
  }
1409
+ root.source = "walk";
1366
1410
  potentialPopups.push(root);
1367
1411
  }
1368
1412
  return potentialPopups.filter((p) => !expanded.some((root) => root !== p.element && composedContains(root, p.element)));
1369
1413
  }
1414
+ var DEFAULT_HIT_TEST_GRID = {
1415
+ columns: [0.1, 0.5, 0.9],
1416
+ // banners sit along the top and bottom edges and modals in the middle; rows 150px in added almost nothing in crawls
1417
+ edgeRows: [30, 90]
1418
+ };
1419
+ function findPopupsByHitTest(deadline, grid) {
1420
+ const climbed = /* @__PURE__ */ new Map();
1421
+ const found = [];
1422
+ const toPopup = (el) => el.querySelector(TEXT_ENTRY_SELECTOR) ? null : toPopupRoot(el);
1423
+ for (const [x, y] of getHitTestPoints(grid)) {
1424
+ if (performance.now() > deadline) {
1425
+ return found;
1426
+ }
1427
+ const path = [];
1428
+ let reached = null;
1429
+ const shouldStop = (el) => {
1430
+ if (climbed.has(el)) {
1431
+ reached = climbed.get(el) ?? null;
1432
+ return true;
1433
+ }
1434
+ const overlapped = found.find((p) => composedOverlaps(p.element, el));
1435
+ if (overlapped) {
1436
+ reached = composedContains(overlapped.element, el) ? overlapped : null;
1437
+ return true;
1438
+ }
1439
+ path.push(el);
1440
+ return false;
1441
+ };
1442
+ const hit = closestHTMLElement(deepElementFromPoint(x, y));
1443
+ const popup = climbToPopup(hit, MAX_HIT_TEST_CLIMB_DEPTH, toPopup, shouldStop);
1444
+ if (popup) {
1445
+ popup.source = "hitTest";
1446
+ found.push(popup);
1447
+ reached = popup;
1448
+ }
1449
+ for (const el of path) {
1450
+ climbed.set(el, reached);
1451
+ }
1452
+ if (reached) {
1453
+ (reached.hitPoints ??= []).push([x, y]);
1454
+ }
1455
+ }
1456
+ return found;
1457
+ }
1458
+ function getHitTestPoints(grid) {
1459
+ const { innerWidth: width, innerHeight: height } = window;
1460
+ const rows = [...grid.edgeRows, height / 2, ...grid.edgeRows.map((y) => height - y)].map((y) => Math.round(y));
1461
+ const ys = [...new Set(rows)].filter((y) => y >= 0 && y < height);
1462
+ return ys.flatMap((y) => grid.columns.map((column) => [Math.round(column * width), y]));
1463
+ }
1464
+ var TEXT_ENTRY_SELECTOR = 'textarea, input:not([type]), input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="tel"], input[type="number"], input[type="url"]';
1465
+ function deepElementFromPoint(x, y) {
1466
+ let el = document.elementFromPoint(x, y);
1467
+ while (el?.shadowRoot) {
1468
+ const inner = el.shadowRoot.elementFromPoint(x, y);
1469
+ if (!inner || inner === el) {
1470
+ break;
1471
+ }
1472
+ el = inner;
1473
+ }
1474
+ return el;
1475
+ }
1476
+ function closestHTMLElement(el) {
1477
+ while (el && !(el instanceof HTMLElement)) {
1478
+ el = el.parentElement;
1479
+ }
1480
+ return el;
1481
+ }
1370
1482
  function toPopupData(el, shadowHosts, text = getElementText(el)) {
1371
1483
  if (!text) {
1372
1484
  return null;
1373
1485
  }
1374
1486
  const buttons = getButtonData(el, shadowHosts);
1375
1487
  classifyButtons(buttons);
1376
- return { text, element: el, buttons };
1488
+ return { text, element: el, buttons, regexClassification: classifyPopup(buttons) };
1377
1489
  }
1378
1490
  function hasPopupButtons(text, buttons) {
1379
1491
  return !isOnlyButtonText(text, buttons) && buttons.some((b) => b.regexClassification !== "other");
@@ -1385,26 +1497,34 @@
1385
1497
  }
1386
1498
  return rest.replace(/\s+/g, "").length < 10;
1387
1499
  }
1500
+ function toPopupRoot(el, shadowHosts) {
1501
+ const text = getElementText(el);
1502
+ if (!text || !hasHeuristicPatterns(text)) {
1503
+ return null;
1504
+ }
1505
+ const popup = toPopupData(el, shadowHosts, text);
1506
+ return popup && hasPopupButtons(text, popup.buttons) ? popup : null;
1507
+ }
1388
1508
  var MAX_FRAGMENT_EXPANSION_DEPTH = 6;
1509
+ var MAX_HIT_TEST_CLIMB_DEPTH = 8;
1510
+ function climbToPopup(el, maxDepth, toPopup, shouldStop) {
1511
+ for (let depth = 0; el && depth < maxDepth; depth++, el = getComposedParent(el)) {
1512
+ if (shouldStop?.(el) || el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1513
+ return null;
1514
+ }
1515
+ const popup = toPopup(el);
1516
+ if (popup) {
1517
+ return popup;
1518
+ }
1519
+ }
1520
+ return null;
1521
+ }
1389
1522
  function expandPopupFragment(popup, shadowHosts) {
1390
1523
  if (hasHeuristicPatterns(popup.text) && hasPopupButtons(popup.text, popup.buttons)) {
1391
1524
  return popup;
1392
1525
  }
1393
- let el = getComposedParent(popup.element);
1394
- for (let depth = 0; el && depth < MAX_FRAGMENT_EXPANSION_DEPTH; depth++, el = getComposedParent(el)) {
1395
- if (el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1396
- break;
1397
- }
1398
- const text = getElementText(el);
1399
- if (!text || !hasHeuristicPatterns(text)) {
1400
- continue;
1401
- }
1402
- const root = toPopupData(el, shadowHosts, text);
1403
- if (root && hasPopupButtons(text, root.buttons)) {
1404
- return root;
1405
- }
1406
- }
1407
- return popup;
1526
+ const parent = getComposedParent(popup.element);
1527
+ return climbToPopup(parent, MAX_FRAGMENT_EXPANSION_DEPTH, (el) => toPopupRoot(el, shadowHosts)) ?? popup;
1408
1528
  }
1409
1529
  function getComposedParent(el) {
1410
1530
  if (el.parentElement) {
@@ -1435,6 +1555,9 @@
1435
1555
  function isRenderedTextElement(el) {
1436
1556
  return el instanceof HTMLElement && !["STYLE", "SCRIPT", "TEMPLATE", "NOSCRIPT", "LINK", "SLOT"].includes(el.tagName) && window.getComputedStyle(el).display !== "none";
1437
1557
  }
1558
+ function composedOverlaps(a, b) {
1559
+ return composedContains(a, b) || composedContains(b, a);
1560
+ }
1438
1561
  function composedContains(container, node) {
1439
1562
  if (container.contains(node)) {
1440
1563
  return true;
@@ -1926,7 +2049,11 @@
1926
2049
  async detectCmp() {
1927
2050
  await new Promise((resolve) => setTimeout(resolve, 0));
1928
2051
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorStart");
1929
- this.popups = getActionablePopups(this.mode, this.autoconsent.config.heuristicPopupSearchTimeout);
2052
+ this.popups = getActionablePopups(
2053
+ this.mode,
2054
+ this.autoconsent.config.heuristicPopupSearchTimeout,
2055
+ this.autoconsent.config.enableHeuristicHitTest
2056
+ );
1930
2057
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorEnd");
1931
2058
  this.autoconsent.config.performanceLoggingEnabled && performance.measure("heuristicDetector", "heuristicDetectorStart", "heuristicDetectorEnd");
1932
2059
  if (this.popups.length > 0) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Autoconsent",
4
- "version": "2026.10.27",
4
+ "version": "2026.10.28",
5
5
  "background": {
6
6
  "scripts": [
7
7
  "background.bundle.js"
@@ -800,6 +800,7 @@
800
800
  },
801
801
  performanceLoggingEnabled: false,
802
802
  heuristicPopupSearchTimeout: 100,
803
+ enableHeuristicHitTest: false,
803
804
  heuristicMode: "off"
804
805
  // heuristic disabled by default
805
806
  };