fix(webvh): parse the daemon's flat auth response shapes (challenge + tokens) - #606
Merged
Conversation
… tokens)
The VTA's webvh_client modelled the did-hosting daemon's auth responses as
data-wrapped ({session_id, data:{...}}), but the daemon (did-hosting-common
v0.7.0) emits them FLAT — matching the auth-challenge shape documented in the
workspace CLAUDE.md. So publishing a did:webvh to a registered webvh server
couldn't parse the daemon's replies:
- POST /api/auth/challenge → "missing field `data`" (the challenge)
- POST /api/auth/ + /refresh → same class, on the {session, tokens} body
Fix both wire parsers to the flat shapes:
- ChallengeResponseWire → { challenge, session_id } (was { session_id, data:{challenge} });
the now-unused ChallengeData is removed.
- TokenResponseWire → { session (ignored), tokens: TokenBundleWire } (was { session_id, data }).
TokenBundleWire carries the daemon's RELATIVE expiresIn / refreshExpiresIn (RFC 6749 §5.1),
converted to the VTA's ABSOLUTE access_expires_at / refresh_expires_at at the wire boundary
(into_token_data, saturating add). A bundle missing refreshToken/refreshExpiresIn is
rejected — the VTA session lifecycle requires rotation.
The internal TokenData secret type keeps its ZeroizeOnDrop + redacting Debug; only the wire
structs + mapping change. The authenticate/refresh REQUEST builders already match the
did-hosting-server handler (DIDComm JWS, {session_id, challenge} / {refresh_token}) — unchanged.
Regression tests deserialize the daemon's real flat bodies (challenge + auth) and assert the
relative→absolute expiry conversion; StubWebvhHost + inline fixtures updated to the flat shape.
Signed-off-by: Glenn Gore <glenn@affinidi.com>
stormer78
added a commit
that referenced
this pull request
Jul 2, 2026
The VTA's RequestUriResponse mirror (POST /api/dids reserve + /api/dids/register)
lacked #[serde(rename_all = "camelCase")], but the daemon
(did-hosting-common::RequestUriResponse) serializes camelCase — so `did_url`
arrives as `didUrl` and the body failed to decode ("webvh-server response parse
error"), breaking the publish path right after auth succeeded.
Add the camelCase rename to match the daemon (the same alignment #606 applied to
the auth response wire types). Regression test deserializes the daemon's real
`{mnemonic, didUrl}` body.
Signed-off-by: Glenn Gore <glenn@affinidi.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Publishing a
did:webvhto a registered did-hosting-daemon webvh server fails in the auth handshake. The VTA'swebvh_clientmodelled the daemon's auth responses as data-wrapped ({session_id, data:{...}}), but the daemon (did-hosting-commonv0.7.0) emits them flat — matching the auth-challenge shape documented in the workspace CLAUDE.md ("DIDComm challenge-response auth"). So the VTA couldn't parse the daemon's replies:POST /api/auth/challenge→challenge response parse error: missing field \data``POST /api/auth/+/api/auth/refresh→ same class, on the{session, tokens}bodyFound during a live cross-service run (cierge online-provision → daemon publish) after #600 + #604 landed: with those merged, the VTA now authenticates to the daemon (the
401/403are resolved), and this response-shape mismatch is the next — and final — blocker.Root cause (confirmed against the daemon repo)
/api/auth/challenge→ChallengeResponse { challenge, session_id, expires_at }(flat, camelCase) —did-hosting-common/src/types.rs:29-37; doc comment: "Flat shape — the framework dropped thedata: {}envelope."/api/auth/+/api/auth/refresh→AuthenticateResponse { session: Session, tokens: TokenBundle }(flat) —types.rs:143-149(+RefreshResponsealias at 176).TokenBundlecarries relativeexpiresIn/refreshExpiresIn(RFC 6749 §5.1),types.rs:109-121. Handlers return these directly viaJson(...), no envelope.So the daemon is spec-correct; the VTA's
ChallengeResponseWire/TokenResponseWirewere the outliers.Fix (
vta-service/src/webvh_client.rs)ChallengeResponseWire→ flat{ challenge, session_id }(was{ session_id, data:{challenge} }); unusedChallengeDataremoved.TokenResponseWire→{ session (ignored viaIgnoredAny), tokens: TokenBundleWire }.TokenBundleWireholds the daemon's relative expiries, converted to the VTA's absoluteaccess_expires_at/refresh_expires_atat the wire boundary (into_token_data, saturating add). A bundle missingrefreshToken/refreshExpiresInis rejected — the VTA session lifecycle requires rotation.TokenDatasecret type keeps itsZeroizeOnDrop+ redactingDebug; only the wire structs + mapping change.did-hosting-serverhandler (DIDComm JWS,{session_id, challenge}/{refresh_token}) — unchanged.Tests
New
webvh_clientunit tests deserialize the daemon's real flat bodies and assert the relative→absolute expiry conversion:challenge_response_deserializes_flat_daemon_body+..._accepts_snake_case_session_id_aliasauth_response_deserializes_flat_daemon_body_and_maps_relative_expiries+auth_response_without_refresh_token_is_rejectedauthenticate_round_trips_against_mock_daemon/refresh_returns_rotated_tokensnow bracket the absolute expiry in anow-window.StubWebvhHost+ inline fixtures updated to the flat shape.cargo build/test/clippy -p vta-service --features webvh,didcommgreen;cargo fmtclean.Follow-up (out of scope, flagged)
At v0.7.0 the daemon has two auth request contracts:
did-hosting-serveraccepts the DIDComm-JWS request the VTA sends (what the live round-trip exercises), whiledid-hosting-controlexpects a SIOPv2 Trust-Task request. Both return the same flat response, so this fix holds either way. If a future unified daemon routes/api/auth/to the control-plane SIOPv2 handler, the VTA's authenticate request shape would need reconciling separately.