Skip to content

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

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#127
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 getUsage() (None, NonMarketing, Standard, Personalized) and isUsageFromConsent(), 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 128, 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.

  • getFlags() is package-private. The byte stays, because the typed accessors are built on it and the package's own tests assert it. All fifteen assertions on the exact byte are in the same package and compile unchanged.
  • getHash(), HASH_OFFSET and HASH_LENGTH are deleted, along with the two tests that existed only to prove the aliases matched their new names.
  • The nine layout constants keep their names and lose public. DidClient and the tests are in the same package and reach them unchanged.
  • getDateMinutes() is deleted, with the private date origin constant it used. Nothing inside the package needed the minutes, so no helper replaces it. Its two tests now assert the same facts through getDate().

The open question, decided

The issue left the raw date minutes as a candidate rather than a definite. It goes, in all five packages that have it. No caller in any of the seven repositories used it, Rust never had one and nobody missed it, and the single purpose the documentation gave it, the OWID public key request, is built by the client itself.

One thing the issue missed

The issue lists the package's own tests and the cloud service as the code that must keep working. In Java the pipeline.developer-examples.fodid module is in a different package and used PAYLOAD_LENGTH, FLAGS_OFFSET, LICENSE_ID_OFFSET, MATCH_KEY_OFFSET, MATCH_KEY_LENGTH and getFlags(), so making the constants package-private breaks it. It is fixed in the same commit: the example states the offsets it needs as its own local constants, with a comment saying that building a payload is the creator's job rather than a reader's, and it prints the usage and the consent bit in place of the raw byte.

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 javadoc and the README link there instead of repeating the tables. Those links resolve once that pull request merges.

Verification

mvn -pl pipeline.did,pipeline.developer-examples/pipeline.developer-examples.fodid -am test on JDK 21 with -Werror:

FodIdTests        Tests run: 40, Failures: 0, Errors: 0, Skipped: 0
FodIdParseTests   Tests run: 22, Failures: 0, Errors: 0, Skipped: 0
DidClientTests    Tests run: 63, Failures: 0, Errors: 0, Skipped: 0
DidClientLiveTests Tests run: 2, Failures: 0, Errors: 0, Skipped: 2
ExampleTests      Tests run:  9, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

The two skips are the live tests, which need cloud credentials and were skipped before this change as well.

Closes #128

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 getUsage()
answers with the highest granted. isUsageFromConsent() reads bit 3,
which records that the usage came from a consent string rather than
being stated. The names match the cloud's id.usage values, which
getIdUsage() gives, and 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. pipeline.did tests: 129 run, 0 failures, two new.
A 51Did gave a caller two ways to read the same thing, being a typed
accessor for each field and the raw bytes with the offsets to walk them by
hand. The second way is the one that produces the mistake the usage
accessor was added to stop, because the usage bits are cumulative, so
non-marketing sets one bit, standard sets two and personalized sets three,
and code that masks the byte for the non-marketing bit alone reads every
marketing identifier as non-marketing, which is backwards for a rule that
says a non-marketing identifier must never reach a demand source. These
packages are not widely deployed, so the raw surface is removed now rather
than deprecated, and the same removal is being made in every language so
that the six packages keep one surface.

Gone from the public surface.

1. getFlags() is now package-private. The byte itself stays because
getType(), getUsage() and isUsageFromConsent() are built on it and this
package's own tests assert the exact byte, and those tests sit in the same
package so they keep working unchanged.

2. getHash(), HASH_OFFSET and HASH_LENGTH are deleted. They were aliases
left over from the match key rename and nothing called them. The two tests
that only asserted the aliases matched their new names go with them.

3. FLAGS_OFFSET, LICENSE_ID_OFFSET, LICENSE_ID_LENGTH, MATCH_KEY_OFFSET,
MATCH_KEY_LENGTH, HEADER_LENGTH, GUID_LENGTH, RANDOM_PAYLOAD_LENGTH and
PAYLOAD_LENGTH keep their names and become package-private. The only reason
to want an offset is to read a bit that now has a name.

4. getDateMinutes() is deleted. Nothing in any of the seven 51Degrees
repositories called it, the Rust package never had it, and its one
documented purpose, being the value the OWID public-key date parameter
takes, is built by DidClient itself. Nothing inside the package needed the
minutes, so the constant that converted them went too and no helper
replaced it. The two tests that asserted on the minutes now assert the same
round trip through the typed getDate(), including that the high bit of the
envelope's unsigned 32-bit minutes field does not read back as a date
before 2020.

Every typed accessor is unchanged, being getUsage(), isUsageFromConsent(),
getType(), getLicenseId(), getMatchKey() and the OWID level fields.

The 51Did example builds a payload byte by byte because it stands in for
the cloud, which is a writer's job rather than a reader's, so it now spells
out the three offsets it needs as its own constants and says why. It prints
the usage and whether the usage came from a consent string in place of the
raw flags byte, and its sample payload now carries standard marketing usage
so the accessor shows something.

Documentation. The class javadoc and the README no longer repeat the byte
layout table. Both name the 51Did specification as the authority for it, at
https://github.com/51Degrees/specifications/blob/main/did-specification/identifier-layout.md
and
https://github.com/51Degrees/specifications/blob/main/did-specification/package-surface.md
keeping only the short summary of the identifier type that changes how the
package behaves. The README gains a section on what a 51Did may be used
for, covering getUsage() and isUsageFromConsent(), the four usage values
and the cumulative bits, and it loses the lines that used the deleted
accessors.

Verified with mvn -pl pipeline.did,pipeline.developer-examples/pipeline.developer-examples.fodid -am test
on JDK 21, which builds with -Xlint:all -Werror. 127 tests pass in
pipeline.did with 2 live cloud tests skipped, and 9 pass in the 51Did
example.

Written with AI assistance and needs human review.

Closes #128
@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