Repository navigation
Releases: ryanbr/consent-rr
Release list
v1.65.0
Sourcepoint
A new family: sourcepoint-reject.js, standing in for
cdn.privacy-mgmt.com/unified/wrapperMessagingWithoutDetection.js.
One rule takes the whole CMP. That file is a webpack bundle whose campaign
modules are chunks it fetches itself, off its own publicPath - ccpa, ccpa-gpp,
custom, gdpr, gdpr-tcf, globalcmp, hbbtv, preferences, usnat, usnat-uspapi.
Nothing else ever asks for them, so replacing it takes the message iframe, the
privacy manager and the consent call with it, and there is no banner left to
hide: their banner is one of those iframes.
/wrapperMessagingWithoutDetection.js$script,important,redirect=sourcepoint-reject.js
important is earned here. __tcfapi is not in the wrapper at all - the
string does not appear in it - it is in the gdpr-tcf chunk. So blocking the
file leaves every vendor waiting on a CMP that is never coming, which is why
the big lists carry a pile of @@||privacy-mgmt.com^$script exceptions for the
sites that wall off behind it. Those are there because the page needs an
answer, and an answer is what this gives it. A site-wide allowlist of your own
still wins.
What the page sees is the state their own script reaches on a property with
nothing to show, measured in build 4.40.3 and in each campaign chunk: the
_sp_ surface, their _sp_queue hand-off, the page's config.events fired in
their order, and __tcfapi refusing as cmpId 6, cmpVersion 1, service
specific, policy version 5, apiVersion the string "2", granting nothing in
any set the string carries. Their five custom commands answer too.
Two things worth knowing if you read their docs rather than their dispatcher:
their callbacks are called (campaign, ...args), campaign first; and their
campaigns do not answer alike - gdpr sends {addtlConsent, applies, consentedToAll}, ccpa, usnat and globalcmp send {applies, gpcEnabled}, and
custom and preferences fire onMessageReceiveData and nothing else.
The refusal is euconsent-v2 and nothing else of theirs: their own script
writes no consent cookie directly - the campaign response carries a cookies
array their setCookiesAction applies - so consentUUID and gdprApplies are
their consent service's to mint. A record a visitor accepted before this was
installed is cleared, localStorage and consentUUID both.
No accept and no unblock resource: nothing is parked for this CMP to free, so
blocking is what keeps a tracker out. No __gpp or __uspapi - their US
refusal payloads are minted server side - and wrapperMessagingWithDetection.js
is left alone.
Filters: filters/sourcepoint.txt. README has the
measured detail and the deliberate gaps.
35 tests for the family, 1099 in the suite, 22 mutations run against the new
tests and all 22 killed.
v1.64.0
transcend-reject 1.5.2 - claim prompted only where something prompted
prompted is Transcend's field for "this user has been shown a prompt", and
the record written where there is no engine was asserting it when no banner had
existed to show one - which told their sync endpoint a prompt had been answered
that was never shown. Their banner sets it itself, on render:
!hs(t) && !e.getConsent().prompted && e.setPrompted(!0)and ui.js carries { confirmed: true, prompted: true } as Xe, the object it
passes with a decision. So the field now splits by whether a banner moment
happened at all: true where their engine readied and this replaced the banner
at the moment it would have rendered, left out where nothing readied and no
banner was ever built. Their schema has it optional and airgap coerces it with
!!, so absent reads as false. It gates nothing either way - the prompt turns
on confirmed.
Found by comparing the record this writes against a real refusal off a live
tenant, the first of six genuine samples to disagree. It is not a
disagreement: the other five came through a banner and that one did not.
Documentation
Their TCF banner. A tenant with the IAB layer on serves a second banner at
/cm/<id>/tcf/ui.js, and because * spans a path separator the shipped rules
match it too. That is now written down as deliberate, with what it trades: that
file owns the TCF record, declaring tcmTcf and tcmTcfMP itself, so
replacing it leaves a TCF vendor with no __tcfapi and no stored string -
no consent signal rather than a fabricated one. The url-matching test carries
the path.
The two rules that take the engine. /*/airgap.js and /cm/ both replace
the half that enforces the refusal and fetches the banner, so the rule somebody
wanted cannot fire at all. The cost of each is now measured on one tenant
across two loads rather than argued.
tcmQuarantine is not a failure signal. It is the engine's own store for
requests and cookies it intercepted, and it stays empty unless the tenant sets
quarantineSyncBudget. The tenant's matcher table is what answers whether a
refusal is being enforced.
Only transcend-reject.js changes. Every other resource is byte-identical to
v1.63.0.
v1.63.0
transcend-reject 1.5.1 - refuse where a page-built airgap hid a replaced engine
Transcend's stand-in refusal - the purpose list written where their engine is
gone, so a filter author gets a record and a log line instead of silence -
never fired on a page that builds self.airgap itself. Routledge does, in two
inline scripts above their tag, the second their documented "Advanced Consent
Override" block:
self.airgap?.ready || (self.airgap = { readyQueue: [],
ready(e) { this.readyQueue.push(e) }, ...self.airgap });
self.airgap = { overrides: [], cookieOverrides: [], ...self.airgap };That object outlives their file never arriving, and withoutTheirs() tested
for the object rather than for their engine - so it came back false, the
refusal was skipped, and the resource reported refused=(none) via=no engine:
no engine and theirs-is-present in the same breath, with nothing written to
either store.
The object alone cannot separate the two cases, because their own prelude
dying mid-boot leaves the same shape and inventing a purpose list there would
be guessing at a tenant whose engine may still arrive. So the cause is read
off the install: served in place of a file, their loader asked for that url
and got this, so their file is gone; injected as a scriptlet it may not be.
Their markers are what proves theirs is really there - purposes from their
prelude, getConsent/setConsent from the booted engine - and a page's own
prelude carries none of the three.
Only transcend-reject.js changes. Every other resource is byte-identical to
v1.62.0.
v1.62.0 - usercentrics 1.9.0
Their API, answered in the shape their API answers.
getConsentDetails, getServicesBaseInfo and getServicesFullInfo stood in with
empty objects, so a page indexing into one - details.services[id].consent.given
-
got a TypeError rather than a no. Read off a live v3 tenant:
getConsentDetails { consent, services, categories }, where consent is the
record their ucString carries, a service is
{ name, consent: { given, type }, essential, subservices }
and a category is { dps, state: ALL_DENIED | ALL_ACCEPTED }
getServicesBaseInfo [ { id, name, consent: { status, history }, isEssential,
isHidden, subServices, subServicesLength } ]
All three now answer from the record. Fields that are theirs to issue are left
out, the way consentHash is - version, processorId, description and
legalBasis come from the configuration this never fetches - and there is a test
asserting their absence, so nobody completes the shape by inventing them.
Their own getServicesBaseInfo resolves a promise; UC_UI's stays synchronous
here, because their blocker checks Array.isArray on it and that is the form the
field confirms. It carries the record's entries now rather than none.
Also in this one, from the same review:
getControllerIdanswered the id a returning visitor carries through
__ucCmpand an empty string throughUC_UI- the same visitor, twice.areAllConsentsAcceptedwas a fixed no, contradicting the granting
resource's own record and keeping a page gated on it shut.areAllRequiredConsentsAcceptedwas no on both paths. Their required consents
are the essential services, which survive their own deny, so a refusal leaves
these accepted; answering no re-prompts a visitor who has answered.- Their categories are carried over from a record that names them: all
consented where this grants, all refused except the essential one where it
refuses. - The console line said
revoked=41andgcm=deniedon the granting resource.
v1.61.0 - usercentrics 1.8.0
Their categories carried over, and a UC_UI that agrees with the record.
Categories. Their consentStatus carries categories whose names are the
tenant's own, so both resources left the field out rather than invent it. A
returning visitor's record already names them, which is reading rather than
inventing: all consented where this grants, all refused where it refuses except
the essential one, which is what their own deny keeps. A first visit still has
none to carry and the field stays absent. It matters most on the granting side,
where a page asking whether the marketing category is consented was getting
undefined from a resource whose job is to look consented.
Three answers that disagreed with the record beside them:
getControllerIdgave the id a returning visitor carries through__ucCmp
and an empty string throughUC_UI- the same visitor, answered twice.areAllConsentsAcceptedwas a fixed no, so the granting resource said not
everything was accepted while its own record said it was, and a page gating on
it stayed shut.areAllRequiredConsentsAcceptedwas no on both paths. Their required consents
are the essential services, which survive their own deny, so a refusal leaves
these accepted - answering no re-prompts a visitor who has already answered.
Also: the accept resource's header now says what a first visit does grant. It
consents to no service, which was the part that was true, but it still grants
their Google consent mode with redaction off.
v1.60.0 - usercentrics 1.7.0
A granting Usercentrics resource, beside the refusing one.
usercentrics-accept.js is the same record with the answer the other way round:
every service the record already names is consented, updatedBy is their
onAcceptAllServices, the name-keyed consentStatus.services map is all true,
and their Google consent mode is granted with adsDataRedaction: false. The v2
uc_settings statuses agree.
What it cannot do is in its header, because it matters more than what it can:
- A first visit grants nothing. Their service ids come from the settings
payload this resource exists to avoid fetching, and their blocker treats an id
it was never told about as blocked. So this is for a wall a returning visitor
finds shut, not a default - there is a test pinning that case, because it is
the one most easily mistaken for a bug. - It grants their services, not IAB consent. On a TCF tenant the TC string
still refuses: consenting there means naming the purposes, special features
and vendors in the tenant's own configuration. - Global Privacy Control is untouched. A browser signal stands on its own.
consentHashis not forged. It differs per visitor for the same consent
state, so it is absent rather than invented.
Point it at a site by name. filters/usercentrics.txt carries a commented
example with a domain=, so it cannot be enabled everywhere by accident.
v1.59.0 - usercentrics 1.6.0
Keep what their own deny-all keeps.
Their essential services survive their own Deny. A captured deny-all on a v3
tenant left six consented - a payment CDN, Cloudflare, Cognigy, Dash0, Google
Fonts and the CMP itself - while this resource refused every service it found,
which refuses more than the visitor did and takes delivery and checkout with it.
Which services a tenant calls essential lives in the configuration this never
fetches, so it is read where it exists and guessed narrowly where it is not:
- a record their CMP wrote for a deny-all (
updatedBy: onDenyAllServices)
names them exactly: anything still consented in it survived a refusal. - with only an accept-all to go on, the fallback is their own CMP by name
prefix, Google Fonts, Cloudflare, and anything named a CDN. reCAPTCHA is
deliberately absent, because this tenant marks it non-essential - a reminder
that a name guess overrides their classification rather than reading it.
Applied to all three records this writes: ucData.consent.services, the
name-keyed gcmStatus.consentStatus.services, and the v2 uc_settings list.
Also: getControllerId answers with the id a returning visitor already carries
instead of an empty string.
Following v1.58.0, which aligned the record with their gcmStatus shape.
v1.58.0 - usercentrics 1.5.0
Usercentrics' own record, matched.
A manual deny-all on a v3 tenant writes:
consent.services keyed by service id
gcmStatus.consentStatus the same services keyed by NAME, with
type EXPLICIT and updatedBy onDenyAllServices
gcmStatus.gcmUpdate the Google consent state
gcmStatus.dataLayerNames [ "dataLayer" ]
ui.language
This resource wrote a flat gcm key instead of that nesting, so a page
reading ucData.gcmStatus.gcmUpdate.adStorage found nothing. It now writes
the shape above, confirmed against captures of both a manual deny and a manual
accept on the same tenant.
Four fields are deliberately absent rather than invented. consentHash varies
per visitor for the same consent state - three captures of the same deny gave
three different hashes - so it is not derivable from outside the CMP.
settingsVersion, reshowAfterDays and categories come from a configuration
this resource never fetches, and categories names are the tenant's own.
Unchanged: what the blocker reads to decide. consent.services and
ui.language were already right, which is why this was invisible.
v1.57.0 - inmobi 1.2.0: a granting resource, and GPC in the USP string
inmobi-accept.js stands in for InMobi Choice's cmp2.js granting rather than refusing - for a wall that keeps the page shut until the answer is yes.
It consents to the purposes, special features and vendors the tenant's own configuration names: vendorPurposeIds, vendorSpecialFeaturesIds and publisherPurposeIds, falling back to all of them where a tenant names none. getTCData answers with what the string carries rather than holding a separate opinion, and so does the tcfeuv2 section __gpp reports - the tests decode our own string with @iabgpp/cmpapi and compare the two, because a section that disagrees with the string beside it is the failure this nearly shipped with.
One boolean through the wrapper, the way appconsent does it, so the two resources are the same file but for the header and that argument - which a test pins, so they cannot drift apart.
It hands consent to IAB vendors, which the refusing resource does not. That is what makes it a per-site tool rather than a default, and filters/inmobi.txt says so where it shows the rule:
```
! ||cmp.inmobi.com/tcfv2/cmp2.js$script,redirect=inmobi-accept.js:10,domain=example.com
! ||cmp.inmobi.com/tcfv2/cmp2-polyfilled.js$script,redirect=inmobi-accept.js:10,domain=example.com
```
Global Privacy Control now reaches __uspapi: 1--- normally, 1-Y- where the browser sends one. The notice and LSPA characters stay dashes, because whether CCPA applies comes from the location lookup these filters noop - but the middle character is not a claim about jurisdiction, it is the visitor's own signal, and osano-usp.js already read it exactly this way.
This mattered more than it looked. Brave sends GPC by default, so a Brave visitor was being told no opt-out applied by the same resource that was already treating that signal as strong enough to withdraw legitimate interest from the TC string. One signal, two contradictory answers, visible only in the browser where it is the default.
Granting does not overrule it either: the purposes the wall waits on are consented to while the objection and the opt-out stand, because they answer different questions. Both the accept resource's own notes and a test say so, so neither gets "fixed" into the other later.
Also here: both layers now read that signal the same guarded way. The core wrapped the property access in a try/catch and the TCF builder did not, so a throwing getter would have taken one layer down and left the other standing - and the two could have disagreed about one signal.
1044 tests. Mutation-tested: fifteen mutations across the granting path, the USP mapping and the GPP section, all caught.
v1.56.1 - inmobi 1.1.1: the usnat section comes back out
A correction to v1.56.0. inmobi-reject 1.1.0 wrote a GPP usnat section for tenants configured with MSPA. Measured against their own reject, from a New Zealand address, on the very tenant that prompted it - mspaJurisdiction: "STATE_AND_NATIONAL" - their IABGPP_HDR_GppString came back DBABMA: section 2 alone. 1.1.0 wrote DBACMMA with a US national section for that visitor.
The premise was wrong. coreConfig.mspaJurisdiction says which jurisdictions the tenant runs MSPA in, not where the visitor is. Their cmp2.js decides that from ||cmp.inmobi.com/geoip, which these filters noop, so a replaced CMP cannot learn it. The note 1.1.0 removed - that the US sections need a jurisdiction the page cannot tell us - was correct, and what replaced it read a tenant setting as a visitor fact. A usnat section written for a visitor outside those jurisdictions is a claim about a jurisdiction they are not in, which is worse than the silence it replaced.
The section is gone and the string is DBABMA~<tc string> again, as in 1.0.1.
The reason it went is now in the source, and so is the encoding: it was read off their own encodeUSString and checked byte for byte against @iabgpp/cmpapi, so anyone who finds a way to establish the visitor's jurisdiction does not have to derive it a second time - section 7, header DBACMMA alongside section 2, Version 2, six notice fields, three opt-out fields, 16 sensitive-data entries and 3 child ones, the 0/2/1 field convention, MspaCoveredTransaction from isCoveredTransaction, MspaOptOutOptionMode from mspaSignalMode, a flat 2 for MspaServiceProviderMode, a GPC segment with no bit of its own for being included, and a bit string padded to a whole byte before it is taken six bits at a time.
What v1.56.0 did that stands: the vendor list version still comes out of a returning visitor's own euconsent-v2 rather than a pin, with the pin behind it at 180. And transcend-reject 1.5.0 is unaffected - it still writes a refusal where their object is absent, which is every $redirect= install.
1036 tests.