This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 21a2891312 Fix remaining Section A docs gaps, BinaryFormat byte[] 
handling, GitControl SecretStore adoption, README refresh (TODO-350/352/353/354)
21a2891312 is described below

commit 21a2891312121e2cb468317c7d41736e9033e61d
Author: James Bognar <[email protected]>
AuthorDate: Thu Aug 13 13:50:53 2026 -0400

    Fix remaining Section A docs gaps, BinaryFormat byte[] handling, GitControl 
SecretStore adoption, README refresh (TODO-350/352/353/354)
    
    TODO-350: completes Section A of the docs-gap sweep — all remaining MED and 
LOW items (fabricated-API corrections, method renames, dead links, 
compile-breaking snippets) across ~55 juneau-docs topic pages. Section A is now 
100% triaged.
    
    TODO-353: Bson/Cbor/MsgPack serializers now honor the configured 
BinaryFormat for byte[] output, and the parser sessions decode 
BinaryFormat-encoded byte[] elements instead of a lossy UTF-8 conversion (a 
latent parse-side bug exposed once the write side stopped emitting native 
binary). Untyped-Map.class recovery limits documented as inherent.
    
    TODO-352: GitControl adopts the new SecretStore SPI via an opt-in 
BeanStore-resolved constructor (char[] token lookup, close() zeroes the 
provider); anonymous and caller-supplied constructors unchanged, no 
default-behavior regression. Docs gain a SecretStore/BeanStore credential 
snippet.
    
    TODO-354: refreshes the root README — MCP section, aggregator-grouped 
module inventory (incl. juneau-secret-keychain and 
juneau-rest-server-datatables), 10.0.0-SNAPSHOT pins, and a 9.x->10.0 upgrade 
callout. Also adds the juneau-secret-keychain docs-inventory row.
---
 README.md                                          | 75 +++++++++++------
 .../juneau/marshall/bson/BsonParserSession.java    |  7 ++
 .../marshall/bson/BsonSerializerSession.java       | 10 ++-
 .../juneau/marshall/cbor/CborParserSession.java    |  9 ++-
 .../marshall/cbor/CborSerializerSession.java       |  6 +-
 .../marshall/msgpack/MsgPackParserSession.java     |  9 ++-
 .../marshall/msgpack/MsgPackSerializerSession.java |  6 +-
 .../juneau/marshall/bson/BsonSerializer_Test.java  | 38 +++++++++
 .../juneau/marshall/cbor/CborSerializer_Test.java  | 37 +++++++++
 .../marshaller/MarshallerVariantClass_Test.java    | 49 ++++++-----
 .../marshall/msgpack/MsgPackSerializerTest.java    | 44 ++++++++++
 .../transforms/BinaryFormat_RoundTrip_Test.java    | 23 ++++--
 .../ByteArrayBase64Swap_ComboRoundTripTest.java    | 40 ++++-----
 juneau-sc/juneau-sc-server/pom.xml                 |  6 ++
 .../server/config/repository/GitControl.java       | 75 ++++++++++++++++-
 .../server/config/repository/GitControl_Test.java  | 94 ++++++++++++++++++++++
 16 files changed, 450 insertions(+), 78 deletions(-)

diff --git a/README.md b/README.md
index 0bc8945754..482576a1ef 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,8 @@
 
 Apache Juneau™ is a single cohesive Java ecosystem consisting of a 
comprehensive toolkit for marshalling POJOs to a wide variety of content types 
using a common framework, along with universal REST server and client APIs for 
creating Swagger-based self-documenting REST interfaces.
 
+> **⚠️ Upgrading from 9.x?** The project is currently developing the 
**10.0.0** release, which includes a number of breaking changes (the public 
`ObjectRest` class removed, `SerializerSet`/`ParserSet` lookups now returning 
`Optional`, the next-gen `RestClient` no longer implicitly defaulting to JSON, 
the `juneau-assertions`/`juneau-bct`/`juneau-junit5` modules merged into a 
single `juneau-test` artifact, and the legacy 
`juneau-my-jetty-microservice`/`juneau-examples-rest` modules removed [...]
+
 ## Key Features
 
 * **Universal Serialization** - Marshal POJOs to JSON, XML, HTML, 
URL-Encoding, UON, MessagePack, CSV, and more
@@ -34,8 +36,20 @@ Apache Juneau™ is a single cohesive Java ecosystem 
consisting of a comprehensi
 * **Fluent Assertions** - Powerful testing framework with fluent-style 
assertions
 * **Type Conversion** - Lightweight, MarshallingContext-free converter 
framework with caching and broad type support
 * **Large-Dataset Streaming** - BeanSupplier/BeanConsumer/BeanChannel APIs for 
serializing and parsing large datasets without loading all elements into 
memory; supports direct database integration via lifecycle methods 
(begin/acceptThrows/onError/complete)
+* **MCP (Model Context Protocol) Support** - First-party server and client 
support for exposing REST resources as MCP tools/prompts/resources, or 
consuming MCP servers from Java, for both the `2025-06-18` and `2026-07-28` 
protocol revisions
 * **Zero Dependencies** - Core marshalling requires no external dependencies
 
+## MCP (Model Context Protocol) Support
+
+Apache Juneau ships first-party [MCP](https://modelcontextprotocol.io/) 
integration, built the same way as the rest of the framework: 
annotation-driven, POJO-based, no magic. The module family is split into 
revision-neutral **cores** plus thin **adapters** per protocol revision, so a 
`2025-06-18`-only deployment never pulls in `2026-07-28`-only dependencies 
(OAuth 2.1, JWT, reactive-streams SSE, etc.):
+
+* **juneau-bean-jsonrpc** - revision-neutral JSON-RPC 2.0 envelope beans, with 
`juneau-bean-mcp-v20250618` / `juneau-bean-mcp-v20260728` adapters for each 
revision's wire beans.
+* **juneau-rest-server-mcp** - revision-neutral server dispatch core, with 
`juneau-rest-server-mcp-v20250618` / `juneau-rest-server-mcp-v20260728` 
adapters for exposing tools, prompts, and resources (dedicated servlet or 
drop-in mixin, plain or Spring Boot).
+* **juneau-rest-client-mcp** - revision-neutral client core, with 
`juneau-rest-client-mcp-v20250618` / `juneau-rest-client-mcp-v20260728` typed 
client facades, plus `juneau-rest-client-mcp-auth` for the client-side OAuth 
2.1 acquisition flow.
+* **juneau-examples-mcp** - a runnable first-party example (notes-service 
demo, with plain, Spring Boot, and OAuth-secured variants).
+
+The `2026-07-28` revision is a strict superset of `2025-06-18` and is where 
new capability work (Multi-Round-Trip Requests/elicitation, subscriptions, 
cache hints, trace-context propagation) lands going forward. See **[MCP (Model 
Context Protocol)](https://juneau.apache.org/docs/topics/JuneauMcp)** for the 
quickstart, setup guide, and full API reference.
+
 ## Documentation & Resources
 
 ### Official Resources
@@ -69,7 +83,7 @@ Apache Juneau™ excels in the following scenarios:
 * **Serverless unit testing of REST APIs** - Test REST services without 
servlet containers using MockRestClient for fast, comprehensive testing
 * **Microservice development** - Build lightweight microservices with embedded 
Jetty or Spring Boot integration
 * **Data transformation and mapping** - Convert between different data formats 
and handle complex object hierarchies with swap mechanisms
-* **Bean-Centric Testing and fluent-style assertions** - Write readable test 
assertions with comprehensive validation capabilities using juneau-bct and 
juneau-assertions
+* **Bean-Centric Testing and fluent-style assertions** - Write readable test 
assertions with comprehensive validation capabilities using juneau-test
 * **Content negotiation and HTTP/2 support** - Handle multiple content types 
automatically with modern HTTP features
 
 ## Getting Started in 5 Minutes
@@ -80,10 +94,12 @@ Apache Juneau™ excels in the following scenarios:
 <dependency>
     <groupId>org.apache.juneau</groupId>
     <artifactId>juneau-shaded-all</artifactId>
-    <version>9.1.0</version>
+    <version>10.0.0-SNAPSHOT</version>
 </dependency>
 ```
 
+> `10.0.0` is currently under development (tracking `-SNAPSHOT` builds) and 
has not yet been released. See the 
[Downloads](https://juneau.apache.org/downloads) page for the latest released 
version.
+
 ### 2. Serialize a POJO to JSON
 
 ```java
@@ -282,27 +298,37 @@ microservice.start();
 
 ## Description
 
-Apache Juneau™ is a single cohesive Java ecosystem consisting of the following 
parts:
-
-* **juneau-marshall**  - A universal toolkit for marshalling POJOs to a 
variety of content types using a common framework with no external library 
dependencies.
-* **juneau-marshall-rdf**      - Additional support for various RDF languages.
-* **juneau-bean-atom, juneau-bean-common, juneau-bean-html5, 
juneau-bean-jsonschema, juneau-bean-openapi-v3**  - A variety of predefined 
serializable beans such as HTML5, Swagger and ATOM.
-* **juneau-config**    - A sophisticated configuration file API.
-* **juneau-assertions** - Fluent-style assertions API.
-* **juneau-bct** - Bean-Centric Testing framework that extends JUnit with 
streamlined assertion methods for Java objects.
-* **juneau-svl** - Simple Variable Language for dynamic string processing.
-* **juneau-rest-common** - REST APIs common to client and server side.
-* **juneau-rest-server**       - A universal REST server API for creating 
Swagger-based self-documenting REST interfaces using POJOs, simply deployed as 
one or more top-level servlets in any Servlet 3.1.0+ container. Includes Spring 
Boot and JAX-RS integration support.
-* **juneau-rest-client** - A universal REST client API for interacting with 
Juneau or 3rd-party REST interfaces using POJOs and proxy interfaces.
-* **juneau-rest-server-springboot** - Spring boot integration for 
juneau-rest-servlet.
-* **juneau-rest-mock** - REST testing API.
-* **juneau-microservice** - Microservice API.
-* **juneau-microservice-jetty** - Jetty microservice API.
-* **juneau-examples-core** - Core code examples.
-* **juneau-petstore-core** - Shared petstore domain + REST resources.
-* **juneau-petstore-jetty** - Jetty/Microservice deployment of the petstore.
-* **juneau-petstore-springboot** - Spring Boot deployment of the petstore.
-* **juneau-shaded** - Shaded (uber) JARs combining multiple Juneau modules for 
simplified dependency management, especially useful for Bazel builds. 
+Apache Juneau™ is a single cohesive Java ecosystem consisting of the following 
parts, grouped by aggregator module. For the complete, always-current 
per-artifact list, see the [Juneau Ecosystem 
Overview](https://juneau.apache.org/docs/topics/JuneauEcosystemOverview).
+
+* **juneau-core** - Core marshalling and support APIs, with no external 
dependencies unless noted:
+  * **juneau-commons** - Shared low-level utilities used across the ecosystem, 
including the Simple Variable Language (SVL) for dynamic string processing.
+  * **juneau-marshall**        - A universal toolkit for marshalling POJOs to 
a variety of content types using a common framework with no external library 
dependencies.
+  * **juneau-marshall-rdf**    - Additional support for various RDF languages.
+  * **juneau-config**  - A sophisticated configuration file API.
+  * **juneau-test** - Unified test-support API combining fluent-style 
assertions, Bean-Centric Testing, and JUnit 5 extensions (replaces the former 
`juneau-assertions`/`juneau-bct`/`juneau-junit5` artifacts).
+* **juneau-bean** - Predefined serializable beans: **juneau-bean-atom**, 
**juneau-bean-common**, **juneau-bean-hal**, **juneau-bean-html5**, 
**juneau-bean-jsonapi**, **juneau-bean-jsonpatch**, **juneau-bean-jsonrpc**, 
**juneau-bean-jsonschema**, **juneau-bean-openapi-v3**, 
**juneau-bean-rfc7807**, **juneau-bean-swagger-v2** - such as HTML5, 
Swagger/OpenAPI, ATOM, HAL, JSON:API, JSON Patch, and RFC 7807 Problem Details. 
(See [MCP Support](#mcp-model-context-protocol-support) above for the [...]
+* **juneau-rest** - REST server and client APIs:
+  * **juneau-rest-common** / **juneau-rest-common-classic** - REST APIs common 
to client and server side (next-gen vs. classic client stack).
+  * **juneau-rest-server**     - A universal REST server API for creating 
Swagger-based self-documenting REST interfaces using POJOs, simply deployed as 
one or more top-level servlets in any Servlet 3.1.0+ container. Includes Spring 
Boot and JAX-RS integration support.
+  * **juneau-rest-server-rdf** - RDF support for the REST server.
+  * **juneau-rest-server-springboot** - Spring Boot integration for 
juneau-rest-server.
+  * **juneau-rest-server-auth-jwt, -saml, -oauth, -oidc-rp** and 
**juneau-rest-auth-oauth-flow** - Authentication/authorization add-ons (JWT, 
SAML, OAuth, OpenID Connect RP).
+  * **juneau-rest-server-metrics-micrometer, -tracing-otel, 
-management-logging** - Observability add-ons (Micrometer metrics, 
OpenTelemetry tracing, request/response logging).
+  * **juneau-rest-server-datatables** - Server-side processing adapter (plus 
browser-side helpers) for [DataTables](https://datatables.net/).
+  * **juneau-rest-server-reactive, -reactive-reactor** - Reactive-streams 
(SSE) response support.
+  * **juneau-rest-server-view-jsp, -thymeleaf, -mustache, -freemarker** - 
View-engine add-ons.
+  * **juneau-rest-client** / **juneau-rest-client-classic** - A universal REST 
client API for interacting with Juneau or 3rd-party REST interfaces using POJOs 
and proxy interfaces (next-gen vs. classic client stack), with 
**juneau-rest-client-apache-httpclient-45/-50**, **juneau-rest-client-jetty**, 
and **juneau-rest-client-okhttp** transport backends.
+  * **juneau-rest-mock** - REST testing API.
+  * MCP client/server modules - see [MCP 
Support](#mcp-model-context-protocol-support) above.
+* **juneau-microservice** - **juneau-microservice**, 
**juneau-microservice-jetty**, **juneau-microservice-tomcat**, 
**juneau-microservice-test**, **juneau-microservice-examples** - Lightweight 
standalone microservice APIs (Jetty and Tomcat), plus a JUnit 5 test harness.
+* **juneau-sc** - **juneau-sc-server** - Git-backed source-control 
configuration server integration (`GitControl`, etc.).
+* **juneau-secret-keychain** - Opt-in `SecretStore` implementation backed by 
the macOS `security` keychain CLI (implements the `SecretStore` SPI in 
`juneau-commons`).
+* **juneau-examples** - **juneau-examples-core** - Core code examples; 
**juneau-examples-mcp** - Runnable MCP example (notes-service demo).
+* **juneau-petstore** - **juneau-petstore-core** - Shared petstore domain + 
REST resources; **juneau-petstore-jetty** - Jetty/Microservice deployment; 
**juneau-petstore-springboot** - Spring Boot deployment.
+* **juneau-shaded** - Shaded (uber) JARs combining multiple Juneau modules for 
simplified dependency management, especially useful for Bazel builds: 
**juneau-shaded-core**, **juneau-shaded-rest-client**, 
**juneau-shaded-rest-server**, **juneau-shaded-rest-server-springboot**, and 
**juneau-shaded-all**.
+* **juneau-bundles** - Curated dependency bundles pulling a coherent module 
set per deployment shape: **juneau-microservice-jetty-bundle**, 
**juneau-microservice-tomcat-bundle**, **juneau-springboot-bundle**, 
**juneau-observability-otlp-bundle**.
+* **juneau-bom** - Published Maven Bill-of-Materials for version-aligning 
Juneau dependencies.
+* **juneau-distrib** - Release distribution assembly.
 
 Questions via email to [email protected] are always welcome.
 
@@ -317,8 +343,9 @@ Juneau is packed with features that may not be obvious at 
first. Users are encou
 * Exhaustively tested
 * Lots of up-to-date documentation and examples
 * Minimal library dependencies:
-   * juneau-marshall, juneau-bean-atom, juneau-bean-common, juneau-bean-html5, 
juneau-bean-jsonschema, juneau-bean-openapi-v3, juneau-svl, juneau-config - No 
external dependencies. Entirely self-contained.
+   * juneau-commons, juneau-marshall, juneau-bean-atom, juneau-bean-common, 
juneau-bean-html5, juneau-bean-jsonschema, juneau-bean-openapi-v3, 
juneau-config - No external dependencies. Entirely self-contained.
    * juneau-marshall-rdf - Optional RDF support. Requires Apache Jena 5.6.0+.
+   * juneau-test - Requires opentest4j (JUnit Jupiter is a provided-scope 
dependency, supplied by your test runtime).
    * juneau-rest-server - Any Servlet 3.1.0+ container.
    * juneau-rest-client - Apache HttpClient 4.5+.
 * Built on top of Servlet and Apache HttpClient APIs that allow you to use the 
newest HTTP/2 features such as request/response multiplexing and server push.
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
index 614b572232..553c889cdc 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
@@ -125,6 +125,13 @@ public class BsonParserSession extends 
InputStreamParserSession implements Recor
                                yield null;
                        }
                };
+               // A byte[]-targeted string element is a BinaryFormat-encoded 
payload (TODO-353 write-side fix
+               // counterpart) rather than a literal string value - decode it 
back to bytes.
+               if (elementType == 0x02 && nn(targetType) && 
targetType.isByteArray()) {
+                       var binaryFormat = getBinaryFormat();
+                       if (binaryFormat != BinaryFormat.NOT_SET)
+                               o = binaryFormat.parse((String)o);
+               }
                if (nn(parentBean()) && nn(o))
                        setParent(targetType, o, parentBean());
                return o;
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonSerializerSession.java
index 5cba3f91e2..74885553c5 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonSerializerSession.java
@@ -202,8 +202,14 @@ public class BsonSerializerSession extends 
OutputStreamSerializerSession impleme
                                out.writeChildDocument(child);
                        }
                } else if (sType.isByteArray()) {
-                       out.writeElement(BINARY.value, name);
-                       out.writeBinary((byte[])o);
+                       var binaryFormat = getBinaryFormat();
+                       if (binaryFormat == BinaryFormat.NOT_SET) {
+                               out.writeElement(BINARY.value, name);
+                               out.writeBinary((byte[])o);
+                       } else {
+                               out.writeElement(STRING.value, name);
+                               out.writeString(binaryFormat.format((byte[])o));
+                       }
                } else if (sType.isCollection() || sType.isArray()) {
                        out.writeElement(ARRAY.value, name);
                        var child = out.createChild();
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
index b202f9fc38..8e8e76a30d 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
@@ -280,7 +280,14 @@ public class CborParserSession extends 
InputStreamParserSession implements Token
                        } else if (sType.isBoolean() || sType.isCharSequence() 
|| sType.isChar() || sType.isNumber() || sType.isByteArray()) {
                                // Merged scalar tier: one CBOR data type 
(BOOLEAN/UINT/NINT/FLOAT/STRING/BINARY) covers
                                // many scalar Java types — the read already 
happened above and convertToType narrows.
-                               o = convertToType(o, sType);
+                               // A byte[]-targeted string element is a 
BinaryFormat-encoded payload (TODO-353 write-side
+                               // fix counterpart) rather than a literal 
string - decode it back to bytes directly, since
+                               // the generic String-to-byte[] conversion 
doesn't know about BinaryFormat.
+                               var binaryFormat = getBinaryFormat();
+                               if (dt == STRING && sType.isByteArray() && 
binaryFormat != BinaryFormat.NOT_SET)
+                                       o = binaryFormat.parse((String)o);
+                               else
+                                       o = convertToType(o, sType);
                        } else if (sType.isDate()) {
                                o = readDate(String.valueOf(o), sType);
                        } else if (sType.isCalendar()) {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborSerializerSession.java
index fb28689b95..e0a1168a4f 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborSerializerSession.java
@@ -218,7 +218,11 @@ public class CborSerializerSession extends 
OutputStreamSerializerSession impleme
                } else if (sType.isCollection()) {
                        writeCollection(out, (Collection)o, eType);
                } else if (sType.isByteArray()) {
-                       out.appendBinary((byte[])o);
+                       var binaryFormat = getBinaryFormat();
+                       if (binaryFormat == BinaryFormat.NOT_SET)
+                               out.appendBinary((byte[])o);
+                       else
+                               
out.appendString(binaryFormat.format((byte[])o));
                } else if (sType.isArray()) {
                        writeCollection(out, toList(sType.inner(), o), eType);
                } else if (sType.isBoolean()) {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
index 32979b4ea2..e52aa04785 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
@@ -310,7 +310,14 @@ public class MsgPackParserSession extends 
InputStreamParserSession implements To
                        } else if (sType.isBoolean() || sType.isCharSequence() 
|| sType.isChar() || sType.isNumber() || sType.isByteArray()) {
                                // Merged scalar tier: one wire-type 
(BOOLEAN/INT/LONG/FLOAT/DOUBLE/STRING/BIN) covers
                                // many scalar Java types — the read already 
happened above and convertToType narrows.
-                               o = convertToType(o, sType);
+                               // A byte[]-targeted string element is a 
BinaryFormat-encoded payload (TODO-353 write-side
+                               // fix counterpart) rather than a literal 
string - decode it back to bytes directly, since
+                               // the generic String-to-byte[] conversion 
doesn't know about BinaryFormat.
+                               var binaryFormat = getBinaryFormat();
+                               if (dt == STRING && sType.isByteArray() && 
binaryFormat != BinaryFormat.NOT_SET)
+                                       o = binaryFormat.parse((String)o);
+                               else
+                                       o = convertToType(o, sType);
                        } else if (sType.isDate()) {
                                o = readDate(String.valueOf(o), sType);
                        } else if (sType.isCalendar()) {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerSession.java
index 7422596c3b..3396456ea6 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerSession.java
@@ -280,7 +280,11 @@ public class MsgPackSerializerSession extends 
OutputStreamSerializerSession impl
                } else if (sType.isCollection()) {
                        writeCollection(out, (Collection)o, eType);
                } else if (sType.isByteArray()) {
-                       out.appendBinary((byte[])o);
+                       var binaryFormat = getBinaryFormat();
+                       if (binaryFormat == BinaryFormat.NOT_SET)
+                               out.appendBinary((byte[])o);
+                       else
+                               
out.appendString(binaryFormat.format((byte[])o));
                } else if (sType.isArray()) {
                        writeCollection(out, toList(sType.inner(), o), eType);
                } else if (sType.isBoolean()) {
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/bson/BsonSerializer_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/bson/BsonSerializer_Test.java
index 4863605212..4d83a3efae 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/bson/BsonSerializer_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/bson/BsonSerializer_Test.java
@@ -24,6 +24,7 @@ import java.time.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.marshall.*;
 import org.apache.juneau.marshall.collections.*;
 import org.junit.jupiter.api.*;
 
@@ -109,5 +110,42 @@ class BsonSerializer_Test extends TestBase {
                assertEquals("LARGE", parsed.get("size"));
        }
 
+       @Test
+       void a09_writeByteArraySpacedHexHonorsBinaryFormat() throws Exception {
+               var bean = new BeanWithBytes();
+               var defaultOut = BsonSerializer.DEFAULT.write(bean);
+               var spacedHexOut = 
BsonSerializer.DEFAULT_SPACED_HEX.write(bean);
+               assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex 
output should differ from the native binary output");
+               var parsed = BsonParser.DEFAULT_SPACED_HEX.read(spacedHexOut, 
BeanWithBytes.class);
+               assertArrayEquals(bean.data, parsed.data);
+       }
+
+       @Test
+       void a10_writeByteArrayBase64HonorsBinaryFormat() throws Exception {
+               var bean = new BeanWithBytes();
+               var defaultOut = BsonSerializer.DEFAULT.write(bean);
+               var base64Out = BsonSerializer.DEFAULT_BASE64.write(bean);
+               assertFalse(Arrays.equals(defaultOut, base64Out), "Base64 
output should differ from the native binary output");
+               var parsed = BsonParser.DEFAULT_BASE64.read(base64Out, 
BeanWithBytes.class);
+               assertArrayEquals(bean.data, parsed.data);
+       }
+
+       @Test
+       void a11_writeByteArrayNotSetKeepsNativeBinary() throws Exception {
+               // NOT_SET (the default on BsonSerializer.DEFAULT) is 
unaffected by the SpacedHex/Base64 fix -- byte[]
+               // values still use BSON's native binary element (subtype 0x05) 
rather than a string encoding.
+               var bean = new BeanWithBytes();
+               var s = 
BsonSerializer.create().keepNullProperties().binaryFormat(BinaryFormat.NOT_SET).build();
+               var bytes = s.write(bean);
+               var p = BsonParser.create().build();
+               var parsed = p.read(bytes, JsonMap.class);
+               assertArrayEquals(bean.data, (byte[])parsed.get("data"));
+       }
+
+       /** Bean with a single byte[] property, used to exercise the 
MarshalledPropertyPostProcessor bean-property path. */
+       public static class BeanWithBytes {
+               public byte[] data = { 0x01, 0x02, 0x03 };
+       }
+
        enum Size { SMALL, MEDIUM, LARGE }
 }
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborSerializer_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborSerializer_Test.java
index 44e1e0426a..c4c77db04f 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborSerializer_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborSerializer_Test.java
@@ -21,6 +21,8 @@ import static org.apache.juneau.commons.utils.Shorts.*;
 import static org.apache.juneau.commons.utils.StringUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 
+import java.util.*;
+
 import org.apache.juneau.*;
 import org.apache.juneau.marshall.*;
 import org.apache.juneau.marshall.collections.*;
@@ -254,6 +256,41 @@ class CborSerializer_Test extends TestBase {
                        CborSerializer.DEFAULT_BASE64.write(JsonMap.of("a", 
1)), JsonMap.class).getInt("a"));
        }
 
+       @Test
+       void c21_spacedHexByteArrayPropertyHonorsBinaryFormat() throws 
Exception {
+               var bean = new BeanWithBytes();
+               var defaultOut = CborSerializer.DEFAULT.write(bean);
+               var spacedHexOut = 
CborSerializer.DEFAULT_SPACED_HEX.write(bean);
+               assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex 
output should differ from the native binary output");
+               var parsed = CborParser.DEFAULT_SPACED_HEX.read(spacedHexOut, 
BeanWithBytes.class);
+               assertArrayEquals(bean.data, parsed.data);
+       }
+
+       @Test
+       void c22_base64ByteArrayPropertyHonorsBinaryFormat() throws Exception {
+               var bean = new BeanWithBytes();
+               var defaultOut = CborSerializer.DEFAULT.write(bean);
+               var base64Out = CborSerializer.DEFAULT_BASE64.write(bean);
+               assertFalse(Arrays.equals(defaultOut, base64Out), "Base64 
output should differ from the native binary output");
+               var parsed = CborParser.DEFAULT_BASE64.read(base64Out, 
BeanWithBytes.class);
+               assertArrayEquals(bean.data, parsed.data);
+       }
+
+       @Test
+       void c23_spacedHexTopLevelByteArrayHonorsBinaryFormat() throws 
Exception {
+               var data = new byte[] { 1, 2, 3 };
+               var defaultOut = CborSerializer.DEFAULT.write(data);
+               var spacedHexOut = 
CborSerializer.DEFAULT_SPACED_HEX.write(data);
+               assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex 
output should differ from the native binary output");
+               var parsed = CborParser.DEFAULT_SPACED_HEX.read(spacedHexOut, 
byte[].class);
+               assertArrayEquals(data, parsed);
+       }
+
+       /** Bean with a single byte[] property, used to exercise the 
MarshalledPropertyPostProcessor bean-property path. */
+       public static class BeanWithBytes {
+               public byte[] data = { 1, 2, 3 };
+       }
+
        public static class Bean1 {
                public String s;
                public int i;
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/marshaller/MarshallerVariantClass_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/marshaller/MarshallerVariantClass_Test.java
index 8982e3a2e3..a627a28e46 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/marshaller/MarshallerVariantClass_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/marshaller/MarshallerVariantClass_Test.java
@@ -327,43 +327,54 @@ class MarshallerVariantClass_Test extends TestBase {
                assertBean(m, "a", "1");
        }
 
-       // NOTE: BsonSerializer/CborSerializer/MsgPackSerializer currently 
always write byte[] values using
-       // each format's native binary wire type regardless of the configured 
BinaryFormat (SPACED_HEX /
-       // BASE64 only affects other conversion paths) -- so for a POJO with a 
byte[] field, the SpacedHex/
-       // Base64 variant serializers produce byte-identical output to DEFAULT. 
These tests therefore verify
-       // wiring (equality to the authoritative *Serializer.DEFAULT_* 
constant) + round-trip rather than a
-       // visibly distinct serialized form.
+       // NOTE (TODO-353): BsonSerializer/CborSerializer/MsgPackSerializer now 
honor the configured
+       // BinaryFormat for byte[] output -- SPACED_HEX/BASE64 switch the 
byte[] wire representation from
+       // each format's native binary opcode to that format's native string 
type containing the
+       // spaced-hex/base64 text, so the SpacedHex/Base64 variant serializers 
produce visibly distinct
+       // output from DEFAULT for a POJO with a byte[] field. These tests 
verify both the distinct wire
+       // form and the round-trip back to the original bytes.
+       //
+       // The round-trip parse is done with an explicit Map<String,byte[]> 
type hint rather than the bare
+       // Map.class used elsewhere in this file: once byte[] is on the wire as 
a string (SPACED_HEX/BASE64),
+       // the string is indistinguishable from a genuine text value without a 
type hint telling the parser
+       // the target is byte[] -- this is expected, since the encoding trades 
the format's native
+       // self-describing binary tag for a text representation that needs the 
same external typing a text
+       // serializer (e.g. JSON) would need to recover byte[] from a string.
 
        @Test void b23_bsonSpacedHexRoundTrip() throws Exception {
                var bean = Map.of("a", new byte[]{1, 2, 3});
                var out = BsonSpacedHex.of(bean);
                
assertArrayEquals(BsonSerializer.DEFAULT_SPACED_HEX.write(bean), out);
-               var m = BsonSpacedHex.to(out, Map.class);
-               assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+               assertFalse(Arrays.equals(Bson.of(bean), out), "SpacedHex 
output should differ from the native binary output");
+               Map<String,byte[]> m = BsonSpacedHex.to(out, Map.class, 
String.class, byte[].class);
+               assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
        }
 
        @Test void b24_bsonBase64RoundTrip() throws Exception {
                var bean = Map.of("a", new byte[]{1, 2, 3});
                var out = BsonBase64.of(bean);
                assertArrayEquals(BsonSerializer.DEFAULT_BASE64.write(bean), 
out);
-               var m = BsonBase64.to(out, Map.class);
-               assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+               assertFalse(Arrays.equals(Bson.of(bean), out), "Base64 output 
should differ from the native binary output");
+               Map<String,byte[]> m = BsonBase64.to(out, Map.class, 
String.class, byte[].class);
+               assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
        }
 
        @Test void b25_cborSpacedHexRoundTrip() throws Exception {
                var bean = Map.of("a", new byte[]{1, 2, 3});
                var out = CborSpacedHex.of(bean);
                
assertArrayEquals(CborSerializer.DEFAULT_SPACED_HEX.write(bean), out);
-               var m = CborSpacedHex.to(out, Map.class);
-               assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+               assertFalse(Arrays.equals(Cbor.of(bean), out), "SpacedHex 
output should differ from the native binary output");
+               Map<String,byte[]> m = CborSpacedHex.to(out, Map.class, 
String.class, byte[].class);
+               assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
        }
 
        @Test void b26_cborBase64RoundTrip() throws Exception {
                var bean = Map.of("a", new byte[]{1, 2, 3});
                var out = CborBase64.of(bean);
                assertArrayEquals(CborSerializer.DEFAULT_BASE64.write(bean), 
out);
-               var m = CborBase64.to(out, Map.class);
-               assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+               assertFalse(Arrays.equals(Cbor.of(bean), out), "Base64 output 
should differ from the native binary output");
+               Map<String,byte[]> m = CborBase64.to(out, Map.class, 
String.class, byte[].class);
+               assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
        }
 
        @Test void b27_cborNativeRoundTrip() throws Exception {
@@ -381,16 +392,18 @@ class MarshallerVariantClass_Test extends TestBase {
                var bean = Map.of("a", new byte[]{1, 2, 3});
                var out = MsgPackSpacedHex.of(bean);
                
assertArrayEquals(MsgPackSerializer.DEFAULT_SPACED_HEX.write(bean), out);
-               var m = MsgPackSpacedHex.to(out, Map.class);
-               assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+               assertFalse(Arrays.equals(MsgPack.of(bean), out), "SpacedHex 
output should differ from the native binary output");
+               Map<String,byte[]> m = MsgPackSpacedHex.to(out, Map.class, 
String.class, byte[].class);
+               assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
        }
 
        @Test void b29_msgPackBase64RoundTrip() throws Exception {
                var bean = Map.of("a", new byte[]{1, 2, 3});
                var out = MsgPackBase64.of(bean);
                assertArrayEquals(MsgPackSerializer.DEFAULT_BASE64.write(bean), 
out);
-               var m = MsgPackBase64.to(out, Map.class);
-               assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+               assertFalse(Arrays.equals(MsgPack.of(bean), out), "Base64 
output should differ from the native binary output");
+               Map<String,byte[]> m = MsgPackBase64.to(out, Map.class, 
String.class, byte[].class);
+               assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
        }
 
        @Test void b30_msgPackNativeRoundTrip() throws Exception {
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerTest.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerTest.java
index aa576f6cf0..0debca23f2 100755
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerTest.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerTest.java
@@ -21,6 +21,8 @@ import static org.apache.juneau.commons.utils.StringUtils.*;
 import static org.apache.juneau.marshall.marshaller.MarshallUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 
+import java.util.*;
+
 import org.apache.juneau.*;
 import org.apache.juneau.marshall.json5.*;
 import org.junit.jupiter.api.*;
@@ -217,6 +219,48 @@ class MsgPackSerializerTest extends TestBase {
                public int age = 21;
        }
 
+       /** Bean with a single byte[] property, used to exercise the 
MarshalledPropertyPostProcessor bean-property path. */
+       public static class BeanWithBytes {
+               public byte[] data = { 1, 2, 3 };
+       }
+
+       
//====================================================================================================
+       // BinaryFormat honored for byte[] output (TODO-353)
+       
//====================================================================================================
+       @Test void b01_spacedHexByteArrayPropertyHonorsBinaryFormat() throws 
Exception {
+               var bean = new BeanWithBytes();
+               var defaultOut = MsgPackSerializer.DEFAULT.write(bean);
+               var spacedHexOut = 
MsgPackSerializer.DEFAULT_SPACED_HEX.write(bean);
+               assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex 
output should differ from the native binary output");
+               var parsed = 
MsgPackParser.DEFAULT_SPACED_HEX.read(spacedHexOut, BeanWithBytes.class);
+               assertArrayEquals(bean.data, parsed.data);
+       }
+
+       @Test void b02_base64ByteArrayPropertyHonorsBinaryFormat() throws 
Exception {
+               var bean = new BeanWithBytes();
+               var defaultOut = MsgPackSerializer.DEFAULT.write(bean);
+               var base64Out = MsgPackSerializer.DEFAULT_BASE64.write(bean);
+               assertFalse(Arrays.equals(defaultOut, base64Out), "Base64 
output should differ from the native binary output");
+               var parsed = MsgPackParser.DEFAULT_BASE64.read(base64Out, 
BeanWithBytes.class);
+               assertArrayEquals(bean.data, parsed.data);
+       }
+
+       @Test void b03_spacedHexTopLevelByteArrayHonorsBinaryFormat() throws 
Exception {
+               var data = new byte[] { 1, 2, 3 };
+               var defaultOut = MsgPackSerializer.DEFAULT.write(data);
+               var spacedHexOut = 
MsgPackSerializer.DEFAULT_SPACED_HEX.write(data);
+               assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex 
output should differ from the native binary output");
+               var parsed = 
MsgPackParser.DEFAULT_SPACED_HEX.read(spacedHexOut, byte[].class);
+               assertArrayEquals(data, parsed);
+       }
+
+       @Test void b04_notSetKeepsNativeBinary() throws Exception {
+               // NOT_SET (the default on MsgPackSerializer.DEFAULT) is 
unaffected by the SpacedHex/Base64 fix --
+               // byte[] values still use MsgPack's native bin family rather 
than a string encoding.
+               var data = new byte[] { 1, 2, 3 };
+               test(data, "C4 03 01 02 03");
+       }
+
        private static void test(Object input, String expected) throws 
Exception {
                var b = msgPack(input);
                assertEquals(expected, toSpacedHex(b));
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/BinaryFormat_RoundTrip_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/BinaryFormat_RoundTrip_Test.java
index 86f308c571..f9abac69b7 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/BinaryFormat_RoundTrip_Test.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/BinaryFormat_RoundTrip_Test.java
@@ -68,14 +68,21 @@ import org.junit.jupiter.params.provider.*;
  * comes to 42 &times; 5 = 210 testers per test method.
  *
  * <p>
- * {@link BinaryFormat} only affects text-based serializers per the 
class-level "Binary serializers" note —
- * BSON / CBOR / MsgPack / Prototext / Parquet emit native bytes regardless of 
the configured constant.  The
- * variant {@code binarySwap} installed by {@code 
MarshalledPropertyPostProcessor} respects that by handing
- * the raw {@code byte[]} back to {@link 
org.apache.juneau.marshall.serializer.OutputStreamSerializerSession}
- * subtypes instead of the formatted wire string, so bean-property round-trips 
through binary serializers
- * still resolve to the original bytes via native handling.  Top-level / 
{@link List}-element paths route
- * through the default-swap dispatch ({@link 
org.apache.juneau.marshall.swaps.BinarySwap}) which short-circuits to
- * raw bytes for binary sessions — same lossless round-trip via the native 
path.
+ * {@link BinaryFormat} affects every text-based serializer, plus BSON / CBOR 
/ MsgPack (binary serializers
+ * with a native byte-array wire type, fixed under TODO-353): {@link 
BinaryFormat#NOT_SET} emits their
+ * native binary opcode as before, while every other constant switches the 
{@code byte[]} wire
+ * representation to that format's native string type carrying the 
spaced-hex/base64/etc. text — the
+ * variant {@code binarySwap} installed by {@code 
MarshalledPropertyPostProcessor} hands the raw
+ * {@code byte[]} straight through to
+ * {@link org.apache.juneau.marshall.serializer.OutputStreamSerializerSession} 
subtypes, which make the
+ * NOT_SET/non-NOT_SET decision themselves at their {@code byte[]}-write 
dispatch site.  Top-level /
+ * {@link List}-element paths route through the default-swap dispatch
+ * ({@link org.apache.juneau.marshall.swaps.BinarySwap}), which likewise 
short-circuits to raw bytes for
+ * binary sessions and lets the same per-format dispatch site apply the 
configured format.  Serializers
+ * without a native byte-array wire type (Parquet, binary RDF) always run 
through the swap-formatted text
+ * string.  Prototext still has the pre-TODO-353 native-bytes-always bug (out 
of scope here — it emits its
+ * native bytes opcode unconditionally); its round-trip below still passes 
because it's lossless either way,
+ * not because it honors the configured format.  All combos round-trip to the 
original bytes regardless.
  */
 @SuppressWarnings({
        "unused" // Exception parameter intentionally unused in catch block; 
only the fact of the exception matters.
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/ByteArrayBase64Swap_ComboRoundTripTest.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/ByteArrayBase64Swap_ComboRoundTripTest.java
index af22885980..129f23958c 100644
--- 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/ByteArrayBase64Swap_ComboRoundTripTest.java
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/ByteArrayBase64Swap_ComboRoundTripTest.java
@@ -64,8 +64,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        .urlEnc("_value=AQID")
                        .urlEncT("_value=AQID")
                        .urlEncR("_value=AQID")
-                       .msgPack("C403010203")
-                       .msgPackT("C403010203")
+                       .msgPack("A441514944")
+                       .msgPackT("A441514944")
                        .verify(x -> verify(x).isType(byte[].class))
                        .build(),
                tester(2, "ByteArray2d", byte[][].class, new 
byte[][]{{1,2,3},{4,5,6},null})
@@ -88,8 +88,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        .urlEnc("0=AQID&1=BAUG&2=null")
                        .urlEncT("0=AQID&1=BAUG&2=null")
                        .urlEncR("0=AQID\n&1=BAUG\n&2=null")
-                       .msgPack("93C403010203C403040506C0")
-                       .msgPackT("93C403010203C403040506C0")
+                       .msgPack("93A441514944A442415547C0")
+                       .msgPackT("93A441514944A442415547C0")
                        .verify(x -> verify(x).isType(byte[][].class))
                        .build(),
                tester(3, "ListOfByteArrays", getType(List.class,byte[].class), 
l(bytes(1,2,3),bytes(4,5,6),null))
@@ -112,8 +112,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        .urlEnc("0=AQID&1=BAUG&2=null")
                        .urlEncT("0=AQID&1=BAUG&2=null")
                        .urlEncR("0=AQID\n&1=BAUG\n&2=null")
-                       .msgPack("93C403010203C403040506C0")
-                       .msgPackT("93C403010203C403040506C0")
+                       .msgPack("93A441514944A442415547C0")
+                       .msgPackT("93A441514944A442415547C0")
                        .verify(x -> verify(x).isType(List.class))
                        .verify(x -> verify(x.get(0)).isType(byte[].class))
                        .build(),
@@ -137,8 +137,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        .urlEnc("foo=AQID&bar=null&null=BAUG&'null'=BwgJ")
                        .urlEncT("foo=AQID&bar=null&null=BAUG&'null'=BwgJ")
                        
.urlEncR("foo=AQID\n&bar=null\n&null=BAUG\n&'null'=BwgJ")
-                       
.msgPack("84A3666F6FC403010203A3626172C0C0C403040506A46E756C6CC403070809")
-                       
.msgPackT("84A3666F6FC403010203A3626172C0C0C403040506A46E756C6CC403070809")
+                       
.msgPack("84A3666F6FA441514944A3626172C0C0A442415547A46E756C6CA44277674A")
+                       
.msgPackT("84A3666F6FA441514944A3626172C0C0A442415547A46E756C6CA44277674A")
                        .verify(x -> verify(x).isType(Map.class))
                        .verify(x -> 
verify(x.keySet().iterator().next()).isType(String.class))
                        .verify(x -> 
verify(x.values().iterator().next()).isType(byte[].class))
@@ -163,8 +163,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        .urlEnc("f=AQID")
                        .urlEncT("f=AQID")
                        .urlEncR("f=AQID")
-                       .msgPack("81A166C403010203")
-                       .msgPackT("81A166C403010203")
+                       .msgPack("81A166A441514944")
+                       .msgPackT("81A166A441514944")
                        .verify(x -> 
verify(x).isType(BeanWithByteArrayField.class))
                        .build(),
                tester(6, "BeanWithByteArray2dField", 
BeanWithByteArray2dField.class, new BeanWithByteArray2dField().init())
@@ -187,8 +187,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        .urlEnc("f=@(AQID,BAUG,null)")
                        .urlEncT("f=@(AQID,BAUG,null)")
                        .urlEncR("f=@(\n\tAQID,\n\tBAUG,\n\tnull\n)")
-                       .msgPack("81A16693C403010203C403040506C0")
-                       .msgPackT("81A16693C403010203C403040506C0")
+                       .msgPack("81A16693A441514944A442415547C0")
+                       .msgPackT("81A16693A441514944A442415547C0")
                        .verify(x -> 
verify(x).isType(BeanWithByteArray2dField.class))
                        .build(),
                tester(7, "BeanWithByteArrayNullField", 
BeanWithByteArrayNullField.class, new BeanWithByteArrayNullField().init())
@@ -235,8 +235,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        .urlEnc("f=@(AQID,BAUG,null)")
                        .urlEncT("f=@(AQID,BAUG,null)")
                        .urlEncR("f=@(\n\tAQID,\n\tBAUG,\n\tnull\n)")
-                       .msgPack("81A16693C403010203C403040506C0")
-                       .msgPackT("81A16693C403010203C403040506C0")
+                       .msgPack("81A16693A441514944A442415547C0")
+                       .msgPackT("81A16693A441514944A442415547C0")
                        .verify(x -> 
verify(x).isType(BeanWithByteArrayListField.class))
                        .build(),
                tester(9, "BeanWithByteArrayMapField", 
BeanWithByteArrayMapField.class, new BeanWithByteArrayMapField().init())
@@ -259,8 +259,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        .urlEnc("f=(foo=AQID,bar=null,null=BAUG)")
                        .urlEncT("f=(foo=AQID,bar=null,null=BAUG)")
                        
.urlEncR("f=(\n\tfoo=AQID,\n\tbar=null,\n\tnull=BAUG\n)")
-                       
.msgPack("81A16683A3666F6FC403010203A3626172C0C0C403040506")
-                       
.msgPackT("81A16683A3666F6FC403010203A3626172C0C0C403040506")
+                       
.msgPack("81A16683A3666F6FA441514944A3626172C0C0A442415547")
+                       
.msgPackT("81A16683A3666F6FA441514944A3626172C0C0A442415547")
                        .verify(x -> 
verify(x).isType(BeanWithByteArrayMapField.class))
                        .build(),
                tester(10, "BeanWithByteArrayBeanListField", 
BeanWithByteArrayBeanListField.class, new 
BeanWithByteArrayBeanListField().init())
@@ -283,8 +283,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        
.urlEnc("f=@((f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)),null)")
                        
.urlEncT("f=@((f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)),null)")
                        
.urlEncR("f=@(\n\t(\n\t\tf1=AQID,\n\t\tf2=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf3=null,\n\t\tf4=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf5=(\n\t\t\tfoo=AQID,\n\t\t\tbar=null,\n\t\t\tnull=BAUG\n\t\t)\n\t),\n\tnull\n)")
-                       
.msgPack("81A1669285A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506C0")
-                       
.msgPackT("81A1669285A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506C0")
+                       
.msgPack("81A1669285A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547C0")
+                       
.msgPackT("81A1669285A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547C0")
                        .verify(x -> 
verify(x).isType(BeanWithByteArrayBeanListField.class))
                        .build(),
                tester(11, "BeanWithByteArrayBeanMapField", 
BeanWithByteArrayBeanMapField.class, new BeanWithByteArrayBeanMapField().init())
@@ -307,8 +307,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends 
ComboRoundTripTest_Base {
                        
.urlEnc("f=(foo=(f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)),bar=null,null=(f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)))")
                        
.urlEncT("f=(foo=(f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)),bar=null,null=(f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)))")
                        
.urlEncR("f=(\n\tfoo=(\n\t\tf1=AQID,\n\t\tf2=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf3=null,\n\t\tf4=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf5=(\n\t\t\tfoo=AQID,\n\t\t\tbar=null,\n\t\t\tnull=BAUG\n\t\t)\n\t),\n\tbar=null,\n\tnull=(\n\t\tf1=AQID,\n\t\tf2=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf3=null,\n\t\tf4=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf5=(\n\t\t\tfoo=AQID,\n\t\t\tbar=null,\n\t\t\tnull=BAUG\n\t\t)\n\t)\n)")
-                       
.msgPack("81A16683A3666F6F85A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506A3626172C0C085A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506")
-                       
.msgPackT("81A16683A3666F6F85A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506A3626172C0C085A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506")
+                       
.msgPack("81A16683A3666F6F85A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547A3626172C0C085A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547")
+                       
.msgPackT("81A16683A3666F6F85A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547A3626172C0C085A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547")
                        .verify(x -> 
verify(x).isType(BeanWithByteArrayBeanMapField.class))
                        .build()
        };
diff --git a/juneau-sc/juneau-sc-server/pom.xml 
b/juneau-sc/juneau-sc-server/pom.xml
index 219b47ac8f..73161f72e4 100644
--- a/juneau-sc/juneau-sc-server/pom.xml
+++ b/juneau-sc/juneau-sc-server/pom.xml
@@ -45,6 +45,12 @@
                        <artifactId>juneau-microservice-jetty</artifactId>
                        <version>${project.version}</version>
                </dependency>
+               <dependency>
+                       <groupId>org.junit.jupiter</groupId>
+                       <artifactId>junit-jupiter</artifactId>
+                       <version>${junit.version}</version>
+                       <scope>test</scope>
+               </dependency>
        </dependencies>
 
        <build>
diff --git 
a/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GitControl.java
 
b/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GitControl.java
index 24d5090340..a7df635de2 100644
--- 
a/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GitControl.java
+++ 
b/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GitControl.java
@@ -18,7 +18,9 @@ package org.apache.juneau.server.config.repository;
 
 import java.io.*;
 
+import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.commons.logging.*;
+import org.apache.juneau.commons.secret.*;
 import org.eclipse.jgit.api.*;
 import org.eclipse.jgit.api.errors.*;
 import org.eclipse.jgit.internal.storage.file.*;
@@ -34,8 +36,20 @@ import org.eclipse.jgit.transport.*;
  * Used to fetch configuration files from a remote Git repository.
  *
  * <p>
+ * Credentials are never hardcoded in this class.  They are either supplied 
directly by the caller (via the 5-arg
+ * constructor) or, preferably, resolved at construction time from a pluggable 
{@link SecretStore} (via the
+ * {@link #GitControl(String, String, String, String, BeanStore, boolean) 
BeanStore/SecretStore} constructor) so the
+ * password/token can be sourced from an environment variable, an OS keychain, 
or another secret backend rather than a
+ * literal string.
+ *
+ * <p>
  * The underlying JGit {@link Repository} and {@link Git} handles are opened 
in the constructor and released by
  * {@link #close()}, so instances must be used with try-with-resources to 
avoid leaking file/native handles.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='jc'>{@link SecretStore}
+ *     <li class='jc'>{@link SecretStores}
+ * </ul>
  */
 public class GitControl implements AutoCloseable {
 
@@ -79,7 +93,7 @@ public class GitControl implements AutoCloseable {
         * @throws IOException If the repository cannot be opened.
         */
        @SuppressWarnings({
-               "resource" // localRepo (FileRepository) and git (Git) are 
long-lived fields closed in close(); Git wraps the externally-created 
Repository without closing it, so both are released there. Warnings surface at 
the constructor assignments (lines 84/87).
+               "resource" // localRepo (FileRepository) and git (Git) are 
long-lived fields closed in close(); Git wraps the externally-created 
Repository without closing it, so both are released there.
        })
        public GitControl(String localPath, String remotePath, String username, 
String password, boolean forcePush) throws IOException {
                this.localPath = localPath;
@@ -90,17 +104,74 @@ public class GitControl implements AutoCloseable {
                git = new Git(localRepo);
        }
 
+       /**
+        * Constructor that resolves the Git password/token from a pluggable 
{@link SecretStore}.
+        *
+        * <p>
+        * The active store is resolved from the supplied {@link BeanStore} via 
{@link SecretStores#resolve(BeanStore)},
+        * defaulting to an {@link InMemorySecretStore} when the bean store is 
<jk>null</jk> or contributes none.  The
+        * password/token is looked up under <jv>secretKey</jv> as a {@code 
char[]} so it is never materialized as a
+        * {@link String}, and the credentials are zeroed by {@link #close()}.  
Pass <jk>null</jk> for <jv>username</jv> to
+        * use anonymous transport (the secret store is not consulted in that 
case).
+        *
+        * @param localPath Local directory path for the repository.
+        * @param remotePath Remote Git repository URI.
+        * @param username Git username, or <jk>null</jk> for anonymous 
transport.
+        * @param secretKey The key under which the password/token is stored in 
the resolved {@link SecretStore}.
+        *      <br>Ignored when <jv>username</jv> is <jk>null</jk>.
+        * @param beanStore The bean store to resolve the {@link SecretStore} 
from.  Can be <jk>null</jk> to use the
+        *      default {@link InMemorySecretStore}.
+        * @param forcePush Whether {@link #pushToRepo()} performs a force-push 
(destructive remote history rewrite).
+        *      <br>Force-push is opt-in; defaults to <jk>false</jk> in the 
other constructors.
+        * @throws IOException If the repository cannot be opened.
+        */
+       @SuppressWarnings({
+               "resource" // localRepo (FileRepository) and git (Git) are 
long-lived fields closed in close(); Git wraps the externally-created 
Repository without closing it, so both are released there.
+       })
+       public GitControl(String localPath, String remotePath, String username, 
String secretKey, BeanStore beanStore, boolean forcePush) throws IOException {
+               this.localPath = localPath;
+               this.remotePath = remotePath;
+               this.localRepo = new FileRepository(localPath + "/.git");
+               this.cp = findCredentialsProvider(username, secretKey, 
beanStore);
+               this.forcePush = forcePush;
+               git = new Git(localRepo);
+       }
+
+       /**
+        * Builds the credentials provider by resolving the password/token from 
the {@link SecretStore} contributed to the
+        * supplied bean store.
+        *
+        * <p>
+        * Returns <jk>null</jk> (anonymous transport) when <jv>username</jv> 
is <jk>null</jk>.  When the secret is absent
+        * from the resolved store an empty password is used, so a caller 
cannot accidentally send a stale literal.  The
+        * retrieved {@code char[]} is handed to the provider and zeroed by 
{@link #close()}.
+        *
+        * @param username Git username, or <jk>null</jk> for anonymous 
transport.
+        * @param secretKey The key under which the password/token is stored.
+        * @param beanStore The bean store to resolve the {@link SecretStore} 
from.  Can be <jk>null</jk>.
+        * @return The credentials provider, or <jk>null</jk> for anonymous 
transport.
+        */
+       static CredentialsProvider findCredentialsProvider(String username, 
String secretKey, BeanStore beanStore) {
+               if (username == null)
+                       return null;
+               var secret = 
SecretStores.resolve(beanStore).find(secretKey).orElseGet(() -> new char[0]);
+               return new UsernamePasswordCredentialsProvider(username, 
secret);
+       }
+
        /**
         * Closes the underlying JGit {@link Git} and {@link Repository} 
handles.
         *
         * <p>
         * The {@link Git} instance wraps an externally-supplied {@link 
Repository} (it does not close it on
-        * {@link Git#close()}), so both are released explicitly here.
+        * {@link Git#close()}), so both are released explicitly here.  Any 
credentials held by the provider are zeroed so
+        * a resolved secret does not linger in memory beyond the life of this 
instance.
         */
        @Override
        public void close() {
                git.close();
                localRepo.close();
+               if (cp instanceof UsernamePasswordCredentialsProvider cp2)
+                       cp2.clear();
        }
 
        /**
diff --git 
a/juneau-sc/juneau-sc-server/src/test/java/org/apache/juneau/server/config/repository/GitControl_Test.java
 
b/juneau-sc/juneau-sc-server/src/test/java/org/apache/juneau/server/config/repository/GitControl_Test.java
new file mode 100644
index 0000000000..a7bb48426e
--- /dev/null
+++ 
b/juneau-sc/juneau-sc-server/src/test/java/org/apache/juneau/server/config/repository/GitControl_Test.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.server.config.repository;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.commons.secret.*;
+import org.eclipse.jgit.transport.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.io.*;
+
+/**
+ * Coverage for {@link GitControl}'s {@link SecretStore}-backed credential 
resolution.
+ *
+ * <p>
+ * The {@code findCredentialsProvider} tests exercise the resolution logic 
directly without opening a repository; the
+ * construction test confirms the {@link SecretStore} constructor wires the 
resolved credentials through without
+ * regressing the anonymous default.
+ */
+class GitControl_Test {
+
+       private static char[] passwordOf(CredentialsProvider cp) throws 
Exception {
+               var pass = new CredentialItem.Password();
+               assertTrue(cp.get(new URIish("https://example.com/repo.git";), 
pass));
+               return pass.getValue();
+       }
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // findCredentialsProvider - the SecretStore resolution logic.
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       @Test void a01_nullUsernameYieldsAnonymousTransport() {
+               assertNull(GitControl.findCredentialsProvider(null, 
"GIT_TOKEN", new BasicBeanStore()));
+       }
+
+       @Test void a02_contributedStoreResolvesSecret() throws Exception {
+               var store = new InMemorySecretStore();
+               store.store("GIT_TOKEN", "hunter2".toCharArray());
+               var beanStore = new BasicBeanStore().addBean(SecretStore.class, 
store);
+
+               var cp = GitControl.findCredentialsProvider("svc-account", 
"GIT_TOKEN", beanStore);
+               assertInstanceOf(UsernamePasswordCredentialsProvider.class, cp);
+               assertArrayEquals("hunter2".toCharArray(), passwordOf(cp));
+       }
+
+       @Test void a03_absentSecretYieldsEmptyPassword() throws Exception {
+               // No store contributed -> InMemorySecretStore default -> key 
absent -> empty password (never a stale literal).
+               var cp = GitControl.findCredentialsProvider("svc-account", 
"GIT_TOKEN", null);
+               assertInstanceOf(UsernamePasswordCredentialsProvider.class, cp);
+               assertArrayEquals(new char[0], passwordOf(cp));
+       }
+
+       @Test void a04_emptyBeanStoreFallsBackToInMemoryDefault() throws 
Exception {
+               var cp = GitControl.findCredentialsProvider("svc-account", 
"GIT_TOKEN", new BasicBeanStore());
+               assertArrayEquals(new char[0], passwordOf(cp));
+       }
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // Constructor integration.
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       @Test void 
b01_secretStoreConstructorWiresCredentialsAndClosesCleanly(@TempDir File dir) 
throws IOException {
+               var store = new InMemorySecretStore();
+               store.store("GIT_TOKEN", "hunter2".toCharArray());
+               var beanStore = new BasicBeanStore().addBean(SecretStore.class, 
store);
+
+               try (var gitControl = new GitControl(dir.getAbsolutePath(), 
"https://example.com/repo.git";, "svc-account", "GIT_TOKEN", beanStore, false)) {
+                       assertNotNull(gitControl);
+               }
+       }
+
+       @Test void b02_nullUsernameConstructorUsesAnonymousTransport(@TempDir 
File dir) throws IOException {
+               try (var gitControl = new GitControl(dir.getAbsolutePath(), 
"https://example.com/repo.git";, null, "GIT_TOKEN", null, false)) {
+                       assertNotNull(gitControl);
+               }
+       }
+}

Reply via email to