Skip to content

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

Closed
Jamesr51d wants to merge 525 commits into
IABTechLab:mainfrom
jwrosewell:split/18-inmobi
Closed

Jamesr51d wants to merge 525 commits into
IABTechLab:mainfrom
jwrosewell:split/18-inmobi

Conversation

@Jamesr51d

Copy link
Copy Markdown

Stacks on the Model Terms cookie name (#1304). The additions that follow the seven pull requests open here are listed in #777.
Compare split/17-cookie-constant with split/18-inmobi
to see only this pull request's change.

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

CI passed on 58312af3a on the fork. 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.

…seam-impl

Brings main at 7a0ecb4 and the refreshed chain below this branch into the
seam implementation. The seam made the Edge Cookie, device and geo traits
async with a runtime services argument, so the Edge Cookie files the chain
refreshed in their synchronous form (the resolve endpoint, the context, the
seam trait file, finalization, the edge cookie helpers, pull sync, batch
sync and the admin lookup) are taken as the chain has them and put back in
the seam's shape, with the seam's own tests that read the config store
through the services restored.

Main's changes that the seam had moved out of core are carried into the
seam's implementations: a Prebid Server stored request is sent only when
the slot's intent allows the fallback, an implementation marks an
impression it has no demand for as omitted, the driver leaves those
impressions out and makes no request when none is left, and the parse
state keeps only the slots that were sent. Main's split between the
reader's and the request's origin requirement is carried into the seam's
publisher, which keys personalization rather than one vendor's tag. The
build script keeps both jobs, the migration guard's generated source list
and the manifest's default config store id.

The names the lower branches gave their identifiers are applied to the
seam's lines, so the seam compiles against the refreshed chain. The seam's
own names keep their spelling for its rename commit. Main's new tests that
name demand sources with hyphens take the snake_case names the seam
requires, and those written against main's plan shape are rewritten onto
the seam's demand table.
The lower branches settled the word: what an operator selects in `[ec]`,
`[geo]`, `[device]`, `[permission_signal]` and `[integration]` is a module,
and `provider` keeps its one meaning in the auction, where `[demand]` and
`[adserver]` select the demand sources and the ad server. This applies that
to the seam's own lines, which were written before the word was settled.

The `[integration]` selector is now `module`, in the settings, the example
template, the starter configuration the CLI writes, the audit draft and
every fixture. The registry's declared capabilities follow: a module
declares a `geo_module`, an `ec_module` or a `device_module`, the registry
resolves `resolve_geo_module`, `resolve_ec_module` and
`resolve_device_module`, and the reserved selector values are
`GEO_MODULE_NONE`, `GEO_MODULE_PLATFORM`, `DEVICE_MODULE_BUILTIN` and
`DEVICE_MODULE_FASTLY`. Startup refusals name the `module` key they are
about, so an operator reading one is told the key to edit.

The generic provider table, the auction plan, the demand and ad server
implementations and the CLI's ad template lint keep `provider`, and the
configuration rules, the configuration guide, the example template, the
CHANGELOG and the agent notes state the convention with both spellings
rather than one word for everything.
The pages that still showed `[auction.providers.<id>]`, `mediator` and
`[integrations.<id>]`, which this branch refuses at startup, now show the
tables it reads. The error reference, the ad serving overview, the auction
testing guide, TESTING.md and the CLI's README carry `[demand.<name>]`
examples with the implementation named, `[adserver] provider` in place of
the mediator, and `[integration] module` as the switch. The API reference's
registration predicates say `named`, meaning the id is in
`[integration] module`, rather than an `enabled` key that no longer exists.

The configuration reference loses its `[integration.adserver_mock]` and
`[integration.aps]` sections, because the ad server mock is configured under
`[adserver.adserver_mock]`, documented in the ad server section, and APS has
no browser table of its own any more. The ad server mock guide is rewritten
on the same terms, with the word mediator gone.
The command still parsed `[auction.providers.<id>]` with a `profile` and
`[integrations.prebid]` with an `enabled` key, so on a configuration written
for this branch it found no Prebid Server demand at all and said so without
error. It now reads the `[demand.<name>]` tables, taking those whose
implementation is `prebid_server`, whether by an `implementation` line or
by the table's name, with `debug`, `test_mode` and the bid parameter
override rules flat in the table where they now live, and it reads the
browser settings from `[integration.prebid]`.

The report says what the configuration selects rather than echoing a flag
that is gone. Each demand source carries `selected`, which is whether
`[demand] provider` names it, and the report's own `selected` is whether
`[integration] module` names `prebid`, in place of `enabled_explicit`. The
`source_sections` field names `demand` and `integration.prebid`. The CLI
crate does not build on the machine this was written on, so the readers
were compiled and run against the branch's core crate in a scratch crate
with the same fixture, and the CLI's own tests run in CI.
The merge of the permission model into this branch settled the inspector's
`Cargo.lock` through the same rename that turned provider into module in
the code, and the crate `icu_provider` became `icu_module`, a package that
does not exist. The inspector still built, because its build script runs
cargo without `--locked` and cargo re-resolved the crate from the registry
each time, so the fault was invisible to CI and visible to anyone reading
the file. The three lines name the crate again, and `cargo check --locked`
accepts the lockfile as written.
Brings the inspector lockfile fix, which names the icu_provider crate again.
…seam-impl

Brings the inspector lockfile fix, which names the icu_provider crate again.
The template cache harness builds its configuration from the example
template and switched Prebid on by rewriting the template's selector line.
The rename left it writing `provider = ["prebid"]`, which this branch reads
as a settings table for an integration called `provider`, so `ts config
validate` refused the file in CI's three harness runs with "[integration.prebid]
and [integration.provider] is configured but not named in [integration]
module". The harness now writes `module = ["prebid"]`. The generated
configuration, in both the esi and inline modes, loads and passes deploy
validation through the core crate's own readers.
A module's name is the path of its crate below `crates/`, with `.` between
the parts, taken from `CARGO_MANIFEST_DIR` when the crate is built, so the
name cannot drift from the folder and no crate carries a hand-written id.
`module_name!()` gives a crate its own name, `module_name::resolve` finds
the name an operator wrote among the modules offered, as written or with
the section's type folder in front, and `module_name::short_form` gives the
name back without that folder. Core's own modules keep bare names.

The Edge Cookie side is the first to use it. The type folder of every Edge
Cookie crate is `edgecookie`, so `[ec] module` may name an injected module
in full or with that folder left off, and an `implementation` line may do
the same. The rule for a name written in `[ec]`, its blocks and their
`implementation` lines is the module name rule, parts joined by `.`, each
of lower case letters, digits, `_` or `-`, in place of snake_case.
Brings the naming module, so a module is named by its crate folder, and the
Edge Cookie side resolving a written name within its type folder. The
host-signal module's key and the type folder sit side by side.
Brings the naming module from the Edge Cookie seam, so a module is named by
its crate folder.
Each of the five modules takes its name from its crate folder through
`module_name!()`, so `crates/permission-signal/gpp` is `gpp` and
`crates/permission-signal/us-privacy` is `us-privacy`, where the two had
called themselves `gpp_sale_opt_out` and `us_privacy` by hand. The page is
told a signal's source by the short name, without the type folder.

The section is `[permission-signal]`, named exactly as its folder, and its
key is `modules`, a list, because signals compose where `[ec]`, `[geo]` and
`[device]` each name one module. A name in the list may be written in full,
as `permission-signal.gpp`, or with the type folder left off, and the
refusal of a name this build does not offer lists the short names. A
configuration still carrying `[permission_signal]`, a `module` key or the
retired `sources` key is refused with directions to the new spelling.
Brings the naming module and the permission signal modules named by their
crate folders, with [permission-signal] modules. The Edge Cookie module
file keeps the client_fixed key beside the type folder, and the agent
guide's capability table keeps this branch's rows with the permission
signal selector as [permission-signal] modules.
Brings the naming module, the permission signal modules named by their
crate folders and [permission-signal] modules, with their guides.
A name written in `[ec] module`, an `[ec.<name>]` block or an
`implementation` line is parts joined by `.`, each of lower case letters,
digits, `_` or `-`, in place of snake_case, and a module from a crate may be
written in full, as `edgecookie.<name>`, or with that type folder left off.
The sections table names `[permission-signal]`.
…seam-impl

Brings the naming module, so a module is named by its crate folder, the
permission signal modules named that way with [permission-signal] modules,
and the guides stating the module name rule. The adapters and the template
take the chain's permission signal wording, the settings keep both the
removed-integrations refusal and the chain's refused_table! machinery, and
the agent guide and the configuration guide keep this branch's tables with
the chain's selector and name rule. The Fastly, Cloudflare and Spin
adapters keep the chain's name for the resolved Edge Cookie module held in
their application state, ec_module, at every use.
`[demand]` selects its sources with `modules`, a list, `[adserver]` is
`[ad-server]`, the name of the type folder its implementations will live
in, and selects with `module`, and a bidder route in `[auction.bidders]`
names its demand source with `module`. The generic table takes the key from
its selection type, `modules` where several run and `module` where one
does, and refuses a `provider` key with the key to write instead. A
configuration still carrying `[adserver]` is refused with its new name.
Both refusals have tests.

With that the word provider has left the configuration altogether, so the
configuration rules, the guide, the template, the CHANGELOG and the agent
notes read as one rule again: every type selects a module, with `module`
or `modules`, and a module's name is its crate folder, with a `[demand]` or
`[ad-server]` name staying snake_case because it may be a label of the
operator's own. The implementation ids `openrtb`, `prebid_server`, `aps`
and `adserver_mock` keep their spelling here; their move to the type paths
of the folders they will live in comes with the type sections.
`[integration]` is gone. A page integration is a module named by its
crate folder, `cmp.didomi`, `tag.google-tag-manager`, `ad-tag.google`,
`ad-tag.google.diagnostics`, `bot-protection.datadome`, `identity.lockr`,
`audience.permutive`, `framework.nextjs`, `auction.prebid` and
`testing.testlight`, with `js_asset_proxy` one of core's own, and it is
selected in the section of its type, written there with the type folder
left off: `[cmp] module = "didomi"`, `[tag] modules =
["google-tag-manager"]`, `[ad-tag] modules = ["google",
"google.diagnostics"]`, `[auction] modules = ["prebid"]` and `[proxy]
modules = ["js_asset_proxy"]`. A module's settings live in the table at
its name beneath that section, so `[integration.prebid]` is
`[auction.prebid]`.

Every top-level table core does not read itself is the section of a module
type, read into `TypeSections`, and `[proxy]` and `[auction]` select modules
beside their own settings. A section that selects nothing, a table its
section does not select, a name selected in two sections and a name no
module in the deployment supplies are each refused at startup, and
`[integration]` and `[integrations]` are refused with directions. A
builder carries its module name and the section that selects it, the
registry runs a builder when a section selects its module, and deploy
validation checks the sections before anything else.

The seam probe moves to crates/testing/seam-probe and names itself from
that folder, so `[testing] modules = ["seam-probe"]` selects it. The audit
generator writes each detected module's section, the bundle and inspect
commands read `[auction.prebid]`, the environment overlay names a section
as written (`TRUSTED_SERVER__AD-TAG__GOOGLE__GAM_ATTRIBUTION_ENABLED`),
and the guides, the template, the changelog and the agent notes describe
the sections. The implementation ids `openrtb`, `prebid_server`, `aps` and
`adserver_mock` keep their spelling for now.
The native-integration note formats the detected integration's section and
name from a String, so the lookup takes a borrow and the fallback clones.
The four implementations this repository ships are named
`auction-protocol.openrtb`, `auction.prebid-server`, `auction.aps` and
`ad-server.mock`, the module paths the design gives the crates they move
to, in place of the ids `openrtb`, `prebid_server`, `aps` and
`adserver_mock`. A `[demand.<name>]` table names its implementation on an
`implementation` line by that path, because `demand` is not the type an
implementation is named under, and `[ad-server] module = "mock"` resolves
within its own type as a name does in every other section. The plan
resolves a written name with the module name resolver, and when the build
has no such implementation it lists the ones it has as the section would
write them. APS keeps `aps` as its bidder and response name on the wire.

The table's validation still named the `provider` key in its messages
after the key became `modules` and `module`. The messages name the key the
table reads, and an `implementation` line is checked as a module name.

`ts prebid server inspect` recognises a Prebid Server table by its
`implementation` line. The tests, the fixtures, the example configuration,
the guides, the changelog and the agent notes use the names, and the
migration tables gain the row for the `prebid-server` and `aps` profiles.
Selecting page integrations in the section of their type left messages,
comments, guides and the environment example pointing at settings under
the `[integration]` table, which is now refused, and left the configuration
reference documenting an `enabled` key that a module's table no longer
takes. An operator following any of them was sent to a place that does not
exist. This brings them into line with the code.

What an operator is told

- Prebid's bidder parameter validation names
  `auction.prebid.bid_param_overrides`, `bid_param_zone_overrides` and
  `bid_param_override_rules`, and the Next.js warning about cross-script
  payloads names `framework.nextjs.max_combined_payload_bytes`.
- `ts prebid client` names `auction.prebid.bundle.modules`,
  `auction.prebid.managed_user_ids` and `auction.prebid.external_bundle_url`
  in every message, and `ts prebid server inspect` reports its browser
  source section as `auction.prebid`.
- The external Prebid bundle build script and the browser console error for
  a client-side bidder with no adapter name
  `auction.prebid.bundle.modules`.

What the guides say

- The configuration reference drops the `enabled` row from the DataDome,
  Didomi, Google Tag Manager, GPT, JS Asset Proxy, lockr and Sourcepoint
  tables, says the GPT diagnostics module has nothing to set, documents
  `modules` in the `[proxy]` and `[auction]` tables, and no longer says
  that `[auction]` selects nothing. Its debug table names
  `include_adserver_response`, which is the key the code reads.
- The integration inventory replaces its Deploy ID column with what the
  settings write to select each integration, and the API reference keys its
  rows by integration ID throughout.
- The guides said that each page integration section is named by a type
  folder under `crates/`. That holds for a module with a crate of its own,
  such as the permission signals. An integration still in core carries the
  name its crate will have in a `MODULE` constant, so its section and table
  stay the same when it moves out, and the guides now say so.
- `.env.example` names each overlay by the section the setting is in, lists
  scalar settings only because an overlay cannot replace a list, and no
  longer points at a section the file does not have.
- Prose that still said mediator says ad server, and prose that still said
  provider profile says demand source or demand implementation.
- The Prebid on AWS planning reference names the tables that
  `ts prebid server inspect` reads.

Tests

- A demand or ad server implementation named in a page integration's
  section is refused like any name no module supplies, which the guides
  state and nothing pinned.
- The environment overlay test sets the bidder route's `module` leaf. It
  was still setting `provider`, which no longer exists, so the overlay it
  was written to prove did nothing.
- The secret redaction test labels the DataDome key by its path under
  `[bot-protection]`.

No behavior changes apart from the wording of the messages above.
Comments on the adapters and two test notes in core described what the code
did before it changed. Each now says what the code does and why.

- The resolved Edge Cookie module held on the Cloudflare, Fastly and Spin
  application state is resolved when the state is built, because resolving
  reads no request data.
- The three tests that a selected module the adapter cannot build fails the
  request say what a default context would do.
- The Spin state builder reads its settings from the config store, and its
  test says why the failure has to be the absence of a config store.
- The test that keeps the internal header list in step with the Edge Cookie
  response headers, and the test for an unknown key in a module block, say
  what they guard.

No code changes.
- The Cloudflare region test says why the region header has to reach the
  privacy outcome.
- The Fastly geo module's re-export and the geo selector's default are
  described as they are.
- The integration fixture's note on the geo selector no longer promises a
  setting that a later change does not add.

No code changes.
- The integration fixture carried two notes on its geo selector that
  contradicted each other. The one that stays says that Viceroy maps the
  loopback client to US/CA, so the platform geo module resolves a real place.
- That fixture, the Viceroy template, the template cache harness and the
  permission model guide no longer mention a default country setting, which
  no released version had.
- Five code comments say what the code does without saying what it did
  before.

No code changes.
Core builds the identity-graph key from the module's code and whatever
`normalize_id_for_kv` returns, so the method decides what two visits must
have in common to be treated as the same visitor. Its documentation
described normalization only and advised a module with a case-sensitive
identifier to return the value unchanged. That is right for a stable
identifier and wrong for one that carries a signature, a nonce or a
timestamp, because each reissue then gets a row of its own and the identity
does not survive it.

The trait documentation now says that the returned value is the identity two
visits must share, and that a module whose identifier has a part reissued
each time must return the stable part. It also says that core asks
`accepts_id` about the returned string as well as about the identifier as
issued, so a module must accept its own canonical form, or no row is read or
written for it.

`two_issues_of_one_identity_share_one_identity_graph_key` drives a fixture
module whose identifier is a stable part and a part that changes on each
issue through `AcceptedModules::canonical_kv_key`. Two identifiers that
differ only in the reissued part reach one key, and a different stable part
reaches another. The test fails for a module that returns the value
unchanged.

No behavior changes.
Jamesr51d and others added 24 commits October 13, 2026 14:14
…d run the Cloudflare auction test with no demand modules

The build script's TRUSTED_SERVER__GIT_VERSION reaches test processes and the strict loader refused it as an unknown field. Cloudflare runs one demand source at a time, so the fixture's two demand modules stopped worker startup.
The wasm clock lint reaches core's build script, which runs on the host.
Core's version_header module is removed, with its call from the finalize
step of the Axum, Cloudflare and Spin adapters and from the Fastly adapter's
health probe and its settings fallback, so no response carries x-ts-version
unless a [[serve]] entry names the service_version middleware. That
middleware now writes the git version the build compiled in from
TRUSTED_SERVER__GIT_VERSION or local git as X-TS-Version, when the build
knows one, and the version the adapter reports for the service as
X-TS-Service-Version, when it reports one. HEADER_X_TS_FASTLY_VERSION, which
nothing wrote, goes with the module.

The health tests of the three adapters prove the probe carries no version,
the middleware's own tests prove each header is written when its version is
known and nothing when neither is, and the publisher tests prove both
headers reach a buffered page and neither reaches a page no entry names the
middleware for. The configuration guide, the getting started guide, the
EdgeZero guide and the changelog say the same.
@jwrosewell

Copy link
Copy Markdown
Collaborator

Moved into this repository as #1359, one branch of the stack, so the review continues there. This pull request is closed in its favor and its comments stay here for the record.

@jwrosewell jwrosewell closed this Oct 15, 2026
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.

2 participants