@duckduckgo/autoconsent 16.59.0 → 16.61.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 (216) hide show
  1. package/.agents/lib/regional-testing/harness.mjs +4 -172
  2. package/.agents/skills/oxylabs-testing/SKILL.md +4 -4
  3. package/.agents/skills/oxylabs-testing/scripts/oxylabs.mjs +17 -164
  4. package/.agents/skills/oxylabs-testing/scripts/oxylabs.test.mjs +6 -3
  5. package/.agents/skills/verify-pr/SKILL.md +1 -1
  6. package/AGENTS.md +1 -1
  7. package/CHANGELOG.md +107 -0
  8. package/Jenkinsfile +36 -1
  9. package/dist/addon-firefox/background.bundle.js +21 -0
  10. package/dist/addon-firefox/compact-rules.json +1 -1
  11. package/dist/addon-firefox/content.bundle.js +253 -93
  12. package/dist/addon-firefox/manifest.json +1 -1
  13. package/dist/addon-firefox/rule-index.json +1 -1
  14. package/dist/addon-firefox/rules.json +1 -1
  15. package/dist/addon-mv3/background.bundle.js +21 -0
  16. package/dist/addon-mv3/compact-rules.json +1 -1
  17. package/dist/addon-mv3/content.bundle.js +253 -93
  18. package/dist/addon-mv3/manifest.json +1 -1
  19. package/dist/addon-mv3/rule-index.json +1 -1
  20. package/dist/addon-mv3/rules.json +1 -1
  21. package/dist/autoconsent.cjs.js +253 -93
  22. package/dist/autoconsent.esm.js +253 -93
  23. package/dist/autoconsent.playwright.js +253 -93
  24. package/dist/autoconsent.standalone.js +254 -94
  25. package/dist/heuristics.cjs.js +29 -3
  26. package/dist/heuristics.mjs +28 -3
  27. package/dist/types/cmps/onetrust.d.ts +1 -0
  28. package/dist/types/eval-snippets.d.ts +3 -0
  29. package/dist/types/heuristic-classify.d.ts +1 -0
  30. package/dist/types/heuristics.d.ts +10 -1
  31. package/lib/cmps/onetrust.ts +15 -0
  32. package/lib/eval-snippets.ts +21 -0
  33. package/lib/heuristic-classify.ts +6 -0
  34. package/lib/heuristic-patterns.ts +26 -1
  35. package/lib/heuristics.ts +256 -104
  36. package/package.json +1 -1
  37. package/playwright/isolated-world.mjs +217 -0
  38. package/playwright/oxylabs.mjs +196 -0
  39. package/playwright/runner.ts +183 -39
  40. package/playwright.config.ts +4 -1
  41. package/rules/autoconsent/aeolservice.json +39 -0
  42. package/rules/autoconsent/airbnb.json +32 -0
  43. package/rules/autoconsent/amazon-music.json +13 -0
  44. package/rules/autoconsent/annunci69.json +19 -0
  45. package/rules/autoconsent/appconsent.json +6 -1
  46. package/rules/autoconsent/arbeidsplassen-nav-no.json +33 -0
  47. package/rules/autoconsent/att.json +36 -0
  48. package/rules/autoconsent/auctionet.json +39 -0
  49. package/rules/autoconsent/autodoc.json +18 -0
  50. package/rules/autoconsent/bbc.com-orb-cosmetic.json +34 -0
  51. package/rules/autoconsent/bbc.com.json +8 -5
  52. package/rules/autoconsent/bokadirekt.se.json +50 -0
  53. package/rules/autoconsent/bouvet-cookiepanel.json +29 -0
  54. package/rules/autoconsent/com-consent-layer.json +10 -0
  55. package/rules/autoconsent/consent-studio.json +25 -13
  56. package/rules/autoconsent/cookielegit.json +21 -0
  57. package/rules/autoconsent/cookieprompter.json +21 -0
  58. package/rules/autoconsent/cookiereports.json +10 -0
  59. package/rules/autoconsent/cookietractor.json +37 -0
  60. package/rules/autoconsent/de-volksbank.json +14 -0
  61. package/rules/autoconsent/dpgmedia-nl.json +20 -8
  62. package/rules/autoconsent/eon.pl.json +18 -0
  63. package/rules/autoconsent/eroticiracconti-it.json +13 -0
  64. package/rules/autoconsent/ersatzteile-24.json +49 -0
  65. package/rules/autoconsent/fastweb-cookie-center.json +13 -0
  66. package/rules/autoconsent/futurabolzano-it.json +35 -0
  67. package/rules/autoconsent/gobasic.json +10 -0
  68. package/rules/autoconsent/gravito.json +23 -16
  69. package/rules/autoconsent/hacon-next-gen.json +41 -0
  70. package/rules/autoconsent/ing-de.json +31 -0
  71. package/rules/autoconsent/interia.json +1 -4
  72. package/rules/autoconsent/intesasanpaolo.json +37 -0
  73. package/rules/autoconsent/jobindex.json +40 -0
  74. package/rules/autoconsent/kaspersky-cookies-notification.json +9 -0
  75. package/rules/autoconsent/lantmateriet.json +13 -0
  76. package/rules/autoconsent/livsmedelsverket.json +18 -0
  77. package/rules/autoconsent/mainichi.json +39 -0
  78. package/rules/autoconsent/mango.json +56 -0
  79. package/rules/autoconsent/nederlandseloterij.json +35 -0
  80. package/rules/autoconsent/newsbreak.json +16 -0
  81. package/rules/autoconsent/nhsapp.json +10 -0
  82. package/rules/autoconsent/nordea.json +21 -0
  83. package/rules/autoconsent/nordnet.json +33 -0
  84. package/rules/autoconsent/ok.json +4 -12
  85. package/rules/autoconsent/onlineradiobox.json +35 -0
  86. package/rules/autoconsent/pe-cookies.json +30 -0
  87. package/rules/autoconsent/pensionsmyndigheten.json +13 -0
  88. package/rules/autoconsent/pro-euclp-cookies-box.json +31 -0
  89. package/rules/autoconsent/pubfactory.json +45 -0
  90. package/rules/autoconsent/puesc-gov-pl.json +37 -0
  91. package/rules/autoconsent/rambler.json +30 -0
  92. package/rules/autoconsent/rejestr-io.json +36 -0
  93. package/rules/autoconsent/sas.json +6 -6
  94. package/rules/autoconsent/scb.se.json +13 -0
  95. package/rules/autoconsent/schumacher.com.json +53 -0
  96. package/rules/autoconsent/sitevision.json +18 -1
  97. package/rules/autoconsent/skatteverket-se.json +33 -0
  98. package/rules/autoconsent/smile-insurances.json +13 -0
  99. package/rules/autoconsent/studentapan.json +16 -0
  100. package/rules/autoconsent/svt.se.json +7 -7
  101. package/rules/autoconsent/temu.json +12 -16
  102. package/rules/autoconsent/thecode.json +21 -0
  103. package/rules/autoconsent/tv2reg-cookie-notice.json +39 -0
  104. package/rules/autoconsent/uk-parliament.json +19 -0
  105. package/rules/autoconsent/wirtualna-polska.json +13 -10
  106. package/rules/autoconsent/xenforo-cookie-consent.json +62 -0
  107. package/rules/autoconsent/zorginstituut-cookiewarning.json +33 -0
  108. package/rules/compact-rules.json +1 -1
  109. package/rules/rule-index.json +1 -1
  110. package/rules/rules.json +1 -1
  111. package/tests/aeolservice.spec.ts +5 -0
  112. package/tests/airbnb.spec.ts +3 -0
  113. package/tests/amazon-music.spec.ts +9 -0
  114. package/tests/annunci69.spec.ts +5 -0
  115. package/tests/appconsent.spec.ts +6 -0
  116. package/tests/arbeidsplassen-nav-no.spec.ts +3 -0
  117. package/tests/att.spec.ts +3 -0
  118. package/tests/auctionet.spec.ts +3 -0
  119. package/tests/autodoc.spec.ts +8 -0
  120. package/tests/bbc.com-orb-cosmetic.spec.ts +8 -0
  121. package/tests/bbc.com.spec.ts +11 -9
  122. package/tests/bokadirekt.se.spec.ts +3 -0
  123. package/tests/bouvet-cookiepanel.spec.ts +9 -0
  124. package/tests/com-consent-layer.spec.ts +6 -0
  125. package/tests/consent-studio.spec.ts +5 -0
  126. package/tests/cookiebot.spec.ts +1 -0
  127. package/tests/cookielegit.spec.ts +3 -0
  128. package/tests/cookieprompter.spec.ts +9 -0
  129. package/tests/cookiereports.spec.ts +13 -0
  130. package/tests/cookietractor.spec.ts +12 -0
  131. package/tests/de-volksbank.spec.ts +5 -0
  132. package/tests/dpgmedia-nl.spec.ts +15 -1
  133. package/tests/eon.pl.spec.ts +3 -0
  134. package/tests/eroticiracconti-it.spec.ts +3 -0
  135. package/tests/ersatzteile-24.spec.ts +6 -0
  136. package/tests/fastweb-cookie-center.spec.ts +3 -0
  137. package/tests/futurabolzano-it.spec.ts +3 -0
  138. package/tests/gobasic.spec.ts +3 -0
  139. package/tests/gravito.spec.ts +7 -1
  140. package/tests/hacon-next-gen.spec.ts +6 -0
  141. package/tests/ing-de.spec.ts +3 -0
  142. package/tests/interia.spec.ts +1 -1
  143. package/tests/intesasanpaolo.spec.ts +3 -0
  144. package/tests/jobindex.spec.ts +3 -0
  145. package/tests/kaspersky-cookies-notification.spec.ts +8 -0
  146. package/tests/lantmateriet.spec.ts +3 -0
  147. package/tests/livsmedelsverket.spec.ts +5 -0
  148. package/tests/mainichi.spec.ts +10 -0
  149. package/tests/mango.spec.ts +3 -0
  150. package/tests/nederlandseloterij.spec.ts +9 -0
  151. package/tests/newsbreak.spec.ts +3 -0
  152. package/tests/nhsapp.spec.ts +3 -0
  153. package/tests/nordea.spec.ts +6 -0
  154. package/tests/nordnet.spec.ts +8 -0
  155. package/tests/onlineradiobox.spec.ts +5 -0
  156. package/tests/pe-cookies.spec.ts +6 -0
  157. package/tests/pensionsmyndigheten.spec.ts +3 -0
  158. package/tests/pro-euclp-cookies-box.spec.ts +8 -0
  159. package/tests/pubfactory.spec.ts +10 -0
  160. package/tests/puesc-gov-pl.spec.ts +3 -0
  161. package/tests/rambler.spec.ts +3 -0
  162. package/tests/rejestr-io.spec.ts +3 -0
  163. package/tests/sas.spec.ts +1 -1
  164. package/tests/scb.se.spec.ts +4 -0
  165. package/tests/schumacher.com.spec.ts +6 -0
  166. package/tests/sitevision-cookie-consent.spec.ts +1 -0
  167. package/tests/sitevision.spec.ts +7 -0
  168. package/tests/skatteverket-se.spec.ts +3 -0
  169. package/tests/smile-insurances.spec.ts +6 -0
  170. package/tests/studentapan.spec.ts +3 -0
  171. package/tests/svt.se.spec.ts +1 -3
  172. package/tests/temu.spec.ts +3 -1
  173. package/tests/thecode.spec.ts +3 -0
  174. package/tests/tv2reg-cookie-notice.spec.ts +16 -0
  175. package/tests/uk-parliament.spec.ts +8 -0
  176. package/tests/wirtualna-polska.spec.ts +6 -1
  177. package/tests/xenforo-cookie-consent.spec.ts +6 -0
  178. package/tests/zorginstituut-cookiewarning.spec.ts +10 -0
  179. package/tests-wtr/heuristics/get-potential-popups.test.ts +330 -9
  180. package/tests-wtr/heuristics/heuristics-utils.test.ts +98 -0
  181. package/rules/autoconsent/twitter.json +0 -34
  182. package/rules/generated/auto_AU_flysas.com_r1s.json +0 -40
  183. package/rules/generated/auto_CH_airbnb.de_83z.json +0 -40
  184. package/rules/generated/auto_CH_autodoc.de_krd.json +0 -40
  185. package/rules/generated/auto_CH_svtplay.se_d39.json +0 -40
  186. package/rules/generated/auto_GB_parliamentlive.tv_r3v.json +0 -28
  187. package/rules/generated/auto_NL_asnbank.nl_e28.json +0 -40
  188. package/rules/generated/auto_NL_eurojackpot.nederlandseloterij.nl_7hr.json +0 -40
  189. package/rules/generated/auto_NL_lotto.nederlandseloterij.nl_7c8.json +0 -40
  190. package/rules/generated/auto_NL_nederlandseloterij.nl_b60.json +0 -40
  191. package/rules/generated/auto_NL_staatsloterij.nederlandseloterij.nl_197.json +0 -40
  192. package/rules/generated/auto_NO_arronet.se_vo5.json +0 -40
  193. package/rules/generated/auto_NO_liseberg.se_qy4.json +0 -40
  194. package/rules/generated/auto_NO_nordnet.se_2en.json +0 -40
  195. package/rules/generated/auto_NO_regeringen.se_uhv.json +0 -40
  196. package/rules/generated/auto_NO_svtplay.se_ujn.json +0 -40
  197. package/rules/generated/auto_NO_uu.se_eea.json +0 -40
  198. package/rules/generated/auto_US_newsbreak.com_0.json +0 -28
  199. package/tests/generated/auto_AU_flysas.com_r1s.spec.ts +0 -2
  200. package/tests/generated/auto_CH_airbnb.de_83z.spec.ts +0 -2
  201. package/tests/generated/auto_CH_autodoc.de_krd.spec.ts +0 -2
  202. package/tests/generated/auto_CH_svtplay.se_d39.spec.ts +0 -2
  203. package/tests/generated/auto_GB_parliamentlive.tv_r3v.spec.ts +0 -6
  204. package/tests/generated/auto_NL_asnbank.nl_e28.spec.ts +0 -6
  205. package/tests/generated/auto_NL_eurojackpot.nederlandseloterij.nl_7hr.spec.ts +0 -6
  206. package/tests/generated/auto_NL_lotto.nederlandseloterij.nl_7c8.spec.ts +0 -6
  207. package/tests/generated/auto_NL_nederlandseloterij.nl_b60.spec.ts +0 -6
  208. package/tests/generated/auto_NL_staatsloterij.nederlandseloterij.nl_197.spec.ts +0 -6
  209. package/tests/generated/auto_NO_arronet.se_vo5.spec.ts +0 -2
  210. package/tests/generated/auto_NO_liseberg.se_qy4.spec.ts +0 -2
  211. package/tests/generated/auto_NO_nordnet.se_2en.spec.ts +0 -2
  212. package/tests/generated/auto_NO_regeringen.se_uhv.spec.ts +0 -6
  213. package/tests/generated/auto_NO_svtplay.se_ujn.spec.ts +0 -2
  214. package/tests/generated/auto_NO_uu.se_eea.spec.ts +0 -2
  215. package/tests/generated/auto_US_newsbreak.com_0.spec.ts +0 -2
  216. package/tests/twitter.spec.ts +0 -10
@@ -13,16 +13,8 @@
13
13
  /** @typedef {import('../../../lib/types.js').Config} Config */
14
14
 
15
15
  /**
16
- * Primitives the message handler uses to talk to a specific frame's content script.
17
- * `frameRef` is the isolated world's execution-context uniqueId the message arrived from;
18
- * the transport resolves it to the owning CDP session and the matching page-world context.
19
- * @typedef {Object} MessageTransport
20
- * @property {(frameRef: string, message: object) => Promise<void>} sendToContentScript
21
- * @property {(frameRef: string, code: string) => Promise<any>} evalInMainWorld
22
- */
23
-
24
- /**
25
- * @typedef {(transport: MessageTransport) => (msg: any, frameRef: string) => Promise<void>} MessageHandlerFactory
16
+ * @typedef {import('../../../playwright/isolated-world.mjs').MessageTransport} MessageTransport
17
+ * @typedef {import('../../../playwright/isolated-world.mjs').MessageHandlerFactory} MessageHandlerFactory
26
18
  */
27
19
 
28
20
  /**
@@ -86,6 +78,7 @@
86
78
  import fs from 'fs';
87
79
  import path from 'path';
88
80
  import { fileURLToPath } from 'url';
81
+ import { injectIntoIsolatedWorld } from '../../../playwright/isolated-world.mjs';
89
82
 
90
83
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
91
84
  export const projectRoot = path.resolve(__dirname, '../../..');
@@ -185,7 +178,7 @@ export async function injectAutoconsent(page, options, provider, extension = {})
185
178
  if (browserName && browserName !== 'chromium') {
186
179
  throw new Error(`Regional testing supports Chromium only (got "${browserName}").`);
187
180
  }
188
- await injectIntoIsolatedWorld(page, createMessageHandler, extension.script ?? '');
181
+ await injectIntoIsolatedWorld(page, createMessageHandler, contentScript, extension.script ?? '');
189
182
 
190
183
  function hasMessage(/** @type {string} */ type) {
191
184
  return received.some((m) => m.type === type);
@@ -259,167 +252,6 @@ export async function injectAutoconsent(page, options, provider, extension = {})
259
252
  return { received, hasMessage, waitForCompletion, waitForMessage, collectResult };
260
253
  }
261
254
 
262
- /**
263
- * Isolated-world injection via CDP (Chromium only). For each frame we create a dedicated isolated
264
- * world via `Page.createIsolatedWorld`, then inject the content script there and bridge messages
265
- * over a per-context CDP binding.
266
- *
267
- * @param {Page} page
268
- * @param {MessageHandlerFactory} createMessageHandler
269
- * @param {string} extraScript - Runs in each isolated world after the content script.
270
- */
271
- async function injectIntoIsolatedWorld(page, createMessageHandler, extraScript) {
272
- // Isolated world name: `<prefix><pageWorldUniqueId><separator><frameId>`. Encoding the page-world
273
- // uniqueId lets us later run eval snippets in that frame's main world.
274
- const WORLD_PREFIX = 'autoconsent_iw_';
275
- const WORLD_SEPARATOR = '_frame_';
276
- const BINDING_PREFIX = 'autoconsentSendMessage_';
277
-
278
- /** @type {Map<string, string>} isolated-world uniqueId -> page (main) world uniqueId */
279
- const isolated2pageWorld = new Map();
280
- /** @type {Map<string, any>} isolated-world uniqueId -> CDP session that owns it */
281
- const sessionByContext = new Map();
282
- /** @type {Map<string, string>} binding name -> isolated-world uniqueId */
283
- const contextByBinding = new Map();
284
-
285
- const handle = createMessageHandler({
286
- sendToContentScript: async (isolatedUniqueId, message) => {
287
- const client = sessionByContext.get(isolatedUniqueId);
288
- if (!client) return;
289
- try {
290
- await client.send('Runtime.evaluate', {
291
- expression: `autoconsentReceiveMessage(${JSON.stringify(message)})`,
292
- uniqueContextId: isolatedUniqueId,
293
- awaitPromise: true,
294
- // Some pages' CSP would otherwise block evaluating in the isolated world.
295
- allowUnsafeEvalBlockedByCSP: true,
296
- });
297
- } catch {
298
- // The context may be gone if the frame navigated or detached.
299
- }
300
- },
301
- evalInMainWorld: async (isolatedUniqueId, code) => {
302
- const client = sessionByContext.get(isolatedUniqueId);
303
- const pageWorldUniqueId = isolated2pageWorld.get(isolatedUniqueId);
304
- if (!client || !pageWorldUniqueId) return false;
305
- const { result, exceptionDetails } = await client.send('Runtime.evaluate', {
306
- expression: code,
307
- uniqueContextId: pageWorldUniqueId,
308
- returnByValue: true,
309
- awaitPromise: true,
310
- // Eval snippets must run even when the page's CSP disallows eval.
311
- allowUnsafeEvalBlockedByCSP: true,
312
- });
313
- return exceptionDetails ? false : (result?.value ?? false);
314
- },
315
- });
316
-
317
- async function attachToSession(/** @type {any} */ client) {
318
- client.on('Runtime.executionContextCreated', async (/** @type {any} */ event) => {
319
- const { context } = event;
320
- const frameId = context.auxData?.frameId;
321
-
322
- // Our isolated world finished initializing: wire up its binding and content script.
323
- if (context.auxData?.type === 'isolated' && typeof context.name === 'string' && context.name.startsWith(WORLD_PREFIX)) {
324
- const separatorIndex = context.name.indexOf(WORLD_SEPARATOR);
325
- const pageWorldUniqueId = context.name.slice(WORLD_PREFIX.length, separatorIndex);
326
- const intendedFrameId = context.name.slice(separatorIndex + WORLD_SEPARATOR.length);
327
- // Chromium may create the named world in other frames too; keep only the one we asked for.
328
- if (intendedFrameId !== frameId) return;
329
-
330
- isolated2pageWorld.set(context.uniqueId, pageWorldUniqueId);
331
- sessionByContext.set(context.uniqueId, client);
332
-
333
- const bindingName = `${BINDING_PREFIX}${context.uniqueId.replace(/\W/g, '_')}`;
334
- contextByBinding.set(bindingName, context.uniqueId);
335
- try {
336
- await client.send('Runtime.addBinding', { name: bindingName, executionContextName: context.name });
337
- // CDP bindings take a single string arg, so wrap it in the shape the content script expects.
338
- await client.send('Runtime.evaluate', {
339
- expression: `window.autoconsentSendMessage = (m) => { window.${bindingName}(JSON.stringify(m)); return Promise.resolve(); };\n${contentScript}\n${extraScript}`,
340
- uniqueContextId: context.uniqueId,
341
- allowUnsafeEvalBlockedByCSP: true,
342
- });
343
- } catch {
344
- // The frame may have navigated or detached before we finished wiring it up.
345
- }
346
- return;
347
- }
348
-
349
- // A regular page (main) world: request an isolated world for it, tagged with its id.
350
- if (!frameId || context.auxData?.type !== 'default' || !context.origin || context.origin === '://') return;
351
- try {
352
- await client.send('Page.createIsolatedWorld', {
353
- frameId,
354
- worldName: `${WORLD_PREFIX}${context.uniqueId}${WORLD_SEPARATOR}${frameId}`,
355
- });
356
- } catch {
357
- // The frame may have navigated or detached.
358
- }
359
- });
360
-
361
- client.on('Runtime.executionContextDestroyed', (/** @type {any} */ event) => {
362
- const uniqueId = event.executionContextUniqueId;
363
- if (!uniqueId) return;
364
- isolated2pageWorld.delete(uniqueId);
365
- sessionByContext.delete(uniqueId);
366
- });
367
-
368
- client.on('Runtime.bindingCalled', (/** @type {any} */ event) => {
369
- const isolatedUniqueId = contextByBinding.get(event.name);
370
- if (!isolatedUniqueId) return;
371
- let msg;
372
- try {
373
- msg = JSON.parse(event.payload);
374
- } catch {
375
- return;
376
- }
377
- handle(msg, isolatedUniqueId);
378
- });
379
-
380
- // Page must be enabled before createIsolatedWorld; Runtime.enable replays existing contexts.
381
- await client.send('Page.enable');
382
- await client.send('Runtime.enable');
383
- }
384
-
385
- // The page session covers the main frame and all same-process (in-process) iframes.
386
- await attachToSession(await page.context().newCDPSession(page));
387
-
388
- // Out-of-process iframes (OOPIFs) are separate CDP targets, so each needs its own session.
389
- // newCDPSession throws for in-process frames, which are already handled above.
390
- const attachedFrames = new WeakSet();
391
- async function attachToOopif(/** @type {import('playwright').Frame} */ frame) {
392
- if (!frame.parentFrame() || attachedFrames.has(frame)) return;
393
- // Mark synchronously (before any await) so concurrent frameattached/framenavigated events
394
- // can't open duplicate sessions for the same frame. Unmark on failure so a later event retries.
395
- attachedFrames.add(frame);
396
- let client;
397
- try {
398
- client = await page.context().newCDPSession(frame);
399
- } catch {
400
- // In-process frame (already covered by the page session) or the frame detached.
401
- attachedFrames.delete(frame);
402
- return;
403
- }
404
- try {
405
- await attachToSession(client);
406
- } catch {
407
- // Wiring up the session failed (e.g. the OOPIF navigated/detached). Detach the
408
- // half-initialized session before unmarking, otherwise a retry would open a second
409
- // session for the same frame, leaving duplicate listeners and possible double injection.
410
- try {
411
- await client.detach();
412
- } catch {
413
- // The session may already be gone.
414
- }
415
- attachedFrames.delete(frame);
416
- }
417
- }
418
- page.on('frameattached', attachToOopif);
419
- page.on('framenavigated', attachToOopif);
420
- await Promise.all(page.frames().map(attachToOopif));
421
- }
422
-
423
255
  /**
424
256
  * A result with nothing detected, optionally carrying an error.
425
257
  * @param {string} url
@@ -69,7 +69,7 @@ Options: everything `testPage` takes in `proxy-testing` (`action`, `screenshotsD
69
69
  | Option | Default | Description |
70
70
  |--------|---------|-------------|
71
71
  | `device` | `'desktop'` | `'desktop' \| 'mobile'` — sets [`?p_device=`](https://developers.oxylabs.io/products/agent-browser/device-type) (viewport, touch, headers, User-Agent). Use it for mobile tests instead of Playwright device descriptors, which would contradict the Oxylabs fingerprint. |
72
- | `solveCaptcha` | `false` | Sends [`?solve_captcha=true`](https://developers.oxylabs.io/products/agent-browser/captcha-handling) so Oxylabs solves captchas on the page, and waits for the solver (see Gotchas). Accounts without the feature get HTTP 400 for every session, so leave it off unless captcha solving is enabled. |
72
+ | `solveCaptcha` | `true` | Sends [`?solve_captcha=true`](https://developers.oxylabs.io/products/agent-browser/captcha-handling) so Oxylabs solves captchas on the page, and waits for the solver (see Gotchas). Our account has captcha solving enabled; an account without it gets HTTP 400 for every session. |
73
73
 
74
74
  Screenshots default to `test-results/oxylabs`, named `<domain>-<region>-oxylabs-final.jpg`.
75
75
 
@@ -85,13 +85,13 @@ Any two-letter country code maps to `?p_cc=` (e.g. `de` → `p_cc=DE`), per the
85
85
 
86
86
  ## Architecture
87
87
 
88
- Injection and result collection are shared with `proxy-testing` in [.agents/lib/regional-testing/harness.mjs](../../lib/regional-testing/harness.mjs): autoconsent runs in an isolated world of every frame (including out-of-process iframes) and talks to Node over CDP bindings, and `eval` snippets run in the page's main world. This skill only adds the Oxylabs connection and the captcha handling.
88
+ Injection and result collection are shared with `proxy-testing` in [.agents/lib/regional-testing/harness.mjs](../../lib/regional-testing/harness.mjs): autoconsent runs in an isolated world of every frame (including out-of-process iframes) and talks to Node over CDP bindings, and `eval` snippets run in the page's main world. The isolated-world transport itself is [playwright/isolated-world.mjs](../../../playwright/isolated-world.mjs), which the Playwright E2E runner also uses for Chromium. The Oxylabs connection and the captcha tracking live in [playwright/oxylabs.mjs](../../../playwright/oxylabs.mjs), shared with the E2E runner's Oxylabs pass in CI.
89
89
 
90
- With `solveCaptcha: true`, a listener in autoconsent's isolated world forwards the Oxylabs runtime's captcha `window` messages to Node over the same CDP binding, so they arrive even if the page navigates right after.
90
+ With captcha solving on, a listener in autoconsent's isolated world forwards the Oxylabs runtime's captcha `window` messages to Node over the same CDP binding, so they arrive even if the page navigates right after.
91
91
 
92
92
  ## Gotchas
93
93
 
94
- - **CAPTCHA events arrive via `window.postMessage`, not CDP.** With `solveCaptcha: true`, autoconsent's wait pauses whenever a start event arrives, until `oxylabs-captcha-end`, `oxylabs-captcha-solve-end` or `oxylabs-captcha-error`, for at most 60s from the first start (the timeout in Oxylabs' example). Oxylabs documents no deadline for the start event, so captcha-free pages aren't held up waiting for one. Agent Browser sends `oxylabs-captcha-solve-start` with a `captchaType` (e.g. `turnstile`); the documented `oxylabs-captcha-start` is accepted too. A Cloudflare Turnstile took about 35-40s to solve. Hard blocks without a captcha (e.g. DataDome "Access is temporarily restricted", Akamai "Access denied") send no events, so solving doesn't help there.
94
+ - **CAPTCHA events arrive via `window.postMessage`, not CDP.** With captcha solving on, autoconsent's wait pauses whenever a start event arrives, until `oxylabs-captcha-end`, `oxylabs-captcha-solve-end` or `oxylabs-captcha-error`, for at most 60s from the first start (the timeout in Oxylabs' example). Oxylabs documents no deadline for the start event, so captcha-free pages aren't held up waiting for one. Agent Browser sends `oxylabs-captcha-solve-start` with a `captchaType` (e.g. `turnstile`); the documented `oxylabs-captcha-start` is accepted too. A Cloudflare Turnstile took about 35-40s to solve. Hard blocks without a captcha (e.g. DataDome "Access is temporarily restricted", Akamai "Access denied") send no events, so solving doesn't help there.
95
95
  - **Main-world CDP bindings don't work on Agent Browser.** `page.exposeBinding`, and `Runtime.addBinding` without `executionContextName`, never reach the page. Bindings scoped to an isolated world (what the harness uses) work, and `window` messages reach isolated-world listeners, so listen there.
96
96
  - **Limits:** 100 concurrent sessions and 10 new sessions per second per account. Going over the per-second limit fails `connectOverCDP` with `WebSocket error: ... 429 Too Many Requests`, and the library does not retry it. `testRegions` opens sessions one at a time, but a script that opens sessions in parallel (several `withOxylabsPage` calls in a `Promise.all`, or several such scripts at once) can easily send 10 within a second. Stagger the session opens (for example 200ms apart), and rerun any test that failed with a 429. A 429 is not a result about the site.
97
97
  - **Call injection before `page.goto`**, so autoconsent is in place before page scripts run.
@@ -12,20 +12,13 @@
12
12
 
13
13
  /**
14
14
  * @typedef {import('playwright').Page} Page
15
- * @typedef {import('playwright').Browser} Browser
16
15
  * @typedef {import('../../../lib/regional-testing/harness.mjs').TestResult} TestResult
17
16
  * @typedef {import('../../../lib/regional-testing/harness.mjs').AutoconsentContext} AutoconsentContext
18
17
  * @typedef {import('../../../lib/regional-testing/harness.mjs').Provider} Provider
19
18
  */
20
19
 
21
20
  /**
22
- * @typedef {'desktop'|'mobile'} OxylabsDevice
23
- */
24
-
25
- /**
26
- * @typedef {Object} OxylabsBrowserOptions
27
- * @property {OxylabsDevice} [device] - Oxylabs ?p_device= value. Oxylabs defaults to 'desktop'.
28
- * @property {boolean} [solveCaptcha=false] - Send ?solve_captcha=true: let Oxylabs solve captchas on the page. Needs the feature enabled on the account.
21
+ * @typedef {import('../../../../playwright/oxylabs.mjs').OxylabsBrowserOptions} OxylabsBrowserOptions
29
22
  */
30
23
 
31
24
  /**
@@ -40,7 +33,7 @@
40
33
  */
41
34
 
42
35
  import path from 'path';
43
- import { chromium } from 'playwright';
36
+ import { connectOxylabs, createCaptchaTracker, redactCredentials } from '../../../../playwright/oxylabs.mjs';
44
37
  import {
45
38
  CORE_REGIONS,
46
39
  emptyResult,
@@ -50,81 +43,14 @@ import {
50
43
  } from '../../../lib/regional-testing/harness.mjs';
51
44
 
52
45
  export { ALL_REGIONS, CORE_REGIONS, EXPANDED_REGIONS, formatResult } from '../../../lib/regional-testing/harness.mjs';
53
-
54
- /**
55
- * US state- and city-targeted region keys, in addition to any two-letter country code. Query params
56
- * follow https://developers.oxylabs.io/products/agent-browser/geolocation-and-proxy-selection
57
- * (`p_state` is US-only; `p_city` needs `p_cc` or `p_state`).
58
- * @type {Record<string, Record<string, string>>}
59
- */
60
- export const OXYLABS_LOCAL_REGIONS = {
61
- 'us-california': { p_state: 'california' },
62
- 'us-newyork': { p_cc: 'US', p_city: 'new_york' },
63
- 'us-losangeles': { p_cc: 'US', p_city: 'los_angeles' },
64
- };
65
-
66
- /**
67
- * @param {string} regionKey - A two-letter country code (e.g. 'de', 'pt') or a key of OXYLABS_LOCAL_REGIONS.
68
- * @returns {Record<string, string>}
69
- */
70
- export function oxylabsRegionParams(regionKey) {
71
- if (Object.prototype.hasOwnProperty.call(OXYLABS_LOCAL_REGIONS, regionKey)) {
72
- return OXYLABS_LOCAL_REGIONS[regionKey];
73
- }
74
- if (/^[a-z]{2}$/i.test(regionKey)) {
75
- return { p_cc: regionKey.toUpperCase() };
76
- }
77
- throw new Error(
78
- `Unknown Oxylabs region "${regionKey}". Use a two-letter country code or one of: ${Object.keys(OXYLABS_LOCAL_REGIONS).join(', ')}`,
79
- );
80
- }
81
-
82
- /**
83
- * @param {NodeJS.ProcessEnv} [env]
84
- */
85
- export function isOxylabsConfigured(env = process.env) {
86
- return Boolean(env.OXYLABS_USER && env.OXYLABS_PASSWORD);
87
- }
88
-
89
- /**
90
- * Build the Oxylabs CDP endpoint for a region. The result contains credentials: never log it.
91
- * @param {string} regionKey
92
- * @param {OxylabsBrowserOptions} [opts]
93
- * @param {NodeJS.ProcessEnv} [env]
94
- */
95
- export function buildOxylabsEndpoint(regionKey, opts = {}, env = process.env) {
96
- const user = env.OXYLABS_USER;
97
- const password = env.OXYLABS_PASSWORD;
98
- const host = env.OXYLABS_HOST || 'hb.oxylabs.io';
99
- if (!user || !password) {
100
- throw new Error('Missing Oxylabs credentials. Set OXYLABS_USER and OXYLABS_PASSWORD.');
101
- }
102
- const params = new URLSearchParams(oxylabsRegionParams(regionKey));
103
- if (opts.device) {
104
- params.set('p_device', opts.device);
105
- }
106
- // Accounts without captcha solving get HTTP 400 for this param, so only send it when asked.
107
- if (opts.solveCaptcha) {
108
- params.set('solve_captcha', 'true');
109
- }
110
- // user/password are interpolated raw — Oxylabs expects literal characters in
111
- // userinfo (e.g. '+' must NOT be percent-encoded), matching their Python/JS
112
- // SDK examples. If your credentials contain ':' or '@', escape upstream.
113
- return `wss://${user}:${password}@${host}/?${params.toString()}`;
114
- }
115
-
116
- /**
117
- * Playwright puts the endpoint URL in connection error messages; strip the credentials.
118
- * @param {string} message
119
- * @param {NodeJS.ProcessEnv} [env]
120
- */
121
- export function redactCredentials(message, env = process.env) {
122
- let out = message;
123
- for (const secret of [env.OXYLABS_PASSWORD, env.OXYLABS_USER]) {
124
- if (secret) out = out.split(secret).join('***');
125
- }
126
- return out;
127
- }
46
+ export {
47
+ OXYLABS_LOCAL_REGIONS,
48
+ buildOxylabsEndpoint,
49
+ connectOxylabs,
50
+ isOxylabsConfigured,
51
+ oxylabsRegionParams,
52
+ redactCredentials,
53
+ } from '../../../../playwright/oxylabs.mjs';
128
54
 
129
55
  /**
130
56
  * @param {TestResult} result
@@ -136,95 +62,22 @@ function redactResult(result) {
136
62
  }
137
63
 
138
64
  /**
139
- * Connect to an Oxylabs remote browser for a specific region.
140
- * @param {string} regionKey
141
- * @param {OxylabsBrowserOptions} [opts]
142
- * @returns {Promise<Browser>}
143
- */
144
- export async function connectOxylabs(regionKey, opts = {}) {
145
- try {
146
- return await chromium.connectOverCDP(buildOxylabsEndpoint(regionKey, opts), { timeout: 30000 });
147
- } catch (e) {
148
- // The first line has the cause; the call log after it repeats the endpoint URL.
149
- const message = redactCredentials((e instanceof Error ? e.message : String(e)).split('\n')[0]);
150
- const hint = opts.solveCaptcha && message.includes('400') ? ' (is captcha solving enabled for this Oxylabs account?)' : '';
151
- throw new Error(message + hint);
152
- }
153
- }
154
-
155
- // Captcha events arrive as window messages from the Oxylabs runtime. A listener in autoconsent's
156
- // isolated world forwards them over its CDP binding: Agent Browser drops main-world bindings, so
157
- // page.exposeBinding can't deliver them. See https://developers.oxylabs.io/products/agent-browser/captcha-handling
158
- const CAPTCHA_MESSAGE = 'oxylabsCaptcha';
159
- // Agent Browser sends `oxylabs-captcha-solve-start`; the docs list `-start`, `-end`, `-solve-end` and `-error`.
160
- const CAPTCHA_START_EVENTS = ['oxylabs-captcha-start', 'oxylabs-captcha-solve-start'];
161
- const CAPTCHA_END_EVENTS = ['oxylabs-captcha-end', 'oxylabs-captcha-solve-end'];
162
- const CAPTCHA_ERROR_EVENTS = ['oxylabs-captcha-error', 'oxylabs-captcha-solve-error'];
163
- // Longest a test pauses for solving, counted from the first start event: the wait in Oxylabs' example.
164
- const CAPTCHA_SOLVE_TIMEOUT_MS = 60000;
165
- const captchaListenerScript = `
166
- window.addEventListener("message", (e) => {
167
- if (e?.data?.source === "oxylabs-runtime" && typeof e.data.type === "string") {
168
- window.autoconsentSendMessage({ type: "${CAPTCHA_MESSAGE}", event: e.data.type });
169
- }
170
- });
171
- `;
172
-
173
- /**
174
- * Inject autoconsent into a page's isolated world via CDP and, with `solveCaptcha`, forward
65
+ * Inject autoconsent into a page's isolated world via CDP and, unless `solveCaptcha` is false, forward
175
66
  * Oxylabs captcha events to the Node side. Call BEFORE navigating to the target URL.
176
67
  * @param {Page} page
177
68
  * @param {Partial<TestOptions>} [options]
178
69
  * @returns {Promise<OxylabsAutoconsentContext>}
179
70
  */
180
71
  export async function injectAutoconsent(page, options = {}) {
181
- /** @type {{ detected: boolean, solved: boolean|null }} */
182
- const captcha = { detected: false, solved: null };
183
- /** @type {() => void} */
184
- let resolveCaptcha = () => {};
185
- /** @type {Promise<void>} */
186
- const captchaPromise = new Promise((resolve) => {
187
- resolveCaptcha = resolve;
188
- });
189
-
190
- let solving = false;
191
- /** @type {number|null} */
192
- let firstStartAt = null;
193
-
194
- function handleCaptchaEvent(/** @type {string} */ type) {
195
- if (CAPTCHA_START_EVENTS.includes(type)) {
196
- captcha.detected = true;
197
- solving = true;
198
- firstStartAt ??= Date.now();
199
- } else if (CAPTCHA_END_EVENTS.includes(type)) {
200
- captcha.detected = true;
201
- captcha.solved = true;
202
- solving = false;
203
- resolveCaptcha();
204
- } else if (CAPTCHA_ERROR_EVENTS.includes(type)) {
205
- captcha.detected = true;
206
- captcha.solved = false;
207
- solving = false;
208
- resolveCaptcha();
209
- }
210
- }
211
-
212
- // Oxylabs gives no deadline for a start event, so autoconsent's wait pauses whenever one arrives.
213
- const isSolving = () => solving && firstStartAt !== null && Date.now() - firstStartAt < CAPTCHA_SOLVE_TIMEOUT_MS;
214
-
72
+ const tracker = createCaptchaTracker();
215
73
  /** @type {import('../../../lib/regional-testing/harness.mjs').IsolatedWorldExtension} */
216
- const captchaListener = {
217
- script: captchaListenerScript,
218
- onMessage: (msg) => {
219
- if (msg?.type === CAPTCHA_MESSAGE) handleCaptchaEvent(msg.event);
220
- },
221
- };
222
- const ctx = await injectThrough(page, options, 'oxylabs', options.solveCaptcha ? captchaListener : {});
74
+ const captchaListener = { script: tracker.script, onMessage: tracker.handleMessage };
75
+ const ctx = await injectThrough(page, options, 'oxylabs', (options.solveCaptcha ?? true) ? captchaListener : {});
223
76
  return {
224
77
  ...ctx,
225
- waitForCompletion: (timeout, detectionTimeout) => ctx.waitForCompletion(timeout, detectionTimeout, isSolving),
226
- captcha,
227
- captchaPromise,
78
+ waitForCompletion: (timeout, detectionTimeout) => ctx.waitForCompletion(timeout, detectionTimeout, tracker.isSolving),
79
+ captcha: tracker.state,
80
+ captchaPromise: tracker.done,
228
81
  };
229
82
  }
230
83
 
@@ -15,10 +15,13 @@ describe('oxylabs config', () => {
15
15
  });
16
16
 
17
17
  it('builds the endpoint with raw credentials and query params', () => {
18
- assert.equal(buildOxylabsEndpoint('de', { device: 'mobile' }, env), 'wss://user:p+ss@hb.oxylabs.io/?p_cc=DE&p_device=mobile');
19
18
  assert.equal(
20
- buildOxylabsEndpoint('us', { solveCaptcha: true }, { ...env, OXYLABS_HOST: 'proxy.example' }),
21
- 'wss://user:p+ss@proxy.example/?p_cc=US&solve_captcha=true',
19
+ buildOxylabsEndpoint('de', { device: 'mobile' }, env),
20
+ 'wss://user:p+ss@hb.oxylabs.io/?p_cc=DE&p_device=mobile&solve_captcha=true',
21
+ );
22
+ assert.equal(
23
+ buildOxylabsEndpoint('us', { solveCaptcha: false }, { ...env, OXYLABS_HOST: 'proxy.example' }),
24
+ 'wss://user:p+ss@proxy.example/?p_cc=US',
22
25
  );
23
26
  });
24
27
 
@@ -47,7 +47,7 @@ If any check fails, fix the issue and re-run before proceeding.
47
47
 
48
48
  **GitHub Actions** (`.github/workflows/checks.yml`): Runs `lint` and `test:lib`. Must pass.
49
49
 
50
- **Jenkins**: Runs Playwright E2E in 9 regions (US, GB, AU, CA, DE, FR, NL, CH, NO) for modified rules only. Posts a PR comment with artifact ZIP and [review tool](https://zok.pw/autoconsent-review-tool/) link for inspecting screenshots.
50
+ **Jenkins**: Runs Playwright E2E in 9 regions (US, GB, AU, CA, DE, FR, NL, CH, NO) for modified rules only. On Chrome, tests that fail on a bot wall (navigation error, or no CMP on an error or challenge page) are rerun in an Oxylabs browser in the same region, which solves captchas, with one retry (`results-<REGION>-oxylabs.xml`, screenshots under `screenshots/oxylabs/`); a pass there counts as a pass. Posts a PR comment with artifact ZIP and [review tool](https://zok.pw/autoconsent-review-tool/) link for inspecting screenshots.
51
51
 
52
52
  ### Flaky E2E failures
53
53
 
package/AGENTS.md CHANGED
@@ -77,7 +77,7 @@ Site-specific rules are rules scoped to specific sites with a `urlPattern`. Rule
77
77
 
78
78
  ### JSON Rules vs Code-based rules
79
79
 
80
- JSON rules live in `rules/autoconsent/` (hand-maintained) and `rules/generated/` (auto-generated). Each file defines one CMP rule following the `AutoConsentCMPRule` type in `lib/rules.ts`. **Add new JSON rules only to `rules/autoconsent/`.** Do not add new files to `rules/generated/`: only the crawler creates them. You may remove a generated rule, for example when it is stale.
80
+ JSON rules live in `rules/autoconsent/` (hand-maintained) and `rules/generated/` (auto-generated). Each file defines one CMP rule following the `AutoConsentCMPRule` type in `lib/rules.ts`. **Add new JSON rules only to `rules/autoconsent/`.** Do not add new files to `rules/generated/`: only the crawler creates them. You may remove a generated rule, for example when it is stale. Before removing any rule, test every site in its spec with the rule removed, in the core regions and every region the spec runs in, and confirm that another rule or the heuristic handles each one, or that the site no longer shows the popup.
81
81
 
82
82
  ```json
83
83
  {
package/CHANGELOG.md CHANGED
@@ -1,3 +1,110 @@
1
+ # v16.61.0 (Sat Oct 24 2026)
2
+
3
+ #### Rules
4
+
5
+ - Rule update: studentapan [#2107](https://github.com/duckduckgo/autoconsent/pull/2107) ([@daxtheduck](https://github.com/daxtheduck))
6
+ - Rule update: AppConsent [#2108](https://github.com/duckduckgo/autoconsent/pull/2108) ([@daxtheduck](https://github.com/daxtheduck))
7
+ - Rule update: scb.se [#2095](https://github.com/duckduckgo/autoconsent/pull/2095) ([@daxtheduck](https://github.com/daxtheduck))
8
+ - Rule update: newsbreak, remove auto_US_newsbreak.com_0 [#2096](https://github.com/duckduckgo/autoconsent/pull/2096) ([@daxtheduck](https://github.com/daxtheduck))
9
+ - Rule update: fastweb-cookie-center [#2097](https://github.com/duckduckgo/autoconsent/pull/2097) ([@daxtheduck](https://github.com/daxtheduck))
10
+ - Rule update: gobasic [#2098](https://github.com/duckduckgo/autoconsent/pull/2098) ([@daxtheduck](https://github.com/daxtheduck))
11
+ - Rule update: com-consent-layer [#2099](https://github.com/duckduckgo/autoconsent/pull/2099) ([@daxtheduck](https://github.com/daxtheduck))
12
+ - Rule update: airbnb, remove auto_CH_airbnb.de_83z [#2100](https://github.com/duckduckgo/autoconsent/pull/2100) ([@daxtheduck](https://github.com/daxtheduck))
13
+ - Rule update: futurabolzano.it [#2101](https://github.com/duckduckgo/autoconsent/pull/2101) ([@daxtheduck](https://github.com/daxtheduck))
14
+ - Rule update: livsmedelsverket [#2102](https://github.com/duckduckgo/autoconsent/pull/2102) ([@daxtheduck](https://github.com/daxtheduck))
15
+ - Rule update: tv2reg-cookie-notice [#2104](https://github.com/duckduckgo/autoconsent/pull/2104) ([@daxtheduck](https://github.com/daxtheduck))
16
+ - Rule update: nhsapp [#2105](https://github.com/duckduckgo/autoconsent/pull/2105) ([@daxtheduck](https://github.com/daxtheduck))
17
+ - Rule update: sas, remove auto_AU_flysas.com_r1s [#2106](https://github.com/duckduckgo/autoconsent/pull/2106) ([@daxtheduck](https://github.com/daxtheduck))
18
+
19
+ #### Authors: 1
20
+
21
+ - Dax ([@daxtheduck](https://github.com/daxtheduck))
22
+
23
+ ---
24
+
25
+ # v16.60.0 (Fri Oct 23 2026)
26
+
27
+ #### Rules
28
+
29
+ - Rule update: CPM crawl #529: autodoc.co.uk [#1534](https://github.com/duckduckgo/autoconsent/pull/1534) ([@cursoragent](https://github.com/cursoragent) [@muodov](https://github.com/muodov) [@daxtheduck](https://github.com/daxtheduck))
30
+ - Rule update: Cookie popup on www.lovethesales.com [#2089](https://github.com/duckduckgo/autoconsent/pull/2089) ([@daxtheduck](https://github.com/daxtheduck))
31
+ - Rule update: Cookie notice on schumacher.com [#2088](https://github.com/duckduckgo/autoconsent/pull/2088) ([@daxtheduck](https://github.com/daxtheduck))
32
+ - Rule update: Cookie popup on www.elibrary.imf.org [#2091](https://github.com/duckduckgo/autoconsent/pull/2091) ([@daxtheduck](https://github.com/daxtheduck))
33
+ - Rule update: cookie pop up shop.mango.com [#2092](https://github.com/duckduckgo/autoconsent/pull/2092) ([@daxtheduck](https://github.com/daxtheduck))
34
+ - Rule update: CPM crawl #1276: parliament.uk [#2093](https://github.com/duckduckgo/autoconsent/pull/2093) ([@daxtheduck](https://github.com/daxtheduck))
35
+ - Rule update: CPM crawl #252: malmo.se [#2086](https://github.com/duckduckgo/autoconsent/pull/2086) ([@muodov](https://github.com/muodov) [@daxtheduck](https://github.com/daxtheduck))
36
+ - Rule update: CPM crawl #29: svtplay.se [#2075](https://github.com/duckduckgo/autoconsent/pull/2075) ([@daxtheduck](https://github.com/daxtheduck))
37
+ - Rule update: CPM crawl #96: obsbygg.no [#2077](https://github.com/duckduckgo/autoconsent/pull/2077) ([@daxtheduck](https://github.com/daxtheduck))
38
+ - Rule update: CPM crawl #173: nordnet.dk [#2081](https://github.com/duckduckgo/autoconsent/pull/2081) ([@daxtheduck](https://github.com/daxtheduck))
39
+ - Rule update: CPM crawl #29: sundhed.dk [#2074](https://github.com/duckduckgo/autoconsent/pull/2074) ([@daxtheduck](https://github.com/daxtheduck))
40
+ - Rule update: CPM crawl cluster #225: asnbank.nl (sel:cookie+cookieconsent|none, 3 sites) [#2079](https://github.com/duckduckgo/autoconsent/pull/2079) ([@daxtheduck](https://github.com/daxtheduck))
41
+ - Rule update: CPM crawl #188: nederlandseloterij.nl [#2080](https://github.com/duckduckgo/autoconsent/pull/2080) ([@daxtheduck](https://github.com/daxtheduck))
42
+ - Rule update: ddg_mac_desktop (eun) CPM magic report: www.kaspersky.com [#2082](https://github.com/duckduckgo/autoconsent/pull/2082) ([@daxtheduck](https://github.com/daxtheduck))
43
+ - Rule update: CPM crawl #137: rejestr.io [#2083](https://github.com/duckduckgo/autoconsent/pull/2083) ([@daxtheduck](https://github.com/daxtheduck))
44
+ - Rule update: CPM crawl #261: bryk.pl [#2085](https://github.com/duckduckgo/autoconsent/pull/2085) ([@daxtheduck](https://github.com/daxtheduck))
45
+ - Rule update: CPM crawl #246: temu.com [#2087](https://github.com/duckduckgo/autoconsent/pull/2087) ([@daxtheduck](https://github.com/daxtheduck))
46
+ - Rule update: Onetrust [#2078](https://github.com/duckduckgo/autoconsent/pull/2078) ([@daxtheduck](https://github.com/daxtheduck) [@muodov](https://github.com/muodov))
47
+ - Rule update: CPM crawl cluster #22: svt.se (sel:cookieconsent|none, 10 sites) [#2073](https://github.com/duckduckgo/autoconsent/pull/2073) ([@daxtheduck](https://github.com/daxtheduck))
48
+ - Rule update: dpgmedia-nl [#2042](https://github.com/duckduckgo/autoconsent/pull/2042) ([@daxtheduck](https://github.com/daxtheduck) [@muodov](https://github.com/muodov))
49
+ - Rule update: gravito [#2046](https://github.com/duckduckgo/autoconsent/pull/2046) ([@daxtheduck](https://github.com/daxtheduck) [@muodov](https://github.com/muodov))
50
+ - Add Italian heuristic detect patterns (x.com IT banner), remove stale twitter rule [#2072](https://github.com/duckduckgo/autoconsent/pull/2072) ([@muodov](https://github.com/muodov))
51
+ - Rule update: bbc.com-orb-cosmetic, bbc.com [#2040](https://github.com/duckduckgo/autoconsent/pull/2040) ([@daxtheduck](https://github.com/daxtheduck))
52
+ - Rule update: cookieprompter [#2071](https://github.com/duckduckgo/autoconsent/pull/2071) ([@daxtheduck](https://github.com/daxtheduck))
53
+ - Rule update: ok [#2070](https://github.com/duckduckgo/autoconsent/pull/2070) ([@daxtheduck](https://github.com/daxtheduck))
54
+ - Rule update: amazon-music [#2069](https://github.com/duckduckgo/autoconsent/pull/2069) ([@daxtheduck](https://github.com/daxtheduck))
55
+ - Rule update: arbeidsplassen-nav-no [#2068](https://github.com/duckduckgo/autoconsent/pull/2068) ([@daxtheduck](https://github.com/daxtheduck))
56
+ - Rule update: bokadirekt.se [#2066](https://github.com/duckduckgo/autoconsent/pull/2066) ([@daxtheduck](https://github.com/daxtheduck))
57
+ - Rule update: pe-cookies [#2065](https://github.com/duckduckgo/autoconsent/pull/2065) ([@daxtheduck](https://github.com/daxtheduck))
58
+ - Rule update: att [#2064](https://github.com/duckduckgo/autoconsent/pull/2064) ([@daxtheduck](https://github.com/daxtheduck))
59
+ - Rule update: mainichi [#2063](https://github.com/duckduckgo/autoconsent/pull/2063) ([@daxtheduck](https://github.com/daxtheduck))
60
+ - Rule update: pensionsmyndigheten [#2062](https://github.com/duckduckgo/autoconsent/pull/2062) ([@daxtheduck](https://github.com/daxtheduck))
61
+ - Rule update: ing-de [#2061](https://github.com/duckduckgo/autoconsent/pull/2061) ([@daxtheduck](https://github.com/daxtheduck))
62
+ - Rule update: auctionet [#2059](https://github.com/duckduckgo/autoconsent/pull/2059) ([@daxtheduck](https://github.com/daxtheduck))
63
+ - Rule update: eroticiracconti-it [#2058](https://github.com/duckduckgo/autoconsent/pull/2058) ([@daxtheduck](https://github.com/daxtheduck))
64
+ - Rule update: sitevision [#2057](https://github.com/duckduckgo/autoconsent/pull/2057) ([@daxtheduck](https://github.com/daxtheduck))
65
+ - Rule update: zorginstituut-cookiewarning [#2056](https://github.com/duckduckgo/autoconsent/pull/2056) ([@daxtheduck](https://github.com/daxtheduck))
66
+ - Rule update: nordea [#2055](https://github.com/duckduckgo/autoconsent/pull/2055) ([@daxtheduck](https://github.com/daxtheduck))
67
+ - Rule update: bouvet-cookiepanel [#2054](https://github.com/duckduckgo/autoconsent/pull/2054) ([@daxtheduck](https://github.com/daxtheduck))
68
+ - Rule update: pro-euclp-cookies-box [#2053](https://github.com/duckduckgo/autoconsent/pull/2053) ([@daxtheduck](https://github.com/daxtheduck))
69
+ - Rule update: lantmateriet [#2051](https://github.com/duckduckgo/autoconsent/pull/2051) ([@daxtheduck](https://github.com/daxtheduck))
70
+ - Rule update: annunci69.it [#2050](https://github.com/duckduckgo/autoconsent/pull/2050) ([@daxtheduck](https://github.com/daxtheduck))
71
+ - Rule update: jobindex [#2048](https://github.com/duckduckgo/autoconsent/pull/2048) ([@daxtheduck](https://github.com/daxtheduck))
72
+ - Rule update: intesasanpaolo [#2044](https://github.com/duckduckgo/autoconsent/pull/2044) ([@daxtheduck](https://github.com/daxtheduck))
73
+ - Rule update: skatteverket-se [#2043](https://github.com/duckduckgo/autoconsent/pull/2043) ([@daxtheduck](https://github.com/daxtheduck))
74
+ - Rule update: wirtualna-polska [#2039](https://github.com/duckduckgo/autoconsent/pull/2039) ([@daxtheduck](https://github.com/daxtheduck))
75
+ - Rule update: onlineradiobox [#2038](https://github.com/duckduckgo/autoconsent/pull/2038) ([@daxtheduck](https://github.com/daxtheduck))
76
+ - Rule update: smile-insurances [#2037](https://github.com/duckduckgo/autoconsent/pull/2037) ([@daxtheduck](https://github.com/daxtheduck))
77
+ - Rule update: rambler [#2036](https://github.com/duckduckgo/autoconsent/pull/2036) ([@daxtheduck](https://github.com/daxtheduck))
78
+ - Rule update: cookielegit [#2035](https://github.com/duckduckgo/autoconsent/pull/2035) ([@daxtheduck](https://github.com/daxtheduck))
79
+ - Rule update: ersatzteile-24 [#2034](https://github.com/duckduckgo/autoconsent/pull/2034) ([@daxtheduck](https://github.com/daxtheduck))
80
+ - Rule update: aeolservice [#2033](https://github.com/duckduckgo/autoconsent/pull/2033) ([@daxtheduck](https://github.com/daxtheduck))
81
+ - Rule update: xenforo-cookie-consent [#2032](https://github.com/duckduckgo/autoconsent/pull/2032) ([@daxtheduck](https://github.com/daxtheduck))
82
+ - Rule update: hacon-next-gen [#2041](https://github.com/duckduckgo/autoconsent/pull/2041) ([@daxtheduck](https://github.com/daxtheduck))
83
+ - Rule update: consent-studio [#1999](https://github.com/duckduckgo/autoconsent/pull/1999) ([@daxtheduck](https://github.com/daxtheduck) [@muodov](https://github.com/muodov))
84
+ - Rule update: puesc-gov-pl [#2031](https://github.com/duckduckgo/autoconsent/pull/2031) ([@daxtheduck](https://github.com/daxtheduck))
85
+ - Rule update: eon.pl [#1991](https://github.com/duckduckgo/autoconsent/pull/1991) ([@daxtheduck](https://github.com/daxtheduck))
86
+
87
+ #### AI / Agent Workflow
88
+
89
+ - Test a rule's spec sites before removing it [#2094](https://github.com/duckduckgo/autoconsent/pull/2094) ([@muodov](https://github.com/muodov))
90
+
91
+ #### Test Infrastructure
92
+
93
+ - Run CI E2E tests in an isolated world and rerun bot-walled tests on Oxylabs [#1672](https://github.com/duckduckgo/autoconsent/pull/1672) ([@muodov](https://github.com/muodov))
94
+
95
+ #### Internal
96
+
97
+ - Detect more types of popup [#1769](https://github.com/duckduckgo/autoconsent/pull/1769) ([@sammacbeth](https://github.com/sammacbeth))
98
+
99
+ #### Authors: 4
100
+
101
+ - Cursor Agent ([@cursoragent](https://github.com/cursoragent))
102
+ - Dax ([@daxtheduck](https://github.com/daxtheduck))
103
+ - Maxim Tsoy ([@muodov](https://github.com/muodov))
104
+ - Sam Macbeth ([@sammacbeth](https://github.com/sammacbeth))
105
+
106
+ ---
107
+
1
108
  # v16.59.0 (Thu Oct 22 2026)
2
109
 
3
110
  #### Rules