Project import generated by Copybara.

GitOrigin-RevId: ae57b289f93d58e7c0c8a0e15db62a9659c85ac8
Change-Id: I5bb44e2cf1472491ae55d154c52f870a45348e0f
diff --git a/BUILD b/BUILD
index db6ef52..5456139 100644
--- a/BUILD
+++ b/BUILD
@@ -114,7 +114,10 @@
 rust_library(
     name = "handle_map",
     srcs = glob(include = ["common/handle_map/src/**/*.rs"]),
-    deps = [":lock_adapter"],
+    deps = [
+        ":lock_adapter",
+        "@crate_index//:lazy_static",
+    ],
 )
 
 rust_library(
@@ -141,6 +144,7 @@
         "@crate_index//:nom",
         "@crate_index//:strum",
         "@crate_index//:tinyvec",
+        "@crate_index//:itertools"
     ],
 )
 
diff --git a/bazel_placeholder/Cargo.lock b/bazel_placeholder/Cargo.lock
index bed8428..7f40048 100644
--- a/bazel_placeholder/Cargo.lock
+++ b/bazel_placeholder/Cargo.lock
@@ -219,6 +219,12 @@
 ]
 
 [[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
+[[package]]
 name = "elliptic-curve"
 version = "0.13.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -331,6 +337,15 @@
 ]
 
 [[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
 name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -397,6 +412,7 @@
  "ed25519-dalek",
  "hkdf",
  "hmac",
+ "itertools",
  "lazy_static",
  "nom",
  "p256",
diff --git a/bazel_placeholder/Cargo.toml b/bazel_placeholder/Cargo.toml
index e7ee82a..3344be6 100644
--- a/bazel_placeholder/Cargo.toml
+++ b/bazel_placeholder/Cargo.toml
@@ -29,4 +29,5 @@
 x25519-dalek = { version = "2.0.0", default-features = false }
 subtle = { version = "2.5.0", default-features = false }
 sec1 = "0.7.3"
-sha2 = { version = "0.10.8", default-features = false }
\ No newline at end of file
+sha2 = { version = "0.10.8", default-features = false }
+itertools = "0.13.0"
\ No newline at end of file
diff --git a/common/Cargo.lock b/common/Cargo.lock
index 496db0d..1b19b5a 100644
--- a/common/Cargo.lock
+++ b/common/Cargo.lock
@@ -3,6 +3,21 @@
 version = 3
 
 [[package]]
+name = "addr2line"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
 name = "aho-corasick"
 version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -34,9 +49,9 @@
 
 [[package]]
 name = "anstream"
-version = "0.6.14"
+version = "0.6.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -49,33 +64,33 @@
 
 [[package]]
 name = "anstyle"
-version = "1.0.7"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.0.3"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
+checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
 dependencies = [
  "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.3"
+version = "3.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
 dependencies = [
  "anstyle",
  "windows-sys 0.52.0",
@@ -83,15 +98,26 @@
 
 [[package]]
 name = "anyhow"
-version = "1.0.83"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 
 [[package]]
-name = "arbitrary"
-version = "1.3.2"
+name = "async-trait"
+version = "0.1.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
 
 [[package]]
 name = "autocfg"
@@ -100,31 +126,92 @@
 checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
 
 [[package]]
-name = "bit-set"
-version = "0.5.3"
+name = "axum"
+version = "0.6.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
+checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
 dependencies = [
- "bit-vec",
+ "async-trait",
+ "axum-core",
+ "bitflags 1.3.2",
+ "bytes",
+ "futures-util",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "hyper 0.14.30",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper 0.1.2",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
 ]
 
 [[package]]
-name = "bit-vec"
-version = "0.6.3"
+name = "axum-core"
+version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "mime",
+ "rustversion",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
 [[package]]
 name = "bitflags"
-version = "2.5.0"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 
 [[package]]
 name = "bstr"
-version = "1.9.1"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
+checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
 dependencies = [
  "memchr",
  "serde",
@@ -152,9 +239,9 @@
 
 [[package]]
 name = "bytes"
-version = "1.6.0"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
 
 [[package]]
 name = "cast"
@@ -164,14 +251,9 @@
 
 [[package]]
 name = "cc"
-version = "1.0.97"
+version = "1.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
-dependencies = [
- "jobserver",
- "libc",
- "once_cell",
-]
+checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
 
 [[package]]
 name = "cesu8"
@@ -196,7 +278,7 @@
  "js-sys",
  "num-traits",
  "wasm-bindgen",
- "windows-targets 0.52.5",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -228,9 +310,9 @@
 
 [[package]]
 name = "clap"
-version = "4.5.4"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -238,9 +320,9 @@
 
 [[package]]
 name = "clap_builder"
-version = "4.5.2"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
 dependencies = [
  "anstream",
  "anstyle",
@@ -250,9 +332,9 @@
 
 [[package]]
 name = "clap_derive"
-version = "4.5.4"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -262,9 +344,9 @@
 
 [[package]]
 name = "clap_lex"
-version = "0.7.0"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
 
 [[package]]
 name = "cmd_runner"
@@ -277,15 +359,17 @@
  "globset",
  "log",
  "owo-colors",
+ "serde",
+ "serde_json",
  "shell-escape",
  "xshell",
 ]
 
 [[package]]
 name = "colorchoice"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
 
 [[package]]
 name = "combine"
@@ -298,6 +382,16 @@
 ]
 
 [[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
 name = "core-foundation-sys"
 version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -354,9 +448,9 @@
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.12"
+version = "0.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
 dependencies = [
  "crossbeam-utils",
 ]
@@ -391,9 +485,9 @@
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
 
 [[package]]
 name = "crunchy"
@@ -402,65 +496,52 @@
 checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
 
 [[package]]
-name = "derive_fuzz_example"
-version = "0.1.0"
-dependencies = [
- "arbitrary",
- "derive_fuzztest",
- "libfuzzer-sys",
- "quickcheck",
-]
-
-[[package]]
-name = "derive_fuzztest"
-version = "0.1.0"
-dependencies = [
- "arbitrary",
- "derive_fuzztest_macro",
- "proptest",
- "proptest-arbitrary-interop",
- "quickcheck",
-]
-
-[[package]]
-name = "derive_fuzztest_macro"
-version = "0.1.0"
-dependencies = [
- "derive_fuzztest",
- "pretty_assertions",
- "prettyplease",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "diff"
-version = "0.1.13"
+name = "directories"
+version = "5.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
 
 [[package]]
 name = "either"
-version = "1.11.0"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
 
 [[package]]
-name = "env_logger"
-version = "0.8.4"
+name = "encoding_rs"
+version = "0.8.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
+checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
 dependencies = [
- "log",
- "regex",
+ "cfg-if",
 ]
 
 [[package]]
-name = "errno"
-version = "0.3.8"
+name = "equivalent"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -492,6 +573,80 @@
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
 name = "getrandom"
 version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -503,6 +658,12 @@
 ]
 
 [[package]]
+name = "gimli"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
+
+[[package]]
 name = "glob"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -522,6 +683,25 @@
 ]
 
 [[package]]
+name = "h2"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http 1.1.0",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
 name = "half"
 version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -541,6 +721,12 @@
 ]
 
 [[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
 name = "heck"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -553,6 +739,176 @@
 checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
 
 [[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http 1.1.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.1",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "0.14.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http 0.2.12",
+ "http-body 0.4.6",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http 1.1.0",
+ "http-body 1.0.1",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
+dependencies = [
+ "futures-util",
+ "http 1.1.0",
+ "hyper 1.4.1",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper 1.4.1",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.1.0",
+ "http-body 1.0.1",
+ "hyper 1.4.1",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
 name = "iana-time-zone"
 version = "0.1.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -576,6 +932,32 @@
 ]
 
 [[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
 name = "is-terminal"
 version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -588,9 +970,9 @@
 
 [[package]]
 name = "is_terminal_polyfill"
-version = "1.70.0"
+version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 
 [[package]]
 name = "itertools"
@@ -609,9 +991,9 @@
 
 [[package]]
 name = "java-locator"
-version = "0.1.5"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90003f2fd9c52f212c21d8520f1128da0080bad6fff16b68fe6e7f2f0c3780c2"
+checksum = "d2abecabd9961c5e01405a6426687fcf1bd94a269927137e4c3cc1a7419b93fd"
 dependencies = [
  "glob",
  "lazy_static",
@@ -642,15 +1024,6 @@
 checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
 
 [[package]]
-name = "jobserver"
-version = "0.1.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
-dependencies = [
- "libc",
-]
-
-[[package]]
 name = "js-sys"
 version = "0.3.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -661,29 +1034,18 @@
 
 [[package]]
 name = "lazy_static"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 dependencies = [
- "spin 0.5.2",
+ "spin",
 ]
 
 [[package]]
 name = "libc"
-version = "0.2.154"
+version = "0.2.155"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
-
-[[package]]
-name = "libfuzzer-sys"
-version = "0.4.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
-dependencies = [
- "arbitrary",
- "cc",
- "once_cell",
-]
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 
 [[package]]
 name = "libloading"
@@ -696,16 +1058,20 @@
 ]
 
 [[package]]
-name = "libm"
-version = "0.2.8"
+name = "libredox"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.6.0",
+ "libc",
+]
 
 [[package]]
 name = "license"
-version = "3.3.1"
+version = "3.4.0+3.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bba2f02ee1d13cd4bea565658939cd851d70e391f34f7c27b45b2077df3a2e4"
+checksum = "a7da1e0d845faf299a9fe5f201a918a0dc0d5fc22c7b9580a6a23fed3a912b37"
 dependencies = [
  "reword",
  "serde",
@@ -714,15 +1080,15 @@
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.13"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
 name = "lock_adapter"
 version = "0.1.0"
 dependencies = [
- "spin 0.9.8",
+ "spin",
 ]
 
 [[package]]
@@ -737,15 +1103,27 @@
 
 [[package]]
 name = "log"
-version = "0.4.21"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
 
 [[package]]
 name = "memchr"
-version = "2.7.2"
+version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 
 [[package]]
 name = "minimal-lexical"
@@ -754,6 +1132,44 @@
 checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
+name = "miniz_oxide"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "wasi",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
 name = "nom"
 version = "7.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -770,7 +1186,32 @@
 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
- "libm",
+]
+
+[[package]]
+name = "oauth"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "axum",
+ "directories",
+ "hex",
+ "log",
+ "open",
+ "rand",
+ "reqwest",
+ "serde",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "object"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e"
+dependencies = [
+ "memchr",
 ]
 
 [[package]]
@@ -781,9 +1222,69 @@
 
 [[package]]
 name = "oorandom"
-version = "11.1.3"
+version = "11.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
+checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9"
+
+[[package]]
+name = "open"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8"
+dependencies = [
+ "pathdiff",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "openssl"
+version = "0.10.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
 [[package]]
 name = "owo-colors"
@@ -792,10 +1293,83 @@
 checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
 
 [[package]]
-name = "plotters"
-version = "0.3.5"
+name = "parking_lot"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "plotters"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3"
 dependencies = [
  "num-traits",
  "plotters-backend",
@@ -806,15 +1380,15 @@
 
 [[package]]
 name = "plotters-backend"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609"
+checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7"
 
 [[package]]
 name = "plotters-svg"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab"
+checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705"
 dependencies = [
  "plotters-backend",
 ]
@@ -846,81 +1420,15 @@
 checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
 [[package]]
-name = "pretty_assertions"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
-dependencies = [
- "diff",
- "yansi",
-]
-
-[[package]]
-name = "prettyplease"
-version = "0.2.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
-dependencies = [
- "proc-macro2",
- "syn",
-]
-
-[[package]]
 name = "proc-macro2"
-version = "1.0.82"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
-name = "proptest"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf"
-dependencies = [
- "bit-set",
- "bit-vec",
- "bitflags",
- "lazy_static",
- "num-traits",
- "rand",
- "rand_chacha",
- "rand_xorshift",
- "regex-syntax",
- "rusty-fork",
- "tempfile",
- "unarray",
-]
-
-[[package]]
-name = "proptest-arbitrary-interop"
-version = "0.1.0"
-source = "git+https://github.com/brson/proptest-arbitrary-interop.git?branch=incorrect-format#9ae407e9805feb109b3d49cc737166bda7e698c3"
-dependencies = [
- "arbitrary",
- "proptest",
-]
-
-[[package]]
-name = "quick-error"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
-
-[[package]]
-name = "quickcheck"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
-dependencies = [
- "env_logger",
- "log",
- "rand",
-]
-
-[[package]]
 name = "quote"
 version = "1.0.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -960,15 +1468,6 @@
 ]
 
 [[package]]
-name = "rand_xorshift"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
-dependencies = [
- "rand_core",
-]
-
-[[package]]
 name = "rayon"
 version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -989,10 +1488,30 @@
 ]
 
 [[package]]
-name = "regex"
-version = "1.10.4"
+name = "redox_syscall"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1002,9 +1521,9 @@
 
 [[package]]
 name = "regex-automata"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1013,9 +1532,53 @@
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+
+[[package]]
+name = "reqwest"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 1.1.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.4.1",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper 1.0.1",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
 
 [[package]]
 name = "reword"
@@ -1027,12 +1590,33 @@
 ]
 
 [[package]]
+name = "ring"
+version = "0.17.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "spin",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
 name = "rustix"
 version = "0.38.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
 dependencies = [
- "bitflags",
+ "bitflags 2.6.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -1040,18 +1624,52 @@
 ]
 
 [[package]]
-name = "rusty-fork"
-version = "0.3.0"
+name = "rustls"
+version = "0.23.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
+checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
 dependencies = [
- "fnv",
- "quick-error",
- "tempfile",
- "wait-timeout",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
 ]
 
 [[package]]
+name = "rustls-pemfile"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+dependencies = [
+ "base64",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+
+[[package]]
 name = "ryu"
 version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1067,25 +1685,57 @@
 ]
 
 [[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
 name = "scopeguard"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
-name = "serde"
-version = "1.0.200"
+name = "security-framework"
+version = "2.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.6.0",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.204"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.200"
+version = "1.0.204"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
+checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1094,9 +1744,9 @@
 
 [[package]]
 name = "serde_json"
-version = "1.0.116"
+version = "1.0.120"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
+checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
 dependencies = [
  "itoa",
  "ryu",
@@ -1104,16 +1754,66 @@
 ]
 
 [[package]]
+name = "serde_path_to_error"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
 name = "shell-escape"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
 
 [[package]]
-name = "spin"
-version = "0.5.2"
+name = "signal-hook-registry"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
 
 [[package]]
 name = "spin"
@@ -1131,10 +1831,16 @@
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
-name = "syn"
-version = "2.0.61"
+name = "subtle"
+version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1142,6 +1848,39 @@
 ]
 
 [[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
 name = "tempfile"
 version = "3.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1155,18 +1894,18 @@
 
 [[package]]
 name = "thiserror"
-version = "1.0.60"
+version = "1.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
+checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.60"
+version = "1.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
+checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1184,10 +1923,142 @@
 ]
 
 [[package]]
-name = "unarray"
-version = "0.1.4"
+name = "tinyvec"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.39.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
+dependencies = [
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
 
 [[package]]
 name = "unicode-ident"
@@ -1196,27 +2067,50 @@
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
 [[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
 name = "unicode-segmentation"
 version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
 
 [[package]]
-name = "utf8parse"
-version = "0.2.1"
+name = "untrusted"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
 
 [[package]]
-name = "wait-timeout"
-version = "0.2.0"
+name = "url"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
 dependencies = [
- "libc",
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
 ]
 
 [[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
 name = "walkdir"
 version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1227,6 +2121,15 @@
 ]
 
 [[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1258,6 +2161,18 @@
 ]
 
 [[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
 name = "wasm-bindgen-macro"
 version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1333,7 +2248,22 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-targets 0.52.5",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
 ]
 
 [[package]]
@@ -1347,11 +2277,20 @@
 
 [[package]]
 name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
 version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.5",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -1371,18 +2310,33 @@
 
 [[package]]
 name = "windows-targets"
-version = "0.52.5"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.5",
- "windows_aarch64_msvc 0.52.5",
- "windows_i686_gnu 0.52.5",
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
  "windows_i686_gnullvm",
- "windows_i686_msvc 0.52.5",
- "windows_x86_64_gnu 0.52.5",
- "windows_x86_64_gnullvm 0.52.5",
- "windows_x86_64_msvc 0.52.5",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
 [[package]]
@@ -1393,9 +2347,15 @@
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.52.5"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -1405,9 +2365,15 @@
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.52.5"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -1417,15 +2383,21 @@
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.5"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
 
 [[package]]
 name = "windows_i686_gnullvm"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -1435,9 +2407,15 @@
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.52.5"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -1447,9 +2425,15 @@
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.5"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -1459,9 +2443,15 @@
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.5"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -1471,9 +2461,25 @@
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.52.5"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
 
 [[package]]
 name = "xshell"
@@ -1491,7 +2497,7 @@
 checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852"
 
 [[package]]
-name = "yansi"
-version = "0.5.1"
+name = "zeroize"
+version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
diff --git a/common/Cargo.toml b/common/Cargo.toml
index 9e8972b..30689fc 100644
--- a/common/Cargo.toml
+++ b/common/Cargo.toml
@@ -2,11 +2,9 @@
 members = [
     "build_scripts",
     "cmd_runner",
-    "derive_fuzztest",
-    "derive_fuzztest/fuzz",
-    "derive_fuzztest_macro",
     "handle_map",
     "lock_adapter",
+    "oauth",
     "pourover",
     "pourover_macro",
 ]
@@ -32,8 +30,6 @@
 [workspace.dependencies]
 # local crates
 cmd_runner = { path = "cmd_runner" }
-derive_fuzztest = { path = "derive_fuzztest" }
-derive_fuzztest_macro = { path = "derive_fuzztest_macro" }
 lock_adapter = { path = "lock_adapter" }
 handle_map = { path = "handle_map" }
 pourover = { path = "pourover" }
@@ -42,7 +38,7 @@
 # from crates.io
 anyhow = "1.0.75"
 arbitrary = "1.3.2"
-clap = { version = "4.4.11", features = ["derive"] }
+clap = { version = "4.5.13", features = ["derive"] }
 criterion = { version = "0.5.1", features = ["html_reports"] }
 jni = "0.21.1"
 lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
@@ -51,18 +47,22 @@
 pretty_assertions = "1.4.0"
 prettyplease = "0.2.16"
 proc-macro2 = "1.0"
-proptest = "1.4.0"
-proptest-arbitrary-interop = { git = "https://github.com/brson/proptest-arbitrary-interop.git", branch = "incorrect-format" }
 quickcheck = "1.0.3"
 quote = "1.0"
 spin = { version = "0.9.8", features = ["once", "lock_api", "rwlock"] }
 syn = { version = "2.0", features = ["full"] }
 xshell = "0.2.6"
+hex = "0.4.3"
+log = "0.4.22"
+rand = "0.8.5"
+reqwest = "0.12.5"
+tokio = { version = "1.39.1", features = ["full"] }
 
 [workspace.package]
 version = "0.1.0"
 edition = "2021"
 publish = false
+license = "Apache-2.0"
 
 [profile.test]
 # speed up test execution
diff --git a/common/build_scripts/Cargo.toml b/common/build_scripts/Cargo.toml
index 8f9d217..35efdb8 100644
--- a/common/build_scripts/Cargo.toml
+++ b/common/build_scripts/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 rust-version = "1.71.0"
 
 [dependencies]
diff --git a/common/cmd_runner/Cargo.toml b/common/cmd_runner/Cargo.toml
index 435b29f..189d607 100644
--- a/common/cmd_runner/Cargo.toml
+++ b/common/cmd_runner/Cargo.toml
@@ -3,15 +3,17 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [dependencies]
 anyhow = "1.0.64"
-shell-escape = "0.1.5"
-owo-colors = "3.5.0"
-xshell = "0.2.6"
-clap = { version = "4.5.4", features = ["derive"] }
-file-header = "0.1.2"
 chrono = "0.4.37"
-log = "0.4.21"
+clap = { version = "4.5.13", features = ["derive"] }
+file-header = "0.1.2"
 globset = "0.4.14"
-
+log = "0.4.21"
+owo-colors = "3.5.0"
+serde = { version = "1.0.203", features = ["derive"] }
+serde_json = "1.0.118"
+shell-escape = "0.1.5"
+xshell = "0.2.6"
diff --git a/common/cmd_runner/src/cargo_workspace.rs b/common/cmd_runner/src/cargo_workspace.rs
index 2b6840e..9bc3f27 100644
--- a/common/cmd_runner/src/cargo_workspace.rs
+++ b/common/cmd_runner/src/cargo_workspace.rs
@@ -40,9 +40,9 @@
 #[derive(clap::Args, Debug, Clone, Default)]
 pub struct CargoOptions {
     #[arg(long, help = "whether to run cargo with --locked")]
-    locked: bool,
+    pub locked: bool,
     #[arg(long, help = "gather coverage metrics")]
-    coverage: bool,
+    pub coverage: bool,
 }
 
 impl CargoOptions {
diff --git a/common/cmd_runner/src/fuzzers.rs b/common/cmd_runner/src/fuzzers.rs
new file mode 100644
index 0000000..2d0ca3b
--- /dev/null
+++ b/common/cmd_runner/src/fuzzers.rs
@@ -0,0 +1,114 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use std::{
+    collections::HashMap,
+    path::{self, PathBuf},
+};
+
+use serde::Deserialize;
+use xshell::{cmd, Shell};
+
+/// Partial structure for parsing the output of `cargo metadata --format-version=1`.
+///
+/// This doesn't contain all the fields in `cargo metadata`, just the ones we need.
+#[derive(Deserialize)]
+struct CargoMetadata {
+    packages: Vec<CargoMetadataPackage>,
+}
+
+#[derive(Deserialize)]
+struct CargoMetadataPackage {
+    manifest_path: String,
+    metadata: Option<HashMap<String, serde_json::Value>>,
+    targets: Vec<CargoMetadataPackageTarget>,
+}
+
+#[derive(Deserialize)]
+struct CargoMetadataPackageTarget {
+    name: String,
+    kind: Vec<String>,
+}
+
+/// A `bin` target defined for fuzzing. See [`iter_fuzz_binaries`].
+#[derive(Debug)]
+pub struct FuzzTarget {
+    pub fuzz_dir: PathBuf,
+    pub target_name: String,
+}
+
+impl FuzzTarget {
+    /// Run the fuzz target using `cargo fuzz run`.
+    pub fn run(&self, sh: &xshell::Shell) -> xshell::Result<()> {
+        let FuzzTarget {
+            fuzz_dir,
+            target_name,
+        } = self;
+        cmd!(
+            sh,
+            "cargo +nightly fuzz run --fuzz-dir {fuzz_dir} {target_name} -- -runs=1000000 -max_total_time=30"
+        )
+        .run()
+    }
+}
+
+/// Create an iterator over all of the fuzz targets defined in the workspace at `root`.
+///
+/// This iterator discovers targets using `cargo metadata`. It looks for crates with the metadata
+/// `cargo-fuzz = true` in its Cargo.toml, and looks for `[[bin]]` targets in all of those crates.
+pub fn iter_fuzz_binaries(root: &path::Path) -> anyhow::Result<impl Iterator<Item = FuzzTarget>> {
+    let sh = Shell::new()?;
+    sh.change_dir(root);
+    let cargo_metadata = cmd!(sh, "cargo metadata --no-deps --format-version=1").read()?;
+    let metadata_json: CargoMetadata = serde_json::from_str(&cargo_metadata)?;
+    let packages = metadata_json.packages.into_iter().filter_map(|package| {
+        let metadata_map = package.metadata.as_ref()?;
+        metadata_map
+            .get("cargo-fuzz")?
+            .as_bool()?
+            .then_some(package)
+    });
+    Ok(packages.flat_map(|package| {
+        let fuzz_manifest = PathBuf::from(&package.manifest_path);
+        let fuzz_dir = fuzz_manifest
+            .parent()
+            .expect("Fuzz manifest should have a parent directory")
+            .to_owned();
+        package
+            .targets
+            .into_iter()
+            .filter(|t| t.kind.contains(&String::from("bin")))
+            .map(move |target| FuzzTarget {
+                fuzz_dir: fuzz_dir.clone(),
+                target_name: target.name.to_string(),
+            })
+    }))
+}
+
+/// Runs all of the fuzz targets defined within the workspace `root`.
+pub fn run_workspace_fuzz_targets(root: &path::Path) -> anyhow::Result<()> {
+    log::info!("Running rust fuzzers");
+    let sh = Shell::new()?;
+    sh.change_dir(root);
+    let fuzz_targets: Vec<_> = iter_fuzz_binaries(root)?.collect();
+    log::info!(
+        "Fuzzing on {} targets. This may take up to {} seconds.",
+        fuzz_targets.len(),
+        30 * fuzz_targets.len()
+    );
+    for fuzz_target in fuzz_targets {
+        fuzz_target.run(&sh)?;
+    }
+    Ok(())
+}
diff --git a/common/cmd_runner/src/lib.rs b/common/cmd_runner/src/lib.rs
index 8c45df8..24b4427 100644
--- a/common/cmd_runner/src/lib.rs
+++ b/common/cmd_runner/src/lib.rs
@@ -17,6 +17,7 @@
 use std::{collections, env, ffi, io, io::BufRead, path, process, thread};
 
 pub mod cargo_workspace;
+pub mod fuzzers;
 pub mod license_checker;
 
 pub fn run_cmd_shell(
diff --git a/common/deny.toml b/common/deny.toml
index bd3f18b..ff83d5e 100644
--- a/common/deny.toml
+++ b/common/deny.toml
@@ -71,6 +71,7 @@
 allow = [
     "MIT",
     "Apache-2.0",
+    "BSD-3-Clause",
     "Unicode-DFS-2016",
     "ISC",
 ]
@@ -85,6 +86,10 @@
     # Each entry is the crate and version constraint, and its specific allow
     # list
 
+
+    # "Reciprocal" licensed crate pulled directly from crates.io without modifications
+    # Important: Update https://third-party-mirror.googlesource.com/option-ext/ if you update this version
+    { allow = ["MPL-2.0"], name = "option-ext", version = "0.2.0" },
 ]
 
 # Some crates don't have (easily) machine readable licensing information,
@@ -103,8 +108,8 @@
 # and the crate will be checked normally, which may produce warnings or errors
 # depending on the rest of your configuration
 #license-files = [
-    # Each entry is a crate relative path, and the (opaque) hash of its contents
-    #{ path = "LICENSE", hash = 0xbd0eed23 }
+# Each entry is a crate relative path, and the (opaque) hash of its contents
+#{ path = "LICENSE", hash = 0xbd0eed23 }
 #]
 
 [[licenses.clarify]]
@@ -121,7 +126,7 @@
 # published to private registries.
 # To see how to mark a crate as unpublished (to the official registry),
 # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
-ignore = true
+ignore = false
 # One or more private registries that you might publish crates to, if a crate
 # is only published to private registries, and ignore is true, the crate will
 # not have its license(s) checked
diff --git a/common/derive_fuzztest/Cargo.toml b/common/derive_fuzztest/Cargo.toml
deleted file mode 100644
index 6a22ce5..0000000
--- a/common/derive_fuzztest/Cargo.toml
+++ /dev/null
@@ -1,24 +0,0 @@
-[package]
-name = "derive_fuzztest"
-version.workspace = true
-edition.workspace = true
-publish.workspace = true
-
-[dependencies]
-arbitrary.workspace = true
-derive_fuzztest_macro.workspace = true
-proptest = { workspace = true, optional = true }
-proptest-arbitrary-interop = { workspace = true, optional = true }
-quickcheck = { workspace = true, optional = true }
-
-[features]
-default = ["quickcheck"]
-quickcheck = ["dep:quickcheck", "derive_fuzztest_macro/quickcheck"]
-proptest = [
-    "dep:proptest",
-    "dep:proptest-arbitrary-interop",
-    "derive_fuzztest_macro/proptest",
-]
-
-[lints]
-workspace = true
diff --git a/common/derive_fuzztest/fuzz/.gitignore b/common/derive_fuzztest/fuzz/.gitignore
deleted file mode 100644
index b94a8f4..0000000
--- a/common/derive_fuzztest/fuzz/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/corpus
\ No newline at end of file
diff --git a/common/derive_fuzztest/fuzz/Cargo.toml b/common/derive_fuzztest/fuzz/Cargo.toml
deleted file mode 100644
index 1f493b6..0000000
--- a/common/derive_fuzztest/fuzz/Cargo.toml
+++ /dev/null
@@ -1,16 +0,0 @@
-[package]
-name = "derive_fuzz_example"
-version.workspace = true
-edition.workspace = true
-publish.workspace = true
-
-[package.metadata]
-cargo-fuzz = true
-
-[dependencies]
-arbitrary.workspace = true
-derive_fuzztest.workspace = true
-quickcheck.workspace = true
-
-[target.'cfg(fuzzing)'.dependencies]
-libfuzzer-sys.workspace = true
diff --git a/common/derive_fuzztest/fuzz/src/bin/arbitrary.rs b/common/derive_fuzztest/fuzz/src/bin/arbitrary.rs
deleted file mode 100644
index 8f8d77b..0000000
--- a/common/derive_fuzztest/fuzz/src/bin/arbitrary.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2024 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#![cfg_attr(fuzzing, no_main)]
-
-use arbitrary::{Arbitrary, Unstructured};
-use derive_fuzztest::fuzztest;
-
-#[derive(Debug, Clone)]
-pub struct SmallU8(u8);
-
-impl<'a> Arbitrary<'a> for SmallU8 {
-    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
-        Ok(SmallU8(u.int_in_range(0..=127)?))
-    }
-}
-
-#[fuzztest]
-pub fn test(a: SmallU8, b: SmallU8) {
-    let _ = a.0 + b.0; // Succeeds because our custom arbitrary impl only generates 0-127 so it never overflows
-}
diff --git a/common/derive_fuzztest/src/lib.rs b/common/derive_fuzztest/src/lib.rs
deleted file mode 100644
index c0dd11c..0000000
--- a/common/derive_fuzztest/src/lib.rs
+++ /dev/null
@@ -1,141 +0,0 @@
-// Copyright 2024 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Derive macros that generates both a fuzz target for use with `cargo fuzz`, and a property test
-//! (via `quickcheck` or `proptest`) for use with `cargo test`.
-//!
-//! The reason for having both is that property testing allows for quick iteration to make sure the
-//! test works, and can be checked in presubmit CI, while fuzzing can test the input space more
-//! exhaustively and run continuously.
-//!
-//! # Example
-//!
-//! ```no_run
-//! #![cfg_attr(fuzzing, no_main)]
-//!
-//! #[derive_fuzztest::fuzztest]
-//! fn transitive_ord(a: u32, b: u32, c: u32) {
-//!     if a >= b && b >= c {
-//!         assert!(a >= c);
-//!     }
-//!     if a <= b && b <= c {
-//!         assert!(a <= c);
-//!     }
-//! }
-//!
-//! #[test]
-//! fn additional_test_here() {
-//!     /* ... */
-//! }
-//! ```
-//!
-//! # Usage
-//!
-//!
-//! Run the generated property tests
-//! ```sh
-//! cargo test
-//! ```
-//!
-//! Run continuous fuzzing
-//! ```sh
-//! cargo +nightly fuzz run <binary name>
-//! ```
-//!
-//! # Crate structure
-//!
-//! If you use `#[fuzz]` or `#[fuzztest]`, the fuzz target imposes the following requirements:
-//!
-//! * The target must be in a separate `[[bin]]` target that only contains a single fuzz target.
-//! * The crate containing the bin target has `[package.metadata] cargo-fuzz = true`
-//! * The bin target is annotated with `#![cfg_attr(fuzzing, no_main)]`
-//!
-//! The recommended structure for your crate `foo` is to put your tests under `foo/fuzz/src/bin`:
-//!
-//! ```text
-//! foo
-//! ├── fuzz
-//! │   ├── src
-//! │   │   └── bin
-//! │   │       └── fuzz_target_1.rs
-//! │   └── Cargo.toml
-//! ├── src
-//! │   └── [project source]
-//! └── Cargo.toml
-//! ```
-//!
-//! This is different from the default structure generated by `cargo fuzz init` or `cargo fuzz add`
-//! so that we can take advantage of [target
-//! auto-discovery](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#target-auto-discovery).
-//! If you prefer, the default structure generated by `cargo fuzz` can also work, but make sure you
-//! remove `test = false` from the generated target in `Cargo.toml`.
-//!
-//! You will also need to declare a dependency on the `libfuzzer-sys` crate, but only if fuzzing is
-//! requested:
-//!
-//! ```toml
-//! [target.'cfg(fuzzing)'.dependencies]
-//! libfuzzer-sys = "*"
-//! ```
-//!
-//! (The reason for this conditional dependency is that `libfuzzer-sys` injects a main function to
-//! the resulting binary, and there will be linking failures if we link that in without defining a
-//! corresponding `fuzz_target`.)
-//!
-//! # Features
-//!
-//! * `quickcheck` (default) — Enable generation of
-//!   [`quickcheck`](https://docs.rs/quickcheck/latest/quickcheck/) property tests.
-//! * `proptest` — Enable generation of [`proptest`](https://docs.rs/proptest/latest/proptest/)
-//!   property tests.
-//!
-//! #### See also
-//! * [Announcing Better Support for Fuzzing with Structured Inputs in
-//!   Rust](https://fitzgeraldnick.com/2020/01/16/better-support-for-fuzzing-structured-inputs-in-rust.html#how-is-all-this-different-from-quickcheck-and-proptest)
-//! * [Bridging Fuzzing and Property
-//!   Testing](https://blog.yoshuawuyts.com/bridging-fuzzing-and-property-testing/)
-
-pub use derive_fuzztest_macro::{fuzz, fuzztest, proptest};
-
-#[doc(hidden)]
-pub mod reexport {
-    #[cfg(feature = "proptest")]
-    pub use proptest;
-    #[cfg(feature = "proptest")]
-    pub use proptest_arbitrary_interop;
-    #[cfg(feature = "quickcheck")]
-    pub use quickcheck;
-}
-
-#[cfg(feature = "quickcheck")]
-#[doc(hidden)]
-pub mod arbitrary_bridge {
-
-    /// Wrapper type that allows `arbitrary::Arbitrary` to be used as `quickcheck::Arbitrary`
-    #[derive(Debug, Clone)]
-    pub struct ArbitraryAdapter<T: for<'a> arbitrary::Arbitrary<'a>>(
-        pub Result<T, arbitrary::Error>,
-    );
-
-    impl<T> quickcheck::Arbitrary for ArbitraryAdapter<T>
-    where
-        T: for<'a> arbitrary::Arbitrary<'a> + Clone + 'static,
-    {
-        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-            let bytes = Vec::<u8>::arbitrary(g);
-            let mut unstructured = arbitrary::Unstructured::new(&bytes);
-            Self(T::arbitrary(&mut unstructured))
-        }
-    }
-}
diff --git a/common/derive_fuzztest_macro/Cargo.toml b/common/derive_fuzztest_macro/Cargo.toml
deleted file mode 100644
index 37b480a..0000000
--- a/common/derive_fuzztest_macro/Cargo.toml
+++ /dev/null
@@ -1,23 +0,0 @@
-[package]
-name = "derive_fuzztest_macro"
-version.workspace = true
-edition.workspace = true
-publish.workspace = true
-
-[dependencies]
-quote.workspace = true
-proc-macro2.workspace = true
-syn = { workspace = true, features = ["extra-traits"]}
-
-[dev-dependencies]
-derive_fuzztest.workspace = true
-pretty_assertions.workspace = true
-prettyplease.workspace = true
-
-[features]
-quickcheck = []
-proptest = []
-
-[lib]
-proc-macro = true
-doc = false
diff --git a/common/derive_fuzztest_macro/src/lib.rs b/common/derive_fuzztest_macro/src/lib.rs
deleted file mode 100644
index beb692a..0000000
--- a/common/derive_fuzztest_macro/src/lib.rs
+++ /dev/null
@@ -1,463 +0,0 @@
-// Copyright 2024 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Internal crate for use by [`derive_fuzztest`](../derive_fuzztest/index.html). See the
-//! documentation there for usage information.
-
-use proc_macro::TokenStream;
-use proc_macro2::TokenStream as TokenStream2;
-use quote::quote;
-use syn::{parse::Nothing, spanned::Spanned, ItemFn, Pat, PatType, Type};
-
-/// Define a fuzz test.
-///
-/// All input parameters of the given function must implement `arbitrary::Arbitrary`.
-///
-/// This macro derives new items based on the given function.
-/// 1. A `fuzz_target!` is generated that can be used with `cargo fuzz`.
-/// 2. Property tests (`quickcheck` or `proptest`, based on which features are enabled) are
-///    generated that can be tested using `cargo test`.
-///
-/// See the crate documentation [`derive_fuzztest`](../derive_fuzztest/index.html) for details.
-#[proc_macro_attribute]
-pub fn fuzztest(attr: TokenStream, item: TokenStream) -> TokenStream {
-    fuzztest_impl(attr.into(), item.into())
-        .unwrap_or_else(|e| e.into_compile_error())
-        .into()
-}
-
-fn fuzztest_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
-    syn::parse2::<Nothing>(attr)?;
-    let func = syn::parse2::<ItemFn>(item)?;
-    let fn_def = FunctionDefinition::parse(func)?;
-    let original_fn = &fn_def.func;
-    let fuzz_target = derive_fuzz_target(&fn_def);
-    let proptest_target = proptest::derive_proptest(&fn_def);
-    let quickcheck_target = quickcheck::derive_quickcheck(&fn_def);
-
-    Ok(quote! {
-        #[allow(unused)]
-        #original_fn
-        #fuzz_target
-        #proptest_target
-        #quickcheck_target
-    })
-}
-
-/// Define a fuzz target only without corresponding test.
-///
-/// All input parameters of the given function must implement `arbitrary::Arbitrary`.
-///
-/// This macro derives a `fuzz_target!` that can be used with `cargo fuzz`. If you wish to generate
-/// property tests that can be used with `cargo test` as well, use [`fuzztest`][macro@fuzztest].
-///
-/// See the crate documentation [`derive_fuzztest`](../derive_fuzztest/index.html) for details.
-#[proc_macro_attribute]
-pub fn fuzz(attr: TokenStream, item: TokenStream) -> TokenStream {
-    fuzz_impl(attr.into(), item.into())
-        .unwrap_or_else(|e| e.into_compile_error())
-        .into()
-}
-
-fn fuzz_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
-    syn::parse2::<Nothing>(attr)?;
-    let func = syn::parse2::<ItemFn>(item)?;
-    let fn_def = FunctionDefinition::parse(func)?;
-    let original_fn = &fn_def.func;
-    let fuzz_target = derive_fuzz_target(&fn_def);
-
-    Ok(quote! {
-        #[allow(unused)]
-        #original_fn
-        #fuzz_target
-    })
-}
-
-/// Define a property test.
-///
-/// This is similar to using `quickcheck!` or `proptest::proptest!` directly.
-///
-/// All input parameters of the given function must implement `arbitrary::Arbitrary`.
-///
-/// Unlike [`fuzztest`][macro@fuzztest], this macro does not have to be placed in a `[[bin]]` target
-/// and a single file can contain multiple of these tests. The generated tests can be run with
-/// `cargo test` as usual.
-#[proc_macro_attribute]
-pub fn proptest(attr: TokenStream, item: TokenStream) -> TokenStream {
-    proptest_impl(attr.into(), item.into())
-        .unwrap_or_else(|e| e.into_compile_error())
-        .into()
-}
-
-fn proptest_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
-    syn::parse2::<Nothing>(attr)?;
-    let func = syn::parse2::<ItemFn>(item)?;
-    let fn_def = FunctionDefinition::parse(func)?;
-    let original_fn = &fn_def.func;
-    let proptest_target = proptest::derive_proptest(&fn_def);
-
-    Ok(quote! {
-        #[allow(unused)]
-        #original_fn
-        #proptest_target
-    })
-}
-
-fn derive_fuzz_target(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
-    let FunctionDefinition { func, args, types } = fn_def;
-    let func_ident = &func.sig.ident;
-    quote! {
-        #[automatically_derived]
-        #[cfg(fuzzing)]
-        ::libfuzzer_sys::fuzz_target!(|args: ( #(#types),* )| {
-            let ( #(#args),* ) = args;  // https://github.com/rust-fuzz/libfuzzer/issues/77
-            #func_ident ( #(#args),* )
-        });
-
-        #[cfg(not(any(fuzzing, rust_analyzer)))]
-        fn main() {
-            ::std::unreachable!("Run this target with `cargo fuzz` or `cargo test` instead");
-        }
-    }
-}
-
-#[cfg(any(feature = "quickcheck", test))]
-mod quickcheck {
-    use crate::FunctionDefinition;
-    use quote::quote;
-
-    pub(crate) fn derive_quickcheck(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
-        let FunctionDefinition { func, args, types } = fn_def;
-        let func_ident = &func.sig.ident;
-        let adapted_types: Vec<_> = types
-            .iter()
-            .map(|ty| quote! { ArbitraryAdapter<#ty> })
-            .collect();
-        let arg_pattern: Vec<_> = args
-            .iter()
-            .map(|arg| quote! { ArbitraryAdapter(::core::result::Result::Ok(#arg)) })
-            .collect();
-        let test_name = quote::format_ident!("quickcheck_{func_ident}");
-        quote! {
-            #[automatically_derived]
-            #[test]
-            fn #test_name() {
-                use ::derive_fuzztest::reexport::quickcheck::TestResult;
-                use ::derive_fuzztest::arbitrary_bridge::ArbitraryAdapter;
-
-                fn inner(args: (#(#adapted_types),*)) -> TestResult {
-                    let (#(#arg_pattern),*) = args else { return TestResult::discard() };
-                    match ::std::panic::catch_unwind(move || {
-                        #func_ident ( #(#args),* );
-                    }) {
-                        ::core::result::Result::Ok(()) => TestResult::passed(),
-                        ::core::result::Result::Err(e) => TestResult::error(::std::format!("{e:?}")),
-                    }
-                }
-
-                ::derive_fuzztest::reexport::quickcheck::QuickCheck::new().tests(1024)
-                    .quickcheck(inner as fn(_) -> TestResult);
-            }
-        }
-    }
-}
-
-#[cfg(not(any(feature = "quickcheck", test)))]
-mod quickcheck {
-    use crate::FunctionDefinition;
-
-    pub(crate) fn derive_quickcheck(_fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
-        proc_macro2::TokenStream::default()
-    }
-}
-
-#[cfg(any(feature = "proptest", test))]
-mod proptest {
-    use crate::FunctionDefinition;
-    use quote::quote;
-    use syn::{Ident, Signature};
-
-    pub(crate) fn derive_proptest(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
-        let FunctionDefinition { func, args, types } = fn_def;
-        let func_attrs = &func.attrs;
-        let Signature {
-            constness,
-            asyncness,
-            unsafety,
-            abi,
-            fn_token,
-            ident,
-            generics,
-            paren_token: _,
-            inputs: _,
-            variadic: _,
-            output,
-        } = &func.sig;
-        let proptest_ident = Ident::new(&format!("proptest_{ident}"), ident.span());
-        quote! {
-            #[automatically_derived]
-            #[cfg(test)]
-            mod #proptest_ident {
-                use super::*;
-                use ::derive_fuzztest::reexport::proptest;
-                use ::derive_fuzztest::reexport::proptest_arbitrary_interop::arb;
-
-                proptest::proptest! {
-                    #![proptest_config(proptest::prelude::ProptestConfig {
-                        cases: 1024,
-                        failure_persistence: Some(Box::new(proptest::test_runner::FileFailurePersistence::WithSource("regression"))),
-                        ..Default::default()
-                    })]
-                    #[test]
-                    #(#func_attrs)*
-                    #constness #asyncness #unsafety #abi #fn_token #proptest_ident #generics ( args in arb::<(#(#types),*)>() ) #output {
-                        let (#(#args),*) = args;
-                        #ident ( #(#args),* );
-                    }
-                }
-            }
-        }
-    }
-}
-
-#[cfg(not(any(feature = "proptest", test)))]
-mod proptest {
-    use crate::FunctionDefinition;
-
-    pub(crate) fn derive_proptest(_fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
-        proc_macro2::TokenStream::default()
-    }
-}
-
-/// Representation of a function definition annotated with one of the attribute macros in this
-/// crate.
-struct FunctionDefinition {
-    func: ItemFn,
-    args: Vec<Pat>,
-    types: Vec<Type>,
-}
-
-impl FunctionDefinition {
-    pub fn parse(func: ItemFn) -> syn::Result<Self> {
-        let (args, types) = func
-            .sig
-            .inputs
-            .clone()
-            .into_iter()
-            .map(|arg| match arg {
-                syn::FnArg::Receiver(arg_receiver) => Err(syn::Error::new(
-                    arg_receiver.span(),
-                    "Receiver not supported",
-                )),
-                syn::FnArg::Typed(PatType {
-                    attrs: _,
-                    pat,
-                    colon_token: _,
-                    ty,
-                }) => Ok((*pat, *ty)),
-            })
-            .try_fold((Vec::new(), Vec::new()), |(mut args, mut types), result| {
-                result.map(|(arg, type_)| {
-                    args.push(arg);
-                    types.push(type_);
-                    (args, types)
-                })
-            })?;
-        Ok(Self { func, args, types })
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::{fuzz_impl, fuzztest_impl, proptest_impl};
-    use quote::quote;
-    use syn::parse_quote;
-
-    /// Assert that a token stream for a `syn::File` is the same as expected.
-    ///
-    /// Usage is similar to `assert_eq!`:
-    /// ```no_run
-    /// assert_syn_file!(
-    ///     macro_impl(quote! {
-    ///         fn foobar() {}
-    ///     }),
-    ///     quote! {
-    ///         fn macro_rewritten_foobar() {}
-    ///     }
-    /// );
-    /// ```
-    macro_rules! assert_syn_file {
-        ($actual:expr, $expected:expr) => {
-            let actual = syn::parse2::<syn::File>($actual).unwrap();
-            let expected: syn::File = $expected;
-            assert!(
-                actual == expected,
-                "{}",
-                pretty_assertions::StrComparison::new(
-                    &prettyplease::unparse(&expected),
-                    &prettyplease::unparse(&actual),
-                )
-            )
-        };
-    }
-
-    #[test]
-    fn test_fuzztest_expansion() {
-        assert_syn_file!(
-            fuzztest_impl(
-                quote! {},
-                quote! {
-                    fn foobar(input: &[u8]) {
-                        panic!("I am just a test")
-                    }
-                }
-            )
-            .unwrap(),
-            parse_quote! {
-                #[allow(unused)]
-                fn foobar(input: &[u8]) {
-                    panic!("I am just a test")
-                }
-
-                #[automatically_derived]
-                #[cfg(fuzzing)]
-                ::libfuzzer_sys::fuzz_target!(|args: (&[u8])| {
-                    let (input) = args;
-                    foobar(input)
-                });
-
-                #[cfg(not(any(fuzzing, rust_analyzer)))]
-                fn main() {
-                    ::std::unreachable!("Run this target with `cargo fuzz` or `cargo test` instead");
-                }
-
-                #[automatically_derived]
-                #[cfg(test)]
-                mod proptest_foobar {
-                    use super::*;
-                    use ::derive_fuzztest::reexport::proptest;
-                    use ::derive_fuzztest::reexport::proptest_arbitrary_interop::arb;
-                    proptest::proptest! {
-                        #![proptest_config(proptest::prelude::ProptestConfig {
-                            cases: 1024,
-                            failure_persistence: Some(Box::new(proptest::test_runner::FileFailurePersistence::WithSource("regression"))),
-                            ..Default::default()
-                        })]
-                        #[test]
-                        fn proptest_foobar(args in arb::<(&[u8])>()) {
-                            let (input) = args;
-                            foobar(input);
-                        }
-                    }
-                }
-
-                #[automatically_derived]
-                #[test]
-                fn quickcheck_foobar() {
-                    use ::derive_fuzztest::reexport::quickcheck::TestResult;
-                    use ::derive_fuzztest::arbitrary_bridge::ArbitraryAdapter;
-
-                    fn inner(args: (ArbitraryAdapter<&[u8]>)) -> TestResult {
-                        let (ArbitraryAdapter(::core::result::Result::Ok(input))) = args else {
-                            return TestResult::discard()
-                        };
-                        match ::std::panic::catch_unwind(move || {
-                            foobar(input);
-                        }) {
-                            ::core::result::Result::Ok(()) => TestResult::passed(),
-                            ::core::result::Result::Err(e) => TestResult::error(::std::format!("{e:?}")),
-                        }
-                    }
-                    ::derive_fuzztest::reexport::quickcheck::QuickCheck::new()
-                        .tests(1024)
-                        .quickcheck(inner as fn(_) -> TestResult);
-                }
-            }
-        );
-    }
-
-    #[test]
-    fn test_fuzz_expansion() {
-        assert_syn_file!(
-            fuzz_impl(
-                quote! {},
-                quote! {
-                    fn foobar(input: &[u8]) {
-                        panic!("I am just a test")
-                    }
-                }
-            )
-            .unwrap(),
-            parse_quote! {
-                #[allow(unused)]
-                fn foobar(input: &[u8]) {
-                    panic!("I am just a test")
-                }
-
-                #[automatically_derived]
-                #[cfg(fuzzing)]
-                ::libfuzzer_sys::fuzz_target!(|args: (&[u8])| {
-                    let (input) = args;
-                    foobar(input)
-                });
-
-                #[cfg(not(any(fuzzing, rust_analyzer)))]
-                fn main() {
-                    ::std::unreachable!("Run this target with `cargo fuzz` or `cargo test` instead");
-                }
-            }
-        );
-    }
-
-    #[test]
-    fn test_proptest_expansion() {
-        assert_syn_file!(
-            proptest_impl(
-                quote! {},
-                quote! {
-                    fn foobar(input: &[u8]) {
-                        panic!("I am just a test")
-                    }
-                }
-            )
-            .unwrap(),
-            parse_quote! {
-                #[allow(unused)]
-                fn foobar(input: &[u8]) {
-                    panic!("I am just a test")
-                }
-
-                #[automatically_derived]
-                #[cfg(test)]
-                mod proptest_foobar {
-                    use super::*;
-                    use ::derive_fuzztest::reexport::proptest;
-                    use ::derive_fuzztest::reexport::proptest_arbitrary_interop::arb;
-                    proptest::proptest! {
-                        #![proptest_config(proptest::prelude::ProptestConfig {
-                            cases: 1024,
-                            failure_persistence: Some(Box::new(proptest::test_runner::FileFailurePersistence::WithSource("regression"))),
-                            ..Default::default()
-                        })]
-                        #[test]
-                        fn proptest_foobar(args in arb::<(&[u8])>()) {
-                            let (input) = args;
-                            foobar(input);
-                        }
-                    }
-                }
-            }
-        );
-    }
-}
diff --git a/common/handle_map/Cargo.toml b/common/handle_map/Cargo.toml
index 13973d4..8db74f3 100644
--- a/common/handle_map/Cargo.toml
+++ b/common/handle_map/Cargo.toml
@@ -3,16 +3,17 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
 
 [dependencies]
+lazy_static.workspace = true
 lock_adapter.workspace = true
 
 [dev-dependencies]
 criterion.workspace = true
-lazy_static.workspace = true
 
 [[bench]]
 name = "benches"
diff --git a/common/handle_map/src/declare_handle_map.rs b/common/handle_map/src/declare_handle_map.rs
index 89078fa..8710275 100644
--- a/common/handle_map/src/declare_handle_map.rs
+++ b/common/handle_map/src/declare_handle_map.rs
@@ -42,9 +42,6 @@
 /// function `sample` which will print "Hello World".
 ///
 /// ```
-/// #[macro_use]///
-/// extern crate lazy_static;
-///
 /// use core::ops::Deref;
 /// use handle_map::{declare_handle_map, HandleMapDimensions, HandleLike};
 ///
@@ -105,7 +102,7 @@
                     " which references values of type `", ::core::stringify!($wrapped_type), "`."
                 )]
         pub mod $handle_module_name {
-            lazy_static! {
+            $crate::reexport::lazy_static::lazy_static! {
                 static ref GLOBAL_HANDLE_MAP: $crate::HandleMap<$wrapped_type> =
                 $crate::HandleMap::with_dimensions($map_dimension_provider);
             }
diff --git a/common/handle_map/src/lib.rs b/common/handle_map/src/lib.rs
index 72bb78a..43eeb19 100644
--- a/common/handle_map/src/lib.rs
+++ b/common/handle_map/src/lib.rs
@@ -29,6 +29,11 @@
 
 use shard::{HandleMapShard, ShardAllocationError};
 
+#[doc(hidden)]
+pub mod reexport {
+    pub use lazy_static;
+}
+
 /// An individual handle to be given out by a [`HandleMap`].
 /// This representation is untyped, and just a wrapper
 /// around a handle-id, in contrast to implementors of `HandleLike`.
diff --git a/common/lock_adapter/Cargo.toml b/common/lock_adapter/Cargo.toml
index 6e3174b..23234ad 100644
--- a/common/lock_adapter/Cargo.toml
+++ b/common/lock_adapter/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/common/pourover/Cargo.toml b/common/pourover/Cargo.toml
index eb03cdb..2ed5642 100644
--- a/common/pourover/Cargo.toml
+++ b/common/pourover/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/common/pourover/src/exception.rs b/common/pourover/src/exception.rs
new file mode 100644
index 0000000..24e7962
--- /dev/null
+++ b/common/pourover/src/exception.rs
@@ -0,0 +1,245 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Utilities for handling errors and propagating them into exceptions in Java.
+
+use jni::{
+    descriptors::Desc,
+    objects::{JClass, JThrowable},
+    JNIEnv,
+};
+use std::fmt::Debug;
+
+/// An error that can be thrown as a Java exception, or an error in the JNI layer.
+///
+/// This allows using Rust's error propagation mechanism (the `?` operator) to surface both JNI
+/// errors and Java throwables, and convert them to Java exceptions at the top level. At the top
+/// level of a JNI function implementation, it can use [`ThrowableJniResultExt::unwrap_or_throw`] to
+/// convert the error to Java exception, or terminate the JVM with a fatal error in the case of JNI
+/// errors.
+pub enum ThrowableJniError<'local> {
+    /// A java throwable object instance. The throwable can be received from the Java side, or
+    /// created using [`call_constructor`][crate::call_constructor].
+    JavaThrowable(JThrowable<'local>),
+    /// A [`jni::errors::Exception`], which contains the class and the message to be able to create
+    /// the exception to be thrown.
+    JavaException(jni::errors::Exception),
+    /// An error from the [`jni`] crate.
+    JniError(jni::errors::Error),
+}
+
+impl<'local> ThrowableJniError<'local> {
+    /// Throws the error as a Java exception.
+    ///
+    /// If the error is a `JavaThrowable` or a `JavaException`, it will be thrown on the given
+    /// `env`. If the error is a `JniError`, this will turn it into a [`JNIEnv::fatal_error`],
+    /// unless the error type is [`jni::errors::Error::JavaException`], in which case the error will
+    /// be ignored, allowing the already-thrown exception to remain in JVM.
+    ///
+    /// In typical usages, callers should return some default value immediately after calling this.
+    /// See the example in [`try_throwable`].
+    pub fn throw_on_jvm(self, env: &mut JNIEnv<'_>) {
+        match self {
+            ThrowableJniError::JavaThrowable(throwable) => match env.throw(throwable) {
+                Ok(()) => {}
+                Err(jni::errors::Error::JavaException) => {
+                    let _ = env.exception_describe();
+                    env.fatal_error("Throwing exception failed");
+                }
+                Err(_) => env.fatal_error("Throwing exception failed"),
+            },
+            ThrowableJniError::JavaException(exception) => match env.throw(exception) {
+                Ok(()) => {}
+                Err(jni::errors::Error::JavaException) => {
+                    let _ = env.exception_describe();
+                    env.fatal_error("Throwing exception failed");
+                }
+                Err(_) => env.fatal_error("Throwing exception failed"),
+            },
+            ThrowableJniError::JniError(err) => {
+                match err {
+                    jni::errors::Error::JavaException => {
+                        // Ignore the exception, it's already pending in the JVM
+                    }
+                    _ => env.fatal_error(format!("{err}")),
+                }
+            }
+        }
+    }
+}
+
+impl Debug for ThrowableJniError<'_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::JavaThrowable(_) => f.debug_tuple("JavaThrowable").finish(),
+            Self::JavaException(arg0) => f
+                .debug_struct("JavaException")
+                .field("class", &arg0.class)
+                .field("msg", &arg0.msg)
+                .finish(),
+            Self::JniError(arg0) => f.debug_tuple("JniError").field(arg0).finish(),
+        }
+    }
+}
+
+impl<'local> From<JThrowable<'local>> for ThrowableJniError<'local> {
+    fn from(value: JThrowable<'local>) -> Self {
+        Self::JavaThrowable(value)
+    }
+}
+
+impl From<jni::errors::Error> for ThrowableJniError<'_> {
+    fn from(value: jni::errors::Error) -> Self {
+        Self::JniError(value)
+    }
+}
+
+impl From<jni::errors::Exception> for ThrowableJniError<'_> {
+    fn from(value: jni::errors::Exception) -> Self {
+        Self::JavaException(value)
+    }
+}
+
+/// Extension trait for `Result<T, ThrowableJniError>`.
+pub trait ThrowableJniResultExt<T> {
+    /// Returns the contained `Ok` value, or throw an exception on the JVM and return the default.
+    ///
+    /// Consumes the `self` argument then, if `Ok`, returns the contained value. Otherwise, if
+    /// `Err`, the error will be thrown on the JVM using [`ThrowableJniError::throw_on_jvm`] and a
+    /// default value will be returned. The value returned by `default` typically does not matter,
+    /// because while there is an exception pending on the JVM, the return value from your JNI
+    /// method is ignored.
+    fn unwrap_or_throw(self, env: &mut JNIEnv<'_>) -> T
+    where
+        T: Default,
+        Self: Sized,
+    {
+        self.unwrap_or_throw_with_default(env, Default::default)
+    }
+
+    /// Returns the contained `Ok` value, or throw an exception on the JVM and return some default.
+    ///
+    /// Consumes the `self` argument then, if `Ok`, returns the contained value. Otherwise, if
+    /// `Err`, the error will be thrown on the JVM using [`ThrowableJniError::throw_on_jvm`] and the
+    /// value from `default` will be returned. The value returned by `default` typically does not
+    /// matter, because while there is an exception pending on the JVM, the return value from your
+    /// JNI method is ignored.
+    fn unwrap_or_throw_with_default(self, env: &mut JNIEnv<'_>, default: impl FnOnce() -> T) -> T;
+}
+
+impl<'local, T> ThrowableJniResultExt<T> for Result<T, ThrowableJniError<'local>> {
+    fn unwrap_or_throw_with_default(self, env: &mut JNIEnv<'_>, default: impl FnOnce() -> T) -> T {
+        match self {
+            Ok(v) => v,
+            Err(e) => {
+                e.throw_on_jvm(env);
+                default()
+            }
+        }
+    }
+}
+
+/// Creates a `NullPointerException` with a default error message.
+pub fn null_pointer_exception() -> jni::errors::Exception {
+    jni::errors::Exception {
+        class: "java/lang/NullPointerException".into(),
+        msg: "Null pointer".into(),
+    }
+}
+
+/// Creates a runtime exception with the given message.
+pub fn runtime_exception(msg: impl ToString) -> jni::errors::Exception {
+    jni::errors::Exception {
+        class: "java/lang/RuntimeException".into(),
+        msg: msg.to_string(),
+    }
+}
+
+/// Runs the given `func` and turns any [`ThrowableJniError`] into Java exceptions.
+///
+/// A convenience function that takes a closure `func`, runs it immediately, and calls
+/// [`ThrowableJniResultExt::unwrap_or_throw`] on the result. This allows the code inside the
+/// closure to propagate [`ThrowableJniErrors`][ThrowableJniError] using the `?` operator.
+///
+/// # Example
+///
+/// ```
+/// use pourover::{jni_method, exception::{runtime_exception, try_throwable}};
+/// use jni::{JNIEnv, objects::{JByteArray, JClass}, sys::{jboolean, JNI_TRUE}};
+///
+/// #[jni_method(package = "com.example", class = "Foo")]
+/// extern "system" fn nativeMaybeThrowException<'local>(
+///     mut env: JNIEnv<'local>,
+///     _cls: JClass<'local>,
+///     value: JByteArray<'local>,
+/// ) -> jboolean {
+///     try_throwable(&mut env, |env| {
+///         let value = env.convert_byte_array(value)?;
+///         if value == b"Throw exception" {
+///             Err(runtime_exception("Argument was \"throw exception\""))?
+///         }
+///         Ok(JNI_TRUE)
+///     })
+/// }
+/// ```
+pub fn try_throwable<'local, R: Default>(
+    env: &mut JNIEnv<'local>,
+    func: impl FnOnce(&mut JNIEnv<'local>) -> Result<R, ThrowableJniError<'local>>,
+) -> R {
+    func(env).unwrap_or_throw(env)
+}
+
+/// Extension trait on `jni::errors::Result<T>`.
+pub trait JniResultExt<T> {
+    /// Extracts an exception of the given class from a [`jni::errors::Result`].
+    ///
+    /// If `self` is `Err(JavaException)` and the pending exception on the JVM matches the given
+    /// `desc`, the exception will be cleared on the JVM and the associated throwable will be
+    /// returned.
+    ///
+    /// # Returns
+    /// A nested result object. If `self` contains an exception matching the given `desc`, this will
+    /// return `Ok(Err(throwable))`. If `self` is `Ok`, this will return `Ok(Ok(value))`. Otherwise
+    /// if there are other errors or non-matching exceptions, the error will be propagated as
+    /// `Err(jni_error)`.
+    fn extract_exception<'local>(
+        self,
+        env: &mut JNIEnv<'local>,
+        desc: impl Desc<'local, JClass<'local>>,
+    ) -> jni::errors::Result<Result<T, JThrowable<'local>>>;
+}
+
+impl<T> JniResultExt<T> for jni::errors::Result<T> {
+    fn extract_exception<'local>(
+        self,
+        env: &mut JNIEnv<'local>,
+        desc: impl Desc<'local, JClass<'local>>,
+    ) -> jni::errors::Result<Result<T, JThrowable<'local>>> {
+        match self {
+            Ok(v) => Ok(Ok(v)),
+            Err(jni::errors::Error::JavaException) => {
+                let throwable = env.exception_occurred()?;
+                // Need to clear the exception ahead of time, otherwise `is_instance_of` will fail
+                env.exception_clear()?;
+                if env.is_instance_of(&throwable, desc)? {
+                    Ok(Err(throwable))
+                } else {
+                    env.throw(throwable)?;
+                    Err(jni::errors::Error::JavaException)
+                }
+            }
+            Err(e) => Err(e),
+        }
+    }
+}
diff --git a/common/pourover/src/lib.rs b/common/pourover/src/lib.rs
index ed861ea..8cadc6e 100644
--- a/common/pourover/src/lib.rs
+++ b/common/pourover/src/lib.rs
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//! Utilties for JNI interactions.
+//! Utilities for JNI interactions.
 
 pub use pourover_macro::{call_constructor, call_method, call_static_method, jni_method};
 
 pub mod desc;
+pub mod exception;
 
 mod conversions;
 pub use conversions::{ToSigned, ToUnsigned};
diff --git a/common/pourover/tests/common/foo_class.rs b/common/pourover/tests/common/foo_class.rs
index 3ded762..a0a59e4 100644
--- a/common/pourover/tests/common/foo_class.rs
+++ b/common/pourover/tests/common/foo_class.rs
@@ -22,8 +22,7 @@
 //! [`jni::JNIEnv::define_class`].
 
 use pourover::desc::*;
-use std::error::Error;
-use std::process::Command;
+use std::{error::Error, process::Command};
 
 pub const CLASS_DESC: &str = "com/example/Foo";
 pub static FOO: ClassDesc = ClassDesc::new(CLASS_DESC);
diff --git a/common/pourover/tests/exception_integration.rs b/common/pourover/tests/exception_integration.rs
new file mode 100644
index 0000000..129d2e6
--- /dev/null
+++ b/common/pourover/tests/exception_integration.rs
@@ -0,0 +1,176 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![allow(unsafe_code, clippy::unwrap_used, clippy::expect_used, clippy::panic)]
+
+use jni::{
+    descriptors::Desc,
+    objects::{JObject, JString},
+    sys::jint,
+    JNIEnv, JavaVM,
+};
+use pourover::{
+    desc::ClassDesc,
+    exception::{null_pointer_exception, runtime_exception, JniResultExt, ThrowableJniResultExt},
+};
+use std::error::Error;
+
+mod common;
+use common::foo_class::*;
+
+#[pourover::jni_method(package = "com.example", class = "Foo")]
+extern "system" fn nativeReturnsInt<'local>(
+    mut env: JNIEnv<'local>,
+    _this: JObject<'local>,
+    n: jint,
+) -> jint {
+    (|| match n {
+        -1 => Err(runtime_exception("test runtime exception"))?,
+        -2 => Err(null_pointer_exception())?,
+        _ => Ok(n),
+    })()
+    .unwrap_or_throw(&mut env)
+}
+
+#[pourover::jni_method(
+    package = "com.example",
+    class = "Foo",
+    panic_returns = JObject::null().into(),
+)]
+extern "system" fn nativeReturnsObject<'local>(
+    _env: JNIEnv<'local>,
+    _this: JObject<'local>,
+    _n: jint,
+) -> JString<'local> {
+    unimplemented!()
+}
+
+pub static RUNTIME_EXCEPTION_CLASS: ClassDesc = ClassDesc::new("java/lang/RuntimeException");
+
+#[test]
+fn can_call_native_method() -> Result<(), Box<dyn Error>> {
+    // Create the environment
+    let vm = JavaVM::new(
+        jni::InitArgsBuilder::new()
+            .version(jni::JNIVersion::V8)
+            .option("-Xcheck:jni")
+            .build()?,
+    )?;
+    let mut env = vm.attach_current_thread()?;
+
+    // Load `Foo.class`
+    {
+        let foo_class = compile_foo()?;
+        let loaded_foo = env.define_class(CLASS_DESC, &JObject::null(), &foo_class)?;
+        env.delete_local_ref(loaded_foo)?;
+    }
+
+    let obj_foo = {
+        let method_id = CONSTRUCTOR.lookup(&mut env)?;
+        let args = &[jni::sys::jvalue { i: 123 }];
+        // Safety: `args` must match the constructor arg count and types.
+        unsafe { env.new_object_unchecked(CONSTRUCTOR.cls(), method_id, args) }?
+    };
+    let obj_foo = env.auto_local(obj_foo);
+
+    // TODO: It would be better if the JVM was able to find the method on its own,
+    // but, since we are using the invocation API to create the JVM, I haven't found a way to make
+    // it visible to the JVM. Normally the methods are loaded dynamically with
+    // `System.loadLibrary`, but in this case the symbols already exist in the executable, and I'm
+    // unsure why the JVM cannot find them. I have tried compiling with
+    // `-Zexport-executable-symbols` but that wasn't working for me.
+    env.register_native_methods(
+        &FOO,
+        &[
+            jni::NativeMethod {
+                name: "nativeReturnsInt".into(),
+                sig: "(I)I".into(),
+                fn_ptr: crate::nativeReturnsInt as *mut std::ffi::c_void,
+            },
+            jni::NativeMethod {
+                name: "nativeReturnsObject".into(),
+                sig: "(I)Ljava/lang/String;".into(),
+                fn_ptr: crate::nativeReturnsObject as *mut std::ffi::c_void,
+            },
+        ],
+    )?;
+
+    // Sub-test 1: Call nativeReturnsInt(99) returns the argument without exceptions
+    {
+        let result =
+            pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, 99);
+        assert_eq!(99, result.unwrap());
+    }
+
+    // Sub-test 2: Call nativeReturnsInt(99) returns the argument without exceptions, so
+    // extract_exception just returns Ok
+    {
+        let result =
+            pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, 99);
+        let extracted_result = result.extract_exception(&mut env, "java/lang/Throwable");
+        let Ok(Ok(99)) = extracted_result else {
+            panic!("extracted_result should be Ok(Ok(99))")
+        };
+    }
+
+    // Sub-test 3: Call nativeReturnsInt(-1) throws RuntimeException("test runtime exception")
+    {
+        let result =
+            pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, -1);
+        let throwable = result
+            .extract_exception(&mut env, "java/lang/RuntimeException")
+            .unwrap()
+            .unwrap_err();
+        let message = pourover::call_method!(
+            &mut env,
+            RUNTIME_EXCEPTION_CLASS,
+            "getMessage",
+            "()Ljava/lang/String;",
+            &throwable
+        )
+        .unwrap();
+        let message = env.get_string(&message).map(String::from).unwrap();
+        assert_eq!("test runtime exception", message);
+    }
+
+    // Sub-test 4: Call nativeReturnsInt(-2) throws NullPointerException()
+    {
+        let result =
+            pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, -2);
+        let _ = result
+            .extract_exception(&mut env, "java/lang/NullPointerException")
+            .unwrap()
+            .unwrap_err();
+    }
+
+    // Sub-test 5: Call nativeReturnsInt(-1) throws RuntimeException(). Calling
+    // extract_exception with NullPointerException should return Err(JavaException).
+    {
+        let result =
+            pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, -1);
+
+        let Err(jni::errors::Error::JavaException) =
+            result.extract_exception(&mut env, "java/lang/NullPointerException")
+        else {
+            panic!(concat!(
+                "Extracting NullPointerException from a result that contains RuntimeException ",
+                "should return Err(JavaException)"
+            ))
+        };
+        // The exception should still be pending on the JVM
+        assert!(env.exception_check().unwrap());
+    }
+
+    Ok(())
+}
diff --git a/common/pourover/tests/jni_method_integration.rs b/common/pourover/tests/jni_method_integration.rs
index 4bd6547..87cb80d 100644
--- a/common/pourover/tests/jni_method_integration.rs
+++ b/common/pourover/tests/jni_method_integration.rs
@@ -21,8 +21,10 @@
     sys::jint,
     JNIEnv, JavaVM,
 };
-use std::error::Error;
-use std::sync::atomic::{AtomicBool, Ordering};
+use std::{
+    error::Error,
+    sync::atomic::{AtomicBool, Ordering},
+};
 
 mod common;
 use common::foo_class::*;
diff --git a/common/pourover_macro/Cargo.toml b/common/pourover_macro/Cargo.toml
index 0473dd4..13dc189 100644
--- a/common/pourover_macro/Cargo.toml
+++ b/common/pourover_macro/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/common/pourover_macro/src/lib.rs b/common/pourover_macro/src/lib.rs
index f46b7b1..9aeca11 100644
--- a/common/pourover_macro/src/lib.rs
+++ b/common/pourover_macro/src/lib.rs
@@ -15,27 +15,29 @@
 //! Proc macros for `pourover`. These macros are reexported by the `pourover` crate, so this crate
 //! is an implementation detail.
 
+#[cfg(proc_macro)]
 use proc_macro::TokenStream;
 
 mod call_method;
 mod jni_method;
 mod type_parser;
 
-/// Export a function as a JNI native method. This will attach a `#[export_name = "..."]` attribute that
-/// is formatted with the given parameters. The provided `package`, `class`, and `method_name` will
-/// be combined and formatted in according to the [JNI method name resolution rules][JNI naming].
+/// Export a function as a JNI native method. This will attach a `#[export_name = "..."]` attribute
+/// that is formatted with the given parameters. The provided `package`, `class`, and `method_name`
+/// will be combined and formatted in according to the [JNI method name resolution rules][JNI
+/// naming].
 ///
-/// [JNI naming]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names
+/// [JNI naming]:
+///     https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names
 ///
 /// # Parameters
 /// - `package` (LitStr): the Java package for the class being implemented
-/// - `class` (LitStr): the Java class being implemented. Use `Foo.Inner` syntax for inner
-/// classes.
+/// - `class` (LitStr): the Java class being implemented. Use `Foo.Inner` syntax for inner classes.
 /// - `method_name` (*optional* LitStr): the method's name in Java. The Rust function name will be
-/// used if this parameter is not set.
+///   used if this parameter is not set.
 /// - `panic_returns` (*optional* Expr): the value to return when a panic is encountered. This can
-/// not access local variables. This may only be used with `panic=unwind` and will produce a
-/// compile error otherwise.
+///   not access local variables. This may only be used with `panic=unwind` and will produce a
+///   compile error otherwise.
 ///
 /// When using `panic_returns` function arguments must be [`std::panic::UnwindSafe`]. See
 /// [`std::panic::catch_unwind`] for details. In practice this will not cause issues as JNI
@@ -57,6 +59,7 @@
 /// ```
 ///
 /// This function will be exported with `#[export_name = "Java_my_package_Foo_getFoo"]`.
+#[cfg(proc_macro)]
 #[proc_macro_attribute]
 pub fn jni_method(meta: TokenStream, item: TokenStream) -> TokenStream {
     use quote::ToTokens;
@@ -75,14 +78,13 @@
 /// - `cls` (Expr: `&'static ClassDesc`): The class containing the method.
 /// - `name` (Expr: `&'static str`): The name of the method.
 /// - `sig` (LitStr): The JNI type signature of the method. This needs to be a literal so that it
-/// can be parsed by the macro to type-check args and return a correctly-typed value.
+///   can be parsed by the macro to type-check args and return a correctly-typed value.
 /// - `this` (Expr: `&JObject`): The Java object receiving the method call.
 /// - `args` (Expr ...): A variable number of arguments to be passed to the method.
 ///
 /// # Caching
-/// Each macro callsite will generate a `static` `MethodDesc` to cache the
-/// method id. Due to this, **this macro call should be wrapped in function** instead of being called
-/// multiple times.
+/// Each macro callsite will generate a `static` `MethodDesc` to cache the method id. Due to this,
+/// **this macro call should be wrapped in function** instead of being called multiple times.
 ///
 /// # Type-Safety
 /// The given type signature will be parsed and arguments will be type checked against it. The
@@ -94,8 +96,8 @@
 /// Similarly, the return type will be one of the types above.
 ///
 /// # Returns
-/// The macro will evaluate to `jni::errors::Result<R>` where `R` is the return type parsed from
-/// the type signature.
+/// The macro will evaluate to `jni::errors::Result<R>` where `R` is the return type parsed from the
+/// type signature.
 ///
 /// # Example
 /// Let's call `sayHello` from the following class.
@@ -119,6 +121,7 @@
 ///     call_method!(env, &MY_CLASS, "sayHello", "(Ljava/lang/String;)I", my_obj, name)
 /// }
 /// ```
+#[cfg(proc_macro)]
 #[proc_macro]
 pub fn call_method(args: TokenStream) -> TokenStream {
     call_method::call_method(args.into())
@@ -134,13 +137,12 @@
 /// - `cls` (Expr: `&'static ClassDesc`): The class containing the method.
 /// - `name` (Expr: `&'static str`): The name of the method.
 /// - `sig` (LitStr): The JNI type signature of the method. This needs to be a literal so that it
-/// can be parsed by the macro to type-check args and return a correctly-typed value.
+///   can be parsed by the macro to type-check args and return a correctly-typed value.
 /// - `args` (Expr ...): A variable number of arguments to be passed to the method.
 ///
 /// # Caching
-/// Each macro callsite will generate a `static` `StaticMethodDesc` to cache the
-/// method id. Due to this, **this macro call should be wrapped in function** instead of being called
-/// multiple times.
+/// Each macro callsite will generate a `static` `StaticMethodDesc` to cache the method id. Due to
+/// this, **this macro call should be wrapped in function** instead of being called multiple times.
 ///
 /// # Type-Safety
 /// The given type signature will be parsed and arguments will be type checked against it. The
@@ -152,8 +154,8 @@
 /// Similarly, the return type will be one of the types above.
 ///
 /// # Returns
-/// The macro will evaluate to `jni::errors::Result<R>` where `R` is the return type parsed from
-/// the type signature.
+/// The macro will evaluate to `jni::errors::Result<R>` where `R` is the return type parsed from the
+/// type signature.
 ///
 /// # Example
 /// Let's call `sayHello` from the following class.
@@ -176,6 +178,7 @@
 ///     call_static_method!(env, &MY_CLASS, "sayHello", "(Ljava/lang/String;)I", name)
 /// }
 /// ```
+#[cfg(proc_macro)]
 #[proc_macro]
 pub fn call_static_method(args: TokenStream) -> TokenStream {
     call_method::call_static_method(args.into())
@@ -189,14 +192,13 @@
 /// `call_constructor!($env, $cls, $sig, $($args),*)`
 /// - `env` (Expr: `&mut jni::JNIEnv`): The JNI environment.
 /// - `cls` (Expr: `&'static ClassDesc`): The class to be constructed.
-/// - `sig` (LitStr): The JNI type signature of the constructor. This needs to be a literal so that it
-/// can be parsed by the macro to type-check args and return a correctly-typed value.
+/// - `sig` (LitStr): The JNI type signature of the constructor. This needs to be a literal so that
+///   it can be parsed by the macro to type-check args and return a correctly-typed value.
 /// - `args` (Expr ...): A variable number of arguments to be passed to the constructor.
 ///
 /// # Caching
-/// Each macro callsite will generate a `static` `MethodDesc` to cache the
-/// method id. Due to this, **this macro call should be wrapped in function** instead of being called
-/// multiple times.
+/// Each macro callsite will generate a `static` `MethodDesc` to cache the method id. Due to this,
+/// **this macro call should be wrapped in function** instead of being called multiple times.
 ///
 /// # Type-Safety
 /// The given type signature will be parsed and arguments will be type checked against it. The
@@ -229,6 +231,7 @@
 ///     call_constructor!(env, &MY_CLASS, "(Ljava/lang/String;)V", name)
 /// }
 /// ```
+#[cfg(proc_macro)]
 #[proc_macro]
 pub fn call_constructor(args: TokenStream) -> TokenStream {
     call_method::call_constructor(args.into())
diff --git a/common/pourover_macro/src/type_parser.rs b/common/pourover_macro/src/type_parser.rs
index e6834e2..9a120d2 100644
--- a/common/pourover_macro/src/type_parser.rs
+++ b/common/pourover_macro/src/type_parser.rs
@@ -90,7 +90,7 @@
     }
 }
 
-#[cfg(jni)]
+#[cfg(feature = "jni")]
 impl<'a> From<JavaType<'a>> for jni::signature::ReturnType {
     fn from(ty: JavaType<'a>) -> Self {
         match ty {
@@ -221,7 +221,7 @@
     }
 }
 
-#[cfg(jni)]
+#[cfg(feature = "jni")]
 impl From<Primitive> for jni::signature::Primitive {
     fn from(p: Primitive) -> Self {
         match p {
@@ -275,7 +275,7 @@
     }
 }
 
-#[cfg(jni)]
+#[cfg(feature = "jni")]
 impl<'a> From<ReturnType<'a>> for jni::signature::ReturnType {
     fn from(ty: ReturnType<'a>) -> Self {
         match ty {
diff --git a/nearby/Cargo.lock b/nearby/Cargo.lock
index 6eb2999..982aed1 100644
--- a/nearby/Cargo.lock
+++ b/nearby/Cargo.lock
@@ -91,9 +91,9 @@
 
 [[package]]
 name = "anstream"
-version = "0.6.14"
+version = "0.6.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -106,33 +106,33 @@
 
 [[package]]
 name = "anstyle"
-version = "1.0.7"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.0.3"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
+checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
 dependencies = [
  "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.3"
+version = "3.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
 dependencies = [
  "anstyle",
  "windows-sys 0.52.0",
@@ -140,9 +140,9 @@
 
 [[package]]
 name = "anyhow"
-version = "1.0.83"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 
 [[package]]
 name = "arbitrary"
@@ -204,9 +204,9 @@
 
 [[package]]
 name = "bitflags"
-version = "2.5.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 
 [[package]]
 name = "blake2"
@@ -248,21 +248,21 @@
 
 [[package]]
 name = "bstr"
-version = "1.9.1"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
+checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
 dependencies = [
  "memchr",
  "serde",
 ]
 
 [[package]]
-name = "build-scripts"
+name = "build_scripts"
 version = "0.1.0"
 dependencies = [
  "anyhow",
  "chrono",
- "clap 4.5.4",
+ "clap 4.5.13",
  "cmd_runner",
  "crossbeam",
  "env_logger 0.10.2",
@@ -295,9 +295,9 @@
 
 [[package]]
 name = "bytes"
-version = "1.6.0"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
 
 [[package]]
 name = "cast"
@@ -335,13 +335,12 @@
 
 [[package]]
 name = "cc"
-version = "1.0.97"
+version = "1.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
+checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549"
 dependencies = [
  "jobserver",
  "libc",
- "once_cell",
 ]
 
 [[package]]
@@ -367,7 +366,7 @@
  "js-sys",
  "num-traits",
  "wasm-bindgen",
- "windows-targets 0.52.5",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -424,9 +423,9 @@
 
 [[package]]
 name = "clap"
-version = "4.5.4"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -434,26 +433,26 @@
 
 [[package]]
 name = "clap_builder"
-version = "4.5.2"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
 dependencies = [
  "anstream",
  "anstyle",
- "clap_lex 0.7.0",
+ "clap_lex 0.7.2",
  "strsim 0.11.1",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "4.5.4"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.72",
 ]
 
 [[package]]
@@ -467,9 +466,9 @@
 
 [[package]]
 name = "clap_lex"
-version = "0.7.0"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
 
 [[package]]
 name = "cmd_runner"
@@ -477,20 +476,22 @@
 dependencies = [
  "anyhow",
  "chrono",
- "clap 4.5.4",
+ "clap 4.5.13",
  "file-header",
  "globset",
  "log",
  "owo-colors",
+ "serde",
+ "serde_json",
  "shell-escape",
  "xshell",
 ]
 
 [[package]]
 name = "colorchoice"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
 
 [[package]]
 name = "combine"
@@ -525,9 +526,9 @@
 
 [[package]]
 name = "crc32fast"
-version = "1.4.0"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
 dependencies = [
  "cfg-if",
 ]
@@ -541,7 +542,7 @@
  "anes",
  "cast",
  "ciborium",
- "clap 4.5.4",
+ "clap 4.5.13",
  "criterion-plot",
  "is-terminal",
  "itertools 0.10.5",
@@ -583,9 +584,9 @@
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.12"
+version = "0.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
 dependencies = [
  "crossbeam-utils",
 ]
@@ -620,9 +621,9 @@
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
 
 [[package]]
 name = "crunchy"
@@ -754,16 +755,15 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.1.2"
+version = "4.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
 dependencies = [
  "cfg-if",
  "cpufeatures",
  "curve25519-dalek-derive",
  "digest",
  "fiat-crypto",
- "platforms",
  "rustc_version",
  "subtle",
  "zeroize",
@@ -777,7 +777,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.72",
 ]
 
 [[package]]
@@ -798,25 +798,30 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.72",
 ]
 
 [[package]]
 name = "derive_fuzztest"
-version = "0.1.0"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98a03c381eaef21fd453d46a2c9be9f7527e588f113fdc63c3c4a11d7da8b473"
 dependencies = [
  "arbitrary",
  "derive_fuzztest_macro",
+ "libfuzzer-sys",
  "quickcheck",
 ]
 
 [[package]]
 name = "derive_fuzztest_macro"
-version = "0.1.0"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "001301b8ebda15d520d9916f226711c0fa30be0bc8f055dd2379523bde1b4a32"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.72",
 ]
 
 [[package]]
@@ -863,9 +868,9 @@
 
 [[package]]
 name = "either"
-version = "1.11.0"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
 
 [[package]]
 name = "elliptic-curve"
@@ -911,9 +916,9 @@
 
 [[package]]
 name = "errno"
-version = "0.3.8"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -937,9 +942,9 @@
 
 [[package]]
 name = "fiat-crypto"
-version = "0.2.8"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
 
 [[package]]
 name = "file-header"
@@ -956,9 +961,9 @@
 
 [[package]]
 name = "flate2"
-version = "1.0.30"
+version = "1.0.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
+checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920"
 dependencies = [
  "crc32fast",
  "miniz_oxide",
@@ -1040,6 +1045,7 @@
 name = "handle_map"
 version = "0.1.0"
 dependencies = [
+ "lazy_static",
  "lock_adapter",
 ]
 
@@ -1191,9 +1197,9 @@
 
 [[package]]
 name = "is_terminal_polyfill"
-version = "1.70.0"
+version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 
 [[package]]
 name = "itertools"
@@ -1206,9 +1212,9 @@
 
 [[package]]
 name = "itertools"
-version = "0.12.1"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
 dependencies = [
  "either",
 ]
@@ -1243,9 +1249,9 @@
 
 [[package]]
 name = "jobserver"
-version = "0.1.31"
+version = "0.1.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
+checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
 dependencies = [
  "libc",
 ]
@@ -1261,11 +1267,11 @@
 
 [[package]]
 name = "lazy_static"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 dependencies = [
- "spin 0.5.2",
+ "spin",
 ]
 
 [[package]]
@@ -1276,7 +1282,7 @@
  "anyhow",
  "base64",
  "blake2",
- "clap 4.5.4",
+ "clap 4.5.13",
  "criterion",
  "crypto_provider",
  "crypto_provider_default",
@@ -1357,7 +1363,7 @@
  "ldt_np_adv",
  "np_hkdf",
  "rand",
- "spin 0.9.8",
+ "spin",
 ]
 
 [[package]]
@@ -1382,9 +1388,9 @@
 
 [[package]]
 name = "libc"
-version = "0.2.154"
+version = "0.2.155"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 
 [[package]]
 name = "libfuzzer-sys"
@@ -1399,9 +1405,9 @@
 
 [[package]]
 name = "license"
-version = "3.3.1"
+version = "3.4.0+3.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bba2f02ee1d13cd4bea565658939cd851d70e391f34f7c27b45b2077df3a2e4"
+checksum = "a7da1e0d845faf299a9fe5f201a918a0dc0d5fc22c7b9580a6a23fed3a912b37"
 dependencies = [
  "reword",
  "serde",
@@ -1410,15 +1416,15 @@
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.13"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
 name = "lock_adapter"
 version = "0.1.0"
 dependencies = [
- "spin 0.9.8",
+ "spin",
 ]
 
 [[package]]
@@ -1433,15 +1439,15 @@
 
 [[package]]
 name = "log"
-version = "0.4.21"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
 
 [[package]]
 name = "memchr"
-version = "2.7.2"
+version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "minimal-lexical"
@@ -1451,9 +1457,9 @@
 
 [[package]]
 name = "miniz_oxide"
-version = "0.7.2"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
 dependencies = [
  "adler",
 ]
@@ -1478,6 +1484,7 @@
  "crypto_provider",
  "crypto_provider_default",
  "hex",
+ "itertools 0.13.0",
  "lazy_static",
  "ldt",
  "ldt_np_adv",
@@ -1509,6 +1516,18 @@
 ]
 
 [[package]]
+name = "np_adv_fuzz"
+version = "0.1.0"
+dependencies = [
+ "arbitrary",
+ "crypto_provider",
+ "crypto_provider_default",
+ "derive_fuzztest",
+ "libfuzzer-sys",
+ "np_adv",
+]
+
+[[package]]
 name = "np_c_ffi"
 version = "0.1.0"
 dependencies = [
@@ -1567,6 +1586,7 @@
 name = "np_java_ffi"
 version = "0.1.0"
 dependencies = [
+ "array_view",
  "crypto_provider_default",
  "handle_map",
  "jni",
@@ -1578,9 +1598,9 @@
 
 [[package]]
 name = "num-bigint"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
 dependencies = [
  "num-integer",
  "num-traits",
@@ -1612,9 +1632,9 @@
 
 [[package]]
 name = "oorandom"
-version = "11.1.3"
+version = "11.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
+checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9"
 
 [[package]]
 name = "opaque-debug"
@@ -1655,16 +1675,10 @@
 ]
 
 [[package]]
-name = "platforms"
-version = "3.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7"
-
-[[package]]
 name = "plotters"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
+checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3"
 dependencies = [
  "num-traits",
  "plotters-backend",
@@ -1675,15 +1689,15 @@
 
 [[package]]
 name = "plotters-backend"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609"
+checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7"
 
 [[package]]
 name = "plotters-svg"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab"
+checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705"
 dependencies = [
  "plotters-backend",
 ]
@@ -1715,14 +1729,17 @@
  "nom",
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.72",
 ]
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.17"
+version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
 
 [[package]]
 name = "primeorder"
@@ -1735,9 +1752,9 @@
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.82"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
 dependencies = [
  "unicode-ident",
 ]
@@ -1884,9 +1901,9 @@
 
 [[package]]
 name = "regex"
-version = "1.10.4"
+version = "1.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1896,9 +1913,9 @@
 
 [[package]]
 name = "regex-automata"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1907,9 +1924,9 @@
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
 
 [[package]]
 name = "relative-path"
@@ -1949,7 +1966,7 @@
  "regex",
  "relative-path",
  "rustc_version",
- "syn 2.0.61",
+ "syn 2.0.72",
  "unicode-ident",
 ]
 
@@ -1962,7 +1979,7 @@
  "quote",
  "rand",
  "rustc_version",
- "syn 2.0.61",
+ "syn 2.0.72",
 ]
 
 [[package]]
@@ -1980,7 +1997,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
 dependencies = [
- "bitflags 2.5.0",
+ "bitflags 2.6.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -1989,9 +2006,9 @@
 
 [[package]]
 name = "rustversion"
-version = "1.0.16"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
 
 [[package]]
 name = "ryu"
@@ -2035,31 +2052,32 @@
 
 [[package]]
 name = "serde"
-version = "1.0.200"
+version = "1.0.204"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
+checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.200"
+version = "1.0.204"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
+checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.72",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.116"
+version = "1.0.122"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
+checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
 dependencies = [
  "itoa",
+ "memchr",
  "ryu",
  "serde",
 ]
@@ -2099,12 +2117,6 @@
 
 [[package]]
 name = "spin"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
-
-[[package]]
-name = "spin"
 version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
@@ -2150,14 +2162,14 @@
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.61",
+ "syn 2.0.72",
 ]
 
 [[package]]
 name = "subtle"
-version = "2.5.0"
+version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
 [[package]]
 name = "syn"
@@ -2172,9 +2184,9 @@
 
 [[package]]
 name = "syn"
-version = "2.0.61"
+version = "2.0.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
+checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2183,14 +2195,15 @@
 
 [[package]]
 name = "tempfile"
-version = "3.10.1"
+version = "3.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
 dependencies = [
  "cfg-if",
  "fastrand",
+ "once_cell",
  "rustix",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -2207,7 +2220,7 @@
 version = "0.1.0"
 dependencies = [
  "hex",
- "itertools 0.12.1",
+ "itertools 0.13.0",
  "serde_json",
 ]
 
@@ -2227,22 +2240,22 @@
 
 [[package]]
 name = "thiserror"
-version = "1.0.60"
+version = "1.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
+checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.60"
+version = "1.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
+checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.72",
 ]
 
 [[package]]
@@ -2257,9 +2270,9 @@
 
 [[package]]
 name = "tinyvec"
-version = "1.6.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
 
 [[package]]
 name = "toml"
@@ -2359,7 +2372,7 @@
 name = "ukey2_shell"
 version = "0.1.0"
 dependencies = [
- "clap 4.5.4",
+ "clap 4.5.13",
  "crypto_provider_rustcrypto",
  "ukey2_connections",
 ]
@@ -2388,15 +2401,15 @@
 
 [[package]]
 name = "utf8parse"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "version_check"
-version = "0.9.4"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
 [[package]]
 name = "walkdir"
@@ -2435,7 +2448,7 @@
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.72",
  "wasm-bindgen-shared",
 ]
 
@@ -2457,7 +2470,7 @@
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.61",
+ "syn 2.0.72",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -2508,11 +2521,11 @@
 
 [[package]]
 name = "winapi-util"
-version = "0.1.8"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
 dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -2527,7 +2540,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-targets 0.52.5",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -2545,7 +2558,16 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.5",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -2565,18 +2587,18 @@
 
 [[package]]
 name = "windows-targets"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.5",
- "windows_aarch64_msvc 0.52.5",
- "windows_i686_gnu 0.52.5",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
  "windows_i686_gnullvm",
- "windows_i686_msvc 0.52.5",
- "windows_x86_64_gnu 0.52.5",
- "windows_x86_64_gnullvm 0.52.5",
- "windows_x86_64_msvc 0.52.5",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
 [[package]]
@@ -2587,9 +2609,9 @@
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -2599,9 +2621,9 @@
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -2611,15 +2633,15 @@
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
 
 [[package]]
 name = "windows_i686_gnullvm"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -2629,9 +2651,9 @@
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -2641,9 +2663,9 @@
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -2653,9 +2675,9 @@
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -2665,9 +2687,9 @@
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "wycheproof"
@@ -2751,7 +2773,28 @@
 ]
 
 [[package]]
-name = "zeroize"
-version = "1.7.0"
+name = "zerocopy"
+version = "0.7.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.72",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
diff --git a/nearby/Cargo.toml b/nearby/Cargo.toml
index f69bcf7..11aecd8 100644
--- a/nearby/Cargo.toml
+++ b/nearby/Cargo.toml
@@ -1,5 +1,6 @@
 [workspace]
 members = [
+    "build_scripts",
     "connections/ukey2/ukey2",
     "connections/ukey2/ukey2_connections",
     "connections/ukey2/ukey2_connections/fuzz",
@@ -22,6 +23,7 @@
     "presence/ldt_np_jni",
     "presence/ldt_tbc",
     "presence/np_adv",
+    "presence/np_adv/fuzz",
     "presence/np_adv_dynamic",
     "presence/np_c_ffi",
     "presence/np_ed25519",
@@ -35,11 +37,13 @@
     "presence/xts_aes",
     "presence/xts_aes/fuzz",
 ]
+default-members = ["build_scripts"]
 
 # TODO: remove boringssl once we figure out a better plan for integrating the build system
 exclude = [
     "crypto/crypto_provider_boringssl",
 ]
+resolver = "2"
 
 [workspace.lints.rust]
 missing_docs = "deny"
@@ -50,6 +54,7 @@
 unused_extern_crates = "deny"
 unused_import_braces = "deny"
 unused_results = "deny"
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)', 'cfg(blaze)'] }
 
 [workspace.lints.clippy]
 expect_used = "deny"
@@ -80,14 +85,12 @@
 np_adv = { path = "presence/np_adv" }
 np_adv_dynamic = { path = "presence/np_adv_dynamic" }
 np_ed25519 = { path = "presence/np_ed25519" }
-np_ffi_core = { path = "presence/np_ffi_core", default-features=false }
+np_ffi_core = { path = "presence/np_ffi_core", default-features = false }
 np_java_ffi = { path = "presence/np_java_ffi" }
 sink = { path = "presence/sink" }
 test_vector_hkdf = { path = "presence/test_vector_hkdf" }
 
 # from utils workspace
-derive_fuzztest = { path = "../common/derive_fuzztest" }
-derive_fuzztest_macro = { path = "../common/derive_fuzztest_macro" }
 handle_map = { path = "../common/handle_map" }
 lock_adapter = { path = "../common/lock_adapter" }
 pourover = { path = "../common/pourover" }
@@ -129,7 +132,7 @@
 log = "0.4.20"
 env_logger = "0.10.1"
 criterion = { version = "0.5.1", features = ["html_reports"] }
-clap = { version = "4.4.11", features = ["derive"] }
+clap = { version = "4.5.13", features = ["derive"] }
 lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
 hex-literal = "0.4.1"
 cfg-if = "1.0.0"
@@ -154,16 +157,19 @@
 syn = { version = "2.0", features = ["full"] }
 proc-macro2 = "1.0"
 quote = "1.0"
-itertools = "0.12.1"
+itertools = { version = "0.13.0", default-features = false }
 quickcheck = "1.0.3"
 proptest = "1.4.0"
 proptest-arbitrary-interop = { git = "https://github.com/brson/proptest-arbitrary-interop.git", branch = "incorrect-format" }
 libfuzzer-sys = "0.4.7"
+derive_fuzztest = "0.1.1"
+derive_fuzztest_macro = "0.1.1"
 
 [workspace.package]
 version = "0.1.0"
 edition = "2021"
 publish = false
+license = "Apache-2.0"
 
 [profile.test]
 # speed up test execution
@@ -183,33 +189,3 @@
 # z optimizes for size
 opt-level = "z"
 strip = true
-
-[package]
-name = "build-scripts"
-version.workspace = true
-edition.workspace = true
-publish.workspace = true
-rust-version = "1.71.0"
-
-[dependencies]
-clap.workspace = true
-cmd_runner = { path = "../common/cmd_runner" }
-anyhow.workspace = true
-shell-escape = "0.1.5"
-owo-colors.workspace = true
-semver = "1.0.17"
-walkdir = "2.3.3"
-globset = "0.4.10"
-glob = "0.3.1"
-crossbeam = "0.8.2"
-chrono.workspace = true
-thiserror.workspace = true
-log.workspace = true
-env_logger.workspace = true
-file-header = "0.1.2"
-serde_json = { workspace = true, features = ["std"] }
-regex = "1.10.2"
-xshell = "0.2.6"
-
-[dev-dependencies]
-tempfile.workspace = true
diff --git a/nearby/build_scripts/Cargo.toml b/nearby/build_scripts/Cargo.toml
new file mode 100644
index 0000000..5349e9c
--- /dev/null
+++ b/nearby/build_scripts/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "build_scripts"
+version.workspace = true
+edition.workspace = true
+publish.workspace = true
+license.workspace = true
+rust-version = "1.71.0"
+
+[dependencies]
+clap.workspace = true
+cmd_runner = { path = "../../common/cmd_runner" }
+anyhow.workspace = true
+shell-escape = "0.1.5"
+owo-colors.workspace = true
+semver = "1.0.17"
+walkdir = "2.3.3"
+globset = "0.4.10"
+glob = "0.3.1"
+crossbeam = "0.8.2"
+chrono.workspace = true
+thiserror.workspace = true
+log.workspace = true
+env_logger.workspace = true
+file-header = "0.1.2"
+serde_json = { workspace = true, features = ["std"] }
+regex = "1.10.2"
+xshell = "0.2.6"
+
+[dev-dependencies]
+tempfile.workspace = true
diff --git a/nearby/src/coverage.rs b/nearby/build_scripts/src/coverage.rs
similarity index 100%
rename from nearby/src/coverage.rs
rename to nearby/build_scripts/src/coverage.rs
diff --git a/nearby/src/crypto_ffi.rs b/nearby/build_scripts/src/crypto_ffi.rs
similarity index 100%
rename from nearby/src/crypto_ffi.rs
rename to nearby/build_scripts/src/crypto_ffi.rs
diff --git a/nearby/src/ffi.rs b/nearby/build_scripts/src/ffi.rs
similarity index 100%
rename from nearby/src/ffi.rs
rename to nearby/build_scripts/src/ffi.rs
diff --git a/nearby/build_scripts/src/fuzzers.rs b/nearby/build_scripts/src/fuzzers.rs
new file mode 100644
index 0000000..c2caf29
--- /dev/null
+++ b/nearby/build_scripts/src/fuzzers.rs
@@ -0,0 +1,56 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use cmd_runner::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
+use std::{fs, path};
+
+pub(crate) fn run_rust_fuzzers(root: &path::Path) -> anyhow::Result<()> {
+    cmd_runner::fuzzers::run_workspace_fuzz_targets(root)?;
+    run_cmd_shell_with_color::<YellowStderr>(
+        &root.join("crypto/crypto_provider_test"),
+        concat!(
+            "cargo +nightly fuzz run fuzz_p256 --features=boringssl --no-default-features ",
+            "-- -runs=1000000 -max_total_time=30"
+        ),
+    )?;
+
+    Ok(())
+}
+
+// Runs the fuzztest fuzzers as short lived unit tests, compatible with gtest
+pub(crate) fn build_fuzztest_unit_tests(root: &path::Path) -> anyhow::Result<()> {
+    log::info!("Checking fuzztest targets in unit test mode");
+    run_cmd_shell(root, "cargo build -p np_c_ffi --release")?;
+    run_cmd_shell(root, "cargo build -p ldt_np_adv_ffi --release")?;
+    let build_dir = root.join("cmake-build");
+    fs::create_dir_all(&build_dir)?;
+    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake -G Ninja -DENABLE_FUZZ=true ..")?;
+
+    for target in ["deserialization_fuzzer", "ldt_fuzzer"] {
+        run_cmd_shell_with_color::<YellowStderr>(
+            &build_dir,
+            format!("cmake --build . --target {}", target),
+        )?;
+    }
+
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir.join("presence/np_cpp_ffi/fuzz/"),
+        "ctest",
+    )?;
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir.join("presence/ldt_np_adv_ffi/c/fuzz/"),
+        "ctest",
+    )?;
+    Ok(())
+}
diff --git a/nearby/src/jni.rs b/nearby/build_scripts/src/jni.rs
similarity index 91%
rename from nearby/src/jni.rs
rename to nearby/build_scripts/src/jni.rs
index 4ef3590..ddea7d2 100644
--- a/nearby/src/jni.rs
+++ b/nearby/build_scripts/src/jni.rs
@@ -30,7 +30,10 @@
 }
 
 pub fn run_np_java_ffi_tests(root: &path::Path) -> anyhow::Result<()> {
-    run_cmd_shell(root, "cargo build -p np_java_ffi -F crypto_provider_default/rustcrypto")?;
+    run_cmd_shell(
+        root,
+        "cargo build -p np_java_ffi -F crypto_provider_default/rustcrypto -F testing",
+    )?;
     let np_java_path = root.to_path_buf().join("presence/np_java_ffi");
     run_cmd_shell(&np_java_path, "./gradlew :test --info --rerun")?;
     Ok(())
diff --git a/nearby/src/license.rs b/nearby/build_scripts/src/license.rs
similarity index 100%
rename from nearby/src/license.rs
rename to nearby/build_scripts/src/license.rs
diff --git a/nearby/src/main.rs b/nearby/build_scripts/src/main.rs
similarity index 91%
rename from nearby/src/main.rs
rename to nearby/build_scripts/src/main.rs
index 6fe4263..63ab0e9 100644
--- a/nearby/src/main.rs
+++ b/nearby/build_scripts/src/main.rs
@@ -19,6 +19,7 @@
 use cmd_runner::{license_checker::LicenseSubcommand, run_cmd, run_cmd_shell, YellowStderr};
 use env_logger::Env;
 use license::LICENSE_CHECKER;
+use log::info;
 use std::{env, ffi::OsString, path};
 
 mod crypto_ffi;
@@ -31,13 +32,15 @@
     env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
     let cli: Cli = Cli::parse();
 
-    let root_dir = path::PathBuf::from(
+    let build_script_dir = path::PathBuf::from(
         env::var("CARGO_MANIFEST_DIR").expect("Must be run via Cargo to establish root directory"),
     );
+    let root_dir =
+        build_script_dir.parent().expect("build_scripts crate directory should have a parent");
 
     match cli.subcommand {
         Subcommand::RunDefaultChecks(ref check_options) => {
-            run_default_checks(&root_dir, check_options)?;
+            run_default_checks(root_dir, check_options)?;
             print!(concat!(
                 "Congratulations, the default checks passed. Since you like quality, here are\n",
                 "some more checks you may like:\n",
@@ -45,33 +48,31 @@
                 "    cargo run -- check-stack-usage\n",
             ));
         }
-        Subcommand::VerifyCi { ref check_options } => verify_ci(&root_dir, check_options)?,
-        Subcommand::RunAllChecks { ref check_options } => run_all_checks(&root_dir, check_options)?,
-        Subcommand::CleanEverything => clean_everything(&root_dir)?,
-        Subcommand::CheckFormat(ref options) => check_format(&root_dir, options)?,
-        Subcommand::CheckWorkspace(ref options) => check_workspace(&root_dir, options)?,
-        Subcommand::CheckAllFfi(ref options) => ffi::check_all_ffi(&root_dir, options, false)?,
-        Subcommand::BazelBuild => bazel_build(&root_dir)?,
-        Subcommand::BuildBoringssl => crypto_ffi::build_boringssl(&root_dir)?,
-        Subcommand::CheckBoringssl(ref options) => crypto_ffi::check_boringssl(&root_dir, options)?,
+        Subcommand::VerifyCi { ref check_options } => verify_ci(root_dir, check_options)?,
+        Subcommand::RunAllChecks { ref check_options } => run_all_checks(root_dir, check_options)?,
+        Subcommand::CleanEverything => clean_everything(root_dir)?,
+        Subcommand::CheckFormat(ref options) => check_format(root_dir, options)?,
+        Subcommand::CheckWorkspace(ref options) => check_workspace(root_dir, options)?,
+        Subcommand::CheckAllFfi(ref options) => ffi::check_all_ffi(root_dir, options, false)?,
+        Subcommand::BazelBuild => bazel_build(root_dir)?,
+        Subcommand::BuildBoringssl => crypto_ffi::build_boringssl(root_dir)?,
+        Subcommand::CheckBoringssl(ref options) => crypto_ffi::check_boringssl(root_dir, options)?,
         Subcommand::CheckBoringsslAtLatest(ref options) => {
-            crypto_ffi::check_boringssl_at_head(&root_dir, options)?
+            crypto_ffi::check_boringssl_at_head(root_dir, options)?
         }
-        Subcommand::RunRustFuzzers => fuzzers::run_rust_fuzzers(&root_dir)?,
-        Subcommand::CheckFuzztest => fuzzers::build_fuzztest_unit_tests(&root_dir)?,
+        Subcommand::RunRustFuzzers => fuzzers::run_rust_fuzzers(root_dir)?,
+        Subcommand::CheckFuzztest => fuzzers::build_fuzztest_unit_tests(root_dir)?,
         Subcommand::License(license_subcommand) => {
-            license_subcommand.run(&LICENSE_CHECKER, &root_dir)?
+            license_subcommand.run(&LICENSE_CHECKER, root_dir)?
         }
-        Subcommand::CheckUkey2Ffi(ref options) => {
-            ffi::check_ukey2_cmake(&root_dir, options, false)?
-        }
-        Subcommand::RunUkey2JniTests => jni::run_ukey2_jni_tests(&root_dir)?,
-        Subcommand::RunNpJavaFfiTests => jni::run_np_java_ffi_tests(&root_dir)?,
-        Subcommand::CheckLdtCmake(ref options) => ffi::check_ldt_cmake(&root_dir, options, false)?,
+        Subcommand::CheckUkey2Ffi(ref options) => ffi::check_ukey2_cmake(root_dir, options, false)?,
+        Subcommand::RunUkey2JniTests => jni::run_ukey2_jni_tests(root_dir)?,
+        Subcommand::RunNpJavaFfiTests => jni::run_np_java_ffi_tests(root_dir)?,
+        Subcommand::CheckLdtCmake(ref options) => ffi::check_ldt_cmake(root_dir, options, false)?,
         Subcommand::CheckNpFfiCmake(ref options) => {
-            ffi::check_np_ffi_cmake(&root_dir, options, false)?
+            ffi::check_np_ffi_cmake(root_dir, options, false)?
         }
-        Subcommand::RunLdtKotlinTests => jni::run_ldt_kotlin_tests(&root_dir)?,
+        Subcommand::RunLdtKotlinTests => jni::run_ldt_kotlin_tests(root_dir)?,
     }
 
     Ok(())
@@ -133,7 +134,7 @@
 }
 
 pub fn check_workspace(root: &path::Path, options: &CheckOptions) -> anyhow::Result<()> {
-    log::info!("Running cargo checks on workspace");
+    info!("Running cargo checks on workspace");
 
     // ensure formatting is correct (Check for it first because it is fast compared to running tests)
     check_format(root, &options.formatter_options)?;
diff --git a/common/derive_fuzztest/fuzz/src/bin/integer_add.rs b/nearby/build_scripts/src/no_std.rs
similarity index 70%
copy from common/derive_fuzztest/fuzz/src/bin/integer_add.rs
copy to nearby/build_scripts/src/no_std.rs
index 38b8172..6258260 100644
--- a/common/derive_fuzztest/fuzz/src/bin/integer_add.rs
+++ b/nearby/build_scripts/src/no_std.rs
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2023 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,12 +12,3 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#![cfg_attr(fuzzing, no_main)]
-
-use derive_fuzztest::fuzztest;
-
-#[fuzztest]
-pub fn test(a: u8, b: u8) {
-    let _ = a.checked_add(b);
-    // a + b;  // This fails because a + b can overflow.
-}
diff --git a/common/derive_fuzztest/fuzz/src/bin/integer_add.rs b/nearby/build_scripts/src/tools.rs
similarity index 70%
copy from common/derive_fuzztest/fuzz/src/bin/integer_add.rs
copy to nearby/build_scripts/src/tools.rs
index 38b8172..6258260 100644
--- a/common/derive_fuzztest/fuzz/src/bin/integer_add.rs
+++ b/nearby/build_scripts/src/tools.rs
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2023 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,12 +12,3 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#![cfg_attr(fuzzing, no_main)]
-
-use derive_fuzztest::fuzztest;
-
-#[fuzztest]
-pub fn test(a: u8, b: u8) {
-    let _ = a.checked_add(b);
-    // a + b;  // This fails because a + b can overflow.
-}
diff --git a/nearby/connections/.clang-format b/nearby/connections/.clang-format
new file mode 100644
index 0000000..8217234
--- /dev/null
+++ b/nearby/connections/.clang-format
@@ -0,0 +1,17 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Language: Cpp
+BasedOnStyle: Google
+QualifierAlignment: Left
\ No newline at end of file
diff --git a/nearby/connections/ukey2/ukey2/Cargo.toml b/nearby/connections/ukey2/ukey2/Cargo.toml
index 35de144..900a5a0 100644
--- a/nearby/connections/ukey2/ukey2/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/connections/ukey2/ukey2_c_ffi/Cargo.toml b/nearby/connections/ukey2/ukey2_c_ffi/Cargo.toml
index 1034359..328400b 100644
--- a/nearby/connections/ukey2/ukey2_c_ffi/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2_c_ffi/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [dependencies]
 ukey2_connections = { path = "../ukey2_connections" }
@@ -25,4 +26,4 @@
 [lib]
 # Static lib is a bit large, resulting in quite a large test executable.
 # This will be also shipped as a dynamic lib in most environments (I think) so good to replicate those conditions.
-crate_type = ["cdylib"]
+crate-type = ["cdylib"]
diff --git a/nearby/connections/ukey2/ukey2_connections/Cargo.toml b/nearby/connections/ukey2/ukey2_connections/Cargo.toml
index 5c9622b..ff2ee44 100644
--- a/nearby/connections/ukey2/ukey2_connections/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2_connections/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
@@ -15,11 +16,11 @@
 [dependencies]
 ukey2_rs = { path = "../ukey2" }
 
-crypto_provider.workspace = true
+crypto_provider = { workspace = true, features = ["alloc"] }
 rand = { workspace = true, features = ["std", "std_rng"] }
 ukey2_proto.workspace = true
 nom = { version = "7.1.3", features = ["alloc"] }
-bytes = "1.5.0"
+bytes = "1.7.1"
 criterion.workspace = true
 
 [dev-dependencies]
diff --git a/nearby/connections/ukey2/ukey2_connections/fuzz/Cargo.toml b/nearby/connections/ukey2/ukey2_connections/fuzz/Cargo.toml
index 2c94ebb..6ba66f5 100644
--- a/nearby/connections/ukey2/ukey2_connections/fuzz/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2_connections/fuzz/Cargo.toml
@@ -2,6 +2,7 @@
 name = "ukey2_connections-fuzz"
 version.workspace = true
 publish.workspace = true
+license.workspace = true
 edition.workspace = true
 
 [package.metadata]
@@ -18,6 +19,9 @@
 [target.'cfg(fuzzing)'.dependencies]
 libfuzzer-sys.workspace = true
 
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
+
 [[bin]]
 name = "fuzz_connection"
 path = "fuzz_targets/fuzz_connection.rs"
diff --git a/nearby/connections/ukey2/ukey2_jni/Cargo.toml b/nearby/connections/ukey2/ukey2_jni/Cargo.toml
index bd615f5..f66ddb7 100644
--- a/nearby/connections/ukey2/ukey2_jni/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2_jni/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
@@ -28,4 +29,4 @@
 std = ["lock_adapter/std"]
 
 [lib]
-crate_type = ["cdylib"]
\ No newline at end of file
+crate-type = ["cdylib"]
diff --git a/nearby/connections/ukey2/ukey2_proto/Cargo.toml b/nearby/connections/ukey2/ukey2_proto/Cargo.toml
index c94d5b8..e7292da 100644
--- a/nearby/connections/ukey2/ukey2_proto/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2_proto/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [dependencies]
diff --git a/nearby/connections/ukey2/ukey2_shell/Cargo.toml b/nearby/connections/ukey2/ukey2_shell/Cargo.toml
index c5c84ea..e81c5dd 100644
--- a/nearby/connections/ukey2/ukey2_shell/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2_shell/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/crypto/crypto_provider/Cargo.toml b/nearby/crypto/crypto_provider/Cargo.toml
index bafdfcc..3862cfd 100644
--- a/nearby/crypto/crypto_provider/Cargo.toml
+++ b/nearby/crypto/crypto_provider/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/crypto/crypto_provider_boringssl/Cargo.lock b/nearby/crypto/crypto_provider_boringssl/Cargo.lock
index 356a65d..cf82847 100644
--- a/nearby/crypto/crypto_provider_boringssl/Cargo.lock
+++ b/nearby/crypto/crypto_provider_boringssl/Cargo.lock
@@ -102,9 +102,9 @@
 
 [[package]]
 name = "itertools"
-version = "0.12.1"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
 dependencies = [
  "either",
 ]
diff --git a/nearby/crypto/crypto_provider_default/Cargo.toml b/nearby/crypto/crypto_provider_default/Cargo.toml
index d79739b..c67bad2 100644
--- a/nearby/crypto/crypto_provider_default/Cargo.toml
+++ b/nearby/crypto/crypto_provider_default/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/crypto/crypto_provider_rustcrypto/Cargo.toml b/nearby/crypto/crypto_provider_rustcrypto/Cargo.toml
index 14e8ad9..c26c7db 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/Cargo.toml
+++ b/nearby/crypto/crypto_provider_rustcrypto/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/crypto/crypto_provider_stubs/Cargo.toml b/nearby/crypto/crypto_provider_stubs/Cargo.toml
index 4e8bdec..5808d6a 100644
--- a/nearby/crypto/crypto_provider_stubs/Cargo.toml
+++ b/nearby/crypto/crypto_provider_stubs/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [dependencies]
 crypto_provider = {workspace = true, features = ["std", "alloc"] }
diff --git a/nearby/crypto/crypto_provider_test/Cargo.toml b/nearby/crypto/crypto_provider_test/Cargo.toml
index 4064330..1191aae 100644
--- a/nearby/crypto/crypto_provider_test/Cargo.toml
+++ b/nearby/crypto/crypto_provider_test/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [dependencies]
 crypto_provider = {  workspace = true, features = ["raw_private_key_permit", "test_vectors", "alloc"] }
diff --git a/nearby/crypto/crypto_provider_test/fuzz/Cargo.toml b/nearby/crypto/crypto_provider_test/fuzz/Cargo.toml
index 10c3e5c..d8dbd6f 100644
--- a/nearby/crypto/crypto_provider_test/fuzz/Cargo.toml
+++ b/nearby/crypto/crypto_provider_test/fuzz/Cargo.toml
@@ -3,6 +3,7 @@
 version = "0.0.0"
 publish = false
 edition = "2021"
+license = "Apache-2.0"
 
 [package.metadata]
 cargo-fuzz = true
@@ -15,6 +16,9 @@
 [target.'cfg(fuzzing)'.dependencies]
 libfuzzer-sys.workspace = true
 
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
+
 [features]
 default = ["crypto_provider_default/default"]
 boringssl = ["crypto_provider_default/boringssl"]
diff --git a/nearby/deny.toml b/nearby/deny.toml
index aaf8eea..bc7f4c5 100644
--- a/nearby/deny.toml
+++ b/nearby/deny.toml
@@ -24,6 +24,9 @@
 ignore = [
     # comment explaining why we have to ignore it
     # "RUSTSEC-FOO",
+
+    # Need a new release of cbindgen: https://github.com/mozilla/cbindgen/issues/983
+    "RUSTSEC-2021-0145",
 ]
 # Threshold for security vulnerabilities, any vulnerability with a CVSS score
 # lower than the range specified will be ignored. Note that ignored advisories
@@ -120,7 +123,7 @@
 # published to private registries.
 # To see how to mark a crate as unpublished (to the official registry),
 # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
-ignore = true
+ignore = false
 # One or more private registries that you might publish crates to, if a crate
 # is only published to private registries, and ignore is true, the crate will
 # not have its license(s) checked
diff --git a/nearby/presence/array_ref/Cargo.toml b/nearby/presence/array_ref/Cargo.toml
index b74168b..c981ecf 100644
--- a/nearby/presence/array_ref/Cargo.toml
+++ b/nearby/presence/array_ref/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/array_view/Cargo.toml b/nearby/presence/array_view/Cargo.toml
index 8ac53d2..94a37d7 100644
--- a/nearby/presence/array_view/Cargo.toml
+++ b/nearby/presence/array_view/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/ldt/Cargo.toml b/nearby/presence/ldt/Cargo.toml
index 337dd0d..bb7c81c 100644
--- a/nearby/presence/ldt/Cargo.toml
+++ b/nearby/presence/ldt/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/ldt/fuzz/Cargo.toml b/nearby/presence/ldt/fuzz/Cargo.toml
index c192792..14659e8 100644
--- a/nearby/presence/ldt/fuzz/Cargo.toml
+++ b/nearby/presence/ldt/fuzz/Cargo.toml
@@ -2,6 +2,7 @@
 name = "ldt-fuzz"
 version.workspace = true
 publish.workspace = true
+license.workspace = true
 edition.workspace = true
 
 [package.metadata]
@@ -17,6 +18,9 @@
 [target.'cfg(fuzzing)'.dependencies]
 libfuzzer-sys.workspace = true
 
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
+
 [[bin]]
 name = "ldt_roundtrip"
 path = "src/bin/ldt_roundtrip.rs"
diff --git a/nearby/presence/ldt_np_adv/Cargo.toml b/nearby/presence/ldt_np_adv/Cargo.toml
index 5d18a28..c846439 100644
--- a/nearby/presence/ldt_np_adv/Cargo.toml
+++ b/nearby/presence/ldt_np_adv/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/ldt_np_adv/fuzz/Cargo.toml b/nearby/presence/ldt_np_adv/fuzz/Cargo.toml
index e4f497c..544b257 100644
--- a/nearby/presence/ldt_np_adv/fuzz/Cargo.toml
+++ b/nearby/presence/ldt_np_adv/fuzz/Cargo.toml
@@ -2,6 +2,7 @@
 name = "ldt-np-adv-fuzz"
 version.workspace = true
 publish.workspace = true
+license.workspace = true
 edition.workspace = true
 
 [package.metadata]
@@ -19,6 +20,9 @@
 [target.'cfg(fuzzing)'.dependencies]
 libfuzzer-sys.workspace = true
 
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
+
 [[bin]]
 name = "ldt_np_roundtrip"
 path = "src/bin/ldt_np_roundtrip.rs"
diff --git a/nearby/presence/ldt_np_adv_ffi/Cargo.toml b/nearby/presence/ldt_np_adv_ffi/Cargo.toml
index fdc5501..37176fe 100644
--- a/nearby/presence/ldt_np_adv_ffi/Cargo.toml
+++ b/nearby/presence/ldt_np_adv_ffi/Cargo.toml
@@ -3,6 +3,7 @@
 version = "0.1.0"
 edition = "2021"
 publish = false
+license = "Apache-2.0"
 
 [dependencies]
 crypto_provider_default.workspace = true
diff --git a/nearby/presence/ldt_np_adv_ffi/c/tests/ldt_ffi_tests.cc b/nearby/presence/ldt_np_adv_ffi/c/tests/ldt_ffi_tests.cc
index 65e0f68..674547a 100644
--- a/nearby/presence/ldt_np_adv_ffi/c/tests/ldt_ffi_tests.cc
+++ b/nearby/presence/ldt_np_adv_ffi/c/tests/ldt_ffi_tests.cc
@@ -279,12 +279,12 @@
 }
 
 TEST(LdtFfiTests, MultiThreadedTests) {
-  int i, num_threads = 100;
+  constexpr int num_threads = 100;
   pthread_t tid[num_threads];
   memset(tid, 0, num_threads * sizeof(pthread_t));
 
   // Create the threads
-  for (i = 0; i < num_threads; i++)
+  for (int i = 0; i < num_threads; i++)
     ASSERT_EQ(pthread_create(&tid[i], nullptr, worker_thread, (void *)&tid[i]),
               0);
 
@@ -295,6 +295,8 @@
   pthread_cond_broadcast(&cond);
 
   // Wait for them all to finish and check the status
-  for (i = 0; i < num_threads; i++) ASSERT_EQ(pthread_join(tid[i], nullptr), 0);
+  for (int i = 0; i < num_threads; i++) {
+    ASSERT_EQ(pthread_join(tid[i], nullptr), 0);
+  }
 }
 #endif
diff --git a/nearby/presence/ldt_np_jni/Cargo.toml b/nearby/presence/ldt_np_jni/Cargo.toml
index cf7c2db..698f022 100644
--- a/nearby/presence/ldt_np_jni/Cargo.toml
+++ b/nearby/presence/ldt_np_jni/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/ldt_tbc/Cargo.toml b/nearby/presence/ldt_tbc/Cargo.toml
index 8941051..bf7d363 100644
--- a/nearby/presence/ldt_tbc/Cargo.toml
+++ b/nearby/presence/ldt_tbc/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/np_adv/Cargo.toml b/nearby/presence/np_adv/Cargo.toml
index 6670192..4b79c97 100644
--- a/nearby/presence/np_adv/Cargo.toml
+++ b/nearby/presence/np_adv/Cargo.toml
@@ -3,12 +3,13 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
 
 [dependencies]
-array_view = { path = "../array_view" }
+array_view.workspace = true
 ldt_np_adv.workspace = true
 ldt.workspace = true
 np_hkdf.workspace = true
@@ -21,12 +22,15 @@
 lazy_static.workspace = true
 sink.workspace = true
 tinyvec.workspace = true
+itertools = { workspace = true, default-features = false }
 
 [features]
 default = ["alloc"]
 devtools = []
 testing = []
 alloc = ["crypto_provider/alloc"]
+# adds std::error::Error impls
+std = ["alloc"]
 
 [dev-dependencies]
 hex.workspace = true
diff --git a/nearby/presence/np_adv/fuzz/.gitignore b/nearby/presence/np_adv/fuzz/.gitignore
new file mode 100644
index 0000000..1a45eee
--- /dev/null
+++ b/nearby/presence/np_adv/fuzz/.gitignore
@@ -0,0 +1,4 @@
+target
+corpus
+artifacts
+coverage
diff --git a/nearby/presence/np_adv/fuzz/Cargo.toml b/nearby/presence/np_adv/fuzz/Cargo.toml
new file mode 100644
index 0000000..d65d5b6
--- /dev/null
+++ b/nearby/presence/np_adv/fuzz/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "np_adv_fuzz"
+version.workspace = true
+publish = false
+edition.workspace = true
+license.workspace = true
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+arbitrary = { workspace = true, features = ["derive"] }
+derive_fuzztest.workspace = true
+np_adv = { workspace = true, features = ["testing"] }
+crypto_provider.workspace = true
+crypto_provider_default = { workspace = true, features = ["std", "rustcrypto"] }
+
+[target.'cfg(fuzzing)'.dependencies]
+libfuzzer-sys.workspace = true
+
+[[bin]]
+name = "actions_de_deser"
+doc = false
+
+[[bin]]
+name = "actions_de_encoder"
+doc = false
+
+[[bin]]
+name = "actions_de_roundtrip"
+doc = false
+
diff --git a/nearby/presence/np_adv/fuzz/src/bin/actions_de_deser.rs b/nearby/presence/np_adv/fuzz/src/bin/actions_de_deser.rs
new file mode 100644
index 0000000..de06305
--- /dev/null
+++ b/nearby/presence/np_adv/fuzz/src/bin/actions_de_deser.rs
@@ -0,0 +1,41 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Fuzz test for actions data element parsing logic
+
+#![cfg_attr(fuzzing, no_main)]
+
+use arbitrary::Unstructured;
+use np_adv::extended::data_elements::DeserializedActionsDE;
+use np_adv::extended::deserialize::data_element::DataElement;
+
+#[derive(arbitrary::Arbitrary, Clone, Debug)]
+struct ActionsDeserFuzzInput {
+    data: [u8; 127],
+    #[arbitrary(with = arbitrary_de_len)]
+    de_len: usize,
+}
+
+fn arbitrary_de_len(u: &mut Unstructured) -> arbitrary::Result<usize> {
+    u.int_in_range(0..=127).map(|val| usize::try_from(val).expect("range is valid for a usize"))
+}
+
+#[derive_fuzztest::fuzztest]
+fn deserialize_actions_de(input: ActionsDeserFuzzInput) {
+    let de = DataElement::new_for_testing(0.into(), 6_u32.into(), &input.data[..input.de_len]);
+    let actions_de = DeserializedActionsDE::try_from(&de).map(|actions| {
+        // collect actions to trigger iterator parsing logic
+        let action_ids = actions.collect_action_ids();
+    });
+}
diff --git a/common/derive_fuzztest/fuzz/src/bin/integer_add.rs b/nearby/presence/np_adv/fuzz/src/bin/actions_de_encoder.rs
similarity index 60%
copy from common/derive_fuzztest/fuzz/src/bin/integer_add.rs
copy to nearby/presence/np_adv/fuzz/src/bin/actions_de_encoder.rs
index 38b8172..9108169 100644
--- a/common/derive_fuzztest/fuzz/src/bin/integer_add.rs
+++ b/nearby/presence/np_adv/fuzz/src/bin/actions_de_encoder.rs
@@ -12,12 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//! Fuzz test for actions data element encoding logic
+
 #![cfg_attr(fuzzing, no_main)]
 
-use derive_fuzztest::fuzztest;
+use np_adv::extended::data_elements::ActionsDataElement;
+use np_adv::ArrayVec;
+use np_adv_fuzz::FuzzInput;
 
-#[fuzztest]
-pub fn test(a: u8, b: u8) {
-    let _ = a.checked_add(b);
-    // a + b;  // This fails because a + b can overflow.
+#[derive_fuzztest::fuzztest]
+fn deserialize_actions_de(input: FuzzInput) {
+    let mut actions = ArrayVec::new();
+    actions.extend_from_slice(&input.data[..input.count]);
+    let actions_de = ActionsDataElement::try_from_actions(actions);
 }
diff --git a/nearby/presence/np_adv/fuzz/src/bin/actions_de_roundtrip.rs b/nearby/presence/np_adv/fuzz/src/bin/actions_de_roundtrip.rs
new file mode 100644
index 0000000..7976c5f
--- /dev/null
+++ b/nearby/presence/np_adv/fuzz/src/bin/actions_de_roundtrip.rs
@@ -0,0 +1,76 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Fuzz test for actions data element encoding and decoding round trip
+
+#![cfg_attr(fuzzing, no_main)]
+
+use crypto_provider_default::CryptoProviderImpl;
+use np_adv::credential::book::CredentialBookBuilder;
+use np_adv::credential::matched::EmptyMatchedCredential;
+use np_adv::extended::data_elements::{ActionsDataElement, DeserializedActionsDE};
+use np_adv::extended::deserialize::{Section, V1DeserializedSection};
+use np_adv::extended::serialize::{
+    AdvBuilder, AdvertisementType, SingleTypeDataElement, UnencryptedSectionEncoder,
+};
+use np_adv::{deserialization_arena, deserialize_advertisement, ArrayVec};
+use np_adv_fuzz::FuzzInput;
+
+#[derive_fuzztest::fuzztest]
+fn deserialize_actions_de(input: FuzzInput) {
+    let mut actions = ArrayVec::new();
+    actions.extend_from_slice(&input.data[..input.count]);
+    let actions_de = match ActionsDataElement::try_from_actions(actions) {
+        Ok(de) => de,
+        Err(_) => return,
+    };
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
+    section_builder.add_de(|_salt| actions_de).unwrap();
+    section_builder.add_to_advertisement::<CryptoProviderImpl>();
+    let arena = deserialization_arena!();
+    let adv = adv_builder.into_advertisement();
+    let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
+    let contents =
+        deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book)
+            .expect("Should be a valid advertisement")
+            .into_v1()
+            .expect("Should be V1");
+    assert_eq!(0, contents.invalid_sections_count());
+    let sections = contents.sections().collect::<Vec<_>>();
+    assert_eq!(1, sections.len());
+    let section = match &sections[0] {
+        V1DeserializedSection::Plaintext(s) => s,
+        _ => panic!("this is a plaintext adv"),
+    };
+    let data_elements = section.iter_data_elements().collect::<Result<Vec<_>, _>>().unwrap();
+    assert_eq!(1, data_elements.len());
+    let de = &data_elements[0];
+    assert_eq!(ActionsDataElement::DE_TYPE, de.de_type());
+    let actions_de =
+        DeserializedActionsDE::try_from(de).expect("Should succeed since this de is an actions de");
+    let decoded_actions = actions_de
+        .collect_action_ids()
+        .iter()
+        .map(|res| res.expect("valid action ids"))
+        .collect::<Vec<_>>();
+    let mut expected = actions.to_vec();
+    expected.sort();
+    expected.dedup();
+    assert_eq!(expected.as_slice(), decoded_actions.as_slice());
+}
diff --git a/nearby/presence/np_adv/fuzz/src/lib.rs b/nearby/presence/np_adv/fuzz/src/lib.rs
new file mode 100644
index 0000000..b10929c
--- /dev/null
+++ b/nearby/presence/np_adv/fuzz/src/lib.rs
@@ -0,0 +1,38 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use arbitrary::Unstructured;
+use np_adv::extended::data_elements::ActionId;
+
+#[derive(arbitrary::Arbitrary, Clone, Debug)]
+pub struct FuzzInput {
+    #[arbitrary(with = arbitrary_action_ids)]
+    pub data: [ActionId; 64],
+    #[arbitrary(with = arbitrary_actions_count)]
+    pub count: usize,
+}
+
+fn arbitrary_actions_count(u: &mut Unstructured) -> arbitrary::Result<usize> {
+    u.int_in_range(0..=64).map(|val| usize::try_from(val).unwrap())
+}
+
+fn arbitrary_action_ids(u: &mut Unstructured) -> arbitrary::Result<[ActionId; 64]> {
+    Ok(std::array::from_fn(|_| {
+        let next = u16::try_from(
+            u.int_in_range(0..=2047).expect("fuzzer should generate enough data for a u16"),
+        )
+        .expect("the range will always be a valid u16");
+        ActionId::try_from(next).expect("rang is valid for action_ids")
+    }))
+}
diff --git a/nearby/presence/np_adv/src/array_vec.rs b/nearby/presence/np_adv/src/array_vec.rs
index bcc387e..3298ace 100644
--- a/nearby/presence/np_adv/src/array_vec.rs
+++ b/nearby/presence/np_adv/src/array_vec.rs
@@ -77,6 +77,13 @@
         self.0.push(Some(value))
     }
 
+    /// Tries to place an element onto the end of the vec.
+    /// Returns back the element if the capacity is exhausted,
+    /// otherwise returns None.
+    pub fn try_push(&mut self, value: A) -> Option<A> {
+        self.0.try_push(Some(value)).unwrap_or_else(|| None)
+    }
+
     /// Returns a reference to an element at the given index.
     pub fn get(&self, index: usize) -> Option<&A> {
         self.0.get(index).and_then(|opt| opt.as_ref())
diff --git a/nearby/presence/np_adv/src/extended/data_elements/actions.rs b/nearby/presence/np_adv/src/extended/data_elements/actions.rs
new file mode 100644
index 0000000..3679555
--- /dev/null
+++ b/nearby/presence/np_adv/src/extended/data_elements/actions.rs
@@ -0,0 +1,557 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#[cfg(feature = "alloc")]
+use alloc::vec::Vec;
+
+use itertools::Itertools;
+use nom::error::ErrorKind;
+use nom::Err::Error;
+use nom::{bytes, combinator, multi};
+use sink::Sink;
+use tinyvec::ArrayVec;
+
+use crate::array_vec::ArrayVecOption;
+use crate::extended::de_type::DeType;
+use crate::extended::deserialize::data_element::DataElement;
+use crate::extended::serialize::{DeHeader, SingleTypeDataElement, WriteDataElement};
+use crate::extended::MAX_DE_LEN;
+
+#[cfg(test)]
+mod tests;
+
+pub const MAX_ACTION_ID_VALUE: u16 = 2047;
+
+pub const MAX_ACTIONS_CONTAINER_LENGTH: usize = 64;
+
+// The minimum length of a single container is 2 (one header byte + 1 byte payload) so an actions DE
+/// can fit at most 127/2 = 63 total actions containers
+pub const MAX_NUM_ACTIONS_CONTAINERS: usize = 63;
+
+/// Represents a valid action ID which can be encoded in an Actions DE
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy, Ord, PartialOrd)]
+pub struct ActionId(u16);
+
+impl ActionId {
+    /// Gets the u16 value of the action id
+    pub fn as_u16(&self) -> u16 {
+        self.0
+    }
+}
+
+// The provided value was not in the range of valid action ids [0, 2047]
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct ActionIdOutOfRange;
+
+impl TryFrom<u16> for ActionId {
+    type Error = ActionIdOutOfRange;
+
+    fn try_from(value: u16) -> Result<Self, Self::Error> {
+        if value <= MAX_ACTION_ID_VALUE {
+            Ok(Self(value))
+        } else {
+            Err(ActionIdOutOfRange)
+        }
+    }
+}
+
+impl From<ActionId> for u16 {
+    fn from(value: ActionId) -> Self {
+        value.0
+    }
+}
+
+#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+enum ContainerType {
+    DeltaEncoded,
+    DeltaEncodedWithOffset,
+    BitVectorOffset,
+}
+
+#[derive(Debug)]
+struct UnknownContainerTypeValue;
+
+impl TryFrom<u8> for ContainerType {
+    type Error = UnknownContainerTypeValue;
+
+    fn try_from(value: u8) -> Result<Self, Self::Error> {
+        match value {
+            0b00 => Ok(ContainerType::DeltaEncoded),
+            0b01 => Ok(ContainerType::DeltaEncodedWithOffset),
+            0b10 => Ok(ContainerType::BitVectorOffset),
+            _ => Err(UnknownContainerTypeValue),
+        }
+    }
+}
+
+impl From<ContainerType> for u8 {
+    fn from(value: ContainerType) -> Self {
+        match value {
+            ContainerType::DeltaEncoded => 0b00,
+            ContainerType::DeltaEncodedWithOffset => 0b01,
+            ContainerType::BitVectorOffset => 0b10,
+        }
+    }
+}
+
+/// Actions data element parsed from a slice of bytes, this type references the original slice of
+/// bytes which was parsed
+#[derive(Debug)]
+pub struct DeserializedActionsDE<'adv> {
+    containers: ArrayVecOption<DeserializedActionsContainer<'adv>, MAX_NUM_ACTIONS_CONTAINERS>,
+}
+
+impl<'adv> SingleTypeDataElement for DeserializedActionsDE<'adv> {
+    const DE_TYPE: DeType = DeType::const_from(0x06);
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum ActionsDeserializationError {
+    InvalidTypeCode,
+    InvalidContainerLength,
+    InvalidContainerType,
+    GenericDeserializationError,
+}
+
+/// Convert a deserialized DE into one you can serialize
+impl<'a> TryFrom<&'a DataElement<'a>> for DeserializedActionsDE<'a> {
+    type Error = ActionsDeserializationError;
+
+    fn try_from(value: &'a DataElement<'a>) -> Result<Self, Self::Error> {
+        if value.de_type() != Self::DE_TYPE {
+            return Err(ActionsDeserializationError::InvalidTypeCode);
+        }
+        DeserializedActionsDE::deserialize(value.contents()).map_err(|e| match e {
+            Error(e) => match e.code {
+                ErrorKind::Eof | ErrorKind::Verify => {
+                    ActionsDeserializationError::InvalidContainerLength
+                }
+                ErrorKind::MapOpt => ActionsDeserializationError::InvalidContainerType,
+                _ => ActionsDeserializationError::GenericDeserializationError,
+            },
+            _ => ActionsDeserializationError::GenericDeserializationError,
+        })
+    }
+}
+
+impl<'adv> DeserializedActionsDE<'adv> {
+    /// Returns a collection over of all action ids contained within the data element
+    #[cfg(feature = "alloc")]
+    pub fn collect_action_ids(&self) -> Vec<Result<ActionId, ActionIdOutOfRange>> {
+        let mut result = Vec::new();
+        self.containers.iter().for_each(|container| {
+            container.iter_action_ids().for_each(|action| result.push(action))
+        });
+        result
+    }
+
+    /// Parses the raw bytes of an Actions DE into an intermediate format, that is the contents
+    /// are separated into containers with their corresponding container type, but no further decoding
+    /// is done on the contents of the containers, leaving the bytes in their compact encoded format.
+    fn deserialize(
+        de_contents: &'adv [u8],
+    ) -> Result<DeserializedActionsDE, nom::Err<nom::error::Error<&[u8]>>> {
+        combinator::all_consuming(multi::fold_many_m_n(
+            1,
+            MAX_NUM_ACTIONS_CONTAINERS,
+            DeserializedActionsContainer::parse,
+            Self::new_empty,
+            |mut acc, item| {
+                acc.add_container(item);
+                acc
+            },
+        ))(de_contents)
+        .map(|(rem, actions)| {
+            debug_assert!(rem.is_empty());
+            actions
+        })
+    }
+
+    /// Initializes an actions de with empty contents
+    fn new_empty() -> Self {
+        let internal = ArrayVecOption::default();
+        Self { containers: internal }
+    }
+
+    /// Appends a container to the DE, panicking in the case where the max length is exceeded
+    fn add_container(&mut self, container: DeserializedActionsContainer<'adv>) {
+        self.containers.push(container);
+    }
+}
+
+#[derive(Debug)]
+struct DeserializedActionsContainer<'adv> {
+    container_type: ContainerType,
+    offset: u16,
+    payload: &'adv [u8],
+}
+
+impl<'adv> DeserializedActionsContainer<'adv> {
+    fn parse(bytes: &'adv [u8]) -> nom::IResult<&[u8], Self> {
+        let (input, (container_type, container_encoded_len)) =
+            combinator::map_opt(nom::number::complete::u8, |b| {
+                // right shift by 6 to obtain the upper 2 type bits TTLLLLLL
+                let type_value = b >> 6;
+                let encoded_len = b & 0b00111111;
+                ContainerType::try_from(type_value)
+                    .map(|container_type| (container_type, encoded_len))
+                    .ok()
+            })(bytes)?;
+
+        let (input, payload_bytes) = bytes::complete::take(container_encoded_len + 1)(input)?;
+
+        let (payload, offset) = match container_type {
+            ContainerType::DeltaEncoded => (payload_bytes, 0u16),
+            // A container which only contains the sub header offset and no
+            // subsequent data is disallowed by the spec
+            ContainerType::DeltaEncodedWithOffset | ContainerType::BitVectorOffset => {
+                combinator::verify(nom::number::complete::u8, |_| payload_bytes.len() > 1)(
+                    payload_bytes,
+                )
+                .map(|(remaining, byte)| (remaining, u16::from(byte) * 8))?
+            }
+        };
+        Ok((input, DeserializedActionsContainer { container_type, offset, payload }))
+    }
+
+    fn iter_action_ids(&self) -> ActionsContainerIterator {
+        ActionsContainerIterator::new(self)
+    }
+}
+
+/// Iterates all the action ids in a container
+enum ActionsContainerIterator<'c> {
+    DeltaEncoded { first: bool, delta: u16, bytes: &'c [u8] },
+    BitVector { current_offset: u16, position_in_byte: u8, current_byte: u8, bytes: &'c [u8] },
+}
+
+impl<'c> ActionsContainerIterator<'c> {
+    fn new(container: &'c DeserializedActionsContainer) -> Self {
+        match container.container_type {
+            ContainerType::DeltaEncoded | ContainerType::DeltaEncodedWithOffset => {
+                Self::DeltaEncoded {
+                    delta: container.offset,
+                    bytes: container.payload,
+                    first: true,
+                }
+            }
+            ContainerType::BitVectorOffset => {
+                let (remaining, byte) =
+                    nom::number::complete::u8::<&[u8], nom::error::Error<_>>(container.payload)
+                        .expect(
+                        "we verified bytes has at least one byte when parsing original container",
+                    );
+                Self::BitVector {
+                    current_offset: container.offset,
+                    position_in_byte: 0,
+                    current_byte: byte,
+                    bytes: remaining,
+                }
+            }
+        }
+    }
+}
+
+impl<'c> Iterator for ActionsContainerIterator<'c> {
+    type Item = Result<ActionId, ActionIdOutOfRange>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match self {
+            ActionsContainerIterator::DeltaEncoded { delta, bytes, first } => {
+                nom::number::complete::u8::<&[u8], nom::error::Error<_>>(bytes)
+                    .map(|(rem, b)| {
+                        let mut result = u16::from(b) + *delta;
+                        if *first {
+                            *first = false;
+                        } else {
+                            result += 1;
+                        }
+                        // save the result to be used as the next delta
+                        *delta = result;
+                        // update the slice to no longer include the current byte since we just used it
+                        *bytes = rem;
+                        result.try_into()
+                    })
+                    .ok()
+            }
+            ActionsContainerIterator::BitVector {
+                current_offset,
+                position_in_byte,
+                current_byte,
+                bytes,
+            } => {
+                while *position_in_byte <= 7 || !bytes.is_empty() {
+                    // take another byte and update position and offset
+                    if *position_in_byte > 7 && !bytes.is_empty() {
+                        let (remaining, byte) =
+                            nom::number::complete::u8::<&[u8], nom::error::Error<_>>(bytes)
+                                .expect("we verified bytes has at least one byte");
+                        *current_byte = byte;
+                        *bytes = remaining;
+                        *current_offset += 8;
+                        *position_in_byte = 0
+                    }
+                    if ((0b10000000 >> *position_in_byte) & *current_byte) != 0 {
+                        let result: Option<Result<ActionId, _>> =
+                            Some((*current_offset + u16::from(*position_in_byte)).try_into());
+                        *position_in_byte += 1;
+                        return result;
+                    }
+                    *position_in_byte += 1;
+                }
+                None
+            }
+        }
+    }
+}
+/// Actions data element consists of one or more containers
+#[derive(Debug)]
+pub struct ActionsDataElement {
+    containers: ArrayVecOption<ActionsContainer, MAX_NUM_ACTIONS_CONTAINERS>,
+    len: usize,
+}
+
+impl ActionsDataElement {
+    /// Creates an Actions DE storing the provided action ids in memory using the default encoding
+    /// scheme of simple delta encoding. This limits the amount of encodable actions to 64, and the
+    /// action id range to what can fit in a byte after being delta encoded.
+    // TODO: provide more flexibility over which encoding scheme is used
+    pub fn try_from_actions(
+        actions: ArrayVec<[ActionId; 64]>,
+    ) -> Result<Self, ActionsDataElementError> {
+        DeltaEncodedContainer::try_from_actions(actions).map(|c| {
+            let mut actions_de = ActionsDataElement::new_empty();
+            // This will always succeed since we are only adding one container which cannot exceed
+            // the 127 max DE length
+            let res = actions_de.try_add_container(c.into());
+            debug_assert!(res.is_none());
+            actions_de
+        })
+    }
+
+    /// Initializes an actions de with empty contents
+    fn new_empty() -> Self {
+        let internal = ArrayVecOption::default();
+        Self { containers: internal, len: 0 }
+    }
+
+    /// Appends a container to the DE, returning back the container in the event that max length of
+    /// a DE would be exceeded or returning `None` in the case that it has been successfully added
+    fn try_add_container(&mut self, container: ActionsContainer) -> Option<ActionsContainer> {
+        if self.len + container.payload.len() + 1 > MAX_DE_LEN {
+            Some(container)
+        } else {
+            self.len = self.len + container.payload.len() + 1;
+            self.containers.push(container);
+            None
+        }
+    }
+}
+
+/// The actions container type saved in the DE which can represent any of the 3 existing container
+/// types as specified in its container_type field. This can be converted into from any of the 3
+/// more specific actions container types and is what is stored in `ActionsDataElement`
+#[derive(Debug, Copy, Clone)]
+struct ActionsContainer {
+    container_type: ContainerType,
+    payload: ArrayVec<[u8; MAX_ACTIONS_CONTAINER_LENGTH]>,
+}
+
+trait ContainerEncoder {
+    const CONTAINER_TYPE: ContainerType;
+    /// encodes the bytes of the container after the initial container header
+    fn encoded_payload(&self) -> ArrayVec<[u8; MAX_ACTIONS_CONTAINER_LENGTH]>;
+}
+
+impl<C: ContainerEncoder> From<C> for ActionsContainer {
+    fn from(value: C) -> Self {
+        Self { container_type: C::CONTAINER_TYPE, payload: value.encoded_payload() }
+    }
+}
+
+#[derive(Debug)]
+struct DeltaEncodedContainer {
+    payload: ArrayVec<[u8; MAX_ACTIONS_CONTAINER_LENGTH]>,
+}
+
+impl DeltaEncodedContainer {
+    fn try_from_actions(
+        actions: ArrayVec<[ActionId; MAX_ACTIONS_CONTAINER_LENGTH]>,
+    ) -> Result<Self, ActionsDataElementError> {
+        let sorted = sort(actions);
+        let mut payload = ArrayVec::<[u8; MAX_ACTIONS_CONTAINER_LENGTH]>::default();
+        let first =
+            u8::try_from(sorted.first().ok_or(ActionsDataElementError::EmptyActions)?.as_u16())
+                .map_err(|_| ActionsDataElementError::ActionIdDeltaOverflow)?;
+        payload.push(first);
+        let remaining = delta_encoding(sorted)?;
+        payload.extend_from_slice(remaining.as_slice());
+        Ok(Self { payload })
+    }
+}
+
+impl ContainerEncoder for DeltaEncodedContainer {
+    const CONTAINER_TYPE: ContainerType = ContainerType::DeltaEncoded;
+    fn encoded_payload(&self) -> ArrayVec<[u8; MAX_ACTIONS_CONTAINER_LENGTH]> {
+        self.payload
+    }
+}
+
+fn sort<const N: usize>(actions: ArrayVec<[ActionId; N]>) -> ArrayVec<[ActionId; N]> {
+    let mut sorted = actions;
+    sorted.sort_unstable_by_key(|x| *x);
+    sorted
+}
+
+/// Calculates the delta encoded for the given sorted collection offset by 1, so an encoding of
+/// 0 represents a delta of 1, and an encoding of 0xFF represents a delta of 256
+fn delta_encoding<const N: usize>(
+    sorted: ArrayVec<[ActionId; N]>,
+) -> Result<ArrayVec<[u8; N]>, ActionsDataElementError> {
+    sorted
+        .iter()
+        .tuple_windows()
+        .filter(|(a, b)| *a != *b)
+        .map(|(a, b)| u8::try_from(b.as_u16() - (a.as_u16() + 1)))
+        .collect::<Result<ArrayVec<[u8; N]>, _>>()
+        .map_err(|_| ActionsDataElementError::ActionIdDeltaOverflow)
+}
+
+#[derive(Debug)]
+struct DeltaEncodedOffsetContainer {
+    payload: ArrayVec<[u8; MAX_ACTIONS_CONTAINER_LENGTH]>,
+}
+
+impl DeltaEncodedOffsetContainer {
+    #[allow(unused)]
+    fn try_from_actions(
+        actions: ArrayVec<[ActionId; MAX_ACTIONS_CONTAINER_LENGTH - 1]>,
+    ) -> Result<Self, ActionsDataElementError> {
+        let sorted = sort(actions);
+        let first_action = sorted.first().ok_or(ActionsDataElementError::EmptyActions)?.as_u16();
+        let mut payload = ArrayVec::<[u8; MAX_ACTIONS_CONTAINER_LENGTH]>::default();
+        let offset = u8::try_from(first_action / 8)
+            .expect("Max action id 2047 divided by 8 is always within range of a valid u8");
+        payload.push(offset);
+        let first_action_encoding = u8::try_from(first_action - u16::from(offset) * 8)
+            .expect("This will always fit into a u8 because of the offset subtraction");
+        payload.push(first_action_encoding);
+
+        let remaining = delta_encoding(sorted)?;
+        payload.extend_from_slice(remaining.as_slice());
+        Ok(Self { payload })
+    }
+}
+
+impl ContainerEncoder for DeltaEncodedOffsetContainer {
+    const CONTAINER_TYPE: ContainerType = ContainerType::DeltaEncodedWithOffset;
+    fn encoded_payload(&self) -> ArrayVec<[u8; MAX_ACTIONS_CONTAINER_LENGTH]> {
+        self.payload
+    }
+}
+
+/// The maximum amount of expressible action_ids in a single bit vector container.
+/// Each byte can hold 8 unique actions with a max of 64 bytes per container.
+const MAX_BIT_VECTOR_ACTIONS: usize = MAX_ACTIONS_CONTAINER_LENGTH * 8;
+
+#[derive(Debug)]
+struct BitVectorOffsetContainer {
+    payload: ArrayVec<[u8; MAX_ACTIONS_CONTAINER_LENGTH]>,
+}
+
+impl BitVectorOffsetContainer {
+    #[allow(unused)]
+    fn try_from_actions(actions: &mut [ActionId]) -> Result<Self, ActionsDataElementError> {
+        if actions.len() > MAX_BIT_VECTOR_ACTIONS {
+            return Err(ActionsDataElementError::TooManyActions);
+        }
+        actions.sort_unstable_by_key(|x| *x);
+
+        let mut payload = ArrayVec::<[u8; MAX_ACTIONS_CONTAINER_LENGTH]>::new();
+        let first = actions.first().ok_or(ActionsDataElementError::EmptyActions)?.as_u16();
+        let offset = u8::try_from(first / 8)
+            .expect("Max action id 2047 divided by 8 is always within range of a valid u8");
+        payload.push(offset);
+
+        let max_value = actions.last().expect("we have verified above that sorted is not empty");
+        let bytes_required = (max_value.as_u16() / 8) + 1 - u16::from(offset);
+        for _ in 0..bytes_required {
+            if payload.try_push(0).is_some() {
+                return Err(ActionsDataElementError::ActionIdOutOfRange);
+            }
+        }
+        actions.iter().for_each(|a| {
+            // which byte this action id belongs in
+            let index = usize::from(a.as_u16() / 8 + 1 - u16::from(offset));
+            // how far the action is shifted from the most significant bit in the byte
+            let shift = a.as_u16() % 8;
+            payload[index] |= 0b1000_0000 >> shift;
+        });
+
+        Ok(Self { payload })
+    }
+}
+
+impl ContainerEncoder for BitVectorOffsetContainer {
+    const CONTAINER_TYPE: ContainerType = ContainerType::BitVectorOffset;
+    fn encoded_payload(&self) -> ArrayVec<[u8; MAX_ACTIONS_CONTAINER_LENGTH]> {
+        self.payload
+    }
+}
+
+/// Errors that can occur constructing an [ActionsDataElement].
+#[derive(Debug, PartialEq, Eq)]
+#[allow(unused)]
+pub enum ActionsDataElementError {
+    /// Must specify at least one action id to encode
+    EmptyActions,
+    /// The provided ActionIds cannot be delta encoded into u8 values
+    ActionIdDeltaOverflow,
+    /// The provided range of action ids cannot be encoded into a single container
+    ActionIdOutOfRange,
+    /// Too many actions provided than what can be encoded into a single container
+    TooManyActions,
+}
+
+impl SingleTypeDataElement for ActionsDataElement {
+    const DE_TYPE: DeType = DeType::const_from(0x06);
+}
+
+impl WriteDataElement for ActionsDataElement {
+    fn de_header(&self) -> DeHeader {
+        DeHeader::new(
+            Self::DE_TYPE,
+            // each containers length is the header byte +  the length of its encoded contents
+            self.containers
+                .iter()
+                .map(|a| 1 + a.payload.len())
+                .sum::<usize>()
+                .try_into()
+                .expect("An actions DE will always be <= 127, this is enforced upon creation"),
+        )
+    }
+
+    fn write_de_contents<S: Sink<u8>>(&self, sink: &mut S) -> Option<()> {
+        // write actions container header and bytes for each container in the de
+        let mut encoded_actions_de_bytes = ArrayVec::<[u8; MAX_DE_LEN]>::new();
+        self.containers.iter().for_each(|a| {
+            let header_byte = (u8::from(a.container_type) << 6) | ((a.payload.len() as u8) - 1);
+            // This will not panic because the length of the actions DE is checked during creation
+            // to not exceed the max limit of 127
+            encoded_actions_de_bytes.extend_from_slice(&[header_byte]);
+            encoded_actions_de_bytes.extend_from_slice(a.payload.as_slice());
+        });
+        sink.try_extend_from_slice(encoded_actions_de_bytes.as_slice())
+    }
+}
diff --git a/nearby/presence/np_adv/src/extended/data_elements/actions/tests.rs b/nearby/presence/np_adv/src/extended/data_elements/actions/tests.rs
new file mode 100644
index 0000000..3abdbd1
--- /dev/null
+++ b/nearby/presence/np_adv/src/extended/data_elements/actions/tests.rs
@@ -0,0 +1,636 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![allow(clippy::unwrap_used)]
+
+use alloc::collections::VecDeque;
+use alloc::vec;
+use alloc::vec::Vec;
+
+extern crate std;
+
+use crate::credential::book::{CachedSliceCredentialBook, CredentialBook, CredentialBookBuilder};
+use crate::credential::matched::EmptyMatchedCredential;
+use crate::deserialization_arena::DeserializationArena;
+use crate::extended::data_elements::actions::{
+    ActionsContainer, ActionsDataElement, ActionsDataElementError, ActionsDeserializationError,
+    BitVectorOffsetContainer, ContainerEncoder, ContainerType, DeltaEncodedContainer,
+    DeltaEncodedOffsetContainer, DeserializedActionsDE, MAX_ACTIONS_CONTAINER_LENGTH,
+};
+use crate::extended::data_elements::ActionId;
+use crate::extended::de_type::DeType;
+use crate::extended::deserialize::data_element::DataElement;
+use crate::extended::deserialize::{Section, V1AdvertisementContents, V1DeserializedSection};
+use crate::extended::serialize::{
+    AdvBuilder, AdvertisementType, EncodedAdvertisement, SingleTypeDataElement,
+    UnencryptedSectionEncoder,
+};
+use crate::{deserialization_arena, deserialize_advertisement};
+use crypto_provider_default::CryptoProviderImpl;
+use nom::error::ErrorKind;
+use nom::Err::Error;
+use np_hkdf::v1_salt::DataElementOffset;
+use rand::distributions::uniform::SampleRange;
+use rand::distributions::Standard;
+use rand::prelude::Distribution;
+use rand::rngs::StdRng;
+use rand::{Rng, SeedableRng};
+use tinyvec::{array_vec, ArrayVec};
+
+#[test]
+fn actions_de_round_trip() {
+    let mut actions = ArrayVec::<[ActionId; 64]>::new();
+    actions.extend_from_slice([100u16, 500, 300, 600].map(|v| v.try_into().unwrap()).as_slice());
+    let actions_de = ActionsDataElement::try_from_actions(actions).expect("should succeed");
+    let adv = create_adv_with_de(actions_de);
+    let arena = deserialization_arena!();
+    let cred_book = create_empty_cred_book();
+    let contents = deser_into_v1_contents(arena, adv.as_slice(), &cred_book);
+    assert_eq!(0, contents.invalid_sections_count());
+    let sections = contents.sections().collect::<Vec<_>>();
+    assert_eq!(1, sections.len());
+    let section = match &sections[0] {
+        V1DeserializedSection::Plaintext(s) => s,
+        _ => panic!("this is a plaintext adv"),
+    };
+    let data_elements = section.iter_data_elements().collect::<Result<Vec<_>, _>>().unwrap();
+    assert_eq!(1, data_elements.len());
+    let de = &data_elements[0];
+    assert_eq!(ActionsDataElement::DE_TYPE, de.de_type());
+    let actions_de =
+        DeserializedActionsDE::try_from(de).expect("Should succeed since this de is an actions de");
+    assert_eq!(
+        actions_de
+            .collect_action_ids()
+            .iter()
+            .map(|res| res.expect("valid action ids").as_u16())
+            .collect::<Vec<_>>(),
+        vec![100, 300, 500, 600]
+    )
+}
+
+#[test]
+fn randomized_containers_roundtrip_tests() {
+    let mut rng = StdRng::from_entropy();
+    for _ in 0..10_000 {
+        let mut actions_de = ActionsDataElement::new_empty();
+        let mut expected = VecDeque::new();
+        for _num_containers in 0..rng.gen_range(1..MAX_ACTIONS_CONTAINER_LENGTH) {
+            let (container, actions) = gen_random_container(&mut rng);
+            if actions_de.try_add_container(container).is_none() {
+                expected.push_back(actions);
+            }
+        }
+        let adv = create_adv_with_de(actions_de);
+        let arena = deserialization_arena!();
+        let cred_book = create_empty_cred_book();
+        let contents = deser_into_v1_contents(arena, adv.as_slice(), &cred_book);
+        assert_eq!(0, contents.invalid_sections_count());
+        let sections = contents.sections().collect::<Vec<_>>();
+        assert_eq!(1, sections.len());
+        let section = match &sections[0] {
+            V1DeserializedSection::Plaintext(s) => s,
+            _ => panic!("this is a plaintext adv"),
+        };
+        let data_elements = section.iter_data_elements().collect::<Result<Vec<_>, _>>().unwrap();
+        assert_eq!(1, data_elements.len());
+        let de = &data_elements[0];
+        assert_eq!(ActionsDataElement::DE_TYPE, de.de_type());
+        let actions_de = DeserializedActionsDE::try_from(de)
+            .expect("Should succeed since this de is an actions de");
+        assert_eq!(actions_de.containers.len(), expected.len());
+        let mut expected_action_ids =
+            expected.iter().flatten().map(|action| action.as_u16()).collect::<Vec<_>>();
+        expected_action_ids.sort();
+        expected_action_ids.dedup();
+        let mut decoded_action_ids =
+            actions_de.collect_action_ids().iter().map(|x| x.unwrap().as_u16()).collect::<Vec<_>>();
+        decoded_action_ids.sort();
+        decoded_action_ids.dedup();
+        assert_eq!(expected_action_ids, decoded_action_ids);
+    }
+}
+
+#[test]
+fn roundtrip_edge_case_actions_ids_in_last_bit_of_byte() {
+    let mut actions =
+        [1260u16, 1268, 1269, 1273, 1285, 1288, 1302, 1303, 1306, 1308, 1310, 1312, 1320, 1330]
+            .map(|v| ActionId::try_from(v).unwrap());
+    let container = BitVectorOffsetContainer::try_from_actions(actions.as_mut_slice()).unwrap();
+    let mut de = ActionsDataElement::new_empty();
+    assert!(de.try_add_container(container.into()).is_none());
+    let adv = create_adv_with_de(de);
+    let arena = deserialization_arena!();
+    let cred_book = create_empty_cred_book();
+    let contents = deser_into_v1_contents(arena, adv.as_slice(), &cred_book);
+    assert_eq!(0, contents.invalid_sections_count());
+    let sections = contents.sections().collect::<Vec<_>>();
+    assert_eq!(1, sections.len());
+    let section = match &sections[0] {
+        V1DeserializedSection::Plaintext(s) => s,
+        _ => panic!("this is a plaintext adv"),
+    };
+    let data_elements = section.iter_data_elements().collect::<Result<Vec<_>, _>>().unwrap();
+    assert_eq!(1, data_elements.len());
+    let de = &data_elements[0];
+    assert_eq!(ActionsDataElement::DE_TYPE, de.de_type());
+    let actions_de =
+        DeserializedActionsDE::try_from(de).expect("Should succeed since this de is an actions de");
+    let decoded_actions =
+        actions_de.collect_action_ids().iter().map(|c| c.expect("")).collect::<Vec<ActionId>>();
+    assert_eq!(actions.as_slice(), decoded_actions.as_slice())
+}
+
+fn create_adv_with_de(actions_de: ActionsDataElement) -> EncodedAdvertisement {
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
+    section_builder.add_de(|_salt| actions_de).unwrap();
+    section_builder.add_to_advertisement::<CryptoProviderImpl>();
+    adv_builder.into_advertisement()
+}
+
+fn create_empty_cred_book<'a>() -> CachedSliceCredentialBook<'a, EmptyMatchedCredential, 0, 0> {
+    CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[])
+}
+
+pub fn deser_into_v1_contents<'adv, 'cred, B>(
+    arena: DeserializationArena<'adv>,
+    adv: &'adv [u8],
+    cred_book: &'cred B,
+) -> V1AdvertisementContents<'adv, B::Matched>
+where
+    B: CredentialBook<'cred>,
+{
+    deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv, cred_book)
+        .expect("Should be a valid advertisement")
+        .into_v1()
+        .expect("Should be V1")
+}
+
+impl Distribution<ContainerType> for Standard {
+    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> ContainerType {
+        match rng.gen_range(0..=2) {
+            0 => ContainerType::DeltaEncoded,
+            1 => ContainerType::DeltaEncodedWithOffset,
+            _ => ContainerType::BitVectorOffset,
+        }
+    }
+}
+
+fn gen_actions_in_rng<const N: usize, R: SampleRange<u16> + Clone>(
+    rng: &mut StdRng,
+    gen_range: R,
+) -> ArrayVec<[ActionId; N]> {
+    let mut actions = ArrayVec::new();
+    for _ in 0..rng.gen_range(1..N) {
+        let action_id: u16 = rng.gen_range(gen_range.clone());
+        actions.push(ActionId::try_from(action_id).expect("range is a valid action id"))
+    }
+    actions
+}
+
+fn gen_delta_actions<const N: usize, R: SampleRange<u16> + Clone>(
+    rng: &mut StdRng,
+    gen_range: R,
+) -> ArrayVec<[ActionId; N]> {
+    let mut actions = ArrayVec::new();
+    let mut previous = rng.gen_range(gen_range.clone());
+    actions.push(ActionId::try_from(previous).unwrap());
+    for _ in 0..rng.gen_range(1..N - 1) {
+        // make sure generated actions are within a valid delta range of other actions
+        let lower_bound = previous.saturating_sub(256);
+        let upper_bound = if previous + 256 > 2047 { 2047 } else { previous + 256 };
+        let action_id = rng.gen_range(lower_bound..=upper_bound);
+        actions.push(ActionId::try_from(action_id).unwrap());
+        previous = action_id
+    }
+    actions
+}
+
+fn gen_random_container(rng: &mut StdRng) -> (ActionsContainer, Vec<ActionId>) {
+    let container_type: ContainerType = rng.gen();
+    match container_type {
+        ContainerType::DeltaEncoded => {
+            let actions = gen_delta_actions(rng, 0..=255);
+            (DeltaEncodedContainer::try_from_actions(actions).unwrap().into(), actions.to_vec())
+        }
+        ContainerType::DeltaEncodedWithOffset => {
+            let actions = gen_delta_actions(rng, 0..=2047);
+            (
+                DeltaEncodedOffsetContainer::try_from_actions(actions).unwrap().into(),
+                actions.to_vec(),
+            )
+        }
+        ContainerType::BitVectorOffset => {
+            let range_lower_bound = rng.gen_range(0u16..2047 - 495);
+            let range_upper_bound = range_lower_bound + 495;
+            let mut actions =
+                gen_actions_in_rng::<512, _>(rng, range_lower_bound..=range_upper_bound);
+            (
+                BitVectorOffsetContainer::try_from_actions(actions.as_mut_slice()).unwrap().into(),
+                actions.to_vec(),
+            )
+        }
+    }
+}
+
+#[test]
+fn parse_single_container_delta_encoded() {
+    let bytes = [0x01, 0x64, 0xC7];
+    let actions_de =
+        DeserializedActionsDE::deserialize(&bytes).expect("bytes parse into valid actions DE");
+    let action_containers = &actions_de.containers;
+    assert_eq!(action_containers.len(), 1);
+    let container = action_containers.first().unwrap();
+    assert_eq!(container.container_type, ContainerType::DeltaEncoded);
+    assert_eq!(container.payload, &[0x64, 0xC7]);
+
+    let action_ids: Vec<_> = container
+        .iter_action_ids()
+        .map(|id| id.expect("test data contains action ids within valid range").as_u16())
+        .collect();
+    assert_eq!(action_ids, vec![100u16, 300]);
+    assert_eq!(
+        actions_de.collect_action_ids(),
+        vec![Ok(100u16.try_into().unwrap()), Ok(300.try_into().unwrap())]
+    );
+}
+
+#[test]
+fn encode_single_container_delta_encoded() {
+    let actions = array_vec!([ActionId; 64] => ActionId::try_from(100).unwrap(), ActionId::try_from(300).unwrap());
+    let container = DeltaEncodedContainer::try_from_actions(actions).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0x64, 0xC7])
+}
+
+#[test]
+fn encode_single_container_delta_encoded_delta_1() {
+    let actions = array_vec!([ActionId; 64] => ActionId::try_from(100).unwrap(), ActionId::try_from(101).unwrap());
+    let container = DeltaEncodedContainer::try_from_actions(actions).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0x64, 0x00])
+}
+
+#[test]
+fn encode_single_container_delta_encoded_max_delta() {
+    let actions = array_vec!([ActionId; 64] => ActionId::try_from(100).unwrap(), ActionId::try_from(356).unwrap());
+    let container = DeltaEncodedContainer::try_from_actions(actions).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0x64, 0xFF])
+}
+
+#[test]
+fn parse_single_container_delta_encoded_with_offset() {
+    let bytes = [0x42, 0x06, 0x02, 0x66];
+    let actions_de =
+        DeserializedActionsDE::deserialize(&bytes).expect("provided bytes are valid actions de");
+    assert_eq!(actions_de.containers.len(), 1);
+    let container = actions_de.containers.first().unwrap();
+    assert_eq!(container.container_type, ContainerType::DeltaEncodedWithOffset);
+    assert_eq!(container.offset, 48);
+    let actions: Vec<u16> = container
+        .iter_action_ids()
+        .map(|action| action.expect("Encoded id is in valid range").as_u16())
+        .collect();
+    assert_eq!(actions, vec![50, 153]);
+    assert_eq!(
+        actions_de.collect_action_ids(),
+        vec![Ok(50.try_into().unwrap()), Ok(153.try_into().unwrap())]
+    );
+}
+
+#[test]
+fn encode_single_container_delta_encoded_with_offset() {
+    let actions = array_vec!([ActionId; 63] => ActionId::try_from(50).unwrap(), ActionId::try_from(153).unwrap());
+    let container = DeltaEncodedOffsetContainer::try_from_actions(actions).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0x06, 0x02, 0x66])
+}
+
+#[test]
+fn encode_single_container_delta_encoded_with_offset_delta_of_1() {
+    let actions = array_vec!([ActionId; 63] => ActionId::try_from(50).unwrap(), ActionId::try_from(51).unwrap());
+    let container = DeltaEncodedOffsetContainer::try_from_actions(actions).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0x06, 0x02, 0x00])
+}
+
+#[test]
+fn encode_single_container_delta_encoded_with_offset_max_delta() {
+    let actions = array_vec!([ActionId; 63] => ActionId::try_from(50).unwrap(), ActionId::try_from(306).unwrap());
+    let container = DeltaEncodedOffsetContainer::try_from_actions(actions).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0x06, 0x02, 0xFF])
+}
+
+#[test]
+fn encode_single_container_delta_encoded_with_offset_delta_of_0() {
+    let actions = array_vec!([ActionId; 63] => ActionId::try_from(48).unwrap(), ActionId::try_from(304).unwrap());
+    let container = DeltaEncodedOffsetContainer::try_from_actions(actions).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0x06, 0x00, 0xFF])
+}
+
+#[test]
+fn parse_single_container_bit_vector_offset() {
+    let bytes = [0x82, 0x01, 0x5C, 0x08];
+    let actions_de =
+        DeserializedActionsDE::deserialize(&bytes).expect("provided bytes are valid actions de");
+    assert_eq!(actions_de.containers.len(), 1);
+    let container = actions_de.containers.first().unwrap();
+    assert_eq!(container.container_type, ContainerType::BitVectorOffset);
+    assert_eq!(container.offset, 8);
+    let actions: Vec<u16> = container
+        .iter_action_ids()
+        .map(|action| action.expect("Encoded id is in valid range").as_u16())
+        .collect();
+    assert_eq!(actions, vec![9, 11, 12, 13, 20]);
+}
+
+#[test]
+fn encode_single_container_bit_vector_offset() {
+    let mut actions = [9, 11, 12, 13, 20].map(|x| ActionId::try_from(x).unwrap());
+    let container =
+        BitVectorOffsetContainer::try_from_actions(actions.as_mut_slice()).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0x01, 0x5C, 0x08])
+}
+
+#[test]
+fn encode_single_container_bit_vector_offset_offset_of_0() {
+    let mut actions = [0, 3, 5, 7].map(|x| ActionId::try_from(x).unwrap());
+    let container =
+        BitVectorOffsetContainer::try_from_actions(actions.as_mut_slice()).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0x00, 0b1001_0101])
+}
+
+#[test]
+fn encode_single_container_bit_vector_offset_max_offset() {
+    let mut actions = [2046, 2047].map(|x| ActionId::try_from(x).unwrap());
+    let container =
+        BitVectorOffsetContainer::try_from_actions(actions.as_mut_slice()).expect("Should succeed");
+    assert_eq!(container.encoded_payload().as_slice(), &[0xFF, 0b0000_0011])
+}
+
+#[test]
+fn encode_single_container_bit_vector_max_range() {
+    // A container is at most 64 bytes. One byte is always used for the offset so that leaves 63 total bytes of
+    // bit vector encoded action ids where each bit represents an action. Since the first byte represent 0-7,
+    // byte 63 represents actions 496-503, making 503 the maximum possible action id which can be encoded
+    // in this container
+    let mut actions = [0, 503].map(|x| ActionId::try_from(x).unwrap());
+    let container =
+        BitVectorOffsetContainer::try_from_actions(actions.as_mut_slice()).expect("Should succeed");
+    let mut expected = [0; 64];
+    expected[1] = 0b1000_0000;
+    expected[63] = 0b0000_0001;
+    assert_eq!(container.encoded_payload().as_slice(), &expected);
+}
+
+#[test]
+fn encode_single_container_bit_vector_invalid_range() {
+    let mut actions = [1, 504].map(|x| ActionId::try_from(x).unwrap());
+    let err = BitVectorOffsetContainer::try_from_actions(actions.as_mut_slice()).unwrap_err();
+    assert_eq!(err, ActionsDataElementError::ActionIdOutOfRange)
+}
+
+#[test]
+fn encode_single_container_bit_vector_max_range_with_offset() {
+    // now 559 should still be in range since this is offset by 7 bytes
+    let mut actions = [56, 559].map(|x| ActionId::try_from(x).unwrap());
+    let container =
+        BitVectorOffsetContainer::try_from_actions(actions.as_mut_slice()).expect("Should succeed");
+    let mut expected = [0; 64];
+    expected[0] = 7;
+    expected[1] = 0b1000_0000;
+    expected[63] = 0b0000_0001;
+    assert_eq!(container.encoded_payload().as_slice(), &expected);
+}
+
+#[test]
+fn encode_single_container_bit_vector_out_of_range_with_offset() {
+    let mut actions = [56, 560].map(|x| ActionId::try_from(x).unwrap());
+    let err = BitVectorOffsetContainer::try_from_actions(actions.as_mut_slice()).unwrap_err();
+    assert_eq!(err, ActionsDataElementError::ActionIdOutOfRange);
+}
+
+#[test]
+fn encode_bit_vector_max_amount_of_ids() {
+    let mut actions_ids = [0xFF; 512].map(|x| ActionId::try_from(x).unwrap());
+    assert!(BitVectorOffsetContainer::try_from_actions(actions_ids.as_mut_slice()).is_ok())
+}
+
+#[test]
+fn encode_bit_vector_too_many_actionids() {
+    let mut actions_ids = [0xFF; 513].map(|x| ActionId::try_from(x).unwrap());
+    assert_eq!(
+        BitVectorOffsetContainer::try_from_actions(actions_ids.as_mut_slice()).unwrap_err(),
+        ActionsDataElementError::TooManyActions
+    );
+}
+
+#[test]
+fn parse_multiple_containers() {
+    let bytes = [
+        vec![0x01, 0x64, 0xC7],       // delta encoded container
+        vec![0x42, 0x06, 0x02, 0x66], // delta encoded with offset
+        vec![0x82, 0x01, 0x5C, 0x08], // bit vector offset
+    ]
+    .concat();
+    let de = DeserializedActionsDE::deserialize(&bytes).expect("bytes are valid de");
+    assert_eq!(de.containers.len(), 3);
+    assert_eq!(de.containers[0].container_type, ContainerType::DeltaEncoded);
+    assert_eq!(de.containers[1].container_type, ContainerType::DeltaEncodedWithOffset);
+    assert_eq!(de.containers[2].container_type, ContainerType::BitVectorOffset);
+
+    let action_ids = de.collect_action_ids();
+    assert_eq!(
+        action_ids
+            .iter()
+            .map(|a| { a.expect("action_ids encoded are in valid range").as_u16() })
+            .collect::<Vec<_>>(),
+        vec![100, 300, 50, 153, 9, 11, 12, 13, 20]
+    );
+}
+
+#[test]
+fn try_from_invalid_type_code() {
+    let data = [];
+    let de = DataElement::new(DataElementOffset::from(0), DeType::const_from(5), &data);
+    assert_eq!(
+        DeserializedActionsDE::try_from(&de).unwrap_err(),
+        ActionsDeserializationError::InvalidTypeCode
+    );
+}
+
+#[test]
+fn try_from_invalid_container_length_empty_after_subheader() {
+    let data = [0x40, 0x64];
+    let de = DataElement::new(DataElementOffset::from(0), DeType::const_from(6), &data);
+    assert_eq!(
+        DeserializedActionsDE::try_from(&de).unwrap_err(),
+        ActionsDeserializationError::InvalidContainerLength
+    );
+}
+
+#[test]
+fn try_from_invalid_container_length_not_enough_data() {
+    let data = [0x02, 0x64, 0xC8];
+    let de = DataElement::new(DataElementOffset::from(0), DeType::const_from(6), &data);
+    assert_eq!(
+        DeserializedActionsDE::try_from(&de).unwrap_err(),
+        ActionsDeserializationError::InvalidContainerLength
+    );
+}
+
+#[test]
+fn try_from_invalid_container_type() {
+    let data = [0xC0, 0x64];
+    let de = DataElement::new(DataElementOffset::from(0), DeType::const_from(6), &data);
+    assert_eq!(
+        DeserializedActionsDE::try_from(&de).unwrap_err(),
+        ActionsDeserializationError::InvalidContainerType
+    );
+}
+
+#[test]
+fn parse_single_byte_delta_encoding() {
+    let bytes = [0x00, 0x64];
+    let actions_de = DeserializedActionsDE::deserialize(&bytes).unwrap();
+    let action_containers = &actions_de.containers;
+    assert_eq!(action_containers.len(), 1);
+    let container = action_containers.first().unwrap();
+    assert_eq!(container.container_type, ContainerType::DeltaEncoded);
+    assert_eq!(container.payload, &[0x64])
+}
+
+#[test]
+fn parse_single_byte_delta_encoding_offset_should_fail() {
+    let bytes = [0x40, 0x64];
+    let err = DeserializedActionsDE::deserialize(&bytes).unwrap_err();
+    assert_eq!(err, Error(nom::error::Error { input: &[100][..], code: ErrorKind::Verify }));
+}
+
+#[test]
+fn parse_single_container_max_length() {
+    let mut bytes = vec![0x3F];
+    bytes.extend_from_slice(&[0x01; 64]);
+
+    let actions_de =
+        DeserializedActionsDE::deserialize(&bytes).expect("bytes parse into valid actions DE");
+    let action_containers = &actions_de.containers;
+    assert_eq!(action_containers.len(), 1);
+    let container = action_containers.first().unwrap();
+    assert_eq!(container.container_type, ContainerType::DeltaEncoded);
+    assert_eq!(container.payload, &[0x01; 64]);
+}
+
+#[test]
+fn parse_single_byte_bit_vector_offset_should_fail() {
+    let bytes = [0x80, 0x64];
+    let err = DeserializedActionsDE::deserialize(&bytes).unwrap_err();
+    assert_eq!(err, Error(nom::error::Error { input: &[100][..], code: ErrorKind::Verify }));
+}
+
+#[test]
+fn parse_actions_de_invalid_length_bytes() {
+    let bytes = [0x02, 0x64, 0xC8];
+    let err = DeserializedActionsDE::deserialize(&bytes).unwrap_err();
+    assert_eq!(err, Error(nom::error::Error { input: &[100u8, 200][..], code: ErrorKind::Eof }))
+}
+
+#[test]
+fn parse_actions_de_extra_bytes() {
+    let bytes = [0x01, 0x64, 0xC8, 0xFF];
+    let err = DeserializedActionsDE::deserialize(&bytes).unwrap_err();
+    assert_eq!(err, Error(nom::error::Error { input: &[255u8][..], code: ErrorKind::Eof }))
+}
+
+#[test]
+fn parse_actions_de_invalid_container_type() {
+    let bytes = [0xC2, 0x64, 0xC8];
+    let err = DeserializedActionsDE::deserialize(&bytes).unwrap_err();
+    assert_eq!(
+        err,
+        Error(nom::error::Error { input: &[0xC2, 0x64, 0xC8][..], code: ErrorKind::MapOpt })
+    )
+}
+
+#[test]
+fn encode_empty_actions() {
+    let actions = ArrayVec::new();
+    let err = ActionsDataElement::try_from_actions(actions).unwrap_err();
+    assert_eq!(err, ActionsDataElementError::EmptyActions);
+}
+
+#[test]
+fn encode_empty_delta_container() {
+    let actions = ArrayVec::new();
+    let err = DeltaEncodedContainer::try_from_actions(actions).unwrap_err();
+    assert_eq!(err, ActionsDataElementError::EmptyActions);
+}
+
+#[test]
+fn encode_empty_delta_offset_container() {
+    let actions = ArrayVec::new();
+    let err = DeltaEncodedOffsetContainer::try_from_actions(actions).unwrap_err();
+    assert_eq!(err, ActionsDataElementError::EmptyActions);
+}
+
+#[test]
+fn encode_empty_bit_vector_offset_container() {
+    let err = BitVectorOffsetContainer::try_from_actions(&mut []).unwrap_err();
+    assert_eq!(err, ActionsDataElementError::EmptyActions);
+}
+
+#[test]
+fn encode_basic_delta_encoding() {
+    let mut actions = ArrayVec::<[ActionId; 64]>::new();
+    actions.extend_from_slice([100u16, 500, 300, 600].map(|v| v.try_into().unwrap()).as_slice());
+    let result = ActionsDataElement::try_from_actions(actions).expect("should succeed");
+    assert_eq!(result.containers[0].container_type, ContainerType::DeltaEncoded);
+    assert_eq!(result.containers[0].payload.as_slice(), &[100, 199, 199, 99]);
+}
+
+#[test]
+fn encode_basic_delta_encoding_id_out_of_range() {
+    let mut actions = ArrayVec::<[ActionId; 64]>::new();
+    actions.extend_from_slice([256].map(|v| v.try_into().unwrap()).as_slice());
+    let err = ActionsDataElement::try_from_actions(actions).unwrap_err();
+    assert_eq!(err, ActionsDataElementError::ActionIdDeltaOverflow);
+}
+
+#[test]
+fn encode_basic_delta_encoding_max_id() {
+    let mut actions = ArrayVec::<[ActionId; 64]>::new();
+    actions.extend_from_slice([255].map(|v| v.try_into().unwrap()).as_slice());
+    let de = ActionsDataElement::try_from_actions(actions).expect("should succeed");
+    assert_eq!(de.containers[0].container_type, ContainerType::DeltaEncoded);
+    assert_eq!(de.containers[0].payload.as_slice(), &[255]);
+}
+
+#[test]
+fn encode_basic_delta_encoding_duplicate_ids() {
+    let mut actions = ArrayVec::<[ActionId; 64]>::new();
+    actions
+        .extend_from_slice([1, 1, 1, 6, 3, 3, 6, 3, 3].map(|v| v.try_into().unwrap()).as_slice());
+    let de = ActionsDataElement::try_from_actions(actions).expect("should succeed");
+    assert_eq!(de.containers[0].container_type, ContainerType::DeltaEncoded);
+    assert_eq!(de.containers[0].payload.as_slice(), &[1, 1, 2]);
+}
+
+#[test]
+fn encode_basic_delta_encoding_delta_out_of_range() {
+    let mut actions = ArrayVec::<[ActionId; 64]>::new();
+    actions.extend_from_slice([100, 400].map(|v| v.try_into().unwrap()).as_slice());
+    let err = ActionsDataElement::try_from_actions(actions).unwrap_err();
+    assert_eq!(err, ActionsDataElementError::ActionIdDeltaOverflow);
+}
diff --git a/nearby/presence/np_adv/src/extended/data_elements/mod.rs b/nearby/presence/np_adv/src/extended/data_elements/mod.rs
index 2989966..a5b7f91 100644
--- a/nearby/presence/np_adv/src/extended/data_elements/mod.rs
+++ b/nearby/presence/np_adv/src/extended/data_elements/mod.rs
@@ -29,6 +29,11 @@
 use array_view::ArrayView;
 use sink::Sink;
 
+mod actions;
+pub use actions::ActionId;
+pub use actions::ActionsDataElement;
+pub use actions::DeserializedActionsDE;
+
 #[cfg(test)]
 mod tests;
 
@@ -102,44 +107,6 @@
     }
 }
 
-/// List of actions
-pub struct ActionsDataElement {
-    actions: tinyvec::ArrayVec<[u8; MAX_DE_LEN]>,
-}
-
-impl ActionsDataElement {
-    /// Returns `Some` if the actions will fit in a DE, `None` otherwise
-    pub fn try_from_actions(actions: &[u8]) -> Result<Self, ActionsDataElementError> {
-        let mut de = Self { actions: tinyvec::ArrayVec::new() };
-
-        de.actions
-            .try_extend_from_slice(actions)
-            .map(|_| de)
-            .ok_or(ActionsDataElementError::ActionsTooLong)
-    }
-}
-
-impl SingleTypeDataElement for ActionsDataElement {
-    const DE_TYPE: DeType = DeType::const_from(0x06);
-}
-
-impl WriteDataElement for ActionsDataElement {
-    fn de_header(&self) -> DeHeader {
-        DeHeader::new(Self::DE_TYPE, self.actions.len().try_into().expect("always <= max length"))
-    }
-
-    fn write_de_contents<S: Sink<u8>>(&self, sink: &mut S) -> Option<()> {
-        sink.try_extend_from_slice(&self.actions)
-    }
-}
-
-/// Errors that can occur constructing an [ActionsDataElement].
-#[derive(Debug, PartialEq, Eq)]
-pub enum ActionsDataElementError {
-    /// Too many action bytes.
-    ActionsTooLong,
-}
-
 /// Context sync sequence number
 pub struct ContextSyncSeqNumDataElement {
     num: ContextSyncSeqNum,
diff --git a/nearby/presence/np_adv/src/extended/data_elements/tests.rs b/nearby/presence/np_adv/src/extended/data_elements/tests.rs
index 018f92c..868a127 100644
--- a/nearby/presence/np_adv/src/extended/data_elements/tests.rs
+++ b/nearby/presence/np_adv/src/extended/data_elements/tests.rs
@@ -16,11 +16,16 @@
 
 extern crate std;
 
-use super::*;
+use tinyvec::ArrayVec;
+
+use crypto_provider_default::CryptoProviderImpl;
+
+use crate::extended::data_elements::actions::{ActionId, ActionsDataElement};
 use crate::extended::serialize::{section_tests::SectionBuilderExt, AdvBuilder};
 use crate::extended::serialize::{AdvertisementType, UnencryptedSectionEncoder};
 use crate::extended::V1_ENCODING_UNENCRYPTED;
-use crypto_provider_default::CryptoProviderImpl;
+
+use super::*;
 
 #[test]
 fn serialize_tx_power_de() {
@@ -40,40 +45,33 @@
     );
 }
 
-#[test]
-fn serialize_actions_de_empty() {
-    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
-    let mut section_builder = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
-
-    section_builder.add_de_res(|_| ActionsDataElement::try_from_actions(&[])).unwrap();
-
-    assert_eq!(
-        &[
-            V1_ENCODING_UNENCRYPTED, //header
-            1,                       // section len
-            0x06,                    // len 0 type 0x06
-        ],
-        section_builder.into_section::<CryptoProviderImpl>().as_slice()
-    );
+fn actions_ids_from_u16_collection<const N: usize>(actions: [u16; N]) -> ArrayVec<[ActionId; 64]> {
+    let mut result = ArrayVec::new();
+    result.extend_from_slice(&actions.map(|x| x.try_into().unwrap()));
+    result
 }
 
-#[rustfmt::skip]
 #[test]
 fn serialize_actions_de_non_empty() {
     let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
-    let mut section_builder =
-        adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
+    let mut section_builder = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
 
     section_builder
-        .add_de_res(|_| ActionsDataElement::try_from_actions(&[1, 1, 2, 3, 5, 8]))
+        .add_de_res(|_| {
+            ActionsDataElement::try_from_actions(actions_ids_from_u16_collection([
+                1, 1, 2, 3, 5, 8, // fibonacci, of course
+            ]))
+        })
         .unwrap();
 
+    #[rustfmt::skip]
     assert_eq!(
         &[
             V1_ENCODING_UNENCRYPTED,
             7, // section len
             0x66, // len 6 type 0x06
-            1, 1, 2, 3, 5, 8 // fibonacci, of course
+            0b00000100, // container type and len TTLLLLLL
+            1, 0, 0, 1, 2 // de-duped delta encoded fibonacci
         ],
         section_builder.into_section::<CryptoProviderImpl>().as_slice()
     );
@@ -196,8 +194,10 @@
 }
 
 mod coverage_gaming {
-    use super::*;
     use alloc::format;
+
+    use super::*;
+
     #[test]
     fn de_type_const_from() {
         let _ = DeType::const_from(3);
@@ -214,13 +214,6 @@
     }
 
     #[test]
-    fn actions_de_error_derives() {
-        let err = ActionsDataElementError::ActionsTooLong;
-        let _ = format!("{:?}", err);
-        assert_eq!(err, err);
-    }
-
-    #[test]
     fn generic_data_element_debug() {
         let generic =
             GenericDataElement::try_from(DeType::from(1000_u32), &[10, 11, 12, 13]).unwrap();
diff --git a/nearby/presence/np_adv/src/extended/de_type.rs b/nearby/presence/np_adv/src/extended/de_type.rs
index baefe6a..be26555 100644
--- a/nearby/presence/np_adv/src/extended/de_type.rs
+++ b/nearby/presence/np_adv/src/extended/de_type.rs
@@ -23,7 +23,7 @@
 
 impl DeType {
     /// A `const` equivalent to `From<u32>` since trait methods can't yet be const.
-    pub(crate) const fn const_from(value: u32) -> Self {
+    pub const fn const_from(value: u32) -> Self {
         Self { code: value }
     }
 
diff --git a/nearby/presence/np_adv/src/extended/deserialize/data_element/mod.rs b/nearby/presence/np_adv/src/extended/deserialize/data_element/mod.rs
index e717fa1..b90de8a 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/data_element/mod.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/data_element/mod.rs
@@ -16,6 +16,7 @@
 
 use crate::extended::{de_requires_extended_bit, de_type::DeType, deserialize, DeLength};
 use array_view::ArrayView;
+use core::fmt;
 use nom::{branch, bytes, combinator, error, number, sequence};
 use np_hkdf::v1_salt;
 
@@ -34,6 +35,22 @@
 }
 
 impl<'adv> DataElement<'adv> {
+    /// The offset of the DE in its containing Section.
+    ///
+    /// Used with the section salt to derive per-DE salt.
+    pub fn offset(&self) -> v1_salt::DataElementOffset {
+        self.offset
+    }
+
+    /// The type of the DE
+    pub fn de_type(&self) -> DeType {
+        self.de_type
+    }
+    /// The contents of the DE
+    pub fn contents(&self) -> &'adv [u8] {
+        self.contents
+    }
+
     pub(crate) fn new(
         offset: v1_salt::DataElementOffset,
         de_type: DeType,
@@ -41,6 +58,17 @@
     ) -> Self {
         Self { offset, de_type, contents }
     }
+
+    /// Exposes the ability to create a DE for testing purposes, real clients should only obtain
+    /// one by deserializing an advertisement
+    #[cfg(feature = "testing")]
+    pub fn new_for_testing(
+        offset: v1_salt::DataElementOffset,
+        de_type: DeType,
+        contents: &'adv [u8],
+    ) -> Self {
+        Self { offset, de_type, contents }
+    }
 }
 
 impl DeHeader {
@@ -144,23 +172,6 @@
     }
 }
 
-impl<'adv> DataElement<'adv> {
-    /// The offset of the DE in its containing Section.
-    ///
-    /// Used with the section salt to derive per-DE salt.
-    pub fn offset(&self) -> v1_salt::DataElementOffset {
-        self.offset
-    }
-    /// The type of the DE
-    pub fn de_type(&self) -> DeType {
-        self.de_type
-    }
-    /// The contents of the DE
-    pub fn contents(&self) -> &'adv [u8] {
-        self.contents
-    }
-}
-
 /// An iterator that parses the given data elements iteratively. In environments where memory is
 /// not severely constrained, it is usually safer to collect this into `Result<Vec<DataElement>>`
 /// so the validity of the whole advertisement can be checked before proceeding with further
@@ -225,6 +236,19 @@
     NomError(error::ErrorKind),
 }
 
+impl fmt::Display for DataElementParseError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            DataElementParseError::UnexpectedDataAfterEnd => write!(f, "Unexpected data after end"),
+            DataElementParseError::TooManyDataElements => write!(f, "Too many data elements"),
+            DataElementParseError::NomError(_) => write!(f, "Nom error"),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for DataElementParseError {}
+
 /// Deserialize-specific version of a DE header that incorporates the header length.
 /// This is needed for encrypted identities that need to construct a slice of everything in the
 /// section following the identity DE header.
diff --git a/nearby/presence/np_adv/src/extended/deserialize/mod.rs b/nearby/presence/np_adv/src/extended/deserialize/mod.rs
index c6c7ba4..123cee6 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/mod.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/mod.rs
@@ -38,7 +38,7 @@
             },
         },
         salt::MultiSalt,
-        V1IdentityToken, NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT,
+        V1IdentityToken, NP_V1_ADV_MAX_SECTION_COUNT,
     },
     header::V1AdvHeader,
     AdvDeserializationError, AdvDeserializationErrorDetailsHazmat,
@@ -94,9 +94,9 @@
 }
 
 /// A section deserialized from a V1 advertisement.
-pub trait Section<'adv, E: Debug> {
+pub trait Section<'adv> {
     /// The iterator type used to iterate over data elements
-    type Iterator: Iterator<Item = Result<DataElement<'adv>, E>>;
+    type Iterator: Iterator<Item = Result<DataElement<'adv>, DataElementParseError>>;
 
     /// Iterator over the data elements in a section, except for any DEs related to resolving the
     /// identity or otherwise validating the payload (e.g. MIC, Signature, any identity DEs like
@@ -106,7 +106,7 @@
     /// Collects the data elements into a vector, eagerly catching and resolving any errors during
     /// parsing.
     #[cfg(any(test, feature = "alloc"))]
-    fn collect_data_elements(&self) -> Result<Vec<DataElement<'adv>>, E>
+    fn collect_data_elements(&self) -> Result<Vec<DataElement<'adv>>, DataElementParseError>
     where
         Self: Sized,
     {
@@ -162,7 +162,7 @@
     }
 }
 
-impl<'adv> Section<'adv, DataElementParseError> for DecryptedSection<'adv> {
+impl<'adv> Section<'adv> for DecryptedSection<'adv> {
     type Iterator = DataElementParsingIterator<'adv>;
 
     fn iter_data_elements(&self) -> Self::Iterator {
@@ -225,14 +225,10 @@
 /// section ordering as they appeared within the original advertisement to ensure
 /// that the fully-deserialized advertisement may be correctly reconstructed.
 struct SectionsInProcessing<'adv, M: MatchedCredential> {
-    deserialized_sections: ArrayVecOption<
-        (usize, V1DeserializedSection<'adv, M>),
-        { NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT },
-    >,
-    encrypted_sections: ArrayVecOption<
-        (usize, ResolvableCiphertextSection<'adv>),
-        { NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT },
-    >,
+    deserialized_sections:
+        ArrayVecOption<(usize, V1DeserializedSection<'adv, M>), { NP_V1_ADV_MAX_SECTION_COUNT }>,
+    encrypted_sections:
+        ArrayVecOption<(usize, ResolvableCiphertextSection<'adv>), { NP_V1_ADV_MAX_SECTION_COUNT }>,
     malformed_sections_count: usize,
 }
 
@@ -387,16 +383,13 @@
 /// The contents of a deserialized and decrypted V1 advertisement.
 #[derive(Debug, PartialEq, Eq)]
 pub struct V1AdvertisementContents<'adv, M: MatchedCredential> {
-    sections: ArrayVecOption<V1DeserializedSection<'adv, M>, NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT>,
+    sections: ArrayVecOption<V1DeserializedSection<'adv, M>, NP_V1_ADV_MAX_SECTION_COUNT>,
     invalid_sections: usize,
 }
 
 impl<'adv, M: MatchedCredential> V1AdvertisementContents<'adv, M> {
     fn new(
-        sections: ArrayVecOption<
-            V1DeserializedSection<'adv, M>,
-            NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT,
-        >,
+        sections: ArrayVecOption<V1DeserializedSection<'adv, M>, NP_V1_ADV_MAX_SECTION_COUNT>,
         invalid_sections: usize,
     ) -> Self {
         Self { sections, invalid_sections }
@@ -406,7 +399,7 @@
     /// which could be successfully deserialized and decrypted
     pub fn into_sections(
         self,
-    ) -> ArrayVecOption<V1DeserializedSection<'adv, M>, NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT> {
+    ) -> ArrayVecOption<V1DeserializedSection<'adv, M>, NP_V1_ADV_MAX_SECTION_COUNT> {
         self.sections
     }
 
@@ -431,6 +424,18 @@
     Decrypted(WithMatchedCredential<M, DecryptedSection<'adv>>),
 }
 
+// Allow easy DE iteration if the user doesn't care which kind of section it is
+impl<'adv, M: MatchedCredential> Section<'adv> for V1DeserializedSection<'adv, M> {
+    type Iterator = DataElementParsingIterator<'adv>;
+
+    fn iter_data_elements(&self) -> Self::Iterator {
+        match self {
+            V1DeserializedSection::Plaintext(p) => p.iter_data_elements(),
+            V1DeserializedSection::Decrypted(d) => d.contents().iter_data_elements(),
+        }
+    }
+}
+
 /// The level of integrity protection in an encrypted section
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
 pub enum VerificationMode {
diff --git a/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/mod.rs b/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/mod.rs
index f998299..707b593 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/mod.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/mod.rs
@@ -29,14 +29,13 @@
             DataElementParsingIterator, Section, SectionMic,
         },
         salt::MultiSalt,
-        NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT, NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT,
+        NP_V1_ADV_MAX_SECTION_COUNT,
     },
     header::V1AdvHeader,
 };
 use crypto_provider::CryptoProvider;
 use nom::{branch, bytes, combinator, error, multi};
 
-use crate::extended::deserialize::data_element::DataElementParseError;
 #[cfg(feature = "devtools")]
 use crate::{
     credential::v1::V1DiscoveryCryptoMaterial, deserialization_arena::DeserializationArenaAllocator,
@@ -55,14 +54,14 @@
     adv_header: V1AdvHeader,
     adv_body: &[u8],
 ) -> Result<
-    ArrayVecOption<IntermediateSection, NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT>,
+    ArrayVecOption<IntermediateSection, NP_V1_ADV_MAX_SECTION_COUNT>,
     nom::Err<error::Error<&[u8]>>,
 > {
     combinator::all_consuming(branch::alt((
         // Public advertisement
         multi::fold_many_m_n(
             1,
-            NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT,
+            NP_V1_ADV_MAX_SECTION_COUNT,
             IntermediateSection::parser_unencrypted_section,
             ArrayVecOption::default,
             |mut acc, item| {
@@ -73,7 +72,7 @@
         // Encrypted advertisement
         multi::fold_many_m_n(
             1,
-            NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT,
+            NP_V1_ADV_MAX_SECTION_COUNT,
             IntermediateSection::parser_encrypted_with_header(adv_header),
             ArrayVecOption::default,
             |mut acc, item| {
@@ -225,7 +224,7 @@
     }
 }
 
-impl<'adv> Section<'adv, DataElementParseError> for PlaintextSection<'adv> {
+impl<'adv> Section<'adv> for PlaintextSection<'adv> {
     type Iterator = DataElementParsingIterator<'adv>;
 
     fn iter_data_elements(&self) -> Self::Iterator {
diff --git a/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/tests.rs b/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/tests.rs
index 84ce763..313a906 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/tests.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/section/intermediate/tests.rs
@@ -32,7 +32,7 @@
         },
         salt::{ShortV1Salt, SHORT_SALT_LEN},
         serialize::{AdvBuilder, AdvertisementType},
-        NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT, V1_ENCODING_ENCRYPTED_MIC_WITH_EXTENDED_SALT_AND_TOKEN,
+        NP_V1_ADV_MAX_SECTION_COUNT, V1_ENCODING_ENCRYPTED_MIC_WITH_EXTENDED_SALT_AND_TOKEN,
         V1_ENCODING_ENCRYPTED_MIC_WITH_SHORT_SALT_AND_TOKEN,
         V1_ENCODING_ENCRYPTED_SIGNATURE_WITH_EXTENDED_SALT_AND_TOKEN, V1_ENCODING_UNENCRYPTED,
         V1_IDENTITY_TOKEN_LEN,
@@ -92,7 +92,7 @@
     #[test]
     fn do_deserialize_max_number_of_public_sections() {
         let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
-        for _ in 0..NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT {
+        for _ in 0..NP_V1_ADV_MAX_SECTION_COUNT {
             let mut section_builder =
                 adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
             section_builder
@@ -110,13 +110,13 @@
             panic!("incorrect header");
         };
         let sections = parse_sections(v1_header, remaining).unwrap();
-        assert_eq!(NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT, sections.len());
+        assert_eq!(NP_V1_ADV_MAX_SECTION_COUNT, sections.len());
     }
 
     #[test]
     fn max_number_encrypted_sections_mic() {
         let mut adv_body = vec![];
-        for _ in 0..NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT {
+        for _ in 0..NP_V1_ADV_MAX_SECTION_COUNT {
             let _ = add_mic_short_salt_section_to_adv(&mut adv_body);
         }
         let adv_header = V1AdvHeader::new(0x20);
@@ -126,7 +126,7 @@
     #[test]
     fn max_number_encrypted_sections_sig() {
         let mut adv_body = vec![];
-        for _ in 0..NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT {
+        for _ in 0..NP_V1_ADV_MAX_SECTION_COUNT {
             let _ = add_sig_encrpyted_section(&mut adv_body, 5, &[0x55; EXTENDED_SALT_LEN]);
         }
         let adv_header = V1AdvHeader::new(0x20);
@@ -346,26 +346,23 @@
         // 2 sections
         let mut adv_body = vec![];
 
-        // section 1 - plaintext - 9 bytes
-        adv_body.push(V1_ENCODING_UNENCRYPTED);
-        adv_body.push(6 + 3); // section len
-                              // de 1 byte header, type 5, len 5
-        adv_body.extend_from_slice(&[0x55, 0x01, 0x02, 0x03, 0x04, 0x05]);
-        // de 2 byte header, type 6, len 1
-        adv_body.extend_from_slice(&[0x81, 0x06, 0x01]);
-
-        // section 2 - plaintext - 10 bytes
-        adv_body.push(V1_ENCODING_UNENCRYPTED);
-        adv_body.push(6 + 3); // section len
-                              // de 1 byte header, type 5, len 5
-        adv_body.extend_from_slice(&[0x55, 0x01, 0x02, 0x03, 0x04, 0x05]);
-        // de 2 byte header, type 6, len 1
-        adv_body.extend_from_slice(&[0x81, 0x06, 0x01]);
+        for _ in 0..=NP_V1_ADV_MAX_SECTION_COUNT {
+            // section 1..=MAX+1 - plaintext - 11 bytes
+            adv_body.push(V1_ENCODING_UNENCRYPTED);
+            adv_body.push(6 + 3); // section len
+                                  // de 1 byte header, type 5, len 5
+            adv_body.extend_from_slice(&[0x55, 0x01, 0x02, 0x03, 0x04, 0x05]);
+            // de 2 byte header, type 6, len 1
+            adv_body.extend_from_slice(&[0x81, 0x06, 0x01]);
+        }
 
         let adv_header = V1AdvHeader::new(0x20);
 
         assert_eq!(
-            nom::Err::Error(error::Error { input: &adv_body[11..], code: error::ErrorKind::Eof }),
+            nom::Err::Error(error::Error {
+                input: &adv_body[(NP_V1_ADV_MAX_SECTION_COUNT * 11)..],
+                code: error::ErrorKind::Eof
+            }),
             parse_sections(adv_header, &adv_body).unwrap_err()
         );
     }
@@ -374,7 +371,7 @@
     fn parse_adv_too_many_encrypted() {
         // 3 sections
         let mut adv_body = vec![];
-        for _ in 0..NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT + 1 {
+        for _ in 0..NP_V1_ADV_MAX_SECTION_COUNT + 1 {
             let _ = add_mic_short_salt_section_to_adv(&mut adv_body);
         }
         let adv_header = V1AdvHeader::new(0x20);
diff --git a/nearby/presence/np_adv/src/extended/mod.rs b/nearby/presence/np_adv/src/extended/mod.rs
index 4a13ea5..128fbf1 100644
--- a/nearby/presence/np_adv/src/extended/mod.rs
+++ b/nearby/presence/np_adv/src/extended/mod.rs
@@ -33,10 +33,7 @@
     - 2;
 
 /// Maximum number of sections in an advertisement
-pub const NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT: usize = 8;
-
-/// Maximum number of public sections in an advertisement
-pub const NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT: usize = 1;
+pub const NP_V1_ADV_MAX_SECTION_COUNT: usize = 8;
 
 /// Maximum size of a NP section, including its length header byte
 pub const NP_ADV_MAX_SECTION_LEN: usize = NP_ADV_MAX_SECTION_CONTENTS_LEN + 1;
diff --git a/nearby/presence/np_adv/src/extended/salt.rs b/nearby/presence/np_adv/src/extended/salt.rs
index b35275f..2799173 100644
--- a/nearby/presence/np_adv/src/extended/salt.rs
+++ b/nearby/presence/np_adv/src/extended/salt.rs
@@ -17,10 +17,11 @@
 use nom::combinator;
 
 use crypto_provider::{aes::ctr::AesCtrNonce, CryptoProvider, CryptoRng, FromCryptoRng};
-use np_hkdf::v1_salt::ExtendedV1Salt;
 
 use crate::helpers::parse_byte_array;
 
+pub use np_hkdf::v1_salt::ExtendedV1Salt;
+
 /// Common behavior for V1 section salts.
 pub trait V1Salt: Copy + Into<MultiSalt> {
     /// Derive the nonce used for section encryption.
diff --git a/nearby/presence/np_adv/src/extended/serialize/adv_tests.rs b/nearby/presence/np_adv/src/extended/serialize/adv_tests.rs
index f094992..2d3827e 100644
--- a/nearby/presence/np_adv/src/extended/serialize/adv_tests.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/adv_tests.rs
@@ -16,7 +16,6 @@
 
 extern crate std;
 use super::*;
-use crate::extended::serialize::section::header::SectionHeader;
 use crate::extended::serialize::section_tests::{fill_section_builder, DummyDataElement};
 use crate::extended::V1_ENCODING_UNENCRYPTED;
 use crypto_provider_default::CryptoProviderImpl;
@@ -95,29 +94,3 @@
 }
 
 // TODO tests for other encoding types interacting with maximum possible section len
-
-/// A placeholder identity with a huge prefix
-#[derive(Default, PartialEq, Eq, Debug)]
-struct EnormousIdentity {}
-
-impl SectionEncoder for EnormousIdentity {
-    const SUFFIX_LEN: usize = 0;
-    const ADVERTISEMENT_TYPE: AdvertisementType = AdvertisementType::Plaintext;
-    type DerivedSalt = ();
-
-    fn header(&self) -> SectionHeader {
-        unimplemented!("Should never be hit")
-    }
-    fn postprocess<C: CryptoProvider>(
-        &mut self,
-        _section_header_without_length: &mut [u8],
-        _section_len: u8,
-        _remaining_content_bytes: &mut [u8],
-    ) {
-        panic!("should never be called, just used for its huge prefix")
-    }
-
-    fn de_salt(&self, _de_offset: DataElementOffset) -> Self::DerivedSalt {
-        panic!("should never be called, just used for its huge prefix")
-    }
-}
diff --git a/nearby/presence/np_adv/src/extended/serialize/mod.rs b/nearby/presence/np_adv/src/extended/serialize/mod.rs
index d94fee2..ad9acab 100644
--- a/nearby/presence/np_adv/src/extended/serialize/mod.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/mod.rs
@@ -109,6 +109,10 @@
 //!         .try_into().expect("array sizes match")
 //! }
 //! ```
+
+#[cfg(feature = "std")]
+extern crate std;
+
 use core::fmt::{self, Display};
 
 use array_view::ArrayView;
@@ -118,8 +122,7 @@
 
 use crate::extended::{
     de_requires_extended_bit, de_type::DeType, serialize::section::EncodedSection, to_array_view,
-    DeLength, BLE_5_ADV_SVC_MAX_CONTENT_LEN, NP_ADV_MAX_SECTION_LEN,
-    NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT, NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT,
+    DeLength, BLE_5_ADV_SVC_MAX_CONTENT_LEN, NP_ADV_MAX_SECTION_LEN, NP_V1_ADV_MAX_SECTION_COUNT,
 };
 
 mod section;
@@ -223,7 +226,7 @@
         &self,
         section_encoder: &SE,
     ) -> Result<(usize, CapacityLimitedVec<u8, NP_ADV_MAX_SECTION_LEN>), AddSectionError> {
-        if self.section_count >= self.advertisement_type.max_sections() {
+        if self.section_count >= NP_V1_ADV_MAX_SECTION_COUNT {
             return Err(AddSectionError::MaxSectionCountExceeded);
         }
         if self.advertisement_type != SE::ADVERTISEMENT_TYPE {
@@ -267,7 +270,7 @@
 pub enum AddSectionError {
     /// The advertisement doesn't have enough space to hold the minimum size of the section
     InsufficientAdvSpace,
-    /// The advertisement can only hold a maximum of NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT number of sections
+    /// The advertisement can only hold a maximum of NP_V1_ADV_MAX_SECTION_COUNT number of sections
     MaxSectionCountExceeded,
     /// An incompatible section trying to be added
     IncompatibleSectionType,
@@ -280,7 +283,7 @@
                 write!(f, "The advertisement (max {BLE_5_ADV_SVC_MAX_CONTENT_LEN} bytes) doesn't have enough remaining space to hold the section")
             }
             AddSectionError::MaxSectionCountExceeded => {
-                write!(f, "The advertisement can only hold a maximum of {NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT} number of sections")
+                write!(f, "The advertisement can only hold a maximum of {NP_V1_ADV_MAX_SECTION_COUNT} number of sections")
             }
             AddSectionError::IncompatibleSectionType => {
                 write!(f, "Public and Encrypted sections cannot be mixed in the same advertisement")
@@ -289,6 +292,9 @@
     }
 }
 
+#[cfg(feature = "std")]
+impl std::error::Error for AddSectionError {}
+
 /// An encoded NP V1 advertisement, starting with the NP advertisement header byte.
 #[derive(Debug, PartialEq, Eq)]
 pub struct EncodedAdvertisement {
@@ -316,15 +322,6 @@
     Encrypted,
 }
 
-impl AdvertisementType {
-    fn max_sections(&self) -> usize {
-        match self {
-            AdvertisementType::Plaintext => NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT,
-            AdvertisementType::Encrypted => NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT,
-        }
-    }
-}
-
 /// Derived salt for an individual data element.
 pub struct DeSalt {
     salt: ExtendedV1Salt,
diff --git a/nearby/presence/np_adv/src/extended/serialize/section/mod.rs b/nearby/presence/np_adv/src/extended/serialize/section/mod.rs
index 8fb76ab..1b8a331 100644
--- a/nearby/presence/np_adv/src/extended/serialize/section/mod.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/section/mod.rs
@@ -12,7 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#[cfg(feature = "std")]
+extern crate std;
+
 use array_view::ArrayView;
+use core::{convert, fmt};
 use crypto_provider::CryptoProvider;
 use np_hkdf::v1_salt::DataElementOffset;
 use sink::Sink as _;
@@ -151,8 +155,8 @@
     pub fn add_de<W: WriteDataElement, F: FnOnce(SE::DerivedSalt) -> W>(
         &mut self,
         build_de: F,
-    ) -> Result<(), AddDataElementError<()>> {
-        self.add_de_res(|derived_salt| Ok::<_, ()>(build_de(derived_salt)))
+    ) -> Result<(), AddDataElementError<convert::Infallible>> {
+        self.add_de_res(|derived_salt| Ok::<_, convert::Infallible>(build_de(derived_salt)))
     }
 
     /// Convert a section builder's contents into an encoded section.
@@ -200,5 +204,19 @@
     InsufficientSectionSpace,
 }
 
+impl<E: fmt::Display> fmt::Display for AddDataElementError<E> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            AddDataElementError::BuildDeError(e) => write!(f, "Build DE error: {}", e),
+            AddDataElementError::InsufficientSectionSpace => {
+                write!(f, "Insufficient section space")
+            }
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl<E: fmt::Debug + fmt::Display> std::error::Error for AddDataElementError<E> {}
+
 /// The encoded form of an advertisement section
 pub(crate) type EncodedSection = ArrayView<u8, NP_ADV_MAX_SECTION_LEN>;
diff --git a/nearby/presence/np_adv/src/extended/serialize/section_tests.rs b/nearby/presence/np_adv/src/extended/serialize/section_tests.rs
index e1e90b7..9fbd039 100644
--- a/nearby/presence/np_adv/src/extended/serialize/section_tests.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/section_tests.rs
@@ -604,10 +604,10 @@
 #[test]
 fn serialize_max_number_of_public_sections() {
     let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
-    for _ in 0..NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT {
+    for _ in 0..NP_V1_ADV_MAX_SECTION_COUNT {
         let mut section_builder = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
         section_builder
-            .add_de(|_| DummyDataElement { de_type: 100_u32.into(), data: vec![0; 98] })
+            .add_de(|_| DummyDataElement { de_type: 100_u32.into(), data: vec![0; 27] })
             .unwrap();
         section_builder.add_to_advertisement::<CryptoProviderImpl>();
     }
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/mod.rs b/nearby/presence/np_adv/src/legacy/data_elements/mod.rs
index 5dd0587..762d752 100644
--- a/nearby/presence/np_adv/src/legacy/data_elements/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/data_elements/mod.rs
@@ -81,6 +81,8 @@
         /// The invalid length
         len: DeEncodedLength,
     },
+    /// The same de type code was encountered more than once in an advertisement
+    DuplicateDeTypes,
     /// Other parse error, e.g. the adv is truncated
     InvalidStructure,
 }
diff --git a/nearby/presence/np_adv/src/legacy/data_elements/tests.rs b/nearby/presence/np_adv/src/legacy/data_elements/tests.rs
index d3f3061..a5e8fbf 100644
--- a/nearby/presence/np_adv/src/legacy/data_elements/tests.rs
+++ b/nearby/presence/np_adv/src/legacy/data_elements/tests.rs
@@ -156,9 +156,11 @@
         DataElementDeserializeError, DataElementSerializationBuffer, DataElementSerializeError,
         DeserializeDataElement, LengthMapper, SerializeDataElement,
     };
-    use crate::legacy::deserialize::{DataElementDeserializer, LengthError, RawDataElement};
+    use crate::legacy::deserialize::{
+        DataElementDeserializer, Deserialized, LengthError, RawDataElement,
+    };
     use crate::legacy::serialize::tests::helpers::{LongDataElement, ShortDataElement};
-    use crate::legacy::{PacketFlavor, NP_MAX_DE_CONTENT_LEN};
+    use crate::legacy::{PacketFlavor, Plaintext, NP_MAX_DE_CONTENT_LEN};
     use crate::private::Sealed;
 
     /// A [DataElementDeserializer] that can deserialize the test stubs [ShortDataElement] and
@@ -214,6 +216,19 @@
         Long(LongDataElement),
     }
 
+    impl Deserialized for TestDataElement {
+        fn de_type_code(&self) -> DeTypeCode {
+            match self {
+                TestDataElement::Short(s) => {
+                    <ShortDataElement as SerializeDataElement<Plaintext>>::de_type_code(s)
+                }
+                TestDataElement::Long(l) => {
+                    <LongDataElement as SerializeDataElement<Plaintext>>::de_type_code(l)
+                }
+            }
+        }
+    }
+
     impl From<ShortDataElement> for TestDataElement {
         fn from(value: ShortDataElement) -> Self {
             Self::Short(value)
diff --git a/nearby/presence/np_adv/src/legacy/deserialize/mod.rs b/nearby/presence/np_adv/src/legacy/deserialize/mod.rs
index d305b57..d2ceab6 100644
--- a/nearby/presence/np_adv/src/legacy/deserialize/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/deserialize/mod.rs
@@ -44,6 +44,7 @@
 use crate::credential::matched::HasIdentityMatch;
 use crate::legacy::data_elements::actions::ActionsDataElement;
 use crate::legacy::data_elements::de_type::{DataElementType, DeActualLength};
+use crate::legacy::data_elements::DataElementDeserializeError::DuplicateDeTypes;
 use crate::legacy::Plaintext;
 /// exposed because the unencrypted case isn't just for intermediate: no further processing is needed
 pub use intermediate::UnencryptedAdvContents;
@@ -121,6 +122,44 @@
     }
 }
 
+struct DeTypeBitFieldPosition(u16);
+impl DeTypeBitFieldPosition {
+    fn as_u16(&self) -> u16 {
+        self.0
+    }
+}
+
+impl From<u16> for DeTypeBitFieldPosition {
+    fn from(value: u16) -> Self {
+        DeTypeBitFieldPosition(value)
+    }
+}
+
+impl From<DeTypeCode> for DeTypeBitFieldPosition {
+    fn from(value: DeTypeCode) -> Self {
+        match value.as_u8() {
+            0 => 0.into(),
+            1 => 0b00000000_00000001.into(),
+            2 => 0b00000000_00000010.into(),
+            3 => 0b00000000_00000100.into(),
+            4 => 0b00000000_00001000.into(),
+            5 => 0b00000000_00010000.into(),
+            6 => 0b00000000_00100000.into(),
+            7 => 0b00000000_01000000.into(),
+            8 => 0b00000000_10000000.into(),
+            9 => 0b00000001_00000000.into(),
+            10 => 0b00000010_00000000.into(),
+            11 => 0b00000100_00000000.into(),
+            12 => 0b00001000_00000000.into(),
+            13 => 0b00010000_00000000.into(),
+            14 => 0b00100000_00000000.into(),
+            15 => 0b01000000_00000000.into(),
+            16 => 0b10000000_00000000.into(),
+            _ => panic!("Invalid v0 De type code"),
+        }
+    }
+}
+
 /// The generified innards of [DeIterator] so that it's possible to also use test-only
 /// deserializers.
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -128,13 +167,21 @@
     /// Data to be parsed, containing a sequence of data elements in serialized
     /// form.
     data: &'d [u8],
+    /// Keeps track of de_types as we iterate to ensure that multiple des of the same type
+    /// are not present in an advertisement
+    already_encountered: u16,
     _flavor_marker: PhantomData<F>,
     _deser_marker: PhantomData<D>,
 }
 
 impl<'d, F, D> GenericDeIterator<'d, F, D> {
     fn new(data: &'d [u8]) -> Self {
-        Self { data, _flavor_marker: Default::default(), _deser_marker: Default::default() }
+        Self {
+            data,
+            already_encountered: 0u16,
+            _flavor_marker: Default::default(),
+            _deser_marker: Default::default(),
+        }
     }
 }
 
@@ -152,6 +199,14 @@
 
         match parse_result.finish() {
             Ok((rem, de)) => {
+                if self.already_encountered
+                    & DeTypeBitFieldPosition::from(de.de_type_code()).as_u16()
+                    != 0
+                {
+                    return Some(Err(DuplicateDeTypes));
+                }
+                self.already_encountered |=
+                    DeTypeBitFieldPosition::from(de.de_type_code()).as_u16();
                 self.data = rem;
                 Some(Ok(de))
             }
@@ -175,6 +230,21 @@
     TxPower(TxPowerDataElement),
 }
 
+impl<F: PacketFlavor> Deserialized for DeserializedDataElement<F> {
+    fn de_type_code(&self) -> DeTypeCode {
+        match self {
+            DeserializedDataElement::Actions(_) => {
+                DeTypeCode::try_from(ActionsDataElement::<F>::DE_TYPE_CODE.as_u8())
+                    .expect("Actions type code is valid so this will always succeed")
+            }
+            DeserializedDataElement::TxPower(_) => {
+                DeTypeCode::try_from(TxPowerDataElement::DE_TYPE_CODE.as_u8())
+                    .expect("TxPower type code is valid so this will always succeed")
+            }
+        }
+    }
+}
+
 impl<F: PacketFlavor> DeserializedDataElement<F> {
     /// Returns the DE type as a u8
     #[cfg(feature = "devtools")]
@@ -254,13 +324,19 @@
     }
 }
 
+pub(in crate::legacy) trait Deserialized:
+    fmt::Debug + PartialEq + Eq + Clone
+{
+    fn de_type_code(&self) -> DeTypeCode;
+}
+
 /// Overall strategy for deserializing adv contents (once decrypted, if applicable) into data
 /// elements
 pub(in crate::legacy) trait DataElementDeserializer: Sized {
-    /// Disambiguates the intermediate form of a DE
+    /// Disambiguate the intermediate form of a DE
     type DeTypeDisambiguator: Copy;
     /// The fully deserialized form of a DE
-    type Deserialized<F: PacketFlavor>: fmt::Debug + PartialEq + Eq + Clone;
+    type Deserialized<F: PacketFlavor>: Deserialized;
 
     /// Map the encoded len found in a DE header to the actual len that should be consumed from the
     /// advertisement payload
diff --git a/nearby/presence/np_adv/src/legacy/deserialize/tests/error_conditions.rs b/nearby/presence/np_adv/src/legacy/deserialize/tests/error_conditions.rs
index 8230548..ace055d 100644
--- a/nearby/presence/np_adv/src/legacy/deserialize/tests/error_conditions.rs
+++ b/nearby/presence/np_adv/src/legacy/deserialize/tests/error_conditions.rs
@@ -83,6 +83,26 @@
     }
 
     #[test]
+    fn iterate_multiple_de_types_same_value() {
+        let input = &[0x15, 0x00, 0x15, 0x00];
+        let mut it = IntermediateAdvContents::deserialize::<CryptoProviderImpl>(
+            V0Encoding::Unencrypted,
+            input,
+        )
+        .unwrap()
+        .as_unencrypted()
+        .unwrap()
+        .data_elements();
+
+        // first element will be valid
+        let _ = it.next();
+        // second will be an error since it is the same type as the first
+        let err = it.next().unwrap().unwrap_err();
+
+        assert_eq!(err, DataElementDeserializeError::DuplicateDeTypes);
+    }
+
+    #[test]
     fn iterate_truncated_contents_error() {
         assert_deser_error(
             // length 3, but only 2 bytes provided
diff --git a/nearby/presence/np_adv/src/legacy/deserialize/tests/happy_path.rs b/nearby/presence/np_adv/src/legacy/deserialize/tests/happy_path.rs
index 1b07ff9..e5ae98f 100644
--- a/nearby/presence/np_adv/src/legacy/deserialize/tests/happy_path.rs
+++ b/nearby/presence/np_adv/src/legacy/deserialize/tests/happy_path.rs
@@ -15,7 +15,7 @@
 mod unencrypted {
     extern crate std;
 
-    use rand::prelude::SliceRandom;
+    use rand::seq::IteratorRandom;
     use std::{prelude::rust_2021::*, vec};
     use strum::IntoEnumIterator;
 
@@ -193,8 +193,13 @@
             let mut des = Vec::new();
             let mut builder = AdvBuilder::new(UnencryptedEncoder);
 
+            let mut possible_de_types = de_types.clone();
             loop {
-                let de_type = *de_types.choose(&mut rng).unwrap();
+                if possible_de_types.is_empty() {
+                    break;
+                }
+                let (i, _) = possible_de_types.iter().enumerate().choose(&mut rng).unwrap();
+                let de_type = possible_de_types.remove(i);
                 let de = build_de(de_type, &mut rng);
 
                 let add_res = add_de(&mut builder, de.clone());
@@ -257,6 +262,7 @@
 mod ldt {
     use crate::credential::matched::HasIdentityMatch;
     use crate::legacy::data_elements::actions::CallTransfer;
+    use crate::legacy::data_elements::de_type::MAX_DE_ENCODED_LEN;
     use crate::{
         credential::v0::V0BroadcastCredential,
         header::V0Encoding,
@@ -264,7 +270,7 @@
             data_elements::{
                 actions::{ActionBits, ActionsDataElement, NearbyShare},
                 de_type::DataElementType,
-                tests::test_des::{random_test_de, TestDataElementType},
+                tests::test_des::TestDataElementType,
                 tests::test_des::{TestDataElement, TestDeDeserializer},
                 tx_power::TxPowerDataElement,
                 DataElementSerializationBuffer, DynamicSerializeDataElement, SerializeDataElement,
@@ -288,7 +294,7 @@
     use alloc::vec::Vec;
     use crypto_provider_default::CryptoProviderImpl;
     use ldt_np_adv::{V0IdentityToken, V0Salt, V0_IDENTITY_TOKEN_LEN};
-    use rand::prelude::SliceRandom;
+    use rand::seq::IteratorRandom;
     use rand::Rng;
     use strum::IntoEnumIterator;
 
@@ -384,8 +390,13 @@
     #[test]
     fn random_test_des_roundtrip() {
         do_random_roundtrip_test::<TestDeDeserializer, _>(
-            TestDataElementType::iter().collect(),
-            random_test_de,
+            vec![TestDataElementType::Short],
+            |_, rng| {
+                let len = rng.gen_range(3_usize..MAX_DE_ENCODED_LEN.into());
+                let mut data = vec![0; len];
+                rng.fill(&mut data[..]);
+                TestDataElement::Short(ShortDataElement::new(data))
+            },
             |builder, de| builder.add_data_element(de),
             serialized_len,
         )
@@ -408,12 +419,12 @@
     {
         let mut rng = rand_ext::seeded_rng();
 
-        for _ in 0..10_000 {
+        for _ in 0..1_000 {
             let mut added_des = Vec::new();
             let mut current_len = 0;
 
             let key_seed: [u8; 32] = rng.gen();
-            let salt: ldt_np_adv::V0Salt = rng.gen::<[u8; 2]>().into();
+            let salt: V0Salt = rng.gen::<[u8; 2]>().into();
             let identity_token = V0IdentityToken::from(rng.gen::<[u8; V0_IDENTITY_TOKEN_LEN]>());
 
             let broadcast_cred = V0BroadcastCredential::new(key_seed, identity_token);
@@ -421,8 +432,13 @@
             let mut builder =
                 AdvBuilder::new(LdtEncoder::<CryptoProviderImpl>::new(salt, &broadcast_cred));
 
+            let mut possible_de_types = de_types.clone();
             loop {
-                let de_type = *de_types.choose(&mut rng).unwrap();
+                if possible_de_types.is_empty() {
+                    break;
+                }
+                let (i, _) = possible_de_types.iter().enumerate().choose(&mut rng).unwrap();
+                let de_type = possible_de_types.remove(i);
                 let de = build_de(de_type, &mut rng);
 
                 if let Err(e) = add_de(&mut builder, de.clone()) {
@@ -432,6 +448,7 @@
                                 < ldt_np_adv::VALID_INPUT_LEN.start - V0_IDENTITY_TOKEN_LEN
                             {
                                 // keep trying, not enough for LDT
+                                possible_de_types.push(de_type);
                                 continue;
                             }
                             // out of room
diff --git a/nearby/presence/np_adv/src/lib.rs b/nearby/presence/np_adv/src/lib.rs
index eb2ef0f..0e958fa 100644
--- a/nearby/presence/np_adv/src/lib.rs
+++ b/nearby/presence/np_adv/src/lib.rs
@@ -24,7 +24,12 @@
 #[cfg(any(test, feature = "alloc"))]
 extern crate alloc;
 
+#[cfg(feature = "std")]
+extern crate std;
+
+use core::fmt;
 pub use strum;
+pub use tinyvec::ArrayVec;
 
 use crate::credential::matched::MatchedCredential;
 use crate::extended::deserialize::{deser_decrypt_v1, V1AdvertisementContents};
@@ -125,7 +130,7 @@
 }
 
 impl Debug for AdvDeserializationError {
-    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             AdvDeserializationError::VersionHeaderParseError => {
                 write!(f, "VersionHeaderParseError")
@@ -135,6 +140,20 @@
     }
 }
 
+impl fmt::Display for AdvDeserializationError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            AdvDeserializationError::VersionHeaderParseError => {
+                write!(f, "VersionHeaderParseError")
+            }
+            AdvDeserializationError::ParseError { .. } => write!(f, "ParseError"),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for AdvDeserializationError {}
+
 /// Potentially hazardous details about deserialization errors. These error information can
 /// potentially expose side-channel information about the plaintext of the advertisements and/or
 /// the keys used to decrypt them. For any place that you avoid exposing the keys directly
@@ -167,6 +186,22 @@
     }
 }
 
+// trivial Debug/Display that won't leak anything
+impl Debug for AdvDeserializationErrorDetailsHazmat {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "AdvDeserializationErrorDetailsHazmat")
+    }
+}
+
+impl fmt::Display for AdvDeserializationErrorDetailsHazmat {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "AdvDeserializationErrorDetailsHazmat")
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for AdvDeserializationErrorDetailsHazmat {}
+
 /// DE length is out of range (e.g. > 4 bits for encoded V0, > max DE size for actual V0, >127 for
 /// V1) or invalid for the relevant DE type.
 #[derive(Debug, PartialEq, Eq)]
diff --git a/nearby/presence/np_adv/src/tests/deser_v1_tests.rs b/nearby/presence/np_adv/src/tests/deser_v1_tests.rs
index d0865c7..78b8dc4 100644
--- a/nearby/presence/np_adv/src/tests/deser_v1_tests.rs
+++ b/nearby/presence/np_adv/src/tests/deser_v1_tests.rs
@@ -256,7 +256,7 @@
     adv_builder: &mut AdvBuilder,
 ) -> Vec<SectionConfig<'a>> {
     let mut expected = Vec::new();
-    for _ in 0..rng.gen_range(1..=NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT) {
+    for _ in 0..rng.gen_range(1..=NP_V1_ADV_MAX_SECTION_COUNT) {
         let identity = identities.pick_random_identity(&mut rng);
         let mode: VerificationMode = random();
         let res = match mode {
diff --git a/nearby/presence/np_adv/src/tests/deser_v1_tests/error_condition.rs b/nearby/presence/np_adv/src/tests/deser_v1_tests/error_condition.rs
index 9f91dbd..57292cf 100644
--- a/nearby/presence/np_adv/src/tests/deser_v1_tests/error_condition.rs
+++ b/nearby/presence/np_adv/src/tests/deser_v1_tests/error_condition.rs
@@ -17,34 +17,6 @@
 use rand::SeedableRng;
 
 #[test]
-fn v1_multiple_plaintext_sections() {
-    let mut rng = StdRng::from_entropy();
-    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
-    add_plaintext_section(&mut rng, &mut adv_builder).unwrap();
-
-    // append an extra plaintext section
-    let adv = [
-        adv_builder.into_advertisement().as_slice(),
-        &[
-            0x00, // format unencrypted
-            0x03, // section len
-        ],
-        &[0xDD; 3], // 3 bytes of de contents
-    ]
-    .concat();
-
-    let arena = deserialization_arena!();
-    let cred_book = build_empty_cred_book();
-    let v1_error = deser_v1_error::<_, CryptoProviderImpl>(arena, &adv, &cred_book);
-    assert_eq!(
-        v1_error,
-        AdvDeserializationError::ParseError {
-            details_hazmat: AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
-        }
-    );
-}
-
-#[test]
 fn v1_plaintext_empty_contents() {
     let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
     let sb = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
@@ -197,7 +169,8 @@
                 // mangle the last 2 bytes of the suffix to invalidate the advertisement.
                 // the identity should still correctly match
                 let adv_len = adv.len();
-                adv[adv_len - 2..].copy_from_slice(&[0xFF; 2]);
+                adv[adv_len - 2] = adv[adv_len - 2].wrapping_add(1);
+                adv[adv_len - 1] = adv[adv_len - 1].wrapping_sub(1);
             },
         )
     }
@@ -248,7 +221,6 @@
     let mut adv = builder.into_advertisement().as_slice().to_vec();
 
     mangle_adv(&mut adv);
-
     let cred_book = identities.build_cred_book::<CryptoProviderImpl>();
     let arena = deserialization_arena!();
     let v1_contents = deser_v1::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book);
diff --git a/nearby/presence/np_adv/src/tests/deser_v1_tests/happy_path.rs b/nearby/presence/np_adv/src/tests/deser_v1_tests/happy_path.rs
index 2171bd6..ee0077d 100644
--- a/nearby/presence/np_adv/src/tests/deser_v1_tests/happy_path.rs
+++ b/nearby/presence/np_adv/src/tests/deser_v1_tests/happy_path.rs
@@ -68,6 +68,30 @@
 }
 
 #[test]
+fn v1_multiple_plaintext_sections() {
+    let mut rng = StdRng::from_entropy();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    add_plaintext_section(&mut rng, &mut adv_builder).unwrap();
+
+    // append an extra plaintext section
+    let adv = [
+        adv_builder.into_advertisement().as_slice(),
+        &[
+            0x00, // format unencrypted
+            0x03, // section len
+        ],
+        &[0xDD; 3], // 3 bytes of de contents
+    ]
+    .concat();
+
+    let arena = deserialization_arena!();
+    let cred_book = build_empty_cred_book();
+    let v1_contents = deser_v1::<_, CryptoProviderImpl>(arena, &adv, &cred_book);
+    assert_eq!(0, v1_contents.invalid_sections_count());
+    assert_eq!(2, v1_contents.sections().len());
+}
+
+#[test]
 fn v1_all_identities_resolvable_ciphertext() {
     let mut rng = StdRng::from_entropy();
     for _ in 0..100 {
diff --git a/nearby/presence/np_adv/tests/examples_v1.rs b/nearby/presence/np_adv/tests/examples_v1.rs
index 57e41fa..7b89f8d 100644
--- a/nearby/presence/np_adv/tests/examples_v1.rs
+++ b/nearby/presence/np_adv/tests/examples_v1.rs
@@ -35,7 +35,7 @@
             AdvBuilder, AdvertisementType, SignedEncryptedSectionEncoder, SingleTypeDataElement,
             UnencryptedSectionEncoder,
         },
-        NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT,
+        NP_V1_ADV_MAX_SECTION_COUNT,
     },
     shared_data::TxPower,
     AdvDeserializationError, AdvDeserializationErrorDetailsHazmat,
@@ -260,7 +260,7 @@
 #[test]
 fn v1_deser_plaintext_over_max_sections() {
     let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
-    for _ in 0..NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT {
+    for _ in 0..NP_V1_ADV_MAX_SECTION_COUNT {
         let mut section_builder = adv_builder.section_builder(UnencryptedSectionEncoder).unwrap();
         section_builder
             .add_de(|_salt| TxPowerDataElement::from(TxPower::try_from(7).unwrap()))
diff --git a/nearby/presence/np_adv_dynamic/Cargo.toml b/nearby/presence/np_adv_dynamic/Cargo.toml
index 5eb6863..17ff642 100644
--- a/nearby/presence/np_adv_dynamic/Cargo.toml
+++ b/nearby/presence/np_adv_dynamic/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/np_c_ffi/Cargo.toml b/nearby/presence/np_c_ffi/Cargo.toml
index 517c2c5..e482cbe 100644
--- a/nearby/presence/np_c_ffi/Cargo.toml
+++ b/nearby/presence/np_c_ffi/Cargo.toml
@@ -3,6 +3,7 @@
 version = "0.1.0"
 edition = "2021"
 publish = false
+license = "Apache-2.0"
 
 [dependencies]
 np_ffi_core = {workspace = true, default-features=false}
diff --git a/nearby/presence/np_c_ffi/include/c/np_c_ffi.h b/nearby/presence/np_c_ffi/include/c/np_c_ffi.h
index d84a49a..2cef974 100644
--- a/nearby/presence/np_c_ffi/include/c/np_c_ffi.h
+++ b/nearby/presence/np_c_ffi/include/c/np_c_ffi.h
@@ -689,7 +689,7 @@
  * A representation of a MatchedCredential which is passable across the FFI boundary
  */
 typedef struct {
-  uint32_t cred_id;
+  int64_t cred_id;
   const uint8_t *encrypted_metadata_bytes_buffer;
   uintptr_t encrypted_metadata_bytes_len;
 } np_ffi_FfiMatchedCredential;
@@ -972,7 +972,7 @@
    * The ID of the credential which
    * matched the deserialized adv
    */
-  uint32_t cred_id;
+  int64_t cred_id;
   /**
    * The 14-byte legacy identity token
    */
@@ -1123,7 +1123,7 @@
    * The ID of the credential which
    * matched the deserialized section.
    */
-  uint32_t cred_id;
+  int64_t cred_id;
   /**
    * The 16-byte metadata key.
    */
@@ -1229,18 +1229,10 @@
 } np_ffi_FixedSizeArray_2;
 
 /**
- * A `#[repr(C)]` handle to a value of type `V1AdvertisementBuilderInternals`
- */
-typedef struct {
-  uint64_t handle_id;
-} np_ffi_V1AdvertisementBuilderHandle;
-
-/**
  * A handle to a builder for V1 advertisements.
  */
 typedef struct {
-  np_ffi_AdvertisementBuilderKind kind;
-  np_ffi_V1AdvertisementBuilderHandle handle;
+  uint64_t handle_id;
 } np_ffi_V1AdvertisementBuilder;
 
 /**
@@ -1248,7 +1240,13 @@
  * the advertisement builder the section builder was originated from.
  */
 typedef struct {
+  /**
+   * The parent advertisement builder for this section
+   */
   np_ffi_V1AdvertisementBuilder adv_builder;
+  /**
+   * This section's index in the parent advertisement
+   */
   uint8_t section_index;
 } np_ffi_V1SectionBuilder;
 
diff --git a/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h b/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h
index 0206f80..919249d 100644
--- a/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h
+++ b/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h
@@ -457,7 +457,7 @@
 
 /// A representation of a MatchedCredential which is passable across the FFI boundary
 struct FfiMatchedCredential {
-  uint32_t cred_id;
+  int64_t cred_id;
   const uint8_t *encrypted_metadata_bytes_buffer;
   uintptr_t encrypted_metadata_bytes_len;
 };
@@ -704,7 +704,7 @@
 struct DeserializedV0IdentityDetails {
   /// The ID of the credential which
   /// matched the deserialized adv
-  uint32_t cred_id;
+  int64_t cred_id;
   /// The 14-byte legacy identity token
   uint8_t identity_token[14];
   /// The 2-byte advertisement salt
@@ -820,7 +820,7 @@
   V1VerificationMode verification_mode;
   /// The ID of the credential which
   /// matched the deserialized section.
-  uint32_t cred_id;
+  int64_t cred_id;
   /// The 16-byte metadata key.
   uint8_t identity_token[16];
 };
@@ -900,21 +900,17 @@
   uint8_t identity_token[14];
 };
 
-/// A `#[repr(C)]` handle to a value of type `V1AdvertisementBuilderInternals`
-struct V1AdvertisementBuilderHandle {
-  uint64_t handle_id;
-};
-
 /// A handle to a builder for V1 advertisements.
 struct V1AdvertisementBuilder {
-  AdvertisementBuilderKind kind;
-  V1AdvertisementBuilderHandle handle;
+  uint64_t handle_id;
 };
 
 /// A handle to a builder for V1 sections. This is not a unique handle; it is the same handle as
 /// the advertisement builder the section builder was originated from.
 struct V1SectionBuilder {
+  /// The parent advertisement builder for this section
   V1AdvertisementBuilder adv_builder;
+  /// This section's index in the parent advertisement
   uint8_t section_index;
 };
 
diff --git a/nearby/presence/np_c_ffi/src/credentials.rs b/nearby/presence/np_c_ffi/src/credentials.rs
index d03f0f6..0c8a316 100644
--- a/nearby/presence/np_c_ffi/src/credentials.rs
+++ b/nearby/presence/np_c_ffi/src/credentials.rs
@@ -153,7 +153,7 @@
 /// A representation of a MatchedCredential which is passable across the FFI boundary
 #[repr(C)]
 pub struct FfiMatchedCredential {
-    cred_id: u32,
+    cred_id: i64,
     encrypted_metadata_bytes_buffer: *const u8,
     encrypted_metadata_bytes_len: usize,
 }
diff --git a/nearby/presence/np_cpp_ffi/tests/v0_encrypted_deserialization_tests.cc b/nearby/presence/np_cpp_ffi/tests/v0_encrypted_deserialization_tests.cc
index bd1f771..37d2746 100644
--- a/nearby/presence/np_cpp_ffi/tests/v0_encrypted_deserialization_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/v0_encrypted_deserialization_tests.cc
@@ -166,6 +166,6 @@
   // Make sure the correct credential matches
   auto identity_details = payload.TryGetIdentityDetails();
   ASSERT_TRUE(identity_details.ok());
-  ASSERT_EQ(identity_details->cred_id, 456u);
+  ASSERT_EQ(identity_details->cred_id, 456);
 }
 // NOLINTEND(readability-magic-numbers)
diff --git a/nearby/presence/np_cpp_ffi/tests/v1_encrypted_deserialization_tests.cc b/nearby/presence/np_cpp_ffi/tests/v1_encrypted_deserialization_tests.cc
index 65b9730..69a042d 100644
--- a/nearby/presence/np_cpp_ffi/tests/v1_encrypted_deserialization_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/v1_encrypted_deserialization_tests.cc
@@ -59,7 +59,7 @@
 
   auto identity_details = section->GetIdentityDetails();
   ASSERT_TRUE(identity_details.ok());
-  ASSERT_EQ(identity_details->cred_id, 123U);
+  ASSERT_EQ(identity_details->cred_id, 123);
   ASSERT_EQ(identity_details->verification_mode,
             nearby_protocol::V1VerificationMode::Signature);
 
diff --git a/nearby/presence/np_ed25519/Cargo.toml b/nearby/presence/np_ed25519/Cargo.toml
index bd42c65..5addbb1 100644
--- a/nearby/presence/np_ed25519/Cargo.toml
+++ b/nearby/presence/np_ed25519/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/np_ffi_core/Cargo.toml b/nearby/presence/np_ffi_core/Cargo.toml
index 7c3c112..4faebe6 100644
--- a/nearby/presence/np_ffi_core/Cargo.toml
+++ b/nearby/presence/np_ffi_core/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
@@ -25,3 +26,5 @@
 default = ["rustcrypto"]
 rustcrypto = ["crypto_provider_default/rustcrypto", "crypto_provider_default/std"]
 boringssl = ["crypto_provider_default/boringssl"]
+testing = ["np_adv/testing"]
+std = []
diff --git a/nearby/presence/np_ffi_core/src/common.rs b/nearby/presence/np_ffi_core/src/common.rs
index da7cb5c..607a6af 100644
--- a/nearby/presence/np_ffi_core/src/common.rs
+++ b/nearby/presence/np_ffi_core/src/common.rs
@@ -219,7 +219,7 @@
     /// ArrayView, which is assumed to be trusted to be
     /// properly initialized, and with a size-bound
     /// under 255 bytes.
-    pub(crate) fn from_array_view(array_view: ArrayView<u8, N>) -> Self {
+    pub fn from_array_view(array_view: ArrayView<u8, N>) -> Self {
         let (len, bytes) = array_view.into_raw_parts();
         let len = len as u8;
         Self { len, bytes }
@@ -267,22 +267,3 @@
 /// in an entirely unexpected way.
 #[derive(Debug)]
 pub struct InvalidStackDataStructure;
-
-/// Error raised when attempting to cast an enum to
-/// one of its variants, but the value is actually
-/// of a different variant than the requested one.
-pub struct EnumCastError {
-    pub(crate) projection_method_name: String,
-    pub(crate) variant_enum_name: String,
-    pub(crate) variant_type_name: String,
-}
-
-impl core::fmt::Debug for EnumCastError {
-    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
-        write!(
-            f,
-            "Attempted to cast a non-{} to a {} via {}",
-            &self.variant_enum_name, &self.variant_type_name, &self.projection_method_name
-        )
-    }
-}
diff --git a/nearby/presence/np_ffi_core/src/credentials.rs b/nearby/presence/np_ffi_core/src/credentials.rs
index 1ad303f..4f6c3cd 100644
--- a/nearby/presence/np_ffi_core/src/credentials.rs
+++ b/nearby/presence/np_ffi_core/src/credentials.rs
@@ -100,24 +100,24 @@
 /// the FFI boundary.
 #[derive(Debug, Clone)]
 pub struct MatchedCredential {
-    cred_id: u32,
+    cred_id: i64,
     encrypted_metadata_bytes: Arc<[u8]>,
 }
 
 impl MatchedCredential {
     /// Constructs a new matched credential from the given match-id
-    /// (some arbitrary `u32` identifier) and encrypted metadata bytes,
+    /// (some arbitrary `i64` identifier) and encrypted metadata bytes,
     /// copied from the given slice.
-    pub fn new(cred_id: u32, encrypted_metadata_bytes: &[u8]) -> Self {
+    pub fn new(cred_id: i64, encrypted_metadata_bytes: &[u8]) -> Self {
         Self::from_arc_bytes(cred_id, encrypted_metadata_bytes.to_vec().into())
     }
     /// Constructs a new matched credential from the given match-id
     /// (some arbitrary `u32` identifier) and encrypted metadata bytes.
-    pub fn from_arc_bytes(cred_id: u32, encrypted_metadata_bytes: Arc<[u8]>) -> Self {
+    pub fn from_arc_bytes(cred_id: i64, encrypted_metadata_bytes: Arc<[u8]>) -> Self {
         Self { cred_id, encrypted_metadata_bytes }
     }
     /// Gets the pre-specified numerical identifier for this matched-credential.
-    pub(crate) fn id(&self) -> u32 {
+    pub(crate) fn id(&self) -> i64 {
         self.cred_id
     }
 }
diff --git a/nearby/presence/np_ffi_core/src/deserialize/v0.rs b/nearby/presence/np_ffi_core/src/deserialize/v0.rs
index 79528a0..67d798d 100644
--- a/nearby/presence/np_ffi_core/src/deserialize/v0.rs
+++ b/nearby/presence/np_ffi_core/src/deserialize/v0.rs
@@ -184,7 +184,7 @@
 pub struct DeserializedV0IdentityDetails {
     /// The ID of the credential which
     /// matched the deserialized adv
-    cred_id: u32,
+    cred_id: i64,
     /// The 14-byte legacy identity token
     identity_token: [u8; 14],
     /// The 2-byte advertisement salt
@@ -193,7 +193,7 @@
 
 impl DeserializedV0IdentityDetails {
     pub(crate) fn new(
-        cred_id: u32,
+        cred_id: i64,
         salt: ldt_np_adv::V0Salt,
         identity_token: ldt_np_adv::V0IdentityToken,
     ) -> Self {
@@ -201,7 +201,7 @@
         Self { cred_id, salt, identity_token: identity_token.bytes() }
     }
     /// Returns the ID of the credential which matched the deserialized adv
-    pub fn cred_id(&self) -> u32 {
+    pub fn cred_id(&self) -> i64 {
         self.cred_id
     }
     /// Returns the 14-byte legacy metadata key
diff --git a/nearby/presence/np_ffi_core/src/deserialize/v1.rs b/nearby/presence/np_ffi_core/src/deserialize/v1.rs
index 6007975..849abab 100644
--- a/nearby/presence/np_ffi_core/src/deserialize/v1.rs
+++ b/nearby/presence/np_ffi_core/src/deserialize/v1.rs
@@ -545,14 +545,14 @@
     verification_mode: V1VerificationMode,
     /// The ID of the credential which
     /// matched the deserialized section.
-    cred_id: u32,
+    cred_id: i64,
     /// The 16-byte metadata key.
     identity_token: [u8; 16],
 }
 
 impl DeserializedV1IdentityDetails {
     pub(crate) fn new(
-        cred_id: u32,
+        cred_id: i64,
         verification_mode: np_adv::extended::deserialize::VerificationMode,
         identity_token: np_adv::extended::V1IdentityToken,
     ) -> Self {
@@ -560,7 +560,7 @@
         Self { cred_id, verification_mode, identity_token: identity_token.into_bytes() }
     }
     /// Returns the ID of the credential which matched the deserialized section.
-    pub fn cred_id(&self) -> u32 {
+    pub fn cred_id(&self) -> i64 {
         self.cred_id
     }
     /// Returns the verification mode (MIC/Signature) employed for the decrypted section.
diff --git a/nearby/presence/np_ffi_core/src/lib.rs b/nearby/presence/np_ffi_core/src/lib.rs
index 2c03ea5..0fd7f1a 100644
--- a/nearby/presence/np_ffi_core/src/lib.rs
+++ b/nearby/presence/np_ffi_core/src/lib.rs
@@ -15,9 +15,6 @@
 //! Core functionality common to all NP Rust FFI layers
 
 #[macro_use]
-extern crate lazy_static;
-
-#[macro_use]
 pub mod utils;
 pub mod common;
 pub mod credentials;
diff --git a/nearby/presence/np_ffi_core/src/serialize/v1.rs b/nearby/presence/np_ffi_core/src/serialize/v1.rs
index c87446f..1191ac1 100644
--- a/nearby/presence/np_ffi_core/src/serialize/v1.rs
+++ b/nearby/presence/np_ffi_core/src/serialize/v1.rs
@@ -20,15 +20,25 @@
 use crate::v1::V1VerificationMode;
 use crypto_provider_default::CryptoProviderImpl;
 use handle_map::{declare_handle_map, HandleLike, HandleMapFullError};
+use np_adv::extended::serialize::AdvertisementType;
+
+#[cfg(feature = "testing")]
+use np_adv::extended::salt::MultiSalt;
 
 /// A handle to a builder for V1 advertisements.
-#[derive(Clone, Copy)]
 #[repr(C)]
+#[derive(Clone, Copy, PartialEq, Eq)]
 pub struct V1AdvertisementBuilder {
-    kind: AdvertisementBuilderKind,
-    handle: V1AdvertisementBuilderHandle,
+    handle_id: u64,
 }
 
+declare_handle_map!(
+    advertisement_builder,
+    crate::common::default_handle_map_dimensions(),
+    super::V1AdvertisementBuilder,
+    super::V1AdvertisementBuilderInternals
+);
+
 impl V1AdvertisementBuilder {
     /// Attempts to create a builder for a new public section within this advertisement, returning
     /// an owned handle to the newly-created section builder if successful.
@@ -41,11 +51,6 @@
         self.section_builder_internals(|internals| internals.public_section_builder())
     }
 
-    /// Gets the kind of advertisement builder (public/encrypted)
-    pub fn kind(&self) -> AdvertisementBuilderKind {
-        self.kind
-    }
-
     /// Attempts to create a builder for a new encrypted section within this advertisement,
     /// returning an owned handle to the newly-created section builder if successful.
     ///
@@ -69,12 +74,38 @@
         })
     }
 
+    /// Attempts to create a builder for a new encrypted section within this advertisement,
+    /// returning an owned handle to the newly-created section builder if successful.
+    ///
+    /// The identity details for the new section builder may be specified
+    /// via providing the broadcast credential data, the kind of encrypted
+    /// identity being broadcast (private/trusted/provisioned).
+    ///
+    ///
+    /// The section will be encrypted with the MIC verification mode using the given salt.
+    ///
+    /// This method may fail if there is another currently-active
+    /// section builder for the same advertisement builder, if the
+    /// kind of section being added does not match the advertisement
+    /// type (public/encrypted), or if the section would not manage
+    /// to fit within the enclosing advertisement.
+    #[cfg(feature = "testing")]
+    pub fn mic_custom_salt_section_builder(
+        &self,
+        broadcast_cred: V1BroadcastCredential,
+        salt: MultiSalt,
+    ) -> CreateV1SectionBuilderResult {
+        self.section_builder_internals(move |internals| {
+            internals.mic_custom_salt_section_builder(broadcast_cred, salt)
+        })
+    }
+
     /// Attempts to serialize the contents of the advertisement builder behind this handle to
     /// bytes. Assuming that the handle is valid, this operation will always take ownership of the
     /// handle and result in the contents behind the advertisement builder handle being
     /// deallocated.
     pub fn into_advertisement(self) -> SerializeV1AdvertisementResult {
-        match self.handle.deallocate() {
+        match self.deallocate() {
             Ok(adv_builder) => adv_builder.into_advertisement(),
             Err(_) => SerializeV1AdvertisementResult::InvalidAdvertisementBuilderHandle,
         }
@@ -86,7 +117,7 @@
             &mut V1AdvertisementBuilderInternals,
         ) -> Result<usize, SectionBuilderError>,
     ) -> CreateV1SectionBuilderResult {
-        match self.handle.get_mut() {
+        match self.get_mut() {
             Ok(mut adv_builder_write_guard) => {
                 match builder_supplier(&mut adv_builder_write_guard) {
                     Ok(section_index) => CreateV1SectionBuilderResult::Success(V1SectionBuilder {
@@ -157,9 +188,10 @@
 pub fn create_v1_advertisement_builder(
     kind: AdvertisementBuilderKind,
 ) -> CreateV1AdvertisementBuilderResult {
-    V1AdvertisementBuilderHandle::allocate(move || V1AdvertisementBuilderInternals::new(kind))
-        .map(|handle| V1AdvertisementBuilder { kind, handle })
-        .into()
+    V1AdvertisementBuilder::allocate(move || {
+        V1AdvertisementBuilderInternals::new(kind.as_internal_v1())
+    })
+    .into()
 }
 
 pub(crate) enum V1AdvertisementBuilderState {
@@ -256,8 +288,7 @@
 }
 
 impl V1AdvertisementBuilderInternals {
-    pub(crate) fn new(kind: AdvertisementBuilderKind) -> Self {
-        let adv_type = kind.as_internal_v1();
+    pub(crate) fn new(adv_type: AdvertisementType) -> Self {
         let builder = np_adv::extended::serialize::AdvBuilder::new(adv_type);
         let builder = builder.into();
         let state = Some(V1AdvertisementBuilderState::Advertisement(builder));
@@ -324,6 +355,21 @@
         };
         self.section_builder_internal(encoder)
     }
+
+    #[cfg(feature = "testing")]
+    pub(crate) fn mic_custom_salt_section_builder(
+        &mut self,
+        broadcast_cred: V1BroadcastCredential,
+        salt: MultiSalt,
+    ) -> Result<usize, SectionBuilderError> {
+        let internal_broadcast_cred = broadcast_cred.into_internal();
+        let encoder = np_adv::extended::serialize::MicEncryptedSectionEncoder::new_with_salt::<
+            CryptoProviderImpl,
+        >(salt, &internal_broadcast_cred);
+        let encoder = np_adv_dynamic::extended::BoxedEncoder::MicEncrypted(encoder);
+        self.section_builder_internal(encoder)
+    }
+
     fn into_advertisement(self) -> SerializeV1AdvertisementResult {
         match self.state {
             Some(V1AdvertisementBuilderState::Advertisement(adv_builder)) => {
@@ -335,20 +381,6 @@
     }
 }
 
-/// A `#[repr(C)]` handle to a value of type `V1AdvertisementBuilderInternals`
-#[repr(C)]
-#[derive(Clone, Copy, PartialEq, Eq)]
-struct V1AdvertisementBuilderHandle {
-    handle_id: u64,
-}
-
-declare_handle_map!(
-    advertisement_builder,
-    crate::common::default_handle_map_dimensions(),
-    super::V1AdvertisementBuilderHandle,
-    super::V1AdvertisementBuilderInternals
-);
-
 /// Discriminant for `CreateV1SectionBuilderResult`
 #[derive(Copy, Clone)]
 #[repr(u8)]
@@ -493,8 +525,10 @@
 #[derive(Clone, Copy)]
 #[repr(C)]
 pub struct V1SectionBuilder {
-    adv_builder: V1AdvertisementBuilder,
-    section_index: u8,
+    /// The parent advertisement builder for this section
+    pub adv_builder: V1AdvertisementBuilder,
+    /// This section's index in the parent advertisement
+    pub section_index: u8,
 }
 
 impl V1SectionBuilder {
@@ -502,7 +536,7 @@
     /// to a section builder to the containing advertisement it
     /// originated from.
     pub fn add_to_advertisement(self) -> AddV1SectionToAdvertisementResult {
-        match self.adv_builder.handle.get_mut() {
+        match self.adv_builder.get_mut() {
             Ok(mut adv_builder) => {
                 let state = adv_builder.state.take();
                 match state {
@@ -579,7 +613,7 @@
         ) -> R,
         invalid_handle_error: R,
     ) -> R {
-        match self.adv_builder.handle.get_mut() {
+        match self.adv_builder.get_mut() {
             Ok(mut adv_builder) => {
                 match adv_builder.state.as_mut() {
                     Some(V1AdvertisementBuilderState::Section(ref mut section_builder)) => {
@@ -641,8 +675,7 @@
     #[allow(clippy::expect_used)]
     #[test]
     fn test_build_section_fails_with_outstanding_section() {
-        let mut adv_builder =
-            V1AdvertisementBuilderInternals::new(AdvertisementBuilderKind::Encrypted);
+        let mut adv_builder = V1AdvertisementBuilderInternals::new(AdvertisementType::Encrypted);
 
         let adv_builder_state =
             adv_builder.state.as_ref().expect("Adv builder state should be present.");
diff --git a/nearby/presence/np_hkdf/Cargo.toml b/nearby/presence/np_hkdf/Cargo.toml
index cd7fb4c..4923d61 100644
--- a/nearby/presence/np_hkdf/Cargo.toml
+++ b/nearby/presence/np_hkdf/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/np_java_ffi/AndroidManifest.xml b/nearby/presence/np_java_ffi/AndroidManifest.xml
new file mode 100644
index 0000000..04ad56d
--- /dev/null
+++ b/nearby/presence/np_java_ffi/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.nearby.presence.rust">
+    <uses-sdk android:minSdkVersion="33" />
+</manifest>
diff --git a/nearby/presence/np_java_ffi/Cargo.toml b/nearby/presence/np_java_ffi/Cargo.toml
index 556b481..134dfbb 100644
--- a/nearby/presence/np_java_ffi/Cargo.toml
+++ b/nearby/presence/np_java_ffi/Cargo.toml
@@ -3,11 +3,16 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
 
+[features]
+testing = ["np_ffi_core/testing"]
+
 [dependencies]
+array_view.workspace = true
 handle_map.workspace = true
 np_adv.workspace = true
 np_ffi_core.workspace = true
diff --git a/nearby/presence/np_java_ffi/build.gradle.kts b/nearby/presence/np_java_ffi/build.gradle.kts
index c502cdb..6fe9e82 100644
--- a/nearby/presence/np_java_ffi/build.gradle.kts
+++ b/nearby/presence/np_java_ffi/build.gradle.kts
@@ -14,8 +14,17 @@
  * limitations under the License.
  */
 
+import net.ltgt.gradle.errorprone.errorprone;
+
 plugins {
   `java-library`
+  id("net.ltgt.errorprone") version "4.0.0"
+}
+
+java {
+  // Gradle JUnit test finder doesn't support Java 21 class files.
+  sourceCompatibility = JavaVersion.VERSION_1_9
+  targetCompatibility = JavaVersion.VERSION_1_9
 }
 
 repositories {
@@ -25,13 +34,14 @@
 
 dependencies {
   implementation("androidx.annotation:annotation:1.6.0")
+  implementation("com.google.errorprone:error_prone_core:2.28.0")
+  implementation("org.checkerframework:checker-qual:3.45.0")
 
   // JUnit Test Support
-  testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
+  testImplementation("junit:junit:4.13")
   testImplementation("com.google.truth:truth:1.1.4")
+  testImplementation("com.google.code.gson:gson:2.10.1")
   testImplementation("org.mockito:mockito-core:5.+")
-  testImplementation("org.mockito:mockito-junit-jupiter:5.+")
-  testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
 }
 
 // Flattened directory layout
@@ -49,7 +59,7 @@
 }
 
 tasks.test {
-  useJUnitPlatform()
+  useJUnit()
   jvmArgs = mutableListOf(
       // libnp_java_ffi.so
       "-Djava.library.path=$projectDir/../../target/debug",
@@ -57,3 +67,18 @@
       "-XX:+EnableDynamicAgentLoading"
   )
 }
+
+tasks.withType<JavaCompile>().configureEach {
+  options.errorprone {
+    error("CheckReturnValue")
+    error("UnnecessaryStaticImport")
+    error("WildcardImport")
+    error("RemoveUnusedImports")
+    error("ReturnMissingNullable")
+    error("FieldMissingNullable")
+    error("AnnotationPosition")
+    error("CheckedExceptionNotThrown")
+    error("NonFinalStaticField")
+    error("InvalidLink")
+  }
+}
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/CooperativeCleaner.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/CooperativeCleaner.java
new file mode 100644
index 0000000..e420f71
--- /dev/null
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/CooperativeCleaner.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.HashSet;
+import org.checkerframework.checker.initialization.qual.Initialized;
+import org.checkerframework.checker.initialization.qual.UnknownInitialization;
+
+/**
+ * A similar class to {@link java.lang.ref.Cleaner} that doesn't require a dedicated thread.
+ *
+ * <p>To avoid using a thread for the reference queue, clients are expected to call {@link
+ * Registration#close()} on the object returned from {@link register(Object, Runnable)}. This will
+ * perform two actions:
+ *
+ * <ol>
+ *   <li>Run the cleanup actions for any registered objects that have been finaized without {@code
+ *       close()} being called.
+ *   <li>Run the cleanup action for the object being closed.
+ * </ol>
+ *
+ * <p>The application should keep a reference to {@code this} instance to avoid the cleanup actions
+ * themselves being garbage collected!
+ */
+public class CooperativeCleaner {
+
+  /**
+   * An object registration with this CooperativeCleaner. Clients should call the {@link
+   * Registration#close()} method when the registered object is ready to be cleaned up.
+   *
+   * @see CooperativeCleaner documentation for more details.
+   */
+  public class Registration implements AutoCloseable {
+    private final CleanableReference ref;
+
+    private Registration(CleanableReference ref) {
+      this.ref = ref;
+    }
+
+    @Override
+    public void close() {
+      // Cleanup any queued objects first (Cooperation!)
+      processQueuedObjects();
+
+      // Run cleanup for the referenced object.
+      ref.performCleanup();
+    }
+  }
+
+  /**
+   * Objects that are not closed will be posted to this reference queue by the garbage collector.
+   */
+  @GuardedBy("queue")
+  private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
+
+  /**
+   * Holds strong references to {@link CleanableReference} instances so that the reference objects
+   * are not garbage collected.
+   */
+  @GuardedBy("refs")
+  private final HashSet<CleanableReference> refs = new HashSet<>();
+
+  /**
+   * Register an object for cleanup. The object will be cleaned up when the returned {@code
+   * Registration} is closed or sometime after the object has been finalized. The returned value
+   * should always be closed if possible. If it is closed the {@code cleanupAction} will be run on
+   * the thread that called {@code close()}; otherwise, it will be run during {@code close()} for a
+   * different object. In all cases {@code cleanupAction} will be run exactly once.
+   *
+   * <p>The given {@code cleanupAction} <b>SHOULD NOT</b> hold a reference to {@code object}.
+   * Holding such a reference would mean that {@code object} is always reachable and it will never
+   * be garbaged collected.
+   */
+  public Registration register(@UnknownInitialization Object object, Runnable cleanupAction) {
+    // The cast here is safe because it is impossible to get object back out of this reference.
+    // PhantomReference will always return null for {@link PhantomReference#get()}. We don't care
+    // about the initialization state of the object; rather, we only care about the identity of the
+    // object. Likewise, an actual reference to object is never stored by this class; that would
+    // stop the object from ever being garbage collected.
+    @SuppressWarnings("nullness:cast.unsafe")
+    CleanableReference ref = new CleanableReference((@Initialized Object) object, cleanupAction);
+
+    // Keep a copy of the reference in {@code refs} to avoid being garbage collected.
+    synchronized (refs) {
+      refs.add(ref);
+    }
+
+    return new Registration(ref);
+  }
+
+  /** Gets the number of objects that are currently registered with this cleaner. */
+  @VisibleForTesting
+  public int getRegisteredObjectCount() {
+    synchronized (refs) {
+      return refs.size();
+    }
+  }
+
+  /** Gets the next {@link CleanableReference} from the queue if there is one. */
+  @Nullable
+  private CleanableReference pollQueue() {
+    synchronized (queue) {
+      return (CleanableReference) queue.poll();
+    }
+  }
+
+  /** Process cleanup actions for all objects in the reference queue. */
+  @VisibleForTesting
+  public void processQueuedObjects() {
+    CleanableReference ref;
+    while ((ref = pollQueue()) != null) {
+      ref.performCleanup();
+    }
+  }
+
+  /**
+   * Stores the {@code cleanupAction} and tracks the lifecycle of {@code object}. {@link
+   * #performCleanup()} can be called early to perform the cleanup action.
+   */
+  private class CleanableReference extends PhantomReference<Object> {
+    @Nullable private Runnable cleanupAction;
+
+    private CleanableReference(Object object, Runnable cleanupAction) {
+      // This reference will be enqueued onto {@code queue} if object becomes phantom-reachable and
+      // {@link #clear()} hasn't been called.
+      super(object, queue);
+
+      this.cleanupAction = cleanupAction;
+    }
+
+    private void performCleanup() {
+      // Cleanup this object
+      @Nullable Runnable action;
+      synchronized (refs) {
+        // Do not post this to the reference queue if this was called before the object was
+        // posted.
+        this.clear();
+
+        // This object is no longer needed, so we can remove our reference to it so that it may be
+        // garbaged collected.
+        refs.remove(this);
+
+        // Make sure that cleanupAction is only run once by taking the value before running it
+        action = this.cleanupAction;
+        this.cleanupAction = null;
+      }
+
+      if (action != null) {
+        // Run cleanupAction outside of the lock so that multiple cleanups may occur on separate
+        // threads.
+        action.run();
+      }
+    }
+  }
+}
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializeResult.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializeResult.java
index c0dc922..5be7f61 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializeResult.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializeResult.java
@@ -34,6 +34,7 @@
 public final class DeserializeResult<M extends CredentialBook.MatchedMetadata>
     implements AutoCloseable {
 
+  /** The kind of result. Negative values are errors and do not contain advertisement instances. */
   @IntDef({
     Kind.UNKNOWN_ERROR,
     Kind.V0_ADVERTISEMENT,
@@ -52,8 +53,8 @@
     return kind <= 0;
   }
 
-  private final @Kind int kind;
-  private final @Nullable DeserializedAdvertisement advertisement;
+  @Kind private final int kind;
+  @Nullable private final DeserializedAdvertisement advertisement;
 
   /** Create a DeserializeResult containing an error code */
   /* package */ DeserializeResult(@Kind int errorKind) {
@@ -66,13 +67,13 @@
   }
 
   /** Create a DeserializeResult containing a V0 advertisement */
-  /* package */ DeserializeResult(DeserializedV0Advertisement advertisement) {
+  /* package */ DeserializeResult(DeserializedV0Advertisement<M> advertisement) {
     this.kind = Kind.V0_ADVERTISEMENT;
     this.advertisement = advertisement;
   }
 
   /** Create a DeserializeResult containing a V1 advertisement */
-  /* package */ DeserializeResult(DeserializedV1Advertisement advertisement) {
+  /* package */ DeserializeResult(DeserializedV1Advertisement<M> advertisement) {
     this.kind = Kind.V1_ADVERTISEMENT;
     this.advertisement = advertisement;
   }
@@ -93,6 +94,7 @@
    *
    * @return the contained V0 advertisement or {@code null} if not present
    */
+  @SuppressWarnings("unchecked")
   @Nullable
   public DeserializedV0Advertisement<M> getAsV0() {
     if (this.kind != Kind.V0_ADVERTISEMENT) {
@@ -106,12 +108,13 @@
    *
    * @return the contained V1 advertisement or {@code null} if not present
    */
+  @SuppressWarnings("unchecked")
   @Nullable
   public DeserializedV1Advertisement<M> getAsV1() {
     if (this.kind != Kind.V1_ADVERTISEMENT) {
       return null;
     }
-    return (DeserializedV1Advertisement) this.advertisement;
+    return (DeserializedV1Advertisement<M>) this.advertisement;
   }
 
   /** Closes the contained advertisement if it exists. */
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV0Advertisement.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV0Advertisement.java
index cb557c5..7df838e 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV0Advertisement.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV0Advertisement.java
@@ -19,6 +19,7 @@
 import androidx.annotation.Nullable;
 import com.google.android.nearby.presence.rust.credential.CredentialBook;
 import java.util.Iterator;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
 
 /**
  * A deserialized V0 advertisement. This class is backed by native data behind the {@link V0Payload}
@@ -32,9 +33,9 @@
   }
 
   private final int numDataElements;
-  private final @Nullable V0Payload payload;
-  private final @IdentityKind int identity;
-  private final CredentialBook<M> credentialBook;
+  @Nullable private final V0Payload payload;
+  @IdentityKind private final int identity;
+  @Nullable private final CredentialBook<M> credentialBook;
 
   /** Create an illegible instance with the given error identity. */
   /* package */ DeserializedV0Advertisement(@IdentityKind int illegibleIdentity) {
@@ -79,8 +80,9 @@
   }
 
   /** Throws {@code IllegalStateException} if this advertisement is not legible. */
+  @EnsuresNonNull({"payload", "credentialBook"})
   private void ensureLegible(String action) {
-    if (!isLegible()) {
+    if (!isLegible() || payload == null || credentialBook == null) {
       throw new IllegalStateException(
           String.format("Cannot %s for non-legible advertisement", action));
     }
@@ -122,7 +124,11 @@
 
   /** Gets all the data elements for iteration. */
   public Iterable<V0DataElement> getDataElements() {
-    return () -> new DataElementIterator(payload, numDataElements);
+    ensureLegible("get data elements");
+    return () -> {
+      ensureLegible("get data element iterator");
+      return new DataElementIterator(payload, numDataElements);
+    };
   }
 
   /** Visits all the data elements with the given visitor. */
@@ -179,6 +185,7 @@
    */
   @Nullable
   public byte[] getDecryptedMetadata() {
+    ensureLegible("get decrypted metadata");
     return payload.getDecryptedMetadata();
   }
 
@@ -196,7 +203,7 @@
 
     @Override
     public boolean hasNext() {
-      return position < (numDataElements - 1);
+      return position < numDataElements;
     }
 
     @Override
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Advertisement.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Advertisement.java
index 8f4c3a3..cbe855a 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Advertisement.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Advertisement.java
@@ -20,7 +20,7 @@
 import java.util.Iterator;
 
 /**
- * A deserialized V0 advertisement. This class is backed by native data behind the {@link
+ * A deserialized V1 advertisement. This class is backed by native data behind the {@link
  * LegibleV1Sections} handle. If this class is closed then the underlying handle will be closed too.
  * Methods on this class should not be called if {@link #close()} has already been called.
  */
@@ -67,13 +67,13 @@
   }
 
   /** Get an iterable of this advertisement's legible sections. */
-  public Iterable<DeserializedV1Section> getSections() {
-    return () -> new SectionIterator(numLegibleSections, legibleSections, credentialBook);
+  public Iterable<DeserializedV1Section<M>> getSections() {
+    return () -> new SectionIterator<>(numLegibleSections, legibleSections, credentialBook);
   }
 
   /** Iterator instance for sections in DeserializedV1Advertisement. */
   private static final class SectionIterator<M extends CredentialBook.MatchedMetadata>
-      implements Iterator<DeserializedV1Section> {
+      implements Iterator<DeserializedV1Section<M>> {
     private final LegibleV1Sections legibleSections;
     private final int numSections;
     private final CredentialBook<M> credentialBook;
@@ -89,11 +89,11 @@
 
     @Override
     public boolean hasNext() {
-      return position < (numSections - 1);
+      return position < numSections;
     }
 
     @Override
-    public DeserializedV1Section next() {
+    public DeserializedV1Section<M> next() {
       return legibleSections.getSection(position++, credentialBook);
     }
   }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Section.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Section.java
index d320963..3c26e9e 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Section.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Section.java
@@ -16,27 +16,21 @@
 
 package com.google.android.nearby.presence.rust;
 
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
 import com.google.android.nearby.presence.rust.credential.CredentialBook;
-import java.lang.annotation.Retention;
 import java.util.Iterator;
 
+/**
+ * A section from a deserialized V1 advertisement. This object is valid for only as long as the
+ * containing advertisement object is valid; it is backed by the same {@link LegibleV1Sections}
+ * instance.
+ */
 public final class DeserializedV1Section<M extends CredentialBook.MatchedMetadata> {
 
-  @IntDef({VerificationMode.MIC, VerificationMode.SIGNATURE})
-  @Retention(SOURCE)
-  public @interface VerificationMode {
-    public static final int MIC = 0;
-    public static final int SIGNATURE = 1;
-  }
-
   private final LegibleV1Sections legibleSections;
   private final int legibleSectionsIndex;
   private final int numDataElements;
-  private final @IdentityKind int identityTag;
+  @IdentityKind private final int identityTag;
   private final CredentialBook<M> credentialBook;
 
   /* package */ DeserializedV1Section(
@@ -66,7 +60,6 @@
   /**
    * Gets the data element at the given {@code index} in this advertisement.
    *
-   * @throws IllegalStateException if the advertisement is not legible ({@link #isLegible()}).
    * @throws IndexOutOfBoundsException if the index is invalid
    */
   public V1DataElement getDataElement(int index) {
@@ -148,7 +141,7 @@
 
     @Override
     public boolean hasNext() {
-      return position < (numDataElements - 1);
+      return position < numDataElements;
     }
 
     @Override
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/LegibleV1Sections.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/LegibleV1Sections.java
index 4a089c6..a5ac931 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/LegibleV1Sections.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/LegibleV1Sections.java
@@ -16,11 +16,8 @@
 
 package com.google.android.nearby.presence.rust;
 
-import static com.google.android.nearby.presence.rust.DeserializedV1Section.VerificationMode;
-
 import androidx.annotation.Nullable;
 import com.google.android.nearby.presence.rust.credential.CredentialBook;
-import java.lang.ref.Cleaner;
 import java.util.Arrays;
 
 /** Internal handle for a V1 deserialized advertisement. */
@@ -71,7 +68,7 @@
   }
 
   /** Create a LegibleV1Sections handle from the raw handle id. */
-  /* package-visible */ LegibleV1Sections(long handleId, Cleaner cleaner) {
+  private LegibleV1Sections(long handleId, CooperativeCleaner cleaner) {
     super(handleId, cleaner, LegibleV1Sections::deallocate);
   }
 
@@ -84,11 +81,11 @@
    */
   public <M extends CredentialBook.MatchedMetadata> DeserializedV1Section<M> getSection(
       int index, CredentialBook<M> credentialBook) {
-    DeserializedV1Section section = nativeGetSection(index, credentialBook);
+    DeserializedV1Section<M> section = nativeGetSection(index, credentialBook);
     if (section == null) {
       throw new IndexOutOfBoundsException();
     }
-    return (DeserializedV1Section<M>) section;
+    return section;
   }
 
   /**
@@ -118,7 +115,8 @@
   }
 
   @Nullable
-  private native DeserializedV1Section nativeGetSection(int index, CredentialBook credentialBook);
+  private native <M extends CredentialBook.MatchedMetadata>
+      DeserializedV1Section<M> nativeGetSection(int index, CredentialBook<M> credentialBook);
 
   @Nullable
   private native V1DataElement nativeGetSectionDataElement(int sectionIndex, int deIndex);
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/NpAdv.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/NpAdv.java
index a349ebc..b7639ab 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/NpAdv.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/NpAdv.java
@@ -18,14 +18,10 @@
 
 import androidx.annotation.Nullable;
 import com.google.android.nearby.presence.rust.credential.CredentialBook;
-import java.lang.ref.Cleaner;
 
 /**
  * The main entrypoint to the library.
  *
- * <p>On Android call {@link #setCleaner} with a {@code SystemCleaner} instance before any other
- * method to avoid creating a new cleaner thread.
- *
  * <h3>Supported Features:</h3>
  *
  * <ul>
@@ -43,7 +39,12 @@
     System.loadLibrary(LIBRARY_NAME);
   }
 
-  private static @Nullable Cleaner CLEANER = null;
+  // This is effectively an injected variable, but without depending on a DI implementation.
+  @SuppressWarnings("NonFinalStaticField")
+  @Nullable
+  private static CooperativeCleaner cleaner = null;
+
+  private NpAdv() {}
 
   /**
    * Deserialize a Nearby Presence advertisement from its service data bytes.
@@ -55,40 +56,26 @@
   public static <M extends CredentialBook.MatchedMetadata>
       DeserializeResult<M> deserializeAdvertisement(
           byte[] serviceData, CredentialBook<M> credentialBook) {
-    DeserializeResult result = nativeDeserializeAdvertisement(serviceData, credentialBook);
+    DeserializeResult<M> result = nativeDeserializeAdvertisement(serviceData, credentialBook);
     if (result == null) {
-      result = new DeserializeResult(DeserializeResult.Kind.UNKNOWN_ERROR);
+      result = new DeserializeResult<M>(DeserializeResult.Kind.UNKNOWN_ERROR);
     }
     return result;
   }
 
   /**
-   * Get the currently configured cleaner. If a cleaner is not configured, a new one will be created
-   * via the {@link Cleaner#create()} factory function.
+   * Get the currently configured cleaner. If a cleaner is not configured, a new one will be
+   * created.
    */
-  public static synchronized Cleaner getCleaner() {
-    if (CLEANER == null) {
-      CLEANER = Cleaner.create();
+  public static synchronized CooperativeCleaner getCleaner() {
+    if (cleaner == null) {
+      cleaner = new CooperativeCleaner();
     }
-    return CLEANER;
-  }
-
-  /**
-   * Configure a {@link Cleaner} to be used by this library. This cleaner will be used to ensure
-   * that {@link OwnedHandle} instances are properly freed. Since each {@code Cleaner} instance
-   * requires its own thread; this can be used to share a {@code Cleaner} instance to reduce the
-   * number of threads used.
-   *
-   * <p>On Android the {@code SystemCleaner} should be provided.
-   */
-  @Nullable
-  public static synchronized Cleaner setCleaner(Cleaner cleaner) {
-    Cleaner old = CLEANER;
-    CLEANER = cleaner;
-    return old;
+    return cleaner;
   }
 
   @Nullable
-  private static native DeserializeResult nativeDeserializeAdvertisement(
-      byte[] serviceData, CredentialBook credentialBook);
+  private static native <M extends CredentialBook.MatchedMetadata>
+      DeserializeResult<M> nativeDeserializeAdvertisement(
+          byte[] serviceData, CredentialBook<M> credentialBook);
 }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/OwnedHandle.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/OwnedHandle.java
index c505f81..a1c1a79 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/OwnedHandle.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/OwnedHandle.java
@@ -16,8 +16,8 @@
 
 package com.google.android.nearby.presence.rust;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.Nullable;
-import java.lang.ref.Cleaner;
 
 /**
  * A handle to natively-allocated object with lifetime control. This is a {@code Handle} that also
@@ -25,7 +25,8 @@
  *
  * <p>Users should call {@link OwnedHandle#close()} when finished with this handle to free the
  * native resources. This can be automatically done when using try-with-resources. If neither are
- * use the handle will still be closed when it is garbage collected.
+ * use the handle will still be freed sometime after this object has been garbage collected by a
+ * subsequent call to {@link OwnedHandle#close()} on another {@code OwnedHandle} instance.
  */
 public abstract class OwnedHandle extends Handle implements AutoCloseable {
 
@@ -34,6 +35,11 @@
    *
    * <p>This MUST not hold a reference to the {@link OwnedHandle} instance. Do not implement this on
    * your subclass; however, it may be implemented by a method reference to a static method.
+   *
+   * <p>This destructor will be run unless {@link OwnedHandle#leak()} is called. It will be run on
+   * the thread that calls {@link OwnedHandle#close()} if the handle is closed. If neither leak nor
+   * close are called and this handle is garbage collected, then it will be run on an unspecified
+   * thread. See {@link CooperativeCleaner}.
    */
   public interface Destructor {
     void deallocate(long handleId);
@@ -47,6 +53,7 @@
   }
 
   private final CleanupAction cleanupAction;
+  private final CooperativeCleaner.Registration cleanerRegistration;
 
   /**
    * Create a new instance and register it with the given cleaner.
@@ -55,22 +62,23 @@
    * @param cleaner The cleaner thread to register with for GC cleanup
    * @param destructor The destructor to run when this handle is closed
    */
-  protected OwnedHandle(long handleId, Cleaner cleaner, Destructor destructor) {
+  protected OwnedHandle(long handleId, CooperativeCleaner cleaner, Destructor destructor) {
     super(handleId);
     this.cleanupAction = new CleanupAction(handleId, destructor);
 
-    cleaner.register(this, this.cleanupAction);
+    cleanerRegistration = cleaner.register(this, this.cleanupAction);
   }
 
   /** Leak this handle. The associated native object will not be deallocated. */
   protected final void leak() {
-    this.cleanupAction.leak();
+    cleanupAction.leak();
+    close();
   }
 
   /** Implement AutoCloseable for try-with-resources support */
   @Override
   public final void close() {
-    this.cleanupAction.cleanupFromCloseable();
+    cleanerRegistration.close();
   }
 
   /**
@@ -79,8 +87,10 @@
    */
   private static final class CleanupAction implements Runnable {
     private final long handleId;
-    private @Nullable Destructor destructor;
-    private boolean freed = false;
+
+    @GuardedBy("this")
+    @Nullable
+    private Destructor destructor;
 
     public CleanupAction(long handleId, Destructor destructor) {
       this.handleId = handleId;
@@ -89,7 +99,9 @@
 
     /** Skip performing cleanup and leak the object instead */
     private void leak() {
-      this.destructor = null;
+      synchronized (this) {
+        this.destructor = null;
+      }
     }
 
     /**
@@ -101,29 +113,26 @@
      * @return {@code true} if the destructor was called.
      */
     private boolean deallocate() {
-      if (this.destructor != null) {
-        this.destructor.deallocate(this.handleId);
-        this.destructor = null;
+      // Take the destructor if present
+      Destructor destructor = null;
+      synchronized (this) {
+        if (this.destructor != null) {
+          destructor = this.destructor;
+          this.destructor = null;
+        }
+      }
+
+      // Run destructor if it was present
+      if (destructor != null) {
+        destructor.deallocate(handleId);
         return true;
       }
       return false;
     }
 
-    /**
-     * Perform the cleanup action. This is separate from {@link #run()} so that we can track if the
-     * handle was manually closed or if it was cleaned up via the {@link Cleaner}.
-     */
-    public void cleanupFromCloseable() {
-      if (!deallocate()) {
-        // FUTURE: log that OwnedHandle#close() was called multiple times.
-      }
-    }
-
     @Override
     public void run() {
-      if (deallocate()) {
-        // FUTURE: log that OwnedHandle#close() was not called.
-      }
+      boolean unused = deallocate();
     }
   }
 }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/SerializationException.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/SerializationException.java
index 4fa4f4f..741b030 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/SerializationException.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/SerializationException.java
@@ -29,6 +29,19 @@
     }
   }
 
+  public static final class UnclosedActiveSectionException extends RuntimeException {
+    public UnclosedActiveSectionException() {
+      super(
+          "There is currently an active section builder which must be finished before continuing");
+    }
+  }
+
+  public static final class InvalidSectionKindException extends RuntimeException {
+    public InvalidSectionKindException() {
+      super("A section of this kind is not allowed in this advertisement");
+    }
+  }
+
   public static final class InsufficientSpaceException extends SerializationException {
     public InsufficientSpaceException() {
       super("There isn't enough space remaining in the advertisement");
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java
index e597303..0061fff 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java
@@ -18,12 +18,15 @@
 
 import com.google.android.nearby.presence.rust.SerializationException.InsufficientSpaceException;
 import com.google.android.nearby.presence.rust.SerializationException.InvalidDataElementException;
+import com.google.android.nearby.presence.rust.V0DataElement.TxPower;
+import com.google.android.nearby.presence.rust.V0DataElement.V0Actions;
 import com.google.android.nearby.presence.rust.credential.V0BroadcastCredential;
-import java.lang.ref.Cleaner;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 
 /**
- * A builder for V0 advertisements. Create a new instance with {@link #newPublic()} for a public
- * advertisement or {@link #newEncrypted()} for an encrypted advertisement.
+ * A builder for V0 advertisements. Create a new instance with {@link
+ * V0AdvertisementBuilder#newPublic()} for a public advertisement or {@link
+ * V0AdvertisementBuilder#newEncrypted()} for an encrypted advertisement.
  */
 public final class V0AdvertisementBuilder implements AutoCloseable {
 
@@ -32,19 +35,19 @@
     return newPublic(NpAdv.getCleaner());
   }
 
+  /** Create a builder for a public advertisement with a specific cleaner. */
+  private static V0AdvertisementBuilder newPublic(CooperativeCleaner cleaner) {
+    return new V0AdvertisementBuilder(new V0BuilderHandle(cleaner));
+  }
+
   /** Create a builder for an encrypted advertisement. */
   public static V0AdvertisementBuilder newEncrypted(V0BroadcastCredential credential, byte[] salt) {
     return newEncrypted(NpAdv.getCleaner(), credential, salt);
   }
 
-  /** Create a builder for a public advertisement with a specific Cleaner. */
-  public static V0AdvertisementBuilder newPublic(Cleaner cleaner) {
-    return new V0AdvertisementBuilder(new V0BuilderHandle(cleaner));
-  }
-
-  /** Create a builder for an encrypted advertisement with a specific Cleaner. */
-  public static V0AdvertisementBuilder newEncrypted(
-      Cleaner cleaner, V0BroadcastCredential credential, byte[] salt) {
+  /** Create a builder for an encrypted advertisement with a specific cleaner. */
+  private static V0AdvertisementBuilder newEncrypted(
+      CooperativeCleaner cleaner, V0BroadcastCredential credential, byte[] salt) {
     return new V0AdvertisementBuilder(new V0BuilderHandle(cleaner, credential, salt));
   }
 
@@ -62,8 +65,11 @@
    *     of range)
    * @throws InsufficientSpaceException when the data element will not fit in the remaining space.
    */
-  public void addDataElement(V0DataElement dataElement) throws InsufficientSpaceException {
+  @CanIgnoreReturnValue
+  public V0AdvertisementBuilder addDataElement(V0DataElement dataElement)
+      throws InsufficientSpaceException {
     builder.addDataElement(dataElement);
+    return this;
   }
 
   /**
@@ -89,26 +95,59 @@
       System.loadLibrary(NpAdv.LIBRARY_NAME);
     }
 
-    public V0BuilderHandle(Cleaner cleaner) {
+    /** Create a public builder. */
+    public V0BuilderHandle(CooperativeCleaner cleaner) {
       super(allocatePublic(), cleaner, V0BuilderHandle::deallocate);
     }
 
-    public V0BuilderHandle(Cleaner cleaner, V0BroadcastCredential credential, byte[] salt) {
+    /** Create an encrypted builder. */
+    public V0BuilderHandle(
+        CooperativeCleaner cleaner, V0BroadcastCredential credential, byte[] salt) {
       super(allocatePrivate(credential, salt), cleaner, V0BuilderHandle::deallocate);
     }
 
-    public void addDataElement(V0DataElement dataElement) {
-      // Call the appropriate native add call based on the data element type.
-      dataElement.visit(
-          new V0DataElement.Visitor() {
-            public void visitTxPower(V0DataElement.TxPower txPower) {
-              nativeAddTxPowerDataElement(txPower);
-            }
+    private class AddDataElementVisitor implements V0DataElement.Visitor {
+      @Override
+      public void visitTxPower(TxPower txPower) {
+        try {
+          nativeAddTxPowerDataElement(txPower);
+        } catch (InsufficientSpaceException ise) {
+          throw new SmuggledInsufficientSpaceException(ise);
+        }
+      }
 
-            public void visitV0Actions(V0DataElement.V0Actions v0Actions) {
-              nativeAddV0ActionsDataElement(v0Actions);
-            }
-          });
+      @Override
+      public void visitV0Actions(V0Actions actions) {
+        try {
+          nativeAddV0ActionsDataElement(actions);
+        } catch (InsufficientSpaceException ise) {
+          throw new SmuggledInsufficientSpaceException(ise);
+        }
+      }
+    }
+
+    /**
+     * Helper to smuggle {@link InsufficientSpaceException} (a checked exception) through APIs that
+     * don't support checked exceptions.
+     */
+    private static class SmuggledInsufficientSpaceException extends RuntimeException {
+      private final InsufficientSpaceException ise;
+
+      public SmuggledInsufficientSpaceException(InsufficientSpaceException ise) {
+        this.ise = ise;
+      }
+
+      public void throwChecked() throws InsufficientSpaceException {
+        throw ise;
+      }
+    }
+
+    public void addDataElement(V0DataElement dataElement) throws InsufficientSpaceException {
+      try {
+        dataElement.visit(new AddDataElementVisitor());
+      } catch (SmuggledInsufficientSpaceException ex) {
+        ex.throwChecked();
+      }
     }
 
     public byte[] build()
@@ -123,9 +162,11 @@
 
     private static native long allocatePrivate(V0BroadcastCredential credential, byte[] salt);
 
-    private native void nativeAddTxPowerDataElement(V0DataElement.TxPower txPower);
+    private native void nativeAddTxPowerDataElement(TxPower txPower)
+        throws InsufficientSpaceException;
 
-    private native void nativeAddV0ActionsDataElement(V0DataElement.V0Actions v0Actions);
+    private native void nativeAddV0ActionsDataElement(V0Actions v0Actions)
+        throws InsufficientSpaceException;
 
     private native byte[] nativeBuild()
         throws SerializationException.LdtEncryptionException,
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java
index c2c6325..0ee2157 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java
@@ -52,6 +52,7 @@
       return txPower;
     }
 
+    @Override
     public void visit(Visitor v) {
       v.visitTxPower(this);
     }
@@ -68,7 +69,7 @@
   })
   @Retention(SOURCE)
   public @interface V0ActionType {
-    // NOTE: Copied from `np_ffi_core::v0::BooleanActionType`.
+    // NOTE: Copied from `np_ffi_core::v0::ActionType`.
     public static final int CROSS_DEV_SDK = 1;
     public static final int CALL_TRANSFER = 4;
     public static final int ACTIVE_UNLOCK = 8;
@@ -83,7 +84,7 @@
       System.loadLibrary(NpAdv.LIBRARY_NAME);
     }
 
-    private final @IdentityKind int identityKind;
+    @IdentityKind private final int identityKind;
     private final int actionBits;
 
     /**
@@ -99,8 +100,8 @@
       this(identityKind, nativeMergeActions(identityKind, actions));
     }
 
-    /** Used by native code. */
-    V0Actions(@IdentityKind int identityKind, int actionBits) {
+    /** Used by native code. This must be private to avoid being confused with the above method. */
+    private V0Actions(@IdentityKind int identityKind, int actionBits) {
       this.identityKind = identityKind;
       this.actionBits = actionBits;
     }
@@ -119,6 +120,7 @@
       return nativeHasAction(identityKind, actionBits, action);
     }
 
+    @Override
     public void visit(Visitor v) {
       v.visitV0Actions(this);
     }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0Payload.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0Payload.java
index e6abea8..588a2ca 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0Payload.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0Payload.java
@@ -17,7 +17,6 @@
 package com.google.android.nearby.presence.rust;
 
 import androidx.annotation.Nullable;
-import java.lang.ref.Cleaner;
 import java.util.Arrays;
 
 /**
@@ -68,7 +67,7 @@
   }
 
   /** Create a V0Payload handle from the raw handle id. */
-  /* package-visible */ V0Payload(long handleId, Cleaner cleaner) {
+  private V0Payload(long handleId, CooperativeCleaner cleaner) {
     super(handleId, cleaner, V0Payload::deallocate);
   }
 
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1AdvertisementBuilder.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1AdvertisementBuilder.java
new file mode 100644
index 0000000..058494b
--- /dev/null
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1AdvertisementBuilder.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import androidx.annotation.VisibleForTesting;
+import com.google.android.nearby.presence.rust.SerializationException.InsufficientSpaceException;
+import com.google.android.nearby.presence.rust.credential.V1BroadcastCredential;
+
+/**
+ * A builder for V1 advertisements. Create a new instance with {@link
+ * V1AdvertisementBuilder#newPublic()} for a public advertisement or {@link
+ * V1AdvertisementBuilder#newEncrypted()} for an encrypted advertisement.
+ *
+ * <p>Public advertisements may only contain public sections. Encrypted advertisements may only
+ * contain encrypted sections. Use {@link V1AdvertisementBuilder#addPublicSection()} and {@link
+ * V1AdvertisementBuilder#addEncryptedSection()} to add sections to the advertisement. Only one
+ * section may be built at a time.
+ */
+public final class V1AdvertisementBuilder implements AutoCloseable {
+
+  /** Create a builder for a public advertisement. */
+  public static V1AdvertisementBuilder newPublic() {
+    return newPublic(NpAdv.getCleaner());
+  }
+
+  /** Create a builder for a public advertisement with a specific cleaner. */
+  private static V1AdvertisementBuilder newPublic(CooperativeCleaner cleaner) {
+    return new V1AdvertisementBuilder(new V1BuilderHandle(cleaner, false));
+  }
+
+  /** Create a builder for an encrypted advertisement. */
+  public static V1AdvertisementBuilder newEncrypted() {
+    return newEncrypted(NpAdv.getCleaner());
+  }
+
+  /** Create a builder for an encrypted advertisement with a specific cleaner. */
+  private static V1AdvertisementBuilder newEncrypted(CooperativeCleaner cleaner) {
+    return new V1AdvertisementBuilder(new V1BuilderHandle(cleaner, true));
+  }
+
+  @VisibleForTesting final V1BuilderHandle builder;
+
+  private V1AdvertisementBuilder(V1BuilderHandle builder) {
+    this.builder = builder;
+  }
+
+  /**
+   * Adds a public section to this advertisement. This is only valid on public advertisements.
+   *
+   * @throws UnclosedActiveSectionException when there is already an active section builder.
+   * @throws InvalidHandleException if this builder has already been closed.
+   * @throws InvalidSectionKindException if this advertisement builder is not a public advertisement
+   *     builder.
+   * @throws InsufficientSpaceException if this advertisement does not have enough space for a new
+   *     section
+   */
+  public V1SectionBuilder addPublicSection() throws InsufficientSpaceException {
+    return builder.addPublicSection();
+  }
+
+  /**
+   * Adds an encrypted section to this advertisement. This is only valid on encrypted
+   * advertisements.
+   *
+   * @throws UnclosedActiveSectionException when there is already an active section builder.
+   * @throws InvalidHandleException if this builder has already been closed.
+   * @throws InvalidSectionKindException if this advertisement builder is not an encrypted
+   *     advertisement builder.
+   * @throws InsufficientSpaceException if this advertisement does not have enough space for a new
+   *     section
+   */
+  public V1SectionBuilder addEncryptedSection(
+      V1BroadcastCredential credential, @VerificationMode int verificationMode)
+      throws InsufficientSpaceException {
+    return builder.addEncryptedSection(credential, verificationMode);
+  }
+
+  /**
+   * Build this advertisement into a byte buffer. This will consume the builder when called.
+   *
+   * @throws UnclosedActiveSectionException when there is an active section builder.
+   */
+  public byte[] build() {
+    return builder.build();
+  }
+
+  /** Deallocates this section builder. */
+  @Override
+  public void close() {
+    builder.close();
+  }
+
+  /** Internal builder handle object. */
+  static final class V1BuilderHandle extends OwnedHandle {
+    static {
+      System.loadLibrary(NpAdv.LIBRARY_NAME);
+    }
+
+    public V1BuilderHandle(CooperativeCleaner cleaner, boolean encrypted) {
+      super(allocate(encrypted), cleaner, V1BuilderHandle::deallocate);
+    }
+
+    public V1SectionBuilder addPublicSection() throws InsufficientSpaceException {
+      return nativeAddPublicSection();
+    }
+
+    public V1SectionBuilder addEncryptedSection(
+        V1BroadcastCredential credential, @VerificationMode int verificationMode)
+        throws InsufficientSpaceException {
+      return nativeAddEncryptedSection(credential, verificationMode);
+    }
+
+    private class AddDataElementVisitor implements V1DataElement.Visitor {
+      private final int section;
+
+      private AddDataElementVisitor(int section) {
+        this.section = section;
+      }
+
+      @Override
+      public void visitGeneric(V1DataElement.Generic generic) {
+        try {
+          nativeAddGenericDataElement(section, generic);
+        } catch (InsufficientSpaceException ise) {
+          throw new SmuggledInsufficientSpaceException(ise);
+        }
+      }
+    }
+
+    /**
+     * Helper to smuggle {@link InsufficientSpaceException} (a checked exception) through APIs that
+     * don't support checked exceptions.
+     */
+    private static class SmuggledInsufficientSpaceException extends RuntimeException {
+      private final InsufficientSpaceException ise;
+
+      public SmuggledInsufficientSpaceException(InsufficientSpaceException ise) {
+        this.ise = ise;
+      }
+
+      public void throwChecked() throws InsufficientSpaceException {
+        throw ise;
+      }
+    }
+
+    public void addDataElement(int section, V1DataElement dataElement)
+        throws InsufficientSpaceException {
+      try {
+        dataElement.visit(new AddDataElementVisitor(section));
+      } catch (SmuggledInsufficientSpaceException ex) {
+        ex.throwChecked();
+      }
+    }
+
+    public void finishSection(int section) {
+      nativeFinishSection(section);
+    }
+
+    public byte[] build() {
+      // `nativeBuild` takes ownership so we leak the Java side here.
+      leak();
+      return nativeBuild();
+    }
+
+    private static native long allocate(boolean encrypted);
+
+    private native V1SectionBuilder nativeAddPublicSection() throws InsufficientSpaceException;
+
+    private native V1SectionBuilder nativeAddEncryptedSection(
+        V1BroadcastCredential credential, @VerificationMode int verificationMode)
+        throws InsufficientSpaceException;
+
+    private native void nativeAddGenericDataElement(int section, V1DataElement.Generic generic)
+        throws InsufficientSpaceException;
+
+    private native void nativeFinishSection(int section);
+
+    private native byte[] nativeBuild();
+
+    private static native void deallocate(long handleId);
+  }
+}
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1DataElement.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1DataElement.java
index b52c7bb..e7d921e 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1DataElement.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1DataElement.java
@@ -56,6 +56,7 @@
       return Arrays.copyOf(data, data.length);
     }
 
+    @Override
     public void visit(Visitor v) {
       v.visitGeneric(this);
     }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1SectionBuilder.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1SectionBuilder.java
new file mode 100644
index 0000000..d2d62cc
--- /dev/null
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V1SectionBuilder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import com.google.android.nearby.presence.rust.SerializationException.InsufficientSpaceException;
+import com.google.android.nearby.presence.rust.V1AdvertisementBuilder.V1BuilderHandle;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+/**
+ * A builder for V1 advertisement sections. Create a new instance with {@link
+ * V1AdvertisementBuilder#addPublicSection()} for a public section or {@link
+ * V1AdvertisementBuilder#addEncryptedSection()} for an encrypted section.
+ */
+public final class V1SectionBuilder implements AutoCloseable {
+
+  private final V1BuilderHandle builder;
+  private final int section;
+
+  /**
+   * Whether {@link #finishSection()} has already been called by the client. This is used to avoid
+   * double-finishing when used in a try-with-resources block.
+   */
+  private boolean finishCalled = false;
+
+  // Native used
+  @SuppressWarnings("UnusedMethod")
+  private V1SectionBuilder(V1BuilderHandle builder, int section) {
+    this.builder = builder;
+    this.section = section;
+  }
+
+  /**
+   * Add a data element to the advertisement. If it cannot be added an exception will be thrown. A
+   * thrown exception will not invalidate this builder.
+   *
+   * @throws InvalidDataElementException when the given data element is not valid (e.g. value out of
+   *     range, too large, etc.)
+   * @throws InsufficientSpaceException when the data element will not fit in the remaining space.
+   */
+  @CanIgnoreReturnValue
+  public V1SectionBuilder addDataElement(V1DataElement dataElement)
+      throws InsufficientSpaceException {
+    builder.addDataElement(section, dataElement);
+    return this;
+  }
+
+  /**
+   * Finishes writing the current seciton. This will consume the section builder when called.
+   *
+   * @throws InvalidHandleException if called multiple times or if the parent advertisement has been
+   *     closed.
+   */
+  public void finishSection() {
+    builder.finishSection(section);
+    finishCalled = true;
+  }
+
+  /**
+   * Close this section builder. This will {@link #finishSection()} if the builder hasn't been
+   * finished yet.
+   */
+  @Override
+  public void close() {
+    // Finish the section if the user has not already done so explicitly
+    if (!finishCalled) {
+      finishSection();
+    }
+  }
+}
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/VerificationMode.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/VerificationMode.java
new file mode 100644
index 0000000..ba7953b
--- /dev/null
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/VerificationMode.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import java.lang.annotation.Retention;
+
+@IntDef({VerificationMode.MIC, VerificationMode.SIGNATURE})
+@Retention(SOURCE)
+public @interface VerificationMode {
+  public static final int MIC = 0;
+  public static final int SIGNATURE = 1;
+}
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialBook.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialBook.java
index 953c6e9..e264f13 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialBook.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialBook.java
@@ -17,11 +17,17 @@
 package com.google.android.nearby.presence.rust.credential;
 
 import androidx.annotation.Nullable;
+import com.google.android.nearby.presence.rust.CooperativeCleaner;
 import com.google.android.nearby.presence.rust.NpAdv;
 import com.google.android.nearby.presence.rust.OwnedHandle;
-import java.lang.ref.Cleaner;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.ArrayList;
 
+/**
+ * A set of credentials used for deserialization. Create a credential book using the builder from
+ * {@link #builder()}. An empty credential book ({@link #empty()}) can be used to deserialize public
+ * advertisements and sections.
+ */
 public final class CredentialBook<M extends CredentialBook.MatchedMetadata> extends OwnedHandle {
 
   static {
@@ -68,7 +74,7 @@
    * {@code getMatchedMetadata()} method on their {@code DeserializedAdvertisement} instance.
    */
   public static final class Builder<M extends MatchedMetadata> {
-    private Cleaner cleaner;
+    private CooperativeCleaner cleaner;
     private CredentialSlab slab;
 
     /**
@@ -79,19 +85,22 @@
 
     /**
      * Create a builder instance. The {@link CredentialBook#builder()} factory method can be used to
-     * create a {@code Builder} with the default {@link Cleaner}. This can fail if there isn't room
-     * to create a new {@code CredentialSlab} handle.
+     * create a {@code Builder} with the default {@link CooperativeCleaner}.
+     *
+     * <p>This can fail if there isn't room to create a new {@code CredentialSlab} handle with
+     * {@link OwnedHandle.NoSpaceLeftException}.
      *
      * @param cleaner The cleaner instance to use for the {@link CredentialSlab} and {@code
      *     CredentialBook}.
      */
-    public Builder(Cleaner cleaner) {
+    private Builder(CooperativeCleaner cleaner) {
       this.cleaner = cleaner;
       this.slab = new CredentialSlab(cleaner);
       this.matchDataList = new ArrayList<>();
     }
 
     /** Add a {@link V0DiscoveryCredential} to the book. */
+    @CanIgnoreReturnValue
     public Builder<M> addDiscoveryCredential(V0DiscoveryCredential credential, M matchData) {
       int credIdx = matchDataList.size();
       matchDataList.add(matchData);
@@ -104,6 +113,7 @@
      * CredentialBook.InvalidPublicKeyException} if the key inside {@code credential} is improperly
      * formatted.
      */
+    @CanIgnoreReturnValue
     public Builder<M> addDiscoveryCredential(V1DiscoveryCredential credential, M matchData) {
       int credIdx = matchDataList.size();
       matchDataList.add(matchData);
@@ -116,7 +126,7 @@
      * CredentialBook} handle.
      */
     public CredentialBook<M> build() {
-      return new CredentialBook(slab, matchDataList, cleaner);
+      return new CredentialBook<>(slab, matchDataList, cleaner);
     }
   }
 
@@ -140,7 +150,8 @@
    * from {@code Builder}. {@code matchData} is formatted so that each credential is given an id of
    * the index of its metadata in {@code matchData}.
    */
-  private CredentialBook(CredentialSlab slab, ArrayList<M> matchData, Cleaner cleaner) {
+  @SuppressWarnings("NonApiType")
+  private CredentialBook(CredentialSlab slab, ArrayList<M> matchData, CooperativeCleaner cleaner) {
     super(allocate(slab.move()), cleaner, CredentialBook::deallocate);
     this.matchData = matchData;
   }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialSlab.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialSlab.java
index 35fc610..29f2a08 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialSlab.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialSlab.java
@@ -16,9 +16,9 @@
 
 package com.google.android.nearby.presence.rust.credential;
 
+import com.google.android.nearby.presence.rust.CooperativeCleaner;
 import com.google.android.nearby.presence.rust.NpAdv;
 import com.google.android.nearby.presence.rust.OwnedHandle;
-import java.lang.ref.Cleaner;
 
 /**
  * A {@code CredentialSlab} handle that is used to build the native-side structures for a {@link
@@ -30,14 +30,16 @@
   }
 
   /** Create a new {@code CredentialSlab} with the given {@code cleaner}. */
-  public CredentialSlab(Cleaner cleaner) {
+  public CredentialSlab(CooperativeCleaner cleaner) {
     super(allocate(), cleaner, CredentialSlab::deallocate);
   }
 
   /** Add a V0 discovery credential to this slab. */
   public void addDiscoveryCredential(
       V0DiscoveryCredential credential, int credId, byte[] encryptedMetadataBytes) {
-    nativeAddV0DiscoveryCredential(handleId, credential, credId, encryptedMetadataBytes);
+    if (!nativeAddV0DiscoveryCredential(handleId, credential, credId, encryptedMetadataBytes)) {
+      throw new IllegalStateException("Failed to add credential to slab");
+    }
   }
 
   /**
@@ -47,7 +49,9 @@
    */
   public void addDiscoveryCredential(
       V1DiscoveryCredential credential, int credId, byte[] encryptedMetadataBytes) {
-    nativeAddV1DiscoveryCredential(handleId, credential, credId, encryptedMetadataBytes);
+    if (!nativeAddV1DiscoveryCredential(handleId, credential, credId, encryptedMetadataBytes)) {
+      throw new IllegalStateException("Failed to add credential to slab");
+    }
   }
 
   /**
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/Utils.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/Utils.java
index 4f070e4..a7b8f3d 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/Utils.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/Utils.java
@@ -21,6 +21,8 @@
 /** Util functions used by multiple files. */
 final class Utils {
 
+  private Utils() {}
+
   /**
    * Create a copy of a {@code n}-byte array of data. Will throw {@code IllegalArgumentException} if
    * the array is not exactly {@code n} bytes.
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0BroadcastCredential.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0BroadcastCredential.java
index 9e8474b..d47a15a 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0BroadcastCredential.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0BroadcastCredential.java
@@ -19,6 +19,7 @@
 import static com.google.android.nearby.presence.rust.credential.Utils.copyBytes;
 
 /** A V0 broadcast credential in a format that is ready to be passed to native code. */
+@SuppressWarnings("UnusedVariable")
 public final class V0BroadcastCredential {
   private final byte[] keySeed;
   private final byte[] identityToken;
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0DiscoveryCredential.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0DiscoveryCredential.java
index 522b7d8..f2d1a41 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0DiscoveryCredential.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0DiscoveryCredential.java
@@ -19,6 +19,7 @@
 import static com.google.android.nearby.presence.rust.credential.Utils.copyBytes;
 
 /** A V0 discovery credential in a format that is ready to be passed to native code. */
+@SuppressWarnings("UnusedVariable")
 public final class V0DiscoveryCredential {
   private final byte[] keySeed;
   private final byte[] identityTokenHmac;
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1BroadcastCredential.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1BroadcastCredential.java
index 5c2547a..d9108a3 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1BroadcastCredential.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1BroadcastCredential.java
@@ -19,6 +19,7 @@
 import static com.google.android.nearby.presence.rust.credential.Utils.copyBytes;
 
 /** A V1 broadcast credential in a format that is ready to be passed to native code. */
+@SuppressWarnings("UnusedVariable")
 public final class V1BroadcastCredential {
   private final byte[] keySeed;
   private final byte[] identityToken;
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1DiscoveryCredential.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1DiscoveryCredential.java
index 2fbf608..53cfcfe 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1DiscoveryCredential.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1DiscoveryCredential.java
@@ -19,6 +19,7 @@
 import static com.google.android.nearby.presence.rust.credential.Utils.copyBytes;
 
 /** A V1 discovery credential in a format that is ready to be passed to native code. */
+@SuppressWarnings("UnusedVariable")
 public final class V1DiscoveryCredential {
   private final byte[] keySeed;
   private final byte[] expectedMicShortSaltIdentityTokenHmac;
diff --git a/nearby/presence/np_java_ffi/src/class.rs b/nearby/presence/np_java_ffi/src/class.rs
index ea53df7..d8742cb 100644
--- a/nearby/presence/np_java_ffi/src/class.rs
+++ b/nearby/presence/np_java_ffi/src/class.rs
@@ -47,8 +47,10 @@
 mod v0_broadcast_credential;
 mod v0_discovery_credential;
 mod v0_payload;
+mod v1_advertisement_builder;
 mod v1_broadcast_credential;
 mod v1_discovery_credential;
+mod verification_mode;
 
 pub mod v0_data_element;
 pub mod v1_data_element;
@@ -58,16 +60,18 @@
 pub use deserialize_result::{DeserializeResult, DeserializeResultError};
 pub use deserialized_v0_advertisement::{DeserializedV0Advertisement, V0AdvertisementError};
 pub use deserialized_v1_advertisement::DeserializedV1Advertisement;
-pub use deserialized_v1_section::{DeserializedV1Section, VerificationMode};
+pub use deserialized_v1_section::DeserializedV1Section;
 pub use handle::InvalidHandleException;
 pub use identity_kind::IdentityKind;
 pub use legible_v1_sections::LegibleV1Sections;
 pub use owned_handle::NoSpaceLeftException;
 pub use serialization_exception::{
-    InsufficientSpaceException, InvalidDataElementException, LdtEncryptionException,
-    UnencryptedSizeException,
+    InsufficientSpaceException, InvalidDataElementException, InvalidSectionKindException,
+    LdtEncryptionException, UnclosedActiveSectionException, UnencryptedSizeException,
 };
 pub use v0_broadcast_credential::V0BroadcastCredential;
 pub use v0_discovery_credential::V0DiscoveryCredential;
+pub use v1_advertisement_builder::{V1BuilderHandle, V1SectionBuilder};
 pub use v1_broadcast_credential::V1BroadcastCredential;
 pub use v1_discovery_credential::V1DiscoveryCredential;
+pub use verification_mode::VerificationMode;
diff --git a/nearby/presence/np_java_ffi/src/class/credential_slab.rs b/nearby/presence/np_java_ffi/src/class/credential_slab.rs
index 7e8f37d..d22dea1 100644
--- a/nearby/presence/np_java_ffi/src/class/credential_slab.rs
+++ b/nearby/presence/np_java_ffi/src/class/credential_slab.rs
@@ -86,7 +86,7 @@
 
         let core_cred = credential.get_as_core(&mut env)?;
         let match_data = MatchedCredential::from_arc_bytes(
-            cred_id as u32,
+            cred_id as i64,
             env.convert_byte_array(&encrypted_metadata_bytes)?.into(),
         );
 
@@ -122,7 +122,7 @@
 
         let core_cred = credential.get_as_core(&mut env)?;
         let match_data = MatchedCredential::from_arc_bytes(
-            cred_id as u32,
+            cred_id as i64,
             env.convert_byte_array(&encrypted_metadata_bytes)?.into(),
         );
 
diff --git a/nearby/presence/np_java_ffi/src/class/deserialized_v1_section.rs b/nearby/presence/np_java_ffi/src/class/deserialized_v1_section.rs
index 3b6b5ac..96b514b 100644
--- a/nearby/presence/np_java_ffi/src/class/deserialized_v1_section.rs
+++ b/nearby/presence/np_java_ffi/src/class/deserialized_v1_section.rs
@@ -13,9 +13,8 @@
 // limitations under the License.
 
 use jni::{objects::JObject, sys::jint, JNIEnv};
-use np_ffi_core::{deserialize::v1::DeserializedV1IdentityKind, v1::V1VerificationMode};
-use pourover::desc::{ClassDesc, StaticFieldDesc};
-use std::sync::RwLock;
+use np_ffi_core::deserialize::v1::DeserializedV1IdentityKind;
+use pourover::desc::ClassDesc;
 
 use crate::class::{CredentialBook, IdentityKind, LegibleV1Sections};
 
@@ -51,70 +50,3 @@
         .map(Self)
     }
 }
-
-static VERIFICATION_MODE_CLASS: ClassDesc = ClassDesc::new(
-    "com/google/android/nearby/presence/rust/DeserializedV1Section$VerificationMode",
-);
-
-/// Rust representation of `@VerificationMode`. These are `jints` on the Java side, so this type can't
-/// be instantiated.
-pub enum VerificationMode {}
-
-impl VerificationMode {
-    /// Convert a Rust verification mode enum to the Java `jint` representation.
-    pub fn value_for<'local>(
-        env: &mut JNIEnv<'local>,
-        mode: V1VerificationMode,
-    ) -> jni::errors::Result<jint> {
-        match mode {
-            V1VerificationMode::Mic => Self::mic(env),
-            V1VerificationMode::Signature => Self::signature(env),
-        }
-    }
-
-    /// Fetch the `SIGNATURE` constant
-    pub fn signature<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
-        static SIGNATURE: StaticFieldDesc = VERIFICATION_MODE_CLASS.static_field("SIGNATURE", "I");
-        static VALUE: RwLock<Option<jint>> = RwLock::new(None);
-        Self::lookup_static_value(env, &SIGNATURE, &VALUE)
-    }
-
-    /// Fetch the `MIC` constant
-    pub fn mic<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
-        static MIC: StaticFieldDesc = VERIFICATION_MODE_CLASS.static_field("MIC", "I");
-        static VALUE: RwLock<Option<jint>> = RwLock::new(None);
-        Self::lookup_static_value(env, &MIC, &VALUE)
-    }
-
-    /// Look up the given field and cache it in the given cache. The lookup will only be performed
-    /// once if successful. This uses `RwLock` instead of `OnceCell` since the fallible `OnceCell`
-    /// APIs are nightly only.
-    fn lookup_static_value<'local>(
-        env: &mut JNIEnv<'local>,
-        field: &StaticFieldDesc,
-        cache: &RwLock<Option<jint>>,
-    ) -> jni::errors::Result<jint> {
-        use jni::signature::{JavaType, Primitive};
-
-        // Read from cache
-        if let Some(value) = *cache.read().unwrap_or_else(|poison| poison.into_inner()) {
-            return Ok(value);
-        }
-
-        // Get exclusive access to the cache for the lookup
-        let mut guard = cache.write().unwrap_or_else(|poison| poison.into_inner());
-
-        // In case of races, only lookup the value once
-        if let Some(value) = *guard {
-            return Ok(value);
-        }
-
-        let value = env
-            .get_static_field_unchecked(field.cls(), field, JavaType::Primitive(Primitive::Int))
-            .and_then(|ret| ret.i())?;
-
-        *guard = Some(value);
-
-        Ok(value)
-    }
-}
diff --git a/nearby/presence/np_java_ffi/src/class/legible_v1_sections.rs b/nearby/presence/np_java_ffi/src/class/legible_v1_sections.rs
index 6bc9225..0e089ab 100644
--- a/nearby/presence/np_java_ffi/src/class/legible_v1_sections.rs
+++ b/nearby/presence/np_java_ffi/src/class/legible_v1_sections.rs
@@ -102,10 +102,10 @@
     pub fn construct_from_parts(
         env: &mut JNIEnv<'local>,
         verification_mode: V1VerificationMode,
-        credential_id: u32,
+        credential_id: i64,
         identity_token: [u8; 16],
     ) -> jni::errors::Result<Self> {
-        let verification_mode = VerificationMode::value_for(env, verification_mode)?;
+        let verification_mode = VerificationMode::from(verification_mode).to_java(env)?;
         let credential_id = credential_id as jint;
         let identity_token = env.byte_array_from_slice(&identity_token)?;
 
diff --git a/nearby/presence/np_java_ffi/src/class/serialization_exception.rs b/nearby/presence/np_java_ffi/src/class/serialization_exception.rs
index 3e3235e..e021927 100644
--- a/nearby/presence/np_java_ffi/src/class/serialization_exception.rs
+++ b/nearby/presence/np_java_ffi/src/class/serialization_exception.rs
@@ -57,83 +57,52 @@
     }
 }
 
-static INSUFFICIENT_SPACE_EXCEPTION: ClassDesc = ClassDesc::new(
-    "com/google/android/nearby/presence/rust/SerializationException$InsufficientSpaceException",
-);
+/// Helper to generate wrapper types for exception classes with no-arg constructors
+macro_rules! exception_wrapper {
+    ($(#[$docs:meta])* $name:ident, $cls:literal) => {
+        $(#[$docs])*
+        #[repr(transparent)]
+        pub struct $name<Obj>(pub Obj);
 
-/// Rust representation of `class SerializationException.InsufficientSpaceException`.
-#[repr(transparent)]
-pub struct InsufficientSpaceException<Obj>(pub Obj);
+        impl<'local> $name<JObject<'local>> {
+            /// Create a new instance.
+            pub fn construct(env: &mut JNIEnv<'local>) -> jni::errors::Result<Self> {
+                static CLS: ClassDesc = ClassDesc::new($cls);
+                pourover::call_constructor!(env, &CLS, "()V",).map(Self)
+            }
 
-impl<'local> InsufficientSpaceException<JObject<'local>> {
-    /// Create a new instance.
-    pub fn construct(env: &mut JNIEnv<'local>) -> jni::errors::Result<Self> {
-        pourover::call_constructor!(env, &INSUFFICIENT_SPACE_EXCEPTION, "()V",).map(Self)
-    }
+            /// Create a new instance and throw it.
+            pub fn throw_new(env: &mut JNIEnv<'local>) -> jni::errors::Result<()> {
+                Self::construct(env)?.throw(env)
+            }
+        }
 
-    /// Create a new instance and throw it.
-    pub fn throw_new(env: &mut JNIEnv<'local>) -> jni::errors::Result<()> {
-        Self::construct(env)?.throw(env)
+        impl<'local, Obj: AsRef<JObject<'local>>> $name<Obj> {
+            /// Throw this exception.
+            pub fn throw<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<()> {
+                env.throw(<&JThrowable>::from(self.0.as_ref()))
+            }
+        }
+
     }
 }
 
-impl<'local, Obj: AsRef<JObject<'local>>> InsufficientSpaceException<Obj> {
-    /// Throw this exception.
-    pub fn throw<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<()> {
-        env.throw(<&JThrowable>::from(self.0.as_ref()))
-    }
-}
+exception_wrapper!(
+    /// Rust representation of `class SerializationException.UnclosedActiveSectionException`.
+    UnclosedActiveSectionException, "com/google/android/nearby/presence/rust/SerializationException$UnclosedActiveSectionException");
 
-static LDT_ENCRYPTION_EXCEPTION_CLASS: ClassDesc = ClassDesc::new(
-    "com/google/android/nearby/presence/rust/SerializationException$LdtEncryptionException",
-);
+exception_wrapper!(
+    /// Rust representation of `class SerializationException.InvalidSectionKindException`.
+    InvalidSectionKindException, "com/google/android/nearby/presence/rust/SerializationException$InvalidSectionKindException");
 
-/// Rust representation of `class SerializationException.LdtEncryptionException`.
-#[repr(transparent)]
-pub struct LdtEncryptionException<Obj>(pub Obj);
+exception_wrapper!(
+    /// Rust representation of `class SerializationException.InsufficientSpaceException`.
+    InsufficientSpaceException, "com/google/android/nearby/presence/rust/SerializationException$InsufficientSpaceException");
 
-impl<'local> LdtEncryptionException<JObject<'local>> {
-    /// Create a new instance.
-    pub fn construct(env: &mut JNIEnv<'local>) -> jni::errors::Result<Self> {
-        pourover::call_constructor!(env, &LDT_ENCRYPTION_EXCEPTION_CLASS, "()V").map(Self)
-    }
+exception_wrapper!(
+    /// Rust representation of `class SerializationException.LdtEncryptionException`.
+    LdtEncryptionException, "com/google/android/nearby/presence/rust/SerializationException$LdtEncryptionException");
 
-    /// Create a new instance and throw it.
-    pub fn throw_new(env: &mut JNIEnv<'local>) -> jni::errors::Result<()> {
-        Self::construct(env)?.throw(env)
-    }
-}
-
-impl<'local, Obj: AsRef<JObject<'local>>> LdtEncryptionException<Obj> {
-    /// Throw this exception.
-    pub fn throw<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<()> {
-        env.throw(<&JThrowable>::from(self.0.as_ref()))
-    }
-}
-
-static UNENCRYPTED_SIZE_EXCEPTION_CLASS: ClassDesc = ClassDesc::new(
-    "com/google/android/nearby/presence/rust/SerializationException$UnencryptedSizeException",
-);
-
-/// Rust representation of `class SerializationException.UnencryptedSizeException`.
-#[repr(transparent)]
-pub struct UnencryptedSizeException<Obj>(pub Obj);
-
-impl<'local> UnencryptedSizeException<JObject<'local>> {
-    /// Create a new instance.
-    pub fn construct(env: &mut JNIEnv<'local>) -> jni::errors::Result<Self> {
-        pourover::call_constructor!(env, &UNENCRYPTED_SIZE_EXCEPTION_CLASS, "()V").map(Self)
-    }
-
-    /// Create a new instance and throw it.
-    pub fn throw_new(env: &mut JNIEnv<'local>) -> jni::errors::Result<()> {
-        Self::construct(env)?.throw(env)
-    }
-}
-
-impl<'local, Obj: AsRef<JObject<'local>>> UnencryptedSizeException<Obj> {
-    /// Throw this exception.
-    pub fn throw<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<()> {
-        env.throw(<&JThrowable>::from(self.0.as_ref()))
-    }
-}
+exception_wrapper!(
+    /// Rust representation of `class SerializationException.UnencryptedSizeException`.
+    UnencryptedSizeException, "com/google/android/nearby/presence/rust/SerializationException$UnencryptedSizeException");
diff --git a/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs b/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs
index 1ba39b6..ccb3a9e 100644
--- a/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs
+++ b/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs
@@ -71,8 +71,13 @@
         data_element: V0DataElement,
     ) -> jni::errors::Result<()> {
         let builder = self.as_rust_handle(env)?;
-        #[allow(clippy::expect_used)]
-        let res = builder.add_de(data_element).expect("valid data structure (created in Rust)");
+        let Ok(res) = builder.add_de(data_element) else {
+            let _ = env.throw_new(
+                "java/lang/IllegalStateException",
+                "V0Actions is not validly constructed",
+            );
+            return Err(jni::errors::Error::JavaException);
+        };
 
         match res {
             AddV0DEResult::Success => {}
diff --git a/nearby/presence/np_java_ffi/src/class/v0_data_element.rs b/nearby/presence/np_java_ffi/src/class/v0_data_element.rs
index 44f0636..4d05a6a 100644
--- a/nearby/presence/np_java_ffi/src/class/v0_data_element.rs
+++ b/nearby/presence/np_java_ffi/src/class/v0_data_element.rs
@@ -283,5 +283,5 @@
         };
     }
 
-    0
+    v0_actions.as_u32() as jint
 }
diff --git a/nearby/presence/np_java_ffi/src/class/v0_payload.rs b/nearby/presence/np_java_ffi/src/class/v0_payload.rs
index e962f5e..c54de0d 100644
--- a/nearby/presence/np_java_ffi/src/class/v0_payload.rs
+++ b/nearby/presence/np_java_ffi/src/class/v0_payload.rs
@@ -43,7 +43,7 @@
     /// Create an IdentityDetails instance
     pub fn construct_from_parts(
         env: &mut JNIEnv<'local>,
-        credential_id: u32,
+        credential_id: i64,
         identity_token: [u8; 14],
         salt: [u8; 2],
     ) -> jni::errors::Result<Self> {
diff --git a/nearby/presence/np_java_ffi/src/class/v1_advertisement_builder.rs b/nearby/presence/np_java_ffi/src/class/v1_advertisement_builder.rs
new file mode 100644
index 0000000..ba1a3ed
--- /dev/null
+++ b/nearby/presence/np_java_ffi/src/class/v1_advertisement_builder.rs
@@ -0,0 +1,354 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use handle_map::{Handle, HandleLike};
+use jni::{
+    objects::{JClass, JObject},
+    sys::{jboolean, jint, jlong, JNI_TRUE},
+    JNIEnv,
+};
+use np_ffi_core::{
+    serialize::v1::{
+        self, create_v1_advertisement_builder, AddV1DEResult, AddV1SectionToAdvertisementResult,
+        CreateV1AdvertisementBuilderResult, CreateV1SectionBuilderResult,
+        SerializeV1AdvertisementResult, V1AdvertisementBuilder,
+    },
+    serialize::AdvertisementBuilderKind,
+};
+use pourover::{desc::ClassDesc, jni_method};
+
+use crate::class::{
+    v1_data_element::Generic, InsufficientSpaceException, InvalidDataElementException,
+    InvalidHandleException, InvalidSectionKindException, NoSpaceLeftException,
+    UnclosedActiveSectionException, V1BroadcastCredential, VerificationMode,
+};
+
+static V1_BUILDER_HANDLE_CLASS: ClassDesc = ClassDesc::new(
+    "com/google/android/nearby/presence/rust/V1AdvertisementBuilder$V1BuilderHandle",
+);
+
+/// Rust representation of `class V1AdvertisementBuilder.V1BuilderHandle`.
+#[repr(transparent)]
+pub struct V1BuilderHandle<Obj>(pub Obj);
+
+impl<'local, Obj: AsRef<JObject<'local>>> V1BuilderHandle<Obj> {
+    /// Get a reference to the inner `jni` crate [`JObject`].
+    pub fn as_obj(&self) -> &JObject<'local> {
+        self.0.as_ref()
+    }
+
+    /// Get the Rust [`HandleLike`] representation from this Java object.
+    pub fn as_rust_handle<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<V1AdvertisementBuilder> {
+        let handle_id = self.get_handle_id(env)?;
+        Ok(V1AdvertisementBuilder::from_handle(Handle::from_id(handle_id as u64)))
+    }
+
+    /// Get `long handleId` from the Java object
+    fn get_handle_id<'env_local>(
+        &self,
+        env: &mut JNIEnv<'env_local>,
+    ) -> jni::errors::Result<jlong> {
+        use V1_BUILDER_HANDLE_CLASS as CLS;
+        pourover::call_method!(env, &CLS, "getId", "()J", self.as_obj())
+    }
+}
+
+static V1_SECTION_BUILDER: ClassDesc =
+    ClassDesc::new("com/google/android/nearby/presence/rust/V1SectionBuilder");
+
+/// Rust representation of `class V1SectionBuilder`.
+#[repr(transparent)]
+pub struct V1SectionBuilder<Obj>(pub Obj);
+
+impl<'local> V1SectionBuilder<JObject<'local>> {
+    /// Create a new V1SectionBuilder instance.
+    pub fn construct<'h>(
+        env: &mut JNIEnv<'local>,
+        handle: V1BuilderHandle<impl AsRef<JObject<'h>>>,
+        section: jint,
+    ) -> jni::errors::Result<Self> {
+        pourover::call_constructor!(
+            env,
+            &V1_SECTION_BUILDER,
+            "(Lcom/google/android/nearby/presence/rust/V1AdvertisementBuilder$V1BuilderHandle;I)V",
+            handle.as_obj(),
+            section,
+        )
+        .map(Self)
+    }
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V1AdvertisementBuilder.V1BuilderHandle"
+)]
+extern "system" fn allocate<'local>(
+    mut env: JNIEnv<'local>,
+    _cls: JClass<'local>,
+    encrypted: jboolean,
+) -> jlong {
+    let kind = if encrypted == JNI_TRUE {
+        AdvertisementBuilderKind::Encrypted
+    } else {
+        AdvertisementBuilderKind::Public
+    };
+    match create_v1_advertisement_builder(kind) {
+        CreateV1AdvertisementBuilderResult::Success(builder) => {
+            builder.get_as_handle().get_id() as jlong
+        }
+        CreateV1AdvertisementBuilderResult::NoSpaceLeft => {
+            let _ = NoSpaceLeftException::throw_new(&mut env);
+            0
+        }
+    }
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V1AdvertisementBuilder.V1BuilderHandle"
+)]
+extern "system" fn nativeAddPublicSection<'local>(
+    mut env: JNIEnv<'local>,
+    this: V1BuilderHandle<JObject<'local>>,
+) -> JObject<'local> {
+    let Ok(handle) = this.as_rust_handle(&mut env) else {
+        return JObject::null();
+    };
+
+    let builder = match handle.public_section_builder() {
+        CreateV1SectionBuilderResult::Success(builder) => builder,
+        CreateV1SectionBuilderResult::UnclosedActiveSection => {
+            let _ = UnclosedActiveSectionException::throw_new(&mut env);
+            return JObject::null();
+        }
+        CreateV1SectionBuilderResult::InvalidAdvertisementBuilderHandle => {
+            let _ = InvalidHandleException::throw_new(&mut env);
+            return JObject::null();
+        }
+        CreateV1SectionBuilderResult::IdentityKindMismatch => {
+            let _ = InvalidSectionKindException::throw_new(&mut env);
+            return JObject::null();
+        }
+        CreateV1SectionBuilderResult::NoSpaceLeft => {
+            let _ = InsufficientSpaceException::throw_new(&mut env);
+            return JObject::null();
+        }
+    };
+
+    assert_eq!(
+        handle.get_as_handle().get_id(),
+        builder.adv_builder.get_as_handle().get_id(),
+        "Section builder must be the same handle so that we can reuse the Java handle object below"
+    );
+
+    let Ok(java_builder) =
+        V1SectionBuilder::construct(&mut env, this, builder.section_index.into())
+    else {
+        return JObject::null();
+    };
+
+    java_builder.0
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V1AdvertisementBuilder.V1BuilderHandle"
+)]
+extern "system" fn nativeAddEncryptedSection<'local>(
+    mut env: JNIEnv<'local>,
+    this: V1BuilderHandle<JObject<'local>>,
+    credential: V1BroadcastCredential<JObject<'local>>,
+    verification_mode: jint,
+) -> JObject<'local> {
+    let Ok(handle) = this.as_rust_handle(&mut env) else {
+        return JObject::null();
+    };
+
+    let Ok(credential) = credential.get_as_core(&mut env) else {
+        return JObject::null();
+    };
+
+    let verification_mode = match VerificationMode::from_java(&mut env, verification_mode) {
+        Ok(Some(verification_mode)) => verification_mode.into(),
+        Ok(None) => {
+            let _ = env
+                .throw_new("java/lang/IllegalArgumentException", "Invalid @VerificationMode value");
+            return JObject::null();
+        }
+        Err(_) => return JObject::null(),
+    };
+
+    let builder = match handle.encrypted_section_builder(credential, verification_mode) {
+        CreateV1SectionBuilderResult::Success(builder) => builder,
+        CreateV1SectionBuilderResult::UnclosedActiveSection => {
+            let _ = UnclosedActiveSectionException::throw_new(&mut env);
+            return JObject::null();
+        }
+        CreateV1SectionBuilderResult::InvalidAdvertisementBuilderHandle => {
+            let _ = InvalidHandleException::throw_new(&mut env);
+            return JObject::null();
+        }
+        CreateV1SectionBuilderResult::IdentityKindMismatch => {
+            let _ = InvalidSectionKindException::throw_new(&mut env);
+            return JObject::null();
+        }
+        CreateV1SectionBuilderResult::NoSpaceLeft => {
+            let _ = InsufficientSpaceException::throw_new(&mut env);
+            return JObject::null();
+        }
+    };
+
+    assert_eq!(
+        handle.get_as_handle().get_id(),
+        builder.adv_builder.get_as_handle().get_id(),
+        "Section builder must be the same handle so that we can reuse the Java handle object below"
+    );
+
+    let Ok(java_builder) =
+        V1SectionBuilder::construct(&mut env, this, builder.section_index.into())
+    else {
+        return JObject::null();
+    };
+
+    java_builder.0
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V1AdvertisementBuilder.V1BuilderHandle"
+)]
+extern "system" fn nativeAddGenericDataElement<'local>(
+    mut env: JNIEnv<'local>,
+    this: V1BuilderHandle<JObject<'local>>,
+    section_index: jint,
+    generic_de: Generic<JObject<'local>>,
+) {
+    let Ok(adv_builder) = this.as_rust_handle(&mut env) else {
+        return;
+    };
+
+    let Ok(section_index) = u8::try_from(section_index) else {
+        return;
+    };
+
+    let section_builder = v1::V1SectionBuilder { adv_builder, section_index };
+
+    let de = match generic_de.as_core_byte_buffer_de(&mut env) {
+        Ok(Some(de)) => de,
+        Ok(None) => {
+            let _ = env
+                .new_string("Data is too long")
+                .map(|s| env.auto_local(s))
+                .and_then(|reason| InvalidDataElementException::throw_new(&mut env, &reason));
+            return;
+        }
+        Err(_jni_err) => {
+            return;
+        }
+    };
+
+    match section_builder.add_127_byte_buffer_de(de) {
+        AddV1DEResult::Success => {}
+        AddV1DEResult::InvalidSectionHandle => {
+            let _ = InvalidHandleException::throw_new(&mut env);
+        }
+        AddV1DEResult::InsufficientSectionSpace => {
+            let _ = InsufficientSpaceException::throw_new(&mut env);
+        }
+        AddV1DEResult::InvalidDataElement => {
+            let _ = env
+                .new_string("Invalid generic data element")
+                .map(|string| env.auto_local(string))
+                .and_then(|reason| InvalidDataElementException::throw_new(&mut env, &reason));
+        }
+    }
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V1AdvertisementBuilder.V1BuilderHandle"
+)]
+extern "system" fn nativeFinishSection<'local>(
+    mut env: JNIEnv<'local>,
+    this: V1BuilderHandle<JObject<'local>>,
+    section_index: jint,
+) {
+    let Ok(adv_builder) = this.as_rust_handle(&mut env) else {
+        return;
+    };
+
+    let Ok(section_index) = u8::try_from(section_index) else {
+        return;
+    };
+
+    let section_builder = v1::V1SectionBuilder { adv_builder, section_index };
+
+    match section_builder.add_to_advertisement() {
+        AddV1SectionToAdvertisementResult::Success => {
+            // It worked.
+        }
+        AddV1SectionToAdvertisementResult::Error => {
+            // This case covers:
+            //  * The handle is invalid
+            //  * The handle is in an incorrect state (no open section)
+            //  * The section builder is not for the currently opened section
+            let _ = InvalidHandleException::throw_new(&mut env);
+        }
+    }
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V1AdvertisementBuilder.V1BuilderHandle"
+)]
+extern "system" fn nativeBuild<'local>(
+    mut env: JNIEnv<'local>,
+    this: V1BuilderHandle<JObject<'local>>,
+) -> JObject<'local> {
+    let Ok(handle) = this.as_rust_handle(&mut env) else {
+        return JObject::null();
+    };
+
+    match handle.into_advertisement() {
+        SerializeV1AdvertisementResult::Success(bytes) => {
+            #[allow(clippy::expect_used)]
+            let adv_bytes = bytes.as_slice().expect("should never be malformed from core");
+            env.byte_array_from_slice(adv_bytes).map_or(JObject::null(), JObject::from)
+        }
+        SerializeV1AdvertisementResult::InvalidBuilderState => {
+            let _ = UnclosedActiveSectionException::throw_new(&mut env);
+            JObject::null()
+        }
+        SerializeV1AdvertisementResult::InvalidAdvertisementBuilderHandle => {
+            let _ = InvalidHandleException::throw_new(&mut env);
+            JObject::null()
+        }
+    }
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V1AdvertisementBuilder.V1BuilderHandle"
+)]
+extern "system" fn deallocate<'local>(
+    _env: JNIEnv<'local>,
+    _cls: JClass<'local>,
+    handle_id: jlong,
+) {
+    let handle = V1AdvertisementBuilder::from_handle(Handle::from_id(handle_id as u64));
+    let _ = handle.deallocate();
+}
diff --git a/nearby/presence/np_java_ffi/src/class/v1_data_element.rs b/nearby/presence/np_java_ffi/src/class/v1_data_element.rs
index 6401165..c404502 100644
--- a/nearby/presence/np_java_ffi/src/class/v1_data_element.rs
+++ b/nearby/presence/np_java_ffi/src/class/v1_data_element.rs
@@ -14,11 +14,13 @@
 
 //! Data Elements for v1 advertisements. See `class V1DataElement`.
 
+use array_view::ArrayView;
 use jni::{
     objects::{JByteArray, JObject},
     sys::jlong,
     JNIEnv,
 };
+use np_ffi_core::{common::ByteBuffer, serialize::v1::V1DE127ByteBuffer};
 use pourover::desc::ClassDesc;
 
 static GENERIC_CLASS: ClassDesc =
@@ -48,4 +50,48 @@
     ) -> jni::errors::Result<Option<Self>> {
         Ok(env.is_instance_of(obj.as_ref(), &GENERIC_CLASS)?.then(|| Self(obj)))
     }
+
+    /// Get a reference to the inner `jni` crate [`JObject`].
+    pub fn as_obj(&self) -> &JObject<'local> {
+        self.0.as_ref()
+    }
+
+    /// Get the data element's type
+    pub fn get_type<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<jlong> {
+        pourover::call_method!(env, &GENERIC_CLASS, "getType", "()J", self.as_obj())
+    }
+
+    /// Get the data element's data. Returns `None` if the data is too long.
+    pub fn get_data<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<Option<ByteBuffer<127>>> {
+        let data = pourover::call_method!(env, &GENERIC_CLASS, "getData", "()[B", self.as_obj())?;
+        let len = env.get_array_length(&data)? as usize;
+
+        if len > 127 {
+            return Ok(None);
+        }
+
+        // Length is validated above
+        #[allow(clippy::unwrap_used)]
+        {
+            let mut buffer = [0; 127];
+            env.get_byte_array_region(&data, 0, buffer.get_mut(..len).unwrap())?;
+            let buffer = buffer.map(|b| b as u8);
+            Ok(Some(ByteBuffer::from_array_view(ArrayView::try_from_array(buffer, len).unwrap())))
+        }
+    }
+
+    /// Get the data element as a `np_ffi_core` byte buffer. Returns `None` if the data element is
+    /// not valid.
+    pub fn as_core_byte_buffer_de<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<Option<V1DE127ByteBuffer>> {
+        let Some(payload) = self.get_data(env)? else {
+            return Ok(None);
+        };
+        Ok(Some(V1DE127ByteBuffer { de_type: self.get_type(env)? as u32, payload }))
+    }
 }
diff --git a/nearby/presence/np_java_ffi/src/class/verification_mode.rs b/nearby/presence/np_java_ffi/src/class/verification_mode.rs
new file mode 100644
index 0000000..f217207
--- /dev/null
+++ b/nearby/presence/np_java_ffi/src/class/verification_mode.rs
@@ -0,0 +1,119 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use jni::{sys::jint, JNIEnv};
+use np_ffi_core::v1::V1VerificationMode;
+use pourover::desc::{ClassDesc, StaticFieldDesc};
+use std::sync::RwLock;
+
+static VERIFICATION_MODE_CLASS: ClassDesc =
+    ClassDesc::new("com/google/android/nearby/presence/rust/VerificationMode");
+
+/// Rust representation of `@VerificationMode`.
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum VerificationMode {
+    /// Verification is done using the Mic scheme.
+    Mic,
+    /// Verification is done using the Signature scheme.
+    Signature,
+}
+
+impl VerificationMode {
+    /// Convert a Java `int` to the Rust version. Will return `None` if the given `value` is not
+    /// valid.
+    pub fn from_java<'local>(
+        env: &mut JNIEnv<'local>,
+        value: jint,
+    ) -> jni::errors::Result<Option<Self>> {
+        if value == Self::mic(env)? {
+            Ok(Some(Self::Mic))
+        } else if value == Self::signature(env)? {
+            Ok(Some(Self::Signature))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Convert to a Java `int` value.
+    pub fn to_java<'local>(&self, env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
+        match self {
+            Self::Mic => Self::mic(env),
+            Self::Signature => Self::signature(env),
+        }
+    }
+
+    /// Fetch the `SIGNATURE` constant
+    fn signature<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
+        static SIGNATURE: StaticFieldDesc = VERIFICATION_MODE_CLASS.static_field("SIGNATURE", "I");
+        static VALUE: RwLock<Option<jint>> = RwLock::new(None);
+        Self::lookup_static_value(env, &SIGNATURE, &VALUE)
+    }
+
+    /// Fetch the `MIC` constant
+    fn mic<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
+        static MIC: StaticFieldDesc = VERIFICATION_MODE_CLASS.static_field("MIC", "I");
+        static VALUE: RwLock<Option<jint>> = RwLock::new(None);
+        Self::lookup_static_value(env, &MIC, &VALUE)
+    }
+
+    /// Look up the given field and cache it in the given cache. The lookup will only be performed
+    /// once if successful. This uses `RwLock` instead of `OnceCell` since the fallible `OnceCell`
+    /// APIs are nightly only.
+    fn lookup_static_value<'local>(
+        env: &mut JNIEnv<'local>,
+        field: &StaticFieldDesc,
+        cache: &RwLock<Option<jint>>,
+    ) -> jni::errors::Result<jint> {
+        use jni::signature::{JavaType, Primitive};
+
+        // Read from cache
+        if let Some(value) = *cache.read().unwrap_or_else(|poison| poison.into_inner()) {
+            return Ok(value);
+        }
+
+        // Get exclusive access to the cache for the lookup
+        let mut guard = cache.write().unwrap_or_else(|poison| poison.into_inner());
+
+        // In case of races, only lookup the value once
+        if let Some(value) = *guard {
+            return Ok(value);
+        }
+
+        let value = env
+            .get_static_field_unchecked(field.cls(), field, JavaType::Primitive(Primitive::Int))
+            .and_then(|ret| ret.i())?;
+
+        *guard = Some(value);
+
+        Ok(value)
+    }
+}
+
+impl From<V1VerificationMode> for VerificationMode {
+    fn from(mode: V1VerificationMode) -> Self {
+        match mode {
+            V1VerificationMode::Mic => Self::Mic,
+            V1VerificationMode::Signature => Self::Signature,
+        }
+    }
+}
+
+impl From<VerificationMode> for V1VerificationMode {
+    fn from(mode: VerificationMode) -> Self {
+        match mode {
+            VerificationMode::Mic => Self::Mic,
+            VerificationMode::Signature => Self::Signature,
+        }
+    }
+}
diff --git a/nearby/presence/np_java_ffi/src/lib.rs b/nearby/presence/np_java_ffi/src/lib.rs
index 7a688e8..bd03c81 100644
--- a/nearby/presence/np_java_ffi/src/lib.rs
+++ b/nearby/presence/np_java_ffi/src/lib.rs
@@ -19,3 +19,5 @@
 #![allow(clippy::needless_lifetimes)]
 
 pub mod class;
+#[cfg(feature = "testing")]
+pub mod testing;
diff --git a/common/derive_fuzztest/fuzz/src/bin/integer_add.rs b/nearby/presence/np_java_ffi/src/testing.rs
similarity index 74%
rename from common/derive_fuzztest/fuzz/src/bin/integer_add.rs
rename to nearby/presence/np_java_ffi/src/testing.rs
index 38b8172..5717c7c 100644
--- a/common/derive_fuzztest/fuzz/src/bin/integer_add.rs
+++ b/nearby/presence/np_java_ffi/src/testing.rs
@@ -12,12 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#![cfg_attr(fuzzing, no_main)]
+//! Java bindings for test-only APIs
 
-use derive_fuzztest::fuzztest;
-
-#[fuzztest]
-pub fn test(a: u8, b: u8) {
-    let _ = a.checked_add(b);
-    // a + b;  // This fails because a + b can overflow.
-}
+mod test_vectors;
diff --git a/nearby/presence/np_java_ffi/src/testing/test_vectors.rs b/nearby/presence/np_java_ffi/src/testing/test_vectors.rs
new file mode 100644
index 0000000..f2d3079
--- /dev/null
+++ b/nearby/presence/np_java_ffi/src/testing/test_vectors.rs
@@ -0,0 +1,103 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use jni::{
+    objects::{JByteArray, JClass, JObject},
+    JNIEnv,
+};
+use np_adv::extended::salt::{ExtendedV1Salt, MultiSalt, ShortV1Salt};
+use np_ffi_core::serialize::v1::CreateV1SectionBuilderResult;
+use pourover::jni_method;
+
+use crate::class::{
+    InsufficientSpaceException, InvalidHandleException, InvalidSectionKindException,
+    UnclosedActiveSectionException, V1BroadcastCredential, V1BuilderHandle, V1SectionBuilder,
+};
+
+#[jni_method(package = "com.google.android.nearby.presence.rust", class = "TestVectors")]
+extern "system" fn nativeAddSaltedSection<'local>(
+    mut env: JNIEnv<'local>,
+    _cls: JClass<'local>,
+    j_handle: V1BuilderHandle<JObject<'local>>,
+    credential: V1BroadcastCredential<JObject<'local>>,
+    salt: JByteArray<'local>,
+) -> JObject<'local> {
+    let Ok(handle) = j_handle.as_rust_handle(&mut env) else {
+        return JObject::null();
+    };
+
+    let Ok(credential) = credential.get_as_core(&mut env) else {
+        return JObject::null();
+    };
+
+    let salt = {
+        match env.get_array_length(&salt) {
+            Ok(2) => {
+                let mut buf = [0; 2];
+                let Ok(_) = env.get_byte_array_region(&salt, 0, &mut buf[..]) else {
+                    return JObject::null();
+                };
+                let buf = buf.map(|b| b as u8);
+                MultiSalt::Short(ShortV1Salt::from(buf))
+            }
+            Ok(16) => {
+                let mut buf = [0; 16];
+                let Ok(_) = env.get_byte_array_region(&salt, 0, &mut buf[..]) else {
+                    return JObject::null();
+                };
+                let buf = buf.map(|b| b as u8);
+                MultiSalt::Extended(ExtendedV1Salt::from(buf))
+            }
+            Ok(_) => {
+                let _ = env.throw_new("java/lang/RuntimeException", "Invalid salt length");
+                return JObject::null();
+            }
+            Err(_jni_err) => return JObject::null(),
+        }
+    };
+
+    let builder = match handle.mic_custom_salt_section_builder(credential, salt) {
+        CreateV1SectionBuilderResult::Success(builder) => builder,
+        CreateV1SectionBuilderResult::UnclosedActiveSection => {
+            let _ = UnclosedActiveSectionException::throw_new(&mut env);
+            return JObject::null();
+        }
+        CreateV1SectionBuilderResult::InvalidAdvertisementBuilderHandle => {
+            let _ = InvalidHandleException::throw_new(&mut env);
+            return JObject::null();
+        }
+        CreateV1SectionBuilderResult::IdentityKindMismatch => {
+            let _ = InvalidSectionKindException::throw_new(&mut env);
+            return JObject::null();
+        }
+        CreateV1SectionBuilderResult::NoSpaceLeft => {
+            let _ = InsufficientSpaceException::throw_new(&mut env);
+            return JObject::null();
+        }
+    };
+
+    assert_eq!(
+        handle.get_as_handle().get_id(),
+        builder.adv_builder.get_as_handle().get_id(),
+        "Section builder must be the same handle so that we can reuse the Java handle object below"
+    );
+
+    let Ok(java_builder) =
+        V1SectionBuilder::construct(&mut env, j_handle, builder.section_index.into())
+    else {
+        return JObject::null();
+    };
+
+    java_builder.0
+}
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DecryptTests.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DecryptTests.java
index f9a7ede..d951cbf 100644
--- a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DecryptTests.java
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DecryptTests.java
@@ -16,12 +16,22 @@
 
 package com.google.android.nearby.presence.rust;
 
-import static com.google.android.nearby.presence.rust.TestData.*;
+import static com.google.android.nearby.presence.rust.TestData.ALICE_METADATA;
+import static com.google.android.nearby.presence.rust.TestData.V0_CRED;
+import static com.google.android.nearby.presence.rust.TestData.V0_ENCRYPTED_ALICE_METADATA;
+import static com.google.android.nearby.presence.rust.TestData.V0_PRIVATE;
+import static com.google.android.nearby.presence.rust.TestData.V1_ALICE_METADATA;
+import static com.google.android.nearby.presence.rust.TestData.V1_CRED;
+import static com.google.android.nearby.presence.rust.TestData.V1_ENCRYPTED_ALICE_METADATA;
+import static com.google.android.nearby.presence.rust.TestData.V1_PRIVATE;
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.android.nearby.presence.rust.credential.CredentialBook;
-import org.junit.jupiter.api.Test;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
+@RunWith(JUnit4.class)
 public class DecryptTests {
 
   public static final class TestMetadata implements CredentialBook.MatchedMetadata {
@@ -55,11 +65,11 @@
   }
 
   @Test
-  void deserializeAdvertisement_v0_canParsePrivate() {
-    try (DeserializeResult result = parsePrivateAdv(V0_PRIVATE)) {
+  public void deserializeAdvertisement_v0_canParsePrivate() {
+    try (DeserializeResult<TestMetadata> result = parsePrivateAdv(V0_PRIVATE)) {
       assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V0_ADVERTISEMENT);
 
-      DeserializedV0Advertisement adv = result.getAsV0();
+      DeserializedV0Advertisement<TestMetadata> adv = result.getAsV0();
 
       assertThat(adv).isNotNull();
       assertThat(adv.isLegible()).isTrue();
@@ -70,20 +80,20 @@
   }
 
   @Test
-  void deserializeAdvertisement_v1_canParsePrivate() {
-    try (DeserializeResult result = parsePrivateAdv(V1_PRIVATE)) {
+  public void deserializeAdvertisement_v1_canParsePrivate() {
+    try (DeserializeResult<TestMetadata> result = parsePrivateAdv(V1_PRIVATE)) {
       assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V1_ADVERTISEMENT);
 
-      DeserializedV1Advertisement adv = result.getAsV1();
+      DeserializedV1Advertisement<TestMetadata> adv = result.getAsV1();
 
       assertThat(adv).isNotNull();
       assertThat(adv.getNumLegibleSections()).isEqualTo(1);
       assertThat(adv.getNumUndecryptableSections()).isEqualTo(0);
 
-      DeserializedV1Section section = adv.getSection(0);
+      DeserializedV1Section<TestMetadata> section = adv.getSection(0);
       assertThat(section.getIdentityKind()).isEqualTo(IdentityKind.DECRYPTED);
       assertThat(section.getMatchedMetadata()).isSameInstanceAs(V1_METADATA);
-      assertThat(section.getDecryptedMetadata()).isEqualTo(ALICE_METADATA);
+      assertThat(section.getDecryptedMetadata()).isEqualTo(V1_ALICE_METADATA);
     }
   }
 }
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DeserializeTests.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DeserializeTests.java
index 7457edb..b4370d2 100644
--- a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DeserializeTests.java
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DeserializeTests.java
@@ -16,24 +16,34 @@
 
 package com.google.android.nearby.presence.rust;
 
-import static com.google.android.nearby.presence.rust.DeserializedV1Section.VerificationMode;
-import static com.google.android.nearby.presence.rust.TestData.*;
-import static com.google.android.nearby.presence.rust.credential.CredentialBook.NoMetadata;
+import static com.google.android.nearby.presence.rust.TestData.V0_CRED;
+import static com.google.android.nearby.presence.rust.TestData.V0_IDENTITY_TOKEN;
+import static com.google.android.nearby.presence.rust.TestData.V0_PRIVATE;
+import static com.google.android.nearby.presence.rust.TestData.V0_PUBLIC;
+import static com.google.android.nearby.presence.rust.TestData.V1_CRED;
+import static com.google.android.nearby.presence.rust.TestData.V1_IDENTITY_TOKEN;
+import static com.google.android.nearby.presence.rust.TestData.V1_PRIVATE;
+import static com.google.android.nearby.presence.rust.TestData.V1_PUBLIC;
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.android.nearby.presence.rust.credential.CredentialBook;
-import org.junit.jupiter.api.Test;
+import com.google.android.nearby.presence.rust.credential.CredentialBook.NoMetadata;
+import java.util.ArrayList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
+@RunWith(JUnit4.class)
 public class DeserializeTests {
 
-  DeserializeResult parsePublicAdv(byte[] bytes) {
+  DeserializeResult<NoMetadata> parsePublicAdv(byte[] bytes) {
     // Call parse with an empty CredentialBook
     try (CredentialBook<NoMetadata> book = CredentialBook.empty()) {
       return NpAdv.deserializeAdvertisement(bytes, book);
     }
   }
 
-  DeserializeResult parsePrivateAdv(byte[] bytes) {
+  DeserializeResult<NoMetadata> parsePrivateAdv(byte[] bytes) {
     try (CredentialBook<NoMetadata> book =
         CredentialBook.<NoMetadata>builder()
             .addDiscoveryCredential(V0_CRED, NoMetadata.INSTANCE)
@@ -44,11 +54,11 @@
   }
 
   @Test
-  void deserializeAdvertisement_v0_canParsePublic() {
-    try (DeserializeResult result = parsePublicAdv(V0_PUBLIC)) {
+  public void deserializeAdvertisement_v0_canParsePublic() {
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(V0_PUBLIC)) {
       assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V0_ADVERTISEMENT);
 
-      DeserializedV0Advertisement adv = result.getAsV0();
+      DeserializedV0Advertisement<NoMetadata> adv = result.getAsV0();
 
       assertThat(adv).isNotNull();
       assertThat(adv.isLegible()).isTrue();
@@ -60,11 +70,11 @@
   }
 
   @Test
-  void deserializeAdvertisement_v0_canParsePublicWithCreds() {
-    try (DeserializeResult result = parsePrivateAdv(V0_PUBLIC)) {
+  public void deserializeAdvertisement_v0_canParsePublicWithCreds() {
+    try (DeserializeResult<NoMetadata> result = parsePrivateAdv(V0_PUBLIC)) {
       assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V0_ADVERTISEMENT);
 
-      DeserializedV0Advertisement adv = result.getAsV0();
+      DeserializedV0Advertisement<NoMetadata> adv = result.getAsV0();
 
       assertThat(adv).isNotNull();
       assertThat(adv.isLegible()).isTrue();
@@ -76,11 +86,11 @@
   }
 
   @Test
-  void deserializeAdvertisement_v0_canParsePrivate() {
-    try (DeserializeResult result = parsePrivateAdv(V0_PRIVATE)) {
+  public void deserializeAdvertisement_v0_canParsePrivate() {
+    try (DeserializeResult<NoMetadata> result = parsePrivateAdv(V0_PRIVATE)) {
       assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V0_ADVERTISEMENT);
 
-      DeserializedV0Advertisement adv = result.getAsV0();
+      DeserializedV0Advertisement<NoMetadata> adv = result.getAsV0();
 
       assertThat(adv).isNotNull();
       assertThat(adv.isLegible()).isTrue();
@@ -88,18 +98,18 @@
       assertThat(adv.getDataElementCount()).isEqualTo(1);
       assertThat(adv.getDataElement(0)).isInstanceOf(V0DataElement.TxPower.class);
       assertThat(adv.getIdentityToken()).isEqualTo(V0_IDENTITY_TOKEN);
-      assertThat(adv.getSalt()).asList().containsExactly((byte) 0x22, (byte) 0x22);
+      assertThat(adv.getSalt()).isEqualTo(new byte[] {(byte) 0x22, (byte) 0x22});
       assertThat(adv.getMatchedMetadata()).isSameInstanceAs(NoMetadata.INSTANCE);
       assertThat(adv.getDecryptedMetadata()).isNull();
     }
   }
 
   @Test
-  void deserializeAdvertisement_v0_cannotParsePrivateWithoutCreds() {
-    try (DeserializeResult result = parsePublicAdv(V0_PRIVATE)) {
+  public void deserializeAdvertisement_v0_cannotParsePrivateWithoutCreds() {
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(V0_PRIVATE)) {
       assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V0_ADVERTISEMENT);
 
-      DeserializedV0Advertisement adv = result.getAsV0();
+      DeserializedV0Advertisement<NoMetadata> adv = result.getAsV0();
 
       assertThat(adv).isNotNull();
       assertThat(adv.isLegible()).isFalse();
@@ -107,9 +117,9 @@
   }
 
   @Test
-  void deserializeAdvertisement_v0_canReadTxPowerDe() {
-    try (DeserializeResult result = parsePublicAdv(V0_PUBLIC)) {
-      DeserializedV0Advertisement adv = result.getAsV0();
+  public void deserializeAdvertisement_v0_canReadTxPowerDe() {
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(V0_PUBLIC)) {
+      DeserializedV0Advertisement<NoMetadata> adv = result.getAsV0();
 
       V0DataElement de = adv.getDataElement(0);
 
@@ -120,9 +130,37 @@
   }
 
   @Test
-  void deserializeAdvertisement_v0_canReadActionsDe() {
-    try (DeserializeResult result = parsePublicAdv(V0_PUBLIC)) {
-      DeserializedV0Advertisement adv = result.getAsV0();
+  public void deserializeAdvertisement_v0_canIterateDataElements() throws Exception {
+    final int numDes = 2;
+    byte[] advBytes;
+
+    try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
+      builder.addDataElement(
+          new V0DataElement.V0Actions(
+              IdentityKind.PLAINTEXT, V0DataElement.V0ActionType.NEARBY_SHARE));
+      builder.addDataElement(new V0DataElement.TxPower(10));
+      advBytes = builder.build();
+    }
+
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(advBytes)) {
+      DeserializedV0Advertisement<NoMetadata> adv = result.getAsV0();
+      ArrayList<V0DataElement> deList = new ArrayList<>();
+      for (V0DataElement de : adv.getDataElements()) {
+        deList.add(de);
+      }
+
+      // Validate de order
+      assertThat(deList.get(0)).isInstanceOf(V0DataElement.V0Actions.class);
+      assertThat(deList.get(1)).isInstanceOf(V0DataElement.TxPower.class);
+      // Validate de count
+      assertThat(deList.size()).isEqualTo(numDes);
+    }
+  }
+
+  @Test
+  public void deserializeAdvertisement_v0_canReadActionsDe() {
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(V0_PUBLIC)) {
+      DeserializedV0Advertisement<NoMetadata> adv = result.getAsV0();
 
       V0DataElement de = adv.getDataElement(1);
 
@@ -135,11 +173,11 @@
   }
 
   @Test
-  void deserializeAdvertisement_v1_canParsePublic() {
-    try (DeserializeResult result = parsePublicAdv(V1_PUBLIC)) {
+  public void deserializeAdvertisement_v1_canParsePublic() {
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(V1_PUBLIC)) {
       assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V1_ADVERTISEMENT);
 
-      DeserializedV1Advertisement adv = result.getAsV1();
+      DeserializedV1Advertisement<NoMetadata> adv = result.getAsV1();
 
       assertThat(adv).isNotNull();
       assertThat(adv.getNumLegibleSections()).isEqualTo(1);
@@ -148,17 +186,17 @@
   }
 
   @Test
-  void deserializeAdvertisement_v1_canParsePrivate() {
-    try (DeserializeResult result = parsePrivateAdv(V1_PRIVATE)) {
+  public void deserializeAdvertisement_v1_canParsePrivate() {
+    try (DeserializeResult<NoMetadata> result = parsePrivateAdv(V1_PRIVATE)) {
       assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V1_ADVERTISEMENT);
 
-      DeserializedV1Advertisement adv = result.getAsV1();
+      DeserializedV1Advertisement<NoMetadata> adv = result.getAsV1();
 
       assertThat(adv).isNotNull();
       assertThat(adv.getNumLegibleSections()).isEqualTo(1);
       assertThat(adv.getNumUndecryptableSections()).isEqualTo(0);
 
-      DeserializedV1Section section = adv.getSection(0);
+      DeserializedV1Section<NoMetadata> section = adv.getSection(0);
       assertThat(section.getIdentityKind()).isEqualTo(IdentityKind.DECRYPTED);
       assertThat(section.getDataElementCount()).isEqualTo(1);
       assertThat(section.getIdentityToken()).isEqualTo(V1_IDENTITY_TOKEN);
@@ -169,11 +207,11 @@
   }
 
   @Test
-  void deserializeAdvertisement_v1_canParsePrivateWithoutCreds() {
-    try (DeserializeResult result = parsePublicAdv(V1_PRIVATE)) {
+  public void deserializeAdvertisement_v1_canParsePrivateWithoutCreds() {
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(V1_PRIVATE)) {
       assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V1_ADVERTISEMENT);
 
-      DeserializedV1Advertisement adv = result.getAsV1();
+      DeserializedV1Advertisement<NoMetadata> adv = result.getAsV1();
 
       assertThat(adv).isNotNull();
       assertThat(adv.getNumLegibleSections()).isEqualTo(0);
@@ -182,11 +220,11 @@
   }
 
   @Test
-  void deserializeAdvertisement_v1_canParsePublicSection() {
-    try (DeserializeResult result = parsePublicAdv(V1_PUBLIC)) {
-      DeserializedV1Advertisement adv = result.getAsV1();
+  public void deserializeAdvertisement_v1_canParsePublicSection() {
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(V1_PUBLIC)) {
+      DeserializedV1Advertisement<NoMetadata> adv = result.getAsV1();
 
-      DeserializedV1Section section = adv.getSection(0);
+      DeserializedV1Section<NoMetadata> section = adv.getSection(0);
 
       assertThat(section).isNotNull();
       assertThat(section.getIdentityKind()).isEqualTo(IdentityKind.PLAINTEXT);
@@ -197,10 +235,10 @@
   }
 
   @Test
-  void deserializeAdvertisement_v1_canReadGenericDe() {
-    try (DeserializeResult result = parsePublicAdv(V1_PUBLIC)) {
-      DeserializedV1Advertisement adv = result.getAsV1();
-      DeserializedV1Section section = adv.getSection(0);
+  public void deserializeAdvertisement_v1_canReadGenericDe() {
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(V1_PUBLIC)) {
+      DeserializedV1Advertisement<NoMetadata> adv = result.getAsV1();
+      DeserializedV1Section<NoMetadata> section = adv.getSection(0);
 
       V1DataElement de = section.getDataElement(0);
 
@@ -208,7 +246,67 @@
       assertThat(de).isInstanceOf(V1DataElement.Generic.class);
       V1DataElement.Generic generic = (V1DataElement.Generic) de;
       assertThat(generic.getType()).isEqualTo(0x05 /* V1 TxPower */);
-      assertThat(generic.getData()).asList().containsExactly((byte) 6);
+      assertThat(generic.getData()).isEqualTo(new byte[] {(byte) 6});
+    }
+  }
+
+  @Test
+  public void deserializeAdvertisement_v1_canIterateSections() throws Exception {
+    final int NUM_SECTIONS = 5;
+    byte[] advBytes;
+
+    try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newPublic()) {
+      for (int i = 0; i < NUM_SECTIONS; i++) {
+        builder
+            .addPublicSection()
+            .addDataElement(new V1DataElement.Generic(123, new byte[] {(byte) i}))
+            .finishSection();
+      }
+      advBytes = builder.build();
+    }
+
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(advBytes)) {
+      DeserializedV1Advertisement<NoMetadata> adv = result.getAsV1();
+
+      byte i = 0;
+      for (DeserializedV1Section<NoMetadata> section : adv.getSections()) {
+        V1DataElement.Generic de = (V1DataElement.Generic) section.getDataElement(0);
+        assertThat(de.getType()).isEqualTo(123);
+        // Validate section order
+        assertThat(de.getData()).isEqualTo(new byte[] {i++});
+      }
+      // Validate section count
+      assertThat(i).isEqualTo(NUM_SECTIONS);
+    }
+  }
+
+  @Test
+  public void deserializeAdvertisement_v1_canIterateDataElements() throws Exception {
+    final int numDes = 5;
+    byte[] advBytes;
+
+    try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newPublic()) {
+      try (V1SectionBuilder section = builder.addPublicSection()) {
+        for (int i = 0; i < numDes; i++) {
+          section.addDataElement(new V1DataElement.Generic(123, new byte[] {(byte) i}));
+        }
+      }
+      advBytes = builder.build();
+    }
+
+    try (DeserializeResult<NoMetadata> result = parsePublicAdv(advBytes)) {
+      DeserializedV1Advertisement<NoMetadata> adv = result.getAsV1();
+      DeserializedV1Section<NoMetadata> section = adv.getSection(0);
+
+      byte i = 0;
+      for (V1DataElement de : section.getDataElements()) {
+        V1DataElement.Generic generic = (V1DataElement.Generic) de;
+        assertThat(generic.getType()).isEqualTo(123);
+        // Validate de order
+        assertThat(generic.getData()).isEqualTo(new byte[] {i++});
+      }
+      // Validate de count
+      assertThat(i).isEqualTo(numDes);
     }
   }
 }
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/OwnedHandleTests.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/OwnedHandleTests.java
new file mode 100644
index 0000000..978a75b
--- /dev/null
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/OwnedHandleTests.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.Verifier;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+
+@RunWith(JUnit4.class)
+public class OwnedHandleTests {
+
+  @Rule public MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+  @Rule
+  public Verifier checkCleanerAfterTest =
+      new Verifier() {
+        @Override
+        protected void verify() {
+          assertThat(cleaner.getRegisteredObjectCount()).isEqualTo(0);
+        }
+      };
+
+  CooperativeCleaner cleaner = new CooperativeCleaner();
+  @Mock OwnedHandle.Destructor destructor;
+
+  class MyHandle extends OwnedHandle {
+    public MyHandle(long handleId) {
+      super(handleId, cleaner, destructor);
+    }
+
+    public MyHandle(CooperativeCleaner cleaner, long handleId) {
+      super(handleId, cleaner, destructor);
+    }
+  }
+
+  @Test
+  public void constructor_registersWithCleaner() {
+    CooperativeCleaner spyCleaner = spy(cleaner);
+    try (MyHandle handle = new MyHandle(spyCleaner, 123)) {
+      verify(spyCleaner).register(same(handle), any());
+    }
+  }
+
+  @Test
+  public void close_callsDestructor() {
+    try (MyHandle handle = new MyHandle(123)) {}
+    verify(destructor).deallocate(eq(123L));
+  }
+
+  @Test
+  public void leaked_willEndUpInCleanerQueue() throws Exception {
+    {
+      // leaked
+      waitForGc(new MyHandle(0xbad));
+    }
+
+    cleaner.processQueuedObjects();
+
+    verify(destructor).deallocate(eq(0xbadL));
+  }
+
+  @Test
+  public void close_callsDestructorOfLeakedObject() throws Exception {
+    {
+      // leaked
+      waitForGc(new MyHandle(0xbad));
+    }
+
+    MyHandle handle = new MyHandle(123);
+    handle.close();
+
+    verify(destructor).deallocate(eq(0xbadL));
+    verify(destructor).deallocate(eq(123L));
+  }
+
+  private void waitForGc(Object object) throws InterruptedException {
+    ReferenceQueue<Object> queue = new ReferenceQueue<>();
+    // Need to keep this around to be notified when object is GC'd
+    PhantomReference<Object> ref = new PhantomReference<>(object, queue);
+
+    object = null;
+    System.gc();
+
+    assertThat(queue.remove()).isSameInstanceAs(ref);
+  }
+}
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/SerializeTests.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/SerializeTests.java
index b8e18fd..7c4d88e 100644
--- a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/SerializeTests.java
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/SerializeTests.java
@@ -16,18 +16,27 @@
 
 package com.google.android.nearby.presence.rust;
 
-import static com.google.android.nearby.presence.rust.TestData.*;
-import static com.google.android.nearby.presence.rust.V0DataElement.TxPower;
-import static com.google.android.nearby.presence.rust.V0DataElement.V0ActionType;
-import static com.google.android.nearby.presence.rust.V0DataElement.V0Actions;
-import static com.google.android.nearby.presence.rust.credential.CredentialBook.NoMetadata;
+import static com.google.android.nearby.presence.rust.TestData.V0_BROADCAST_CRED;
+import static com.google.android.nearby.presence.rust.TestData.V0_CRED;
+import static com.google.android.nearby.presence.rust.TestData.V1_BROADCAST_CRED;
+import static com.google.android.nearby.presence.rust.TestData.V1_CRED;
+import static com.google.android.nearby.presence.rust.TestData.V1_PRIVATE;
+import static com.google.android.nearby.presence.rust.TestData.V1_PUBLIC;
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.Assert.assertThrows;
 
+import com.google.android.nearby.presence.rust.V0DataElement.TxPower;
+import com.google.android.nearby.presence.rust.V0DataElement.V0ActionType;
+import com.google.android.nearby.presence.rust.V0DataElement.V0Actions;
+import com.google.android.nearby.presence.rust.V1DataElement.Generic;
 import com.google.android.nearby.presence.rust.credential.CredentialBook;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
+import com.google.android.nearby.presence.rust.credential.CredentialBook.NoMetadata;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
+@RunWith(JUnit4.class)
 public class SerializeTests {
 
   public static final TxPower TX_POWER = new TxPower(7);
@@ -38,10 +47,144 @@
   public static final V0Actions PRIVATE_ACTIONS =
       new V0Actions(IdentityKind.DECRYPTED, V0ActionType.CALL_TRANSFER, V0ActionType.NEARBY_SHARE);
 
+  public static final Generic GENERIC_DE = new Generic(1234, new byte[] {0x01, 0x02, 0x03, 0x04});
+
+  @SuppressWarnings("MutablePublicArray")
   public static final byte[] SALT = new byte[] {0x12, 0x34};
 
   @Test
-  void serializeAdvertisement_v0_canSerialize() throws Exception {
+  public void constructActionsDe_mergedValueIsNonZero() {
+    assertThat(PUBLIC_ACTIONS.getActionBits()).isNotEqualTo(0);
+    assertThat(PRIVATE_ACTIONS.getActionBits()).isNotEqualTo(0);
+  }
+
+  @Test
+  public void serializeAdvertisement_v1_canCreatePubSection() throws Exception {
+    try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newPublic()) {
+      try (V1SectionBuilder sectionBuilder = builder.addPublicSection()) {
+        sectionBuilder.addDataElement(GENERIC_DE);
+      }
+      byte[] adv = builder.build();
+
+      assertThat(adv).isNotNull();
+      assertThat(adv).isNotEmpty();
+    }
+  }
+
+  @Test
+  public void serializeAdvertisement_v1_canCreateMicEncryptedSection() throws Exception {
+    try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newEncrypted()) {
+      try (V1SectionBuilder sectionBuilder =
+          builder.addEncryptedSection(V1_BROADCAST_CRED, VerificationMode.MIC)) {
+        sectionBuilder.addDataElement(GENERIC_DE);
+      }
+      byte[] adv = builder.build();
+
+      assertThat(adv).isNotNull();
+      assertThat(adv).isNotEmpty();
+    }
+  }
+
+  @Test
+  public void serializeAdvertisement_v1_canCreateEmptyPublicAdvertisement() throws Exception {
+    try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newPublic()) {
+
+      byte[] adv = builder.build();
+
+      assertThat(adv).isNotNull();
+    }
+  }
+
+  @Test
+  public void serializeAdvertisement_v1_canCreateEmptyPublicSection() throws Exception {
+    try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newPublic()) {
+      builder.addPublicSection().close();
+
+      byte[] adv = builder.build();
+
+      assertThat(adv).isNotNull();
+    }
+  }
+
+  @Test
+  public void serializeAdvertisement_v1_canCreateSignatureEncryptedSection() throws Exception {
+    try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newEncrypted()) {
+      try (V1SectionBuilder sectionBuilder =
+          builder.addEncryptedSection(V1_BROADCAST_CRED, VerificationMode.SIGNATURE)) {
+        sectionBuilder.addDataElement(GENERIC_DE);
+      }
+      byte[] adv = builder.build();
+
+      assertThat(adv).isNotNull();
+      assertThat(adv).isNotEmpty();
+    }
+  }
+
+  @Test
+  public void serializeAdvertisement_v1_canCreateRoundtripEncrypted() throws Exception {
+    try (CredentialBook<NoMetadata> book =
+            CredentialBook.<NoMetadata>builder()
+                .addDiscoveryCredential(V1_CRED, NoMetadata.INSTANCE)
+                .build();
+        DeserializeResult<NoMetadata> original = NpAdv.deserializeAdvertisement(V1_PRIVATE, book);
+        V1AdvertisementBuilder builder = V1AdvertisementBuilder.newEncrypted()) {
+      for (DeserializedV1Section<NoMetadata> section : original.getAsV1().getSections()) {
+        try (V1SectionBuilder sectionBuilder =
+            builder.addEncryptedSection(V1_BROADCAST_CRED, VerificationMode.SIGNATURE)) {
+          for (V1DataElement de : section.getDataElements()) {
+            sectionBuilder.addDataElement(de);
+          }
+        }
+      }
+      byte[] adv = builder.build();
+
+      // Can't check contents due to mismatched salts
+      assertThat(adv.length).isEqualTo(V1_PRIVATE.length);
+    }
+  }
+
+  @Test
+  public void serializeAdvertisement_v1_canCreateRoundtripPublic() throws Exception {
+    try (CredentialBook<NoMetadata> book = CredentialBook.empty();
+        DeserializeResult<NoMetadata> original = NpAdv.deserializeAdvertisement(V1_PUBLIC, book);
+        V1AdvertisementBuilder builder = V1AdvertisementBuilder.newPublic()) {
+      for (DeserializedV1Section<NoMetadata> section : original.getAsV1().getSections()) {
+        try (V1SectionBuilder sectionBuilder = builder.addPublicSection()) {
+          for (V1DataElement de : section.getDataElements()) {
+            sectionBuilder.addDataElement(de);
+          }
+        }
+      }
+      byte[] adv = builder.build();
+
+      assertThat(adv).isEqualTo(V1_PUBLIC);
+    }
+  }
+
+  @Test
+  public void serializeAdvertisement_v1_cantCreatePubSectionInEncryptedAdv() throws Exception {
+    try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newEncrypted()) {
+      assertThrows(
+          SerializationException.InvalidSectionKindException.class,
+          () -> {
+            builder.addPublicSection().close();
+          });
+    }
+  }
+
+  @Test
+  public void serializeAdvertisement_v1_cantCreateEncryptedSectionInPublicAdv() throws Exception {
+    try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newPublic()) {
+      assertThrows(
+          SerializationException.InvalidSectionKindException.class,
+          () -> {
+            builder.addEncryptedSection(V1_BROADCAST_CRED, VerificationMode.SIGNATURE).close();
+          });
+    }
+  }
+
+  @Test
+  public void serializeAdvertisement_v0_canSerialize() throws Exception {
     try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
       builder.addDataElement(TX_POWER);
       builder.addDataElement(PUBLIC_ACTIONS);
@@ -51,7 +194,7 @@
   }
 
   @Test
-  void serializeAdvertisement_v0_canSerializePrivate() throws Exception {
+  public void serializeAdvertisement_v0_canSerializePrivate() throws Exception {
     try (V0AdvertisementBuilder builder =
         V0AdvertisementBuilder.newEncrypted(V0_BROADCAST_CRED, SALT)) {
       builder.addDataElement(TX_POWER);
@@ -62,15 +205,15 @@
   }
 
   @Test
-  void serializeAdvertisement_v0_canRoundtrip() throws Exception {
+  public void serializeAdvertisement_v0_canRoundtrip() throws Exception {
     try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic();
-        CredentialBook book = CredentialBook.empty()) {
+        CredentialBook<NoMetadata> book = CredentialBook.empty()) {
       builder.addDataElement(TX_POWER);
       builder.addDataElement(PUBLIC_ACTIONS);
       byte[] advBytes = builder.build();
 
-      DeserializeResult result = NpAdv.deserializeAdvertisement(advBytes, book);
-      DeserializedV0Advertisement adv = result.getAsV0();
+      DeserializeResult<NoMetadata> result = NpAdv.deserializeAdvertisement(advBytes, book);
+      DeserializedV0Advertisement<NoMetadata> adv = result.getAsV0();
 
       assertThat(adv).isNotNull();
       assertThat(adv.getDataElementCount()).isEqualTo(2);
@@ -87,17 +230,19 @@
   }
 
   @Test
-  void serializeAdvertisement_v0_canRoundtripPrivate() throws Exception {
+  public void serializeAdvertisement_v0_canRoundtripPrivate() throws Exception {
     try (V0AdvertisementBuilder builder =
             V0AdvertisementBuilder.newEncrypted(V0_BROADCAST_CRED, SALT);
-        CredentialBook book =
-            CredentialBook.builder().addDiscoveryCredential(V0_CRED, NoMetadata.INSTANCE).build()) {
+        CredentialBook<NoMetadata> book =
+            CredentialBook.<NoMetadata>builder()
+                .addDiscoveryCredential(V0_CRED, NoMetadata.INSTANCE)
+                .build()) {
       builder.addDataElement(TX_POWER);
       builder.addDataElement(PRIVATE_ACTIONS);
       byte[] advBytes = builder.build();
 
-      DeserializeResult result = NpAdv.deserializeAdvertisement(advBytes, book);
-      DeserializedV0Advertisement adv = result.getAsV0();
+      DeserializeResult<NoMetadata> result = NpAdv.deserializeAdvertisement(advBytes, book);
+      DeserializedV0Advertisement<NoMetadata> adv = result.getAsV0();
 
       assertThat(adv).isNotNull();
       assertThat(adv.getDataElementCount()).isEqualTo(2);
@@ -114,14 +259,14 @@
   }
 
   @Test
-  void serializeAdvertisement_v0_emptyIsError() throws Exception {
+  public void serializeAdvertisement_v0_emptyIsError() throws Exception {
     try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
       assertThrows(SerializationException.UnencryptedSizeException.class, () -> builder.build());
     }
   }
 
   @Test
-  void serializeAdvertisement_v0_fullIsError() throws Exception {
+  public void serializeAdvertisement_v0_fullIsError() throws Exception {
     try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
       assertThrows(
           SerializationException.InsufficientSpaceException.class,
@@ -135,7 +280,7 @@
   }
 
   @Test
-  void serializeAdvertisement_v0_publicAdvPrivateActionsIsError() throws Exception {
+  public void serializeAdvertisement_v0_publicAdvPrivateActionsIsError() throws Exception {
     try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
       assertThrows(
           SerializationException.InvalidDataElementException.class,
@@ -144,7 +289,7 @@
   }
 
   @Test
-  void serializeAdvertisement_v0_privateAdvPublicActionsIsError() throws Exception {
+  public void serializeAdvertisement_v0_privateAdvPublicActionsIsError() throws Exception {
     try (V0AdvertisementBuilder builder =
         V0AdvertisementBuilder.newEncrypted(V0_BROADCAST_CRED, SALT)) {
       assertThrows(
@@ -154,7 +299,7 @@
   }
 
   @Test
-  void serializeAdvertisement_v0_invalidTxPowerIsError() throws Exception {
+  public void serializeAdvertisement_v0_invalidTxPowerIsError() throws Exception {
     try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
       assertThrows(
           SerializationException.InvalidDataElementException.class,
@@ -163,18 +308,18 @@
   }
 
   @Test
-  void serializeAdvertisement_v0_handleIsConsumedByBuild() throws Exception {
+  public void serializeAdvertisement_v0_handleIsConsumedByBuild() throws Exception {
     try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
       builder.addDataElement(TX_POWER);
-      byte[] adv = builder.build();
+      byte[] unused = builder.build();
       assertThrows(Handle.InvalidHandleException.class, () -> builder.addDataElement(TX_POWER));
       assertThrows(Handle.InvalidHandleException.class, () -> builder.build());
     }
   }
 
   @Test
-  @Disabled("b/311225033: Duplicate data element spec change not implemented")
-  void serializeAdvertisement_v0_deNotAddedTwice() throws Exception {
+  @Ignore("b/311225033: Duplicate data element spec change not implemented")
+  public void serializeAdvertisement_v0_deNotAddedTwice() throws Exception {
     try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
       builder.addDataElement(TX_POWER);
       assertThrows(Exception.class, () -> builder.addDataElement(TX_POWER));
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestData.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestData.java
index 122b2a9..7408de6 100644
--- a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestData.java
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestData.java
@@ -18,10 +18,14 @@
 
 import com.google.android.nearby.presence.rust.credential.V0BroadcastCredential;
 import com.google.android.nearby.presence.rust.credential.V0DiscoveryCredential;
+import com.google.android.nearby.presence.rust.credential.V1BroadcastCredential;
 import com.google.android.nearby.presence.rust.credential.V1DiscoveryCredential;
 
+@SuppressWarnings("MutablePublicArray")
 public class TestData {
 
+  private TestData() {}
+
   public static final byte[] V0_PUBLIC = {
     0x00, // adv header
     0x15, 20, // tx power
@@ -141,67 +145,6 @@
     (byte) 0x68
   };
 
-  public static final byte[] V1_ENCRYPTED_ALICE_METADATA = {
-    (byte) 0x0c,
-    (byte) 0x27,
-    (byte) 0x43,
-    (byte) 0x58,
-    (byte) 0xb8,
-    (byte) 0xb9,
-    (byte) 0x90,
-    (byte) 0x72,
-    (byte) 0xb1,
-    (byte) 0xe8,
-    (byte) 0xba,
-    (byte) 0x7d,
-    (byte) 0x8e,
-    (byte) 0xdb,
-    (byte) 0xac,
-    (byte) 0x7e,
-    (byte) 0x15,
-    (byte) 0xd5,
-    (byte) 0x05,
-    (byte) 0x30,
-    (byte) 0x4d,
-    (byte) 0xb7,
-    (byte) 0xe0,
-    (byte) 0xbd,
-    (byte) 0x8f,
-    (byte) 0xe9,
-    (byte) 0xab,
-    (byte) 0x48,
-    (byte) 0x66,
-    (byte) 0xac,
-    (byte) 0x2c,
-    (byte) 0x8a,
-    (byte) 0x24,
-    (byte) 0x0c,
-    (byte) 0x02,
-    (byte) 0xb4,
-    (byte) 0xd0,
-    (byte) 0xb7,
-    (byte) 0x10,
-    (byte) 0xd6,
-    (byte) 0x30,
-    (byte) 0x12,
-    (byte) 0xd6,
-    (byte) 0x98,
-    (byte) 0xdc,
-    (byte) 0x55,
-    (byte) 0xae,
-    (byte) 0xb0,
-    (byte) 0x9c,
-    (byte) 0x9a,
-    (byte) 0x7d,
-    (byte) 0x2e,
-    (byte) 0xcb,
-    (byte) 0xba,
-    (byte) 0xdd,
-    (byte) 0x7d,
-    (byte) 0x4e,
-    (byte) 0xd1
-  };
-
   public static final byte[] V0_KEY_SEED = {
     0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
     0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
@@ -247,12 +190,6 @@
     (byte) 0xFD
   };
 
-  public static final V0DiscoveryCredential V0_CRED =
-      new V0DiscoveryCredential(V0_KEY_SEED, V0_IDENTITY_TOKEN_HMAC);
-
-  public static final V0BroadcastCredential V0_BROADCAST_CRED =
-      new V0BroadcastCredential(V0_KEY_SEED, V0_IDENTITY_TOKEN);
-
   public static final byte[] V0_PRIVATE = {
     0x04, // adv header
     0x22,
@@ -275,305 +212,559 @@
     (byte) 0xEC // ciphertext for metadata key & txpower DE
   };
 
-  public static final byte[] V1_IDENTITY_TOKEN = {
-    (byte) 0x58,
-    (byte) 0x31,
-    (byte) 0x00,
-    (byte) 0x48,
-    (byte) 0x11,
-    (byte) 0xe4,
-    (byte) 0xea,
-    (byte) 0x43,
-    (byte) 0xe9,
-    (byte) 0x01,
-    (byte) 0x76,
-    (byte) 0x25,
-    (byte) 0xd8,
-    (byte) 0xaf,
-    (byte) 0xd6,
-    (byte) 0x92
-  };
+  public static final V0DiscoveryCredential V0_CRED =
+      new V0DiscoveryCredential(V0_KEY_SEED, V0_IDENTITY_TOKEN_HMAC);
+
+  public static final V0BroadcastCredential V0_BROADCAST_CRED =
+      new V0BroadcastCredential(V0_KEY_SEED, V0_IDENTITY_TOKEN);
 
   public static final byte[] V1_KEY_SEED = {
-    (byte) 0xc8,
-    (byte) 0xdd,
-    (byte) 0x01,
-    (byte) 0x4d,
-    (byte) 0x25,
-    (byte) 0x01,
-    (byte) 0xc0,
-    (byte) 0xbf,
-    (byte) 0x5b,
-    (byte) 0x2a,
-    (byte) 0x05,
-    (byte) 0x48,
-    (byte) 0x49,
-    (byte) 0x8c,
-    (byte) 0xe6,
-    (byte) 0xbf,
-    (byte) 0x48,
-    (byte) 0x5b,
-    (byte) 0x89,
-    (byte) 0xb8,
-    (byte) 0x47,
-    (byte) 0x13,
-    (byte) 0xcc,
-    (byte) 0xdd,
-    (byte) 0xa0,
-    (byte) 0x18,
-    (byte) 0xac,
-    (byte) 0xd9,
-    (byte) 0xef,
-    (byte) 0x58,
-    (byte) 0x9f,
-    (byte) 0x76
-  };
-
-  public static final byte[] V1_MIC_SHORT_HMAC = {
-    (byte) 0x09,
-    (byte) 0x48,
-    (byte) 0x4e,
-    (byte) 0x8f,
+    (byte) 0x3B,
+    (byte) 0x4C,
+    (byte) 0x0E,
     (byte) 0x39,
-    (byte) 0xdc,
-    (byte) 0x16,
-    (byte) 0x27,
+    (byte) 0x95,
+    (byte) 0x92,
+    (byte) 0x47,
+    (byte) 0xC8,
     (byte) 0x85,
-    (byte) 0x0a,
-    (byte) 0xea,
-    (byte) 0xfc,
-    (byte) 0x84,
-    (byte) 0xf6,
-    (byte) 0x43,
-    (byte) 0x51,
-    (byte) 0x62,
-    (byte) 0x16,
-    (byte) 0xf1,
-    (byte) 0x8d,
-    (byte) 0xda,
-    (byte) 0xd3,
-    (byte) 0xbc,
-    (byte) 0xba,
-    (byte) 0x43,
-    (byte) 0xf1,
-    (byte) 0x62,
-    (byte) 0x4e,
-    (byte) 0xa7,
-    (byte) 0x09,
-    (byte) 0xda,
-    (byte) 0xde
-  };
-
-  public static final byte[] V1_MIC_LONG_HMAC = {
-    (byte) 0xb9,
-    (byte) 0x6a,
-    (byte) 0xd2,
-    (byte) 0x3e,
-    (byte) 0x8e,
-    (byte) 0x08,
-    (byte) 0xe0,
-    (byte) 0xf4,
-    (byte) 0xe9,
-    (byte) 0xba,
-    (byte) 0xe9,
-    (byte) 0xbb,
-    (byte) 0x3d,
-    (byte) 0xe3,
-    (byte) 0x2f,
-    (byte) 0xd1,
-    (byte) 0x14,
-    (byte) 0x3a,
-    (byte) 0x51,
-    (byte) 0x19,
-    (byte) 0x54,
-    (byte) 0xf8,
-    (byte) 0x66,
-    (byte) 0x9f,
-    (byte) 0xf6,
-    (byte) 0xdb,
-    (byte) 0xf6,
-    (byte) 0x03,
-    (byte) 0xf7,
-    (byte) 0x41,
-    (byte) 0x20,
-    (byte) 0xd7
-  };
-
-  public static final byte[] V1_SIG_HMAC = {
-    (byte) 0xc4,
-    (byte) 0x19,
-    (byte) 0x6e,
-    (byte) 0x84,
-    (byte) 0x95,
-    (byte) 0x3a,
-    (byte) 0x8a,
-    (byte) 0x97,
-    (byte) 0xb9,
-    (byte) 0xed,
-    (byte) 0xf0,
-    (byte) 0xba,
-    (byte) 0xd2,
-    (byte) 0x5d,
-    (byte) 0xa4,
+    (byte) 0xCC,
+    (byte) 0x87,
+    (byte) 0x3B,
+    (byte) 0x2E,
+    (byte) 0x70,
+    (byte) 0x75,
+    (byte) 0x87,
+    (byte) 0xB5,
+    (byte) 0x85,
+    (byte) 0x9C,
+    (byte) 0x35,
+    (byte) 0x27,
+    (byte) 0xD8,
+    (byte) 0x22,
+    (byte) 0xFD,
     (byte) 0x32,
-    (byte) 0xb1,
-    (byte) 0xf2,
-    (byte) 0x1a,
-    (byte) 0xf7,
-    (byte) 0x7d,
+    (byte) 0x0C,
+    (byte) 0x57,
+    (byte) 0xB3,
+    (byte) 0x40,
+    (byte) 0xE4,
+    (byte) 0x3E,
+    (byte) 0xAE
+  };
+
+  public static final byte[] V1_IDENTITY_TOKEN = {
     (byte) 0x95,
-    (byte) 0x8f,
-    (byte) 0xeb,
-    (byte) 0x5f,
-    (byte) 0xbe,
-    (byte) 0xfd,
-    (byte) 0x62,
-    (byte) 0xa7,
-    (byte) 0xc0,
-    (byte) 0x16,
-    (byte) 0x66
+    (byte) 0x71,
+    (byte) 0x75,
+    (byte) 0x38,
+    (byte) 0x49,
+    (byte) 0xEF,
+    (byte) 0x1F,
+    (byte) 0xA7,
+    (byte) 0x10,
+    (byte) 0x58,
+    (byte) 0xDD,
+    (byte) 0x2C,
+    (byte) 0xD5,
+    (byte) 0x7E,
+    (byte) 0xE6,
+    (byte) 0x14
+  };
+
+  public static final byte[] V1_PRIVATE_KEY = {
+    (byte) 0x21,
+    (byte) 0xD4,
+    (byte) 0x08,
+    (byte) 0x74,
+    (byte) 0x0F,
+    (byte) 0xD5,
+    (byte) 0x14,
+    (byte) 0x29,
+    (byte) 0xBF,
+    (byte) 0x52,
+    (byte) 0x3B,
+    (byte) 0x10,
+    (byte) 0x1B,
+    (byte) 0x23,
+    (byte) 0x23,
+    (byte) 0x80,
+    (byte) 0xCF,
+    (byte) 0xA6,
+    (byte) 0x86,
+    (byte) 0x80,
+    (byte) 0x8A,
+    (byte) 0x34,
+    (byte) 0xAC,
+    (byte) 0x06,
+    (byte) 0xC5,
+    (byte) 0x06,
+    (byte) 0x43,
+    (byte) 0x28,
+    (byte) 0x50,
+    (byte) 0xD5,
+    (byte) 0x9C,
+    (byte) 0x89
   };
 
   public static final byte[] V1_PUB_KEY = {
-    (byte) 0x3c,
-    (byte) 0x59,
-    (byte) 0xd7,
-    (byte) 0x30,
-    (byte) 0x58,
-    (byte) 0x8c,
-    (byte) 0x45,
-    (byte) 0x26,
-    (byte) 0x7e,
-    (byte) 0x52,
-    (byte) 0x29,
+    (byte) 0xC0,
+    (byte) 0x87,
+    (byte) 0x25,
+    (byte) 0xE9,
+    (byte) 0x0F,
+    (byte) 0x5A,
+    (byte) 0xD7,
+    (byte) 0xA4,
+    (byte) 0xD8,
+    (byte) 0x7E,
+    (byte) 0x9E,
+    (byte) 0x76,
+    (byte) 0xDF,
+    (byte) 0x10,
+    (byte) 0x44,
+    (byte) 0xE9,
+    (byte) 0x78,
+    (byte) 0xF2,
+    (byte) 0x80,
+    (byte) 0x0F,
+    (byte) 0x2B,
+    (byte) 0x43,
+    (byte) 0x03,
+    (byte) 0xCA,
+    (byte) 0x64,
+    (byte) 0x0A,
+    (byte) 0x7A,
+    (byte) 0xA1,
+    (byte) 0xE4,
+    (byte) 0x43,
+    (byte) 0x63,
+    (byte) 0xAF
+  };
+
+  public static final byte[] V1_MIC_SHORT_HMAC = {
+    (byte) 0x90,
+    (byte) 0x8D,
+    (byte) 0x6D,
+    (byte) 0x40,
+    (byte) 0x76,
+    (byte) 0x0E,
+    (byte) 0x13,
+    (byte) 0xC8,
+    (byte) 0x3C,
+    (byte) 0x76,
+    (byte) 0xD2,
+    (byte) 0xE4,
+    (byte) 0xED,
+    (byte) 0x8F,
+    (byte) 0xBD,
+    (byte) 0x83,
+    (byte) 0xED,
+    (byte) 0xEC,
+    (byte) 0xFD,
+    (byte) 0xCE,
+    (byte) 0x0A,
+    (byte) 0x90,
+    (byte) 0x85,
+    (byte) 0x47,
+    (byte) 0x5D,
+    (byte) 0xAA,
+    (byte) 0x3F,
+    (byte) 0xAE,
+    (byte) 0x6C,
+    (byte) 0x90,
+    (byte) 0x43,
+    (byte) 0x15
+  };
+
+  public static final byte[] V1_MIC_LONG_HMAC = {
+    (byte) 0x57,
+    (byte) 0x99,
     (byte) 0x54,
-    (byte) 0xca,
-    (byte) 0xc9,
-    (byte) 0xcb,
-    (byte) 0xca,
+    (byte) 0x4E,
+    (byte) 0xB2,
+    (byte) 0xB0,
+    (byte) 0xA1,
+    (byte) 0x99,
+    (byte) 0x2F,
+    (byte) 0x0D,
+    (byte) 0x13,
+    (byte) 0x25,
+    (byte) 0xAE,
+    (byte) 0xE0,
+    (byte) 0x8F,
+    (byte) 0xB3,
+    (byte) 0xE2,
+    (byte) 0x8F,
+    (byte) 0xD9,
+    (byte) 0x56,
+    (byte) 0x8B,
+    (byte) 0x70,
+    (byte) 0xDE,
+    (byte) 0x28,
+    (byte) 0x82,
+    (byte) 0x60,
+    (byte) 0xE7,
+    (byte) 0x71,
+    (byte) 0xA4,
+    (byte) 0x57,
+    (byte) 0xCE,
+    (byte) 0x9E
+  };
+
+  public static final byte[] V1_SIG_HMAC = {
+    (byte) 0x37,
+    (byte) 0x53,
+    (byte) 0x78,
+    (byte) 0x59,
+    (byte) 0x3F,
+    (byte) 0x8A,
+    (byte) 0x06,
+    (byte) 0x5B,
+    (byte) 0xE5,
+    (byte) 0x03,
+    (byte) 0x05,
+    (byte) 0xE7,
+    (byte) 0xC1,
+    (byte) 0xD0,
+    (byte) 0x29,
+    (byte) 0x3D,
+    (byte) 0x2C,
+    (byte) 0x3D,
+    (byte) 0xDA,
+    (byte) 0x43,
+    (byte) 0x8D,
+    (byte) 0x0A,
+    (byte) 0x29,
+    (byte) 0x23,
+    (byte) 0x58,
+    (byte) 0xAC,
+    (byte) 0x98,
+    (byte) 0x10,
+    (byte) 0x1A,
+    (byte) 0x0A,
+    (byte) 0xA6,
+    (byte) 0xB2
+  };
+
+  public static final byte[] V1_ALICE_METADATA = {
+    (byte) 0x7B,
+    (byte) 0x22,
+    (byte) 0x75,
+    (byte) 0x75,
+    (byte) 0x69,
+    (byte) 0x64,
+    (byte) 0x22,
+    (byte) 0x3A,
+    (byte) 0x22,
+    (byte) 0x33,
+    (byte) 0x37,
+    (byte) 0x38,
+    (byte) 0x38,
+    (byte) 0x34,
+    (byte) 0x35,
+    (byte) 0x65,
+    (byte) 0x31,
+    (byte) 0x2D,
+    (byte) 0x32,
+    (byte) 0x36,
+    (byte) 0x31,
+    (byte) 0x36,
+    (byte) 0x2D,
+    (byte) 0x34,
+    (byte) 0x32,
+    (byte) 0x30,
+    (byte) 0x64,
+    (byte) 0x2D,
+    (byte) 0x38,
+    (byte) 0x36,
+    (byte) 0x66,
+    (byte) 0x35,
+    (byte) 0x2D,
+    (byte) 0x36,
+    (byte) 0x37,
+    (byte) 0x34,
+    (byte) 0x31,
+    (byte) 0x37,
+    (byte) 0x37,
+    (byte) 0x61,
+    (byte) 0x37,
+    (byte) 0x35,
+    (byte) 0x30,
+    (byte) 0x34,
+    (byte) 0x64,
+    (byte) 0x22,
+    (byte) 0x2C,
+    (byte) 0x22,
+    (byte) 0x64,
+    (byte) 0x69,
+    (byte) 0x73,
+    (byte) 0x70,
+    (byte) 0x6C,
+    (byte) 0x61,
+    (byte) 0x79,
+    (byte) 0x5F,
+    (byte) 0x6E,
+    (byte) 0x61,
+    (byte) 0x6D,
+    (byte) 0x65,
+    (byte) 0x22,
+    (byte) 0x3A,
+    (byte) 0x22,
+    (byte) 0x41,
+    (byte) 0x6C,
+    (byte) 0x69,
+    (byte) 0x63,
+    (byte) 0x65,
+    (byte) 0x22,
+    (byte) 0x2C,
+    (byte) 0x22,
+    (byte) 0x6C,
+    (byte) 0x6F,
+    (byte) 0x63,
+    (byte) 0x61,
+    (byte) 0x74,
+    (byte) 0x69,
+    (byte) 0x6F,
+    (byte) 0x6E,
+    (byte) 0x22,
+    (byte) 0x3A,
+    (byte) 0x22,
+    (byte) 0x57,
+    (byte) 0x6F,
+    (byte) 0x6E,
+    (byte) 0x64,
+    (byte) 0x65,
     (byte) 0x72,
-    (byte) 0x94,
-    (byte) 0x24,
-    (byte) 0xd8,
-    (byte) 0xf5,
-    (byte) 0xa6,
-    (byte) 0x1e,
-    (byte) 0xcf,
+    (byte) 0x6C,
+    (byte) 0x61,
+    (byte) 0x6E,
+    (byte) 0x64,
+    (byte) 0x22,
+    (byte) 0x7D
+  };
+
+  public static final byte[] V1_ENCRYPTED_ALICE_METADATA = {
+    (byte) 0xFD,
+    (byte) 0xEC,
+    (byte) 0x0B,
+    (byte) 0xE8,
+    (byte) 0x79,
+    (byte) 0x1C,
+    (byte) 0x10,
+    (byte) 0x6D,
+    (byte) 0x33,
+    (byte) 0x11,
+    (byte) 0xC7,
+    (byte) 0xF3,
+    (byte) 0x9B,
+    (byte) 0x90,
+    (byte) 0x8A,
+    (byte) 0x5A,
+    (byte) 0x74,
+    (byte) 0x7F,
+    (byte) 0x9C,
+    (byte) 0x3C,
+    (byte) 0xC9,
+    (byte) 0xBC,
+    (byte) 0x90,
+    (byte) 0xC5,
+    (byte) 0x88,
+    (byte) 0xE0,
+    (byte) 0xED,
+    (byte) 0x28,
+    (byte) 0xDC,
+    (byte) 0x25,
+    (byte) 0xE5,
+    (byte) 0xD8,
+    (byte) 0x9C,
+    (byte) 0x28,
+    (byte) 0x0F,
+    (byte) 0x23,
+    (byte) 0x9D,
+    (byte) 0x21,
+    (byte) 0xC7,
+    (byte) 0xFF,
+    (byte) 0xE1,
+    (byte) 0x60,
+    (byte) 0x62,
+    (byte) 0x5B,
+    (byte) 0xBC,
+    (byte) 0x22,
+    (byte) 0xB4,
+    (byte) 0x3A,
+    (byte) 0x02,
+    (byte) 0x9D,
+    (byte) 0x69,
+    (byte) 0x53,
+    (byte) 0x2A,
+    (byte) 0x62,
+    (byte) 0xB0,
+    (byte) 0x82,
+    (byte) 0xC7,
+    (byte) 0x79,
+    (byte) 0x9E,
+    (byte) 0x30,
+    (byte) 0x76,
+    (byte) 0xB0,
+    (byte) 0x8A,
+    (byte) 0xC3,
+    (byte) 0xCD,
+    (byte) 0x11,
+    (byte) 0xEA,
+    (byte) 0x45,
+    (byte) 0x4A,
+    (byte) 0x3C,
+    (byte) 0x25,
+    (byte) 0x45,
+    (byte) 0xD9,
+    (byte) 0x44,
+    (byte) 0x5B,
+    (byte) 0xE3,
+    (byte) 0xA0,
+    (byte) 0x3E,
     (byte) 0x04,
-    (byte) 0x3e,
-    (byte) 0x8f,
-    (byte) 0x91,
+    (byte) 0x79,
+    (byte) 0x6B,
+    (byte) 0xC3,
+    (byte) 0x62,
+    (byte) 0xA4,
+    (byte) 0xA1,
+    (byte) 0xF4,
+    (byte) 0x76,
+    (byte) 0x58,
+    (byte) 0x42,
+    (byte) 0xDC,
+    (byte) 0x37,
+    (byte) 0x93,
     (byte) 0x81,
-    (byte) 0x6d,
-    (byte) 0x19,
-    (byte) 0x74
+    (byte) 0x60,
+    (byte) 0x8E,
+    (byte) 0x00,
+    (byte) 0x6D,
+    (byte) 0xE0,
+    (byte) 0x22,
+    (byte) 0x82,
+    (byte) 0x98,
+    (byte) 0x40,
+    (byte) 0x59,
+    (byte) 0x41,
+    (byte) 0xF9,
+    (byte) 0x88,
+    (byte) 0xB8,
+    (byte) 0xFB,
+    (byte) 0x9E,
+    (byte) 0xED
+  };
+
+  public static final byte[] V1_PRIVATE = {
+    (byte) 0x20,
+    (byte) 0x03,
+    (byte) 0x6C,
+    (byte) 0x94,
+    (byte) 0x77,
+    (byte) 0x9A,
+    (byte) 0xF9,
+    (byte) 0x89,
+    (byte) 0x83,
+    (byte) 0x9F,
+    (byte) 0x41,
+    (byte) 0x98,
+    (byte) 0x33,
+    (byte) 0x68,
+    (byte) 0x24,
+    (byte) 0x9A,
+    (byte) 0xCF,
+    (byte) 0xAC,
+    (byte) 0x8E,
+    (byte) 0xC3,
+    (byte) 0x46,
+    (byte) 0x02,
+    (byte) 0x30,
+    (byte) 0x89,
+    (byte) 0x68,
+    (byte) 0x99,
+    (byte) 0xA2,
+    (byte) 0xF5,
+    (byte) 0x32,
+    (byte) 0x36,
+    (byte) 0x06,
+    (byte) 0x18,
+    (byte) 0x0D,
+    (byte) 0xF6,
+    (byte) 0x42,
+    (byte) 0x52,
+    (byte) 0xA1,
+    (byte) 0x59,
+    (byte) 0x46,
+    (byte) 0xC8,
+    (byte) 0xD1,
+    (byte) 0x27,
+    (byte) 0x74,
+    (byte) 0xFF,
+    (byte) 0xBF,
+    (byte) 0x53,
+    (byte) 0xFE,
+    (byte) 0x51,
+    (byte) 0xCB,
+    (byte) 0x39,
+    (byte) 0x6D,
+    (byte) 0x28,
+    (byte) 0x3C,
+    (byte) 0x7E,
+    (byte) 0xD9,
+    (byte) 0x6F,
+    (byte) 0xB0,
+    (byte) 0x70,
+    (byte) 0x82,
+    (byte) 0x26,
+    (byte) 0x51,
+    (byte) 0x11,
+    (byte) 0xF2,
+    (byte) 0x90,
+    (byte) 0xC0,
+    (byte) 0xBE,
+    (byte) 0x34,
+    (byte) 0x96,
+    (byte) 0x57,
+    (byte) 0x1F,
+    (byte) 0x4F,
+    (byte) 0xC0,
+    (byte) 0x87,
+    (byte) 0xF0,
+    (byte) 0xA8,
+    (byte) 0x0D,
+    (byte) 0xD8,
+    (byte) 0xE3,
+    (byte) 0xC7,
+    (byte) 0x8C,
+    (byte) 0xAA,
+    (byte) 0x7E,
+    (byte) 0x41,
+    (byte) 0x14,
+    (byte) 0x58,
+    (byte) 0xF4,
+    (byte) 0xD3,
+    (byte) 0x8E,
+    (byte) 0x8E,
+    (byte) 0xA0,
+    (byte) 0x57,
+    (byte) 0xF4,
+    (byte) 0x63,
+    (byte) 0x43,
+    (byte) 0x74,
+    (byte) 0x08,
+    (byte) 0xC7,
+    (byte) 0xB8,
+    (byte) 0x51,
+    (byte) 0x66,
+    (byte) 0xB9
   };
 
   public static final V1DiscoveryCredential V1_CRED =
       new V1DiscoveryCredential(
           V1_KEY_SEED, V1_MIC_SHORT_HMAC, V1_MIC_LONG_HMAC, V1_SIG_HMAC, V1_PUB_KEY);
 
-  public static final byte[] V1_PRIVATE = {
-    (byte) 0x20,
-    (byte) 0x03,
-    (byte) 0xfc,
-    (byte) 0x32,
-    (byte) 0xb7,
-    (byte) 0x5d,
-    (byte) 0xdd,
-    (byte) 0x6a,
-    (byte) 0xdb,
-    (byte) 0xb0,
-    (byte) 0x89,
-    (byte) 0x7d,
-    (byte) 0xb9,
-    (byte) 0xcd,
-    (byte) 0xa9,
-    (byte) 0x6e,
-    (byte) 0x73,
-    (byte) 0x6d,
-    (byte) 0x7a,
-    (byte) 0xfc,
-    (byte) 0xeb,
-    (byte) 0x2b,
-    (byte) 0x0c,
-    (byte) 0x02,
-    (byte) 0x3d,
-    (byte) 0xc8,
-    (byte) 0xfa,
-    (byte) 0xc8,
-    (byte) 0x78,
-    (byte) 0x83,
-    (byte) 0x56,
-    (byte) 0xfa,
-    (byte) 0x53,
-    (byte) 0x11,
-    (byte) 0x42,
-    (byte) 0x08,
-    (byte) 0x9e,
-    (byte) 0xfe,
-    (byte) 0x70,
-    (byte) 0xd0,
-    (byte) 0x68,
-    (byte) 0x6c,
-    (byte) 0x7c,
-    (byte) 0x29,
-    (byte) 0x86,
-    (byte) 0xd6,
-    (byte) 0x76,
-    (byte) 0x2b,
-    (byte) 0x03,
-    (byte) 0xa4,
-    (byte) 0xc7,
-    (byte) 0x47,
-    (byte) 0x5c,
-    (byte) 0x41,
-    (byte) 0x9d,
-    (byte) 0x21,
-    (byte) 0x15,
-    (byte) 0x54,
-    (byte) 0x89,
-    (byte) 0x43,
-    (byte) 0x32,
-    (byte) 0x44,
-    (byte) 0x47,
-    (byte) 0x34,
-    (byte) 0xd7,
-    (byte) 0xbd,
-    (byte) 0x4f,
-    (byte) 0x38,
-    (byte) 0x83,
-    (byte) 0x74,
-    (byte) 0xe4,
-    (byte) 0xdb,
-    (byte) 0xcf,
-    (byte) 0xfe,
-    (byte) 0xe4,
-    (byte) 0x7a,
-    (byte) 0xae,
-    (byte) 0xa8,
-    (byte) 0xe2,
-    (byte) 0xf5,
-    (byte) 0x69,
-    (byte) 0xb8,
-    (byte) 0x42,
-    (byte) 0xf5,
-    (byte) 0x67,
-    (byte) 0x7a,
-    (byte) 0x34,
-    (byte) 0x6d,
-    (byte) 0x86,
-    (byte) 0x8b,
-    (byte) 0x4c,
-    (byte) 0xa9,
-    (byte) 0x7f,
-    (byte) 0x45,
-    (byte) 0x1c,
-    (byte) 0x37,
-    (byte) 0xf1,
-    (byte) 0x6e,
-    (byte) 0xfc,
-    (byte) 0xae,
-    (byte) 0xc6
-  };
+  public static final V1BroadcastCredential V1_BROADCAST_CRED =
+      new V1BroadcastCredential(V1_KEY_SEED, V1_IDENTITY_TOKEN, V1_PRIVATE_KEY);
 }
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestVectors.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestVectors.java
new file mode 100644
index 0000000..c496023
--- /dev/null
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestVectors.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.android.nearby.presence.rust.credential.V1BroadcastCredential;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.reflect.TypeToken;
+import java.io.BufferedReader;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class TestVectors {
+
+  static final class DataElement {
+    byte[] contents;
+    int deType;
+  }
+
+  static final class TestVector {
+    byte[] keySeed;
+    byte[] identityToken;
+    byte[] advHeaderByte;
+    byte[] sectionSalt;
+    DataElement[] dataElements;
+    byte[] aesKey;
+    byte[] sectionMicHmacKey;
+    byte[] nonce;
+    byte[] encodedSection;
+  }
+
+  static final class HexArrayDeserializer implements JsonDeserializer<byte[]> {
+
+    private static final byte byteFromHexChar(char c) {
+      if (c >= '0' && c <= '9') {
+        return (byte) (c - '0');
+      } else if (c >= 'a' && c <= 'f') {
+        return (byte) (c - 'a' + 0x0a);
+      } else if (c >= 'A' && c <= 'F') {
+        return (byte) (c - 'A' + 0x0a);
+      } else {
+        throw new IllegalArgumentException("Invalid hex char: " + c);
+      }
+    }
+
+    @Override
+    public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      String hex = json.getAsString();
+      int byteLen = hex.length() / 2;
+      byte[] out = new byte[byteLen];
+
+      for (int i = 0; i < byteLen; i++) {
+        int offset = i * 2;
+        out[i] =
+            (byte)
+                ((byteFromHexChar(hex.charAt(offset)) << 4)
+                    | byteFromHexChar(hex.charAt(offset + 1)));
+      }
+
+      return out;
+    }
+  }
+
+  static Gson createJsonParser() {
+    return new GsonBuilder()
+        .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+        .registerTypeAdapter(byte[].class, new HexArrayDeserializer())
+        .create();
+  }
+
+  @Test
+  public void micExtendedSaltEncryptedTestVectors() throws Exception {
+    Gson gson = createJsonParser();
+    TestVector[] testVectors = null;
+    try (BufferedReader br =
+        Files.newBufferedReader(
+            Paths.get(
+                "..",
+                "np_adv",
+                "resources",
+                "test",
+                "mic-extended-salt-encrypted-test-vectors.json"))) {
+      testVectors = gson.fromJson(br, new TypeToken<TestVector[]>() {});
+    }
+    assertThat(testVectors).isNotNull();
+
+    for (TestVector tv : testVectors) {
+      byte[] privateKey = new byte[32];
+      SecureRandom.getInstanceStrong().nextBytes(privateKey);
+      V1BroadcastCredential credential =
+          new V1BroadcastCredential(tv.keySeed, tv.identityToken, privateKey);
+      try (V1AdvertisementBuilder builder = V1AdvertisementBuilder.newEncrypted();
+          V1SectionBuilder section =
+              nativeAddSaltedSection(builder.builder, credential, tv.sectionSalt)) {
+        for (DataElement de : tv.dataElements) {
+          section.addDataElement(new V1DataElement.Generic(de.deType, de.contents));
+        }
+        section.finishSection();
+
+        byte[] adv = builder.build();
+        byte[] sectionBytes = Arrays.copyOfRange(adv, 1, adv.length);
+
+        assertThat(sectionBytes).isEqualTo(tv.encodedSection);
+      }
+    }
+  }
+
+  @Test
+  public void createJsonParser_canParseTestVector() {
+    Gson gson = createJsonParser();
+    String testJson =
+        "{ \"adv_header_byte\": \"20\", \"aes_key\": \"F3DB017C70E08EC5178C92F3AEA0C362\","
+            + " \"data_elements\": [ { \"contents\": \"CF75D23EDA8F6E4A23\", \"de_type\": 383 }, {"
+            + " \"contents\": \"731B76151735869205CC41\", \"de_type\": 73 }, { \"contents\":"
+            + " \"7C2A8DE86B2CBB997703\", \"de_type\": 228 }, { \"contents\":"
+            + " \"99F5163DCA0BB9BE89755A6C5AB321\", \"de_type\": 446 } ], \"encoded_section\":"
+            + " \"6D91100056F596D16E1F87B107EE86102FFC6D5E9002F00C41CDDF533667362CD14AC54A9388FA3D30AA7CA6603071B8B0FA19BF582479F773F1C7D0EADF98E98B9447139F244D571B780475ACD9CB248F33B2C085925213360732D44081C27CA6EB40BD8A626BC776D88C5FDB09\","
+            + " \"key_seed\": \"F0ED9126768CE7DC685FF74932AC5A876442C4E42359A43F720A575142A45043\","
+            + " \"identity_token\": \"45795EE4C6533A830886E2C5885EB9E5\", \"nonce\":"
+            + " \"40E95D525FAEA1C1FEE39A8E\", \"section_mic_hmac_key\":"
+            + " \"A22386E85112EF883218A5B75669B7102E017E9AA149F408A079F60B1D14B4F4\","
+            + " \"section_salt\": \"56F596D16E1F87B107EE86102FFC6D5E\" }";
+
+    TestVector testVector = gson.fromJson(testJson, TestVector.class);
+
+    assertThat(testVector.aesKey)
+        .isEqualTo(
+            new byte[] {
+              (byte) 0xF3,
+              (byte) 0xDB,
+              (byte) 0x01,
+              (byte) 0x7C,
+              (byte) 0x70,
+              (byte) 0xE0,
+              (byte) 0x8E,
+              (byte) 0xC5,
+              (byte) 0x17,
+              (byte) 0x8C,
+              (byte) 0x92,
+              (byte) 0xF3,
+              (byte) 0xAE,
+              (byte) 0xA0,
+              (byte) 0xC3,
+              (byte) 0x62
+            });
+  }
+
+  // Expose the test-only salted section API
+  private static native V1SectionBuilder nativeAddSaltedSection(
+      V1AdvertisementBuilder.V1BuilderHandle builder,
+      V1BroadcastCredential credential,
+      byte[] salt);
+}
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/credential/CredentialBookTests.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/credential/CredentialBookTests.java
deleted file mode 100644
index 12302d3..0000000
--- a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/credential/CredentialBookTests.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2023 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.android.nearby.presence.rust.credential;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.*;
-
-import java.lang.ref.Cleaner;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-@ExtendWith(MockitoExtension.class)
-public class CredentialBookTests {
-
-  @Mock Cleaner cleaner;
-
-  @Test
-  void CredentialBook_wasRegisteredWithCleaner() {
-    try (CredentialBook book = new CredentialBook.Builder(cleaner).build()) {
-      assertThat(book).isNotNull();
-      verify(cleaner).register(same(book), any());
-    }
-  }
-}
diff --git a/nearby/presence/rand_ext/Cargo.toml b/nearby/presence/rand_ext/Cargo.toml
index 79a8ea8..da5744e 100644
--- a/nearby/presence/rand_ext/Cargo.toml
+++ b/nearby/presence/rand_ext/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/sink/Cargo.toml b/nearby/presence/sink/Cargo.toml
index a479205..60727c0 100644
--- a/nearby/presence/sink/Cargo.toml
+++ b/nearby/presence/sink/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/test_helper/Cargo.toml b/nearby/presence/test_helper/Cargo.toml
index 340a843..6fd2e3a 100644
--- a/nearby/presence/test_helper/Cargo.toml
+++ b/nearby/presence/test_helper/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
@@ -11,3 +12,7 @@
 hex.workspace = true
 serde_json.workspace = true
 itertools.workspace = true
+
+[features]
+default = ["std"]
+std = ["itertools/use_std"]
diff --git a/nearby/presence/test_helper/src/lib.rs b/nearby/presence/test_helper/src/lib.rs
index 76dccf4..41446fd 100644
--- a/nearby/presence/test_helper/src/lib.rs
+++ b/nearby/presence/test_helper/src/lib.rs
@@ -73,6 +73,7 @@
 ///
 /// assert_eq!("0x12, 0x34", hex_bytes(&[0x12, 0x34]));
 /// ```
+#[cfg(feature = "std")]
 pub fn hex_bytes(data: impl AsRef<[u8]>) -> String {
     hex::encode_upper(data).chars().tuples().map(|(a, b)| format!("0x{}{}", a, b)).join(", ")
 }
diff --git a/nearby/presence/test_vector_hkdf/Cargo.toml b/nearby/presence/test_vector_hkdf/Cargo.toml
index cf5d1a2..76d1e6c 100644
--- a/nearby/presence/test_vector_hkdf/Cargo.toml
+++ b/nearby/presence/test_vector_hkdf/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/xts_aes/Cargo.toml b/nearby/presence/xts_aes/Cargo.toml
index 86745c4..b003951 100644
--- a/nearby/presence/xts_aes/Cargo.toml
+++ b/nearby/presence/xts_aes/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 [lints]
 workspace = true
diff --git a/nearby/presence/xts_aes/fuzz/Cargo.toml b/nearby/presence/xts_aes/fuzz/Cargo.toml
index 5d99173..a231176 100644
--- a/nearby/presence/xts_aes/fuzz/Cargo.toml
+++ b/nearby/presence/xts_aes/fuzz/Cargo.toml
@@ -4,6 +4,7 @@
 authors = ["Automatically generated"]
 publish = false
 edition = "2018"
+license = "Apache-2.0"
 
 [package.metadata]
 cargo-fuzz = true
@@ -19,6 +20,9 @@
 [target.'cfg(fuzzing)'.dependencies]
 libfuzzer-sys.workspace = true
 
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
+
 [[bin]]
 name = "xts_roundtrip"
 path = "src/bin/xts_roundtrip.rs"
diff --git a/nearby/src/fuzzers.rs b/nearby/src/fuzzers.rs
deleted file mode 100644
index e809518..0000000
--- a/nearby/src/fuzzers.rs
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-use cmd_runner::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
-use std::{fs, path};
-
-pub(crate) fn run_rust_fuzzers(root: &path::Path) -> anyhow::Result<()> {
-    log::info!("Running rust fuzzers");
-    run_cmd_shell_with_color::<YellowStderr>(
-        &root.join("presence/xts_aes"),
-        "cargo +nightly fuzz run xts_roundtrip -- -runs=10000 -max_total_time=60",
-    )?;
-    run_cmd_shell_with_color::<YellowStderr>(
-        &root.join("presence/ldt"),
-        "cargo +nightly fuzz run ldt_roundtrip -- -runs=10000 -max_total_time=60",
-    )?;
-    run_cmd_shell_with_color::<YellowStderr>(
-        &root.join("presence/ldt_np_adv"),
-        "cargo +nightly fuzz run ldt_np_decrypt -- -runs=10000 -max_total_time=60",
-    )?;
-    run_cmd_shell_with_color::<YellowStderr>(
-        &root.join("presence/ldt_np_adv"),
-        "cargo +nightly fuzz run ldt_np_roundtrip -- -runs=10000 -max_total_time=60",
-    )?;
-    run_cmd_shell_with_color::<YellowStderr>(
-        &root.join("connections/ukey2/ukey2_connections"),
-        "cargo +nightly fuzz run fuzz_connection -- -runs=10000 -max_total_time=60",
-    )?;
-    run_cmd_shell_with_color::<YellowStderr>(
-        &root.join("connections/ukey2/ukey2_connections"),
-        "cargo +nightly fuzz run fuzz_from_saved_session -- -runs=10000 -max_total_time=60",
-    )?;
-    run_cmd_shell_with_color::<YellowStderr>(
-        &root.join("connections/ukey2/ukey2_connections"),
-        "cargo +nightly fuzz run fuzz_handshake -- -runs=10000 -max_total_time=60",
-    )?;
-    run_cmd_shell_with_color::<YellowStderr>(
-        &root.join("crypto/crypto_provider_test"),
-        "cargo +nightly fuzz run fuzz_p256 -- -runs=10000 -max_total_time=60",
-    )?;
-    run_cmd_shell_with_color::<YellowStderr>(
-        &root.join("crypto/crypto_provider_test"),
-        concat!(
-            "cargo +nightly fuzz run fuzz_p256 --features=boringssl --no-default-features ",
-            "-- -runs=10000 -max_total_time=60"
-        ),
-    )?;
-
-    Ok(())
-}
-
-// Runs the fuzztest fuzzers as short lived unit tests, compatible with gtest
-pub(crate) fn build_fuzztest_unit_tests(root: &path::Path) -> anyhow::Result<()> {
-    log::info!("Checking fuzztest targets in unit test mode");
-    run_cmd_shell(root, "cargo build -p np_c_ffi --release")?;
-    run_cmd_shell(root, "cargo build -p ldt_np_adv_ffi --release")?;
-    let build_dir = root.join("cmake-build");
-    fs::create_dir_all(&build_dir)?;
-    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake -G Ninja -DENABLE_FUZZ=true ..")?;
-
-    for target in ["deserialization_fuzzer", "ldt_fuzzer"] {
-        run_cmd_shell_with_color::<YellowStderr>(
-            &build_dir,
-            format!("cmake --build . --target {}", target),
-        )?;
-    }
-
-    run_cmd_shell_with_color::<YellowStderr>(
-        &build_dir.join("presence/np_cpp_ffi/fuzz/"),
-        "ctest",
-    )?;
-    run_cmd_shell_with_color::<YellowStderr>(
-        &build_dir.join("presence/ldt_np_adv_ffi/c/fuzz/"),
-        "ctest",
-    )?;
-    Ok(())
-}
diff --git a/remoteauth/Cargo.lock b/remoteauth/Cargo.lock
index 7e17954..ab31216 100644
--- a/remoteauth/Cargo.lock
+++ b/remoteauth/Cargo.lock
@@ -58,9 +58,9 @@
 
 [[package]]
 name = "anstyle-query"
-version = "1.0.3"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
+checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
 dependencies = [
  "windows-sys",
 ]
@@ -77,9 +77,9 @@
 
 [[package]]
 name = "anyhow"
-version = "1.0.83"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 
 [[package]]
 name = "autocfg"
@@ -98,7 +98,7 @@
 ]
 
 [[package]]
-name = "build-scripts"
+name = "build_scripts"
 version = "0.1.0"
 dependencies = [
  "anyhow",
@@ -116,9 +116,9 @@
 
 [[package]]
 name = "cc"
-version = "1.0.97"
+version = "1.0.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
+checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695"
 
 [[package]]
 name = "cfg-if"
@@ -142,9 +142,9 @@
 
 [[package]]
 name = "clap"
-version = "4.5.4"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -152,9 +152,9 @@
 
 [[package]]
 name = "clap_builder"
-version = "4.5.2"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
 dependencies = [
  "anstream",
  "anstyle",
@@ -164,9 +164,9 @@
 
 [[package]]
 name = "clap_derive"
-version = "4.5.4"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -176,9 +176,9 @@
 
 [[package]]
 name = "clap_lex"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
 
 [[package]]
 name = "cmd_runner"
@@ -191,6 +191,8 @@
  "globset",
  "log",
  "owo-colors",
+ "serde",
+ "serde_json",
  "shell-escape",
  "xshell",
 ]
@@ -222,9 +224,9 @@
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.12"
+version = "0.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
 dependencies = [
  "crossbeam-utils",
 ]
@@ -259,9 +261,9 @@
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
 
 [[package]]
 name = "ctap_protocol"
@@ -390,15 +392,15 @@
 
 [[package]]
 name = "libc"
-version = "0.2.154"
+version = "0.2.155"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 
 [[package]]
 name = "license"
-version = "3.3.1"
+version = "3.4.0+3.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bba2f02ee1d13cd4bea565658939cd851d70e391f34f7c27b45b2077df3a2e4"
+checksum = "a7da1e0d845faf299a9fe5f201a918a0dc0d5fc22c7b9580a6a23fed3a912b37"
 dependencies = [
  "reword",
  "serde",
@@ -413,9 +415,9 @@
 
 [[package]]
 name = "memchr"
-version = "2.7.2"
+version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "num-traits"
@@ -447,9 +449,9 @@
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.82"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
 dependencies = [
  "unicode-ident",
 ]
@@ -465,9 +467,9 @@
 
 [[package]]
 name = "regex"
-version = "1.10.4"
+version = "1.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -477,9 +479,9 @@
 
 [[package]]
 name = "regex-automata"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -488,9 +490,9 @@
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
 
 [[package]]
 name = "remote_auth_protocol"
@@ -525,18 +527,18 @@
 
 [[package]]
 name = "serde"
-version = "1.0.200"
+version = "1.0.203"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.200"
+version = "1.0.203"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -545,9 +547,9 @@
 
 [[package]]
 name = "serde_json"
-version = "1.0.116"
+version = "1.0.118"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
+checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
 dependencies = [
  "itoa",
  "ryu",
@@ -568,9 +570,9 @@
 
 [[package]]
 name = "syn"
-version = "2.0.61"
+version = "2.0.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
+checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -588,18 +590,18 @@
 
 [[package]]
 name = "thiserror"
-version = "1.0.60"
+version = "1.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.60"
+version = "1.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -620,9 +622,9 @@
 
 [[package]]
 name = "utf8parse"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "walkdir"
diff --git a/remoteauth/Cargo.toml b/remoteauth/Cargo.toml
index d86adc0..68580eb 100644
--- a/remoteauth/Cargo.toml
+++ b/remoteauth/Cargo.toml
@@ -1,27 +1,18 @@
 [workspace]
 members = [
+    "build_scripts",
     "ctap_protocol",
     "platform",
     "remote_auth_protocol",
 ]
+default-members = ["build_scripts"]
+resolver = "2"
 
 [workspace.package]
 version = "0.1.0"
 edition = "2021"
 publish = false
+license = "Apache-2.0"
 
 [workspace.dependencies]
 anyhow = "1.0.72"
-
-[package]
-name = "build-scripts"
-version.workspace = true
-edition.workspace = true
-publish.workspace = true
-
-[dependencies]
-anyhow.workspace = true
-clap = { version = "4.0.25", features = ["derive"] }
-cmd_runner = { path = "../common/cmd_runner"  }
-env_logger = "0.10.0"
-log = "0.4.17"
\ No newline at end of file
diff --git a/remoteauth/build_scripts/Cargo.toml b/remoteauth/build_scripts/Cargo.toml
new file mode 100644
index 0000000..604a617
--- /dev/null
+++ b/remoteauth/build_scripts/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "build_scripts"
+version.workspace = true
+edition.workspace = true
+publish.workspace = true
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+clap = { version = "4.5.13", features = ["derive"] }
+cmd_runner = { path = "../../common/cmd_runner"  }
+env_logger = "0.10.0"
+log = "0.4.17"
diff --git a/remoteauth/src/ctap_protocol.rs b/remoteauth/build_scripts/src/ctap_protocol.rs
similarity index 100%
rename from remoteauth/src/ctap_protocol.rs
rename to remoteauth/build_scripts/src/ctap_protocol.rs
diff --git a/remoteauth/src/main.rs b/remoteauth/build_scripts/src/main.rs
similarity index 83%
rename from remoteauth/src/main.rs
rename to remoteauth/build_scripts/src/main.rs
index e0851eb..2caf8c0 100644
--- a/remoteauth/src/main.rs
+++ b/remoteauth/build_scripts/src/main.rs
@@ -17,7 +17,10 @@
 use clap::Parser as _;
 use cmd_runner::run_cmd_shell;
 use env_logger::Env;
-use std::{env, path};
+use std::{
+    env,
+    path::{self, PathBuf},
+};
 
 mod ctap_protocol;
 mod platform;
@@ -27,17 +30,20 @@
     env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
     let cli: Cli = Cli::parse();
 
-    let root_dir: path::PathBuf = env::var("CARGO_MANIFEST_DIR")
-        .expect("Must be run via Cargo to establish root directory")
-        .into();
+    let build_scripts_dir = PathBuf::from(
+        env::var("CARGO_MANIFEST_DIR").expect("Must be run via Cargo to establish root directory"),
+    );
+    let root_dir = build_scripts_dir
+        .parent()
+        .expect("build_scripts directory should have a parent");
 
     match cli.subcommand {
-        Subcommand::CheckEverything(ref options) => check_everything(&root_dir, options)?,
-        Subcommand::CheckWorkspace(ref options) => check_workspace(&root_dir, options)?,
-        Subcommand::CheckCtapProtocol => ctap_protocol::check_ctap_protocol(&root_dir)?,
-        Subcommand::CheckPlatform => platform::check_platform(&root_dir)?,
+        Subcommand::CheckEverything(ref options) => check_everything(root_dir, options)?,
+        Subcommand::CheckWorkspace(ref options) => check_workspace(root_dir, options)?,
+        Subcommand::CheckCtapProtocol => ctap_protocol::check_ctap_protocol(root_dir)?,
+        Subcommand::CheckPlatform => platform::check_platform(root_dir)?,
         Subcommand::CheckRemoteAuthProtocol => {
-            remote_auth_protocol::check_remote_auth_protocol(&root_dir)?
+            remote_auth_protocol::check_remote_auth_protocol(root_dir)?
         }
     }
 
diff --git a/remoteauth/src/platform.rs b/remoteauth/build_scripts/src/platform.rs
similarity index 100%
rename from remoteauth/src/platform.rs
rename to remoteauth/build_scripts/src/platform.rs
diff --git a/remoteauth/src/remote_auth_protocol.rs b/remoteauth/build_scripts/src/remote_auth_protocol.rs
similarity index 100%
rename from remoteauth/src/remote_auth_protocol.rs
rename to remoteauth/build_scripts/src/remote_auth_protocol.rs
diff --git a/remoteauth/ctap_protocol/Cargo.toml b/remoteauth/ctap_protocol/Cargo.toml
index 8a06840..d5ed9c2 100644
--- a/remoteauth/ctap_protocol/Cargo.toml
+++ b/remoteauth/ctap_protocol/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
diff --git a/remoteauth/deny.toml b/remoteauth/deny.toml
index 2bf5920..7f0eda7 100644
--- a/remoteauth/deny.toml
+++ b/remoteauth/deny.toml
@@ -9,24 +9,6 @@
 # The values provided in this template are the default values that will be used
 # when any section or field is not specified in your own configuration
 
-# If 1 or more target triples (and optionally, target_features) are specified,
-# only the specified targets will be checked when running `cargo deny check`.
-# This means, if a particular package is only ever used as a target specific
-# dependency, such as, for example, the `nix` crate only being used via the
-# `target_family = "unix"` configuration, that only having windows targets in
-# this list would mean the nix crate, as well as any of its exclusive
-# dependencies not shared by any other crates, would be ignored, as the target
-# list here is effectively saying which targets you are building for.
-targets = [
-    # The triple can be any string, but only the target triples built in to
-    # rustc (as of 1.40) can be checked against actual config expressions
-    #{ triple = "x86_64-unknown-linux-musl" },
-    # You can also specify which target_features you promise are enabled for a
-    # particular target. target_features are currently not validated against
-    # the actual valid features supported by the target architecture.
-    #{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
-]
-
 # This section is considered when running `cargo deny check advisories`
 # More documentation for the advisories section can be found here:
 # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
@@ -35,16 +17,8 @@
 db-path = "~/.cargo/advisory-db"
 # The url(s) of the advisory databases to use
 db-urls = ["https://github.com/rustsec/advisory-db"]
-# The lint level for security vulnerabilities
-vulnerability = "deny"
-# The lint level for unmaintained crates
-unmaintained = "warn"
 # The lint level for crates that have been yanked from their source registry
 yanked = "warn"
-# The lint level for crates with security notices. Note that as of
-# 2019-12-17 there are no security notice advisories in
-# https://github.com/rustsec/advisory-db
-notice = "warn"
 # A list of advisory IDs to ignore. Note that ignored advisories will still
 # output a note when they are encountered.
 ignore = [
@@ -70,8 +44,6 @@
 # More documentation for the licenses section can be found here:
 # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
 [licenses]
-# The lint level for crates which do not have a detectable license
-unlicensed = "deny"
 unused-allowed-license = "allow"
 # List of explicitly allowed licenses
 # See https://spdx.org/licenses/ for list of possible licenses
@@ -87,26 +59,6 @@
     "OpenSSL",
     "Unlicense"
 ]
-# List of explicitly disallowed licenses
-# See https://spdx.org/licenses/ for list of possible licenses
-# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
-deny = [
-    #"Nokia",
-]
-# Lint level for licenses considered copyleft
-copyleft = "warn"
-# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
-# * both - The license will be approved if it is both OSI-approved *AND* FSF
-# * either - The license will be approved if it is either OSI-approved *OR* FSF
-# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
-# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
-# * neither - This predicate is ignored and the default lint level is used
-allow-osi-fsf-free = "neither"
-# Lint level used when no other predicates are matched
-# 1. License isn't in the allow or deny lists
-# 2. License isn't copyleft
-# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
-default = "deny"
 # The confidence threshold for detecting a license from license text.
 # The higher the value, the more closely the license text must be to the
 # canonical license text of a valid SPDX license file.
@@ -154,7 +106,7 @@
 # published to private registries.
 # To see how to mark a crate as unpublished (to the official registry),
 # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
-ignore = true
+ignore = false
 # One or more private registries that you might publish crates to, if a crate
 # is only published to private registries, and ignore is true, the crate will
 # not have its license(s) checked
diff --git a/remoteauth/platform/Cargo.toml b/remoteauth/platform/Cargo.toml
index 0136c27..23b2074 100644
--- a/remoteauth/platform/Cargo.toml
+++ b/remoteauth/platform/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
diff --git a/remoteauth/remote_auth_protocol/Cargo.toml b/remoteauth/remote_auth_protocol/Cargo.toml
index e9fd6d2..e0fcad5 100644
--- a/remoteauth/remote_auth_protocol/Cargo.toml
+++ b/remoteauth/remote_auth_protocol/Cargo.toml
@@ -3,6 +3,7 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+license.workspace = true
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html