Skip to content

Add the permission model with the Privacy Taxonomy vocabulary - #1045

Open
jwrosewell wants to merge 160 commits into
IABTechLab:mainfrom
jwrosewell:split/3-permissions
Open

jwrosewell wants to merge 160 commits into
IABTechLab:mainfrom
jwrosewell:split/3-permissions

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator

This description was updated on 7 October 2026, and on 8 October for the merge of main at 182fdf4. The branch now carries
the refreshed device and geo branch (#1044 at 6fc9cdf) and, through it,
the Edge Cookie seam and main at 182fdf4. Since the last version of this
text:

  1. What an operator selects is a module, and a module is named by its
    crate folder. The selector reads [permission-signal] modules, a list
    because several signals run in order, the trait is
    PermissionSignalModule, and every name this pull request adds says
    module, as the seams below it do. Each of the five crates names itself
    with module_name!(), so crates/permission-signal/gpp is
    permission-signal.gpp and the hand-written ids this branch started
    with (gpp_sale_opt_out, us_privacy) are gone. In the section the
    type folder may be left off, so the modules are selected as gpc,
    gpp, us-privacy, tcf and mtm. A configuration still carrying
    [permission_signal], sources or module is refused with the key to
    write.
  2. The permission state says which permissions are still waiting for a
    signal
    , as awaiting beside set, narrowed to what some configured
    module could still grant, so a page can tell a prompt that has not run
    from a visitor who said no.
  3. Each module owns its own unreadable signal. The override that revoked
    everything ahead of the modules when a record arrived unreadable is gone.
    A module reads its own unreadable record as the refusal it may have
    carried and says nothing about any other scheme, and the state carries
    signals, one entry for each signal a module read and found valid, as
    received. The page receives all four lists.
  4. The Model Terms for Marketing module ships, a fifth scheme under
    crates/permission-signal/mtm, reading the preference a platform
    recorded in the first party cookie __mtm_pref, which any platform may
    set and Trusted Server never writes. It is the first scheme that declares
    terms.
  5. The five permission signal crates' own tests run in CI through the Axum
    aliases, which did not name them before.
  6. Withdrawal on this branch is the permission state's answer, so the pull
    sync marker expiry main added reads it, and the legacy consent store
    main removed is gone here too.
  7. Comments and the permission model guide say what is true now. The
    integration fixture carried two notes on its geo selector that
    contradicted each other, and it, the Viceroy template, the template
    cache harness and the guide mentioned a default country setting that
    no released version had. The branch also carries Add a pluggable Edge Cookie module seam with the built-in HMAC module #1043's statement of
    what normalize_id_for_kv decides.

The commits since the merge are 66b4bf6 (the merge), 796bb47 (the no-location warning says what happens), 207d04e (which permissions are still waiting for a signal), c04649f (await only what a configured module could still grant), ddceadd (each module owns its own unreadable signal, and the state names the valid ones), 7554c3e (the Model Terms for Marketing module), fc2a355 (the permission model says module), 1e8b612 (the module crates' tests run with the Axum adapter) and 511db9b (comments and tests brought into line), then on 7 October 6489135 (the merge of #1044 at 6fc9cdf) and fd51fe8 (the permission signal modules are named by their crate folders), then 05a1cce (the merge of #1044 at cec6052), 9da9a65 (comments and the permission model guide say what is true now), c0a0f3a (the merge of #1044 at 09514b1) and 613cd18 (the merge of #1044 at fb1d369, which brings #1043's wording of the identity graph's comments as device records).

Stacks on device and location module selection (#1044), and depends on its
[geo] module selector, whose default this pull request changes to no
geolocation. Through #1044 this pull request also depends on the Edge Cookie
module seam (#1043) for the EdgeCookieModule trait, which it gates on
permissions. The stack has six pull requests
(#1043, #1044, #1045, #1046, #1047, #1094), each targeting main, with this one
third, and the first five decompose #838 as requested in the #986 review.
Compare split/2-device-geo with split/3-permissions
to see only this pull request's change.

The design specs for the series are carried by the spec pull request (#1084).
The spec for this pull request is
2026-07-30-permission-model-design.md,
revised to match this implementation, with revision records listing every
divergence and why, and section 13 covering the permission signal modules.
Reader documentation lands with this pull request at
docs/guide/permission-model.md
and
docs/guide/permission-signals.md.

What this pull request does

Permissions become the primitive that gates identity features, and consent is
one of several ways a permission is established, alongside a country baseline,
an opt-out signal and configuration.

  • permissions.rs
    resolves a per-request permission state, being the country and region
    baseline from permissions.yaml amended by the signals the request carries.
    permissions.yaml is compiled into the build, and the repository sample is
    config/permissions/sample.yaml.
    Permission names follow the IAB Privacy Taxonomy Data Uses.

How the signal modules are selected

Signals are read by permission signal modules, which are crates outside core
under
crates/permission-signal/,
being Global Privacy Control, the GPP sale opt-out, the US Privacy string, TCF
v2 and the Model Terms for Marketing, each implementing the
PermissionSignalModule trait that core defines in
permission_signal/mod.rs.

[permission-signal]
modules = ["gpc", "gpp", "us-privacy", "tcf", "mtm"]

An adapter selects them once at startup from [permission-signal] modules,
refusing an unknown or repeated name there rather than on the first request,
and carries them on the request services. An unknown name is reported with the
names the build does have, so a typo is answered with the list to pick from.

Precedence is the configured order. For each permission, core asks the
modules in the order [permission-signal] modules lists them. Each answers
grant, revoke or neutral, a neutral answer leaves the prior value standing, and
the last module with an opinion decides. Leaving the list out runs every
module the adapter links, in the adapter's order, and an empty list runs
none, which leaves every permission at its country and region baseline. Every
adapter offers the five in the same default order, being gpc, gpp,
us-privacy, tcf and mtm. Global Privacy Control, a
browser setting with no interface of its own, is asked first, and the schemes
that carry a choice someone made through an interface are asked after it, which
means an answer given at a prompt amends the header the visitor arrived with. A
deployment that wants an opt-out to stand lists it after tcf.

What the state carries

The page receives the state as window.tsjs.permissions, with four lists:
set, the permissions that are set; awaiting, the permissions whose
baseline requires a signal and for which no configured module has answered,
narrowed to what some configured module could still grant, so a page can hold
what depends on them rather than read "nobody has answered" as "refused";
signals, one entry for each signal a module read and found valid, as it was
received, with the module and the scheme, so page code and bid requests rely
on exactly those signals and no other; and tdls, the terms documents the
data is available under. An empty state renders every list empty, and each
is an answer rather than a missing value.

The rest of the model

  • Each module owns its own unreadable signal. What an absent, unreadable or
    expired record means for the permissions is the decision of the module for
    that scheme, taken in its own answer. The shipped modules read their own
    unreadable record as the refusal it may have carried, on the permissions
    their scheme covers, and say nothing about any other scheme, so the
    configured order decides a readable record from one scheme beside an
    unreadable one from another. After assembly the consent context keeps only
    the raw strings the valid signals name, so a corrupt record never reaches a
    bid request.
  • Leaving gpc off the list stops that module, but the consent pipeline
    still turns the same header into a US Privacy opt-out for a US privacy state
    when the consent setting gpc_implies_optout is on, which it is by default,
    and the us-privacy module then acts on that record. A deployment that
    wants the header to have no effect turns that setting off as well.
  • Cross-scheme tests in
    crates/trusted-server-adapter-axum/tests/permission_signals.rs
    assemble the real modules and cover the default order against each opt-out,
    the reversed order, a module left off the list, one opt-out removed while
    the others stand, and the Model Terms words.
  • The TCF purpose to Data Use mapping lives in the TCF module crate
    (crates/permission-signal/tcf/src/mapping.rs),
    in code with tests, because which purpose grants which Data Use is that
    scheme's own meaning rather than a deployment's policy. permissions.yaml
    keeps what a deployment decides, being which opt-out sources revoke which
    Data Uses and whether a TCF record answers at all. The purposes are the IAB
    TCF Europe purposes and what they grant are IAB Tech Lab Privacy Taxonomy
    Data Uses, so the mapping is checked against the industry's own documents
    rather than against us. Two purposes have no Data Use yet, so the crate
    carries the proposed necessary.operations.storage key for purpose 1 and the
    TCF identifier select-basic-content for purpose 11 until the taxonomy adds
    them.
  • The permission state also carries tdls, the terms documents the request's
    data is available under, declared by the modules in the order they are
    asked with duplicates removed. A permission says what may be done with the
    data and not on what basis, and a recipient offered data needs both, because
    it has to decide whether the terms are ones it accepts and whether it may
    pass the data on. Each entry addresses a published document that must never
    be edited once published, which is why a version belongs in its address, and
    the name matches the tdl member the
    Data Labels work
    puts on an OpenRTB node. Of the five schemes only the Model Terms for
    Marketing declares terms, the versioned Model Terms document, whenever an
    answer is present.
  • The Model Terms for Marketing module reads one of the words standard,
    personalized and non-marketing from __mtm_pref. Both marketing words
    grant storage, contextual advertising, measurement, content performance,
    market research, product improvement and basic content selection, and only
    personalized grants the four Data Uses that depend on browsing history or
    interactions, which standard refuses because the visitor chose against
    them. non-marketing changes nothing, so the country and region rules
    decide. No word withdraws. The five schemes are a starting set rather than
    the list, and
    docs/guide/permission-signals.md
    says how a scheme is added as a crate with no change to core.
  • Destructive withdrawal is narrow. A module answers withdrawal separately
    through withdraws, core scopes the answer to the storage baseline, and of
    the five only TCF answers it. Only a TCF record refusing storage in a
    jurisdiction whose baseline did not grant storage therefore expires the
    cookie, the pull sync marker and writes the identity-graph tombstone.
    Opt-outs suppress use, with Edge Cookie headers stripped and nothing sent
    beyond the edge, but never destroy an issued identifier, so lifting the
    opt-out restores the identity. This differs from main, where
    has_explicit_ec_withdrawal in
    consent/mod.rs
    also withdraws in a US privacy state for a Global Privacy Control signal, a
    GPP sale opt-out or a US Privacy sale opt-out. Under this pull request those
    opt-outs suppress use and keep the cookie, and the withdrawal tests main
    wrote around a California Global Privacy Control opt-out state the
    withdrawal on the permission state.
  • Sharing beyond the edge, being the bidstream user.id, the identify response
    and partner pull sync, requires both storage and personalized-ad selection
    (StoreOnDevice and SelectPersonalisedAds in ec_sharing_allowed), the
    same pair that gates bidstream EIDs, so a storage-only grant keeps
    first-party use while withholding partner sharing.
  • The Edge Cookie gate moves from raw consent to the permission model. A
    module declares required_permissions() and core runs it only when every
    one is set.
  • The permission baseline and the jurisdiction to assume when no country
    resolves are declared together on the top node of the rules tree in
    permissions.yaml, so there is always a defined baseline and one file states
    the policy for both. A failed geo lookup is distinct from an unmatched
    country. It resolves at the requires-signal floor instead of the declared
    default and is logged at error level, and the one-time warning for a lookup
    that resolves no location says that the declared jurisdiction applies.
    Geolocation is off by default, and a deployment that runs an Edge Cookie
    module with no location module must set
    [geo] assume_single_jurisdiction = true, acknowledging that every request
    is treated as the declared jurisdiction.
  • permissions.yaml rules use an explicit per-permission acquisition map
    (granted, requires_signal, denied). Parsing rejects an unknown group,
    Data Use, acquisition or revoke keyword, a top node missing group or
    jurisdiction, and two place codes that differ only by case, each case in
    one table of refusals. An EU-27 plus EEA coverage test locks the gdpr-eu
    mapping.

Changed from an earlier description

An earlier version of this text described precedence as a rule fixed in code,
where an opt-out always beat a consenting TCF record. Precedence is the
configured order described above. Under the default order, a visitor who sends
Sec-GPC: 1 and then consents through a consent management platform has the
Data Uses that the TCF record consents to set, which is the opposite of the old
outcome, and a deployment gets the old outcome back by listing the opt-out
after tcf. An earlier version also said an unreadable consent record revoked
everything ahead of the modules. It no longer does, for the reason given
above.

How it was verified

The head this pull request shows now is 0afd715c5, which merges #1044 at 47551a622 and, through it, main at 182fdf4 into 613cd1831. The merge brings the ts dev lint domains and ts dev install-hooks commands (#733) and a design document (#930), and changes none of this pull request's own code. No dependency of core or of an adapter changed version, and the one line of core the merge touches gains a comment. On 0afd715c5 these ran locally on Windows: cargo fmt --all --check, a locked dependency check (cargo tree --locked) and the core suite natively (3,114 tests).

613cd1831 adds to fd51fe830
one commit of comment and guide wording and three merges of #1044, which bring
comment wording, the identity graph's comments, one doc comment and one
test. On 613cd1831 these ran
locally on Windows: cargo fmt --all --check, Clippy with warnings denied on
all four adapters and the five permission signal crates, the core suite
natively (3,114 tests), the five permission signal crates' own tests
(55 tests), and the docs lint, Prettier and VitePress build. On fd51fe830
the wider set ran locally, being the core suite under Viceroy (3,350 across
the crate's test binaries), the Axum suite with the five permission signal
crates (123 tests), the Cloudflare (56) and Spin (90) adapter suites and the
cross-adapter parity suite (17), and CI runs all of them on this head. The
CLI tests, the browser integration tests and the Fastly Edge Cookie
lifecycle test run only in CI.

CI on 0afd715c5: every check passes, being Run Tests, Run Format, Integration Tests, Permissions Inspector and CodeQL Advanced, with the Fastly Edge Cookie lifecycle test in the integration run.

Framing

Privacy is a spectrum and technology is neutral. This model encodes no
jurisdiction's law. The deployer brings the policy in permissions.yaml and
configuration, decides their own baselines, and the code makes those decisions
inspectable and enforced. Trust comes from that flexibility being respected,
not from constraint.

References #777 and #779. Decomposes #838. Spec baseline from #986.

@jwrosewell
jwrosewell force-pushed the split/3-permissions branch 2 times, most recently from 6d20255 to 760b921 Compare August 20, 2026 02:22
@jwrosewell jwrosewell mentioned this pull request Aug 25, 2026
15 of 17 tasks
@jwrosewell
jwrosewell force-pushed the split/3-permissions branch from 760b921 to b3a0eae Compare August 25, 2026 10:51
@jwrosewell
jwrosewell force-pushed the split/3-permissions branch from b3a0eae to 5a707bf Compare August 25, 2026 13:37
@jwrosewell
jwrosewell force-pushed the split/3-permissions branch 6 times, most recently from 4d591e9 to 35f6ef2 Compare August 31, 2026 12:50
jwrosewell added a commit to jwrosewell/trusted-server that referenced this pull request Aug 31, 2026
The review of IABTechLab#1043 asked that spec changes land before the code that
implements them, so a divergence is a decision taken in review rather
than a ratification of something already merged. PRs IABTechLab#1043 to IABTechLab#1047
each carried the design document for their own step, and IABTechLab#1043 carried
a 607-line spec describing device providers, geo providers, the
permission model and the browser resolve endpoint, none of which is in
that PR.

Move all six series documents here, so this PR carries the complete
normative set and no code:

- 2026-07-30-pluggable-providers-design.md (from IABTechLab#1043)
- provider-code-registry.md (from IABTechLab#1043)
- 2026-07-30-permission-model-design.md (from IABTechLab#1045)
- 2026-07-30-client-cycle-ec-resolve-design.md (from IABTechLab#1046, later
  revised by IABTechLab#1047)
- 2026-07-30-integration-response-header-hook-design.md (from IABTechLab#1047)
- 2026-07-30-provider-migration-rollout-design.md (from IABTechLab#1047)

Each file is taken verbatim at the tip of the stack, so the later
revisions are preserved: the provider-switching continuity section, the
geo requires-signal floor, and the code-envelope paragraph IABTechLab#1047 added
to the client-cycle spec. The revision-record tables are unchanged. No
document's substance was edited.

The only edits are to this spec's own status line, which said the PR
adds one document and that the series specs land with IABTechLab#1047, and a
revision-record row recording the move.
@aram356

aram356 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

The sequencing discussion for this series is on #1084. This PR is superseded rather than rejected. The design in §3.6 is accepted and most of the provider work carries over onto the reordered base. See #1084.

…ider

First of five PRs decomposing the provider and permission epic. The
EdgeCookieProvider trait routes Edge Cookie minting, cookie read-back,
and KV keying through the selected provider, so a vendor identifier
round-trips verbatim instead of being dropped by the built-in shape
check.

- [ec] provider selector with per-provider [ec.providers.<key>] blocks.
  The deprecated [ec] passphrase form still starts for one release
  cycle: it maps to provider = "hmac" with a deprecation warning, and a
  configuration carrying both forms is rejected. provider = "none"
  spells explicit statelessness. A configured block that is not the
  selected provider is rejected at startup, as is a block with no
  selector.
- Global identifier bounds enforced by core at mint, read-back, and
  cookie write: the cookie-safe alphabet [A-Za-z0-9._~-] and a 256-byte
  cap. An identifier outside the bounds is rejected loudly, never
  rewritten, so the cookie value and the identity-graph key can never
  silently diverge.
- The identity graph is keyed by the provider's canonical form of the
  identifier (normalize_id_for_kv), so equivalent representations of
  one identity share one row.
- Request evidence abstraction (crate::evidence) giving providers read
  access to the client IP, headers (including cookies), URL path, and
  query parameters.
- Adapter injection seam: RuntimeServices carries an optional vendor
  provider, so a vendor provider lives in its own crate and core never
  names it. A selected provider the adapter does not inject fails the
  request loudly rather than silently running stateless.
- Provider generate failures log at error level with the request
  proceeding stateless.

Edge Cookie creation and use stay gated by the existing consent context
exactly as on main, including with no provider selected; the permission
model replaces that input in the third PR of this series.

Config migration: move [ec] passphrase to [ec.providers.hmac] and set
[ec] provider = "hmac". The old form keeps working for one release with
a warning. Passphrases shorter than 32 characters are now rejected at
startup; previously they were accepted.

The design spec for this slice and the next lives at
docs/superpowers/specs/2026-07-30-pluggable-providers-design.md, the
2026-07-31 draft revised to match the implementation with a
revision-record table of every divergence.

Every provider carries a mandatory registered four-character code
(provider-code-registry.md): core mints {code}~value, checks the code
at read-back, and keys the identity graph with it, so identifiers from
different providers can never collide and a switch of provider cannot
silently adopt another provider's identities. The built-in hmac
provider mints hmac~<hash>.<suffix> and dual-reads its pre-envelope
bare form for one release cycle.
Since the provider-code envelope, the mint path issues identifiers as
hmac~{64hex}.{6alnum}, and that is the value identify hands to partners.
Pull sync, batch sync and the admin lookup still validated the bare
shape through is_valid_ec_id, so pull sync skipped every freshly minted
identifier, batch sync answered invalid_ec_id for the value partners were
given, and the admin lookup answered 400. CI stayed green because the
lifecycle scenario seeds a bare cookie.

is_valid_ec_id now accepts the hmac envelope as well as the legacy bare
form and rejects any other provider's code, and normalize_ec_id_for_kv
keeps the envelope so the key matches the one written at mint. Tests
cover the validator, the normalizer and each of the three call sites
with a coded identifier.
CodeQL's cleartext-logging query treats a call whose name contains
"passphrase" as a sensitive source, and because the method mutates the
Settings it belongs to, every later log line that prints anything from
Settings (store names, timeouts, header names) is reported as writing a
secret to a log. The passphrase itself is a Redacted<String> and none of
the flagged lines prints it. The method now describes what it does,
migrate_legacy_ec_layout, and its behavior is unchanged.
A reviewer raised a P1 against the pluggable Edge Cookie provider work:
three of the four adapters broke the provider contract that an
unavailable required service or an uninjected provider stops the
request.

The Axum, Cloudflare and Spin adapters each read the Edge Cookie context
with `EcContext::read_from_request_with_geo(...).unwrap_or_else(...)`,
logged a warning and continued with `EcContext::default()`. A deployment
whose selected provider could not be built therefore came up and served
every request with no identity, silently. The Fastly adapter already
kept the report and answered with an error response.

`build_ec_context` on the three adapters now returns
`Result<EcContext, Report<TrustedServerError>>` and every call site
propagates it to that adapter's own `http_error`, the same helper Fastly
uses, so all four answer with the same status and shape.

The design this implements has the composition root check a selected
provider's needs once at startup rather than per request, so
`ensure_provider_available` was added to `ec/provider.rs` and is called
from `build_state_with_settings` on all four adapters (Fastly included,
so the rule is uniform). Building a provider reads no request data, so
a selection an adapter can never supply now fails when application state
is built, and the three adapters answer every route from their existing
`startup_error_router` instead of coming up. Statelessness, meaning no
`[ec] provider` selector or the explicit `"none"`, still passes and
still serves.

The widening question was checked rather than assumed.
`read_from_request_with_geo` can only fail from two places: the provider
build, and a `Cookie` header that is not valid UTF-8. A malformed cookie
value is dropped with a warning by `request_ec_id_if_allowed`, consent
parsing returns a value rather than a `Result`, and the geo lookup is
already swallowed by the adapter before the call, so no ordinary parse
problem reaches the error path and none is turned into a failed request.

Tests: each of the three adapters gains a route test proving an
uninjected provider fails at startup, and an in-crate test proving
`build_ec_context` returns the error rather than a default context.
Core gains a test that the startup check rejects an uninjected provider
and still allows statelessness both ways.

Addresses: Christian Pavilonis review of PR 1043, crates/trusted-server-core/src/ec/provider.rs:317 (P1)
`Settings::finalize_deserialized` runs derive validation before
`Ec::migrate_legacy_ec_layout`, and the deprecated `[ec] passphrase`
field carries no `#[validate]` attribute of its own, so the advertised
32-byte minimum was only enforced on the new `[ec.providers.hmac]`
location. A configuration still on the old form could start with
`passphrase = "short"`, or with an empty value, and mint identifiers
from keying material the new location rejects.

The migration now calls `Ec::validate_passphrase` on the value it is
about to move, before it logs the deprecation warning and writes the
`[ec.providers.hmac]` block, and reports a configuration error naming
the minimum length and the new location.

Tests: `a_legacy_passphrase_is_held_to_the_passphrase_rules` drives
`Settings::from_toml` with the `[ec]` section rewritten to the
deprecated form and proves a short value and an empty value are both
rejected, and that a passphrase of adequate length still migrates to
`provider = "hmac"` with the passphrase in the hmac block. Removing the
new check makes that test fail, so it tests the fix rather than the
surrounding code.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/settings.rs:658 (wrench)
The provider spec (section 6) says `deny_unknown_fields` is set on both
built-in provider config structs, but `HmacProviderConfig` carried no
such attribute, so `[ec.providers.hmac] typo_key = "x"` was accepted
silently. An operator who mistypes a key gets a deployment that starts
and quietly uses the default for the setting they meant to change.

`HmacProviderConfig` now sets `#[serde(deny_unknown_fields)]`, matching
`Ec` itself and the rest of the settings tree. The struct is a plain
field of `EcProviders` rather than a flattened one, so the attribute
does not collide with the `#[serde(flatten)]` vendor map alongside it.

Tests: `an_unknown_key_in_the_hmac_provider_block_is_rejected` adds an
unknown key to the block in the crate test configuration and proves
`Settings::from_toml` fails and names the key. Removing the attribute
makes that test fail.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/settings.rs:726 (wrench)
`build_provider`'s `"hmac"` arm mapped over `ec.providers.hmac`, so a
deployment that selected `provider = "hmac"` with no
`[ec.providers.hmac]` block got `Ok(None)` and ran stateless under a
selector that says it has an identity provider. Every other unbuildable
selection in the same match already errors.

The arm now returns `TrustedServerError::EdgeCookie` naming the missing
block, which the startup check `ensure_provider_available` turns into a
failed application state on every adapter.

`Ec::validate_provider_selection` rejects that pair before settings
reach the composition root, so nothing routes through the new arm today.
It is the drift guard for the case where the two checks stop agreeing,
which is exactly the shape of the defect being fixed, so it is worth
keeping rather than leaving the silent branch in place.

Tests: `selecting_hmac_without_its_block_fails_loudly` builds the `Ec`
programmatically, bypassing settings validation to reach the seam, and
proves the error names the missing block. The doc comment's `# Errors`
section is corrected in the same commit, since it still claimed no
built-in construction can fail.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/provider.rs:304 (refactor)
The error raised when a provider mints an identifier outside the
identifier bounds was written across two source lines without the
trailing backslash that joins them, so the 22 spaces of source
indentation became part of the literal and the logged message read
"...bytes, or                      outside the cookie-safe alphabet".

The continuation is restored, so the message reads as one sentence.

The whole of ec/mod.rs was scanned for the same fault, matching every
string literal and stripping real continuations before looking for runs
of more than one space or a newline inside a literal. This message was
the only one.

Addresses: Aram Grigoryan review of PR 1043, crates/trusted-server-core/src/ec/mod.rs:444 (nitpick)
What an operator selects to supply a capability is a module, and provider
keeps the one meaning it has on main, an auction provider instance. This
renames the words this branch adds on top of the device and geo seams,
which already say module, and only those words: every provider word was
compared with upstream main, and a word that exists there is untouched,
as is the icu_provider crate the permissions inspector's lock file names.

- PermissionSignalProvider is PermissionSignalModule, and GpcProvider,
  GppSaleOptOutProvider, UsPrivacyProvider, TcfProvider and MtmProvider
  are GpcModule, GppSaleOptOutModule, UsPrivacyModule, TcfModule and
  MtmModule.
- build_permission_signal_providers is build_permission_signal_modules,
  the adapters' shipped_signal_providers is shipped_signal_modules, and
  the permission_signal_providers field and arguments say modules.
- The [permission_signal] selector reads module, as the seams' selectors
  do, so the refusal of the removed sources key names module.
- Test doubles, test helpers and test names say module, and the plain word
  says module wherever it meant a permission signal, geo, device or Edge
  Cookie module, in the core code, the five module crates, the adapters,
  the sample policy, the fixtures and the guides.
The five permission signal crates carry their own unit tests, and no CI job
ran them, because the test-axum alias covered the adapter crate alone. The
test-axum and clippy-axum aliases now name the five crates as well, so the
Axum job tests and lints them.
Comments say what the code does. The "today" qualifiers go, along with
the claim that no shipped geo module reports a lookup failure and the
counts of shipped schemes, which change as schemes are added. Device
storage gates every shipped Edge Cookie module, not only the built-in
one.

The fifteen tests of how from_yaml refuses a malformed policy are one
table, each case named with the refusal it must report, so a new refusal
is one row rather than a new test shaped like the last one.
The sandbox tests load settings that select the HMAC Edge Cookie module
through the deprecated passphrase form and no geo module. On this branch
that pair needs the single-jurisdiction acknowledgment, as every other
test settings document in the adapter carries, so without it the settings
refuse to load and the test traps under Viceroy before it starts.
Both pages failed the docs prettier check in CI. This is prettier's own
output, with no change to the wording.
The inspector called ConsentContext::has_malformed_record, which went
when each permission signal module took over the meaning of its own
unreadable signal, so the inspector's wasm build failed in CI. The field
is removed rather than replaced, because nothing at this level can
answer it and reporting false would read as none having been found.
The workspace pins the edgezero adapters to a revision rather than the
v0.0.8 tag, and the inspector resolves against the same crates, so its
own lock follows.
The docs format job also runs prettier over the Markdown outside docs/,
which rejected both files. This is prettier's own output, with no change
to the wording.
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.
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.
…ords

`KvNetwork` said a low cluster count "indicates an individual or
household". A count of connections is a fact about a network, and reading
a low one as an individual describes a device record as a record about a
person. It now says a small network, such as a home connection.

`KvEntry::ids` now says what it holds, which is each partner's identifier
for the same browser on the same device, never an identifier for a person.

Comments only, with no change in behavior.

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.

3 participants