Skip to content

Name the usage a 51Did was created for, and take the raw byte surface off FodId - #76

Open
jwrosewell wants to merge 2 commits into
mainfrom
feature/usage-accessor
Open

Name the usage a 51Did was created for, and take the raw byte surface off FodId#76
jwrosewell wants to merge 2 commits into
mainfrom
feature/usage-accessor

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Two changes to the same surface, in the order they were made.

1. Name the usage a 51Did was created for

FodId gains usage (None, NonMarketing, Standard, Personalized) and usage_from_consent, reading bits 0 to 2 and bit 3 of the flags byte. The usage bits are cumulative, so the accessor answers the highest usage granted rather than leaving callers to mask bits and misread every marketing identifier as non-marketing. Same accessor, same names, in all six 51Did packages. Found by the Trusted Server work, which must work only through this package.

2. Take the raw byte surface off FodId

This is issue 77, raised on all six packages so the surface stays the same in every language. Once every bit has a name, the raw byte and the offsets are the remaining way to get it wrong, so they go.

  • The flags property is removed. The byte stays as _flags, which the typed accessors read and which the tests that pin the byte now read.
  • hash, HASH_OFFSET and HASH_LENGTH are deleted, with the alias tests and the warnings import they needed.
  • The nine layout constants move to _layout.py, which is not exported from __init__.
  • date_minutes is removed. did_client.py did use it internally to pick the signing key, so it is now the private helper _date_minutes. DATE_EPOCH is unchanged.
  • A new test, test_raw_surface_is_not_public, asserts that all fourteen removed names are absent from the class and from an instance.

The example built its payload from the class constants, so it now states its own sample layout locally, which is right for a consumer, and prints the usage, its id.usage string and the consent bit instead of the raw byte.

The open question, decided

The issue left the raw date minutes as a candidate rather than a definite. It goes as a public accessor in all five packages that have it. No caller outside the packages used it and Rust never had one. Python is the case where the client itself needed the value, so it keeps it privately.

The specification

The byte structure is now specified once for all six languages, on specifications 25, at Identifier layout with the surface each package exposes at Package surface. The class docstring and the README link there instead of repeating the tables. Those links resolve once that pull request merges.

Verification

python -m pytest tests -q
146 passed, 2 skipped in 1.38s

The count was 147 before, and the one fewer is the deleted alias test. Running the tests needs the owid-python submodule and ci/copy-owid-source.ps1, which writes the ignored _owid package, and neither is committed. flake8 reports the same twenty pre-existing findings as it does on the unchanged branch point, none of them in the changed lines. This repository's continuous integration runs no linter for the Python packages, only tox -e py.

Closes #77

Produced with AI assistance and needs human review.

FodId exposed the flags byte and a type read from bits 6-7, and nothing
for the usage in bits 0-2, so a caller deciding whether an identifier may
be passed to a demand source had to mask the byte by hand. The three
usages are cumulative in the byte, non-marketing 0b001, standard 0b011
and personalized 0b111, so a caller masking for the non-marketing bit
alone would read every marketing identifier as non-marketing, the wrong
way round for a data protection decision.

Usage names the four states, NONE where no bit is set, and the usage
property answers with the highest granted. usage_from_consent reads bit
3, which records that the usage came from a consent string rather than
being stated. Usage.id_usage gives the cloud's id.usage value. The names
are the same in every 51Did package, which all gain the accessor
together.

Found by the Trusted Server work, which must work only through this
package. fiftyone_pipeline_did tests: 147 passed, 2 skipped, two new.
A 51Did could be read in two ways, being a typed accessor for each field
and the raw byte with the offsets to read the same thing by hand. The
second way is the one a caller is told never to use, and it is the way
that produces the mistake the usage accessor was added to stop, because
the usage bits are cumulative rather than exclusive, so a caller masking
the byte for the non-marketing bit reads every marketing identifier as
non-marketing, which is exactly backwards for a rule that says a
non-marketing identifier must never be passed to a demand source. These
packages are not widely deployed, so the raw surface is removed now
rather than deprecated, and the same removal is made in every language so
that the surface stays the same across all of them.

Gone from FodId are the raw flags byte, the deprecated hash alias and the
HASH_OFFSET and HASH_LENGTH constants left over from the match key
rename, and the nine byte layout constants FLAGS_OFFSET,
LICENSE_ID_OFFSET, LICENSE_ID_LENGTH, MATCH_KEY_OFFSET, MATCH_KEY_LENGTH,
HEADER_LENGTH, GUID_LENGTH, RANDOM_PAYLOAD_LENGTH and PAYLOAD_LENGTH. The
nine constants move to the new private module _layout, which the package
and its own tests import and which is not exported from the package
__init__, because the tests and the client build and measure payloads
byte by byte and still need them. The flags byte itself stays on the
instance as _flags, which is where the typed accessors read it and where
the tests that pin the exact byte now read it.

The raw date minutes goes too. No caller in any of the seven 51Degrees
repositories used it, Rust never had it, and the one documented purpose,
being the OWID public key request, is built by the 51Did client itself.
The Python client did read it internally to pick the signing key, so that
reading is now a private helper, _date_minutes in fod_id, and no longer a
property anyone outside the package can reach. The tests that asserted on
the minutes assert the same fact through the typed date.

Every typed accessor is unchanged, being type, usage, usage_from_consent,
license_id and match_key, plus the OWID level fields. The readme drops
the byte layout table and the removed accessors from its example, gains a
section on the usage accessor, and points at the specification for the
layout and the package surface, as the fod_id docstrings now do.

Closes #77
@jwrosewell jwrosewell changed the title Name the usage a 51Did was created for, as the highest granted Name the usage a 51Did was created for, and take the raw byte surface off FodId Sep 6, 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.

Remove the raw flags accessor, the byte layout constants and the Hash aliases from FodId (breaking change)

1 participant