Skip to content

Add the InMobi Choice consent prompt as a module - #1359

Open
jwrosewell wants to merge 50 commits into
feature/17-cookie-constantfrom
feature/18-inmobi
Open

jwrosewell wants to merge 50 commits into
feature/17-cookie-constantfrom
feature/18-inmobi

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/cmp/inmobi 10 files, 1458 lines added, 0 removed
docs/guide 4 files, 144 lines added, 3 removed
crates/trusted-server-core/src 3 files, 107 lines added, 0 removed
crates/cmp/sourcepoint 1 file, 71 lines added, 6 removed
crates/auction/prebid 1 file, 69 lines added, 5 removed
crates/cmp/didomi 1 file, 64 lines added, 4 removed
crates/ad-tag/google 1 file, 54 lines added, 7 removed
crates/tag/google-tag-manager 1 file, 41 lines added, 1 removed
crates/bot-protection/datadome 1 file, 39 lines added, 1 removed
crates/testing/seam-probe 1 file, 39 lines added, 3 removed
crates/audience/permutive 1 file, 38 lines added, 1 removed
crates/identity/lockr 1 file, 38 lines added, 1 removed
Cargo.lock 1 file, 13 lines added, 0 removed
trusted-server.example.toml 1 file, 9 lines added, 0 removed
.cargo/config.toml 1 file, 6 lines added, 6 removed
.github/workflows 1 file, 6 lines added, 0 removed
crates/trusted-server-modules/src 1 file, 5 lines added, 0 removed
CHANGELOG.md 1 file, 2 lines added, 0 removed
Cargo.toml 1 file, 2 lines added, 0 removed
AGENTS.md 1 file, 1 line added, 1 removed
crates/trusted-server-modules/Cargo.toml 1 file, 1 line added, 0 removed
docs/.vitepress 1 file, 1 line added, 0 removed

Closes

Closes #1276

Spec

Spec: inmobi-choice-module

Test plan

CI on this pull request at 5aeb169 passed all 20 checks.

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: the full workflow set of this repository, run on this pull request at 5aeb169

Checklist

  • Feature PRs: spec linked above, ending in a review log of two or more review rounds
  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

Stacks on the Model Terms cookie name (#1358).

Kept current. This update only merges the pull request below (#1358), which brings Tech Lab's main at 7aa5c99 and the changes made lower in the stack.

What this pull request does

crates/cmp/inmobi, a consent management platform module for InMobi Choice, the TCF consent prompt a publisher loads from their own InMobi account.

[cmp]
module = "inmobi"

[cmp.inmobi]
script_url = "https://cmp.inmobi.com/choice/<account>/<site>/choice.js?tag_version=V3"
# cmp_id = 10

[[fetch]]
media_type = "text/html"
middleware = ["cmp.inmobi", "tag.google-tag-manager"]

Where a [[fetch]] entry for text/html names cmp.inmobi, the module's middleware writes three things into the head, in this order, and the stored page every reader is served from carries them.

  1. The IAB TCF v2 stub, which creates the __tcfapiLocator frame, queues every __tcfapi call made before the prompt arrives, answers ping as a stub, and relays calls posted from frames.
  2. InMobi's GPP stub, which answers ping and the other generic __gpp commands at once with the CMP id, queues every other call for the prompt, creates the __gppLocator frame, and relays calls posted from frames.
  3. The account's Choice loader, deferred, so a slow consent vendor never holds up the page and the loader never runs ahead of a stub the publisher's own head carries.

Each stub defines nothing where the page already has its interface, and the TCF stub defines nothing where a window above the page, up to the top, already carries a __tcfapiLocator. The module serves no browser module, because the stubs and the Choice script are the whole client side. The prompt writes the reader's answer to the euconsent-v2 cookie, which core reads on the next request, and the TCF permission signal module reads the record core decodes from it, the way every signal reaches the permission model.

The stubs land ahead of the page's own <meta charset>, so the guide page says to send the encoding in Content-Type as well.

What is refused

  • A script_url that is empty, is not a URL, is not https, is on a host other than cmp.inmobi.com or cdn.inmobi.com, or carries a user name or password. The value becomes a <script src> on every page, and a settings document reaches the service from a config store rather than from a person reading it. A host that merely contains an allowed name is refused by what it is rather than by what it looks like.
  • A document that selects the prompt and names it in no [[fetch]] entry for text/html, because the prompt would never be written.
  • A field in [cmp.inmobi] the module does not read.

What a consent prompt module keeps to

The guide page states six things this module keeps to, and they are what every [cmp] module will be held to: the prompt is written before any vendor's tag, the IAB interfaces are stubbed ahead of the loader, the loader is deferred, the reader's answer reaches the permission model through the permission signal modules and no other path, a script address written into every page is checked at load, and a selected prompt that no entry places is refused.

Maintainers

The crate's manifest names the Trusted Server maintainers as its owner, with the status seeking vendor owner, until InMobi adopt it, as Didomi and Sourcepoint do. With it the stock build has 1 crate vendor owned, 19 seeking vendor owner and 4 project owned.

CI

The two stubs are tested in a stand-in for the browser with Node's test runner and no packages, as tests/*.test.mjs beside the module. The vitest job now runs every module's such tests after the browser bundle's.

How it was verified

Not exercised. No page has been served to a real browser with the prompt loading from InMobi. The stubs run in a stand-in for the browser, and the tag's parts and order are checked as written.

References #1276.

jwrosewell and others added 11 commits October 10, 2026 02:10
`[cmp] module = "inmobi"` selects it, with the account's Choice loader in
`[cmp.inmobi] script_url` and the IAB CMP id in `cmp_id`, which is 10 unless
written. Where a `[[fetch]]` entry names `cmp.inmobi`, the module's
middleware writes three things into the head, in this order: the
IAB TCF v2 stub, which queues every `__tcfapi` call made before the prompt
arrives and answers `ping` as a stub; InMobi's GPP stub, which answers the
generic `__gpp` commands at once with the CMP id and queues the rest; and
the Choice loader, deferred, so a slow consent vendor never holds up the
page and the loader never runs ahead of a stub the publisher's own head
carries. Each stub defines nothing where the page already has its
interface. The module serves no browser module, because the stubs and the
Choice script are the whole client side. The prompt writes the reader's
answer to the cookie the TCF permission signal reads, so the answer reaches
the permission model on the next request the way every signal does.

The script URL becomes a `<script src>` on every page, and a settings
document reaches the service from a config store rather than from a person
reading it, so it is refused when it is empty, is not a URL, is not `https`,
is on a host other than `cmp.inmobi.com` or `cdn.inmobi.com`, or carries a
user name or password. A host that merely contains an allowed name is
refused by what it is rather than by what it looks like, and the URL is
escaped again when it is written, because a URL on the right host can still
carry a quote in its query. A document that selects the prompt and names it
in no entry is refused, because the prompt would never be written, and the
table refuses a field it does not read.

The two stubs are tested in a stand-in for the browser with Node's test
runner, `node --test "crates/cmp/inmobi/tests/*.test.mjs"`, which runs the
scripts as the page carries them. The module's tests cover each refusal,
the tag's parts and their order, that a vendor's tag named after the prompt
is written after it, and the page a reader receives, recorded in
`src/fixtures/page-change.recorded.html`.

The stock list offers the module after Didomi and lists its middleware in
the fetch phase, the example configuration names it in its fetch entry and
shows its table, and the cargo aliases build and test it with the other
modules. 51Degrees maintain the crate until InMobi adopt it, which its
manifest states.
A module that writes a script into pages keeps that script's tests beside
it, as `tests/*.test.mjs`, run with Node's test runner and no packages. The
vitest job runs every such file after the browser bundle's tests, from the
repository root, so a stub that changes is tested the way a page runs it.
The InMobi Choice module's two stubs are the first.
A guide page for the module, with its settings, where to place it, what is
checked, and what a consent prompt module keeps to, in the sidebar beside
the other consent management platforms. The configuration guide's section
table, its list of the modules this repository ships, its placing table and
its integration sections carry the module, the integrations overview lists
it, the crate list in AGENTS.md names it, and the changelog records it.
The no-entry check reads the media type of each fetch entry, so an entry for another media type naming the prompt is refused. The TCF stub walks from its window to the top and defines nothing where a __tcfapiLocator already exists, as the IAB reference stub does. The crate is held by the Trusted Server maintainers and marked development, validate and Choice::new are crate private, and the guide says the page should send its charset in Content-Type because the stubs push the meta tag past the prescan window.
@@ -0,0 +1,29 @@
(function(cmpId){
@jwrosewell
jwrosewell removed this pull request from stack #1401 October 15, 2026 20:51
The check that a selected prompt is placed passed when any [[fetch]]
entry for text/html named cmp.inmobi. A response takes the first entry
that covers it, so a prompt entry limited by path, with no entry for the
rest of the site, served every other page with no prompt, and so did an
earlier entry for part of the site that names other middleware.

The check now walks the entries in order. Every entry up to the first
that covers every path has to name the prompt, and such an entry has to
exist, or the document is refused when the settings load and when the
registry is built, with a message naming the entry and the pages left
without the prompt.

Tests: a prompt entry limited to /news/ and an earlier /news/ entry
without the prompt are each refused naming the pages, and entries that
put the prompt on every page, one entry for / among them, are accepted.
cmp_id was read as a u16, so 0 and every value up to 65535 were
accepted, and a value of the wrong type was refused with a parse error
that named the table but not the setting.

A TC string holds the CMP id in 12 bits, so 4095 is the largest, and the
GPP CMP API gives 0 as what a stub reports before it has an id. cmp_id
is now read as a whole number from 1 to 4095, and any other value is
refused when the settings load with a message that names cmp_id and the
range.

Test: 0, 4096, 70000, -1, a string and a fraction are each refused by
the load validation, by validate and by register, naming the setting,
and 1 and 4095 are accepted.
… it again

The fingerprint that keys a stored page covers the settings, the browser
modules the registry serves and core's build, and not markup that a
module crate compiles in. A change to the InMobi stubs alone left the
fingerprint as it was, so a page stored before the change kept the old
stubs until it expired.

Middleware gains template_key, the text a stored page is keyed by,
being what the middleware writes that no setting decides. It is empty
by default. The fingerprint hashes each registered middleware's name and
template_key, and the InMobi prompt gives its whole tag, so a change to
either stub selects a new stored page.

Tests: in core, a middleware whose compiled-in markup changes selects a
different fingerprint and the same markup selects the same one, and in
the InMobi crate the registered middleware's template_key holds both
stubs.
…stored page

The InMobi guide, its section of the configuration guide and the
changelog entry say that entries leaving any page without the prompt
are refused, naming the pages, and that cmp_id is a whole number from 1
to 4095. The configuration guide's list of what keys a stored page, and
the integration guide's section on middleware, name template_key.
The test's settings name the template's placeholder origin, which the
load validation refuses before any module's own check, so its load leg
failed on the origin rather than on cmp_id. A real origin stands in.
The Google ad tag writes the `tsjs.adInit` bootstrap into the head in the
fetch phase, from `gpt_bootstrap.js` compiled into its crate, and points
the GPT script's address at `/integrations/gpt/script`. Neither is in
core's build digest or the settings, so a change to either left the
template fingerprint as it was, and a page stored before the change kept
the old bootstrap until it expired.

The page change's `template_key` is now the head scripts as written, then
the script path, built once when the module registers.

The test `the_bootstrap_is_in_what_keys_a_stored_page` builds the registry
from the recorded page's settings and finds the bootstrap and the script
path in the key of the registered middleware.
From this branch a refused module table names the setting at fault and
never its value, so loading settings with an out of range cmp_id says that
cmp_id in [cmp.inmobi] holds a value of the wrong type or form, and no
longer the range message the module's reader gives, which quotes the
value. The InMobi test of the CMP id range checks each of its six values
for the table and the setting, and that the value is not quoted.
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
@jwrosewell
jwrosewell added this pull request to stack #1418 October 19, 2026 12:18
@jwrosewell
jwrosewell removed this pull request from stack #1418 October 19, 2026 16:30
@jwrosewell
jwrosewell added this pull request to stack #1421 October 19, 2026 17:38
@jwrosewell
jwrosewell removed this pull request from stack #1421 October 19, 2026 18:29
@jwrosewell
jwrosewell added this pull request to stack #1422 October 19, 2026 19:20
@jwrosewell
jwrosewell marked this pull request as ready for review October 19, 2026 20:23
@jwrosewell
jwrosewell removed this pull request from stack #1422 October 28, 2026 11:47

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A publisher using InMobi Choice has no consent prompt module, and what a consent prompt module keeps to is written nowhere

2 participants