consent-rr
v1.55.0
Published
Cookie-consent resource replacements for uBlock Origin
Downloads
6,729
Maintainers
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.comUn-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 deferredUn-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
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.jsThen 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
mainto 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.jsThe package is
dist/andfilters/and nothing else;npm i consent-rris 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.Filters. Paste
filters/onetrust.txt,filters/cookieinformation.txt,filters/inmobi.txt,filters/osano.txtandfilters/civic.txtandfilters/cookiebot.txtandfilters/securiti.txtandfilters/transcend.txtandfilters/usercentrics.txtandfilters/pubtech.txtandfilters/termly.txtandfilters/ketch.txtandfilters/appconsent.txtandfilters/cookiescript.txtandfilters/didomi.txtandfilters/complianz.txtandfilters/zdconsent.txtandfilters/fundingchoices.txtandfilters/tarteaucitron.txtandfilters/cookieyes.txtandfilters/consentmanager.txtandfilters/cookiez.txtinto 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=refusedand 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_REDIRECTand 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 ontools/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.filterResponseDatais 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 ofWebRequestProxyingURLLoaderFactoryto 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()returnsERR_UNSAFE_REDIRECTthroughIsSafeRedirect(), 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
$replacein their own engine, because they already own the hard part:components/body_sniffer/is aURLLoaderThrottlewith aBodyHandlerthat buffers and rewrites a response body, used today by De-AMP. That leaves addingreplaceto 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. ThelocalStoragerecord is written by their code, in response to this resource calling theirsetConsent- 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 enginewhich 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.OnetrustActiveGroupsandwindow.OptanonActiveGroups, both as,C0001,C0002,— the SDK's exact format.window.OneTrustandwindow.Optanon, one object, assigned over anything the page preset there (a site can setgeolocationResponsebefore load, and the real SDK keeps it).OptanonConsentandOptanonAlertBoxClosedcookies, with the fields the SDK writes, includinginteractionCount=1andintType(1is its "Banner - Allow All",2its "Banner - Reject All"). An existingconsentIdis reused so a site does not see a brand new visitor on every page load.dataLayerseeded or pushed with the SDK's ownOneTrustLoaded/OptanonLoadedevents, plus theOneTrustGroupsUpdatedentry the banner half adds once consent is announced, so GTM triggers still fire.InsertScript()andInsertHtml()gated per category the waycanInsertForGroup()is, so a strictly-necessary insert still goes in while everything else is refused, andoptions.ignoreGroupCheckstill overrides.window.OptanonWrapper()called once, and kept looked-for a few seconds because pages often declare it after the SDK tag.OneTrustGroupsUpdateddispatched onwindowwith the granted ids.- Server-rendered banner markup (
#onetrust-consent-sdkand friends) removed, as it arrives. - The IAB GPP layer, the US counterpart, which
otSDKStub.jsinstalls itself:window.__gpp(ping,addEventListener,removeEventListener,hasSection,getSection,getField, plus thequeueandeventsaccessors), the__gppLocatorframe and the__gppCallbridge. The section isusnat: 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.getGPPDatais 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__tcfapiLocatorframe and thepostMessagebridge framed vendors use, calls a page stub parked on__tcfapi.aanswered, and the TC string stored ineupubconsent-v2. - Both halves of the SDK's
substitutePlainTextScriptTags(): ascript[type="text/plain"]gated on a category is replaced by a live copy of itself, and a tag carryingdata-srcgets itssrcback. As inreactivateTag(), only the tag's own categories decide, never the mode - so a tag gated on nothing butC0001is revived by reject too, while one namingC0004as well stays parked. Categories are read fromoptanon-category-*andot-vscat-*class names, including ids the site invented, and aMutationObserverkeeps handling tags added later. - Tags a site parked itself rather than letting
OtAutoBlock.jsdo it: the categories in adata-optanon-categoryattribute, the source indata-srcor base64 indata-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, ascanInsertForGroup()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
publisherCCisAA, 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.vendorListVersionis178, which every sample carried. Consent language is read off the page'slang. 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 theOTAdditionalConsentStringcookie and asaddtlConsentin 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-v2at 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 probingwindow.__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__tcfapinever starts without one, with no banner left to click.Category sets are tenant-specific - one site defines
C0001toC0004plus an IAB stack group, another onlyC0001,C0002andC0004- so the cookie carriesC0001toC0005plus 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
localStoragecookieChoiceMadetotrue- thecookiechoices.jsconvention, 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-siteset-local-storage-itemrule;filters/onetrust.txtshows the form.consent.onetrustis 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
OptanonConsentcookies, 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
dntwhere the group's ownIsDntEnabledis set, which theircheckIfGroupHasConsentthen treats as not consented. A refusal writes every group off anyway, so this only shows up inonetrust-accept.js, which grants a group their own code would have left alone. Which groups carryIsDntEnabledis 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.
- and gives a group the status
A page whose CSP omits
data:for scripts silently refuses a redirected resource. User resources have no extension URL, so uBO serves them as adata:URI, andscript-src-elem/script-src/default-srcwithoutdata: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, andCookieInformation.consentRRorOneTrust.consentRRbeing 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.jschoice.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.jswindow.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, thesrcsetters,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.parsefirst and only then decrypts, so what this writes is whatosano.jsitself 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 aCookieheader 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
USwhere the location lookup has not answered; this writesAA, the user-assigned code, becauseUSnames 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:
jurisdictionandcountryCodecome from a location lookup,revision,cmpContentHashandpublishTimestampfrom the tenant's own configuration.gdprAppliesanswerstrue, the protective answer where it cannot be known.
Civic Cookie Control
||cc.cdn.civiccomputing.com/9/cookieControl-9*.js$script,redirect=civic-reject.jsThe 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.comThe :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=writtenPlenty 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-categoryanddata-src, and their script frees it by copyingdata-srcintosrcwhen that category is accepted. None is, so parked tags stay parked. TheirdeleteAll- which removes every cookie outside the consented set on each load - answersfalsehere: 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
onAcceptonly for accepted categories, andonRevokeonly when somebody changes one. Nothing is accepted and nobody changed anything, so neither fires.onLoaddoes, a second later, as theirs does. - The decision cannot be changed from the page.
changeCategory,toggleCategory,acceptAllandrejectAllanswer 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. tcfPolicyVersionis 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.geoisnullandgeoInfo()answersfalse. - 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.jsuc.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
necessaryruns; everything else stays parked. Their own check tests a tag's categories againstpreferences,statisticsandmarketingonly, so a tag naming none of those is freed - by this as by them.script[type="text/plain"][data-cookieconsent]and thedata-src/data-cookieblock-srcforms oniframe,img,embed,video,audio,pictureandsourceare all handled,ignoreis left alone, and thecookieconsent-optin-…classes go on either way. - No IAB TCF layer. Where a site sets
data-frameworkto one of the IAB values,uc.jsinstalls 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=IABrather thaniab=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,withdrawandsubmitCustomConsentanswer 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.jsThe 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>.jsLeave 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.
__isTcfEnabledForLocationisfalseandgetUserLocationAndLanguage()answersnull; 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.jsand 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.jsReplace 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
