Skip to content

fix(webvh): parse the daemon's flat auth response shapes (challenge + tokens) - #606

Merged
stormer78 merged 1 commit into
mainfrom
fix/webvh-challenge-parse
Jul 2, 2026
Merged

fix(webvh): parse the daemon's flat auth response shapes (challenge + tokens)#606
stormer78 merged 1 commit into
mainfrom
fix/webvh-challenge-parse

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Problem

Publishing a did:webvh to a registered did-hosting-daemon webvh server fails in the auth handshake. The VTA's webvh_client modelled the 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 ("DIDComm challenge-response auth"). So the VTA couldn't parse the daemon's replies:

  • POST /api/auth/challengechallenge response parse error: missing field \data``
  • POST /api/auth/ + /api/auth/refresh → same class, on the {session, tokens} body

Found 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/403 are resolved), and this response-shape mismatch is the next — and final — blocker.

Root cause (confirmed against the daemon repo)

  • /api/auth/challengeChallengeResponse { challenge, session_id, expires_at } (flat, camelCase) — did-hosting-common/src/types.rs:29-37; doc comment: "Flat shape — the framework dropped the data: {} envelope."
  • /api/auth/ + /api/auth/refreshAuthenticateResponse { session: Session, tokens: TokenBundle } (flat) — types.rs:143-149 (+ RefreshResponse alias at 176). TokenBundle carries relative expiresIn / refreshExpiresIn (RFC 6749 §5.1), types.rs:109-121. Handlers return these directly via Json(...), no envelope.

So the daemon is spec-correct; the VTA's ChallengeResponseWire / TokenResponseWire were the outliers.

Fix (vta-service/src/webvh_client.rs)

  • ChallengeResponseWire → flat { challenge, session_id } (was { session_id, data:{challenge} }); unused ChallengeData removed.
  • TokenResponseWire{ session (ignored via IgnoredAny), tokens: TokenBundleWire }. TokenBundleWire holds the daemon's relative expiries, 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.

Tests

New webvh_client unit 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_alias
  • auth_response_deserializes_flat_daemon_body_and_maps_relative_expiries + auth_response_without_refresh_token_is_rejected
  • authenticate_round_trips_against_mock_daemon / refresh_returns_rotated_tokens now bracket the absolute expiry in a now-window.
  • StubWebvhHost + inline fixtures updated to the flat shape.

cargo build/test/clippy -p vta-service --features webvh,didcomm green; cargo fmt clean.

Follow-up (out of scope, flagged)

At v0.7.0 the daemon has two auth request contracts: did-hosting-server accepts the DIDComm-JWS request the VTA sends (what the live round-trip exercises), while did-hosting-control expects 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.

… 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
stormer78 requested a review from a team as a code owner July 2, 2026 10:26
@stormer78
stormer78 merged commit e285ba2 into main Jul 2, 2026
9 of 10 checks passed
@stormer78
stormer78 deleted the fix/webvh-challenge-parse branch July 2, 2026 10:30
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant