@duckduckgo/autoconsent 16.58.0 → 16.60.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 (332) 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 +176 -0
  8. package/Jenkinsfile +36 -1
  9. package/dist/addon-firefox/background.bundle.js +22 -1
  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/popup.bundle.js +1 -1
  14. package/dist/addon-firefox/rule-index.json +1 -1
  15. package/dist/addon-firefox/rules.json +1 -1
  16. package/dist/addon-mv3/background.bundle.js +22 -1
  17. package/dist/addon-mv3/compact-rules.json +1 -1
  18. package/dist/addon-mv3/content.bundle.js +253 -93
  19. package/dist/addon-mv3/manifest.json +1 -1
  20. package/dist/addon-mv3/popup.bundle.js +1 -1
  21. package/dist/addon-mv3/rule-index.json +1 -1
  22. package/dist/addon-mv3/rules.json +1 -1
  23. package/dist/autoconsent.cjs.js +253 -93
  24. package/dist/autoconsent.esm.js +253 -93
  25. package/dist/autoconsent.playwright.js +253 -93
  26. package/dist/autoconsent.standalone.js +254 -94
  27. package/dist/heuristics.cjs.js +29 -3
  28. package/dist/heuristics.mjs +28 -3
  29. package/dist/types/cmps/onetrust.d.ts +1 -0
  30. package/dist/types/eval-snippets.d.ts +3 -0
  31. package/dist/types/heuristic-classify.d.ts +1 -0
  32. package/dist/types/heuristics.d.ts +10 -1
  33. package/lib/cmps/onetrust.ts +15 -0
  34. package/lib/eval-snippets.ts +21 -0
  35. package/lib/heuristic-classify.ts +6 -0
  36. package/lib/heuristic-patterns.ts +26 -1
  37. package/lib/heuristics.ts +256 -104
  38. package/package.json +4 -6
  39. package/playwright/isolated-world.mjs +217 -0
  40. package/playwright/oxylabs.mjs +196 -0
  41. package/playwright/runner.ts +183 -39
  42. package/playwright.config.ts +4 -1
  43. package/rules/autoconsent/123inkt.json +9 -0
  44. package/rules/autoconsent/aeolservice.json +39 -0
  45. package/rules/autoconsent/amazon-music.json +13 -0
  46. package/rules/autoconsent/animatetimes.json +34 -0
  47. package/rules/autoconsent/annunci69.json +19 -0
  48. package/rules/autoconsent/arbeidsplassen-nav-no.json +33 -0
  49. package/rules/autoconsent/asahi-interactive.json +30 -0
  50. package/rules/autoconsent/att.json +36 -0
  51. package/rules/autoconsent/auctionet.json +39 -0
  52. package/rules/autoconsent/autodoc.json +18 -0
  53. package/rules/autoconsent/avada-privacy-bar-cosmetic.json +28 -0
  54. package/rules/autoconsent/avada-privacy-bar.json +70 -0
  55. package/rules/autoconsent/axesor.json +13 -0
  56. package/rules/autoconsent/bbc.com-orb-cosmetic.json +34 -0
  57. package/rules/autoconsent/bbc.com.json +8 -5
  58. package/rules/autoconsent/blitzrechner.json +16 -0
  59. package/rules/autoconsent/bokadirekt.se.json +50 -0
  60. package/rules/autoconsent/bouvet-cookiepanel.json +29 -0
  61. package/rules/autoconsent/brainsum-cookieconsent.json +25 -0
  62. package/rules/autoconsent/bwr-cookie.json +13 -0
  63. package/rules/autoconsent/capitalone-ca.json +13 -0
  64. package/rules/autoconsent/carpetaciudadana.json +51 -0
  65. package/rules/autoconsent/cf-gdpr-cookie-consent.json +23 -0
  66. package/rules/autoconsent/chatgpt.json +3 -3
  67. package/rules/autoconsent/cksmodal.json +9 -0
  68. package/rules/autoconsent/consent-studio.json +41 -0
  69. package/rules/autoconsent/cookie-popup-cookieno.json +29 -0
  70. package/rules/autoconsent/cookie-popup-cookieok.json +25 -0
  71. package/rules/autoconsent/cookielegit.json +21 -0
  72. package/rules/autoconsent/cookieman.json +23 -4
  73. package/rules/autoconsent/cookieprompter.json +21 -0
  74. package/rules/autoconsent/cookiereports.json +10 -0
  75. package/rules/autoconsent/cookietractor.json +37 -0
  76. package/rules/autoconsent/danskebank.json +17 -0
  77. package/rules/autoconsent/de-volksbank.json +14 -0
  78. package/rules/autoconsent/dintel.json +13 -0
  79. package/rules/autoconsent/dpgmedia-nl.json +20 -8
  80. package/rules/autoconsent/e-grip.json +63 -0
  81. package/rules/autoconsent/eccmm.json +30 -0
  82. package/rules/autoconsent/england-nhs-uk.json +18 -0
  83. package/rules/autoconsent/eon.pl.json +18 -0
  84. package/rules/autoconsent/erdee-media.json +24 -0
  85. package/rules/autoconsent/eroticiracconti-it.json +13 -0
  86. package/rules/autoconsent/ersatzteile-24.json +49 -0
  87. package/rules/autoconsent/escorteintime.json +15 -0
  88. package/rules/autoconsent/gays-cruising.json +18 -0
  89. package/rules/autoconsent/gov-pl-cookies-info.json +28 -0
  90. package/rules/autoconsent/gravito.json +23 -16
  91. package/rules/autoconsent/grupa-pracuj.json +73 -0
  92. package/rules/autoconsent/grupa-rmf.json +21 -0
  93. package/rules/autoconsent/gsb-cookiebanner-selection.json +12 -0
  94. package/rules/autoconsent/gwo.json +53 -0
  95. package/rules/autoconsent/hacon-next-gen.json +41 -0
  96. package/rules/autoconsent/hsbc-au.json +35 -0
  97. package/rules/autoconsent/ign-es.json +35 -0
  98. package/rules/autoconsent/impress-co-jp.json +10 -0
  99. package/rules/autoconsent/ing-de.json +31 -0
  100. package/rules/autoconsent/interia.json +1 -4
  101. package/rules/autoconsent/intesasanpaolo.json +37 -0
  102. package/rules/autoconsent/iworks-footer-notice.json +19 -0
  103. package/rules/autoconsent/jobindex.json +40 -0
  104. package/rules/autoconsent/juzaphoto.json +13 -0
  105. package/rules/autoconsent/kaspersky-cookies-notification.json +9 -0
  106. package/rules/autoconsent/lantmateriet.json +13 -0
  107. package/rules/autoconsent/leukhuis-nl.json +35 -0
  108. package/rules/autoconsent/lg-cookiecontrol.json +16 -0
  109. package/rules/autoconsent/libris.nl.json +19 -0
  110. package/rules/autoconsent/lineagrafica-reject.json +32 -0
  111. package/rules/autoconsent/mainichi.json +39 -0
  112. package/rules/autoconsent/mango.json +56 -0
  113. package/rules/autoconsent/matemaks.json +29 -0
  114. package/rules/autoconsent/merito.pl.json +44 -0
  115. package/rules/autoconsent/minitool.json +31 -0
  116. package/rules/autoconsent/miteco.json +41 -0
  117. package/rules/autoconsent/mobile-de.json +13 -0
  118. package/rules/autoconsent/mq-edu-au.json +26 -0
  119. package/rules/autoconsent/nederlandseloterij.json +35 -0
  120. package/rules/autoconsent/nordea.json +21 -0
  121. package/rules/autoconsent/nordnet.json +33 -0
  122. package/rules/autoconsent/nowaera.json +44 -0
  123. package/rules/autoconsent/ok.json +4 -12
  124. package/rules/autoconsent/olvg.json +45 -0
  125. package/rules/autoconsent/onlineradiobox.json +35 -0
  126. package/rules/autoconsent/optyczne-pl.json +28 -0
  127. package/rules/autoconsent/orbys.json +35 -0
  128. package/rules/autoconsent/payfip.json +36 -0
  129. package/rules/autoconsent/pdf24.json +48 -0
  130. package/rules/autoconsent/pe-cookies.json +30 -0
  131. package/rules/autoconsent/pekao24.json +33 -0
  132. package/rules/autoconsent/pensionsmyndigheten.json +13 -0
  133. package/rules/autoconsent/praca-pl.json +17 -0
  134. package/rules/autoconsent/pro-euclp-cookies-box.json +31 -0
  135. package/rules/autoconsent/pubfactory.json +45 -0
  136. package/rules/autoconsent/puesc-gov-pl.json +37 -0
  137. package/rules/autoconsent/rambler.json +30 -0
  138. package/rules/autoconsent/rejestr-io.json +36 -0
  139. package/rules/autoconsent/remove-cookie-notice.json +18 -0
  140. package/rules/autoconsent/rijksoverheid-notification-bar.json +34 -0
  141. package/rules/autoconsent/route-inn.json +13 -0
  142. package/rules/autoconsent/schumacher.com.json +53 -0
  143. package/rules/autoconsent/sedlak-gdpr-cookie.json +10 -0
  144. package/rules/autoconsent/seniorweb.json +42 -0
  145. package/rules/autoconsent/shouldianswer.json +14 -0
  146. package/rules/autoconsent/shueisha-gigaviewer.json +10 -0
  147. package/rules/autoconsent/sitevision.json +18 -1
  148. package/rules/autoconsent/skatteverket-se.json +33 -0
  149. package/rules/autoconsent/smile-insurances.json +13 -0
  150. package/rules/autoconsent/snb.ch.json +18 -0
  151. package/rules/autoconsent/sujetdebac.fr.json +10 -0
  152. package/rules/autoconsent/svt.se.json +7 -7
  153. package/rules/autoconsent/swisscom-ecare.json +31 -0
  154. package/rules/autoconsent/telegram-im.json +36 -0
  155. package/rules/autoconsent/temu.json +12 -16
  156. package/rules/autoconsent/thecode.json +21 -0
  157. package/rules/autoconsent/thomsonlocal.com.json +10 -0
  158. package/rules/autoconsent/todostuslibros.json +51 -0
  159. package/rules/autoconsent/turkishairlines.com.json +33 -0
  160. package/rules/autoconsent/uk-parliament.json +19 -0
  161. package/rules/autoconsent/usherbrooke.json +16 -0
  162. package/rules/autoconsent/vdg-cookies.json +36 -0
  163. package/rules/autoconsent/warszawa19115.pl.json +52 -0
  164. package/rules/autoconsent/wirtualna-polska.json +41 -0
  165. package/rules/autoconsent/wojsko-polskie-pl.json +28 -0
  166. package/rules/autoconsent/xenforo-cookie-consent.json +62 -0
  167. package/rules/autoconsent/xtb.json +18 -0
  168. package/rules/autoconsent/zorginstituut-cookiewarning.json +33 -0
  169. package/rules/autoconsent/zurich-cookies-notification-banner.json +17 -0
  170. package/rules/compact-rules.json +1 -1
  171. package/rules/rule-index.json +1 -1
  172. package/rules/rules.json +1 -1
  173. package/tests/123inkt.spec.ts +5 -0
  174. package/tests/aeolservice.spec.ts +5 -0
  175. package/tests/amazon-music.spec.ts +9 -0
  176. package/tests/animatetimes.spec.ts +3 -0
  177. package/tests/annunci69.spec.ts +5 -0
  178. package/tests/arbeidsplassen-nav-no.spec.ts +3 -0
  179. package/tests/asahi-interactive.spec.ts +6 -0
  180. package/tests/att.spec.ts +3 -0
  181. package/tests/auctionet.spec.ts +3 -0
  182. package/tests/autodoc.spec.ts +8 -0
  183. package/tests/avada-privacy-bar-cosmetic.spec.ts +3 -0
  184. package/tests/avada-privacy-bar.spec.ts +12 -0
  185. package/tests/axesor.spec.ts +3 -0
  186. package/tests/bbc.com-orb-cosmetic.spec.ts +8 -0
  187. package/tests/bbc.com.spec.ts +11 -9
  188. package/tests/blitzrechner.spec.ts +3 -0
  189. package/tests/bokadirekt.se.spec.ts +3 -0
  190. package/tests/bouvet-cookiepanel.spec.ts +9 -0
  191. package/tests/brainsum-cookieconsent.spec.ts +9 -0
  192. package/tests/bwr-cookie.spec.ts +3 -0
  193. package/tests/capitalone-ca.spec.ts +3 -0
  194. package/tests/carpetaciudadana.spec.ts +6 -0
  195. package/tests/cf-gdpr-cookie-consent.spec.ts +3 -0
  196. package/tests/chatgpt.spec.ts +3 -1
  197. package/tests/cksmodal.spec.ts +3 -0
  198. package/tests/consent-studio.spec.ts +15 -0
  199. package/tests/cookie-popup-cookieno.spec.ts +8 -0
  200. package/tests/cookie-popup-cookieok.spec.ts +9 -0
  201. package/tests/cookiebot.spec.ts +1 -0
  202. package/tests/cookielegit.spec.ts +3 -0
  203. package/tests/cookieman.spec.ts +1 -0
  204. package/tests/cookieprompter.spec.ts +9 -0
  205. package/tests/cookiereports.spec.ts +13 -0
  206. package/tests/cookietractor.spec.ts +12 -0
  207. package/tests/danskebank.spec.ts +12 -0
  208. package/tests/de-volksbank.spec.ts +5 -0
  209. package/tests/dintel.spec.ts +10 -0
  210. package/tests/dpgmedia-nl.spec.ts +15 -1
  211. package/tests/e-grip.spec.ts +8 -0
  212. package/tests/eccmm.spec.ts +6 -0
  213. package/tests/england-nhs-uk.spec.ts +3 -0
  214. package/tests/eon.pl.spec.ts +3 -0
  215. package/tests/erdee-media.spec.ts +3 -0
  216. package/tests/eroticiracconti-it.spec.ts +3 -0
  217. package/tests/ersatzteile-24.spec.ts +6 -0
  218. package/tests/escorteintime.spec.ts +3 -0
  219. package/tests/gays-cruising.spec.ts +3 -0
  220. package/tests/gov-pl-cookies-info.spec.ts +3 -0
  221. package/tests/gravito.spec.ts +7 -1
  222. package/tests/grupa-pracuj.spec.ts +8 -0
  223. package/tests/grupa-rmf.spec.ts +5 -0
  224. package/tests/gsb-cookiebanner-selection.spec.ts +7 -0
  225. package/tests/gwo.spec.ts +6 -0
  226. package/tests/hacon-next-gen.spec.ts +6 -0
  227. package/tests/hsbc-au.spec.ts +6 -0
  228. package/tests/ign-es.spec.ts +3 -0
  229. package/tests/impress-co-jp.spec.ts +8 -0
  230. package/tests/ing-de.spec.ts +3 -0
  231. package/tests/interia.spec.ts +1 -1
  232. package/tests/intesasanpaolo.spec.ts +3 -0
  233. package/tests/iworks-footer-notice.spec.ts +3 -0
  234. package/tests/jobindex.spec.ts +3 -0
  235. package/tests/juzaphoto.spec.ts +7 -0
  236. package/tests/kaspersky-cookies-notification.spec.ts +8 -0
  237. package/tests/lantmateriet.spec.ts +3 -0
  238. package/tests/leukhuis-nl.spec.ts +3 -0
  239. package/tests/lg-cookiecontrol.spec.ts +10 -0
  240. package/tests/libris.nl.spec.ts +3 -0
  241. package/tests/lineagrafica-reject.spec.ts +8 -0
  242. package/tests/mainichi.spec.ts +10 -0
  243. package/tests/mango.spec.ts +3 -0
  244. package/tests/matemaks.spec.ts +3 -0
  245. package/tests/merito.pl.spec.ts +3 -0
  246. package/tests/minitool.spec.ts +8 -0
  247. package/tests/miteco.spec.ts +8 -0
  248. package/tests/mobile-de.spec.ts +3 -0
  249. package/tests/mq-edu-au.spec.ts +3 -0
  250. package/tests/nederlandseloterij.spec.ts +9 -0
  251. package/tests/nordea.spec.ts +6 -0
  252. package/tests/nordnet.spec.ts +8 -0
  253. package/tests/nowaera.spec.ts +3 -0
  254. package/tests/olvg.spec.ts +6 -0
  255. package/tests/onlineradiobox.spec.ts +5 -0
  256. package/tests/optyczne-pl.spec.ts +3 -0
  257. package/tests/orbys.spec.ts +3 -0
  258. package/tests/payfip.spec.ts +3 -0
  259. package/tests/pdf24.spec.ts +3 -0
  260. package/tests/pe-cookies.spec.ts +6 -0
  261. package/tests/pekao24.spec.ts +3 -0
  262. package/tests/pensionsmyndigheten.spec.ts +3 -0
  263. package/tests/praca-pl.spec.ts +3 -0
  264. package/tests/pro-euclp-cookies-box.spec.ts +8 -0
  265. package/tests/pubfactory.spec.ts +10 -0
  266. package/tests/puesc-gov-pl.spec.ts +3 -0
  267. package/tests/rambler.spec.ts +3 -0
  268. package/tests/rejestr-io.spec.ts +3 -0
  269. package/tests/remove-cookie-notice.spec.ts +3 -0
  270. package/tests/rijksoverheid-notification-bar.spec.ts +3 -0
  271. package/tests/route-inn.spec.ts +3 -0
  272. package/tests/schumacher.com.spec.ts +6 -0
  273. package/tests/sedlak-gdpr-cookie.spec.ts +6 -0
  274. package/tests/seniorweb.spec.ts +3 -0
  275. package/tests/shouldianswer.spec.ts +8 -0
  276. package/tests/shueisha-gigaviewer.spec.ts +3 -0
  277. package/tests/sitevision-cookie-consent.spec.ts +1 -0
  278. package/tests/sitevision.spec.ts +7 -0
  279. package/tests/skatteverket-se.spec.ts +3 -0
  280. package/tests/smile-insurances.spec.ts +6 -0
  281. package/tests/snb.ch.spec.ts +3 -0
  282. package/tests/sujetdebac.fr.spec.ts +3 -0
  283. package/tests/svt.se.spec.ts +1 -3
  284. package/tests/swisscom-ecare.spec.ts +7 -0
  285. package/tests/telegram-im.spec.ts +6 -0
  286. package/tests/temu.spec.ts +3 -1
  287. package/tests/thecode.spec.ts +3 -0
  288. package/tests/thomsonlocal.com.spec.ts +3 -0
  289. package/tests/todostuslibros.spec.ts +3 -0
  290. package/tests/turkishairlines.com.spec.ts +3 -0
  291. package/tests/uk-parliament.spec.ts +8 -0
  292. package/tests/usherbrooke.spec.ts +6 -0
  293. package/tests/vdg-cookies.spec.ts +8 -0
  294. package/tests/warszawa19115.pl.spec.ts +3 -0
  295. package/tests/wirtualna-polska.spec.ts +8 -0
  296. package/tests/wojsko-polskie-pl.spec.ts +3 -0
  297. package/tests/xenforo-cookie-consent.spec.ts +6 -0
  298. package/tests/xtb.spec.ts +3 -0
  299. package/tests/zorginstituut-cookiewarning.spec.ts +10 -0
  300. package/tests/zurich-cookies-notification-banner.spec.ts +3 -0
  301. package/tests-wtr/heuristics/get-potential-popups.test.ts +330 -9
  302. package/tests-wtr/heuristics/heuristics-utils.test.ts +98 -0
  303. package/rules/autoconsent/twitter.json +0 -34
  304. package/rules/generated/auto_CH_autodoc.de_krd.json +0 -40
  305. package/rules/generated/auto_CH_svtplay.se_d39.json +0 -40
  306. package/rules/generated/auto_GB_parliamentlive.tv_r3v.json +0 -28
  307. package/rules/generated/auto_NL_asnbank.nl_e28.json +0 -40
  308. package/rules/generated/auto_NL_eurojackpot.nederlandseloterij.nl_7hr.json +0 -40
  309. package/rules/generated/auto_NL_lotto.nederlandseloterij.nl_7c8.json +0 -40
  310. package/rules/generated/auto_NL_nederlandseloterij.nl_b60.json +0 -40
  311. package/rules/generated/auto_NL_staatsloterij.nederlandseloterij.nl_197.json +0 -40
  312. package/rules/generated/auto_NO_arronet.se_vo5.json +0 -40
  313. package/rules/generated/auto_NO_liseberg.se_qy4.json +0 -40
  314. package/rules/generated/auto_NO_nordnet.se_2en.json +0 -40
  315. package/rules/generated/auto_NO_regeringen.se_uhv.json +0 -40
  316. package/rules/generated/auto_NO_svtplay.se_ujn.json +0 -40
  317. package/rules/generated/auto_NO_uu.se_eea.json +0 -40
  318. package/tests/generated/auto_CH_autodoc.de_krd.spec.ts +0 -2
  319. package/tests/generated/auto_CH_svtplay.se_d39.spec.ts +0 -2
  320. package/tests/generated/auto_GB_parliamentlive.tv_r3v.spec.ts +0 -6
  321. package/tests/generated/auto_NL_asnbank.nl_e28.spec.ts +0 -6
  322. package/tests/generated/auto_NL_eurojackpot.nederlandseloterij.nl_7hr.spec.ts +0 -6
  323. package/tests/generated/auto_NL_lotto.nederlandseloterij.nl_7c8.spec.ts +0 -6
  324. package/tests/generated/auto_NL_nederlandseloterij.nl_b60.spec.ts +0 -6
  325. package/tests/generated/auto_NL_staatsloterij.nederlandseloterij.nl_197.spec.ts +0 -6
  326. package/tests/generated/auto_NO_arronet.se_vo5.spec.ts +0 -2
  327. package/tests/generated/auto_NO_liseberg.se_qy4.spec.ts +0 -2
  328. package/tests/generated/auto_NO_nordnet.se_2en.spec.ts +0 -2
  329. package/tests/generated/auto_NO_regeringen.se_uhv.spec.ts +0 -6
  330. package/tests/generated/auto_NO_svtplay.se_ujn.spec.ts +0 -2
  331. package/tests/generated/auto_NO_uu.se_eea.spec.ts +0 -2
  332. package/tests/twitter.spec.ts +0 -10
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Runs the autoconsent content script in an isolated world of every frame of a Chromium page (via CDP),
3
+ * the way the browser extension does, and bridges its messages to Node. Shared by the Playwright E2E
4
+ * runner and the regional-testing harness used by agent skills.
5
+ */
6
+
7
+ /**
8
+ * @typedef {import('playwright').Page} Page
9
+ */
10
+
11
+ /**
12
+ * Primitives the message handler uses to talk to a specific frame's content script.
13
+ * `frameRef` is the isolated world's execution-context uniqueId the message arrived from;
14
+ * the transport resolves it to the owning CDP session and the matching page-world context.
15
+ * @typedef {Object} MessageTransport
16
+ * @property {(frameRef: string, message: object) => Promise<void>} sendToContentScript
17
+ * @property {(frameRef: string, code: string) => Promise<any>} evalInMainWorld
18
+ * @property {(frameRef: string) => boolean} isMainFrame - Whether the frame is the page's top frame.
19
+ */
20
+
21
+ /**
22
+ * @typedef {(transport: MessageTransport) => (msg: any, frameRef: string) => Promise<void>} MessageHandlerFactory
23
+ */
24
+
25
+ /**
26
+ * Isolated-world injection via CDP (Chromium only). For each frame we create a dedicated isolated
27
+ * world via `Page.createIsolatedWorld`, then inject the content script there and bridge messages
28
+ * over a per-context CDP binding.
29
+ *
30
+ * @param {Page} page
31
+ * @param {MessageHandlerFactory} createMessageHandler
32
+ * @param {string} contentScript - The autoconsent content script bundle.
33
+ * @param {string} [extraScript] - Runs in each isolated world after the content script.
34
+ */
35
+ export async function injectIntoIsolatedWorld(page, createMessageHandler, contentScript, extraScript = '') {
36
+ // Isolated world name: `<prefix><pageWorldUniqueId><separator><frameId>`. Encoding the page-world
37
+ // uniqueId lets us later run eval snippets in that frame's main world.
38
+ const WORLD_PREFIX = 'autoconsent_iw_';
39
+ const WORLD_SEPARATOR = '_frame_';
40
+ const BINDING_PREFIX = 'autoconsentSendMessage_';
41
+
42
+ /** @type {Map<string, string>} isolated-world uniqueId -> page (main) world uniqueId */
43
+ const isolated2pageWorld = new Map();
44
+ /** @type {Map<string, any>} isolated-world uniqueId -> CDP session that owns it */
45
+ const sessionByContext = new Map();
46
+ /** @type {Map<string, string>} binding name -> isolated-world uniqueId */
47
+ const contextByBinding = new Map();
48
+ /** @type {Map<string, string>} isolated-world uniqueId -> binding name */
49
+ const bindingByContext = new Map();
50
+ /** @type {Map<string, string>} isolated-world uniqueId -> CDP frame id */
51
+ const frameByContext = new Map();
52
+ /** @type {Map<string, string>} CDP frame id -> uniqueId of the frame's current page (main) world */
53
+ const pageWorldByFrame = new Map();
54
+ /** @type {any} */
55
+ let pageSession = null;
56
+ /** @type {string|null} */
57
+ let mainFrameId = null;
58
+
59
+ const handle = createMessageHandler({
60
+ sendToContentScript: async (isolatedUniqueId, message) => {
61
+ const client = sessionByContext.get(isolatedUniqueId);
62
+ if (!client) return;
63
+ try {
64
+ await client.send('Runtime.evaluate', {
65
+ expression: `autoconsentReceiveMessage(${JSON.stringify(message)})`,
66
+ uniqueContextId: isolatedUniqueId,
67
+ awaitPromise: true,
68
+ // Some pages' CSP would otherwise block evaluating in the isolated world.
69
+ allowUnsafeEvalBlockedByCSP: true,
70
+ });
71
+ } catch {
72
+ // The context may be gone if the frame navigated or detached.
73
+ }
74
+ },
75
+ evalInMainWorld: async (isolatedUniqueId, code) => {
76
+ const client = sessionByContext.get(isolatedUniqueId);
77
+ const pageWorldUniqueId = isolated2pageWorld.get(isolatedUniqueId);
78
+ if (!client || !pageWorldUniqueId) return false;
79
+ const { result, exceptionDetails } = await client.send('Runtime.evaluate', {
80
+ expression: code,
81
+ uniqueContextId: pageWorldUniqueId,
82
+ returnByValue: true,
83
+ awaitPromise: true,
84
+ // Eval snippets must run even when the page's CSP disallows eval.
85
+ allowUnsafeEvalBlockedByCSP: true,
86
+ });
87
+ return exceptionDetails ? false : (result?.value ?? false);
88
+ },
89
+ isMainFrame: (isolatedUniqueId) =>
90
+ sessionByContext.get(isolatedUniqueId) === pageSession && frameByContext.get(isolatedUniqueId) === mainFrameId,
91
+ });
92
+
93
+ async function attachToSession(/** @type {any} */ client) {
94
+ client.on('Runtime.executionContextCreated', async (/** @type {any} */ event) => {
95
+ const { context } = event;
96
+ const frameId = context.auxData?.frameId;
97
+
98
+ // Our isolated world finished initializing: wire up its binding and content script.
99
+ if (context.auxData?.type === 'isolated' && typeof context.name === 'string' && context.name.startsWith(WORLD_PREFIX)) {
100
+ const separatorIndex = context.name.indexOf(WORLD_SEPARATOR);
101
+ const pageWorldUniqueId = context.name.slice(WORLD_PREFIX.length, separatorIndex);
102
+ const intendedFrameId = context.name.slice(separatorIndex + WORLD_SEPARATOR.length);
103
+ // Chromium may create the named world in other frames too, and recreates it in the frame's
104
+ // next document; keep only the one we asked for in the current document.
105
+ if (intendedFrameId !== frameId || pageWorldByFrame.get(frameId) !== pageWorldUniqueId) return;
106
+
107
+ isolated2pageWorld.set(context.uniqueId, pageWorldUniqueId);
108
+ sessionByContext.set(context.uniqueId, client);
109
+ frameByContext.set(context.uniqueId, frameId);
110
+
111
+ const bindingName = `${BINDING_PREFIX}${context.uniqueId.replace(/\W/g, '_')}`;
112
+ contextByBinding.set(bindingName, context.uniqueId);
113
+ bindingByContext.set(context.uniqueId, bindingName);
114
+ try {
115
+ await client.send('Runtime.addBinding', { name: bindingName, executionContextName: context.name });
116
+ // CDP bindings take a single string arg, so wrap it in the shape the content script expects.
117
+ await client.send('Runtime.evaluate', {
118
+ expression: `window.autoconsentSendMessage = (m) => { window.${bindingName}(JSON.stringify(m)); return Promise.resolve(); };\n${contentScript}\n${extraScript}`,
119
+ uniqueContextId: context.uniqueId,
120
+ allowUnsafeEvalBlockedByCSP: true,
121
+ });
122
+ } catch {
123
+ // The frame may have navigated or detached before we finished wiring it up.
124
+ }
125
+ return;
126
+ }
127
+
128
+ // A regular page (main) world: request an isolated world for it, tagged with its id.
129
+ if (!frameId || context.auxData?.type !== 'default') return;
130
+ pageWorldByFrame.set(frameId, context.uniqueId);
131
+ if (!context.origin || context.origin === '://') return;
132
+ try {
133
+ await client.send('Page.createIsolatedWorld', {
134
+ frameId,
135
+ worldName: `${WORLD_PREFIX}${context.uniqueId}${WORLD_SEPARATOR}${frameId}`,
136
+ });
137
+ } catch {
138
+ // The frame may have navigated or detached.
139
+ }
140
+ });
141
+
142
+ client.on('Runtime.executionContextDestroyed', (/** @type {any} */ event) => {
143
+ const uniqueId = event.executionContextUniqueId;
144
+ if (!uniqueId) return;
145
+ isolated2pageWorld.delete(uniqueId);
146
+ sessionByContext.delete(uniqueId);
147
+ frameByContext.delete(uniqueId);
148
+ const bindingName = bindingByContext.get(uniqueId);
149
+ if (bindingName) {
150
+ bindingByContext.delete(uniqueId);
151
+ contextByBinding.delete(bindingName);
152
+ // Each document gets its own binding, so drop the old one with its context.
153
+ client.send('Runtime.removeBinding', { name: bindingName }).catch(() => {});
154
+ }
155
+ });
156
+
157
+ client.on('Runtime.bindingCalled', (/** @type {any} */ event) => {
158
+ const isolatedUniqueId = contextByBinding.get(event.name);
159
+ if (!isolatedUniqueId) return;
160
+ let msg;
161
+ try {
162
+ msg = JSON.parse(event.payload);
163
+ } catch {
164
+ return;
165
+ }
166
+ handle(msg, isolatedUniqueId);
167
+ });
168
+
169
+ // Page must be enabled before createIsolatedWorld; Runtime.enable replays existing contexts.
170
+ await client.send('Page.enable');
171
+ await client.send('Runtime.enable');
172
+ }
173
+
174
+ // The page session covers the main frame and all same-process (in-process) iframes.
175
+ pageSession = await page.context().newCDPSession(page);
176
+ const { frameTree } = await pageSession.send('Page.getFrameTree');
177
+ mainFrameId = frameTree.frame.id;
178
+ // Follow the top frame if Chromium replaces it (e.g. on prerender activation).
179
+ pageSession.on('Page.frameNavigated', (/** @type {any} */ event) => {
180
+ if (!event.frame.parentId) mainFrameId = event.frame.id;
181
+ });
182
+ await attachToSession(pageSession);
183
+
184
+ // Out-of-process iframes (OOPIFs) are separate CDP targets, so each needs its own session.
185
+ // newCDPSession throws for in-process frames, which are already handled above.
186
+ const attachedFrames = new WeakSet();
187
+ async function attachToOopif(/** @type {import('playwright').Frame} */ frame) {
188
+ if (!frame.parentFrame() || attachedFrames.has(frame)) return;
189
+ // Mark synchronously (before any await) so concurrent frameattached/framenavigated events
190
+ // can't open duplicate sessions for the same frame. Unmark on failure so a later event retries.
191
+ attachedFrames.add(frame);
192
+ let client;
193
+ try {
194
+ client = await page.context().newCDPSession(frame);
195
+ } catch {
196
+ // In-process frame (already covered by the page session) or the frame detached.
197
+ attachedFrames.delete(frame);
198
+ return;
199
+ }
200
+ try {
201
+ await attachToSession(client);
202
+ } catch {
203
+ // Wiring up the session failed (e.g. the OOPIF navigated/detached). Detach the
204
+ // half-initialized session before unmarking, otherwise a retry would open a second
205
+ // session for the same frame, leaving duplicate listeners and possible double injection.
206
+ try {
207
+ await client.detach();
208
+ } catch {
209
+ // The session may already be gone.
210
+ }
211
+ attachedFrames.delete(frame);
212
+ }
213
+ }
214
+ page.on('frameattached', attachToOopif);
215
+ page.on('framenavigated', attachToOopif);
216
+ await Promise.all(page.frames().map(attachToOopif));
217
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Oxylabs Agent Browser connection: region targeting, the CDP endpoint, and credential redaction.
3
+ * Docs: https://developers.oxylabs.io/products/agent-browser
4
+ * Shared by the Playwright E2E runner and the `oxylabs-testing` skill.
5
+ *
6
+ * Requires env vars: OXYLABS_USER, OXYLABS_PASSWORD.
7
+ * Optional: OXYLABS_HOST (defaults to hb.oxylabs.io).
8
+ */
9
+
10
+ /**
11
+ * @typedef {import('@playwright/test').Browser} Browser
12
+ */
13
+
14
+ /**
15
+ * @typedef {'desktop'|'mobile'} OxylabsDevice
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} OxylabsBrowserOptions
20
+ * @property {OxylabsDevice} [device] - Oxylabs ?p_device= value. Oxylabs defaults to 'desktop'.
21
+ * @property {boolean} [solveCaptcha=true] - Send ?solve_captcha=true: let Oxylabs solve captchas on the page.
22
+ */
23
+
24
+ import { chromium } from '@playwright/test';
25
+
26
+ /**
27
+ * US state- and city-targeted region keys, in addition to any two-letter country code. Query params
28
+ * follow https://developers.oxylabs.io/products/agent-browser/geolocation-and-proxy-selection
29
+ * (`p_state` is US-only; `p_city` needs `p_cc` or `p_state`).
30
+ * @type {Record<string, Record<string, string>>}
31
+ */
32
+ export const OXYLABS_LOCAL_REGIONS = {
33
+ 'us-california': { p_state: 'california' },
34
+ 'us-newyork': { p_cc: 'US', p_city: 'new_york' },
35
+ 'us-losangeles': { p_cc: 'US', p_city: 'los_angeles' },
36
+ };
37
+
38
+ /**
39
+ * @param {string} regionKey - A two-letter country code (e.g. 'de', 'pt') or a key of OXYLABS_LOCAL_REGIONS.
40
+ * @returns {Record<string, string>}
41
+ */
42
+ export function oxylabsRegionParams(regionKey) {
43
+ if (Object.prototype.hasOwnProperty.call(OXYLABS_LOCAL_REGIONS, regionKey)) {
44
+ return OXYLABS_LOCAL_REGIONS[regionKey];
45
+ }
46
+ if (/^[a-z]{2}$/i.test(regionKey)) {
47
+ return { p_cc: regionKey.toUpperCase() };
48
+ }
49
+ throw new Error(
50
+ `Unknown Oxylabs region "${regionKey}". Use a two-letter country code or one of: ${Object.keys(OXYLABS_LOCAL_REGIONS).join(', ')}`,
51
+ );
52
+ }
53
+
54
+ /**
55
+ * @param {NodeJS.ProcessEnv} [env]
56
+ */
57
+ export function isOxylabsConfigured(env = process.env) {
58
+ return Boolean(env.OXYLABS_USER && env.OXYLABS_PASSWORD);
59
+ }
60
+
61
+ /**
62
+ * Build the Oxylabs CDP endpoint for a region. The result contains credentials: never log it.
63
+ * @param {string} regionKey
64
+ * @param {OxylabsBrowserOptions} [opts]
65
+ * @param {NodeJS.ProcessEnv} [env]
66
+ */
67
+ export function buildOxylabsEndpoint(regionKey, opts = {}, env = process.env) {
68
+ const user = env.OXYLABS_USER;
69
+ const password = env.OXYLABS_PASSWORD;
70
+ const host = env.OXYLABS_HOST || 'hb.oxylabs.io';
71
+ if (!user || !password) {
72
+ throw new Error('Missing Oxylabs credentials. Set OXYLABS_USER and OXYLABS_PASSWORD.');
73
+ }
74
+ const params = new URLSearchParams(oxylabsRegionParams(regionKey));
75
+ if (opts.device) {
76
+ params.set('p_device', opts.device);
77
+ }
78
+ // Accounts without captcha solving get HTTP 400 for this param, so only send it when asked.
79
+ if (opts.solveCaptcha ?? true) {
80
+ params.set('solve_captcha', 'true');
81
+ }
82
+ // user/password are interpolated raw — Oxylabs expects literal characters in
83
+ // userinfo (e.g. '+' must NOT be percent-encoded), matching their Python/JS
84
+ // SDK examples. If your credentials contain ':' or '@', escape upstream.
85
+ return `wss://${user}:${password}@${host}/?${params.toString()}`;
86
+ }
87
+
88
+ /**
89
+ * Playwright puts the endpoint URL in connection error messages; strip the credentials.
90
+ * @param {string} message
91
+ * @param {NodeJS.ProcessEnv} [env]
92
+ */
93
+ export function redactCredentials(message, env = process.env) {
94
+ let out = message;
95
+ for (const secret of [env.OXYLABS_PASSWORD, env.OXYLABS_USER]) {
96
+ if (secret) out = out.split(secret).join('***');
97
+ }
98
+ return out;
99
+ }
100
+
101
+ /**
102
+ * Connect to an Oxylabs remote browser for a specific region.
103
+ * @param {string} regionKey
104
+ * @param {OxylabsBrowserOptions} [opts]
105
+ * @returns {Promise<Browser>}
106
+ */
107
+ export async function connectOxylabs(regionKey, opts = {}) {
108
+ try {
109
+ return await chromium.connectOverCDP(buildOxylabsEndpoint(regionKey, opts), { timeout: 30000 });
110
+ } catch (e) {
111
+ // The first line has the cause; the call log after it repeats the endpoint URL.
112
+ const message = redactCredentials((e instanceof Error ? e.message : String(e)).split('\n')[0]);
113
+ const hint =
114
+ (opts.solveCaptcha ?? true) && message.includes('400') ? ' (is captcha solving enabled for this Oxylabs account?)' : '';
115
+ // eslint-disable-next-line preserve-caught-error -- the original error carries the credentials
116
+ throw new Error(message + hint);
117
+ }
118
+ }
119
+
120
+ // Captcha events arrive as window messages from the Oxylabs runtime. A listener in autoconsent's
121
+ // isolated world forwards them over its CDP binding: Agent Browser drops main-world bindings, so
122
+ // page.exposeBinding can't deliver them. See https://developers.oxylabs.io/products/agent-browser/captcha-handling
123
+ const CAPTCHA_MESSAGE = 'oxylabsCaptcha';
124
+ // Agent Browser sends `oxylabs-captcha-solve-start`; the docs list `-start`, `-end`, `-solve-end` and `-error`.
125
+ const CAPTCHA_START_EVENTS = ['oxylabs-captcha-start', 'oxylabs-captcha-solve-start'];
126
+ const CAPTCHA_END_EVENTS = ['oxylabs-captcha-end', 'oxylabs-captcha-solve-end'];
127
+ const CAPTCHA_ERROR_EVENTS = ['oxylabs-captcha-error', 'oxylabs-captcha-solve-error'];
128
+ // Longest a test pauses for solving, counted from the first start event: the wait in Oxylabs' example.
129
+ const CAPTCHA_SOLVE_TIMEOUT_MS = 60000;
130
+ const captchaListenerScript = `
131
+ window.addEventListener("message", (e) => {
132
+ if (e?.data?.source === "oxylabs-runtime" && typeof e.data.type === "string") {
133
+ window.autoconsentSendMessage({ type: "${CAPTCHA_MESSAGE}", event: e.data.type });
134
+ }
135
+ });
136
+ `;
137
+
138
+ /**
139
+ * Tracks the Oxylabs captcha solver for one page. Run `script` in each isolated world next to the
140
+ * content script and pass every message from it to `handleMessage`.
141
+ * @typedef {Object} CaptchaTracker
142
+ * @property {{ detected: boolean, solved: boolean|null }} state - The solver state.
143
+ * @property {Promise<void>} done - Resolves when solving ends (success or error).
144
+ * @property {() => boolean} isSolving - Whether waits should pause for the solver.
145
+ * @property {string} script
146
+ * @property {(msg: any) => boolean} handleMessage - Returns true for a captcha event.
147
+ */
148
+
149
+ /**
150
+ * @returns {CaptchaTracker}
151
+ */
152
+ export function createCaptchaTracker() {
153
+ /** @type {{ detected: boolean, solved: boolean|null }} */
154
+ const state = { detected: false, solved: null };
155
+ /** @type {() => void} */
156
+ let resolveDone = () => {};
157
+ /** @type {Promise<void>} */
158
+ const done = new Promise((resolve) => {
159
+ resolveDone = resolve;
160
+ });
161
+
162
+ let solving = false;
163
+ /** @type {number|null} */
164
+ let firstStartAt = null;
165
+
166
+ function handleCaptchaEvent(/** @type {string} */ type) {
167
+ if (CAPTCHA_START_EVENTS.includes(type)) {
168
+ state.detected = true;
169
+ solving = true;
170
+ firstStartAt ??= Date.now();
171
+ } else if (CAPTCHA_END_EVENTS.includes(type)) {
172
+ state.detected = true;
173
+ state.solved = true;
174
+ solving = false;
175
+ resolveDone();
176
+ } else if (CAPTCHA_ERROR_EVENTS.includes(type)) {
177
+ state.detected = true;
178
+ state.solved = false;
179
+ solving = false;
180
+ resolveDone();
181
+ }
182
+ }
183
+
184
+ return {
185
+ state,
186
+ done,
187
+ // Oxylabs gives no deadline for a start event, so waits pause whenever one arrives.
188
+ isSolving: () => solving && firstStartAt !== null && Date.now() - firstStartAt < CAPTCHA_SOLVE_TIMEOUT_MS,
189
+ script: captchaListenerScript,
190
+ handleMessage: (msg) => {
191
+ if (msg?.type !== CAPTCHA_MESSAGE) return false;
192
+ handleCaptchaEvent(msg.event);
193
+ return true;
194
+ },
195
+ };
196
+ }