npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

consent-rr

v1.55.0

Published

Cookie-consent resource replacements for uBlock Origin

Downloads

6,729

Readme

consent-rr

Cookie-consent resource replacements for uBlock Origin.

A consent manager is normally dealt with by hiding its banner and clicking its buttons (trusted-click-element), which means the banner has to render first, the selector has to keep matching, and the click has to land. These resources take the other route: uBO redirects the CMP's own script to a stub that reports a decision the visitor already made. No banner is ever built, nothing has to be clicked, and the page's consent API answers normally.

Currently covered: OneTrust (and its CookiePro tier), Cookie Information, InMobi Choice (formerly Quantcast Choice), Osano, Civic Cookie Control, Cookiebot, Securiti, Transcend, Usercentrics, PubTech, Termly, Ketch, AppConsent, CookieScript, Didomi, Complianz, Ziff Davis's own zdconsent, Google Funding Choices, tarteaucitron, CookieYes, consentmanager.net and Cookiez.

| Resource | What the page sees | | --- | --- | | onetrust-reject.js | A stored reject all: C0001 on, everything else off. Tags parked behind a category stay parked. | | onetrust-accept.js | A stored accept all: every category on, and tags parked behind one are switched back on. | | cookieinformation-reject.js | Cookie Information: the necessary category approved, everything else denied. One resource - no accept or unblock variant. | | inmobi-reject.js | InMobi Choice: a stored refusal. Nothing consented to, a TC string that says so, and __tcfapi, __gpp and __uspapi all answering instead of stalling. | | civic-reject.js | Civic Cookie Control: every optional category the site declares recorded as revoked, the necessary ones untouched, and CookieControl answering. | | civic-reject-unblock.js | Civic, for a site that withholds content until a category is on: accepts the categories that do not read as tracking, refuses the ones that do, and still refuses the IAB layer. | | cookiebot-reject.js | Cookiebot: their own default state, which is already a refusal - necessary true, preferences, statistics and marketing false - with CookieConsent answering and parked tags left parked. | | securiti-reject.js | Securiti: a refusal recorded in their own __privaci_cookie_consents, with the API their loader parks answering instead of queueing for an SDK that never arrives. | | transcend-reject.js | Transcend: no banner, and the refusal recorded through airgap's own API - which leaves airgap itself in place, blocking by that refusal. | | cookiescript-reject.js | CookieScript: their own reject-all record in their cookie, their consent mode denied, and the tags their auto-blocker parked left parked. | | appconsent-reject.js | AppConsent: nothing consented, their IABTCF_ keys saying so, and __tcfapi answering instead of a stub nothing will replace. | | appconsent-accept.js | AppConsent, granting - for a consent-or-pay wall that keeps the page shut until the answer is yes. Every purpose and vendor consented, and a vendor handed that string may act on it. | | ketch-reject.js | Ketch: their 1.9MB SDK never fetched, their command queue answering a refusal, and a returning visitor's record revoked code by code. | | ketch-reject-unblock.js | Ketch, for a site that withholds content until a purpose is consented to: the same stored and sent refusal, while the API tells the page every purpose is on. | | termly-reject.js | Termly: no banner, their own opted-in record - essential alone - with their denied Google consent mode, and the tags their auto-blocker parked left parked. | | pubtech-reject.js | PubTech CMP: no banner, their publisher-cookie string with every choice off, and __tcfapi answering a refusal the IAB's own library agrees is one. | | usercentrics-reject.js | Usercentrics: no banner, and no service consented in the record their own blocker reads - which leaves that blocker in place, blocking by it. A service a returning visitor had accepted is revoked by name. | | didomi-reject.js | Didomi: no banner, their own token with nothing consented - in the cookie and in localStorage, as their storage service writes both - and the three events their own refusal emits, in their order. No __tcfapi yet. | | didomi-accept.js | Didomi, granting: every purpose they define consented, plus any the tenant named in didomiConfig. Content a site gates on its own purpose read is shown. Still no TC string, so IAB vendors are not told anything. | | complianz-reject.js | Complianz (WordPress): their own refusal written per category under the site's own cookie prefix, with their policy id kept so the next page does not wipe it, and the tags their blocker parked left parked. | | complianz-accept.js | Complianz, granting: every category consented, and the elements their blocker rewrote to data-src-cmplz put back - script, iframe, image and stylesheet - with their content notice removed. | | zdconsent-reject.js | Ziff Davis (mashable.com, speedtest.net, askmen.com): their own Deny All record, the OneTrust record underneath that their script actually reads, and the queued work a page waits on run - which blocking the file outright leaves unrun. | | zdconsent-accept.js | Ziff Davis, granting - for the EU build, where nothing is consented until a visitor answers, and for an accept-or-pay site where their script rewrites OneTrust's reject button into a subscribe link. It will not overrule a GPC header. | | fundingchoices-reject.js | Google Funding Choices: the inactive path their own script takes - the two iframes consumers wait on, their internal queue answering instead of collecting - plus the IAB layer it leaves out, __tcfapi refusing as cmpId 300. Their FCCDCF consent cookie is cleared rather than replaced, because absent is how Google's own readers read a refusal. No accept resource. | | tarteaucitron-reject.js | tarteaucitron, hosted or self-hosted: their own refusal in their own cookie, one !service=false entry each, and the per-service events their Google, Bing and Clarity glue listens for - so the refusal reaches consent mode and not just the cookie. No banner, no reload, and their pro() beacon not sent. | | tarteaucitron-reject-unblock.js | tarteaucitron, consenting to video and social and refusing the rest - their own service types make the cut. A consented embed is started by their own launcher, so the video actually appears; ads, analytics and the rest stay refused. | | cookieyes-reject.js | CookieYes: their own reject-all in their cookieyes-consent record - necessary yes, the other five no - the two events they fire at the document, and the IAB layer their file only stubs answered as a refusal. Their consentid is kept, never minted, and their page-view beacon is not sent. | | cookieyes-reject-unblock.js | CookieYes, for a site that withholds something until a category is on: the same stored refusal, while the page's own scripts are told every category is on and the tags their plugin parked are let go - only the ones they parked. | | consentmanager-reject.js | consentmanager.net: their __cmp answering a refusal across all eighteen commands, the IAB layer refused as cmpId 31 with euconsent-v2 written, and their events fired where they fire them - cmpEvent at the window, their WordPress bridge at the document. One rule drops better than half a megabyte of delivery. | | consentmanager-reject-unblock.js | consentmanager.net, for a site that withholds something until a purpose is on: the same stored and sent refusal, while the tags their blocker parked are let go by their own data-cmp-src contract. | | cookiez-reject.js | Cookiez (WordPress): their own refusal in their own record - necessary true, the other four false - carrying the cookiesHash their gate checks, without which the record is thrown away and the banner returns. Their WordPress Consent API and Google consent mode bridges go with it; nothing is posted to their REST route. | | cookiez-reject-unblock.js | Cookiez, for a site that withholds something until a category is on: the same stored refusal, and the scripts their blocker parked freed by their own selector - including leaving data-cc-mode="always" nodes parked, which is their never-free marker. | | ampconsent-reject.js | AMP's own amp-consent extension: their refusal (REJECTED) in their own store, amp-consent:<instanceId> as {"s":0}, and every consent policy answered with their own unblockOn arithmetic - an element on the default policy stays blocked, one on _till_responded builds, because a refusal is a response. This one cannot be blocked instead: with no extension registered the AMP runtime never builds anything carrying data-block-on-consent. | | ampconsent-reject-unblock.js | AMP amp-consent, for a page that withholds content behind the default policy: the same stored and reported refusal, with only the runtime's build gate answered yes. | | iubenda-reject.js | iubenda's Cookie Solution: their own refusal in their own record - necessary true, functionality, experience, measurement and marketing false - under the cookie their configuration names, with their api answering, their callbacks fired in their own order, consent mode told denied through their own mapping and the IAB layer refusing as cmp 123 where their tenant switched it on. One rule drops a 4KB loader and the 450KB core behind it. | | iubenda-reject-unblock.js | iubenda, for a site that withholds content: the same stored and sent refusal, while the page's own scripts are told every purpose is on and the tags their auto-blocker parked are freed by their own _iub_cs_activate and data-suppressedsrc markers. | | iubenda-accept.js | iubenda, for when you actually mean it - which on their deployments is often the only way past the page, because theirs is frequently run as a consent wall and a wall answers to the record rather than to the tags. Grants every purpose, tells consent mode granted, and consents to their own vendor count in the IAB string. | | cookieconsent-reject.js | CookieConsent v3 by Orest Bida (not Osano's old library of the same name): their own refusal in their own record - the read-only categories accepted, everything else refused, with the consentId, both timestamps and the revision their gate checks. Their whole API answers, their auto-clear deletes what a refused category names, and their cc:onConsent goes to the window as theirs does. | | cookieconsent-reject-unblock.js | CookieConsent v3, for a site that withholds something: the same stored refusal, while the page is told every category is accepted and the tags their manager parked are freed by their own data-category / data-src / data-type contract. | | cookieconsent-accept.js | CookieConsent v3, granting - every category their config names, their services with them, and the parked tags freed, which is what their accept-all does. | | cookielawinfo-reject.js | The legacy Cookie Law Info plugin for WordPress (WebToffee's "GDPR Cookie Consent", not the hosted CookieYes script): their decline in their own viewed_cookie_policy cookie, their two globals put back because the page calls one from an inline script, and their server-rendered bar taken out - on 1.x the markup is printed by PHP and their own script is what hides it. The smallest resource here, and one rule by path covers every site that self-hosts it. | | webtoffee-reject.js | WebToffee's current GDPR Cookie Consent plugin - the one with categories, an API and real script blocking, which the legacy resource above does not cover. Their refusal in all three of their records (viewed_cookie_policy, cli_user_preference and the base64 CookieLawInfoConsent), their non-necessary category cookies erased, their whole banner and settings modal taken out, their parked tags left parked, and their cli_consent_update fired at the document where theirs fires it. | | webtoffee-reject-unblock.js | The same stored refusal, for a site that withholds something: the page is told every category is allowed and the tags their blocker parked are freed by their own data-cli-src / data-cli-script-type contract. | | chcookieconsent-reject.js | ConnectHolland's CookieConsentBundle for Symfony, where the consent is written by their server: their script only POSTs the banner's form, and their Twig helper gates trackers at render time. Their own refusal goes straight into their cookies - Cookie_Consent, Cookie_Consent_Key and Cookie_Category_<category> set to false - and their server-rendered banner is removed, because their own buttons are type="button" and blocking the file leaves a banner that cannot be dismissed. A filter can name the categories to allow - +js(chcookieconsent-reject, youtube, open_street_map, podigee) - for a site that puts its videos or its maps behind one. | | osano-reject.js | Osano: their own default state, which is already a refusal - ESSENTIAL accepted, STORAGE, MARKETING, PERSONALIZATION and ANALYTICS denied - stored where they store it, with Osano.cm, __tcfapi, __gpp and __uspapi answering. | | onetrust-reject-unblock.js | Stores and sends the same refusal as reject - cookie, TCF and GPP all say no - while telling the page's own scripts every category is on, and letting every parked tag go. |

Pick reject as the default. reject-unblock is for a site that withholds the content until you agree: it stores and sends the same refusal as reject - the cookie, the TC string and the GPP string all say no - and separately tells the page's own scripts that every category is on, because that is a variable on the page rather than anything transmitted. It also un-parks every gated tag. Sites gate their players on precisely that read:

window.OptanonActiveGroups.includes('C0004')   // automobiles.honda.com

Un-parking is a pass that runs again, not once. A resource replacement runs where the CMP's own script tag is, which is in <head>, and uBlock Origin runs it at document_start - so every tag the page parked is still below it and does not exist yet. The pass therefore runs at boot, on each batch of nodes the parser delivers, at DOMContentLoaded and at load, and a pass after the first that frees something says so:

[consent-rr] cookieyes-reject-unblock 1.1.0 freed=3 deferred

Un-parking a tag claims no consent and asks for nothing on its own: uBlock Origin's own blocking still applies to whatever the freed tag then requests.

accept is for when you actually mean it: it grants consent in the cookie, to every TCF vendor and in the GPP string as well.

COMPARISON.md sets the three side by side, row by row, and then every resource in the repo against each other - measured by running the built files, not described.

Install

  1. Resources. uBlock Origin → Settings → Advanced settings → userResourcesLocation. Set it to whichever resource you want, or to both, whitespace-separated:

    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/onetrust-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/onetrust-accept.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/onetrust-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookieinformation-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookielawinfo-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/webtoffee-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/webtoffee-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/chcookieconsent-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/inmobi-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/iubenda-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/iubenda-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/iubenda-accept.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/osano-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/civic-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/civic-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookiebot-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookieconsent-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookieconsent-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookieconsent-accept.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/securiti-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/transcend-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/usercentrics-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/pubtech-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/termly-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/ketch-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/ketch-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/appconsent-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/appconsent-accept.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookiescript-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/didomi-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/didomi-accept.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/complianz-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/complianz-accept.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/zdconsent-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/zdconsent-accept.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/fundingchoices-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/tarteaucitron-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/tarteaucitron-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookieyes-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookieyes-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/consentmanager-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/consentmanager-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookiez-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/cookiez-reject-unblock.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/ampconsent-reject.js
    https://raw.githubusercontent.com/ryanbr/consent-rr/v1.55.0/dist/ampconsent-reject-unblock.js

    Then reload the filter lists (Filter lists → Purge all caches → Update now).

    Pinned to a release: the URL never moves, and it changes with each one, which is what makes uBO refetch - it will not ask again for a URL it already has. Swap the tag for main to track every push instead.

    Every release is on npm as well, so the same files come off a CDN if you would rather not fetch from GitHub - same bytes, same pinning:

    https://cdn.jsdelivr.net/npm/[email protected]/dist/onetrust-reject.js
    https://unpkg.com/[email protected]/dist/onetrust-reject.js

    The package is dist/ and filters/ and nothing else; npm i consent-rr is for hosting the files yourself rather than for importing anything.

    Each file stands on its own: nothing else has to be loaded for it to work. Its first line, /// onetrust-reject.js, is the resource header uBO reads - and a comment to JavaScript, so the file is a readable script at the same time.

  2. Filters. Paste filters/onetrust.txt, filters/cookieinformation.txt, filters/inmobi.txt, filters/osano.txt and filters/civic.txt and filters/cookiebot.txt and filters/securiti.txt and filters/transcend.txt and filters/usercentrics.txt and filters/pubtech.txt and filters/termly.txt and filters/ketch.txt and filters/appconsent.txt and filters/cookiescript.txt and filters/didomi.txt and filters/complianz.txt and filters/zdconsent.txt and filters/fundingchoices.txt and filters/tarteaucitron.txt and filters/cookieyes.txt and filters/consentmanager.txt and filters/cookiez.txt into My filters, or host them and subscribe via Import.

Redirecting the SDK's own request is the usual way in, but where a tag manager loads OneTrust there is no request to redirect - uBO's lists neuter googletagmanager.com/gtm.js, so the container never runs and otSDKStub.js is never asked for. The same resources inject as scriptlets, which also puts them at document_start:

example.com##+js(onetrust-reject)

Written without .js: uBO appends that itself when resolving a scriptlet token, so +js(onetrust-reject.js) looks for onetrust-reject.js.js, finds nothing and injects nothing at all. Only $redirect= takes the full resource name.

Check it took: each resource announces itself on load, so the console on a OneTrust site shows a line like

[consent-rr] onetrust-reject 1.6.0 groups=,C0001, tcf=refused gpp=refused

and OneTrust.consentRR reports the same mode and version.

uBlock Origin (the MV2 extension) only — uBO Lite cannot load user resources.

On Chromium, use the scriptlet form

A $redirect= rule naming a user resource cannot work in Chrome, Brave or Edge. Reported from the field as

GET https://transcend-cdn.com/cm/<id>/airgap.js net::ERR_UNSAFE_REDIRECT

and it is uBO's own storage that explains it. A resource uBO ships gets a warURL and is served from the extension; one of yours does not:

// redirect-engine.js, for its own
const entry = RedirectEntry.fromDetails({ ..., warURL: `/web_accessible_resources/${name}` });
// and for yours
this.resources.set(name, RedirectEntry.fromDetails({ mime, data, origin: 'user' }));

With no warURL, toURL() can only return a data: URI, and traffic.js hands that to the browser as the redirect target unchanged - it rewrites only the /-prefixed extension paths. Chromium refuses to redirect a network request to a data: URL, so the request fails outright: the real script does not load and neither does the replacement. Firefox allows it, which is the platform that path is built around.

Which api does what, per mechanism:

| mechanism | Firefox | Chromium | | --- | --- | --- | | $redirect= to one of uBO's own resources | webRequest.onBeforeRequest returns redirectUrl, an extension url under the manifest's web_accessible_resources: ["/web_accessible_resources/*"] | the same | | $redirect= to one of your resources | the same call, with redirectUrl: data:... - Firefox accepts a data: redirect target | nothing to call. Chromium accepts only http(s) or an extension url as a redirect target, and web_accessible_resources is declared in the manifest - there is no api, in MV2 or MV3, that adds a file to it at runtime. So a resource uBO was handed at runtime can never have an extension url, and the data: url it does have is refused: ERR_UNSAFE_REDIRECT | | $replace= | webRequest.filterResponseData(), a StreamFilter with ondata / onstop | no equivalent. uBO gates the whole feature on canFilterResponseData: typeof browser.webRequest.filterResponseData === 'function', and Session.canFilter() returns early when it is false | | scriptlet ##+js(...) | scripting.registerContentScripts(), tabs.executeScript | the same |

So the asymmetry is not that Chromium is missing a body-rewriting api it could be given - it is that both ways of replacing a request need something Chromium does not offer a user resource, while injection needs neither. Brave's own shields, with no extension in the picture, land the same way: their engine implements $redirect only to its own bundled resource library, and does not parse replace at all (brave/brave-browser#56671).

If you want $redirect= on Chromium, patch uBO rather than the browser

The reason a user resource cannot be redirected to is that it has no warURL, and the reason it has no warURL is that web_accessible_resources is declared in a manifest. Put the file in the extension and both go away. uBO keeps its own in one folder and one map:

// src/js/redirect-resources.js
export default new Map([
    [ '1x1.gif', { alias: '1x1-transparent.gif', data: 'blob' } ],
    ...
npm run ubo-war -- ../uBlock --check     # say what it would do
npm run ubo-war -- ../uBlock             # copy them in and list them
cd ../uBlock && ./tools/make-chromium.sh
# then load dist/build/uBlock0.chromium unpacked, with developer mode on

tools/ubo-war.mjs copies dist/*.js into their src/web_accessible_resources/ and adds one block to the Map in their src/js/redirect-resources.js:

export default new Map([
    // consent-rr resources, added by tools/ubo-war.mjs
    [ 'ampconsent-reject.js', {} ],
    ...
    // end consent-rr resources
    [ '1x1.gif', { alias: '1x1-transparent.gif', data: 'blob' } ],

It refuses a directory that is not a uBO checkout, writes nothing under --check, replaces its own block rather than stacking another one on each rebuild, and names a resource or two to take just those. Drop those names from userResourcesLocation afterwards - they are in the extension now, and a name in both places is served twice.

storeWAR() then gives it warURL: /web_accessible_resources/<name>, patchLocalRedirectURL() turns that into an extension url, and $redirect=transcend-reject.js works on Chromium exactly as it does on Firefox - with no userResourcesLocation entry at all, because the resource is in the extension now.

The cost is an unpacked build that does not auto-update, and redoing it on each uBO release. Which is fine for testing a filter and no use for shipping to anyone else - so the scriptlet form is still what a list ships.

Patching the browser instead is the expensive way round, and for this it does not even arrive:

  • webRequest.filterResponseData is what $replace= needs, and Chromium has never exposed a response body to an extension. Porting it means a new extension api - schema, renderer bindings for the StreamFilter object, and taking the body data pipe out of WebRequestProxyingURLLoaderFactory to ferry chunks to the extension and back - a permanently diverging patch for an api only uBO would call. And $replace= carries its replacement inline in the filter line: 11 kB over 320 lines, with no newlines allowed and every $, / and \ escaped. It is built for surgical edits, not for shipping a program.
  • Allowing a data: redirect target is the small patch - URLRequestJob::CanFollowRedirect() returns ERR_UNSAFE_REDIRECT through IsSafeRedirect(), and the extensions layer blocks it too - but it reopens something Chromium closed deliberately, for every extension the browser has, to save a file copy.
  • The one Brave would plausibly take is $replace in their own engine, because they already own the hard part: components/body_sniffer/ is a URLLoaderThrottle with a BodyHandler that buffers and rewrites a response body, used today by De-AMP. That leaves adding replace to adblock-rust's option parser, which is brave/brave-browser#56671.

So on a Chromium browser, inject instead of redirecting - same resource, same document_start, no redirect involved:

example.com##+js(transcend-reject)

and where the CMP's own script would otherwise still run, block it with a plain rule beside the scriptlet rather than redirecting it:

||example.com/wp-content/plugins/webtoffee-gdpr-cookie-consent/public/js/cookie-law-info-public.js$script
example.com##+js(webtoffee-reject)

A broad rule now says so, instead of going quiet

||transcend-cdn.com^$script,redirect=transcend-reject.js writes nothing, in either browser, and for two separate reasons:

  • On Chromium the request fails before anything runs - ERR_UNSAFE_REDIRECT, per above. Nothing is stored anywhere, because nothing executes.
  • In any browser it catches airgap.js, which is the engine. The localStorage record is written by their code, in response to this resource calling their setConsent - so with the engine replaced there is nobody to write it and nobody to enforce it either.

The gate makes that worse before it makes it better: waiting for their global means a replaced engine never defines one, so a resource that used to say via=no engine would wait in silence. So the gate also looks at document.currentScript - a scriptlet is injected inline and carries an empty src, while a resource served in place of a file carries the url that was asked for. Served as a file, the body runs at once and says what it found:

[consent-rr] transcend-reject 1.4.0 refused=(none) via=no engine

which is the one line that tells a filter author the rule took the engine.

One line for every site, rather than a line per site

A redirect rule needs no domain, and a filter list wants the scriptlet form to need none either:

*##+js(transcend-reject)

which injects it everywhere - so the resource has to be inert on a page that has never heard of its CMP. Measured when this was written, with each resource dropped onto a blank page: 2 of 46 were. The rest wrote a cookie, installed a global, changed the document or said a line, none of which belongs on somebody else's page. test/global-inject.test.mjs is that measurement as a test, with the list of the ones still to do.

src/shared/lib/present.js is the gate, and Transcend is gated: their loader defines self.airgap before anything of theirs can be used, so the assignment is the signal. It waits for that with an accessor, hands the value straight back as a plain property, and queues the refusal in the queue their own prelude made. On a page that never defines it, nothing runs - no global carrying a value, no cookie, no storage, no line. So on Chromium, Transcend needs exactly one global line and no redirect rule at all:

*##+js(transcend-reject)

Their own ui.js is never fetched once a refusal is recorded, so there is nothing left to block.

Or name the sites, because it only has to run once on each. Their record is persistent and their own boot takes it: with a confirmed refusal stored, their engine does not prompt and so never fetches the banner. The tenants read here carry consentExpiry: 525600 minutes with onConsentExpiry: Prompt, so one run answers for a year - and the line wants leaving in place for the next one, and for a visitor who clears their storage. filters/transcend.txt ships the tenants this repo has evidence for:

airtable.com,classy.org,costco.ca,costco.com,gofundme.com,indiegogo.com,mayoclinic.org,skilljar.com,transcend.io##+js(transcend-reject)

uBO matches subdomains, so costco.ca covers www.costco.ca. To add one of your own - and to find the rest of a tenant's domains while you are there - a tenant's own airgap.js names them, at the id in its url:

transcend-cdn.com/cm/8aaeb48f-.../airgap.js
sites: "gofundme.com gfm-test01.com gfm-dev.com gfm-dev01.com classy.org
        classy-test.org skilljar.com gfm-lab.com"

which is the same list their engine matches the host against to decide whether to write the tcm cookie at all.

Two families will not take a block rule beside the scriptlet, for reasons in their own sections: Transcend, where airgap.js is the half that does the enforcing, and AMP, where blocking amp-consent-0.1.mjs makes the runtime re-fetch the real extension from its /rtv/ url.

Every family's filters/*.txt carries the scriptlet line as well as the redirect rules, and the console line is how to tell which one took.

What the stub actually does

The surface was read off OneTrust's own otSDKStub.js and otBannerSdk.js, so page code cannot tell it apart from a return visit. No OneTrust code is reproduced.

  • window.OnetrustActiveGroups and window.OptanonActiveGroups, both as ,C0001,C0002, — the SDK's exact format.
  • window.OneTrust and window.Optanon, one object, assigned over anything the page preset there (a site can set geolocationResponse before load, and the real SDK keeps it).
  • OptanonConsent and OptanonAlertBoxClosed cookies, with the fields the SDK writes, including interactionCount=1 and intType (1 is its "Banner - Allow All", 2 its "Banner - Reject All"). An existing consentId is reused so a site does not see a brand new visitor on every page load.
  • dataLayer seeded or pushed with the SDK's own OneTrustLoaded / OptanonLoaded events, plus the OneTrustGroupsUpdated entry the banner half adds once consent is announced, so GTM triggers still fire.
  • InsertScript() and InsertHtml() gated per category the way canInsertForGroup() is, so a strictly-necessary insert still goes in while everything else is refused, and options.ignoreGroupCheck still overrides.
  • window.OptanonWrapper() called once, and kept looked-for a few seconds because pages often declare it after the SDK tag.
  • OneTrustGroupsUpdated dispatched on window with the granted ids.
  • Server-rendered banner markup (#onetrust-consent-sdk and friends) removed, as it arrives.
  • The IAB GPP layer, the US counterpart, which otSDKStub.js installs itself: window.__gpp (ping, addEventListener, removeEventListener, hasSection, getSection, getField, plus the queue and events accessors), the __gppLocator frame and the __gppCall bridge. The section is usnat: refusing asserts the sale, sharing and targeted-advertising opt-outs, accepting declines them, and the Global Privacy Control bit carries the browser's own signal either way, since that is a fact about the request rather than part of the decision. getGPPData is refused, as it is not a command in GPP 1.1 and the reference implementation refuses it too.
  • The IAB TCF layer the SDK installs when a tenant enables it: window.__tcfapi (ping, getTCData, getInAppTCData, addEventListener, removeEventListener), the __tcfapiLocator frame and the postMessage bridge framed vendors use, calls a page stub parked on __tcfapi.a answered, and the TC string stored in eupubconsent-v2.
  • Both halves of the SDK's substitutePlainTextScriptTags(): a script[type="text/plain"] gated on a category is replaced by a live copy of itself, and a tag carrying data-src gets its src back. As in reactivateTag(), only the tag's own categories decide, never the mode - so a tag gated on nothing but C0001 is revived by reject too, while one naming C0004 as well stays parked. Categories are read from optanon-category-* and ot-vscat-* class names, including ids the site invented, and a MutationObserver keeps handling tags added later.
  • Tags a site parked itself rather than letting OtAutoBlock.js do it: the categories in a data-optanon-category attribute, the source in data-src or base64 in data-obfuscated-src, which is moved across and decoded the way such a loader does it. Every category a tag names still has to be consented, as canInsertForGroup() requires - a site's own loader may only ask whether any of them is, but being that loose would load an advertising tag off the back of a consented necessary one.

Deliberate gaps

  • The TC string follows the resource, and its shape is copied from one a real OneTrust reject-all wrote: consents all zero, but vendor legitimate interests left intact, because refusing does not object to legitimate interest - that needs a separate action. Policy version 5, and timestamps rounded to midday UTC so the string is stable for a day rather than unique per page load. Vendor ids are handled as one range to 1500 instead of a bit each.

  • What varies per tenant is left alone rather than guessed. Four sites sampled disagreed on the publisher country (DE, DE, US), on whether a refusal keeps legitimate interest at the purpose level (two of three did not), on how many vendors keep it (15, 22, 390) and on publisher restrictions (none, none, ten). So publisherCC is AA, the user-assigned code rather than a country; a refusal keeps legitimate interest for vendors but not for purposes, the majority shape; and no publisher restrictions are written. vendorListVersion is 178, which every sample carried. Consent language is read off the page's lang. The string carries the publisher segment beside the core, as a real one does, and vendor ids run to 2000 - a real string reached 1650.

  • Google's Additional Consent string is written as 2~~dv, in the OTAdditionalConsentString cookie and as addtlConsent in the TCF answer: version, then an empty consented list, then an empty disclosed one. A real refusal consents to no AC vendor either - its long tail is a disclosure record, not consent. A real acceptance moves some 600 ids into the consented slot, which is Google's own global list rather than anything a replacement can derive, so neither resource claims them.

  • The IAB layer goes in whether or not the tenant had one. Plenty of OneTrust tenants run with the IAB module off and write no eupubconsent-v2 at all, and a replacement cannot tell which, since that lives in domain data it never fetches. So those pages get a CMP where they had none, and anything probing window.__tcfapi - Google's ad stack, mostly - starts taking TCF into account. On reject that is the conservative direction, non-personalised or limited ads; on accept it grants. It goes in unconditionally because the alternative fails worse: a site that gates its player on __tcfapi never starts without one, with no banner left to click.

  • Category sets are tenant-specific - one site defines C0001 to C0004 plus an IAB stack group, another only C0001, C0002 and C0004 - so the cookie carries C0001 to C0005 plus whatever the page's own class names mention. Deliberately a superset: a site asking about a category its tenant never defined still gets an answer rather than nothing.

  • Sites commonly keep their own record of the choice beside OneTrust's and re-prompt until it is set, so both resources set localStorage cookieChoiceMade to true - the cookiechoices.js convention, which OneTrust itself never touches. The key records that a choice was made, not which way it went. A site using some other key needs a per-site set-local-storage-item rule; filters/onetrust.txt shows the form.

  • consent.onetrust is never dispatched. OnConsentChanged() registers a real listener, but consent never changes here — exactly like a return visit whose choice is already stored. A site that only initialises from that event will behave as it does for a returning visitor.

  • No claimed location. getGeolocationData() answers empty rather than inventing a region for a site to branch on.

  • Cookies are scoped to the registered domain, as the SDK scopes its own - found by probing, since a page has no public suffix list and a cookie set on one is refused. A host-only copy would not replace the SDK's, it would shadow it: two OptanonConsent cookies, and a site taking the first match reads whichever is older. Seen happening on a live site.

  • Their do-not-track flag is not reproduced. Theirs reads it - this.DNTEnabled = "yes" === navigator.doNotTrack || "1" === navigator.doNotTrack

    • and gives a group the status dnt where the group's own IsDntEnabled is set, which their checkIfGroupHasConsent then treats as not consented. A refusal writes every group off anyway, so this only shows up in onetrust-accept.js, which grants a group their own code would have left alone. Which groups carry IsDntEnabled is tenant configuration that is not on the page, so nothing here can tell them apart; GPC is honoured instead because the flag is per-visitor rather than per-group.
  • A page whose CSP omits data: for scripts silently refuses a redirected resource. User resources have no extension URL, so uBO serves them as a data: URI, and script-src-elem/script-src/default-src without data: blocks it. Nothing of ours runs: no console line, no cookie, no marker - and because the real SDK was replaced at the network layer, the banner is gone too, which makes it look like the resource worked. Seen on almbrand.dk, whose CSP allows 'self' and named hosts only.

    The scriptlet form is not fetched, so it is not subject to that directive:

    ||policy.app.cookieinformation.com/uc.js$script,redirect=cookieinformation-reject.js,domain=example.com
    example.com##+js(cookieinformation-reject)

    The redirect keeps the real SDK out; the scriptlet supplies the stub. The tell is a CSP violation in the console naming a data: script, and CookieInformation.consentRR or OneTrust.consentRR being undefined.

  • Trusted Types enforcement can likewise block tag revival.

  • Accept mode revives advertising tags too, which is what accepting means. uBO still blocks the requests they make.

InMobi Choice

Two files make up this CMP, and the replacement goes on the second one:

||cmp.inmobi.com/tcfv2/cmp2.js$script,redirect=inmobi-reject.js

choice.js is a per-site loader. It inserts cmp2.js, injects the banner's CSS, and calls __tcfapi('init', 2, fn, config) with the tenant's entire configuration inline. cmp2.js reads that config back out of the page's IAB stub - window.__tcfapi() with no arguments returns the stub's queue, and the argument list whose first entry is init carries it. So leaving choice.js alone is what gets the tenant's own publisher country, consent language and legitimate-interest purposes into the answer; replacing choice.js instead works too, on defaults.

The stub then installs what cmp2.js installs: window.__tcfapi (the built-in commands plus the custom ones the page itself calls, init, getConfig and the displayConsentUi behind a privacy-settings button), window.__gpp with a tcfeuv2 section, window.__uspapi, window.__tcfapiui, both locator frames and both postMessage bridges, and a gtag shim on dataLayer where the page has none. Anything the page parked before the redirect landed is replayed. The TC string goes into euconsent-v2 and the GPP string into IABGPP_HDR_GppString, with the attributes and the 390-day life cmp2.js uses - scoped to the hostname, as its own writer scopes it, rather than to the registered domain.

Its refusal was measured against a real one on the same tenant, and differs in three places, all documented in the source: a real refusal keeps legitimate interest for 212 named vendors where this keeps it as one range (which 212 is a fact about the vendor list, not about the page); it carries five publisher restrictions and a disclosed-vendors segment listing 1015 vendors, neither of which can be derived. Global Privacy Control withdraws legitimate interest altogether, as it does for OneTrust. addtl_consent is deliberately not written: with nothing consented to, cmp2.js deletes that cookie rather than writing one. __uspapi answers 1---, no notice and no opt-out applicable, because where in the world the visitor is is not something a page can tell.

Osano

The whole CMP is one per-tenant file, so there is one thing to replace:

||cmp.osano.com/*/osano.js$script,redirect=osano-reject.js

window.Osano is a function of their own making - osano.js installs Osano = Osano || function(){ Osano.data.push(arguments) } so a page can call it before the script lands, then drains that queue and replaces its push so later calls are handled live. This does the same, with their own mapping: Osano("onConsentSaved", fn) becomes the osano-cm-consent-saved listener, and any other first argument sets a property on Osano.cm.

Osano.cm answers as it would on a return visit: getConsent(), the analytics / marketing / personalization / optOut flags, locale, userData, the event methods, and the show/hide methods as no-ops, because nothing was rendered to show. The record goes into osano_consentmanager and osano_consentmanager_uuid - in localStorage and a cookie, as theirs does, scoped to the registered domain for a year - and osano_consentmanager_expdate is cleared, which is what their own save does. An id and timestamp already stored are kept, so a site does not see a decision made afresh on every load. Google consent mode gets their signal map, with ad_storage, ad_user_data, ad_personalization, analytics_storage and personalization_storage denied and the two ESSENTIAL ones granted.

The IAB layers

How much Osano installs depends on the tenant, and the tail of its bundle says which: C({usp: ...}) for a tenant with the IAB module off, or C({gpp: ..., tcf: ..., usp: ...}) for one with it on. All three go in here, because that switch lives in configuration a page cannot be asked, and a vendor stalled on an API that never answers is the worse failure.

__tcfapi carries their own values - cmpId 279, cmpVersion 3332, policy version 5, GVL fallback 187 - and their own default IAB state: no purpose consents, legitimate interest for purposes 2, 7, 8, 9, 10 and 11, and no vendors at all, which is where this differs from the OneTrust and InMobi resources, both of which grant vendors a range. Their command set is setGdprApplies, ping, getTCData, addEventListener and removeEventListener - no getInAppTCData, no getVendorList - and that is the set answered. The string carries the core segment alone, as their field sequence does, with timestamps at UTC midnight.

__gpp reports the two sections it can build, tcfeuv2 (2) and uspv1 (6), under the DBACNYA header; their Canadian section is left out rather than invented. Their own passthrough works too: __gpp("uspv1.getUSPData", fn) is routed to that section's API. __uspapi answers 1---, or 1-Y- where the browser sends Global Privacy Control - which is also the one input their code turns into a CCPA opt-out by itself, so OPT_OUT follows it, and here it withdraws legitimate interest as well.

Deliberate gaps

  • Nothing is un-blocked, because nothing was blocked. Osano holds tags back by patching the DOM at runtime - createElement, setAttribute, the src setters, document.cookie - rather than by parking them in the markup the way OneTrust and Cookie Information do. With the CMP replaced, a tag it would have held back simply runs, and uBlock Origin blocks what it makes of it at the network layer. Re-implementing that interception would mean shipping a second content blocker inside a consent stub.
  • The record is plain JSON where theirs is encrypted. Their own reader tries JSON.parse first and only then decrypts, so what this writes is what osano.js itself would read back if it ever loaded - it would honour the refusal rather than re-prompt. The cookie copy is percent-encoded, unlike theirs: an unencoded quote or comma in a Cookie header is what a strict server-side parser refuses, taking the rest of the header with it. localStorage, which their reader consults first, carries it verbatim.
  • Two departures inside the IAB layer. Their publisher country falls back to US where the location lookup has not answered; this writes AA, the user-assigned code, because US names a country a page cannot know. And Global Privacy Control withdraws legitimate interest here, as it does in the other resources - their own default keeps it either way, but that signal is the objection a plain refusal is not.
  • Tenant data is left empty rather than invented: jurisdiction and countryCode come from a location lookup, revision, cmpContentHash and publishTimestamp from the tenant's own configuration. gdprApplies answers true, the protective answer where it cannot be known.

Civic Cookie Control

||cc.cdn.civiccomputing.com/9/cookieControl-9*.js$script,redirect=civic-reject.js

The page drives this one. It loads the script and then calls CookieControl.load({...}) with its whole configuration inline - the categories, their onAccept and onRevoke callbacks, the cookie settings, and whether the IAB module is on - so everything the stub answers with is the site's own, and none of it has to be guessed at.

CookieControl is in place before that call and carries their method set: load, update, config, info, getCategoryConsent, changeCategory, toggleCategory, open, hide, notify, acceptAll, rejectAll, the getCookie / getAllCookies / saveCookie / delete helpers, geoInfo and geoTest. The decision goes into their CookieControl cookie as URL-encoded JSON - necessaryCookies, optionalCookies keyed by their own _validCookieName (the name with separators stripped, so marketing (social) becomes marketingsocial), statement, consentDate, consentExpiry, interactedWith and user - scoped to the registered domain, SameSite=Lax, for the site's consentCookieExpiry or 90 days. An existing record's user and consentDate are kept, so a site does not see a decision made afresh each load.

interactedWith: true is what does the work: their finaliseSetup only builds a notification when it is false.

One difference from what their own script writes, and it is deliberate: a real refusal leaves optionalCookies empty, where this names every category as revoked. Their code accepts anything that is not revoked - a ccpa-mode site does that to every category on load, and a gdpr-mode one to any category whose lawfulBasis is legitimate interest - so an empty map hands those straight back. Everything else matches field for field, down to the uuid shape.

The record goes in as plain JSON, not percent-encoded - their saveConsent passes configuration.encodeCookie as the encode flag and it is false by default. That is not a detail: sites read this cookie back with their own helpers, and those do not decode. Goldsmiths runs JSON.parse straight over the raw value and asks whether a category is accepted, so an encoded record throws there and the site concludes nothing was consented to - which is exactly what an earlier version of this resource caused. Where a site sets encodeCookie, theirs encodes and so does this. (Osano's cookie is the other way round: theirs is encrypted, so the plain JSON written there is percent-encoded to keep a Cookie header well formed. The rule is the CMP's own format, not a preference.)

The IAB layer

Unlike the other consent managers here, this one needs no guessing: the IAB module is a paid option and the page declares it as iabCMP: true. With it off their script installs no __tcfapi at all, and neither does this. With it on, __tcfapi goes in with cmpId 259 and cmpVersion 9, their update / ping / getTCData / addEventListener / removeEventListener set, the __tcfapiLocator frame and the postMessage bridge - and the TC string goes where theirs goes, into iabConsent inside the same cookie, which their reader takes verbatim when no compressed addtlConsent sits beside it. The separate CookieControlTC cookie follows setCookieControlTC, as theirs does.

A refusal here turns legitimate interest off as well, which is where this differs from the OneTrust and InMobi resources: their _defaultStore has every purpose consent and legitimate interest false, and their reject-all leaves them that way.

Sites that withhold content

Refusing is the point, but a site may gate its videos, maps or embeds on one of its own categories and show a placeholder until that category's onAccept has run. Goldsmiths does exactly that, from the configuration on its own page:

{ name: "embedded", label: "Embedded content", …
  onAccept: function() {
      dataLayer.push({ civic_cookies_embedded: "consent_given", … });
      document.dispatchEvent(new Event("embeddedConsentGiven"));   // the page listens for this
  } }

The category is the site's own, so no resource can know its name. Name it in the filter instead, alongside the redirect:

gold.ac.uk##+js(civic-reject, embedded)

That category is then recorded as accepted, its onAccept runs, and anything parked for it with data-cc-category gets its data-src back - their own accept path, for that one category. Everything else stays refused. Up to three names, and * for all of them; the console line prints the names a site uses.

Both lines are needed: the scriptlet supplies the stub with its argument at document_start, and the redirect keeps the real script from replacing it.

Arguments only reach the scriptlet form - a $redirect= takes none. Where one line is wanted instead, civic-reject-unblock.js decides for itself:

||cc.cdn.civiccomputing.com/9/cookieControl-9*.js$script,redirect=civic-reject-unblock.js:10,domain=example.com

The :10 raises its priority above the plain civic-reject.js rule, which matches the same request.

It accepts the categories whose name and label do not read as tracking, and refuses the ones that do - analyt, statistic, performance, advertis, marketing, targeting, tracking, remarket, personali[sz]. On Goldsmiths that is embedded accepted, analytics and advertising refused, so the videos play while the two gtag("consent", "update", …) calls their other categories make are never run.

This is the one thing in the repo that is guessed at rather than read off somebody's code, so the console line prints both lists:

[consent-rr] civic-reject-unblock 1.4.0 mode=gdpr revoked=analytics,advertising accepted=embedded iab=off cookie=written

Plenty of sites park their embeds under a category called marketing, where that guess refuses the thing you wanted. Name it instead - an argument overrules the guess, in either direction. Either way the IAB layer still refuses: unblocking a site's own content is no reason to consent for a vendor list.

Deliberate gaps

  • Nothing is freed and nothing is deleted. A tag parked for a category carries data-cc-category and data-src, and their script frees it by copying data-src into src when that category is accepted. None is, so parked tags stay parked. Their deleteAll - which removes every cookie outside the consented set on each load - answers false here: that is the blocking half of this CMP, uBlock Origin is doing it, and deleting a visitor's cookies is not a consent stub's to do.
  • The category callbacks are not called. Their own load calls onAccept only for accepted categories, and onRevoke only when somebody changes one. Nothing is accepted and nobody changed anything, so neither fires. onLoad does, a second later, as theirs does.
  • The decision cannot be changed from the page. changeCategory, toggleCategory, acceptAll and rejectAll answer without doing anything - theirs re-render a panel that was never built. A site whose own preferences page is built on those calls will find them inert, so a category that has to be on is named in the filter instead.
  • tcfPolicyVersion is answered as 4 while the string carries 5. That is their inconsistency - their API hardcodes 4, their encoder takes 5 from the vendor list they fetch - kept rather than tidied up, so a vendor branching on either gets what their script would have given it.
  • No API key check, and no claimed location. Theirs will not start without validating the key against apikeys.civiccomputing.com, which also returns the visitor's country. geo is null and geoInfo() answers false.
  • Version 9 only. Version 8 is a different, much smaller build and the filter deliberately does not match it.

Cookiebot

||consent.cookiebot.com/uc.js$script,redirect=cookiebot-reject.js
||consent.cookiebot.eu/uc.js$script,redirect=cookiebot-reject.js

uc.js is the engine - it defines the API, blocks the tags, writes the cookie and fires the events. cc.js beside it is the dialog and the site's own configuration, and is never asked for once uc.js is replaced.

window.CookieConsent and window.Cookiebot are one object, as theirs are, and it carries their default state, which is already a refusal: necessary true, preferences, statistics and marketing false, consented false, declined true, and hasResponse true, which is what stops their banner being built. The site's configuration is read off their own script tag - data-cbid, data-framework, data-user-country - so Cookiebot.serial answers with the site's id rather than an empty string.

The cookie is written the way theirs is, with the quotes and commas already percent-escaped inside the value, which their own reader unescapes:

CookieConsent={stamp:%270%27%2Cnecessary:true%2Cpreferences:false%2Cstatistics:false%2Cmarketing:false%2Cmethod:%27explicit%27%2Cver:1%2Cutc:…}

Run through their own parser that yields declined. A stamp already issued is kept; where there is none their placeholder 0 stands in, because the real one is a hash their server issues and nothing here can compute it. The region is named only where the site's tag says which it is.

Their events fire in their order - CookiebotOnLoad, then the declined half, then CookiebotOnTagsExecuted, then CookiebotOnConsentReady a tick later - each with its CookieConsent… twin and its CookiebotCallback_… global. The consent-mode signals are theirs too, values and all: Google's seven keys with security_storage granted and the rest denied, their developer id, ads_data_redaction, Microsoft's uetq and Clarity.

Deliberate gaps

  • A tag marked necessary runs; everything else stays parked. Their own check tests a tag's categories against preferences, statistics and marketing only, so a tag naming none of those is freed - by this as by them. script[type="text/plain"][data-cookieconsent] and the data-src / data-cookieblock-src forms on iframe, img, embed, video, audio, picture and source are all handled, ignore is left alone, and the cookieconsent-optin-… classes go on either way.
  • No IAB TCF layer. Where a site sets data-framework to one of the IAB values, uc.js installs the IAB stub and then loads a separate module that implements __tcfapi. That module is in neither file, so its identity cannot be read off anything and a TC string is not something to invent. Such a site is named on the console line - iab=IAB rather than iab=off - so it is visible rather than silent. Tell me if you hit one and it can be built from that site's own module.
  • The decision cannot be changed from the page. show, renew, withdraw and submitCustomConsent answer without doing anything: theirs re-render a dialog that was never built.

Securiti

||cdn-prod.securiti.ai/consent/cookie-consent-sdk-loader.js$script,redirect=securiti-reject.js

The loader is a bootstrapper: it asks app.securiti.ai where the visitor is, decides whether TCF applies, and then fetches the SDK - 600 kB of it - with its stylesheet, its utils and the site's configuration. Replacing the loader means none of that is requested.

It parks five functions for the SDK to drain - initCmp, setConsentBannerParams, showConsentPreferencesPopup, overrideThemeMatching and registerSrtiCookieSDKEvents - and those answer here rather than queueing for something that never arrives. window.SecuritiSDK carries their registerEvent and onReady, and the events that describe a decision already made - onLoad, onReady, onConsentGiven - are answered on registration, because theirs fire them once the SDK is ready and this is ready as soon as it exists. Their Google consent mode goes out denied, in their own key order, the way gtag pushes it.

The categories are the part no page can supply. They live in the tenant's configuration, fetched from their CDN by id, so a refusal cannot name them - and does not have to. Every reader in their SDK asks whether a category's id is set in the record's consents map, so a record whose map is empty refuses all of them, whatever they turn out to be called:

{"consents":{},"st":{},"gcm":{"…":"…","security_storage":"granted"},"ts":1790666096}

That goes in __privaci_cookie_consents with __privaci_cookie_consent_uuid beside it, and __privaci_cookie_no_action - the marker that says nobody has answered - is cleared. A visitor id and timestamp already stored are kept.

Their auto-blocking script

A site may load a second, per-tenant file beside the loader:

cdn-app3.securiti.ai/consent/auto_blocking/<tenant>/<domain>.js

Leave it alone. It blocks tags by the site's own classification - moving src to data-src and the type to text/plain - and releases a category when the SDK calls setConsentedCategories. With the loader replaced that call never comes, and its own rule is

function O(e) {                                 // allow this resource?
    var t = n.concat(c.non_optout_categories);  // consented ids + Essential
    return t.length && e && e.length && t.some(t => -1 < e.indexOf(t));
}

so it reads the refusal this writes, releases nothing, and still lets essential scripts run. That is the refusal enforced a second time by the site's own list, at no cost - blocking or nooping that file makes things worse, not better.

One caveat: it reads the consent cookie when it loads, which can be before the loader runs. A visitor who had previously accepted gets one more page load on the old cookie before this takes over.

Deliberate gaps

  • The category names are never known, so anything a site drives off them - onCategoryConsented, a preference centre built from them - sees an empty map rather than a list of refusals. Nothing is granted either way.
  • No location is claimed. __isTcfEnabledForLocation is false and getUserLocationAndLanguage() answers null; theirs come back from the lookup this never makes.
  • No IAB layer. Where a tenant's location has TCF on, their loader also fetches sdk-stub.js and the SDK implements __tcfapi. None of that is put back, for the same reason as Cookiebot: the identity is not in any file served here, and a TC string is not something to invent.

Transcend

||transcend-cdn.com/cm*/*/ui.js$script,redirect=transcend-reject.js
||transcend-cdn.com/cm*/*/uiV2.js$script,redirect=transcend-reject.js
||assets.mayoclinic.org/content/dam/cpm-transcend/ui.js$script,redirect=transcend-reject.js

Replace the banner, not the engine. Transcend ships in two halves, and the page loads the engine first: airgap.js is the init script, and it carries the tenant's whole configuration - the purposes, the cookie-to-purpose table, the allowed hosts - and blocks requests and cookies itself, by consent. ui.js is the banner, 390 kB of Preact, which airgap fetches only when it decides to prompt.

So this stands in for ui.js: nothing is rendered, and the refusal is recorded through airgap's own API, which leaves the engine in place as the thing enforcing it.

airgap.ready(ag => {
    const purposes = ag.getConsent().purposes;   // the tenant's own names
    …                                            // every one of them false
    ag.setConsent(null, refused, { confirmed: true, prompted: true, timestamp });
});

The purpose names never have to be known: getConsent() hands them over, including their tri-state "Auto", which becomes an explicit no. Eight tenants sampled - Costco, Airtable, Mayo Clinic and five others - have between four and seven purposes, and barely any two sets are the same; Airtable's include Marketing, Sales and EnrichmentConsent, and others add Video, GcmAdvanced, BrowserCookies or AdvertisingSaleOfInfo. The resource was run against all e