Skip to content

Commit 5f92e0c

Browse files
committed
Name the declared permissions in the device refusal and align the guides with the flow
The startup refusal for a module device provider that declares permissions now names the Data Uses it declared, which the pluggable-providers spec already said it did. The permission-model guide and the permissions module doc no longer say a device or geo declaration is recorded rather than enforced, because a module device declaration is refused at startup and a geo declaration is not consulted. The guide now says request filters receive the state on the Fastly adapter, the only adapter that runs the filter step, that the head script needs a head to inject into, that a bot-classified visitor receives the state resolved for the request rather than an empty one, and that the client-fixed demo script declares and checks its required Data Use. The integration guide gains a section on what a module sees of the permission state, because it said nothing about it.
1 parent 7d0d5e1 commit 5f92e0c

4 files changed

Lines changed: 64 additions & 18 deletions

File tree

crates/trusted-server-core/src/integrations/registry.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -964,9 +964,15 @@ fn resolve_device_provider(
964964
if let Some(provider) = resolved.as_ref()
965965
&& provider.required_permissions() != crate::permissions::PermissionSet::none()
966966
{
967+
let declared = provider
968+
.required_permissions()
969+
.iter()
970+
.map(crate::permissions::Permission::as_str)
971+
.collect::<Vec<_>>()
972+
.join(", ");
967973
let message = format!(
968-
"integration module `{}` declares a device provider with required permissions, \
969-
and Trusted Server has no per-request gate that can enforce them yet, so the \
974+
"integration module `{}` declares a device provider requiring `{declared}`, \
975+
and Trusted Server has no per-request gate that can enforce that yet, so the \
970976
selection is refused rather than silently ignored",
971977
selector.unwrap_or_default(),
972978
);
@@ -3773,7 +3779,10 @@ mod tests {
37733779
"the failure should name the module, got: {rendered}"
37743780
);
37753781
assert!(
3776-
rendered.contains("required permissions"),
3782+
rendered.contains(&format!(
3783+
"requiring `{}`",
3784+
crate::permissions::Permission::StoreOnDevice.as_str()
3785+
)),
37773786
"the failure should say why it was refused, got: {rendered}"
37783787
);
37793788
}

crates/trusted-server-core/src/permissions.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
//! signals and the country it resolves to, and refuses to run the Edge Cookie
66
//! provider when its required permissions are not set. The device and geo
77
//! providers declare their requirements through the same method, and the
8-
//! built-in ones require none; gating their execution on that declaration is
9-
//! follow-up work.
8+
//! built-in and host ones require none. A device provider supplied by an
9+
//! integration module that declares any permission is refused at startup,
10+
//! because no per-request device gate exists yet to honor it. A geo
11+
//! declaration is not consulted, because geo runs before the permissions are
12+
//! assembled and supplies the country they depend on.
1013
//!
1114
//! The vocabulary is the IAB Privacy Taxonomy Data Uses, mapped from the IAB TCF
1215
//! Europe purposes and used **only** as a technical identifier for a permission.

docs/guide/integration-guide.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,28 @@ The three delivery choices are exclusive and the last call wins, so a builder ch
317317

318318
A module may declare a geo, identity or device provider that no selector chooses, which is a configuration a deployment can hold while it switches providers, so the registry logs a warning naming the unselected capability rather than refusing to start.
319319

320+
### What a Module Sees of the Permission State
321+
322+
Trusted Server resolves the permission state for a request once, at the start
323+
of the request cycle, and hands it to a module in two places rather than having
324+
the module derive its own.
325+
326+
- A request filter receives `permissions: Option<&PermissionState>` on its
327+
`RequestFilterInput`, next to the geo result. The filter step runs on the
328+
Fastly adapter today, and there the state is built before any filter runs.
329+
- A page module reads `window.tsjs.permissions`, an object `{"set": [...]}` of
330+
the Data Use names set for the request, and waits on `tsjs.whenPermissions()`
331+
because the value arrives at `<head>` open under inline assembly and at the
332+
`</body>` seam under a shared template. A module declares the permissions it
333+
requires in its own source, the same names its server-side provider declares,
334+
and does nothing with identity and contacts no vendor until the promise
335+
resolves with those names in the set. The client-fixed demo script under
336+
`integrations/ec_client_fixed` is the worked example.
337+
338+
A device provider a module supplies must require no permission, because no
339+
per-request device gate exists yet; a module that declares one is refused at
340+
startup. See the permission model guide for the model itself.
341+
320342
### How an Adapter Composes It In
321343

322344
The Axum, Cloudflare and Spin adapters take the builders as arguments, so no adapter names a vendor.

docs/guide/permission-model.md

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,16 @@ vendor-neutral default needs no consent prompt and no per-request
4747
policy interaction.
4848

4949
Device and geo providers declare their required permissions through the same
50-
method, and the core does not yet gate either of them on what they declare, so
51-
for those two the declaration is recorded rather than enforced. The only place
52-
a declaration currently decides whether a provider runs is the Edge Cookie
53-
path, in `ec/mod.rs`. Treat a device or geo declaration as a statement of
54-
intent until that gap is closed, and do not rely on it to keep a provider from
55-
running.
50+
method. The built-in and host providers require none. A device provider that
51+
an integration module supplies and that declares any permission is refused at
52+
startup, with a message naming the module and the permissions it declared,
53+
because device classification runs before the permission state for the request
54+
is assembled and there is no per-request gate that could honor the
55+
declaration. Refusing it is preferred to recording a requirement that never
56+
binds. A geo declaration is not consulted, because geo runs first and supplies
57+
the country the permission baseline depends on. The only declaration that
58+
decides whether a provider runs per request is the Edge Cookie provider's, in
59+
`ec/mod.rs`.
5660

5761
## Evidence is not rationed, use is
5862

@@ -388,7 +392,9 @@ its own, so there is a single decision per request.
388392
**Server side.** An integration request filter receives
389393
`permissions: Option<&PermissionState>` on its `RequestFilterInput`, alongside
390394
the geo result it already receives, so a filter can skip or narrow what it does
391-
when a permission it depends on is unset. None of the shipped filters changes
395+
when a permission it depends on is unset. Request filters run on the Fastly
396+
adapter today, which is the only adapter that runs the filter step, and there
397+
the state is built before the filters run. None of the shipped filters changes
392398
its behavior on that input yet, so for now the state is carried and available
393399
rather than acted on.
394400

@@ -409,22 +415,28 @@ the `permissions.yaml` keys and `Permission::as_str()`.
409415
Delivery follows the pattern already used for `adSlots` and `bids`, and the
410416
timing depends on how the page is assembled. Under inline assembly the value is
411417
injected as a `<script>` at the open of `<head>`, before the tsjs bundle, on
412-
every HTML document, so it is there before any page module runs. Under
418+
every HTML document that has a `<head>`, so it is there before any page module
419+
runs. Under
413420
shared-template (ESI) assembly the `<head>` is part of a template shared across
414421
visitors and must carry nothing request-scoped, so the value is spliced into the
415422
per-request script at the `</body>` seam instead. A permissions-only seam script
416-
is emitted even when the ad stack did not run, so a visitor whose consent was
417-
denied or who was classified as a bot still receives the state, empty in that
418-
case rather than missing.
423+
is emitted even when the ad stack did not run, so a visitor who was
424+
classified as a bot, or for whom the auction was gated off, still receives the
425+
state resolved for the request rather than nothing.
419426

420427
Because the arrival point moves, a page module must not read `tsjs.permissions`
421428
directly at load. TSJS core defaults the value to `{ set: [] }` and exposes
422429
`tsjs.whenPermissions()`, a promise that resolves when the real value arrives,
423430
immediately in the head-first case or at the body seam, with a
424431
`DOMContentLoaded` fallback. That promise is the waiting point for a vendor page
425432
module, which does nothing with identity and contacts no vendor until the
426-
promise resolves. Wiring the existing integrations' JavaScript to wait on it is
427-
follow-up work.
433+
promise resolves. If nothing arrived by `DOMContentLoaded` the promise resolves
434+
with the empty state, which a module reads as nothing set. The client-fixed
435+
demo page script does exactly this. It declares the same Data Use its
436+
server-side provider requires, `necessary.operations.storage`, and posts to the
437+
resolve endpoint only when the promise resolves with that Data Use in the set,
438+
so it is the pattern a vendor page module follows. Wiring the other existing
439+
integration scripts to wait on the promise is follow-up work.
428440

429441
The page cannot work this out for itself. Consent is only one of the ways a
430442
permission is set, and the country and region baselines and the deployer's

0 commit comments

Comments
 (0)